Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"php tests/default-agents-chat-handler-smoke.php",
"php tests/ability-tool-executor-smoke.php",
"php tests/ability-tool-ceiling-smoke.php",
"php tests/ability-tool-capability-denied-event-smoke.php",
"php tests/tool-mediation-runner-smoke.php",
"php tests/conversation-loop-tool-execution-smoke.php",
"php tests/tool-call-gate-smoke.php",
Expand Down
26 changes: 26 additions & 0 deletions docs/runtime-and-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,32 @@ Canonical action-policy values are `direct`, `preview`, and `forbidden`. Resolut

On WordPress < 7.1 the underlying filters are never applied, so registered handlers stay idle.

## Capability-denied audit event

When an ability-backed tool declares a `required_capability` and the execution principal's capability ceiling does not permit it, `WP_Agent_Ability_Tool_Executor` denies the call before the ability runs. The `agents_api_tool_capability_denied` action fires at the moment of denial so hosts can log, telemeter, or alert on ceiling-denied tool calls. The substrate emits the event only; it owns no storage, logger, or telemetry backend.

```php
add_action( 'agents_api_tool_capability_denied', function ( array $denial, array $context ): void {
// Persist to a private audit log, push to telemetry, or trigger an alert.
}, 10, 2 );
```

The `$denial` payload is JSON-friendly and redaction-safe:

| Key | Value |
| --- | --- |
| `schema_version` | Event 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` when a principal was present but the ceiling/user denied the capability, or `principal_unavailable` when no principal was threaded (fail closed). |
| `principal` | Safe principal metadata from `WP_Agent_Execution_Principal::to_safe_metadata()`, or `null` when no principal was threaded. Omits token ids, owner keys, request metadata, audience claims, capability details, and binding claims. |
| `parameters` | Redacted ability parameters from `WP_Agent_Ability_Dispatcher::redacted_parameters()`. |
| `parameters_redacted` | Always `true`. |

The hook is a pure notification: it fires after the denial is decided and before the denial error is returned, and it does not alter control flow or the returned tool result. It never carries raw parameters, secrets, or unredacted principal fields. Hosts that need richer audit trails should persist the full `to_array()` principal shape in private storage they control.

## Compaction and conservation

Conversation compaction is opt-in. Agents can declare `supports_conversation_compaction` and a `conversation_compaction_policy`; callers provide the summarizer. `WP_Agent_Conversation_Compaction::compact()` returns transformed messages, compaction metadata, and lifecycle events. If summarization fails, the original transcript is preserved and a failure event is emitted.
Expand Down
43 changes: 42 additions & 1 deletion src/Tools/class-wp-agent-ability-tool-executor.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ public function executeWP_Agent_Tool_Call( array $tool_call, array $tool_definit
* and any token/client restrictions; this method depends only on the ceiling
* being present on the principal, not on how it was derived.
*
* When a denial is decided, the `agents_api_tool_capability_denied` action
* fires with a redaction-safe payload before the denial error is returned.
* The hook is a pure notification and does not alter control flow.
*
* @param string $tool_name Model-facing tool name.
* @param string $ability_name Resolved ability name.
* @param string $required_capability Required WordPress capability.
Expand Down Expand Up @@ -133,14 +137,51 @@ private function authorize_required_capability( string $tool_name, string $abili
return null;
}

$redacted_parameters = WP_Agent_Ability_Dispatcher::redacted_parameters( $ability_name, $parameters );

/**
* Fires when an ability-backed tool call is denied on capability grounds.
*
* Hosts subscribe to log, telemeter, or alert on ceiling-denied tool calls.
* The substrate emits this notification only; it owns no storage, logger,
* or telemetry backend. The hook is a pure observer: it fires after the
* denial is decided and before the denial error is returned, and it must
* not alter control flow or the returned tool result.
*
* The payload is JSON-friendly and redaction-safe. It reuses the safe
* principal metadata from WP_Agent_Execution_Principal::to_safe_metadata()
* (which omits token ids, owner keys, request metadata, audience claims,
* capability details, and binding claims) and the already-redacted ability
* parameters from WP_Agent_Ability_Dispatcher::redacted_parameters(). It
* never carries raw parameters, secrets, or unredacted principal fields.
*
* @param array<string, mixed> $denial Redaction-safe denial event (see keys below).
* @param array<mixed> $context Host runtime context for this invocation.
*/
do_action(
'agents_api_tool_capability_denied',
array(
'schema_version' => 1,
'operation' => 'tool_execution',
'tool_name' => $tool_name,
'ability_name' => $ability_name,
'required_capability' => $required_capability,
'reason' => $reason,
'principal' => $safe_metadata,
'parameters' => $redacted_parameters,
'parameters_redacted' => true,
),
$context
);

return WP_Agent_Tool_Result::error(
$tool_name,
sprintf( 'Tool "%1$s" requires the "%2$s" capability, which is not permitted for this execution.', $tool_name, $required_capability ),
array(
'error_type' => 'capability_denied',
'required_capability' => $required_capability,
'ability_name' => $ability_name,
'parameters' => WP_Agent_Ability_Dispatcher::redacted_parameters( $ability_name, $parameters ),
'parameters' => $redacted_parameters,
'parameters_redacted' => true,
'denial' => array(
'allowed' => false,
Expand Down
Loading