Skip to content

feat: emit audit action hook on capability-denied tool calls (#412 Stage D)#419

Open
chubes4 wants to merge 1 commit into
Automattic:mainfrom
chubes4:ceiling-denial-telemetry
Open

feat: emit audit action hook on capability-denied tool calls (#412 Stage D)#419
chubes4 wants to merge 1 commit into
Automattic:mainfrom
chubes4:ceiling-denial-telemetry

Conversation

@chubes4

@chubes4 chubes4 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Closes #412 (Stage d — audit/telemetry).

Summary

This is the last remaining part of #412. Stages (a)/(b)/(c) merged in #413 and #414, which added the capability ceiling and the capability_denied tool-error result. This PR adds the audit/telemetry surface: a pure notification action hook that fires at the moment a tool call is denied on capability grounds, so hosts can log, telemeter, or alert on ceiling-denied tool calls — without the substrate owning any storage, logger, or telemetry backend.

What changed

In WP_Agent_Ability_Tool_Executor::authorize_required_capability(), right before the existing denial error is returned (after $allowed is known false), emit:

do_action( 'agents_api_tool_capability_denied', $denial_event, $context );

The method then returns the same denial error as before. The hook does not alter control flow or the returned result.

Hook name

agents_api_tool_capability_denied — follows the substrate's existing agents_api_* action convention (cf. agents_api_runtime_tool_request_created, agents_api_ability_executed, agents_api_loop_event), matching the repo's past-tense / noun-event naming.

Payload contract

The $denial_event (first arg) is JSON-friendly and redaction-safe:

Key Value
schema_version 1
operation Always tool_execution
tool_name Model-facing tool name
ability_name Resolved ability name
required_capability Required WordPress capability
reason capability_not_permitted (principal present but ceiling/user denied) or principal_unavailable (no principal threaded — fail closed)
principal Safe metadata from WP_Agent_Execution_Principal::to_safe_metadata(), or null when no principal was threaded
parameters Redacted ability parameters from WP_Agent_Ability_Dispatcher::redacted_parameters()
parameters_redacted Always true

The second arg is the caller-owned $context array, passed through unchanged (same convention as the runtime-tool lifecycle actions).

Redaction guarantees

  • Principal: reuses to_safe_metadata(), which intentionally omits token ids, owner keys, request metadata, audience claims, capability details, and cryptographic binding claims. Hosts needing richer audit trails should persist the full to_array() shape in private storage they control.
  • Parameters: reuses redacted_parameters(), which redacts via JSON-schema annotations, ability metadata, and a conservative key-name fallback (api_key, authorization, token, secret, password, cookie, credential, nonce, …). Raw parameter values never appear in the event.
  • The event never carries raw parameters, secrets, or unredacted principal fields.

Layer purity

The substrate emits the event only. It does not add a table, a logger, a telemetry backend, or any storage. No consumer-specific concepts are referenced. This is a generic notification surface; hosts own what happens next.

Success-side symmetry — deliberately not added

A symmetric agents_api_tool_capability_allowed event was considered but intentionally omitted. Rationale:

If a future need for per-capability-check success telemetry emerges, it can be added then without a parallel system. Shipping only the denial event keeps this change minimal and focused.

Refactor note

redacted_parameters() is now computed once into a local and reused in both the event payload and the returned denial error (previously it was called inline only for the error). This is behavior-preserving — the returned error is identical — and avoids double redaction work.

Verification

  • New smoke test tests/ability-tool-capability-denied-event-smoke.php (42 assertions), wired into composer smoke:
    • Denied call fires the event exactly once with the expected redaction-safe payload (correct tool_name/ability_name/required_capability/reason, principal safe-metadata present, sensitive api_key parameter redacted to [redacted], non-sensitive draft preserved, raw secret never present in the JSON-encoded payload, context passed through).
    • The returned denial error is unchanged (hook did not alter control flow).
    • An allowed call does not fire the denial event.
    • The principal_unavailable path fires the event with principal set to null.
    • The hook is safe with no listener attached (plain do_action; result unchanged).
    • No-listener safety is also covered by the existing ability-tool-ceiling-smoke.php, which runs the denial path without attaching a listener.
  • composer smoke — green (exit 0).
  • composer phpstan (level: max, memory-limit 2G) — [OK] No errors.
  • Docs: added a "Capability-denied audit event" section to docs/runtime-and-tools.md documenting the hook name, payload contract, and redaction guarantees, matching the existing hook-documentation style.

Definition of done

A layer-pure, redaction-safe audit action hook fires on every capability-denied tool call; no storage added to the substrate; returned result unchanged; smoke test + docs cover it; phpstan clean.

Do not merge/release/deploy — PR only.

…tic#412 Stage D)

Fire the `agents_api_tool_capability_denied` action at the moment an
ability-backed tool call is denied on capability grounds, so hosts can
log, telemeter, or alert on ceiling-denied tool calls. The substrate
emits the event only and owns no storage, logger, or telemetry backend.

The payload is JSON-friendly and redaction-safe: it reuses the safe
principal metadata from `to_safe_metadata()` and the already-redacted
ability parameters from `redacted_parameters()`, and never carries raw
parameters, secrets, or unredacted principal fields. The hook is a pure
notification that does not alter control flow or the returned tool
result, and is safe when no listener is attached.

Closes Automattic#412 (Stage d — audit/telemetry).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enforce capability ceiling at ability/tool execution — the Core-worthy boundary for autonomous writes (beneath #408/#411)

1 participant