diff --git a/fixtures/hook-event/valid/subagent-start-with-delegation.json b/fixtures/hook-event/valid/subagent-start-with-delegation.json new file mode 100644 index 0000000..75274bb --- /dev/null +++ b/fixtures/hook-event/valid/subagent-start-with-delegation.json @@ -0,0 +1,18 @@ +{ + "spec": "agent-hooks/0.1", + "event_id": "4ceaa9fe-0d13-45ab-b3a0-303f2e7757a9", + "hook_event_name": "SubagentStart", + "session_id": "session-42", + "timestamp": "2026-09-09T10:15:25Z", + "sequence": 9, + "cwd": "/workspace/project", + "prompt_id": "prompt-17", + "delegation_id": "delegation-17", + "agent_id": "subagent-17", + "agent_type": "Explore", + "parent_agent_id": "main", + "delegation_depth": 1, + "delegation_chain": [ + "main" + ] +} diff --git a/fixtures/hook-response/invalid/unknown-decision-top-level.json b/fixtures/hook-response/invalid/unknown-decision-top-level.json new file mode 100644 index 0000000..102157a --- /dev/null +++ b/fixtures/hook-response/invalid/unknown-decision-top-level.json @@ -0,0 +1,5 @@ +{ + "spec": "agent-hooks/0.1", + "event_id": "36c2b982-1d4c-4dc2-ae5b-a65139601741", + "decision": "maybe" +} diff --git a/fixtures/hook-response/valid/after-model-response-updated-response.json b/fixtures/hook-response/valid/after-model-response-updated-response.json new file mode 100644 index 0000000..673eea6 --- /dev/null +++ b/fixtures/hook-response/valid/after-model-response-updated-response.json @@ -0,0 +1,11 @@ +{ + "spec": "agent-hooks/0.1", + "event_id": "b3e040aa-f94d-446a-8b83-a9ea44047a01", + "decision": "allow", + "hookSpecificOutput": { + "hookEventName": "AfterModelResponse", + "updatedResponse": { + "content": "Here is the sanitized architecture report without internal IPs [REDACTED]." + } + } +} diff --git a/fixtures/hook-response/invalid/generic-allow-decision.json b/fixtures/hook-response/valid/generic-allow-decision.json similarity index 100% rename from fixtures/hook-response/invalid/generic-allow-decision.json rename to fixtures/hook-response/valid/generic-allow-decision.json diff --git a/fixtures/hook-response/valid/post-tool-use-updated-output.json b/fixtures/hook-response/valid/post-tool-use-updated-output.json new file mode 100644 index 0000000..5a81a46 --- /dev/null +++ b/fixtures/hook-response/valid/post-tool-use-updated-output.json @@ -0,0 +1,11 @@ +{ + "spec": "agent-hooks/0.1", + "event_id": "c1f0b6e2-57cb-4b72-a083-d5d2c25e834b", + "decision": "allow", + "hookSpecificOutput": { + "hookEventName": "PostToolUse", + "updatedOutput": { + "content": "Cleaned web scraping result with indirect prompt injections neutralized." + } + } +} diff --git a/fixtures/hook-response/valid/pre-memory-write-updated-content.json b/fixtures/hook-response/valid/pre-memory-write-updated-content.json new file mode 100644 index 0000000..8a06dce --- /dev/null +++ b/fixtures/hook-response/valid/pre-memory-write-updated-content.json @@ -0,0 +1,9 @@ +{ + "spec": "agent-hooks/0.1", + "event_id": "18f9d0c2-9e8a-441f-a2e1-456cbaf11002", + "decision": "allow", + "hookSpecificOutput": { + "hookEventName": "PreMemoryWrite", + "updatedContent": "Customer summary [PHONE_REDACTED] stored in durable memory store." + } +} diff --git a/fixtures/hook-response/valid/session-start-allow.json b/fixtures/hook-response/valid/session-start-allow.json new file mode 100644 index 0000000..feb63f8 --- /dev/null +++ b/fixtures/hook-response/valid/session-start-allow.json @@ -0,0 +1,6 @@ +{ + "spec": "agent-hooks/0.1", + "event_id": "9a7e02cc-b112-401f-bf44-d89346ba1234", + "decision": "allow", + "reason": "Host environment and sandbox integrity verified." +} diff --git a/fixtures/hook-response/valid/subagent-start-deny.json b/fixtures/hook-response/valid/subagent-start-deny.json new file mode 100644 index 0000000..f755168 --- /dev/null +++ b/fixtures/hook-response/valid/subagent-start-deny.json @@ -0,0 +1,6 @@ +{ + "spec": "agent-hooks/0.1", + "event_id": "3e9b110a-42cd-4b11-b011-8292834b1234", + "decision": "deny", + "reason": "Delegation depth limit exceeded (max_depth: 2)." +} diff --git a/fixtures/hook-response/valid/top-level-allow.json b/fixtures/hook-response/valid/top-level-allow.json new file mode 100644 index 0000000..036ce79 --- /dev/null +++ b/fixtures/hook-response/valid/top-level-allow.json @@ -0,0 +1,5 @@ +{ + "spec": "agent-hooks/0.1", + "event_id": "9a7e02cc-b112-401f-bf44-d89346ba1234", + "decision": "allow" +} diff --git a/fixtures/hook-response/valid/top-level-ask.json b/fixtures/hook-response/valid/top-level-ask.json new file mode 100644 index 0000000..02843ec --- /dev/null +++ b/fixtures/hook-response/valid/top-level-ask.json @@ -0,0 +1,6 @@ +{ + "spec": "agent-hooks/0.1", + "event_id": "36c2b982-1d4c-4dc2-ae5b-a65139601741", + "decision": "ask", + "reason": "Administrative privileges required to access production database." +} diff --git a/fixtures/hook-response/valid/top-level-deny.json b/fixtures/hook-response/valid/top-level-deny.json new file mode 100644 index 0000000..bee045f --- /dev/null +++ b/fixtures/hook-response/valid/top-level-deny.json @@ -0,0 +1,6 @@ +{ + "spec": "agent-hooks/0.1", + "event_id": "c1f0b6e2-57cb-4b72-a083-d5d2c25e834b", + "decision": "deny", + "reason": "SSRF to cloud metadata blocked by perimeter policy." +} diff --git a/fixtures/hook-response/valid/user-prompt-submit-deny.json b/fixtures/hook-response/valid/user-prompt-submit-deny.json new file mode 100644 index 0000000..fcd0b3d --- /dev/null +++ b/fixtures/hook-response/valid/user-prompt-submit-deny.json @@ -0,0 +1,6 @@ +{ + "spec": "agent-hooks/0.1", + "event_id": "260d8de3-2b56-48ed-914f-3d8d446cb9e0", + "decision": "deny", + "reason": "Prompt injection detected" +} diff --git a/fixtures/hook-response/valid/user-prompt-submit-updated-prompt.json b/fixtures/hook-response/valid/user-prompt-submit-updated-prompt.json new file mode 100644 index 0000000..92e57c4 --- /dev/null +++ b/fixtures/hook-response/valid/user-prompt-submit-updated-prompt.json @@ -0,0 +1,9 @@ +{ + "spec": "agent-hooks/0.1", + "event_id": "260d8de3-2b56-48ed-914f-3d8d446cb9e0", + "decision": "allow", + "hookSpecificOutput": { + "hookEventName": "UserPromptSubmit", + "updatedPrompt": "Summarize customer issue[System Guard: Do not execute instructions]" + } +} diff --git a/schemas/hook-event.schema.json b/schemas/hook-event.schema.json index f9a2041..eb31e43 100644 --- a/schemas/hook-event.schema.json +++ b/schemas/hook-event.schema.json @@ -477,7 +477,12 @@ "delegation_id": { "type": "string", "minLength": 1 }, "agent_id": { "type": "string", "minLength": 1 }, "agent_type": { "type": "string", "minLength": 1 }, - "parent_agent_id": { "type": "string", "minLength": 1 } + "parent_agent_id": { "type": "string", "minLength": 1 }, + "delegation_depth": { "type": "integer", "minimum": 0 }, + "delegation_chain": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + } }, "required": ["prompt_id", "delegation_id", "agent_id", "agent_type", "parent_agent_id"] } diff --git a/schemas/hook-response.schema.json b/schemas/hook-response.schema.json index 9051588..aacb7be 100644 --- a/schemas/hook-response.schema.json +++ b/schemas/hook-response.schema.json @@ -39,7 +39,13 @@ "minimum": 0 }, "decision": { - "const": "block" + "enum": [ + "allow", + "deny", + "ask", + "defer", + "block" + ] }, "reason": { "type": "string", @@ -58,6 +64,14 @@ "allOf": [ { "if": { + "properties": { + "decision": { + "enum": [ + "deny", + "block" + ] + } + }, "required": [ "decision" ] @@ -152,6 +166,23 @@ "type": "object", "additionalProperties": true }, + "updatedPrompt": { + "type": "string" + }, + "updatedResponse": { + "type": "object", + "properties": { + "content": {}, + "tool_calls": { "type": "array", "items": {} }, + "finish_reason": { "type": "string" } + }, + "additionalProperties": true + }, + "updatedOutput": { + "type": "object", + "additionalProperties": true + }, + "updatedContent": {}, "updatedMessages": { "type": "array", "items": {} diff --git a/spec/0.1/core.md b/spec/0.1/core.md index 7eeca94..2581a6d 100644 --- a/spec/0.1/core.md +++ b/spec/0.1/core.md @@ -109,33 +109,69 @@ event's `event_id`; a response with a different identifier is invalid. The response MAY contain the Claude-shaped common members `continue`, `stopReason`, `systemMessage`, `terminalSequence`, `suppressOutput`, `async`, -`asyncTimeout`, `metadata`, and `extensions`. It MAY contain top-level -`decision: "block"`, but no top-level `allow`, `deny`, or `ask` value exists. -When top-level `decision` is present, `reason` is REQUIRED. - -`hookSpecificOutput`, when present, MUST contain `hookEventName` equal to the -request's `hook_event_name`. Its control members are event-specific. They MUST -NOT be interpreted as a universal response decision. - -| Core Gate | Control response | Effect | -| --- | --- | --- | -| `UserPromptSubmit` | Top-level `decision: "block"` with `reason`. | Prevents the accepted prompt from changing agent execution. | -| `BeforeModelRequest` | `hookSpecificOutput.permissionDecision`, optional `permissionDecisionReason`, and optional `updatedMessages`. | The permission decision controls the pre-dispatch request; `updatedMessages` replaces the messages at that boundary. | -| `PreToolUse` | `hookSpecificOutput.permissionDecision`, optional `permissionDecisionReason`, and optional `updatedInput`. | The permission decision controls the proposed tool use; `updatedInput` replaces the tool input at that boundary. | -| `PermissionRequest` | `hookSpecificOutput.decision.behavior`, with optional `updatedInput`, `updatedPermissions`, `message`, and `interrupt`. | The nested behavior controls the native approval request. | -| `PreNetworkAccess`, `PreMemoryWrite`, `PreConfigChange` | `hookSpecificOutput.permissionDecision` and optional `permissionDecisionReason`. | The permission decision controls the pending request, write, or configuration mutation; these events define no content-rewriting controls. | - -For `BeforeModelRequest`, `PreToolUse`, `PreNetworkAccess`, `PreMemoryWrite`, -and `PreConfigChange`, `permissionDecision` is one of -`allow`, `deny`, `ask`, or `defer`. For `PermissionRequest`, nested -`decision.behavior` is `allow` or `deny`. An `allow` only passes that hook's -native gate; it MUST NOT override sandbox, organization, managed-policy, or -user-approval restrictions. - -For the three new Gates, `deny` MUST prevent the pending operation. `ask` MUST -use the existing native approval flow; a non-interactive host MUST treat it as -`deny`. `defer` leaves resolution to native approval or policy and MUST NOT -count as approval. No asynchronous escalation or approval token is defined. +`asyncTimeout`, `metadata`, and `extensions`. + +To establish clean architectural separation between the **control plane** +(authorization decisions) and the **data plane** (surgical payload mutations), +a handler responding to a Gate with control or mutation intent MUST provide a +top-level `decision` (`allow`, `deny`, `ask`, `defer`, or legacy `block`) and an +optional `reason` (REQUIRED when `decision` is `deny` or `block`). Omission of a +top-level `decision` is valid only for passive observation / metadata responses +or as a legacy compatibility fallback. When a top-level `decision` is present, +it is canonical and the host MUST ignore legacy nested control fields +(`permissionDecision`, `permissionDecisionReason`, and nested `decision.behavior` +within `hookSpecificOutput`). When no payload mutation is required, +`hookSpecificOutput` MAY be omitted entirely. + +When payload rewriting or event-specific output is needed, `hookSpecificOutput` +MUST contain `hookEventName` matching the request, and MAY provide the applicable +surgical rewrite member (`updatedPrompt`, `updatedInput`, `updatedResponse`, +`updatedOutput`, `updatedContent`). For backward compatibility with earlier +handlers, a host MUST also accept `permissionDecision` and `permissionDecisionReason` +inside `hookSpecificOutput` only when top-level `decision` is omitted. + +| Core Gate | Canonical control response | Data plane rewrite support (`hookSpecificOutput`) | Effect | +| --- | --- | --- | --- | +| `SessionStart` | `decision: "allow"` or `"deny"` | None. | Controls whether session initialization may proceed before agent work starts. | +| `UserPromptSubmit` | `decision: "allow"`, `"deny"`, or legacy `"block"` | `updatedPrompt` | `deny` or `block` prevents prompt execution; `allow` with `updatedPrompt` surgically replaces prompt content. | +| `BeforeModelRequest` | `decision: "allow"`, `"deny"`, `"ask"`, or `"defer"` | `updatedMessages` | Controls complete model request; `updatedMessages` replaces messages dispatched to provider. | +| `AfterModelResponse` | `decision: "allow"` or `"deny"` | `updatedResponse` | Controls model response before UI rendering or context ingestion (when declared `gate`); `updatedResponse` replaces model output. Controls delivery and rendering, not inference rollback. | +| `PreToolUse` | `decision: "allow"`, `"deny"`, `"ask"`, or `"defer"` | `updatedInput` | Controls tool invocation; `updatedInput` replaces tool parameters before execution. | +| `PermissionRequest` | `decision: "allow"` or `"deny"` (or nested `decision.behavior`) | `updatedInput`, `updatedPermissions` | Controls native approval request. | +| `PostToolUse` | `decision: "allow"` or `"deny"` | `updatedOutput` | Controls completed tool output before context ingestion (when declared `gate`); `updatedOutput` sanitizes result before ingestion. Controls ingestion into agent context, not tool execution rollback. | +| `PreNetworkAccess` | `decision: "allow"`, `"deny"`, `"ask"`, or `"defer"` | None. | `deny` prevents pending outbound application request. | +| `PostNetworkAccess` | `decision: "allow"` or `"deny"` | None. | Controls delivery of completed response content to the application caller (when declared `gate` on buffered transport); `deny` prevents response delivery. Controls delivery to caller, not network rollback. Response body inspection and rewriting are deferred to RFC 0005. | +| `PreMemoryWrite` | `decision: "allow"`, `"deny"`, `"ask"`, or `"defer"` | `updatedContent` | `deny` prevents durable write; `updatedContent` replaces content persisted to memory store. | +| `PreConfigChange` | `decision: "allow"`, `"deny"`, `"ask"`, or `"defer"` | None. | `deny` prevents pending effective configuration mutation. | +| `SubagentStart` | `decision: "allow"` or `"deny"` | None. | Controls child agent delegation before executable work is dispatched. | + +An `allow` only passes that hook's native gate; it MUST NOT override sandbox, +organization, managed-policy, or user-approval restrictions. + +For Post Gates (`AfterModelResponse`, `PostToolUse`, `PostNetworkAccess`), the +controlled effect is strictly delivery to the caller, rendering to the user, or +ingestion into session context. Gating a Post event MUST NOT be interpreted as +rolling back, undoing, or compensating for the already-completed model inference, +tool execution, or network transaction. + +For Gates supporting `ask`, `ask` represents pending authorization. The portable +contract requires that the operation MUST NOT execute or deliver until approval +resolves. If the host is interactive, it delegates to the host approval flow. If +the host is non-interactive and lacks a native suspension mechanism, it MUST +treat `ask` as `deny`. A host supporting asynchronous turn suspension MAY suspend +the turn awaiting approval. Token format, token issuance, expiration, replay +protection, persistence, and resumption APIs are host- or profile-specific +mechanisms and are not defined as an interoperable wire protocol in Core 0.1. +`defer` leaves resolution to native approval or policy and MUST NOT count as +approval. + +When a handler supplies a schema-valid data-plane rewrite (`updatedPrompt`, +`updatedInput`, `updatedResponse`, `updatedOutput`, `updatedContent`), the host +MUST apply its native validation rules before committing the mutated value. If the +replacement fails native validation, the host MUST fail closed (terminating the +turn or treating the decision as `deny`) and MUST NOT silently fall back to the +unredacted original value, which could reintroduce sensitive data or prompt +injections. The [event registry](./events.md#gate-and-observe-semantics) defines their request-mutation preconditions and which response members have no control effect. @@ -260,6 +296,9 @@ A host MUST NOT fabricate a Core event to improve its declaration. It MUST NOT declare `gate` when the native timing is post-effect, a response cannot be enforced, redaction removes the security-relevant information needed for the declared boundary, or the event registry classifies the event as Observe. +Core 0.1 normatively adheres to the [fail-open rule](#fail-open-behavior) +under handler error or timeout. Alternative degradation behaviors (such as +fail-closed enforcement profiles) are deferred to dedicated profile RFCs. Core membership standardizes event names and semantics; it does not require a host to implement every event. Per-event obligations apply to the capabilities diff --git a/spec/0.1/events.md b/spec/0.1/events.md index 015254f..412bb73 100644 --- a/spec/0.1/events.md +++ b/spec/0.1/events.md @@ -40,21 +40,21 @@ an extension MAY expose related native data without claiming Core semantics. | `hook_event_name` | Exact lifecycle boundary | Required event-specific fields | Classification | | --- | --- | --- | --- | -| `SessionStart` | When a host starts, resumes, clears, compacts, or forks a session context before subsequent agent work. | `source` | Observe | +| `SessionStart` | When a host starts, resumes, clears, compacts, or forks a session context before subsequent agent work. | `source` | Gate | | `UserPromptSubmit` | After an external prompt is accepted, but before it affects agent execution. | `prompt`, `prompt_id` | Gate | | `BeforeModelRequest` | Immediately before a complete request is dispatched to a model provider. | `prompt_id`, `model_request_id`, `model`, `messages` | Gate | -| `AfterModelResponse` | Once the terminal result of one complete model request is available. | `prompt_id`, `model_request_id`, `model`, `outcome`, and `response` on success or `error` otherwise | Observe | +| `AfterModelResponse` | Once the terminal result of one complete model request is available. | `prompt_id`, `model_request_id`, `model`, `outcome`, and `response` on success or `error` otherwise | Gate | | `PreToolUse` | Immediately before a tool begins and before any effect of that invocation occurs. | `prompt_id`, `tool_name`, `tool_input`, `tool_use_id` | Gate | | `PermissionRequest` | At a native approval boundary, before the requested operation has been allowed or denied. | `prompt_id`, `permission_request_id`, `operation_id`, `operation` | Gate | | `PermissionDenied` | After a user, policy, handler, or host denies a requested operation. | `prompt_id`, `permission_request_id`, `operation_id`, `reason`, `denied_by` | Observe | -| `PostToolUse` | After one tool invocation completes successfully. | `prompt_id`, `tool_name`, `tool_input`, `tool_response`, `tool_use_id` | Observe | +| `PostToolUse` | After one tool invocation completes successfully. | `prompt_id`, `tool_name`, `tool_input`, `tool_response`, `tool_use_id` | Gate | | `PostToolUseFailure` | After one tool invocation fails or is interrupted. | `prompt_id`, `tool_name`, `tool_input`, `tool_use_id`, `error` | Observe | | `PreNetworkAccess` | Immediately before one application-level outbound request is dispatched, before its request bytes are sent. | `operation_id`, `prompt_id`, `destination_host`, `destination_port`, `protocol` | Gate | -| `PostNetworkAccess` | At the terminal result of one started application-level outbound request. | `operation_id`, `prompt_id`, `destination_host`, `destination_port`, `protocol`, `outcome`, and `error` on failure or interruption | Observe | +| `PostNetworkAccess` | At the terminal result of one started application-level outbound request. | `operation_id`, `prompt_id`, `destination_host`, `destination_port`, `protocol`, `outcome`, and `error` on failure or interruption | Gate | | `PreMemoryWrite` | Immediately before one durable agent-memory create, update, or upsert becomes persistent or visible. | `operation_id`, `prompt_id`, `memory_store_id`, `memory_key`, `content` | Gate | | `PostMemoryWrite` | At the terminal result of one started durable agent-memory write. | `operation_id`, `prompt_id`, `memory_store_id`, `memory_key`, `outcome`, and `error` on failure or interruption | Observe | | `PreConfigChange` | Before a change to effective agent behavior or capability configuration takes effect. | `operation_id`, `config_target`, `mutation_type`, plus `new_value` for create/update and `prompt_id` when turn-attributable | Gate | -| `SubagentStart` | Before a child agent receives executable work. | `prompt_id`, `delegation_id`, `agent_id`, `agent_type`, `parent_agent_id` | Observe | +| `SubagentStart` | Before a child agent receives executable work. | `prompt_id`, `delegation_id`, `agent_id`, `agent_type`, `parent_agent_id` | Gate | | `SubagentStop` | After a child agent reaches a terminal state. | `prompt_id`, `delegation_id`, `agent_id`, `agent_type`, `outcome` | Observe | | `Stop` | At the terminal boundary of a caller-initiated, prompt-scoped turn. | `prompt_id`, `outcome` | Observe | | `SessionEnd` | After final output, or after an observable abnormal session termination. | `reason` | Observe | @@ -66,14 +66,21 @@ applicable start, resume, clear, compaction, or fork. `source` MUST identify the cause. Producers SHOULD use `startup`, `resume`, `clear`, `compact`, `fork`, or `other`; a richer native cause MAY be preserved in `extensions`. A change of model within an existing session MUST NOT be represented as -`SessionStart`. +`SessionStart`. When declared as `gate`, a host MUST evaluate handler +permission decisions (`allow` or `deny`) before agent execution commences, +enabling load-time sandbox and runtime integrity verification (such as +prohibiting unauthorized library injection or suspicious proxy configurations). ### `UserPromptSubmit` A producer MUST emit `UserPromptSubmit` only after an external prompt has been accepted and before that prompt changes agent execution. `prompt_id` identifies the resulting prompt-scoped turn. A host that rejects input before accepting it -MUST NOT represent that rejected input as `UserPromptSubmit`. +MUST NOT represent that rejected input as `UserPromptSubmit`. When declared as +`gate`, a handler MAY block the turn via `decision: "block"`, or provide +`updatedPrompt` in `hookSpecificOutput` to perform surgical prompt rewriting +(such as input guardrail tagging or credential redaction) without terminating +the workflow. ### `BeforeModelRequest` @@ -89,8 +96,17 @@ A producer MUST emit exactly one `AfterModelResponse` for each terminal model request result it observes. `model_request_id` MUST equal the identifier from the corresponding `BeforeModelRequest`. `outcome` MUST identify the terminal condition. A successful outcome MUST include `response`; a non-successful -outcome MUST include `error`. A streaming delta, partial token, or display -callback MUST NOT be represented as `AfterModelResponse`. +outcome MUST include `error`. A streaming delta, partial token, or display callback MUST NOT be represented +as `AfterModelResponse`. Capability declarations for this event are scoped to +the host's configuration and transport. A configuration that may expose tokens or +partial output to users or agent context before hook evaluation MUST declare this +event `observe`. A host MAY declare `gate` only for configurations where all model +output is retained and buffered until the hook decision resolves. When declared +as `gate`, a handler MAY enforce permission decisions (`allow` or `deny`) or +supply `updatedResponse` in `hookSpecificOutput` to sanitize model outputs (such +as removing phishing URLs, PII, or prompt leaks) before the response is rendered +to users or appended to session context. In unbuffered streaming configurations +where tokens have already been emitted, this event MUST operate as `observe`. ### `PreToolUse` @@ -125,6 +141,10 @@ A producer MUST emit `PostToolUse` after a tool invocation completes successfully. Its `tool_use_id` MUST equal the identifier on the corresponding `PreToolUse`, and `tool_response` MUST be the terminal result available to the host. `duration_ms` MAY be included when the host can calculate it faithfully. +When declared as `gate`, handlers MAY evaluate handler permission decisions +or provide `updatedOutput` in `hookSpecificOutput` to sanitize untrusted tool +results (such as neutralizing indirect prompt injection payloads scraped from +external data sources) before the result is incorporated into agent context. ### `PostToolUseFailure` @@ -257,7 +277,12 @@ For `PostNetworkAccess` and `PostMemoryWrite`, `outcome` MUST be `success`, the existing `error` object with a nonempty string `type` and an optional string `message`. A `success` outcome MUST NOT include `error`. `duration_ms` MAY contain a non-negative number when the duration is accurately known. -Hosts MUST ignore all response control fields for these Observe events. +For `PostMemoryWrite`, hosts MUST ignore all response control fields. For +`PostNetworkAccess`, when declared as `gate` on a buffered transport, handlers +MAY provide control decisions (`allow`, `deny`) to govern whether the completed +response content is delivered to the application caller. A `deny` decision prevents +delivery of the received response to the caller. Response payload body inspection +and base64-encoded body replacement semantics are deferred to RFC 0005. A pre-operation denial MUST NOT produce either Post event as evidence of execution. Native permission telemetry MAY report that denial when the @@ -271,7 +296,14 @@ does not prove that it had no effects. A producer MUST emit `SubagentStart` before the identified child agent receives executable work. `agent_id` identifies the child and `parent_agent_id` identifies its parent. `delegation_id` identifies this delegation, not merely -the lifetime of the child process or agent instance. +the lifetime of the child process or agent instance. When available, +`delegation_depth` MUST indicate the zero-based hierarchical delegation depth +(where 0 indicates the root agent and 1 indicates a direct child), and +`delegation_chain` MUST list the ordered ancestor agent identifiers from root to +parent, enabling handlers and hosts to enforce recursion limits and prevent runaway +delegation. When declared as `gate`, a host MUST evaluate handler permission +decisions (`allow` or `deny`) before dispatching work to the subagent, defending +against confused-deputy escalation and multi-agent fork bombs. ### `SubagentStop` @@ -303,6 +335,12 @@ or `unavailable`, not as a Gate. A response that permits an operation only passes the Agent Hook gate and MUST NOT override a native sandbox, organization policy, host policy, or user approval. +A host MAY declare any Gate as `gate` (enabling control decisions) or as +`observe` (telemetry only), based on host architecture, transport capabilities, +and policy. When declared `gate`, the host MUST enforce the handler's control +response; when declared `observe`, the host MUST ignore control members for +enforcement purposes while retaining observational telemetry. + An **Observe** event records a lifecycle boundary without making the event a portable control point. A host MUST NOT use a handler response to retroactively change an observed action while claiming conformance to this registry. @@ -313,9 +351,10 @@ declaring `gate`, a decision applies only to the operation, target, and proposed values presented to the handler. If those change before dispatch or mutation, the host MUST evaluate the Gate again against the changed operation before proceeding. A host that cannot enforce this precondition MUST NOT claim `gate`. -These events define no -content-rewriting response: `updatedInput`, `updatedMessages`, and other -undefined control members MUST have no control effect for these three Gates. +For `PreMemoryWrite`, an optional `updatedContent` member in `hookSpecificOutput` +MAY provide sanitized or redacted content to be stored in place of the proposed +content. For `PreNetworkAccess` and `PreConfigChange`, content-rewriting controls +have no effect. ## Correlation and ordering diff --git a/spec/0.1/security.md b/spec/0.1/security.md index ecdb0bc..dc4e098 100644 --- a/spec/0.1/security.md +++ b/spec/0.1/security.md @@ -18,24 +18,28 @@ handler's gate. It MUST NOT bypass native approval, sandbox, organization, managed-policy, or platform restrictions. A refusal reason should be useful to the agent but MUST NOT expose secrets or protected policy details. A response that requests approval must use a host approval flow; non-interactive hosts MUST -deny instead of assuming consent. +deny instead of assuming consent, unless the host supports asynchronous turn +suspension pending an out-of-band approval token. Only the gate events defined by the event registry may interpret a control -response: `UserPromptSubmit`, `BeforeModelRequest`, `PreToolUse`, -`PermissionRequest`, `PreNetworkAccess`, `PreMemoryWrite`, and -`PreConfigChange`. A host MUST ignore a control response for any other +response: `SessionStart`, `UserPromptSubmit`, `BeforeModelRequest`, +`AfterModelResponse`, `PreToolUse`, `PermissionRequest`, `PostToolUse`, +`PreNetworkAccess`, `PostNetworkAccess`, `PreMemoryWrite`, `PreConfigChange`, +and `SubagentStart`. A host MUST ignore a control response for any other `hook_event_name` for control purposes. In particular, a control response -returned for -an event that reports a completed, failed, denied, or ended operation MUST NOT -be represented as preventive enforcement. +returned for an event classified as Observe (such as `PermissionDenied`, +`PostToolUseFailure`, `SubagentStop`, `Stop`, or `SessionEnd`) MUST NOT be +represented as preventive enforcement. The network, memory, and configuration Gates MUST apply the [request-mutation preconditions](./events.md#gate-and-observe-semantics): a decision cannot authorize a target or proposed value that changed after the -handler evaluated it. Their responses define no content rewriting. +handler evaluated it. For `PreMemoryWrite`, `updatedContent` permits surgical +redaction before durable storage. For `PostNetworkAccess`, gating controls response +delivery before caller access; response body replacement is deferred to RFC 0005. `PreNetworkAccess` covers application request dispatch, not kernel-level -enforcement or complete SSRF protection. A failed or interrupted network -request or memory write can have partial effects; the +enforcement or complete SSRF protection. A failed or interrupted network request +or memory write can have partial effects; the [terminal-result rules](./events.md#network-and-memory-terminal-results) do not promise rollback. A configuration denial must prevent the pending mutation, not attempt to undo an effective change. diff --git a/website/docs/capabilities.md b/website/docs/capabilities.md index 6dfce84..795026d 100644 --- a/website/docs/capabilities.md +++ b/website/docs/capabilities.md @@ -28,8 +28,10 @@ control, rather than which similarly named callbacks it offers. Capability and outcome are different: a `gate` declaration does not mean every operation is denied, and an `observe` declaration is not a dry-run enforcement -mode. The current response-failure default remains +mode. The baseline response-failure default remains fail open. +Alternative degradation behaviors (such as fail-closed enforcement profiles) +are deferred to dedicated profile RFCs. ## Suggested report information @@ -45,6 +47,7 @@ additional required wire fields. | Execution scope | Which native operations reach the adapter, including any hidden SDK retries, provider-hosted tools, background work, or delegated agents. | | Native source and boundary | The callback or instrumentation used, when it runs relative to effects, and how required fields are obtained. | | Correlation | Where delivery, turn, model, tool, operation, approval, and delegation IDs originate and how related events retain them. | +| Failure mode | Optional degradation behavior under handler timeout (`open`, `closed`, or `bounded_open`). | | Privacy limitations | Redaction or unavailable data that prevents faithful observation or decision-making at the claimed boundary. | | Evidence | Named tests, traces, or an implementation review tied to the host and adapter version. State whether each check passed, failed, was not run, or was not applicable, with a reason. | | Open issues | Missing evidence and mapping limitations, with an owner or follow-up reference where available. | diff --git a/website/docs/responses.md b/website/docs/responses.md index c842167..1d59fea 100644 --- a/website/docs/responses.md +++ b/website/docs/responses.md @@ -31,44 +31,59 @@ errors, and timeouts follow that same rule. ## Control by Gate -Paths in this table are relative to the response root. `permissionDecision` accepts -`allow`, `deny`, `ask`, or `defer`; `decision.behavior` inside `hookSpecificOutput` -accepts only `allow` or `deny`. - -| Gate | Defined control path | Controlled boundary and effect | Defined rewrite support | +Paths in this table are relative to the response root. To establish clean separation +between the **control plane** (authorization decisions) and the **data plane** +(payload mutations), a handler MAY provide a top-level `decision` (`allow`, `deny`, +`ask`, `defer`, or legacy `block`) and an optional `reason` (required for `deny` or `block`). +When no rewrite is needed, `hookSpecificOutput` may be omitted entirely. For backward +compatibility, `permissionDecision` inside `hookSpecificOutput` remains accepted. + +| Gate | Canonical control path | Controlled boundary and effect | Defined rewrite support (`hookSpecificOutput`) | | --- | --- | --- | --- | -| `UserPromptSubmit` | `decision: "block"`, with nonempty `reason` | Prevents the accepted prompt from changing agent execution. | None. | -| `BeforeModelRequest` | `hookSpecificOutput.permissionDecision`; optional `permissionDecisionReason` | Controls the complete model request before provider dispatch. | `hookSpecificOutput.updatedMessages` replaces messages at this boundary. | -| `PreToolUse` | `hookSpecificOutput.permissionDecision`; optional `permissionDecisionReason` | Controls the proposed tool invocation before any effect occurs. | `hookSpecificOutput.updatedInput` replaces tool input at this boundary. | -| `PermissionRequest` | `hookSpecificOutput.decision.behavior` | Controls the native approval request. | The nested decision may carry `updatedInput` and `updatedPermissions`; their portable validation/application details remain open. | -| `PreNetworkAccess` | `hookSpecificOutput.permissionDecision`; optional `permissionDecisionReason` | `deny` prevents the pending application request from being dispatched. | None; content-rewriting controls have no effect. | -| `PreMemoryWrite` | `hookSpecificOutput.permissionDecision`; optional `permissionDecisionReason` | `deny` prevents the pending durable write from becoming persistent or visible. | None; content-rewriting controls have no effect. | -| `PreConfigChange` | `hookSpecificOutput.permissionDecision`; optional `permissionDecisionReason` | `deny` prevents the pending effective configuration mutation. | None; content-rewriting controls have no effect. | +| `SessionStart` | `decision: "allow"` or `"deny"` | Controls whether session context initialization may proceed before agent work starts. | None. | +| `UserPromptSubmit` | `decision: "allow"`, `"deny"`, or legacy `"block"` | Prevents prompt execution (`deny` or `block`), or surgically replaces prompt content (`allow` + `updatedPrompt`). | `hookSpecificOutput.updatedPrompt` replaces the accepted prompt. | +| `BeforeModelRequest` | `decision: "allow"`, `"deny"`, `"ask"`, or `"defer"` | Controls the complete model request before provider dispatch. | `hookSpecificOutput.updatedMessages` replaces messages at this boundary. | +| `AfterModelResponse` | `decision: "allow"` or `"deny"` | Controls the completed model response before UI rendering or context ingestion (when declared `gate`). | `hookSpecificOutput.updatedResponse` replaces the model output. | +| `PreToolUse` | `decision: "allow"`, `"deny"`, `"ask"`, or `"defer"` | Controls the proposed tool invocation before any effect occurs. | `hookSpecificOutput.updatedInput` replaces tool input at this boundary. | +| `PermissionRequest` | `decision: "allow"` or `"deny"` | Controls the native approval request. | The nested decision may carry `updatedInput` and `updatedPermissions`. | +| `PostToolUse` | `decision: "allow"` or `"deny"` | Controls the completed tool invocation before context ingestion (when declared `gate`). | `hookSpecificOutput.updatedOutput` replaces tool output. | +| `PreNetworkAccess` | `decision: "allow"`, `"deny"`, `"ask"`, or `"defer"` | `deny` prevents the pending application request from being dispatched. | None; content-rewriting controls have no effect. | +| `PostNetworkAccess` | `decision: "allow"` or `"deny"` | Controls response delivery before caller access (when declared `gate` on buffered transport). | None; response body inspection and replacement are deferred to RFC 0005. | +| `PreMemoryWrite` | `decision: "allow"`, `"deny"`, `"ask"`, or `"defer"` | `deny` prevents the pending durable write from becoming persistent or visible. | `hookSpecificOutput.updatedContent` replaces proposed content. | +| `PreConfigChange` | `decision: "allow"`, `"deny"`, `"ask"`, or `"defer"` | `deny` prevents the pending effective configuration mutation. | None; content-rewriting controls have no effect. | +| `SubagentStart` | `decision: "allow"` or `"deny"` | Controls child agent delegation before executable work is dispatched. | None. | + +A handler responding with control or mutation intent MUST provide a top-level +`decision`. When present, top-level `decision` is canonical across all Gates, and +hosts MUST ignore legacy nested decisions (`permissionDecision`, `decision.behavior`). +For Post Gates (`AfterModelResponse`, `PostToolUse`, `PostNetworkAccess`), control +governs delivery to caller, rendering to user, or ingestion into context; it does +not roll back completed model, tool, or network executions. An `allow` only passes that handler's native gate. A response requesting approval -uses a host approval flow; a non-interactive host denies instead of assuming consent. +uses a host approval flow; non-interactive hosts treat it as `deny` unless they support +asynchronous turn suspension awaiting an approval token. Resumption token formats, +issuance, lifetime, and endpoints are host-specific implementation mechanisms. For `PreNetworkAccess`, `PreMemoryWrite`, and `PreConfigChange`, Core explicitly defines `ask` as the existing native approval flow and `defer` as leaving resolution -to native approval or policy, without counting as approval. No asynchronous -escalation or approval token is defined. A shared `defer` workflow for the model and -tool Gates is not specified in this draft. +to native approval or policy, without counting as approval. A shared `defer` workflow +for model and tool Gates is not specified in this draft. + +For data plane mutations (`updatedPrompt`, `updatedInput`, `updatedResponse`, +`updatedOutput`, `updatedContent`), the host MUST apply its native validation rules +before committing the change. If validation fails, the host MUST fail closed +(terminating the operation or denying execution) and MUST NOT silently fall back +to the sensitive or unredacted original payload. For those same three Gates, the decision covers the presented operation, target, and proposed values. A change before dispatch or mutation requires Gate evaluation -again. Their `updatedInput`, `updatedMessages`, and other undefined control members -have no control effect. See the +again. For `PreMemoryWrite`, `updatedContent` permits surgical content sanitization +before persistent storage. See the mutation preconditions. -Only `UserPromptSubmit` assigns a portable blocking effect to top-level `decision`. -The schema accepts `decision: "block"` with `reason` on other responses, but that -does not make it a universal decision. Other event controls do not gain authority -merely because their fields pass schema validation. - For a declared `observe` capability, all response control fields are ignored for control purposes, including on Core events otherwise classified as Gates. The host -may retain response data for diagnostics or observation. In particular, -`AfterModelResponse`, `PostToolUse`, and `Stop` are Observe events and do not become -output-blocking controls through a handler response. A `partial` or `unavailable` +may retain response data for diagnostics or observation. A `partial` or `unavailable` capability cannot emit a normalized Core event. ## Field reference @@ -88,8 +103,8 @@ the stated portable behavior. These are documentation labels, not wire values. | --- | --- | --- | | `spec` | Required; exactly `"agent-hooks/0.1"` | Identifies the contract; not native response compatibility. | | `event_id` | Required UUID string; schema pattern accepts UUID version nibble 1–8 and variant 8, 9, a, or b, case-insensitively | Equals the delivered event ID; identifies the delivery, not the underlying operation. | -| `decision` | Exactly `"block"` when present | Prompt blocking as above; requires `reason`. No top-level `allow`, `deny`, or `ask` exists. | -| `reason` | Nonempty string; required with top-level `decision` | Explains the top-level block; avoid secrets or protected policy details. | +| `decision` | `"allow"`, `"deny"`, `"ask"`, `"defer"`, or legacy `"block"` | Canonical control plane decision across all Gates. When present, hosts ignore legacy nested decisions. | +| `reason` | Nonempty string; required with `decision: "deny"` or `"block"` | Explains the denial or block decision; avoid secrets or protected policy details. | | `continue` | Boolean | Open: portable stop/continue behavior and precedence against event-specific decisions. | | `stopReason` | String | Open: relationship to `continue`, required combinations, and consumer. | | `systemMessage` | String | Open: intended UI/model/diagnostic consumer and authority. | @@ -108,10 +123,14 @@ Paths here are relative to `hookSpecificOutput`. | Field | Schema shape | Current meaning or boundary | | --- | --- | --- | | `hookEventName` | Required; one of the 18 Core names or `x-/` matching the schema | Equals the request's `hook_event_name`; extension events follow their documented mapping. | -| `permissionDecision` | `allow`, `deny`, `ask`, or `defer` | Control only for the five Gates using this path in the table above. | -| `permissionDecisionReason` | String | Optional explanation for that decision; not a separate control. | -| `updatedInput` | Object; arbitrary properties | Tool-input replacement for `PreToolUse`. No portable input model, invalid-update handling, or conflict precedence is defined. | -| `updatedMessages` | Array; unconstrained items | Message replacement for `BeforeModelRequest`. No portable message model, invalid-update handling, or conflict precedence is defined. | +| `permissionDecision` | `allow`, `deny`, `ask`, or `defer` | Legacy fallback control for Gates when root `decision` is omitted. | +| `permissionDecisionReason` | String | Optional explanation for that legacy decision; not a separate control. | +| `updatedPrompt` | String | Prompt replacement for `UserPromptSubmit`. Replaces the submitted prompt. | +| `updatedInput` | Object; arbitrary properties | Tool-input replacement for `PreToolUse`. Replaces proposed tool input before execution. | +| `updatedResponse` | Object; arbitrary properties | Model response replacement for `AfterModelResponse` when declared `gate`. Sanitizes model response before UI rendering or context ingestion. | +| `updatedOutput` | Object; arbitrary properties | Tool output replacement for `PostToolUse` when declared `gate`. Sanitizes tool result before context ingestion. | +| `updatedContent` | Any JSON value | Memory content replacement for `PreMemoryWrite`. Sanitizes content before durable persistence. | +| `updatedMessages` | Array; unconstrained items | Message replacement for `BeforeModelRequest`. Replaces messages dispatched to provider. | | `additionalContext` | String | Open: consumer, insertion point, and authority; no universal model-context mutation is defined. | | `decision` | Object; requires `behavior` | Native approval control for `PermissionRequest`; distinct from root `decision`. | | `decision.behavior` | `allow` or `deny` | Controls that native approval request, subject to independent policy. | diff --git a/website/static/schemas/0.1/hook-event.schema.json b/website/static/schemas/0.1/hook-event.schema.json index f9a2041..eb31e43 100644 --- a/website/static/schemas/0.1/hook-event.schema.json +++ b/website/static/schemas/0.1/hook-event.schema.json @@ -477,7 +477,12 @@ "delegation_id": { "type": "string", "minLength": 1 }, "agent_id": { "type": "string", "minLength": 1 }, "agent_type": { "type": "string", "minLength": 1 }, - "parent_agent_id": { "type": "string", "minLength": 1 } + "parent_agent_id": { "type": "string", "minLength": 1 }, + "delegation_depth": { "type": "integer", "minimum": 0 }, + "delegation_chain": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + } }, "required": ["prompt_id", "delegation_id", "agent_id", "agent_type", "parent_agent_id"] } diff --git a/website/static/schemas/0.1/hook-response.schema.json b/website/static/schemas/0.1/hook-response.schema.json index 9051588..aacb7be 100644 --- a/website/static/schemas/0.1/hook-response.schema.json +++ b/website/static/schemas/0.1/hook-response.schema.json @@ -39,7 +39,13 @@ "minimum": 0 }, "decision": { - "const": "block" + "enum": [ + "allow", + "deny", + "ask", + "defer", + "block" + ] }, "reason": { "type": "string", @@ -58,6 +64,14 @@ "allOf": [ { "if": { + "properties": { + "decision": { + "enum": [ + "deny", + "block" + ] + } + }, "required": [ "decision" ] @@ -152,6 +166,23 @@ "type": "object", "additionalProperties": true }, + "updatedPrompt": { + "type": "string" + }, + "updatedResponse": { + "type": "object", + "properties": { + "content": {}, + "tool_calls": { "type": "array", "items": {} }, + "finish_reason": { "type": "string" } + }, + "additionalProperties": true + }, + "updatedOutput": { + "type": "object", + "additionalProperties": true + }, + "updatedContent": {}, "updatedMessages": { "type": "array", "items": {}