diff --git a/docs/auth-consent-context-memory.md b/docs/auth-consent-context-memory.md index 2f633d4..438e948 100644 --- a/docs/auth-consent-context-memory.md +++ b/docs/auth-consent-context-memory.md @@ -60,7 +60,40 @@ Missing headers produce a top-of-chain context. Malformed headers throw `Invalid ## Execution principals and workspace scope -`AgentsAPI\AI\WP_Agent_Execution_Principal` represents one runtime actor: acting user id, effective agent id/slug, auth source, request context, optional token id, workspace id, client id, capability ceiling, caller context, and JSON-friendly metadata. +`AgentsAPI\AI\WP_Agent_Execution_Principal` represents one runtime actor: acting user id, effective agent id/slug, auth source, request context, optional token id, workspace id, client id, capability ceiling, caller context, non-user audience id/claims, optional transcript owner, and JSON-friendly metadata. + +Generic principal fields: + +| Field | Purpose | +| --- | --- | +| `acting_user_id` | WordPress user ID on whose behalf execution happens, or `0` for non-user/runtime/system principals. Hosts still enforce capabilities with WordPress-native auth, caps, and REST permission callbacks. | +| `effective_agent_id` | Registered agent id/slug effective for this run. This is routing identity, not a permission grant by itself. | +| `auth_source` | Product-neutral source such as `user`, `application_password`, `agent_token`, `audience`, `runtime`, or `system`; hosts may register additional sources with `wp_agent_known_auth_sources`. | +| `request_context` | Entry context such as `rest`, `cli`, `cron`, `chat`, or `runtime`. | +| `workspace_id` | Host-owned scope id for persistence, tool policy, audit, or result filtering. | +| `client_id` | Host/client surface identifier, such as a frontend, bridge, runtime, or API client. | +| `audience_id` / `audience_claims` | Host-resolved non-user audience context. Claims are private runtime/audit data, not a display contract. | +| `owner_type` / `owner_key` | Optional transcript/session owner. Non-user principals must use opaque host-owned owner keys; audience access alone is not a transcript owner. | +| `capability_ceiling` | Optional ceiling intersected by authorization policy with WordPress capabilities. | +| `caller_context` | Cross-agent/cross-host chain context for delegation and loop prevention. | +| `request_metadata` / `binding` | Private host audit/runtime data. These fields are not safe citation or frontend metadata. | + +Permission-aware tools and retrieval surfaces should attach only safe principal metadata to user-visible citations, diagnostics, or frontend result objects. `to_safe_metadata()` returns the generic safe shape: schema version, effective agent, auth source, request context, acting user id, workspace id, client id, audience id, owner type, and boolean flags for conversation-owner/capability/caller-context presence. It intentionally omits token ids, owner keys, request metadata, audience claims, capability details, and cryptographic binding claims. + +Hosts that need richer audit trails should persist the full `to_array()` shape in private storage they control. Frontend clients, citations, and source diagnostics should use the safe metadata shape plus result-level status fields instead of receiving raw credentials, tokens, opaque session ids, or authorization internals. + +Client context is caller-owned runtime context carried on conversation requests and frontend channel payloads. It may describe selected UI state, host context, explicit routing hints, or opaque client metadata, but Agents API does not infer tool arguments from it. Tool declarations must opt in with `client_context_bindings`, either as `array( 'parameter_name' )` or `array( 'parameter_name' => 'context_key' )`. Sensitive ambient keys such as `api_key`, `token`, `authorization`, `cookie`, `nonce`, or `password` must be passed only through explicit bindings/defaults and host-owned authorization policy; matching key names alone never satisfies required parameters. + +Permission-aware retrieval results should use product-neutral status vocabulary so frontend clients can explain restricted output without learning product-specific policy: + +| Status | Meaning | +| --- | --- | +| `allowed` | The source/result was available to the current principal. | +| `denied` | The source/result exists but the current principal cannot access it. | +| `partial` | Some matching items were withheld or redacted. | +| `source_restricted` | The source itself is unavailable in the current workspace/client/principal context. | + +Result and citation metadata may include `access_status`, `restriction_reason`, `source`, `source_id`, `item_id`, `fragment_id`, `source_title`, `source_url`, `score`, `excerpt`, and `principal` using `to_safe_metadata()`. Concrete auth checks remain downstream: WordPress users, roles, capabilities, REST permission callbacks, and host policy decide access; Agents API only defines the transportable, product-neutral context shape. `AgentsAPI\Core\Workspace\WP_Agent_Workspace_Scope` is the generic workspace identity shared by memory, transcripts, persistence, and audit adapters. It is deliberately `(workspace_type, workspace_id)` rather than a WordPress site id so consumers can map sites, networks, code workspaces, pull requests, or ephemeral environments without changing generic contracts. diff --git a/docs/runtime-and-tools.md b/docs/runtime-and-tools.md index 6086ca7..cc247bd 100644 --- a/docs/runtime-and-tools.md +++ b/docs/runtime-and-tools.md @@ -430,6 +430,18 @@ If a concrete executor returns its own `runtime` metadata, the normalized result Retrieval tools place canonical citations in result metadata under `metadata['citations']`. The substrate citation shape is intentionally small and generic: each citation may include `source`, `source_id`, `item_id`, `fragment_id`, `source_title`, `source_url`, `score`, and `excerpt`. Agents API normalizes this citation list for mediated tool results and delegated runtime tool results while preserving unrelated caller-owned metadata and additional caller-owned fields inside each citation. +Permission-aware citations and source diagnostics may add product-neutral access metadata: + +| Key | Meaning | +| --- | --- | +| `access_status` | `allowed`, `denied`, `partial`, or `source_restricted`. | +| `restriction_reason` | Optional generic reason such as `capability`, `workspace`, `audience`, `source_policy`, or `redacted`. | +| `principal` | Optional safe principal metadata from `WP_Agent_Execution_Principal::to_safe_metadata()`. | + +Do not attach raw request metadata, token ids, owner keys, audience claims, capability lists, cookies, nonces, credentials, or cryptographic binding claims to citation metadata. If a host needs those details for audits, it should write a private audit record and expose only safe principal/source metadata to model-visible or frontend-visible result surfaces. + +Frontend clients should treat citation access metadata as explanation and display context, not as authorization. Downstream WordPress auth, capabilities, REST permission callbacks, source adapters, and host policy remain the enforcement points. + ## Tool execution core `WP_Agent_Tool_Execution_Core` mediates calls without owning any concrete tool implementation. diff --git a/src/Runtime/class-wp-agent-execution-principal.php b/src/Runtime/class-wp-agent-execution-principal.php index b363e80..8528585 100644 --- a/src/Runtime/class-wp-agent-execution-principal.php +++ b/src/Runtime/class-wp-agent-execution-principal.php @@ -327,6 +327,36 @@ public function to_array(): array { ); } + /** + * Export the safe principal metadata shape for citations and diagnostics. + * + * This intentionally omits request metadata, token ids, audience claims, + * capability details, owner keys, and binding claims. Those fields may carry + * credentials, opaque session ids, or host-specific authorization material. + * Hosts that need richer audit data should persist it in a private audit log + * rather than attaching it to user-visible citations or tool diagnostics. + * + * @return array + */ + public function to_safe_metadata(): array { + $owner = $this->conversation_owner(); + + return array( + 'schema_version' => 1, + 'effective_agent_id' => $this->effective_agent_id, + 'auth_source' => $this->auth_source, + 'request_context' => $this->request_context, + 'acting_user_id' => $this->acting_user_id, + 'workspace_id' => $this->workspace_id, + 'client_id' => $this->client_id, + 'audience_id' => $this->audience_id, + 'owner_type' => is_array( $owner ) ? $owner['type'] : null, + 'has_conversation_owner' => is_array( $owner ), + 'has_capability_ceiling' => $this->capability_ceiling instanceof \WP_Agent_Capability_Ceiling, + 'has_caller_context' => $this->caller_context instanceof \WP_Agent_Caller_Context, + ); + } + /** * Return host-owned cryptographic binding claims for this principal. * diff --git a/tests/execution-principal-smoke.php b/tests/execution-principal-smoke.php index 2af4dfb..63f5d3d 100644 --- a/tests/execution-principal-smoke.php +++ b/tests/execution-principal-smoke.php @@ -54,6 +54,21 @@ agents_api_smoke_assert_equals( array( 'edit_posts' ), $principal_array['capability_ceiling']['allowed_capabilities'], 'principal exports capability ceiling', $failures, $passes ); agents_api_smoke_assert_equals( array( 'type' => AgentsAPI\AI\WP_Agent_Execution_Principal::OWNER_TYPE_TOKEN, 'key' => '456' ), $principal->conversation_owner(), 'agent token derives token conversation owner', $failures, $passes ); +$safe_metadata = $principal->to_safe_metadata(); +agents_api_smoke_assert_equals( 1, $safe_metadata['schema_version'], 'safe metadata declares schema version', $failures, $passes ); +agents_api_smoke_assert_equals( 'content-helper', $safe_metadata['effective_agent_id'], 'safe metadata includes effective agent id', $failures, $passes ); +agents_api_smoke_assert_equals( AgentsAPI\AI\WP_Agent_Execution_Principal::AUTH_SOURCE_AGENT_TOKEN, $safe_metadata['auth_source'], 'safe metadata includes auth source', $failures, $passes ); +agents_api_smoke_assert_equals( 'site:42', $safe_metadata['workspace_id'], 'safe metadata includes workspace id', $failures, $passes ); +agents_api_smoke_assert_equals( 'kimaki', $safe_metadata['client_id'], 'safe metadata includes client id', $failures, $passes ); +agents_api_smoke_assert_equals( AgentsAPI\AI\WP_Agent_Execution_Principal::OWNER_TYPE_TOKEN, $safe_metadata['owner_type'], 'safe metadata includes owner type without owner key', $failures, $passes ); +agents_api_smoke_assert_equals( true, $safe_metadata['has_conversation_owner'], 'safe metadata reports conversation owner presence', $failures, $passes ); +agents_api_smoke_assert_equals( true, $safe_metadata['has_capability_ceiling'], 'safe metadata reports capability ceiling presence', $failures, $passes ); +agents_api_smoke_assert_equals( false, array_key_exists( 'token_id', $safe_metadata ), 'safe metadata omits token id', $failures, $passes ); +agents_api_smoke_assert_equals( false, array_key_exists( 'request_metadata', $safe_metadata ), 'safe metadata omits request metadata', $failures, $passes ); +agents_api_smoke_assert_equals( false, array_key_exists( 'owner_key', $safe_metadata ), 'safe metadata omits owner key', $failures, $passes ); +agents_api_smoke_assert_equals( false, array_key_exists( 'capability_ceiling', $safe_metadata ), 'safe metadata omits capability details', $failures, $passes ); +agents_api_smoke_assert_equals( false, array_key_exists( 'binding', $safe_metadata ), 'safe metadata omits binding claims', $failures, $passes ); + $from_array = AgentsAPI\AI\WP_Agent_Execution_Principal::from_array( array( 'acting_user_id' => '7',