diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3841473..33bca58 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ -# Contributing to Agent Hook Spec +# Contributing to Agent Hook Unity -Thank you for your interest in improving the Agent Hook Spec. We welcome +Thank you for your interest in improving the Agent Hook Unity. We welcome documentation corrections, schema and example improvements, feature proposals, and fixes. @@ -16,7 +16,7 @@ and fixes. ## Discussing ideas and reporting issues Use [GitHub Discussions](https://github.com/trendmicro/agent-hook-unity/discussions) -for questions, ideas, use cases, and proposed changes to the Agent Hook Spec. +for questions, ideas, use cases, and proposed changes to the Agent Hook Unity. Start a discussion before drafting an RFC so the community can help shape the proposal. @@ -33,6 +33,13 @@ For substantial changes to the protocol, schema, or semantics, follow the RFC process in [GOVERNANCE.md](GOVERNANCE.md). A formal RFC pull request must link to its prior GitHub Discussion. +The repository's existing unaccepted draft files include a consolidated +candidate described by [RFC 0007](rfcs/0007-core-draft-consolidation.md). +They are preparatory review material, not a record of completed governance. +Discussions was enabled on 2026-09-21; formal review still requires a linked +Discussion and the normal review window and votes. Do not mark a proposal +Accepted merely because its candidate files have been merged. + ## Contributing changes 1. Fork the repository and clone your fork locally. diff --git a/README.md b/README.md index c6fd324..3bebd82 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Agent Hook Spec +# Agent Hook Unity -Agent Hook Spec is a community effort to define a portable lifecycle-hook +Agent Hook Unity is a community effort to define a portable lifecycle-hook protocol for AI agents and their tooling. It will let agent builders describe events, hook payloads, hook responses, and security telemetry using a shared, interoperable model. @@ -16,11 +16,17 @@ occur. ## Status -The repository contains an adoption-ready Agent Hook 0.1 draft proposed by -[RFC 0001](rfcs/0001-agent-hook-core-event-contract.md), with five additional -standard events proposed by -[RFC 0004](rfcs/0004-standard-lifecycle-events.md). It is not active until -accepted through the RFC process. Join the +The repository contains a candidate Agent Hook Unity 0.1 draft for review. +[RFC 0007](rfcs/0007-core-draft-consolidation.md) consolidates the original +RFC 0001 and RFC 0004 proposals with subsequent working-draft changes, the +12 Gate / 6 Observe registry, and the minimum cross-handler deny guarantee. +The canonical wire identifier is `agent-hook-unity/0.1`; see the +[migration guide](schemas/README.md#migrating-the-unaccepted-draft) before +upgrading an integration that uses the former `agent-hooks/0.1` identifier. + +The candidate is not an active standard until accepted through the RFC +process. Working files and merged PRs do not substitute for recorded review +and votes. Join the [GitHub Discussions](https://github.com/trendmicro/agent-hook-unity/discussions) to help shape it. @@ -30,6 +36,7 @@ to help shape it. - [`spec/`](spec/README.md) — canonical normative specification Markdown. - [`schemas/`](schemas/README.md) — machine-readable JSON Schemas. - [`fixtures/`](fixtures/README.md) — schema-validation fixtures. +- [`conformance/`](conformance/gate-composition.md) — behavioral scenarios for host integration tests; not an executed runtime claim. - [`examples/`](examples/README.md) — illustrative integrations. - [`website/`](website/) — Docusaurus source for the GitHub Pages site. diff --git a/conformance/gate-composition.md b/conformance/gate-composition.md new file mode 100644 index 0000000..58ddc87 --- /dev/null +++ b/conformance/gate-composition.md @@ -0,0 +1,64 @@ +# Agent Hook Unity Core Gate composition scenarios + +These behavioral scenarios exercise the minimum composition rules in the Agent +Hook Unity 0.1 draft. They do not define a reference reducer, handler scheduler, +or mutation algorithm. A host harness that claims to test Core Gate composition +MUST cover every scenario whose decision or rewrite form applies to the Gate +capabilities it declares. It MUST record the host version, adapter version, +capability mode, handler set, scheduling mode, and configured deadlines used for +the run. Passing these scenarios is useful evidence, not by itself a claim of +runtime conformance. + +All example events use `spec: "agent-hook-unity/0.1"`. `A` and `B` are +handlers. `D1`, `D2`, and `D3` are distinct schema-valid UUID `event_id` values. +Unless a scenario says otherwise, the events describe one pending action at one +Gate boundary, all responses are schema-valid and correlated to their own +delivery, no native policy independently blocks the action, and no approval is +outstanding. + +Every enforcement scenario below requires a host declaration of `gate` at the +tested boundary. A registry Gate exposed by the host as `observe` is covered +by the final isolation case, not by an obligation to enforce its responses. + +The consolidation that introduces these obligations is tracked by +[draft RFC 0007](../rfcs/0007-core-draft-consolidation.md). The RFC remains +unaccepted. + +The approval scenario applies to `BeforeModelRequest`, `PreToolUse`, +`PreNetworkAccess`, `PreMemoryWrite`, and `PreConfigChange`, whose canonical +response shapes support `ask` and `defer`. Rewrite scenarios apply only to +`UserPromptSubmit` (`updatedPrompt`), `BeforeModelRequest` (`updatedMessages`), +`AfterModelResponse` (`updatedResponse`), `PreToolUse` and `PermissionRequest` +(`updatedInput`, plus `updatedPermissions` for `PermissionRequest`), +`PostToolUse` (`updatedOutput`), and `PreMemoryWrite` (`updatedContent`). + +## Required scenarios + +| Scenario | Harness stimulus | Required observation | +| --- | --- | --- | +| Sequential allow then deny | Deliver the pending action to A as D1 and accept `allow`. Deliver it to B as D2 and accept `deny`. | The host does not cross the Gate. It waits for B instead of treating A's earlier `allow` as final permission. | +| Sequential deny then allow | Deliver the pending action to A as D1 and accept `deny`, then obtain `allow` from B as D2 if the host continues scheduling. | The host does not cross the Gate. B's later `allow` does not clear A's denial. The host may short-circuit B only after the action is irrevocably denied. | +| Parallel outcomes | Invoke A and B in parallel. Exercise both completion orders with one valid `allow` and one valid `deny`; repeat with the other outcome replaced by a timeout or handler error. | Every run containing an accepted denial prevents the action, independent of completion order. A timeout or error supplies no result and does not erase the denial. | +| Invalid or mismatched response | A returns a schema-invalid response or a response correlated to a different `event_id`; B returns a valid `deny` for D2. Repeat with B returning `allow`. | The invalid or mismatched response supplies no result. The valid denial prevents the action. With only B's valid `allow`, the response failure does not independently deny the action. | +| Timeout only | Every applicable handler invocation reaches its declared deadline without a usable response, with no accepted denial, unresolved approval, or native restriction. | The timeouts supply no Agent Hook decision. The Core default permits the host to continue after the deadlines; the harness does not report the timeouts as approval decisions. | +| Approval remains unresolved | For a Gate whose response shape supports `ask` and `defer`, A returns `ask`. Exercise an interactive host while approval is pending, and a non-interactive host without native suspension. Also exercise `defer` while native policy has not resolved. | The interactive host does not cross the Gate while approval is pending. The non-interactive host treats `ask` as denial. `defer` does not count as approval. | +| Deadline and late denial | A returns `allow` promptly. B returns `deny` immediately before B's declared deadline. Repeat with B returning only after the deadline and after the host has crossed the Gate under the timeout rule. | The host waits through B's deadline and honors the on-time denial. A response after the expired invocation cannot retroactively undo an action already performed; if the host performs a new reevaluation, it uses a new delivery and applies that evaluation before crossing the still-pending boundary. | +| Redelivery with distinct event IDs | Deliver the same pending action and boundary as D1 and accept `deny`; redeliver it as D2 and receive `allow`. For events with `operation_id`, retain the same value. | D1 and D2 remain separately correlated deliveries, while the D1 denial remains binding to the same pending action. The host does not cross the Gate. | +| Reevaluation after change | Accept `deny` for D1. Change a target or proposed value in a way that requires reevaluation of the same pending action, deliver D2, and receive `allow`. | The host performs the required reevaluation but does not clear the earlier denial. Re-proposing the work as a truly new action requires treating it as a distinct underlying action, using a distinct correlation identifier where Core defines one, and obtaining all applicable decisions again. | +| Same operation, distinct Pre and Post boundaries | For one `operation_id`, accept `allow` at `PreNetworkAccess`, complete the request on a buffered transport, then accept `deny` at `PostNetworkAccess`. | The request may be dispatched after the Pre Gate permits it. The Post denial prevents response delivery to the caller and does not claim rollback of the request. A Pre denial instead prevents dispatch and produces no Post event as execution evidence. The shared `operation_id` does not merge the two pending actions. | +| Unrelated operations | Deny operation X at a Gate, then evaluate unrelated operation Y at the same event boundary with a distinct operation identity and accept `allow`. | X remains denied. X's denial does not deny Y; Y proceeds only after its own applicable handler outcomes, approvals, and native policy permit it. | +| Rewrite plus response failure | For a Gate with an event-supported rewrite, A returns a valid `allow` with that rewrite. B times out or returns an invalid response. | B supplies no result and does not silently restore the original value. If the action is otherwise permitted, the host carries A's accepted rewrite into its host-defined mutation ordering and native validation. If native validation of the value to be committed fails, the host fails closed as required by Core. | +| Conflicting rewrites and denial | For a Gate with an event-supported rewrite, A and B return different valid rewrites under each scheduling mode the host supports; add a valid denial from either handler in a second run. | The host applies its own ordering or conflict policy and native validation in the first run. The denial prevents the action in the second run regardless of which rewrite would otherwise win. | + +### Observe isolation + +Expose a registry Gate as `observe`, or use an Observe event. Return +schema-valid control responses, including a deny and event-supported rewrite +fields. The host MUST ignore these responses for control purposes and make no +Gate enforcement claim for this capability. Native policy remains independent. + +For Post Gates, “cross the Gate” means delivery, rendering, or context +ingestion, not rollback of the completed inference, tool execution, or network +request. The `PostNetworkAccess` cases exercise delivery control on a buffered +transport only. They do not require response-body fields, inspection, or +rewriting, which remain outside the current Core behavior. diff --git a/docs/superpowers/plans/2026-09-21-core-draft-consolidation.md b/docs/superpowers/plans/2026-09-21-core-draft-consolidation.md new file mode 100644 index 0000000..b279eb4 --- /dev/null +++ b/docs/superpowers/plans/2026-09-21-core-draft-consolidation.md @@ -0,0 +1,74 @@ +# Core Draft Consolidation Implementation Plan + +> **For agentic workers:** Use superpowers:subagent-driven-development to implement the independent tasks and review the combined change. + +**Goal:** Resolve the four PM findings against main `08ecf2973ed8a3fb7b09f7a8f3a9450f2ea206b3` with a coherent, explicitly unaccepted draft baseline. + +**Architecture:** Keep this repository a specification and schema project. Clarify the minimum host composition invariant, synchronize consumer guidance, and introduce the user-approved `agent-hook-unity/0.1` identity. Record the provenance and outstanding adoption decisions in consolidation RFC 0007 without inventing review dates or votes. + +**Tech Stack:** Markdown, JSON Schema Draft 2020-12, Node.js validation, Docusaurus. + +**Spec:** User-approved PM-feedback assessment in this task; normative source files in `spec/0.1/` and the proposed consolidation in `rfcs/0007-core-draft-consolidation.md`. + +## Global Constraints + +- Preserve 18 Core events: 12 Gate and 6 Observe; per-host capabilities remain explicit. +- Preserve supported legacy nested control when top-level decision is absent. +- A valid denial of the same pending action survives other handler outcomes and redelivery; timeout alone retains baseline fail-open behavior. +- Preserve native policy and unresolved approval restrictions; no action execution before required decisions finish or reach their declared deadlines. +- Do not claim runtime conformance from schema validation or a reference reducer. +- Keep RFCs Draft and review/vote metadata truthful. No fabricated acceptance or external publication. +- New wire identity: `agent-hook-unity/0.1`; canonical schema IDs use the existing `agent-hook-unity` Pages paths. Do not silently negotiate the colliding old identity. +- Product repositioning and full Responsible AI interoperability remain an explicitly separate design question. + +## Task 1: Core composition and behavioral conformance + +**Files:** `spec/0.1/core.md`, `spec/0.1/events.md`, `spec/0.1/security.md`, `website/docs/conformance.md`, `conformance/`. + +- [x] Define accepted valid deny as binding to the host's same pending action, across per-handler event IDs, retries of delivery, and reevaluation. +- [x] Qualify every fail-open continuation with other applicable decisions and independent native policy; distinguish timeout from permission. +- [x] Leave scheduling and mutation conflict resolution host-defined subject to deny preservation and existing approval rules. +- [x] Replace stale unresolved rewrite-validation language with the current fail-closed requirement. +- [x] Publish behavioral test scenarios for allow/deny order, failures, parallel outcomes, deadlines, redelivery, and distinct operations; label them as obligations for a host harness. + +## Task 2: Consumer-facing baseline synchronization + +**Files:** `spec/0.1/index.md`, `spec/0.1/adapters.md`, `examples/`, `website/docs/intro.md`, `website/docs/responses.md`, `website/docs/capabilities.md`. + +- [x] Align event modes and result-delivery semantics with the registry; remove stale seven/eleven counts. +- [x] Use canonical top-level decisions in examples and explicitly explain nested legacy fallback. +- [x] Align adapter and capability guidance with all current Gate controls and supported rewrites. +- [x] Explain draft status, partial RFC 0005 implementation, consolidation provenance, and safe host capability claims. + +## Task 3: Mechanical regression checks + +**Files:** `scripts/validate.mjs`, focused validator helpers/tests if needed, `package.json`. + +- [x] Demonstrate failing checks for mismatched published schema copies and registry/schema event drift before implementing them. +- [x] Derive the active identity from canonical schemas and validate current Markdown JSON examples against it, detecting stale envelope identifiers instead of silently skipping them. +- [x] Check event counts where summarized and canonical Gate membership where mirrored; avoid enforcing arbitrary prose wording. +- [x] Verify the validator with deliberate temporary mutations in isolated test data, including newly introduced or removed events and missing files. + +## Task 4: Consolidation RFC, identity migration, publication guidance + +**Files:** `rfcs/0007-core-draft-consolidation.md`, existing RFC baseline/status summaries, `schemas/`, `fixtures/`, published schema copies, active identifier references, root and website status guidance. + +- [x] Record which behavior came from PR #9, which RFC 0005 features remain proposals, and what RFC 0006 actually proposes. +- [x] Document new Core deny invariant, identity migration, schema locations, compatibility impact, and explicitly pending review requirements. +- [x] Move canonical `spec` constants and current examples to `agent-hook-unity/0.1`; add invalid fixtures for the former colliding identity. +- [x] Change schema IDs to their current published URLs; link immutable pre-migration schemas for pinned old integrations and explain that `$id` is not a retrieval requirement. +- [x] Document the future telemetry/correlation interoperability evaluation without claiming an adapter or external certification exists. + +## Exit Gate + +- [x] Simplify for reuse, quality, and efficiency; remove redundant checks and contradictory claims. +- [x] Run focused validator tests, `npm run validate`, `npm run build`, and `git diff --check`. +- [x] Run scoped independent `codex exec -p review` diff review; fix findings and re-review until Mergeable. +- [x] Report concrete changes, checks, review verdict, and remaining external adoption/publication steps. + +Validation: 14 validator regression tests, schema/fixture/Markdown validation, +image parser guards, the website production build, and five executable +PreToolUse example cases passed. The independent review profile returned +**Mergeable** with zero findings after the two initial findings were fixed. +This branch prepares the candidate only; RFC acceptance, a tested external +adapter, and publication after merge remain separate steps. diff --git a/examples/README.md b/examples/README.md index 3cd510d..a5b82cb 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,9 +1,12 @@ # Examples This directory contains non-normative, runnable or copyable examples of Agent -Hook behavior. The initial 0.1 draft includes a +Hook Unity behavior. The initial 0.1 draft includes a [pre-tool security guard](./pre-tool-guard.md) and a [post-tool audit event](./post-tool-audit.md). Examples must identify the specification and schema version they target, and must not introduce behavior absent from the canonical specification. +Current examples target the unaccepted `agent-hook-unity/0.1` draft. A host's +declared capability still determines whether a registry Gate is enforced or +delivered for observation only. diff --git a/examples/post-tool-audit.md b/examples/post-tool-audit.md index 0e480a1..a096a92 100644 --- a/examples/post-tool-audit.md +++ b/examples/post-tool-audit.md @@ -1,14 +1,16 @@ # Post-tool audit event This non-normative example records a successful tool invocation after it has -completed. `PostToolUse` is an Observe event, so the handler returns +completed. The registry classifies `PostToolUse` as a Gate, but the host in +this example declares it `observe`: it can report the completed invocation but +cannot hold the result before context ingestion. The handler therefore returns correlated metadata rather than a control response. Event delivered by the adapter: ```json { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "862a966f-6f7b-4c15-a8c5-40df353eeaac", "hook_event_name": "PostToolUse", "session_id": "session-42", @@ -26,7 +28,7 @@ Response from the audit handler: ```json { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "862a966f-6f7b-4c15-a8c5-40df353eeaac", "metadata": { "audit_id": "audit-20260909-17", @@ -37,3 +39,5 @@ Response from the audit handler: Handlers should not copy raw prompts, tool arguments, outputs, or credentials into audit records unless a documented privacy policy permits it. +Because this delivery is `observe`, any response control fields would be +ignored. The completed tool execution also cannot be rolled back. diff --git a/examples/pre-tool-guard.md b/examples/pre-tool-guard.md index 84984fa..f78df70 100644 --- a/examples/pre-tool-guard.md +++ b/examples/pre-tool-guard.md @@ -11,33 +11,40 @@ import process from 'node:process'; let input = ''; for await (const chunk of process.stdin) input += chunk; const event = JSON.parse(input); +if ( + event.spec !== 'agent-hook-unity/0.1' || + event.hook_event_name !== 'PreToolUse' +) { + throw new Error('Expected an agent-hook-unity/0.1 PreToolUse event.'); +} const command = event.tool_input?.command ?? ''; const response = { - spec: 'agent-hooks/0.1', - event_id: event.event_id, - hookSpecificOutput: { - hookEventName: 'PreToolUse' - } + spec: 'agent-hook-unity/0.1', + event_id: event.event_id }; if (/\brm\s+-rf\b/.test(command)) { - response.hookSpecificOutput.permissionDecision = 'deny'; - response.hookSpecificOutput.permissionDecisionReason = - 'Destructive recursive deletion is blocked.'; + response.decision = 'deny'; + response.reason = 'Destructive recursive deletion is blocked.'; } else if (/\bdeploy\b.*\bproduction\b/i.test(command)) { - response.hookSpecificOutput.permissionDecision = 'ask'; - response.hookSpecificOutput.permissionDecisionReason = - 'Confirm the production deployment.'; + response.decision = 'ask'; + response.reason = 'Confirm the production deployment.'; } else { - response.hookSpecificOutput.permissionDecision = 'allow'; + response.decision = 'allow'; } process.stdout.write(JSON.stringify(response) + '\n'); ``` -The `PreToolUse` response uses Claude Code's -`hookSpecificOutput.permissionDecision` convention. An `allow` only passes -this hook's gate; a sandbox, organization policy, host policy, or native -approval flow can still block the action. `ask` requires host-native approval. -See the [core protocol](../spec/0.1/core.md) for the normative behavior. +The `PreToolUse` response uses the canonical top-level `decision`. Legacy +`hookSpecificOutput.permissionDecision` remains valid only when a top-level +decision is absent. An `allow` is only this handler's result; another accepted +denial, a sandbox, organization policy, host policy, or native approval flow +can still block the action. `ask` requires host-native approval and does not +permit execution while that approval remains unresolved. See the +[core protocol](../spec/0.1/core.md) for the normative behavior. + +The host is responsible for validating the complete event schema before +delivery. The handler still checks the wire identifier and event name so it +does not silently relabel an older or unrelated request in its response. diff --git a/fixtures/hook-event/invalid/after-model-response-failure-with-response.json b/fixtures/hook-event/invalid/after-model-response-failure-with-response.json index 05d554e..55538be 100644 --- a/fixtures/hook-event/invalid/after-model-response-failure-with-response.json +++ b/fixtures/hook-event/invalid/after-model-response-failure-with-response.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "96090b9c-a4d8-49b8-a3e0-ed41917bce79", "hook_event_name": "AfterModelResponse", "session_id": "session-42", diff --git a/fixtures/hook-event/invalid/after-model-response-success-with-error.json b/fixtures/hook-event/invalid/after-model-response-success-with-error.json index 0417fd6..b778b2b 100644 --- a/fixtures/hook-event/invalid/after-model-response-success-with-error.json +++ b/fixtures/hook-event/invalid/after-model-response-success-with-error.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "ae8e9b81-5d66-438e-9a5f-13244da034ce", "hook_event_name": "AfterModelResponse", "session_id": "session-42", diff --git a/fixtures/hook-event/invalid/colliding-legacy-spec-identifier.json b/fixtures/hook-event/invalid/colliding-legacy-spec-identifier.json new file mode 100644 index 0000000..59f5f5f --- /dev/null +++ b/fixtures/hook-event/invalid/colliding-legacy-spec-identifier.json @@ -0,0 +1,11 @@ +{ + "spec": "agent-hooks/0.1", + "event_id": "862a966f-6f7b-4c15-a8c5-40df353eeaac", + "hook_event_name": "SessionStart", + "session_id": "session-42", + "timestamp": "2026-09-09T10:15:00Z", + "sequence": 0, + "cwd": "/workspace/project", + "source": "startup", + "model": "claude-opus-5" +} diff --git a/fixtures/hook-event/invalid/invalid-extension-namespace.json b/fixtures/hook-event/invalid/invalid-extension-namespace.json index 9ed3676..a7278d6 100644 --- a/fixtures/hook-event/invalid/invalid-extension-namespace.json +++ b/fixtures/hook-event/invalid/invalid-extension-namespace.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "6edab8d4-1bb3-4c37-bd6c-7ad7da707b7a", "hook_event_name": "PreToolUse", "session_id": "session-42", diff --git a/fixtures/hook-event/invalid/invalid-version.json b/fixtures/hook-event/invalid/invalid-version.json index 7086aae..2d3b789 100644 --- a/fixtures/hook-event/invalid/invalid-version.json +++ b/fixtures/hook-event/invalid/invalid-version.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.2", + "spec": "agent-hook-unity/0.2", "event_id": "6edab8d4-1bb3-4c37-bd6c-7ad7da707b7a", "hook_event_name": "PreToolUse", "session_id": "session-42", diff --git a/fixtures/hook-event/invalid/legacy-event-type.json b/fixtures/hook-event/invalid/legacy-event-type.json index 985bc34..1feb8fb 100644 --- a/fixtures/hook-event/invalid/legacy-event-type.json +++ b/fixtures/hook-event/invalid/legacy-event-type.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "6edab8d4-1bb3-4c37-bd6c-7ad7da707b7a", "hook_event_name": "PreToolUse", "event_type": "agent-hook.tool.pre", diff --git a/fixtures/hook-event/invalid/missing-event-id.json b/fixtures/hook-event/invalid/missing-event-id.json index bb63266..2b6743d 100644 --- a/fixtures/hook-event/invalid/missing-event-id.json +++ b/fixtures/hook-event/invalid/missing-event-id.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "hook_event_name": "PreToolUse", "session_id": "session-42", "timestamp": "2026-09-09T10:15:30Z", diff --git a/fixtures/hook-event/invalid/post-memory-write-interrupted-missing-error.json b/fixtures/hook-event/invalid/post-memory-write-interrupted-missing-error.json index 04568c0..25d2403 100644 --- a/fixtures/hook-event/invalid/post-memory-write-interrupted-missing-error.json +++ b/fixtures/hook-event/invalid/post-memory-write-interrupted-missing-error.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "8c7d9b12-4e68-4c2f-9af5-8476ae8b3021", "hook_event_name": "PostMemoryWrite", "session_id": "session-42", diff --git a/fixtures/hook-event/invalid/post-memory-write-success-with-error.json b/fixtures/hook-event/invalid/post-memory-write-success-with-error.json index f75b941..e930b53 100644 --- a/fixtures/hook-event/invalid/post-memory-write-success-with-error.json +++ b/fixtures/hook-event/invalid/post-memory-write-success-with-error.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "8c7d9b12-4e68-4c2f-9af5-8476ae8b3021", "hook_event_name": "PostMemoryWrite", "session_id": "session-42", diff --git a/fixtures/hook-event/invalid/post-network-access-failure-missing-error.json b/fixtures/hook-event/invalid/post-network-access-failure-missing-error.json index 9604af5..df20343 100644 --- a/fixtures/hook-event/invalid/post-network-access-failure-missing-error.json +++ b/fixtures/hook-event/invalid/post-network-access-failure-missing-error.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "8c7d9b12-4e68-4c2f-9af5-8476ae8b3021", "hook_event_name": "PostNetworkAccess", "session_id": "session-42", diff --git a/fixtures/hook-event/invalid/post-network-access-missing-protocol.json b/fixtures/hook-event/invalid/post-network-access-missing-protocol.json index 57672a6..fc3a167 100644 --- a/fixtures/hook-event/invalid/post-network-access-missing-protocol.json +++ b/fixtures/hook-event/invalid/post-network-access-missing-protocol.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "8c7d9b12-4e68-4c2f-9af5-8476ae8b3021", "hook_event_name": "PostNetworkAccess", "session_id": "session-42", diff --git a/fixtures/hook-event/invalid/post-network-access-negative-bytes-recv.json b/fixtures/hook-event/invalid/post-network-access-negative-bytes-recv.json index 4ee508e..d7b72f0 100644 --- a/fixtures/hook-event/invalid/post-network-access-negative-bytes-recv.json +++ b/fixtures/hook-event/invalid/post-network-access-negative-bytes-recv.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "8c7d9b12-4e68-4c2f-9af5-8476ae8b3021", "hook_event_name": "PostNetworkAccess", "session_id": "session-42", diff --git a/fixtures/hook-event/invalid/post-network-access-negative-bytes-sent.json b/fixtures/hook-event/invalid/post-network-access-negative-bytes-sent.json index 72900ff..b1c9cec 100644 --- a/fixtures/hook-event/invalid/post-network-access-negative-bytes-sent.json +++ b/fixtures/hook-event/invalid/post-network-access-negative-bytes-sent.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "f2e90286-1e22-4bc4-b52d-e2a89e4f19b9", "hook_event_name": "PostNetworkAccess", "session_id": "session-42", diff --git a/fixtures/hook-event/invalid/post-network-access-string-status-code.json b/fixtures/hook-event/invalid/post-network-access-string-status-code.json index bfcf0be..b64fd28 100644 --- a/fixtures/hook-event/invalid/post-network-access-string-status-code.json +++ b/fixtures/hook-event/invalid/post-network-access-string-status-code.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "c7dbba16-5d81-4d1f-a4e3-09047f34420b", "hook_event_name": "PostNetworkAccess", "session_id": "session-42", diff --git a/fixtures/hook-event/invalid/post-network-access-success-with-error.json b/fixtures/hook-event/invalid/post-network-access-success-with-error.json index aca26cf..379f093 100644 --- a/fixtures/hook-event/invalid/post-network-access-success-with-error.json +++ b/fixtures/hook-event/invalid/post-network-access-success-with-error.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "8c7d9b12-4e68-4c2f-9af5-8476ae8b3021", "hook_event_name": "PostNetworkAccess", "session_id": "session-42", diff --git a/fixtures/hook-event/invalid/pre-config-change-create-missing-new-value.json b/fixtures/hook-event/invalid/pre-config-change-create-missing-new-value.json index 6afdbb9..6b28bb2 100644 --- a/fixtures/hook-event/invalid/pre-config-change-create-missing-new-value.json +++ b/fixtures/hook-event/invalid/pre-config-change-create-missing-new-value.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "8c7d9b12-4e68-4c2f-9af5-8476ae8b3021", "hook_event_name": "PreConfigChange", "session_id": "session-42", diff --git a/fixtures/hook-event/invalid/pre-config-change-delete-with-new-value.json b/fixtures/hook-event/invalid/pre-config-change-delete-with-new-value.json index 73096e8..6880951 100644 --- a/fixtures/hook-event/invalid/pre-config-change-delete-with-new-value.json +++ b/fixtures/hook-event/invalid/pre-config-change-delete-with-new-value.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "8c7d9b12-4e68-4c2f-9af5-8476ae8b3021", "hook_event_name": "PreConfigChange", "session_id": "session-42", diff --git a/fixtures/hook-event/invalid/pre-config-change-update-missing-new-value.json b/fixtures/hook-event/invalid/pre-config-change-update-missing-new-value.json index 1f28526..80ba9cf 100644 --- a/fixtures/hook-event/invalid/pre-config-change-update-missing-new-value.json +++ b/fixtures/hook-event/invalid/pre-config-change-update-missing-new-value.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "8c7d9b12-4e68-4c2f-9af5-8476ae8b3021", "hook_event_name": "PreConfigChange", "session_id": "session-42", diff --git a/fixtures/hook-event/invalid/pre-memory-write-missing-content.json b/fixtures/hook-event/invalid/pre-memory-write-missing-content.json index fc60b5a..58db59c 100644 --- a/fixtures/hook-event/invalid/pre-memory-write-missing-content.json +++ b/fixtures/hook-event/invalid/pre-memory-write-missing-content.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "8c7d9b12-4e68-4c2f-9af5-8476ae8b3021", "hook_event_name": "PreMemoryWrite", "session_id": "session-42", diff --git a/fixtures/hook-event/invalid/pre-network-access-bracketed-address.json b/fixtures/hook-event/invalid/pre-network-access-bracketed-address.json index 0d4b843..5eda2ba 100644 --- a/fixtures/hook-event/invalid/pre-network-access-bracketed-address.json +++ b/fixtures/hook-event/invalid/pre-network-access-bracketed-address.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "7b42b9ef-a080-53f1-948a-72305405be7e", "hook_event_name": "PreNetworkAccess", "session_id": "session-42", diff --git a/fixtures/hook-event/invalid/pre-network-access-host-with-credentials.json b/fixtures/hook-event/invalid/pre-network-access-host-with-credentials.json index 9be0f3b..08c75be 100644 --- a/fixtures/hook-event/invalid/pre-network-access-host-with-credentials.json +++ b/fixtures/hook-event/invalid/pre-network-access-host-with-credentials.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "8c7d9b12-4e68-4c2f-9af5-8476ae8b3021", "hook_event_name": "PreNetworkAccess", "session_id": "session-42", diff --git a/fixtures/hook-event/invalid/pre-network-access-host-with-port.json b/fixtures/hook-event/invalid/pre-network-access-host-with-port.json index 776ab39..c1d948b 100644 --- a/fixtures/hook-event/invalid/pre-network-access-host-with-port.json +++ b/fixtures/hook-event/invalid/pre-network-access-host-with-port.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "7b42b9ef-a080-53f1-948a-72305405be7e", "hook_event_name": "PreNetworkAccess", "session_id": "session-42", diff --git a/fixtures/hook-event/invalid/pre-network-access-missing-operation-id.json b/fixtures/hook-event/invalid/pre-network-access-missing-operation-id.json index 7a36087..e54bd20 100644 --- a/fixtures/hook-event/invalid/pre-network-access-missing-operation-id.json +++ b/fixtures/hook-event/invalid/pre-network-access-missing-operation-id.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "8c7d9b12-4e68-4c2f-9af5-8476ae8b3021", "hook_event_name": "PreNetworkAccess", "session_id": "session-42", diff --git a/fixtures/hook-event/invalid/pre-network-access-missing-protocol.json b/fixtures/hook-event/invalid/pre-network-access-missing-protocol.json index 4ba612f..2fa7753 100644 --- a/fixtures/hook-event/invalid/pre-network-access-missing-protocol.json +++ b/fixtures/hook-event/invalid/pre-network-access-missing-protocol.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "8c7d9b12-4e68-4c2f-9af5-8476ae8b3021", "hook_event_name": "PreNetworkAccess", "session_id": "session-42", diff --git a/fixtures/hook-event/invalid/pre-network-access-port-too-large.json b/fixtures/hook-event/invalid/pre-network-access-port-too-large.json index 88e3468..863e857 100644 --- a/fixtures/hook-event/invalid/pre-network-access-port-too-large.json +++ b/fixtures/hook-event/invalid/pre-network-access-port-too-large.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "8c7d9b12-4e68-4c2f-9af5-8476ae8b3021", "hook_event_name": "PreNetworkAccess", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/after-model-response.json b/fixtures/hook-event/valid/after-model-response.json index c84ae20..f1c014e 100644 --- a/fixtures/hook-event/valid/after-model-response.json +++ b/fixtures/hook-event/valid/after-model-response.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "51ad25cc-a1a6-4acb-9531-ae50a9b0bceb", "hook_event_name": "AfterModelResponse", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/before-model-request.json b/fixtures/hook-event/valid/before-model-request.json index 49e94d7..e8fea64 100644 --- a/fixtures/hook-event/valid/before-model-request.json +++ b/fixtures/hook-event/valid/before-model-request.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "3c6477d4-f95c-4c45-b601-46e652956a54", "hook_event_name": "BeforeModelRequest", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/permission-denied.json b/fixtures/hook-event/valid/permission-denied.json index 9455e9b..525b77a 100644 --- a/fixtures/hook-event/valid/permission-denied.json +++ b/fixtures/hook-event/valid/permission-denied.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "b1a3d2e8-b5f8-4e0e-b1d9-78de90d370f5", "hook_event_name": "PermissionDenied", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/permission-request.json b/fixtures/hook-event/valid/permission-request.json index e9a7188..ff3a12d 100644 --- a/fixtures/hook-event/valid/permission-request.json +++ b/fixtures/hook-event/valid/permission-request.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "a248c9ee-5290-4680-b028-a98b6fbd19ef", "hook_event_name": "PermissionRequest", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/post-memory-write-failure.json b/fixtures/hook-event/valid/post-memory-write-failure.json index 21623d3..2a34ea8 100644 --- a/fixtures/hook-event/valid/post-memory-write-failure.json +++ b/fixtures/hook-event/valid/post-memory-write-failure.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "55b3bf68-5701-5e1c-bd3a-753d7e86353c", "hook_event_name": "PostMemoryWrite", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/post-memory-write-interrupted.json b/fixtures/hook-event/valid/post-memory-write-interrupted.json index acd2644..2f91403 100644 --- a/fixtures/hook-event/valid/post-memory-write-interrupted.json +++ b/fixtures/hook-event/valid/post-memory-write-interrupted.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "b9d121d1-411d-5bb4-adf9-7347d851404d", "hook_event_name": "PostMemoryWrite", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/post-memory-write.json b/fixtures/hook-event/valid/post-memory-write.json index be79ca0..c947fc0 100644 --- a/fixtures/hook-event/valid/post-memory-write.json +++ b/fixtures/hook-event/valid/post-memory-write.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "e8981ea4-2680-57d5-8ed8-708614ba4b09", "hook_event_name": "PostMemoryWrite", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/post-network-access-failure.json b/fixtures/hook-event/valid/post-network-access-failure.json index f97532f..f53d140 100644 --- a/fixtures/hook-event/valid/post-network-access-failure.json +++ b/fixtures/hook-event/valid/post-network-access-failure.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "d23bb7d2-d5ba-53df-9a4d-99028ce8d0e4", "hook_event_name": "PostNetworkAccess", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/post-network-access-http-forbidden.json b/fixtures/hook-event/valid/post-network-access-http-forbidden.json index a0a3f40..a5812a5 100644 --- a/fixtures/hook-event/valid/post-network-access-http-forbidden.json +++ b/fixtures/hook-event/valid/post-network-access-http-forbidden.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "4146f55a-f6ee-4f6a-b6f5-84019b9b763d", "hook_event_name": "PostNetworkAccess", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/post-network-access-interrupted-with-status.json b/fixtures/hook-event/valid/post-network-access-interrupted-with-status.json index cadf504..5b85be7 100644 --- a/fixtures/hook-event/valid/post-network-access-interrupted-with-status.json +++ b/fixtures/hook-event/valid/post-network-access-interrupted-with-status.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "5191b878-ec16-4810-83f3-4ccf4745a75c", "hook_event_name": "PostNetworkAccess", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/post-network-access-interrupted.json b/fixtures/hook-event/valid/post-network-access-interrupted.json index e96ff77..3042342 100644 --- a/fixtures/hook-event/valid/post-network-access-interrupted.json +++ b/fixtures/hook-event/valid/post-network-access-interrupted.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "76f447b0-90ca-5b6e-acd3-fadd24d697a2", "hook_event_name": "PostNetworkAccess", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/post-network-access.json b/fixtures/hook-event/valid/post-network-access.json index 1631545..0c6d14c 100644 --- a/fixtures/hook-event/valid/post-network-access.json +++ b/fixtures/hook-event/valid/post-network-access.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "aa5612f6-21f2-50a2-80bd-d3eeafc75fad", "hook_event_name": "PostNetworkAccess", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/post-tool-use-failure.json b/fixtures/hook-event/valid/post-tool-use-failure.json index a014226..c8a6be1 100644 --- a/fixtures/hook-event/valid/post-tool-use-failure.json +++ b/fixtures/hook-event/valid/post-tool-use-failure.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "9bc1db77-460b-4208-9a3b-164e0189c1e3", "hook_event_name": "PostToolUseFailure", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/post-tool-use.json b/fixtures/hook-event/valid/post-tool-use.json index d6836f8..081051d 100644 --- a/fixtures/hook-event/valid/post-tool-use.json +++ b/fixtures/hook-event/valid/post-tool-use.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "7a83870b-fd4b-4f44-a49e-cc2e2511e5ff", "hook_event_name": "PostToolUse", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/pre-config-change-create-null.json b/fixtures/hook-event/valid/pre-config-change-create-null.json index d0aa9e0..8c2a2a5 100644 --- a/fixtures/hook-event/valid/pre-config-change-create-null.json +++ b/fixtures/hook-event/valid/pre-config-change-create-null.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "20392f64-3f41-5d56-9a2c-7b524fa9d832", "hook_event_name": "PreConfigChange", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/pre-config-change-create.json b/fixtures/hook-event/valid/pre-config-change-create.json index 82cd239..e3ff670 100644 --- a/fixtures/hook-event/valid/pre-config-change-create.json +++ b/fixtures/hook-event/valid/pre-config-change-create.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "b35cf6e6-1659-5d5f-ab7e-17e6b7e1df56", "hook_event_name": "PreConfigChange", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/pre-config-change-delete.json b/fixtures/hook-event/valid/pre-config-change-delete.json index cb96054..1d053b7 100644 --- a/fixtures/hook-event/valid/pre-config-change-delete.json +++ b/fixtures/hook-event/valid/pre-config-change-delete.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "df08a46f-f71d-56a6-87a4-e99cff8b5414", "hook_event_name": "PreConfigChange", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/pre-config-change-update-null.json b/fixtures/hook-event/valid/pre-config-change-update-null.json index b12d99f..d9c83ea 100644 --- a/fixtures/hook-event/valid/pre-config-change-update-null.json +++ b/fixtures/hook-event/valid/pre-config-change-update-null.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "4b215ef9-6e0c-575a-926b-5bff9396ce5a", "hook_event_name": "PreConfigChange", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/pre-config-change-update.json b/fixtures/hook-event/valid/pre-config-change-update.json index bf4d183..b5fd8cb 100644 --- a/fixtures/hook-event/valid/pre-config-change-update.json +++ b/fixtures/hook-event/valid/pre-config-change-update.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "36006c70-281a-5e27-b302-f30619897899", "hook_event_name": "PreConfigChange", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/pre-memory-write.json b/fixtures/hook-event/valid/pre-memory-write.json index 7cc7f2c..f58edaf 100644 --- a/fixtures/hook-event/valid/pre-memory-write.json +++ b/fixtures/hook-event/valid/pre-memory-write.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "c796687b-7acd-5b87-8701-7162382ec68d", "hook_event_name": "PreMemoryWrite", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/pre-network-access.json b/fixtures/hook-event/valid/pre-network-access.json index 9253e70..fdfa273 100644 --- a/fixtures/hook-event/valid/pre-network-access.json +++ b/fixtures/hook-event/valid/pre-network-access.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "7b42b9ef-a080-53f1-948a-72305405be7e", "hook_event_name": "PreNetworkAccess", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/pre-tool-use-wire-signing.json b/fixtures/hook-event/valid/pre-tool-use-wire-signing.json index a52cbdf..de95790 100644 --- a/fixtures/hook-event/valid/pre-tool-use-wire-signing.json +++ b/fixtures/hook-event/valid/pre-tool-use-wire-signing.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "8f2ab3e1-4c5d-4e6f-8a9b-0c1d2e3f4a5b", "hook_event_name": "PreToolUse", "session_id": "sess_production_9981", diff --git a/fixtures/hook-event/valid/session-ended.json b/fixtures/hook-event/valid/session-ended.json index d0f50a7..7698cb5 100644 --- a/fixtures/hook-event/valid/session-ended.json +++ b/fixtures/hook-event/valid/session-ended.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "135c51e7-1ae2-4a87-a00f-dab80b3779d8", "hook_event_name": "SessionEnd", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/session-start.json b/fixtures/hook-event/valid/session-start.json index 59f5f5f..ba2c6e7 100644 --- a/fixtures/hook-event/valid/session-start.json +++ b/fixtures/hook-event/valid/session-start.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "862a966f-6f7b-4c15-a8c5-40df353eeaac", "hook_event_name": "SessionStart", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/stop.json b/fixtures/hook-event/valid/stop.json index 9e445ec..5f1b58d 100644 --- a/fixtures/hook-event/valid/stop.json +++ b/fixtures/hook-event/valid/stop.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "dac1a092-1121-4e95-a542-b3dccb0a2f4b", "hook_event_name": "Stop", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/subagent-start-with-delegation.json b/fixtures/hook-event/valid/subagent-start-with-delegation.json index 75274bb..7f8ef03 100644 --- a/fixtures/hook-event/valid/subagent-start-with-delegation.json +++ b/fixtures/hook-event/valid/subagent-start-with-delegation.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "4ceaa9fe-0d13-45ab-b3a0-303f2e7757a9", "hook_event_name": "SubagentStart", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/subagent-start.json b/fixtures/hook-event/valid/subagent-start.json index 984cc87..7060480 100644 --- a/fixtures/hook-event/valid/subagent-start.json +++ b/fixtures/hook-event/valid/subagent-start.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "4ceaa9fe-0d13-45ab-b3a0-303f2e7757a9", "hook_event_name": "SubagentStart", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/subagent-stop.json b/fixtures/hook-event/valid/subagent-stop.json index 540c6d0..d9c002c 100644 --- a/fixtures/hook-event/valid/subagent-stop.json +++ b/fixtures/hook-event/valid/subagent-stop.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "f9c74d10-f408-448d-9579-7ddab4245388", "hook_event_name": "SubagentStop", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/tool-pre.json b/fixtures/hook-event/valid/tool-pre.json index 726a6d5..e9e9b8c 100644 --- a/fixtures/hook-event/valid/tool-pre.json +++ b/fixtures/hook-event/valid/tool-pre.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "36c2b982-1d4c-4dc2-ae5b-a65139601741", "hook_event_name": "PreToolUse", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/user-prompt-submit.json b/fixtures/hook-event/valid/user-prompt-submit.json index 33cfda7..6905de8 100644 --- a/fixtures/hook-event/valid/user-prompt-submit.json +++ b/fixtures/hook-event/valid/user-prompt-submit.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "6edab8d4-1bb3-4c37-bd6c-7ad7da707b7a", "hook_event_name": "UserPromptSubmit", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/vendor-event.json b/fixtures/hook-event/valid/vendor-event.json index 8b62b88..2f4f374 100644 --- a/fixtures/hook-event/valid/vendor-event.json +++ b/fixtures/hook-event/valid/vendor-event.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "6706d7e5-8efa-49f2-a8a1-4b19c0b216c8", "hook_event_name": "x-example/PromptExpanded", "session_id": "session-42", diff --git a/fixtures/hook-event/valid/vendor-session-revoke.json b/fixtures/hook-event/valid/vendor-session-revoke.json index 28c1384..6a36580 100644 --- a/fixtures/hook-event/valid/vendor-session-revoke.json +++ b/fixtures/hook-event/valid/vendor-session-revoke.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "9f3bc4e2-5d6e-4f7a-9b0c-1d2e3f4a5b6c", "hook_event_name": "x-nemo/SessionRevoke", "session_id": "sess_production_9981", diff --git a/fixtures/hook-response/invalid/colliding-legacy-spec-identifier.json b/fixtures/hook-response/invalid/colliding-legacy-spec-identifier.json new file mode 100644 index 0000000..32d2486 --- /dev/null +++ b/fixtures/hook-response/invalid/colliding-legacy-spec-identifier.json @@ -0,0 +1,11 @@ +{ + "spec": "agent-hooks/0.1", + "event_id": "7a83870b-fd4b-4f44-a49e-cc2e2511e5ff", + "metadata": { + "audit_id": "audit-17", + "latency_ms": 2.4, + "triggered_rules": [ + "tool-audit" + ] + } +} diff --git a/fixtures/hook-response/invalid/deny-without-reason.json b/fixtures/hook-response/invalid/deny-without-reason.json index 65221a2..f577b6d 100644 --- a/fixtures/hook-response/invalid/deny-without-reason.json +++ b/fixtures/hook-response/invalid/deny-without-reason.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "36c2b982-1d4c-4dc2-ae5b-a65139601741", "decision": "block" } diff --git a/fixtures/hook-response/invalid/invalid-version.json b/fixtures/hook-response/invalid/invalid-version.json index df33f49..122658f 100644 --- a/fixtures/hook-response/invalid/invalid-version.json +++ b/fixtures/hook-response/invalid/invalid-version.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.2", + "spec": "agent-hook-unity/0.2", "event_id": "36c2b982-1d4c-4dc2-ae5b-a65139601741", "hookSpecificOutput": { "hookEventName": "PreToolUse", diff --git a/fixtures/hook-response/invalid/missing-event-id.json b/fixtures/hook-response/invalid/missing-event-id.json index 90ea11e..b18bc54 100644 --- a/fixtures/hook-response/invalid/missing-event-id.json +++ b/fixtures/hook-response/invalid/missing-event-id.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "allow" diff --git a/fixtures/hook-response/invalid/unknown-decision-top-level.json b/fixtures/hook-response/invalid/unknown-decision-top-level.json index 102157a..0123a61 100644 --- a/fixtures/hook-response/invalid/unknown-decision-top-level.json +++ b/fixtures/hook-response/invalid/unknown-decision-top-level.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "36c2b982-1d4c-4dc2-ae5b-a65139601741", "decision": "maybe" } diff --git a/fixtures/hook-response/invalid/unknown-decision.json b/fixtures/hook-response/invalid/unknown-decision.json index e9bdfe0..a683e03 100644 --- a/fixtures/hook-response/invalid/unknown-decision.json +++ b/fixtures/hook-response/invalid/unknown-decision.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "36c2b982-1d4c-4dc2-ae5b-a65139601741", "hookSpecificOutput": { "hookEventName": "PreToolUse", diff --git a/fixtures/hook-response/valid/after-model-response-updated-response.json b/fixtures/hook-response/valid/after-model-response-updated-response.json index 673eea6..b0b8a2a 100644 --- a/fixtures/hook-response/valid/after-model-response-updated-response.json +++ b/fixtures/hook-response/valid/after-model-response-updated-response.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "b3e040aa-f94d-446a-8b83-a9ea44047a01", "decision": "allow", "hookSpecificOutput": { diff --git a/fixtures/hook-response/valid/allow.json b/fixtures/hook-response/valid/allow.json index b2ffda3..3d07c02 100644 --- a/fixtures/hook-response/valid/allow.json +++ b/fixtures/hook-response/valid/allow.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "36c2b982-1d4c-4dc2-ae5b-a65139601741", "hookSpecificOutput": { "hookEventName": "PreToolUse", diff --git a/fixtures/hook-response/valid/ask-with-hitl-suspension.json b/fixtures/hook-response/valid/ask-with-hitl-suspension.json index 7633346..f5e0843 100644 --- a/fixtures/hook-response/valid/ask-with-hitl-suspension.json +++ b/fixtures/hook-response/valid/ask-with-hitl-suspension.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/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/ask.json b/fixtures/hook-response/valid/ask.json index 2b00c5a..98215f2 100644 --- a/fixtures/hook-response/valid/ask.json +++ b/fixtures/hook-response/valid/ask.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "36c2b982-1d4c-4dc2-ae5b-a65139601741", "hookSpecificOutput": { "hookEventName": "PreToolUse", diff --git a/fixtures/hook-response/valid/before-model-request.json b/fixtures/hook-response/valid/before-model-request.json index 388f7b8..90a27f1 100644 --- a/fixtures/hook-response/valid/before-model-request.json +++ b/fixtures/hook-response/valid/before-model-request.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "7a83870b-fd4b-4f44-a49e-cc2e2511e5ff", "hookSpecificOutput": { "hookEventName": "BeforeModelRequest", diff --git a/fixtures/hook-response/valid/bounded-open-degradation-policy.json b/fixtures/hook-response/valid/bounded-open-degradation-policy.json index 738566c..fd11f45 100644 --- a/fixtures/hook-response/valid/bounded-open-degradation-policy.json +++ b/fixtures/hook-response/valid/bounded-open-degradation-policy.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "b4e2d194-3f6e-4fe4-8f7d-c87351823963", "decision": "allow", "extensions": { diff --git a/fixtures/hook-response/valid/deny.json b/fixtures/hook-response/valid/deny.json index 931b829..fc25ffb 100644 --- a/fixtures/hook-response/valid/deny.json +++ b/fixtures/hook-response/valid/deny.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "36c2b982-1d4c-4dc2-ae5b-a65139601741", "hookSpecificOutput": { "hookEventName": "PreToolUse", diff --git a/fixtures/hook-response/valid/fail-closed-degradation-policy.json b/fixtures/hook-response/valid/fail-closed-degradation-policy.json index 65fbb57..3cc9c2c 100644 --- a/fixtures/hook-response/valid/fail-closed-degradation-policy.json +++ b/fixtures/hook-response/valid/fail-closed-degradation-policy.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "47d3c093-2e5d-4ed3-bf6c-b76240712852", "decision": "deny", "reason": "Security policy requires fail-closed enforcement on uninspected tool execution during PDP timeout.", diff --git a/fixtures/hook-response/valid/generic-allow-decision.json b/fixtures/hook-response/valid/generic-allow-decision.json index df33b75..090ceee 100644 --- a/fixtures/hook-response/valid/generic-allow-decision.json +++ b/fixtures/hook-response/valid/generic-allow-decision.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "36c2b982-1d4c-4dc2-ae5b-a65139601741", "decision": "allow" } diff --git a/fixtures/hook-response/valid/hitl-resumption-allow.json b/fixtures/hook-response/valid/hitl-resumption-allow.json index a2a44fc..c3f8bb6 100644 --- a/fixtures/hook-response/valid/hitl-resumption-allow.json +++ b/fixtures/hook-response/valid/hitl-resumption-allow.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "36c2b982-1d4c-4dc2-ae5b-a65139601741", "decision": "allow", "reason": "Approved by security administrator Alice.", diff --git a/fixtures/hook-response/valid/observational.json b/fixtures/hook-response/valid/observational.json index 32d2486..49109ff 100644 --- a/fixtures/hook-response/valid/observational.json +++ b/fixtures/hook-response/valid/observational.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "7a83870b-fd4b-4f44-a49e-cc2e2511e5ff", "metadata": { "audit_id": "audit-17", diff --git a/fixtures/hook-response/valid/permission-request.json b/fixtures/hook-response/valid/permission-request.json index ae45e1c..94a090e 100644 --- a/fixtures/hook-response/valid/permission-request.json +++ b/fixtures/hook-response/valid/permission-request.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "e627aa91-5e3a-410c-8f83-d0723e863c6f", "hookSpecificOutput": { "hookEventName": "PermissionRequest", diff --git a/fixtures/hook-response/valid/post-tool-use-updated-output.json b/fixtures/hook-response/valid/post-tool-use-updated-output.json index 5a81a46..bf0eb2e 100644 --- a/fixtures/hook-response/valid/post-tool-use-updated-output.json +++ b/fixtures/hook-response/valid/post-tool-use-updated-output.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "c1f0b6e2-57cb-4b72-a083-d5d2c25e834b", "decision": "allow", "hookSpecificOutput": { diff --git a/fixtures/hook-response/valid/pre-config-change-ask.json b/fixtures/hook-response/valid/pre-config-change-ask.json index 6670160..1968608 100644 --- a/fixtures/hook-response/valid/pre-config-change-ask.json +++ b/fixtures/hook-response/valid/pre-config-change-ask.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "36006c70-281a-5e27-b302-f30619897899", "hookSpecificOutput": { "hookEventName": "PreConfigChange", diff --git a/fixtures/hook-response/valid/pre-memory-write-ask.json b/fixtures/hook-response/valid/pre-memory-write-ask.json index abcbf60..277e439 100644 --- a/fixtures/hook-response/valid/pre-memory-write-ask.json +++ b/fixtures/hook-response/valid/pre-memory-write-ask.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "c796687b-7acd-5b87-8701-7162382ec68d", "hookSpecificOutput": { "hookEventName": "PreMemoryWrite", diff --git a/fixtures/hook-response/valid/pre-memory-write-updated-content.json b/fixtures/hook-response/valid/pre-memory-write-updated-content.json index 8a06dce..cfd2e64 100644 --- a/fixtures/hook-response/valid/pre-memory-write-updated-content.json +++ b/fixtures/hook-response/valid/pre-memory-write-updated-content.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "18f9d0c2-9e8a-441f-a2e1-456cbaf11002", "decision": "allow", "hookSpecificOutput": { diff --git a/fixtures/hook-response/valid/pre-network-access-ask.json b/fixtures/hook-response/valid/pre-network-access-ask.json index 86aa0e0..cea2078 100644 --- a/fixtures/hook-response/valid/pre-network-access-ask.json +++ b/fixtures/hook-response/valid/pre-network-access-ask.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "7b42b9ef-a080-53f1-948a-72305405be7e", "hookSpecificOutput": { "hookEventName": "PreNetworkAccess", diff --git a/fixtures/hook-response/valid/session-start-allow.json b/fixtures/hook-response/valid/session-start-allow.json index feb63f8..4a48af4 100644 --- a/fixtures/hook-response/valid/session-start-allow.json +++ b/fixtures/hook-response/valid/session-start-allow.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/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 index f755168..41b9806 100644 --- a/fixtures/hook-response/valid/subagent-start-deny.json +++ b/fixtures/hook-response/valid/subagent-start-deny.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/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 index 036ce79..174ef2d 100644 --- a/fixtures/hook-response/valid/top-level-allow.json +++ b/fixtures/hook-response/valid/top-level-allow.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/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 index 02843ec..ec16453 100644 --- a/fixtures/hook-response/valid/top-level-ask.json +++ b/fixtures/hook-response/valid/top-level-ask.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/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 index bee045f..72cc23e 100644 --- a/fixtures/hook-response/valid/top-level-deny.json +++ b/fixtures/hook-response/valid/top-level-deny.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/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-block.json b/fixtures/hook-response/valid/user-prompt-submit-block.json index 1babcd0..f8eab77 100644 --- a/fixtures/hook-response/valid/user-prompt-submit-block.json +++ b/fixtures/hook-response/valid/user-prompt-submit-block.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "260d8de3-2b56-48ed-914f-3d8d446cb9e0", "decision": "block", "reason": "The submitted prompt violates the workspace policy." diff --git a/fixtures/hook-response/valid/user-prompt-submit-deny.json b/fixtures/hook-response/valid/user-prompt-submit-deny.json index fcd0b3d..cf19ae7 100644 --- a/fixtures/hook-response/valid/user-prompt-submit-deny.json +++ b/fixtures/hook-response/valid/user-prompt-submit-deny.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/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 index 92e57c4..c342edf 100644 --- a/fixtures/hook-response/valid/user-prompt-submit-updated-prompt.json +++ b/fixtures/hook-response/valid/user-prompt-submit-updated-prompt.json @@ -1,5 +1,5 @@ { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "260d8de3-2b56-48ed-914f-3d8d446cb9e0", "decision": "allow", "hookSpecificOutput": { diff --git a/package.json b/package.json index 0c22ceb..cf98dbd 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "version": "0.0.0", "description": "The community home for the Agent Hook Spec.", "scripts": { - "validate": "node scripts/validate.mjs && node scripts/validate-image-size-patch.mjs", + "test": "node --test scripts/validate.test.mjs", + "validate": "npm test && node scripts/validate.mjs && node scripts/validate-image-size-patch.mjs", "build": "npm --prefix website run build", "start": "npm --prefix website run start" }, diff --git a/rfcs/0001-agent-hook-core-event-contract.md b/rfcs/0001-agent-hook-core-event-contract.md index 3d323c1..0c8d1f8 100644 --- a/rfcs/0001-agent-hook-core-event-contract.md +++ b/rfcs/0001-agent-hook-core-event-contract.md @@ -1,9 +1,9 @@ --- title: "RFC 0001: Agent Hook 0.1 Core Event Contract" status: Draft -discussion: "TBD — GitHub Discussion required before formal review" -review-start: TBD -review-end: TBD +discussion: "Pending — prerequisite Discussion has not been opened" +review-start: "Not started" +review-end: "Not scheduled" maintainer-votes: [] decision: "Pending" supersedes: [] @@ -14,6 +14,14 @@ superseded-by: [] ## Summary +**Historical proposal baseline:** The original proposal below describes 13 +events and the earlier event-specific response model. Subsequent draft changes +are consolidated by [RFC 0007](./0007-core-draft-consolidation.md), including +18 events, top-level decisions, deny preservation, and the distinct +`agent-hook-unity/0.1` identity. Follow the current canonical draft for that +candidate's requirements. This RFC remains unaccepted; links to current files +do not retroactively change this original proposal's review status. + This RFC proposes Agent Hook 0.1, a portable JSON contract for lifecycle-hook events and hook responses. It defines a native envelope, a 13-event Core registry for security telemetry, decision semantics, JSON Schemas, fixtures, diff --git a/rfcs/0004-standard-lifecycle-events.md b/rfcs/0004-standard-lifecycle-events.md index 2678397..ee7a35c 100644 --- a/rfcs/0004-standard-lifecycle-events.md +++ b/rfcs/0004-standard-lifecycle-events.md @@ -1,7 +1,7 @@ --- title: "RFC 0004: Standard network, memory, and configuration lifecycle events" status: Draft -discussion: "Pending — repository Discussions are not enabled" +discussion: "Pending — prerequisite Discussion has not been opened" review-start: "Not started" review-end: "Not scheduled" maintainer-votes: [] @@ -14,6 +14,13 @@ superseded-by: [] ## Summary +**Historical proposal baseline:** The event table below preserves this +proposal's original classifications, including Observe-only +`PostNetworkAccess`. PR #9 subsequently changed the working draft, and +[RFC 0007](./0007-core-draft-consolidation.md) records the resulting candidate +baseline and identity migration. This RFC remains unaccepted; its original +proposal does not override the current canonical candidate. + Extend the Agent Hook 0.1 draft's single standard event registry with `PreNetworkAccess`, `PostNetworkAccess`, `PreMemoryWrite`, `PostMemoryWrite`, and `PreConfigChange`. This adds five events to the thirteen proposed by @@ -26,8 +33,9 @@ capability declarations. A host claiming support must implement the declared event faithfully; no separate optional-event namespace is introduced. This is a proposal, not an accepted specification. Formal review has not -started. Repository Discussions must be enabled and a prior Discussion linked -before the formal review window begins under [GOVERNANCE.md](../GOVERNANCE.md). +started. Repository Discussions was enabled on 2026-09-21; a prior Discussion +must still be linked before the formal review window begins under +[GOVERNANCE.md](../GOVERNANCE.md). ## Motivation @@ -53,7 +61,7 @@ appear in all capitals. Event requirements apply to hosts declaring the event `gate` or `observe`; `partial` and `unavailable` MUST NOT be presented as faithful standard-event deliveries. -The existing flat `spec: "agent-hooks/0.1"` envelope, delivery identifiers, +The original flat `spec: "agent-hooks/0.1"` envelope, delivery identifiers, session ordering, redaction rules, and native policy authority are preserved. No runtime, transport, handler discovery, capability negotiation, signature, ledger, approval gateway, or managed session revocation is specified here. diff --git a/rfcs/0005-network-response-delivery-inspection.md b/rfcs/0005-network-response-delivery-inspection.md index d459acd..2b4fa0d 100644 --- a/rfcs/0005-network-response-delivery-inspection.md +++ b/rfcs/0005-network-response-delivery-inspection.md @@ -1,7 +1,7 @@ --- title: "RFC 0005: Inspect and control response content with PostNetworkAccess" status: Draft -discussion: "Pending — repository Discussions are not enabled" +discussion: "Pending — prerequisite Discussion has not been opened" review-start: "Not started" review-end: "Not scheduled" maintainer-votes: [] @@ -28,15 +28,18 @@ access it. A Post handler can allow the body, allow a replacement body, or deny delivery. A host that only reports network results retains `observe` behavior. The Core fail-open default is preserved. -This is a proposal for a subsequent revision of the **unaccepted Agent Hook -0.1 draft**. Current main classifies `PostNetworkAccess` as Observe and requires -hosts to ignore its control responses. This RFC proposes changing that contract; -it does not change the canonical specification, schemas, or runtime behavior -in this PR. No new event name is proposed. +This remains a proposal against the **unaccepted Agent Hook Unity 0.1 draft**. +Since this RFC was introduced, PR #9 added buffered `PostNetworkAccess` +allow/deny delivery control to the working Core draft. Complete-body inspection, +recipient/body fields, and body replacement proposed here are still absent +from the canonical schemas. [RFC 0007](./0007-core-draft-consolidation.md) +records that partial adoption into the working draft and its pending review +status; it does not mark this RFC Accepted. No new event name is proposed. Formal review has not started. The prerequisite Discussion and review window required by [GOVERNANCE.md](../GOVERNANCE.md) remain pending; repository -Discussions are currently disabled. This Draft PR is preparatory material. +Discussions was enabled on 2026-09-21, but that does not start formal review. +This Draft is preparatory material. ## Motivation @@ -167,22 +170,27 @@ It MUST NOT rewrite the original network facts. ### 4. Event-specific control and body replacement Reuse the correlated response envelope: required `spec`, matching `event_id`, -and `hookSpecificOutput.hookEventName: "PostNetworkAccess"`. Delivery controls +and canonical top-level `decision`. A replacement additionally requires +`hookSpecificOutput.hookEventName: "PostNetworkAccess"`. Delivery controls apply only when the host declares `gate`, the event has `outcome: "success"`, and the complete body and recipient fields are present. -| `hookSpecificOutput.permissionDecision` | Effect | +| Top-level `decision` | Effect | | --- | --- | | `allow` | Permit delivery of the evaluated body, or the explicit replacement below, subject to independent native policies. | | `deny` | Withhold the body. The host may return a safe local refusal without exposing denied bytes. | | `ask` | Keep the body unavailable until the existing native approval flow resolves. A non-interactive host MUST treat this as `deny`. | | `defer` | Leave the decision to native approval or policy; this is not approval. | -`permissionDecisionReason` MAY explain the decision. Introduce one replacement +Top-level `reason` explains the decision and is required for `deny`. The +supported nested control fields remain a fallback only when top-level +`decision` is absent. `ask` and `defer` here are additional proposed Post +controls; the current Core delivery Gate supports only allow/deny. +Introduce one replacement member: **`hookSpecificOutput.updatedResponseBodyBase64`**. When present it MUST be a string containing valid standard padded base64 without whitespace. An empty string represents a deliberate empty replacement, not an omitted value. -Its replacement effect applies only with an explicit `permissionDecision: "allow"`. +Its replacement effect applies only with an explicit effective `allow` decision. With `deny`, `ask`, `defer`, or no decision, it has no replacement effect; in particular, a denial MUST NOT release replacement bytes. @@ -205,7 +213,7 @@ the original sensitive body. That host application failure is distinct from an invalid or missing handler response. This event gives no delivery-control or rewriting effect to `updatedInput`, -`updatedMessages`, top-level `decision`, or other event-inapplicable controls. It adds no +`updatedMessages`, or other event-inapplicable controls. It adds no `transform` or `quarantine` decision enum. A valid deny withholds the body; quarantine storage, retention, deletion, and any later release remain native responsibilities. Retaining denied content MUST NOT make it readable to the @@ -298,7 +306,8 @@ native permission boundary and all required identifiers are actually present. ## Illustrative exchange -These examples describe the **candidate semantics**, not current main behavior. +These examples include the **proposed body-inspection semantics**, beyond the +current candidate's buffered allow/deny delivery control. The existing schemas permit additional event and event-specific response members; structural validation does not mean an existing Observe host will apply them. @@ -307,7 +316,7 @@ has buffered it for one recipient and declares the revised Post `gate` contract: ```json { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "018f6c3a-9214-7abc-9f12-34567890ab01", "hook_event_name": "PostNetworkAccess", "session_id": "session-42", @@ -331,12 +340,12 @@ Option A: the handler allows replacement with the ten bytes `[REDACTED]`: ```json { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "018f6c3a-9214-7abc-9f12-34567890ab01", + "decision": "allow", + "reason": "Replace the sensitive text before delivery.", "hookSpecificOutput": { "hookEventName": "PostNetworkAccess", - "permissionDecision": "allow", - "permissionDecisionReason": "Replace the sensitive text before delivery.", "updatedResponseBodyBase64": "W1JFREFDVEVEXQ==" } } @@ -350,13 +359,10 @@ Alternatively, option B denies delivery of the same response: ```json { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "018f6c3a-9214-7abc-9f12-34567890ab01", - "hookSpecificOutput": { - "hookEventName": "PostNetworkAccess", - "permissionDecision": "deny", - "permissionDecisionReason": "This response must not be delivered." - } + "decision": "deny", + "reason": "This response must not be delivered." } ``` @@ -366,17 +372,20 @@ result remains success. No additional network event is needed in either case. ## Compatibility impact -This proposal keeps all 18 event names and reuses the existing envelope. It -**changes an existing Observe contract** to permit response-delivery control. -Current main requires controls on `PostNetworkAccess` to be ignored; adding -content or a permission decision alone does not enable this behavior. +This proposal keeps all 18 event names and reuses the existing envelope. Its +original baseline classified `PostNetworkAccess` as Observe. The current +working draft already permits buffered response-delivery allow/deny for a host +declaring `gate`; this proposal additionally introduces complete-body inspection +and replacement. Existing `observe` configurations continue to ignore controls. Adopters MUST update the event registry, schemas, capability declarations, and host/handler configuration together for the adopted revision. They MUST enable the revised Post contract only for explicitly configured compatible hosts and handlers. An old telemetry handler's response MUST NOT silently acquire control -authority. Keeping `spec: "agent-hooks/0.1"` during an unaccepted draft does not -establish compatibility or automatic negotiation. Existing Observe mappings +authority. The consolidation candidate uses `spec: "agent-hook-unity/0.1"`; +its identifier alone does not establish support for this further body-inspection +proposal. [RFC 0007](./0007-core-draft-consolidation.md) documents migration from +the former colliding identifier. Existing Observe mappings remain usable as Observe and gain no delivery-enforcement claim. If 0.1 is accepted before this proposal is decided, revisit versioning through @@ -401,14 +410,15 @@ authenticity. Default fail-open remains part of the proposed contract. | Alternative | Trade-off | | --- | --- | -| Add `BeforeNetworkResponseDelivery` | Preserves the existing Observe-only definition, but adds a third network event for a flow that the Pre/Post pair can cover. This proposal favors fewer event names and explicitly revises Post semantics. | -| Keep Post strictly observational and use a vendor event | Supports experimentation before adoption, but offers no shared content-control behavior across hosts. | +| Add `BeforeNetworkResponseDelivery` | Would preserve the original Observe-only baseline, but adds a third network event for a flow that the current Pre/Post pair already covers. | +| Keep full-body inspection in a vendor event | Supports experimentation before adoption, but offers no shared body-inspection and replacement behavior across hosts. | | Inspect each streaming chunk | Reduces buffering latency but requires cross-chunk, cancellation, and already-delivered-content rules. It is outside this full-body proposal. | | Reuse `updatedInput` or add `transform`/`quarantine` decisions | Obscures whether input or response bytes change, or mixes delivery decisions with storage policy. An explicit body replacement field plus existing permission decisions is sufficient. | ## Follow-up implementation and acceptance criteria -After acceptance, a follow-up PR must update `spec/0.1/events.md` (including +After acceptance, a follow-up PR for the remaining body-inspection and +replacement features must update `spec/0.1/events.md` (including the registry and shared network/memory terminal rules), `core.md` response and capability rules, adapter/security guidance, root and published schemas, fixtures, and examples together. `PostMemoryWrite` stays Observe. The event @@ -459,4 +469,5 @@ This RFC does not claim vendor acceptance, co-authorship, or implementation. Pending. The prerequisite Discussion, at least 14 calendar days of public review, and maintainer decision remain outstanding under repository governance. -RFC 0004 remains the current definition until an adopted revision changes it. +The working draft includes later changes described by RFC 0007; their presence +does not accept this RFC's remaining body-inspection and replacement proposal. diff --git a/rfcs/0006-enterprise-security-extensions-strategy.md b/rfcs/0006-enterprise-security-extensions-strategy.md index 97555d6..1a3765b 100644 --- a/rfcs/0006-enterprise-security-extensions-strategy.md +++ b/rfcs/0006-enterprise-security-extensions-strategy.md @@ -1,7 +1,7 @@ --- title: "RFC 0006: Enterprise Security Extensions Strategy & Non-Core Capabilities" status: Draft -discussion: "Pending — repository Discussions are not enabled" +discussion: "Pending — prerequisite Discussion has not been opened" review-start: "Not started" review-end: "Not scheduled" maintainer-votes: [] @@ -16,9 +16,21 @@ superseded-by: [] This RFC proposes a standardized architectural strategy and recommended extension profiles for enterprise-grade security capabilities within the Agent Hook ecosystem. +This is an unaccepted proposal. Its top-level `decision` baseline was already +introduced by PR #9 before this RFC; the enterprise profiles below did not +introduce that Core response shape. [RFC 0007](./0007-core-draft-consolidation.md) +records the consolidated working baseline and migration to +`agent-hook-unity/0.1`, including the corresponding signing-domain update in +this draft. The profiles remain optional proposals and have not been accepted +or certified by their appearance in this repository. + To preserve the minimalism, zero-dependency, and lightweight nature of the **Agent Hook Core 0.1 Specification**, heavy enterprise defense features—such as cryptographic wire signing (Ed25519/TPM), tamper-evident audit ledgers (hash-chaining), asynchronous Human-in-the-Loop (HITL) suspension, Time-of-Check to Time-of-Use (TOCTOU) payload verification, and out-of-band administrative session revocation—are explicitly designated as **optional, non-core extension profiles**. -These profiles leverage the existing standard `extensions` container defined in [`spec/0.1/extensions.md`](../spec/0.1/extensions.md). Conforming implementations are free to adopt, customize, or omit these extensions without breaking Core 0.1 interoperability. +These profiles use the existing `extensions` container defined in +[`spec/0.1/extensions.md`](../spec/0.1/extensions.md). Unknown extension data can +be ignored without changing Core schema compatibility. Interoperability and +enforcement for a profile require explicit agreement on its version and +semantics; independently customized profiles do not imply compatibility. --- @@ -40,7 +52,11 @@ Forcing heavy enterprise armor into the **Core 0.1 normative specification** wou > **"Core does subtraction (protecting a universal minimal baseline); Extensions do addition (mounting modular enterprise armor on demand)."** -By establishing a standardized yet strictly optional **Enterprise Security Extension Profile**, this RFC provides a common blueprint for high-security implementations (such as PEP proxies like NVIDIA NeMo Guardrails/Relay and Policy Decision Points like Trend Micro Vision One) while guaranteeing 100% interoperability with lightweight Core 0.1 agents. +This RFC proposes optional enterprise profiles for implementations such as PEP +proxies and policy decision points. Lightweight agents can retain Core schema +compatibility while ignoring those extensions, but profile-specific guarantees +require matching implementations and explicit configuration on the participating +hosts. No vendor adoption or interoperability certification is asserted. --- @@ -114,7 +130,7 @@ Used to guarantee message authenticity and provenance between Agent, PEP (Relay) ##### Request / Response Example ```json { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "8f2ab3e1-4c5d-4e6f-8a9b-0c1d2e3f4a5b", "hook_event_name": "PreToolUse", "session_id": "sess_production_9981", @@ -159,9 +175,9 @@ To ensure consistent interoperability across distinct runtime languages (Python, - The self-referential signature property (`extensions["sec.enterprise.crypto"].signature`) and `canonical_hash` (if present) MUST be excluded prior to canonicalization. 3. **Domain Separator & Preimage**: - The preimage MUST be prefixed with a strict profile/version domain separation string: - `agent-hooks/0.1:sec.enterprise.crypto:v1\n` + `agent-hook-unity/0.1:sec.enterprise.crypto:v1\n` - The complete byte sequence for signing and verification is: - $$\text{PREIMAGE\_BYTES} = \text{"agent-hooks/0.1:sec.enterprise.crypto:v1\n"} \,||\, \text{JCS}(\text{payload\_without\_sig})$$ + $$\text{PREIMAGE\_BYTES} = \text{"agent-hook-unity/0.1:sec.enterprise.crypto:v1\n"} \,||\, \text{JCS}(\text{payload\_without\_sig})$$ - `canonical_hash` is computed as `:` over $\text{PREIMAGE\_BYTES}$. 4. **Replay & Freshness Binding**: - The payload MUST include a valid ISO-8601 `timestamp` and a unique UUID `event_id`. @@ -218,7 +234,7 @@ Standardizes asynchronous human intervention when a Policy Decision Point return ##### PDP Response with Suspension Challenge ```json { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "36c2b982-1d4c-4dc2-ae5b-a65139601741", "decision": "ask", "reason": "Execution of bash shell with root privilege requires administrator sign-off.", @@ -238,7 +254,7 @@ Standardizes asynchronous human intervention when a Policy Decision Point return When human approval resolves out-of-band, the enterprise PDP or callback service delivers an asynchronous correlated response matching the original `event_id` (`36c2b982-1d4c-4dc2-ae5b-a65139601741`) to the host or PEP resumption endpoint: ```json { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "36c2b982-1d4c-4dc2-ae5b-a65139601741", "decision": "allow", "reason": "Approved by security administrator Alice.", @@ -429,7 +445,7 @@ When `mode` is `"bounded_open"`, the host or PEP MUST implement the circuit brea - In distributed deployments, the host MAY accept degradation policies provisioned dynamically by an authorized Policy Administration Point (PAP) or Policy Decision Point (PDP) via an authenticated control-plane channel (e.g. mTLS or cryptographically signed policy bundle). - **Precedence & Security Invariant**: An ordinary, unauthenticated hook handler responding to tool or lifecycle events MUST NOT be permitted to downgrade or overwrite an administrator's degradation policy (e.g., a failing handler cannot unilaterally switch the host from `strict_fail_closed` to `fail_open_monitored`). Hook responses MAY only report policy state or request a degradation policy if the issuer is explicitly authenticated as possessing administrative policy authority. 2. **Bootstrap Behavior**: - - When an agent host boots with no preconfigured degradation policy and no cached policy from an authorized PAP, it defaults to the Core 0.1 baseline (fail-open for handler errors with standard error logging), unless booted in an `enterprise-strict` profile which defaults to `strict_fail_closed` for all mutating gates. + - When an agent host boots with no preconfigured degradation policy and no cached policy from an authorized PAP, it defaults to the Core 0.1 baseline: a failed invocation supplies no decision, other applicable decisions and native policy remain effective, and minimal diagnostics are recommended. An explicitly configured `enterprise-strict` profile instead defaults to `strict_fail_closed` for all mutating gates. 3. **Persistence, Expiry, Replacement, and Revocation**: - Policies dynamically provisioned by an authorized PAP MAY declare `ttl_seconds` or `expires_at`. Upon expiration, the host evicts the cached policy and falls back to host bootstrap defaults. - An administrator or authorized PAP MAY revoke or replace a degradation policy at any time via control-plane push or administrative event (`x-nemo/SessionRevoke`), which takes effect immediately for all subsequent gate evaluations. @@ -469,7 +485,7 @@ Implementations MAY support administrative revocation through either: ```json { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "9f3bc4e2-5d6e-4f7a-9b0c-1d2e3f4a5b6c", "hook_event_name": "x-nemo/SessionRevoke", "session_id": "sess_production_9981", @@ -523,8 +539,15 @@ Upon receiving a valid revocation command, the PEP/Host MUST: ## Compatibility Impact -- **Core 0.1 Compatibility**: **100% Compatible**. All mechanisms defined in this RFC reside inside the `extensions` dictionary or out-of-band endpoints. No Core schema fields or mandatory behaviors are altered. -- **Backward Compatibility**: Existing agents that do not understand these extensions continue to function normally. Gate decisions (`allow`, `deny`, `ask`, `defer`) remain in their canonical top-level format. +- **Core 0.1 Compatibility**: These proposed profiles use `extensions` or + out-of-band endpoints and add no mandatory Core dependency. Enforcement + guarantees require explicit agreement and implementation by participating + hosts; ignoring extension data does not provide the profile's guarantees. +- **Draft migration**: Hosts and handlers must coordinate the + `agent-hook-unity/0.1` identity change described by RFC 0007. Signed payloads, + domain separators, and content-bound approvals require migration as well. + Within that declared contract, existing supported nested controls remain a + fallback only when the canonical top-level decision is absent. --- diff --git a/rfcs/0007-core-draft-consolidation.md b/rfcs/0007-core-draft-consolidation.md new file mode 100644 index 0000000..ddca372 --- /dev/null +++ b/rfcs/0007-core-draft-consolidation.md @@ -0,0 +1,216 @@ +--- +title: "RFC 0007: Consolidate the Core draft and distinguish its wire identity" +status: Draft +discussion: "Pending — prerequisite Discussion has not been opened" +review-start: "Not started" +review-end: "Not scheduled" +maintainer-votes: [] +decision: "Pending" +supersedes: [] +superseded-by: [] +--- + +# RFC 0007: Consolidate the Core draft and distinguish its wire identity + +## Summary + +Establish one reviewable working baseline for Agent Hook Unity 0.1: 18 Core +events (12 Gate and 6 Observe), canonical top-level decisions with the existing +nested fallback, a minimum cross-handler deny-preservation obligation, and the +distinct wire identifier `agent-hook-unity/0.1`. + +The accompanying specification, schemas, examples, and website implement this +**candidate draft baseline for review**. Their presence on a branch or on main +does not constitute RFC acceptance or an active standard. This RFC remains +Draft until the Discussion, review window, and maintainer votes required by +[governance](../GOVERNANCE.md) are recorded. No existing RFC is declared +Accepted or Superseded by this preparatory change. + +## Motivation and provenance + +The baseline before this consolidation is commit +[`08ecf2973ed8a3fb7b09f7a8f3a9450f2ea206b3`](https://github.com/trendmicro/agent-hook-unity/tree/08ecf2973ed8a3fb7b09f7a8f3a9450f2ea206b3). +Its normative pages, introductory pages, adapter guidance, and proposal status +described different contracts. + +| Source | Contribution and disposition in this candidate | +| --- | --- | +| [RFC 0001](./0001-agent-hook-core-event-contract.md) | Original flat event and correlated response proposal; remains unaccepted. | +| [RFC 0004](./0004-standard-lifecycle-events.md) | Network, memory, and configuration event expansion to 18 events; remains unaccepted. | +| [PR #9](https://github.com/trendmicro/agent-hook-unity/pull/9), commit `ee25d77` | Introduced canonical top-level decisions, added five Gate classifications, and revised payload rewrites. This preceded RFC 0006. Its resulting registry is the candidate's 12 Gate / 6 Observe baseline. | +| [RFC 0005](./0005-network-response-delivery-inspection.md) | Buffered `PostNetworkAccess` allow/deny delivery control is already in the working Core draft. Complete-body inspection, recipient/body fields, and body replacement remain proposals and are not introduced here. Its narrow deny-preservation rule informs the generic Core rule below. | +| [RFC 0006](./0006-enterprise-security-extensions-strategy.md) | Proposes optional enterprise extension profiles on top of the existing top-level response baseline; it did not originate that response shape. No enterprise profile becomes mandatory or accepted here. | +| This RFC | Consolidates the candidate baseline, adds the generic deny invariant, disambiguates identity, and records migration and review obligations. | + +The old summaries said seven Gate and eleven Observe events. They also treated +completed model and tool results as purely observational even after Core added +delivery and ingestion controls. Core itself listed native rewrite-validation +failure as unresolved after already requiring fail-closed behavior. These are +cross-document consistency defects, not alternative implementation choices. + +## Proposal + +### 1. One explicit candidate baseline + +The [event registry](../spec/0.1/events.md) defines event classification. The +[Core protocol](../spec/0.1/core.md) defines response and host obligations. +Consumer guidance must agree with those documents; schemas and examples must +use the same identity and supported fields. + +A registry Gate provides preventive control only where a host faithfully +declares `gate`. A host may expose that boundary as `observe` and must then +ignore control fields. Post Gates control result delivery, rendering, or +context ingestion; they cannot undo completed inference, tool execution, or +network side effects. `PostNetworkAccess` requires buffering for its delivery +Gate claim; this consolidation does not add response-body inspection fields. + +Top-level `decision` is canonical. Supported legacy nested control remains a +fallback only when the top-level decision is absent; the canonical examples +use the top-level form. This syntax compatibility is separate from the wire +identifier migration below. + +### 2. Minimum Core composition guarantee + +For one pending action at one boundary declared `gate`, an accepted valid, +correlated `deny` or applicable legacy `block` MUST remain effective across +other handler responses, missing/invalid responses, handler errors, timeouts, +redelivery, and reevaluation. Another handler's `allow` MUST NOT authorize the +denied action. Required unresolved approval and independent native policy +also continue to constrain release. + +The host associates decisions with its pending action and controlled boundary. +An `event_id` identifies a single handler delivery, so it cannot by itself +identify the cross-handler decision set. A new delivery ID does not clear a +denial; an unrelated operation or the separate Post boundary of an operation +does not inherit a Pre-boundary denial as a new independent policy decision. + +The host MUST NOT release the action while evaluations required by its own +declared dispatch policy remain pending before their deadlines. A declared +short-circuit policy may stop after a deny because the action is already +blocked. Ordering, sequential/parallel dispatch, discovery, deadlines, and +rewrite composition remain host-defined subject to the Core invariants. + +An invocation failure produces no decision from that invocation. Baseline +fail-open permits continuation only when no other applicable decision, +unresolved approval, or independent native policy blocks it. Thus an accepted +deny plus timeout is denied; timeout alone does not become a synthesized deny. +A late response after an invocation has expired cannot retroactively retract +an action. Hosts requiring successful inspection before release need an +explicit stricter policy. Failure must not erase an already applicable rewrite +and expose the unredacted original; schema-valid rewrites that fail native +validation retain the existing fail-closed requirement. + +The [composition conformance scenarios](../conformance/gate-composition.md) +describe observations that host integration tests must demonstrate. The +repository has no host runtime, and document checks do not execute these +scenarios or certify an implementation. + +### 3. Distinct wire and schema identity + +The canonical request and response `spec` value becomes +`agent-hook-unity/0.1`. This is an intentional incompatible identifier change +within an unaccepted draft; it is not a patch-compatible alias for the former +`agent-hooks/0.1` value. The new namespace starts at 0.1 and does not indicate +compatibility with another project's 0.1. + +Canonical schema identifiers and download locations are: + +- [Request schema](https://trendmicro.github.io/agent-hook-unity/schemas/0.1/hook-event.schema.json). +- [Response schema](https://trendmicro.github.io/agent-hook-unity/schemas/0.1/hook-response.schema.json). + +Previously, `$id` used the non-resolving `agent-hook-standard` Pages path. +JSON Schema identifies resources by URI and does not require network retrieval +from that URI. This change makes the new resource identity match its published +location; it does not silently reassign the old identity to a new contract. +The [schema migration guidance](../schemas/README.md) links immutable copies of +the previous schemas for deployments that must remain pinned. + +### 4. Interoperability direction + +The distinct [Responsible AI Agent Hooks contract](https://github.com/responsibleai/agent-hooks) +uses `agent-hooks/0.1`, eight interception points, and its own context, verdict, +composition, and fail-closed obligations. It is not wire-compatible with this +repository. Shared lifecycle concepts or similar names do not confer +conformance. Its [conformance claims](https://github.com/responsibleai/agent-hooks/blob/main/conformance/CLAIMS.md) +are declared-surface test results, not security certifications. + +A subsequent interoperability proposal should evaluate a telemetry and +correlation profile plus an adapter. It must cover: + +1. Mapping session, prompt, model-request, tool-call, operation, approval, and + delegation identities without conflating handler delivery with an action. +2. Mapping only equivalent control boundaries; documenting network, memory, + configuration, and other events without a faithful counterpart. +3. Preserving deny, approval, rewrite, buffering, and failure semantics. Our + fail-open default cannot be translated into an upstream conformant control + path merely by renaming fields. +4. Separating observational telemetry from enforced interception and recording + unsupported capabilities without fabricating events. +5. Running the applicable external conformance harness against a real adapter + and reporting the exact supported revision and declared surface. + +This RFC establishes the independent identity and evaluates that direction; it +does not deliver an adapter, assert external certification, or adopt a new +product positioning as an approved interoperability profile. + +## Compatibility impact and migration + +1. Pin the old schemas and the precise old draft revision if continued use of + `agent-hooks/0.1` is required. Do not resolve an incoming ambiguous identifier + by trying both projects' schemas until one happens to validate. +2. Upgrade host, adapter, and handler contract configuration together to + `agent-hook-unity/0.1`, the new schema IDs, and this candidate revision. +3. New canonical schemas reject the former identifier. An explicitly + configured legacy ingress may translate only when the old contract and + revision are known and the host can honor the new obligations; it must + validate each side and must not claim automatic negotiation. +4. Update signed payloads, signing domain separators, schema registrations, + and content-derived identities as applicable. Changing `spec` changes the + signed document; do not relabel an existing signature or approval grant. +5. Retain legacy nested response syntax only within the declared contract's + supported fallback rules. It does not make an old envelope identifier valid. +6. Revalidate Gate capability claims and execute the host behavioral scenarios. + A host exposing only observations must keep that declaration. + +## Security and privacy impact + +Deny preservation closes a composition ambiguity, but does not turn the +cooperative host contract into a sandbox or guarantee every operation was +successfully scanned. Baseline fail-open is unchanged when no applicable +blocking decision exists. Identity migration must not reinterpret another +project's documents or reuse approvals/signatures across changed content. +No new telemetry payload, enterprise dependency, or body-content collection is +required by this consolidation. + +## Alternatives considered + +| Alternative | Reason for this proposal | +| --- | --- | +| Fix only the event count | Leaves contradictory response, rewrite, failure, and proposal-status claims. | +| Standardize a complete handler runner | Adds scheduling and mutation algorithms beyond the required deny invariant. | +| Keep the colliding identifier with a disclaimer | Requires every integration to disambiguate identical version strings indefinitely. | +| Rename only the repository or site title | Does not distinguish wire payloads or schema resources. | +| Immediately replace the protocol with an external telemetry profile | Requires a tested semantic mapping and product decision that this consolidation does not establish. | +| Mark previously merged RFCs Accepted | Would invent review and voting decisions that have not occurred. | + +## Validation and adoption criteria + +- Canonical and published schema copies are identical; registry/schema event + sets, mirrored counts, and current JSON examples are checked together. +- New identity fixtures pass, and old colliding identity fixtures fail against + the new schemas. Legacy nested responses remain structurally supported. +- Documentation states the same Gate, response, failure, and draft status. +- A claiming host supplies behavioral evidence for the composition scenarios; + this repository's schema checks are not a substitute. +- The prerequisite Discussion is linked, the public review window lasts at + least 14 calendar days, and maintainer votes and disposition are recorded + before formal acceptance. Website and schema publication follows the normal + Pages deployment after merge; preparing these files does not publish them. + +## Decision record + +Pending. This preparatory RFC records the candidate and its provenance without +claiming a completed review, maintainer vote, or external endorsement. Final +acceptance must also record the disposition of overlapping portions of RFCs +0001, 0004, and 0005; RFC 0006's optional enterprise profiles remain a separate +proposal. diff --git a/rfcs/README.md b/rfcs/README.md index 9e4cdfa..e850010 100644 --- a/rfcs/README.md +++ b/rfcs/README.md @@ -20,12 +20,12 @@ See [GOVERNANCE.md](../GOVERNANCE.md) for review, voting, and status rules. ## Response inspection proposal [RFC 0005](./0005-network-response-delivery-inspection.md) proposes extending -`PostNetworkAccess` to inspect, replace, or withhold buffered response content -before an agent receives it. It keeps the Pre/Post pair and all 18 Core event -names, preserves Observe-only implementations and the fail-open default, and -requires explicit configuration for the revised control semantics. The draft -awaits the prerequisite Discussion and formal review; the current specification -and schemas do not yet implement it. +`PostNetworkAccess` to inspect and replace complete buffered response content +before an agent receives it. Buffered delivery allow/deny is already present in +the working Core draft through PR #9; the proposed body fields and replacement +are not implemented. The Pre/Post pair and all 18 event names remain, and +Observe configurations retain their behavior. The RFC remains Draft with its +prerequisite Discussion and formal review pending. ## Enterprise security extensions proposal @@ -38,3 +38,12 @@ and administrative session revocation). It preserves Core 0.1 minimalism and interoperability while offering standard integration blueprints for enterprise PEPs (e.g. NeMo Relay) and PDPs (e.g. Trend Micro Vision One). +## Consolidation candidate + +[RFC 0007](./0007-core-draft-consolidation.md) records the working baseline's +provenance, the 12 Gate / 6 Observe classification, top-level responses and +legacy syntax fallback, cross-handler deny preservation, and migration to the +distinct `agent-hook-unity/0.1` identity and current schema URLs. Its companion +files make the candidate testable; no RFC becomes Accepted merely because the +draft changes are present in the repository. External telemetry/correlation +interoperability remains a separate evaluation, with no adapter claim. diff --git a/schemas/README.md b/schemas/README.md index ae01df8..ef666ef 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -1,14 +1,52 @@ # Schemas -This directory contains machine-readable JSON Schemas for Agent Hook Spec +This directory contains machine-readable JSON Schemas for Agent Hook Unity versions. The 0.1 draft defines `hook-event.schema.json` and `hook-response.schema.json`. Each schema declares JSON Schema Draft 2020-12, a stable `$id`, and a descriptive `title`. -The published 0.1 downloads use the `agent-hook-unity` website; see the -[core protocol](../spec/0.1/core.md) for download links. The schemas retain -their original 0.1 `$id` values for compatibility. These identifiers are -independent of the download URLs. +The canonical `spec` value is `agent-hook-unity/0.1`. Each schema's `$id` +matches its published download URL: + +- [Request schema](https://trendmicro.github.io/agent-hook-unity/schemas/0.1/hook-event.schema.json). +- [Response schema](https://trendmicro.github.io/agent-hook-unity/schemas/0.1/hook-response.schema.json). + +The files in `website/static/schemas/0.1/` must be identical to the canonical +files here; `npm run validate` checks this. The Pages workflow publishes the +new files after merge to main; a local change or PR does not update the live +downloads. + +## Migrating the unaccepted draft + +[RFC 0007](../rfcs/0007-core-draft-consolidation.md) changes the previous +`agent-hooks/0.1` identifier to distinguish this contract from the separate +Responsible AI Agent Hooks contract. New schemas reject the old identifier. +Update host, handler, adapter, schema registration, and capability declaration +together, and pin the exact candidate revision. Legacy nested control remains +supported where Core allows it; that syntax fallback does not accept an old +envelope identity. + +Previously, `$id` used `https://trendmicro.github.io/agent-hook-standard/`. +Those paths returned HTTP 404 when checked on 2026-09-21. A JSON Schema `$id` +is a resource identifier and base URI, not a requirement to fetch the schema +over HTTP; see the [JSON Schema identification guidance](https://json-schema.org/understanding-json-schema/structuring). +The new IDs identify the new contract resources. This repository does not +publish a redirect at the old Pages path or rebind its identity. + +Integrations remaining on the old contract can explicitly pin the +pre-migration revision `08ecf2973ed8a3fb7b09f7a8f3a9450f2ea206b3` and vendor its +[request schema](https://raw.githubusercontent.com/trendmicro/agent-hook-unity/08ecf2973ed8a3fb7b09f7a8f3a9450f2ea206b3/schemas/hook-event.schema.json) +and [response schema](https://raw.githubusercontent.com/trendmicro/agent-hook-unity/08ecf2973ed8a3fb7b09f7a8f3a9450f2ea206b3/schemas/hook-response.schema.json). +Register those resources locally under their original `$id` values. Do not +infer which project's contract applies from `agent-hooks/0.1`, or automatically +retry a failed document against a different schema. A deliberately configured +translation must validate both contracts and preserve their semantics. + +Changing `spec` also changes signed or hashed payloads. Update applicable +signing domain separators and recompute signatures and content identities; +do not reuse approvals bound to the previous content. + +## Adding schemas Place a schema at `schemas/.schema.json`. Pair it with fixtures at `fixtures//valid/` and `fixtures//invalid/`. Valid fixtures must diff --git a/schemas/hook-event.schema.json b/schemas/hook-event.schema.json index eb31e43..e4f5b98 100644 --- a/schemas/hook-event.schema.json +++ b/schemas/hook-event.schema.json @@ -1,8 +1,8 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://trendmicro.github.io/agent-hook-standard/schemas/0.1/hook-event.schema.json", - "title": "Agent Hook 0.1 Request", - "description": "A Claude Code-derived, security-telemetry request for an Agent Hook 0.1 event.", + "$id": "https://trendmicro.github.io/agent-hook-unity/schemas/0.1/hook-event.schema.json", + "title": "Agent Hook Unity 0.1 Request", + "description": "A Claude Code-derived, security-telemetry request for an Agent Hook Unity 0.1 event.", "$ref": "#/$defs/HookRequest", "$defs": { "Uuid": { @@ -66,7 +66,7 @@ "CommonRequest": { "type": "object", "properties": { - "spec": { "const": "agent-hooks/0.1" }, + "spec": { "const": "agent-hook-unity/0.1" }, "event_id": { "$ref": "#/$defs/Uuid" }, "hook_event_name": { "$ref": "#/$defs/EventName" }, "session_id": { "type": "string", "minLength": 1 }, diff --git a/schemas/hook-response.schema.json b/schemas/hook-response.schema.json index aacb7be..e4b083f 100644 --- a/schemas/hook-response.schema.json +++ b/schemas/hook-response.schema.json @@ -1,8 +1,8 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://trendmicro.github.io/agent-hook-standard/schemas/0.1/hook-response.schema.json", - "title": "Agent Hook 0.1 Response", - "description": "A correlated Claude Code-shaped response to an Agent Hook 0.1 request.", + "$id": "https://trendmicro.github.io/agent-hook-unity/schemas/0.1/hook-response.schema.json", + "title": "Agent Hook Unity 0.1 Response", + "description": "A correlated Claude Code-shaped response to an Agent Hook Unity 0.1 request.", "type": "object", "additionalProperties": false, "required": [ @@ -11,7 +11,7 @@ ], "properties": { "spec": { - "const": "agent-hooks/0.1" + "const": "agent-hook-unity/0.1" }, "event_id": { "$ref": "#/$defs/Uuid" diff --git a/scripts/validate.mjs b/scripts/validate.mjs index 5adbb78..702661c 100644 --- a/scripts/validate.mjs +++ b/scripts/validate.mjs @@ -4,12 +4,36 @@ import process from 'node:process'; import { fileURLToPath } from 'node:url'; import Ajv2020 from 'ajv/dist/2020.js'; -const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const defaultRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const rootArgumentIndex = process.argv.indexOf('--root'); +const root = rootArgumentIndex === -1 + ? defaultRoot + : path.resolve(process.argv[rootArgumentIndex + 1] ?? ''); const schemaDirectory = path.join(root, 'schemas'); const fixtureDirectory = path.join(root, 'fixtures'); const rfcDirectory = path.join(root, 'rfcs'); -const ignoredDirectories = new Set(['.git', 'node_modules', 'build', '.docusaurus']); +const publishedSchemaDirectory = path.join(root, 'website', 'static', 'schemas', '0.1'); +const ignoredDirectories = new Set([ + '.agents', + '.atlassian', + '.claude', + '.codex', + '.docusaurus', + '.git', + '.superpowers', + '.worktrees', + 'build', + 'node_modules', + 'third_party', +]); const errors = []; +const numberWords = new Map([ + ['zero', 0], ['one', 1], ['two', 2], ['three', 3], ['four', 4], + ['five', 5], ['six', 6], ['seven', 7], ['eight', 8], ['nine', 9], + ['ten', 10], ['eleven', 11], ['twelve', 12], ['thirteen', 13], + ['fourteen', 14], ['fifteen', 15], ['sixteen', 16], ['seventeen', 17], + ['eighteen', 18], ['nineteen', 19], ['twenty', 20], +]); async function filesUnder(directory, predicate = () => true) { const entries = await readdir(directory, { withFileTypes: true }); @@ -34,6 +58,64 @@ async function readJson(file) { } } +async function readOptional(file) { + try { + return await readFile(file, 'utf8'); + } catch (error) { + if (error.code === 'ENOENT') return null; + throw error; + } +} + +function difference(left, right) { + return [...left].filter((value) => !right.has(value)).sort(); +} + +function describeSetDifference(left, right, leftLabel, rightLabel) { + const parts = []; + const missingFromRight = difference(left, right); + const missingFromLeft = difference(right, left); + if (missingFromRight.length) parts.push(`missing from ${rightLabel}: ${missingFromRight.join(', ')}`); + if (missingFromLeft.length) parts.push(`missing from ${leftLabel}: ${missingFromLeft.join(', ')}`); + return parts.join('; '); +} + +function schemaSpecIdentifier(schema) { + return schema?.properties?.spec?.const + ?? schema?.$defs?.CommonRequest?.properties?.spec?.const; +} + +function responseEventNames(schema) { + const coreNames = schema?.$defs?.EventName?.anyOf?.find((entry) => Array.isArray(entry.enum)); + return coreNames?.enum; +} + +function parseRegistry(markdown) { + const events = new Map(); + for (const match of markdown.matchAll(/^\|\s*`([^`]+)`\s*\|.*\|\s*(Gate|Observe)\s*\|\s*$/gm)) { + events.set(match[1], match[2]); + } + return events; +} + +function parseGateTable(markdown, header) { + const lines = markdown.split('\n'); + const headerIndex = lines.findIndex((line) => line.match(/^\|\s*([^|]+?)\s*\|/)?.[1].trim() === header); + if (headerIndex === -1) return null; + + const names = new Set(); + for (const line of lines.slice(headerIndex + 2)) { + if (!line.startsWith('|')) break; + const match = line.match(/^\|\s*`([^`]+)`\s*\|/); + if (match) names.add(match[1]); + } + return names; +} + +function parseCount(value) { + return /^\d+$/.test(value) ? Number(value) : numberWords.get(value.toLowerCase()); +} + async function checkMarkdownLinks(markdown, file) { const checks = []; for (const match of markdown.matchAll(/!?\[[^\]]*\]\(([^)\s]+)(?:\s+[^)]*)?\)/g)) { @@ -76,15 +158,135 @@ async function validateRfcs() { } } +async function validatePublishedSchemaCopies(schemaFiles) { + const canonicalNames = new Set(schemaFiles.map((file) => path.basename(file))); + for (const required of ['hook-event.schema.json', 'hook-response.schema.json']) { + if (!canonicalNames.has(required)) errors.push(`missing canonical schema schemas/${required}`); + } + + for (const canonical of schemaFiles) { + const published = path.join(publishedSchemaDirectory, path.basename(canonical)); + const [canonicalContent, publishedContent] = await Promise.all([ + readFile(canonical, 'utf8'), + readOptional(published), + ]); + if (publishedContent === null) { + errors.push(`missing published schema copy ${path.relative(root, published)}`); + } else if (publishedContent !== canonicalContent) { + errors.push(`${path.relative(root, published)} must exactly match ${path.relative(root, canonical)}`); + } + } +} + +async function validateRegistryAndSummaries(schemaDocuments) { + const registryFile = path.join(root, 'spec', '0.1', 'events.md'); + const registryContent = await readOptional(registryFile); + if (registryContent === null) { + errors.push('missing Core event registry spec/0.1/events.md'); + return; + } + + const registry = parseRegistry(registryContent); + if (!registry.size) { + errors.push('spec/0.1/events.md contains no Core event registry rows'); + return; + } + + const eventSchema = schemaDocuments.get('hook-event'); + const responseSchema = schemaDocuments.get('hook-response'); + const schemaNames = eventSchema?.$defs?.CoreEventName?.enum; + if (!Array.isArray(schemaNames)) { + errors.push('schemas/hook-event.schema.json must define $defs.CoreEventName.enum'); + return; + } + + const registryNames = new Set(registry.keys()); + const canonicalNames = new Set(schemaNames); + const registryDifference = describeSetDifference( + canonicalNames, + registryNames, + 'schema', + 'registry', + ); + if (registryDifference) { + errors.push(`spec/0.1/events.md registry events differ from schemas/hook-event.schema.json $defs.CoreEventName (${registryDifference})`); + } + + const responseNames = responseEventNames(responseSchema); + if (!Array.isArray(responseNames)) { + errors.push('schemas/hook-response.schema.json must enumerate Core names in $defs.EventName'); + } else { + const responseDifference = describeSetDifference( + canonicalNames, + new Set(responseNames), + 'event schema', + 'response schema', + ); + if (responseDifference) { + errors.push(`schemas/hook-response.schema.json event names differ from schemas/hook-event.schema.json (${responseDifference})`); + } + } + + const counts = { + Core: registry.size, + Gate: [...registry.values()].filter((classification) => classification === 'Gate').length, + Observe: [...registry.values()].filter((classification) => classification === 'Observe').length, + }; + const gateNames = new Set( + [...registry].filter(([, classification]) => classification === 'Gate').map(([name]) => name), + ); + const gateMirrors = [ + [path.join(root, 'spec', '0.1', 'core.md'), 'Core Gate'], + [path.join(root, 'website', 'docs', 'responses.md'), 'Gate'], + ]; + for (const [file, header] of gateMirrors) { + const content = await readOptional(file); + if (content === null) continue; + const mirroredNames = parseGateTable(content, header); + if (mirroredNames === null) continue; + const missing = difference(gateNames, mirroredNames); + const extra = difference(mirroredNames, gateNames); + if (missing.length || extra.length) { + const details = [ + missing.length ? `missing from table: ${missing.join(', ')}` : '', + extra.length ? `non-Gate in table: ${extra.join(', ')}` : '', + ].filter(Boolean).join('; '); + errors.push(`${path.relative(root, file)} ${header} table differs from the event registry (${details})`); + } + } + const summaryFiles = [ + path.join(root, 'spec', '0.1', 'index.md'), + path.join(root, 'website', 'docs', 'intro.md'), + path.join(root, 'website', 'docs', 'capabilities.md'), + ]; + const countPattern = /\b(\d+|zero|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty)\s+(Core|Gate|Observe)\s+events?\b/gi; + for (const file of summaryFiles) { + const content = await readOptional(file); + if (content === null) continue; + const plainContent = content.replace(/[*_`]/g, ''); + for (const match of plainContent.matchAll(countPattern)) { + const stated = parseCount(match[1]); + const category = `${match[2][0].toUpperCase()}${match[2].slice(1).toLowerCase()}`; + if (stated !== counts[category]) { + errors.push(`${path.relative(root, file)} states ${stated} ${category} events; registry defines ${counts[category]}`); + } + } + } +} + async function validateSchemasAndFixtures() { const schemaFiles = await filesUnder(schemaDirectory, (file) => file.endsWith('.schema.json')); const schemas = new Map(); + const schemaDocuments = new Map(); const ajv = new Ajv2020({ allErrors: true, strict: false }); + await validatePublishedSchemaCopies(schemaFiles); + for (const file of schemaFiles) { const schema = await readJson(file); if (!schema) continue; const label = path.relative(root, file); + schemaDocuments.set(path.basename(file, '.schema.json'), schema); if (schema.$schema !== 'https://json-schema.org/draft/2020-12/schema') errors.push(`${label} must declare JSON Schema Draft 2020-12`); if (typeof schema.$id !== 'string' || !schema.$id) errors.push(`${label} must declare a stable $id`); if (typeof schema.title !== 'string' || !schema.title) errors.push(`${label} must declare a title`); @@ -95,6 +297,19 @@ async function validateSchemasAndFixtures() { } } + await validateRegistryAndSummaries(schemaDocuments); + + const eventSpec = schemaSpecIdentifier(schemaDocuments.get('hook-event')); + const responseSpec = schemaSpecIdentifier(schemaDocuments.get('hook-response')); + if (typeof eventSpec !== 'string' || !eventSpec) { + errors.push('schemas/hook-event.schema.json must declare a spec const'); + } + if (typeof responseSpec !== 'string' || !responseSpec) { + errors.push('schemas/hook-response.schema.json must declare a spec const'); + } else if (eventSpec && responseSpec !== eventSpec) { + errors.push(`schema spec identifiers differ: hook-event uses "${eventSpec}" and hook-response uses "${responseSpec}"`); + } + const fixtureNames = new Set((await readdir(fixtureDirectory, { withFileTypes: true })) .filter((entry) => entry.isDirectory()) .map((entry) => entry.name)); @@ -132,8 +347,19 @@ async function validateSchemasAndFixtures() { } catch { continue; } - if (parsed && typeof parsed === 'object' && parsed.spec === 'agent-hooks/0.1') { - const isEvent = Boolean(parsed.hook_event_name); + const isObject = parsed && typeof parsed === 'object' && !Array.isArray(parsed); + const isHookEnvelope = isObject + && Object.hasOwn(parsed, 'spec') + && (parsed.spec === eventSpec + || Object.hasOwn(parsed, 'event_id') + || Object.hasOwn(parsed, 'hook_event_name') + || Object.hasOwn(parsed, 'hookSpecificOutput')); + if (isHookEnvelope) { + if (parsed.spec !== eventSpec) { + errors.push(`${path.relative(root, file)} (JSON block ${i + 1}) uses spec ${JSON.stringify(parsed.spec)}; expected ${JSON.stringify(eventSpec)}`); + continue; + } + const isEvent = Object.hasOwn(parsed, 'hook_event_name'); const schemaName = isEvent ? 'hook-event' : 'hook-response'; const validate = schemas.get(schemaName); if (validate && !validate(parsed)) { diff --git a/scripts/validate.test.mjs b/scripts/validate.test.mjs new file mode 100644 index 0000000..adaf72e --- /dev/null +++ b/scripts/validate.test.mjs @@ -0,0 +1,316 @@ +import assert from 'node:assert/strict'; +import { mkdir, mkdtemp, readFile, rm, unlink, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const validator = path.join(projectRoot, 'scripts', 'validate.mjs'); +const activeSpec = 'agent-hook-unity/0.1'; +const eventNames = ['BeforeAction', 'AfterAction']; + +function eventSchema(names = eventNames) { + return { + $schema: 'https://json-schema.org/draft/2020-12/schema', + $id: 'https://example.test/schemas/0.1/hook-event.schema.json', + title: 'Test hook event', + type: 'object', + properties: { + spec: { const: activeSpec }, + event_id: { type: 'string' }, + hook_event_name: { $ref: '#/$defs/EventName' }, + }, + required: ['spec', 'event_id', 'hook_event_name'], + $defs: { + CoreEventName: { enum: names }, + EventName: { + anyOf: [ + { $ref: '#/$defs/CoreEventName' }, + { type: 'string', pattern: '^x-[a-z]+/[A-Z][A-Za-z]+$' }, + ], + }, + }, + }; +} + +function responseSchema(names = eventNames) { + return { + $schema: 'https://json-schema.org/draft/2020-12/schema', + $id: 'https://example.test/schemas/0.1/hook-response.schema.json', + title: 'Test hook response', + type: 'object', + properties: { + spec: { const: activeSpec }, + event_id: { type: 'string' }, + decision: { enum: ['allow', 'deny'] }, + hookSpecificOutput: { $ref: '#/$defs/HookSpecificOutput' }, + }, + required: ['spec', 'event_id'], + additionalProperties: false, + $defs: { + EventName: { + anyOf: [ + { enum: names }, + { type: 'string', pattern: '^x-[a-z]+/[A-Z][A-Za-z]+$' }, + ], + }, + HookSpecificOutput: { + type: 'object', + properties: { hookEventName: { $ref: '#/$defs/EventName' } }, + required: ['hookEventName'], + }, + }, + }; +} + +async function writeJson(file, value) { + await mkdir(path.dirname(file), { recursive: true }); + await writeFile(file, `${JSON.stringify(value, null, 2)}\n`); +} + +async function makeRepository(t) { + const repository = await mkdtemp(path.join(tmpdir(), 'agent-hook-validator-')); + t.after(() => rm(repository, { recursive: true, force: true })); + + const event = eventSchema(); + const response = responseSchema(); + await Promise.all([ + mkdir(path.join(repository, 'fixtures'), { recursive: true }), + mkdir(path.join(repository, 'rfcs'), { recursive: true }), + mkdir(path.join(repository, 'website/docs'), { recursive: true }), + mkdir(path.join(repository, 'spec/0.1'), { recursive: true }), + ]); + await Promise.all([ + writeJson(path.join(repository, 'schemas/hook-event.schema.json'), event), + writeJson(path.join(repository, 'schemas/hook-response.schema.json'), response), + writeJson(path.join(repository, 'website/static/schemas/0.1/hook-event.schema.json'), event), + writeJson(path.join(repository, 'website/static/schemas/0.1/hook-response.schema.json'), response), + writeFile(path.join(repository, 'spec/0.1/events.md'), `# Event registry + +| \`hook_event_name\` | Boundary | Classification | +| --- | --- | --- | +| \`BeforeAction\` | Before action. | Gate | +| \`AfterAction\` | After action. | Observe | +`), + writeFile(path.join(repository, 'spec/0.1/index.md'), 'The registry defines 2 Core events: 1 Gate event and 1 Observe event.\n'), + ]); + + return repository; +} + +function validate(repository) { + return spawnSync(process.execPath, [validator, '--root', repository], { + cwd: projectRoot, + encoding: 'utf8', + }); +} + +function output(result) { + return `${result.stdout}${result.stderr}`; +} + +async function updateCanonicalAndPublished(repository, name, mutate) { + const canonical = path.join(repository, 'schemas', `${name}.schema.json`); + const schema = JSON.parse(await readFile(canonical, 'utf8')); + mutate(schema); + await Promise.all([ + writeJson(canonical, schema), + writeJson(path.join(repository, 'website/static/schemas/0.1', `${name}.schema.json`), schema), + ]); +} + +test('accepts a synchronized minimal repository', async (t) => { + const repository = await makeRepository(t); + const result = validate(repository); + + assert.equal(result.status, 0, output(result)); +}); + +test('rejects a published schema copy that differs from its canonical schema', async (t) => { + const repository = await makeRepository(t); + const published = path.join(repository, 'website/static/schemas/0.1/hook-event.schema.json'); + const schema = JSON.parse(await readFile(published, 'utf8')); + schema.title = 'Drifted copy'; + await writeJson(published, schema); + + const result = validate(repository); + + assert.equal(result.status, 1, output(result)); + assert.match(output(result), /website\/static\/schemas\/0\.1\/hook-event\.schema\.json must exactly match schemas\/hook-event\.schema\.json/); +}); + +test('rejects a newly introduced schema event missing from the registry', async (t) => { + const repository = await makeRepository(t); + await updateCanonicalAndPublished(repository, 'hook-event', (schema) => { + schema.$defs.CoreEventName.enum.push('DuringAction'); + }); + + const result = validate(repository); + + assert.equal(result.status, 1, output(result)); + assert.match(output(result), /missing from registry: DuringAction/); +}); + +test('rejects a registry event removed from the schema', async (t) => { + const repository = await makeRepository(t); + await updateCanonicalAndPublished(repository, 'hook-event', (schema) => { + schema.$defs.CoreEventName.enum = ['BeforeAction']; + }); + + const result = validate(repository); + + assert.equal(result.status, 1, output(result)); + assert.match(output(result), /missing from schema: AfterAction/); +}); + +test('rejects response event membership that differs from the canonical event schema', async (t) => { + const repository = await makeRepository(t); + await updateCanonicalAndPublished(repository, 'hook-response', (schema) => { + schema.$defs.EventName.anyOf[0].enum = ['BeforeAction']; + }); + + const result = validate(repository); + + assert.equal(result.status, 1, output(result)); + assert.match(output(result), /hook-response\.schema\.json event names differ.*missing from response schema: AfterAction/); +}); + +test('rejects a mirrored Core Gate table that differs from registry classifications', async (t) => { + const repository = await makeRepository(t); + await writeFile(path.join(repository, 'spec/0.1/core.md'), `# Core + +| Core Gate | Control | +| --- | --- | +| \`AfterAction\` | deny | +`); + + const result = validate(repository); + + assert.equal(result.status, 1, output(result)); + assert.match(output(result), /spec\/0\.1\/core\.md Core Gate table differs from the event registry \(missing from table: BeforeAction; non-Gate in table: AfterAction\)/); +}); + +test('rejects a missing published schema copy', async (t) => { + const repository = await makeRepository(t); + await unlink(path.join(repository, 'website/static/schemas/0.1/hook-response.schema.json')); + + const result = validate(repository); + + assert.equal(result.status, 1, output(result)); + assert.match(output(result), /missing published schema copy website\/static\/schemas\/0\.1\/hook-response\.schema\.json/); +}); + +test('rejects a stale spec identifier in a JSON hook envelope', async (t) => { + const repository = await makeRepository(t); + await writeFile(path.join(repository, 'website/docs/example.md'), `# Example + +\`\`\`json +{ + "spec": "agent-hooks/0.1", + "event_id": "example-id", + "hook_event_name": "BeforeAction" +} +\`\`\` +`); + + const result = validate(repository); + + assert.equal(result.status, 1, output(result)); + assert.match(output(result), /website\/docs\/example\.md \(JSON block 1\) uses spec "agent-hooks\/0\.1"; expected "agent-hook-unity\/0\.1"/); +}); + +test('rejects active-spec event and response examples that omit event_id', async (t) => { + const repository = await makeRepository(t); + await writeFile(path.join(repository, 'website/docs/missing-event-id.md'), `# Missing correlation + +\`\`\`json +{"spec": "${activeSpec}", "hook_event_name": "BeforeAction"} +\`\`\` + +\`\`\`json +{"spec": "${activeSpec}", "decision": "allow"} +\`\`\` +`); + + const result = validate(repository); + + assert.equal(result.status, 1, output(result)); + assert.match(output(result), /missing-event-id\.md \(JSON block 1\) failed validation against hook-event\.schema\.json/); + assert.match(output(result), /missing-event-id\.md \(JSON block 2\) failed validation against hook-response\.schema\.json/); +}); + +test('ignores unrelated JSON configs and proposed jsonc examples', async (t) => { + const repository = await makeRepository(t); + await writeFile(path.join(repository, 'website/docs/config.md'), `# Config + +\`\`\`json +{"spec": "unrelated-tool/1", "enabled": true} +\`\`\` + +\`\`\`jsonc +{"spec": "agent-hooks/0.1", "event_id": "proposal"} +\`\`\` +`); + + const result = validate(repository); + + assert.equal(result.status, 0, output(result)); +}); + +test('ignores hook examples in repository-local worktrees', async (t) => { + const repository = await makeRepository(t); + const historicalExample = path.join(repository, '.worktrees/old/examples/historical.md'); + await mkdir(path.dirname(historicalExample), { recursive: true }); + await writeFile(historicalExample, `\`\`\`json +{"spec": "agent-hooks/0.1", "event_id": "old", "hook_event_name": "BeforeAction"} +\`\`\` +`); + + const result = validate(repository); + + assert.equal(result.status, 0, output(result)); +}); + +test('ignores vendored and local agent-tool Markdown', async (t) => { + const repository = await makeRepository(t); + for (const directory of ['third_party', '.agents', '.atlassian', '.claude', '.codex', '.superpowers']) { + const localMarkdown = path.join(repository, directory, 'local.md'); + await mkdir(path.dirname(localMarkdown), { recursive: true }); + await writeFile(localMarkdown, `# Local tooling + +[Missing local file](./missing.md) + +\`\`\`json +{"spec": "agent-hooks/0.1", "event_id": "old", "hook_event_name": "BeforeAction"} +\`\`\` +`); + } + + const result = validate(repository); + + assert.equal(result.status, 0, output(result)); +}); + +test('rejects explicit event-count summaries that disagree with the registry', async (t) => { + const repository = await makeRepository(t); + await writeFile(path.join(repository, 'spec/0.1/index.md'), 'The registry defines 999 Core events: 998 Gate events and 1 Observe event.\n'); + + const result = validate(repository); + + assert.equal(result.status, 1, output(result)); + assert.match(output(result), /spec\/0\.1\/index\.md states 999 Core events; registry defines 2/); + assert.match(output(result), /spec\/0\.1\/index\.md states 998 Gate events; registry defines 1/); +}); + +test('rejects emphasized event-count summaries that disagree with the registry', async (t) => { + const repository = await makeRepository(t); + await writeFile(path.join(repository, 'spec/0.1/index.md'), 'The registry distinguishes seven **Gate** events from eleven **Observe** events.\n'); + + const result = validate(repository); + + assert.equal(result.status, 1, output(result)); + assert.match(output(result), /spec\/0\.1\/index\.md states 7 Gate events; registry defines 1/); + assert.match(output(result), /spec\/0\.1\/index\.md states 11 Observe events; registry defines 1/); +}); diff --git a/spec/0.1/adapters.md b/spec/0.1/adapters.md index 13a3a5a..a5d38f7 100644 --- a/spec/0.1/adapters.md +++ b/spec/0.1/adapters.md @@ -72,45 +72,60 @@ observation or enforcement. ## Response mappings -The Agent Hook response is Claude-shaped and event-specific, not a universal -allow/deny/ask decision document. An adapter MUST interpret a control response -only at an event classified as a Gate by the registry, declared `gate`, and -reached through a native control point that has not passed the relevant effect -boundary. +The Agent Hook response uses a canonical top-level `decision`, with +event-specific allowed values, effects, and optional rewrites. An adapter MUST +interpret a control response only at an event classified as a Gate by the +registry, declared `gate`, and reached through a native control point that can +still enforce the event's controlled effect. | Core Gate | Standard response shape | Claude Code direction | | --- | --- | --- | -| `UserPromptSubmit` | Top-level `decision: "block"` plus `reason`. | Map to Claude Code's prompt block response. | -| `BeforeModelRequest` | `hookSpecificOutput` with `hookEventName`, `permissionDecision`, optional `permissionDecisionReason`, and optional `updatedMessages`. | Claude Code has no native event; declare it `unavailable` rather than fabricate a mapping. | -| `PreToolUse` | `hookSpecificOutput` with `hookEventName`, `permissionDecision`, optional `permissionDecisionReason`, and optional `updatedInput`. | Map to Claude Code's `PreToolUse` permission decision. | -| `PermissionRequest` | `hookSpecificOutput.decision.behavior`, with optional input, permission, message, and interrupt updates. | Map to Claude Code's nested permission-request decision. | -| `PreNetworkAccess`, `PreMemoryWrite`, `PreConfigChange` | `hookSpecificOutput` with `hookEventName`, `permissionDecision`, and optional `permissionDecisionReason`. | No mapping defined here; enforce only at an actual native boundary that meets the registry's Gate requirements. | - -`permissionDecision` uses `allow`, `deny`, `ask`, or `defer`; nested -`decision.behavior` uses `allow` or `deny`. A response with -`hookSpecificOutput` MUST use a `hookEventName` that matches the request. An -adapter MUST NOT let an allow result override native or organization policy. -The three added Gates MUST follow the -[Core response rules](./core.md#response-envelope) for native approval, -non-interactive `ask`, and `defer`; they define no input-rewriting control. - -Top-level `decision: "block"` and all `hookSpecificOutput` control members are -ignored for an Observe event. Claude Code's other response shapes are likewise -event-specific: `PostToolUse`, for example, can replace model-visible output -only after the effect completed, and `Stop` blocking has a native retry limit. -Those facts do not make either event an Agent Hook `gate`. No universal Claude -Code response compatibility is claimed, and no Claude mapping exists for the -two model events in 0.1. - -Adapters MUST correlate the native invocation with `event_id` and MUST apply the -core fail-open rule to absent, invalid, timed-out, or errored Agent Hook -responses. A product's independently configured native behavior may be stricter -but is not Agent Hook 0.1 behavior. - -Before delivering the five added events, adapters MUST satisfy the -[revised-draft compatibility requirements](./core.md#versioning-and-conformance). -Older 0.1 schemas reject these names; an unchanged `spec` value alone does not -establish that a handler is configured to accept them. +| `SessionStart` | `decision: "allow"` or `"deny"`; no rewrite. | This guide claims no Claude Code Gate mapping. Declare `observe`, `partial`, or `unavailable` unless the native boundary can prevent agent work from starting. | +| `UserPromptSubmit` | `decision: "allow"`, `"deny"`, or legacy `"block"`; optional `hookSpecificOutput.updatedPrompt`. | Map denial or legacy block to Claude Code's prompt block response. Map a rewrite only if the native callback can replace the accepted prompt before execution. | +| `BeforeModelRequest` | `decision: "allow"`, `"deny"`, `"ask"`, or `"defer"`; optional `hookSpecificOutput.updatedMessages`. | Claude Code has no native event; declare it `unavailable` rather than fabricate a mapping. | +| `AfterModelResponse` | `decision: "allow"` or `"deny"`; optional `hookSpecificOutput.updatedResponse`. | Claude Code has no faithful native event. A Gate requires the complete result to remain buffered before rendering or context ingestion. | +| `PreToolUse` | `decision: "allow"`, `"deny"`, `"ask"`, or `"defer"`; optional `hookSpecificOutput.updatedInput`. | Map the canonical decision and supported rewrite to Claude Code's native `PreToolUse` response. | +| `PermissionRequest` | `decision: "allow"` or `"deny"`; optional native approval updates under `hookSpecificOutput.decision`. | Map to Claude Code's permission-request response while preserving native policy authority. | +| `PostToolUse` | `decision: "allow"` or `"deny"`; optional `hookSpecificOutput.updatedOutput`. | Claim `gate` only when the adapter can hold the completed result before model-context ingestion and map the replacement faithfully. The tool execution has already completed. | +| `PreNetworkAccess` | `decision: "allow"`, `"deny"`, `"ask"`, or `"defer"`; no rewrite. | No Claude Code mapping is defined here. Enforce only at an actual pre-dispatch request boundary. | +| `PostNetworkAccess` | `decision: "allow"` or `"deny"`; no rewrite. | No Claude Code mapping is defined here. A Gate requires buffered response delivery; Core defines no body-inspection or body-replacement field. | +| `PreMemoryWrite` | `decision: "allow"`, `"deny"`, `"ask"`, or `"defer"`; optional `hookSpecificOutput.updatedContent`. | No Claude Code mapping is defined here. Apply a replacement only before durable persistence or visibility. | +| `PreConfigChange` | `decision: "allow"`, `"deny"`, `"ask"`, or `"defer"`; no rewrite. | No Claude Code mapping is defined here. Enforce only before the effective mutation. | +| `SubagentStart` | `decision: "allow"` or `"deny"`; no rewrite. | Preserve the native event, but claim `gate` only if a denial prevents executable work from reaching the child. | + +When top-level `decision` is present, it is canonical. The adapter MUST ignore +legacy nested controls (`permissionDecision`, `permissionDecisionReason`, and +`hookSpecificOutput.decision.behavior`). For backward compatibility, nested +controls remain valid only when top-level `decision` is absent. +`hookSpecificOutput.hookEventName` MUST match the request whenever +`hookSpecificOutput` is present. An `allow` MUST NOT override native, +organization, sandbox, managed-policy, or user-approval restrictions. + +For Gates supporting `ask`, adapters MUST follow the +[Core response rules](./core.md#response-envelope): the controlled operation +does not proceed while approval is unresolved, and a non-interactive host +without suspension treats `ask` as `deny`. `defer` leaves resolution to native +approval or policy and is not approval. A schema-valid rewrite that fails +native validation fails closed; the adapter MUST NOT restore the original +unredacted value. + +All response controls are ignored for an event the host declares `observe`. +For the three Post Gates, control applies to buffered rendering, delivery, or +context ingestion and MUST NOT be represented as rollback of completed model, +tool, or network work. Claude Code's native response shapes remain +event-specific, and this guide claims no universal byte-for-byte compatibility. + +Adapters MUST correlate the native invocation with `event_id`. An absent, +invalid, timed-out, or errored response supplies no decision from that handler +invocation. A timeout alone follows the Core fail-open default, but a failure +MUST NOT erase another accepted denial, rewrite, or approval requirement for +the same pending action. Independent native policy still applies. + +Before delivering the current eighteen-event baseline, adapters MUST satisfy +the [draft compatibility requirements](./core.md#versioning-and-conformance). +The current wire identifier is `agent-hook-unity/0.1`. Earlier local drafts +used the colliding `agent-hooks/0.1` value; adapters MUST update configuration +and schemas explicitly and MUST NOT silently negotiate between the identifiers. ## Non-normative stdio adapter pattern diff --git a/spec/0.1/core.md b/spec/0.1/core.md index 2581a6d..af4e85f 100644 --- a/spec/0.1/core.md +++ b/spec/0.1/core.md @@ -6,7 +6,7 @@ sidebar_position: 2 ## Status and terminology -This document defines the Agent Hook 0.1 draft. The key words **MUST**, +This document defines the Agent Hook Unity 0.1 draft. The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this document are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and @@ -37,7 +37,7 @@ Every event MUST contain these flat members: | Member | Meaning | | --- | --- | -| `spec` | The Agent Hook contract identifier, exactly `agent-hooks/0.1`. | +| `spec` | The Agent Hook contract identifier, exactly `agent-hook-unity/0.1`. | | `event_id` | A UUID that uniquely identifies this delivered event. | | `hook_event_name` | A Core or extension event name. Core values are defined by the event registry. | | `session_id` | An opaque identifier for the host session that emitted the event. | @@ -125,9 +125,11 @@ within `hookSpecificOutput`). When no payload mutation is required, 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` +rewrite members listed in the Gate table below. Native approval updates for +`PermissionRequest` retain their existing `hookSpecificOutput.decision` +container; its `behavior` is structurally required but ignored for control +when a canonical top-level decision is present. 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 | @@ -137,7 +139,7 @@ inside `hookSpecificOutput` only when top-level `decision` is omitted. | `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. | +| `PermissionRequest` | `decision: "allow"` or `"deny"` (or nested `decision.behavior`) | `decision.updatedInput`, `decision.updatedPermissions` | Controls native approval request. The nested update members are relative to `hookSpecificOutput`. | | `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. | @@ -148,6 +150,42 @@ inside `hookSpecificOutput` only when top-level `decision` is omitted. An `allow` only passes that hook's native gate; it MUST NOT override sandbox, organization, managed-policy, or user-approval restrictions. +### Multiple handlers and repeated evaluation + +A **pending action** is the effect controlled at one declared Gate boundary: +for a Pre Gate, the dispatch or mutation that has not occurred; for a Post +Gate, the delivery, rendering, or ingestion that has not occurred. A host that +accepts a valid `deny` (or `block` where that event supports it) for a pending +action MUST NOT perform that action. An accepted denial can use the canonical +top-level decision or a supported legacy nested denial when the top-level +decision is absent. This denial is binding for the same pending action at the +same Gate boundary. It MUST survive: + +- another handler returning `allow`, `ask`, `defer`, or a rewrite; +- another handler returning no usable control result because its response is + absent, invalid, mismatched, errored, or timed out; +- redelivery to the same or another handler with a different `event_id`; and +- reevaluation of that pending action, including reevaluation required after + its target or proposed values change. + +`event_id` correlates one response to one delivery; changing it MUST NOT clear +a denial of the underlying pending action. An operation identifier can span +different lifecycle boundaries, but a Pre boundary and its Post boundary +control distinct pending actions. A denial at one boundary therefore does not +become a control result at another boundary, and a denial for one operation +MUST NOT deny an unrelated operation. + +Core does not prescribe whether hosts invoke handlers sequentially or in +parallel, or how they order or reconcile multiple valid rewrites. Unless an +accepted denial has already made the action impossible, a host MUST NOT cross +the Gate until every applicable handler invocation has completed or reached +its declared deadline, every applicable `ask` or native approval has resolved, +and native policy permits the action. A response failure supplies no decision; +it MUST NOT erase another valid decision or cause an already accepted rewrite +to be silently discarded. If the action is otherwise permitted, accepted +rewrites remain subject to the host's mutation ordering and conflict policy +and to the native validation rule below. + 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 @@ -165,8 +203,8 @@ 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 +When a handler supplies a schema-valid, event-supported rewrite or native +approval update from the Gate table above, 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 @@ -192,11 +230,16 @@ capability MUST NOT emit a normalized Core event. ## Fail-open behavior An absent response, invalid JSON, schema-invalid response, correlation -mismatch, handler error, or timeout MUST result in no Agent Hook control result. -A host that declared the event `gate` MUST continue the affected operation -unless an independent native policy blocks it. Adapters SHOULD emit a diagnostic -record containing the event ID, hook event name, failure class, and handler -identity without retaining sensitive event data. +mismatch, handler error, or timeout supplies no Agent Hook control result for +that invocation. It is not an implicit denial or permission. After every +applicable handler invocation has completed or reached its deadline, the host +MUST continue the pending action if no accepted denial binds it, no other +applicable valid Agent Hook decision withholds permission, required approvals +have resolved, and independent native policy permits it. Thus a timeout as the +only handler outcome retains the 0.1 fail-open default, while a timeout +alongside an accepted denial does not weaken that denial. Adapters SHOULD emit +a diagnostic record containing the event ID, hook event name, failure class, +and handler identity without retaining sensitive event data. This requirement controls only Agent Hook 0.1. It does not weaken host, sandbox, administrative, or organization policy. A host requiring fail-closed @@ -216,18 +259,19 @@ guide, not a new execution order for native policy checks or multiple handlers. | Declare the actual boundary | Publish a mode for every Core event. Claim `gate` only when the registry classifies the event as a Gate and the host can faithfully observe and enforce it. Declare limitations; do not normalize a `partial` or `unavailable` signal into a Core event. | [Versioning and conformance](#versioning-and-conformance) | | Construct a faithful event | Emit a schema-valid event at the event's defined boundary, with the required delivery, session, turn, and operation identifiers. Preserve correlation through the adapter. Omit optional context that cannot be supplied faithfully and apply the existing redaction rules. | [Event envelope](#event-envelope), [security correlation](#security-correlation), [data minimization](#data-minimization-and-redaction), [event registry](./events.md#core-event-registry) | | Validate and pair the response | Check the response schema and match `event_id` to this delivery. If `hookSpecificOutput` is present, also match its `hookEventName` to the event's `hook_event_name`. Schema validation alone does not establish these cross-document matches. | [Response envelope](#response-envelope) | -| Apply the event's Gate control | Interpret control only for a registry Gate declared `gate`. Apply that event's response shape and effect, including its rewrite support where defined. A valid denial prevents the action at the stated boundary. The network, memory, and configuration Gates require reevaluation if the presented operation, target, or proposed values change before dispatch or mutation. | [Response envelope](#response-envelope), [Gate and Observe semantics](./events.md#gate-and-observe-semantics) | +| Apply the event's Gate control | Interpret control only for a registry Gate declared `gate`. Apply that event's response shape and effect, including its rewrite support where defined. An accepted valid denial remains binding to the same pending action at that boundary across handlers, delivery retries, and reevaluation. The network, memory, and configuration Gates require reevaluation if the presented operation, target, or proposed values change before dispatch or mutation. | [Response envelope](#response-envelope), [multiple handlers and repeated evaluation](#multiple-handlers-and-repeated-evaluation), [Gate and Observe semantics](./events.md#gate-and-observe-semantics) | | Keep Observe responses observational | Ignore response controls for control purposes on an `observe` event. Diagnostic or observational retention is optional; retaining a response does not authorize changing the observed action. | [Response envelope](#response-envelope), [Gate and Observe semantics](./events.md#gate-and-observe-semantics) | | Preserve native authority | An Agent Hook `allow` passes only that hook's native gate. Sandbox, organization, managed-policy, and user-approval restrictions still apply. Follow the event-specific approval semantics: `PreNetworkAccess`, `PreMemoryWrite`, and `PreConfigChange` use native approval for `ask`, treat `ask` as `deny` on a non-interactive host, and do not treat `defer` as approval. | [Response envelope](#response-envelope) | -| Handle response failures | An absent or unusable response supplies no Agent Hook control result. For a declared Gate, continue the affected operation unless an independent native policy blocks it. Adapters should record the minimal diagnostics described below. | [Fail-open behavior](#fail-open-behavior) | +| Handle response failures | An absent or unusable response supplies no Agent Hook control result for that invocation. Continue a pending action only when no accepted denial, other valid Agent Hook decision, unresolved approval, or independent native policy blocks it. Adapters should record the minimal diagnostics described below. | [Fail-open behavior](#fail-open-behavior) | ### Response-failure reference All conditions in this table have the same outcome under the existing -[fail-open rule](#fail-open-behavior): no Agent Hook control result. For an -event declared `gate`, the operation continues unless independent native -policy blocks it. For an `observe` event, a response has no control effect in -the first place. +[fail-open rule](#fail-open-behavior): no Agent Hook control result from that +invocation. For an event declared `gate`, an isolated failure does not deny the +pending action, but it does not override accepted denials, other applicable +decisions, unresolved approvals, or independent native policy. For an +`observe` event, a response has no control effect in the first place. | Condition | Why it is not a valid control result | | --- | --- | @@ -256,23 +300,35 @@ rely on a shared behavior: | Question | Current boundary | | --- | --- | | Common-field precedence on a Gate | The schema accepts common members such as `continue` and `stopReason`, but 0.1 does not fully define their interaction with event-specific Gate controls. For example, it does not define a portable precedence rule for `continue: false` together with `permissionDecision: "allow"`. The existing Observe control-ignore rule still applies. | -| A rewrite that fails native validation | The response envelope identifies the events that accept `updatedInput` or `updatedMessages`. It does not fully define how a host handles a schema-valid response whose replacement fails a native tool or model-input constraint. Such a failure is distinct from a response that fails the Hook Response schema; the response-failure table does not choose a replacement, retry, or rejection policy for it. | +| Ordering or conflict between multiple valid rewrites | Core does not prescribe sequential or parallel handler scheduling, mutation order, or a winner between conflicting valid rewrites. The host's policy is still subject to deny preservation, completion and deadline safety, and fail-closed native validation of the value it would commit. A response failure alone cannot erase an accepted rewrite or restore the original value. | | Failure to construct a valid event | Producers still owe schema-valid, faithful events and accurate capability declarations. The fail-open clause covers response and handler failures; it does not define a general disposition of the pending operation when the producer cannot construct a valid event. Fabricating missing data or treating an unfaithful signal as a normalized Core event would violate the existing requirements. | -Handler ordering, response composition, transport bindings, and asynchronous -approval workflows remain outside this section's scope. No fail-closed mode or -new approval mechanism is introduced here. +Handler scheduling, rewrite conflict resolution, transport bindings, and +asynchronous approval mechanisms remain outside this section's scope except +for the minimum composition and approval-safety requirements above. No general +fail-closed profile or new approval mechanism is introduced here. ## Versioning and conformance `spec` is a major/minor contract identifier. A 0.1 implementation MUST emit and -accept exactly `agent-hooks/0.1`; patch-only specification changes do not change -the member. Future incompatible envelopes require a new major version. - -This revision extends an unaccepted 0.1 draft with `PreNetworkAccess`, +accept exactly `agent-hook-unity/0.1`. The earlier `agent-hooks/0.1` value +collides with a different project and MUST NOT be silently accepted or +negotiated as an alias. The namespace change is a coordinated migration to a +separate wire identity, not a compatibility claim. Pinned pre-migration schemas +remain linked from the +[schema guide](https://github.com/trendmicro/agent-hook-unity/blob/main/schemas/README.md) +for integrations that deliberately retain the old draft. Future incompatible +envelopes require a new major version. + +This revision consolidates an unaccepted 0.1 draft under the new identity and +extends it with `PreNetworkAccess`, `PostNetworkAccess`, `PreMemoryWrite`, `PostMemoryWrite`, and `PreConfigChange` -while retaining `agent-hooks/0.1`. Earlier 0.1 schemas reject these names; -the unchanged identifier does not imply compatibility with existing handlers. +while preserving the current partial `PostNetworkAccess` Gate: it can control +delivery on a buffered transport, while response-body inspection and rewriting +remain deferred to RFC 0005. The original 13-event 0.1 schemas reject the five +additional event names. The immediate pre-migration 18-event schemas include +them but use the former colliding identifier. Implementations MUST treat this +as an explicit migration rather than inferred compatibility. Adopters MUST update their schemas and capability declarations and ensure handler compatibility and configuration before enabling the new events. Hosts MUST deliver these events only to handlers configured for this revised @@ -281,6 +337,12 @@ an unknown event MUST NOT be treated as an implicit `allow` response. The [fail-open rule](#fail-open-behavior) still applies to response failures for configured event deliveries. +The provenance and still-pending adoption decisions for this consolidation are +recorded in +[draft RFC 0007](https://github.com/trendmicro/agent-hook-unity/blob/main/rfcs/0007-core-draft-consolidation.md). +That RFC is not accepted, and this text does not represent an acceptance +decision. + A host claiming 0.1 conformance MUST publish a capability declaration that enumerates every Core `hook_event_name` in the event registry. For each name, the declaration MUST state exactly one of these modes: @@ -296,8 +358,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 +Core 0.1 normatively adheres to the [fail-open rule](#fail-open-behavior) for an +individual handler error or timeout while preserving other valid decisions for +the same pending action. Alternative degradation behaviors (such as general fail-closed enforcement profiles) are deferred to dedicated profile RFCs. Core membership standardizes event names and semantics; it does not require a diff --git a/spec/0.1/events.md b/spec/0.1/events.md index 412bb73..cd47b65 100644 --- a/spec/0.1/events.md +++ b/spec/0.1/events.md @@ -4,7 +4,8 @@ sidebar_position: 3 # Event registry -This document defines the Core event registry for the Agent Hook 0.1 draft. +This document defines the Core event registry for the Agent Hook Unity 0.1 +draft. The capitalized key words **MUST**, **MUST NOT**, **REQUIRED**, **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this document are to be interpreted as described in the [Core protocol](./core.md). @@ -337,9 +338,11 @@ 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. +and policy. When declared `gate`, the host MUST enforce applicable valid +control responses under the Core +[composition rules](./core.md#multiple-handlers-and-repeated-evaluation); 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 @@ -350,7 +353,9 @@ change an observed action while claiming conformance to this registry. 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`. +proceeding. Reevaluation does not clear an accepted denial of the same pending +action at this boundary, even though it uses a new delivery and `event_id`. A +host that cannot enforce this precondition MUST NOT claim `gate`. 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 @@ -358,8 +363,11 @@ have no effect. ## Correlation and ordering -`event_id` identifies a single delivery to a single handler. It MUST NOT be -used in place of an action or lifecycle correlation identifier. +`event_id` identifies a single delivery to a single handler. Separate handlers, +redelivery, and reevaluation use distinct `event_id` values. Those new delivery +identifiers MUST NOT be used to clear a decision binding the same pending +action, and they MUST NOT be used in place of an action or lifecycle +correlation identifier. - `model_request_id` correlates `BeforeModelRequest` with `AfterModelResponse`. @@ -383,11 +391,13 @@ For the network, memory, and configuration events, `operation_id` MUST be a nonempty opaque string identifying one underlying operation, unique within the session across these activities. The host MUST generate it at the underlying operation boundary even when a pre-event cannot be observed. It -MUST remain stable across paired pre/post events and redelivery. If the exact -same operation reaches `PermissionRequest` or `PermissionDenied`, those events -MUST retain this `operation_id`; a different underlying operation MUST NOT -reuse it. A consumer MUST NOT substitute a destination, memory key, -configuration target, or delivery `event_id` for operation correlation. +MUST remain stable across paired pre/post events, redelivery, and reevaluation +of the same underlying operation. Stability does not merge the distinct +pending actions controlled at Pre and Post Gate boundaries. If the exact same +operation reaches `PermissionRequest` or `PermissionDenied`, those events MUST +retain this `operation_id`; a different underlying operation MUST NOT reuse it. +A consumer MUST NOT substitute a destination, memory key, configuration target, +or delivery `event_id` for operation correlation. ## Sensitive content and telemetry diff --git a/spec/0.1/index.md b/spec/0.1/index.md index afc3614..dc0c5ac 100644 --- a/spec/0.1/index.md +++ b/spec/0.1/index.md @@ -3,9 +3,9 @@ sidebar_position: 1 slug: /0.1 --- -# Agent Hook 0.1 draft +# Agent Hook Unity 0.1 draft -Agent Hook 0.1 is a draft portable security and telemetry contract for an agent +Agent Hook Unity 0.1 is a draft portable security and telemetry contract for an agent runtime to deliver a lifecycle event to a hook and receive a structured response. Its flat request shape and PascalCase `hook_event_name` values are Claude-shaped for practical adapter compatibility, but it is not a Claude Code @@ -27,17 +27,23 @@ execution, ordering, and native policy remain host concerns. | Adapter | Preserve the event's meaning and correlation while translating native callbacks and supported responses. It may be implemented inside the host. | | Handler | Accept an event and return a schema-valid response correlated to that delivery. | -The registry distinguishes seven Gate events from eleven Observe events. +The registry distinguishes twelve Gate events from six Observe events. Control requires both a Gate event and a host declaration of `gate`; a faithful observation alone does not provide preventive enforcement. The default for -missing or invalid handler responses is fail open, subject to independent -native restrictions. Completed model and tool results are Observe events, not -portable result-filtering or rollback boundaries. +an individual missing, invalid, timed-out, or errored handler response is no +decision from that invocation. A timeout alone therefore fails open, subject +to other valid handler decisions and independent native restrictions; it does +not erase an accepted denial, rewrite, or approval requirement for the same +pending action. Completed model, tool, and network operations can expose Gate +boundaries only when the host buffers their results until control completes. +Those Post Gates control delivery, rendering, or context ingestion, not +rollback of the completed work. The contract covers the lifecycle boundaries a host can expose faithfully. It does not establish sandbox isolation or visibility into every internal runtime or provider operation. Its per-delivery event and response semantics -do not define how multiple handlers compose. See the +leave handler scheduling and rewrite conflict resolution to the host, subject +to the minimum decision-preservation rules in Core. See the [host obligations](./core.md#host-obligations) for existing responsibilities and the boundaries that remain open in this draft. @@ -53,8 +59,8 @@ introducing a new response contract or capability configuration format. ## Documents - [Core protocol](./core.md) defines the flat envelope, security correlation, - Claude-shaped event-specific control responses, capability declarations, - fail-open behavior, versioning, and conformance requirements. + canonical top-level control responses, event-specific rewrites, capability + declarations, fail-open behavior, versioning, and conformance requirements. - [Event registry](./events.md) defines the 18 Core PascalCase event names, their timing, required flat fields, and intended capability boundaries. - [Extensions](./extensions.md) defines portable extension boundaries. @@ -75,11 +81,20 @@ The original draft is proposed by [RFC 0001](https://github.com/trendmicro/agent-hook-unity/blob/main/rfcs/0001-agent-hook-core-event-contract.md). The five-event expansion is proposed by [RFC 0004](https://github.com/trendmicro/agent-hook-unity/blob/main/rfcs/0004-standard-lifecycle-events.md). -Neither proposal has yet been accepted through the repository RFC process. +The current repository draft also incorporates the top-level decision and +additional Gate work merged in +[PR #9](https://github.com/trendmicro/agent-hook-unity/pull/9), plus the buffered +response-delivery control portion of draft RFC 0005. RFC 0005's response-body +inspection and replacement fields remain unimplemented proposals. The +provenance and remaining adoption work are recorded in +[draft RFC 0007](https://github.com/trendmicro/agent-hook-unity/blob/main/rfcs/0007-core-draft-consolidation.md). +None of these proposals has yet been accepted through the repository RFC +process. The separate [Responsible AI Agent Hooks project](https://responsibleai.github.io/agent-hooks/) -also uses the identifier `agent-hooks/0.1`. Its context and verdict documents +uses the identifier `agent-hooks/0.1`. Its context and verdict documents are not this draft's flat event and correlated response documents. Neither -that shared identifier nor the Claude-shaped field names imply wire -compatibility; the applicable schemas and semantics must be identified during -integration. This draft does not define automatic contract negotiation. +that former shared identifier nor the Claude-shaped field names imply wire +compatibility. This draft now uses `agent-hook-unity/0.1`; adopters of earlier +local drafts must explicitly update their configured identifier and schemas. +There is no automatic wire negotiation between the old and new identifiers. diff --git a/spec/0.1/security.md b/spec/0.1/security.md index dc4e098..e09574b 100644 --- a/spec/0.1/security.md +++ b/spec/0.1/security.md @@ -17,9 +17,17 @@ An Agent Hook control response that permits an operation only passes that 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, unless the host supports asynchronous turn -suspension pending an out-of-band approval token. +that requests approval must use a host approval flow. The pending action MUST +NOT proceed while approval is unresolved; non-interactive hosts without a +native suspension mechanism MUST deny instead of assuming consent. + +Once the host accepts a valid denial for a pending action at a Gate boundary, +that denial remains binding across other handler results, delivery retries, +and reevaluation of the same pending action. A failure, timeout, or later +`allow` MUST NOT turn that denial into permission. This rule is scoped to the +same pending action and boundary: a related Post Gate controls a distinct +delivery or ingestion action, and an unrelated operation requires its own +decisions. Only the gate events defined by the event registry may interpret a control response: `SessionStart`, `UserPromptSubmit`, `BeforeModelRequest`, @@ -91,13 +99,16 @@ calls or subagents run concurrently. ## Failure and telemetry -The 0.1 default is fail open: an unavailable or malformed handler response -does not become a denial. Hosts SHOULD record a minimal diagnostic with the -event ID, `hook_event_name`, handler identity, and failure class. They SHOULD -redact or omit prompt text, model data, tool data, secrets, and personal data -from those records. A diagnostic record SHOULD retain only the correlation +The 0.1 default is fail open for an individual unavailable, malformed, errored, +or timed-out handler response: that invocation supplies no control result. It +does not become a denial, erase an accepted denial or rewrite, resolve an +approval, or override native policy. Hosts SHOULD record a minimal diagnostic +with the event ID, `hook_event_name`, handler identity, and failure class. They +SHOULD redact or omit prompt text, model data, tool data, secrets, and personal +data from those records. A diagnostic record SHOULD retain only the correlation identifiers and outcome needed to investigate the event. Handlers SHOULD validate the event schema before producing a security control -response and SHOULD return only one correlated response. A response for a different -event ID is invalid and must fail open under the core protocol. +response and SHOULD return only one correlated response. A response for a +different event ID is invalid and supplies no control result for that +invocation under the Core protocol; it cannot override another valid result. diff --git a/spec/README.md b/spec/README.md index 4cf1537..d374c3c 100644 --- a/spec/README.md +++ b/spec/README.md @@ -1,10 +1,11 @@ # Specification -This directory is the canonical home for normative Agent Hook Spec Markdown. -The [0.1](./0.1/index.md) protocol is an adoption-ready draft proposed by -RFC 0001, with the network, memory, and configuration event expansion proposed -by [RFC 0004](../rfcs/0004-standard-lifecycle-events.md). Neither proposal is -active until accepted. +This directory is the canonical home for normative Agent Hook Unity Markdown. +The [0.1](./0.1/index.md) protocol is a candidate draft for review, consolidated +by [RFC 0007](../rfcs/0007-core-draft-consolidation.md). Its `agent-hook-unity/0.1` +identity distinguishes this contract from Responsible AI Agent Hooks. +Normative language states the candidate's requirements; neither these files +nor a merged draft PR constitute formal RFC acceptance. Accepted RFCs add or amend versioned specification documents here. Normative requirements use RFC 2119 and RFC 8174 terms where applicable. Keep protocol diff --git a/spec/index.md b/spec/index.md index 8015567..0a10b50 100644 --- a/spec/index.md +++ b/spec/index.md @@ -1,10 +1,11 @@ # Specification status -Agent Hook 0.1 is an adoption-ready draft proposed by -[RFC 0001](https://github.com/trendmicro/agent-hook-unity/blob/main/rfcs/0001-agent-hook-core-event-contract.md). -The revised draft also includes the five standard lifecycle events proposed by -[RFC 0004](https://github.com/trendmicro/agent-hook-unity/blob/main/rfcs/0004-standard-lifecycle-events.md). -It is not active until accepted through the process documented in +Agent Hook Unity 0.1 is a candidate draft for review, using the distinct +`agent-hook-unity/0.1` wire identifier. +[RFC 0007](https://github.com/trendmicro/agent-hook-unity/blob/main/rfcs/0007-core-draft-consolidation.md) +records the original RFC 0001 and RFC 0004 proposals, subsequent changes, and +the consolidated baseline. The candidate remains unaccepted; its presence in +the repository does not replace the review and votes required by [GOVERNANCE.md](https://github.com/trendmicro/agent-hook-unity/blob/main/GOVERNANCE.md). The draft's canonical normative documents are in [0.1](./0.1/index.md). diff --git a/website/docs/capabilities.md b/website/docs/capabilities.md index 795026d..1dc4dfa 100644 --- a/website/docs/capabilities.md +++ b/website/docs/capabilities.md @@ -21,17 +21,17 @@ control, rather than which similarly named callbacks it offers. | Mode | Meaning for the declared event | | --- | --- | -| `gate` | The host observes the exact pre-action boundary and can enforce a valid control response there. Only events classified as Gate can use this mode. | +| `gate` | The host observes the registry's exact control boundary and can enforce a valid response before the controlled effect. For a Post Gate, that effect is buffered delivery, rendering, or context ingestion after the underlying work completes. Only registry Gates can use this mode. | | `observe` | The host faithfully emits the event but treats responses as observation or diagnostics, without control effects. A registry Gate may be declared `observe` if its event timing, fields, and correlation are faithful but control is unavailable. | | `partial` | A related native signal cannot meet one or more required timing, field, correlation, control, or privacy semantics. Identify the limitation; do not emit it as a normalized Core event. | | `unavailable` | The host cannot emit the event faithfully. Do not fabricate a Core equivalent. | 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 baseline response-failure default remains -fail open. -Alternative degradation behaviors (such as fail-closed enforcement profiles) -are deferred to dedicated profile RFCs. +mode. A timeout alone supplies no decision and follows the +Core fail-open default, +subject to other valid controls for the same pending action and independent +native policy. ## Suggested report information @@ -43,11 +43,11 @@ additional required wire fields. | Report item | What to record | | --- | --- | | Host and adapter | Names, versions, and the native runtime or SDK version whose behavior was assessed. | -| Specification baseline | Contract name, spec identifier, exact reviewed revision, and the event/response schemas used. `agent-hooks/0.1` alone does not distinguish every draft or another project's contract. | +| Specification baseline | Contract name, spec identifier, exact reviewed revision, and the event/response schemas used. The current identifier is `agent-hook-unity/0.1`; the draft defines no automatic negotiation with the earlier colliding identifier. | | 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`). | +| Handler deadlines and failures | Timeout value and native handling. Under Core, a failed invocation supplies no decision and does not erase other accepted controls for the same pending action. Record any independent native policy separately. | | 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. | @@ -67,7 +67,7 @@ these rows; all evidence listed below is **planned, not run**. | Report item | Illustrative value | | --- | --- | | Host / adapter | Example Host 0.1 / Example Adapter 0.1 (fictional). | -| Spec baseline | Agent Hook 0.1 revised draft with all 18 Core events; an actual report would attach its exact reviewed revision and schemas. | +| Spec baseline | Agent Hook Unity 0.1 revised draft with all 18 Core events; an actual report would attach its exact reviewed revision and schemas. | | Scope | Caller-initiated turns, host-dispatched tools, native approval, per-request completion tracking, and durable memory writes. Model provider internals and delegated agents are not exposed. | | Correlation | Adapter-managed delivery and turn IDs; native tool IDs; operation IDs allocated at the actual operation boundary and retained through approval and completion. | | Evidence status | Every check below is an example of evidence to collect, not an executed result. | @@ -79,12 +79,12 @@ these rows; all evidence listed below is **planned, not run**. | `BeforeModelRequest` | `unavailable` | The complete provider request is not exposed before dispatch. | Document the missing boundary; no normalized event. | | `AfterModelResponse` | `unavailable` | No complete provider terminal-result callback with the required request correlation. | Document the missing result boundary; no normalized event. | | `PreToolUse` | `gate` | All host tool invocations pass through a pre-dispatch callback with the actual proposed input and a stable tool ID. | Valid deny leaves tool invocation count at zero. | -| `PermissionRequest` | `gate` | Native approval callback before resolution; operation and permission-request IDs retained by the adapter. | Nested deny prevents the requested operation; hook allow cannot override native restrictions. | +| `PermissionRequest` | `gate` | Native approval callback before resolution; operation and permission-request IDs retained by the adapter. | Top-level deny prevents the requested operation; hook allow cannot override native restrictions. | | `PermissionDenied` | `observe` | Native rejection result for a concrete approval request, retaining both IDs and the denying authority. | Request/denial pair with the same identifiers. | -| `PostToolUse` | `observe` | Successful terminal tool result with the input and tool ID used for the invocation. | Success pairs with its Pre event; response controls have no effect. | +| `PostToolUse` | `observe` | Successful terminal tool result with the input and tool ID used for the invocation, but the host cannot hold it before context ingestion. | Success pairs with its Pre event; response controls have no effect for this host capability. | | `PostToolUseFailure` | `observe` | Failed or interrupted terminal tool result with the same tool ID. | One observed terminal outcome per invocation, not both success and failure. | | `PreNetworkAccess` | `partial` | Only the initial outbound request has a pre-dispatch callback; internal redirect and retry dispatches cannot be gated. | Record this limitation; no normalized Pre event for this partial mapping. | -| `PostNetworkAccess` | `observe` | Separate per-request completion tracking exposes each request's original target and terminal result, including redirects/retries, with IDs allocated at dispatch. | Distinct request IDs, faithful terminal targets, and no fabricated Pre events. | +| `PostNetworkAccess` | `observe` | Separate per-request completion tracking exposes each request's original target and terminal result, including redirects/retries, but response delivery is not buffered. | Distinct request IDs, faithful terminal targets, no fabricated Pre events, and ignored response controls. | | `PreMemoryWrite` | `gate` | Durable-memory callback before the proposed value becomes persistent or visible. | Deny prevents the write; a changed target or value requires re-evaluation. | | `PostMemoryWrite` | `observe` | Terminal durable-write result retains store, key, operation, and turn IDs. | Correlated success/failure/interruption; no rollback inference. | | `PreConfigChange` | `partial` | A watcher reports the effective configuration only after a mutation took effect. | Record the timing limitation; no normalized Core Pre event. | @@ -98,11 +98,12 @@ per-request completion data. Observing only an initial URL or a final redirect result would not justify that row. Operation IDs still originate at the underlying operation boundary, even when no faithful Pre event can be emitted. -Network and memory Pre/Post capabilities are independently declared by the -current registry. Do not infer equivalent independence for every other pair: -model, tool, approval, and delegation mappings still have their own required -correlation and lifecycle rules. Likewise, a failed or interrupted operation -may have partial effects; its Post event does not prove rollback. +Every event capability is declared independently. Model, tool, network, +memory, approval, and delegation pairs still have their own required +correlation and lifecycle rules. A Gate-capable Post event may therefore be +declared `observe` when the host cannot buffer its controlled delivery or +ingestion boundary. Likewise, a failed or interrupted operation may have +partial effects; its Post event does not prove rollback. ## Applying the report to a real host diff --git a/website/docs/conformance.md b/website/docs/conformance.md index dc00936..b7ca0c2 100644 --- a/website/docs/conformance.md +++ b/website/docs/conformance.md @@ -6,8 +6,8 @@ import Link from '@docusaurus/Link'; # Conformance -Agent Hook 0.1 defines three conformance roles: event producer, handler, and -adapter. The draft's normative requirements and role definitions are in the +Agent Hook Unity 0.1 defines three conformance roles: event producer, handler, +and adapter. The draft's normative requirements and role definitions are in the core protocol. The repository validates the 0.1 event and response JSON Schemas against @@ -29,8 +29,19 @@ event-specific controls and identifies unresolved portable behavior. Schema validation proves document shape, not complete runtime behavior. An adapter review must additionally verify correct event mapping, `event_id` -correlation, decision handling, and the required fail-open behavior for invalid, -missing, timed-out, or errored handler responses. +correlation, decision handling, same-action deny preservation, and the required +fail-open behavior for an individual invalid, missing, timed-out, or errored +handler response. Passing repository schema checks does not establish runtime +conformance. + +The repository's +[Core Gate composition scenarios](https://github.com/trendmicro/agent-hook-unity/blob/main/conformance/gate-composition.md) +state the behavioral cases a host harness should exercise. They cover handler +order and parallel completion, invalid and mismatched responses, deadlines, +redelivery, reevaluation, distinct Pre and Post actions, unrelated operations, +and accepted rewrites. They are review obligations rather than an executable +reference reducer, and a result report is evidence only for the host and +configuration actually tested. For the network and memory event pairs, verify that concurrent operations at the same destination or memory key retain distinct `operation_id` values, and @@ -39,7 +50,14 @@ and retries require separate request boundaries. `PreConfigChange` denial must prevent the change before it takes effect. An Observe callback cannot claim any of these preventive effects. -The revised draft adds five event names. Existing 0.1 schemas do not recognize -them; upgrade schemas and capability declarations and configure compatible -handlers before delivery. Native `ask` still uses the host's approval flow; -no asynchronous approval profile is required. +The revised draft uses `agent-hook-unity/0.1`. The original 13-event schemas do +not recognize the five later additions. The immediate pre-migration 18-event +schemas recognize them but use the colliding `agent-hooks/0.1` identity. Upgrade +schemas and capability declarations and configure compatible handlers before +delivery; do not silently treat the old identity as an alias. Native `ask` +still uses the host's approval flow, and a pending action cannot proceed while +required approval remains unresolved. + +The migration and composition changes are tracked in +[draft RFC 0007](https://github.com/trendmicro/agent-hook-unity/blob/main/rfcs/0007-core-draft-consolidation.md). +The RFC remains unaccepted; this page does not claim an adoption decision. diff --git a/website/docs/governance.md b/website/docs/governance.md index cb38f01..b6f01ef 100644 --- a/website/docs/governance.md +++ b/website/docs/governance.md @@ -4,7 +4,7 @@ sidebar_position: 3 # Governance and RFCs -The Agent Hook Spec is developed in public. A proposal starts in GitHub +The Agent Hook Unity is developed in public. A proposal starts in GitHub Discussions and becomes a formal, numbered RFC only after that conversation has helped shape it. @@ -12,6 +12,12 @@ An RFC remains open for public review for at least 14 days. Acceptance requires at least two active maintainer votes and a simple majority of yes over no votes. The RFC records its Discussion, review window, votes, decision, and status. +Discussions was enabled on 2026-09-21. The +[RFC 0007 consolidation candidate](https://github.com/trendmicro/agent-hook-unity/blob/main/rfcs/0007-core-draft-consolidation.md) +and its preceding RFCs remain unaccepted. Candidate specification and schema +files make the draft reviewable; a merged PR does not replace the prerequisite +Discussion, public review window, or recorded votes. + An RFC number identifies one proposal and stays the same as that proposal is revised. Follow-up implementation PRs reference the existing RFC. Routine fixes and documentation updates do not each require a new RFC; distinct substantial diff --git a/website/docs/intro.md b/website/docs/intro.md index 1f7efac..ec15f89 100644 --- a/website/docs/intro.md +++ b/website/docs/intro.md @@ -5,9 +5,9 @@ sidebar_position: 1 import Link from '@docusaurus/Link'; -# Agent Hook Spec +# Agent Hook Unity -Agent Hook Spec defines a shared lifecycle event and response contract for AI +Agent Hook Unity defines a shared lifecycle event and response contract for AI agents and their tooling. It gives runtime builders and handler authors a common vocabulary for describing an operation, correlating its events, and identifying the boundaries at which a handler can influence execution. @@ -33,7 +33,7 @@ transport, or the order of multiple handlers. ## What the contract guarantees The event registry defines eighteen -Core events: seven **Gate** events and eleven **Observe** events. Gate events +Core events: twelve **Gate** events and six **Observe** events. Gate events can control a pending operation when the host declares and implements that capability. Observe events report a lifecycle boundary; a response does not turn them into preventive controls. @@ -50,37 +50,43 @@ execution order, process lifecycle, authentication, and transport remain host concerns. An interoperable document does not establish identical multi-handler policy composition across hosts. -The 0.1 draft treats missing, invalid, timed-out, or errored handler responses -as providing no Agent Hook control result. For an event declared `gate`, the -default is **fail open**: the operation continues unless an independent native -policy blocks it. Observe responses have no control effect. The contract does -not provide a sandbox or guarantee that every -internal runtime or provider operation is visible. In particular, the Observe -events for completed model and tool results do not promise output filtering -or rollback. The host +The 0.1 draft treats a missing, invalid, timed-out, or errored handler response +as no control result from that invocation. A timeout alone is **fail open**, +subject to other valid decisions for the same pending action and independent +native policy. It cannot erase an accepted denial, rewrite, or approval +requirement. Observe responses have no control effect. Completed model, tool, +and network +operations are Gate-capable only when the host buffers the relevant result; +their controls govern rendering, delivery, or context ingestion and do not +roll back completed work. The contract does not provide a sandbox or guarantee +that every internal runtime or provider operation is visible. The +host obligations and security considerations explain these boundaries. ## Current status -The repository contains an unaccepted **Agent Hook 0.1 draft** proposed by +The repository contains an unaccepted **Agent Hook Unity 0.1 draft** proposed by RFC 0001, with standard network, memory, and configuration events proposed by -RFC 0004. It defines a portable event and response contract, schemas, fixtures, -examples, and adapter guidance. It is not active until accepted through the -public RFC process. +RFC 0004. It defines a portable security and telemetry contract, schemas, +fixtures, examples, and adapter guidance. It is not active until accepted +through the public RFC process. Local agents can use the draft without enterprise identity, remote approval, -or audit services. Earlier 0.1 schemas do not recognize the five added network, -memory, and configuration events; use compatible schemas and configured -handlers as described in the +or audit services. The original 13-event schemas do not recognize the five +later network, memory, and configuration additions. The immediate pre-migration +draft already has 18 events but uses the old wire identity; update schemas and +handler configuration together as described in the versioning rules. -This contract is neither a byte-for-byte Claude Code hook interface nor the +This contract now uses `agent-hook-unity/0.1` on the wire. It is neither a +byte-for-byte Claude Code hook interface nor the separate [Responsible AI Agent Hooks contract](https://responsibleai.github.io/agent-hooks/). -The latter also uses `agent-hooks/0.1`, but its `interception_point` context and +The latter uses `agent-hooks/0.1`, but its `interception_point` context and verdict documents are different from this draft's `hook_event_name` events and -correlated responses. The identifier alone does not establish compatibility. -This clarification does not change the identifier or either contract's behavior. +correlated responses. Earlier local Agent Hook drafts used that same identifier. +Migrating implementations must update their configured identifier and schemas +explicitly; the draft defines no automatic wire negotiation. ## Start reading @@ -94,14 +100,19 @@ The response reference and capability example are informative guides to the canonical specification. They identify unresolved behavior without defining new controls or claiming tested support for a real host. -### Draft response-inspection proposal +### Consolidated draft provenance -[RFC 0005 / PR #8](https://github.com/trendmicro/agent-hook-unity/pull/8) proposes -extending `PostNetworkAccess` to inspect, replace, or withhold response content -before an agent receives it. It keeps the existing Pre/Post pair and all -eighteen event names. The RFC is a draft awaiting the prerequisite Discussion -and formal review; it has not been adopted. The published 0.1 draft still -defines `PostNetworkAccess` as Observe and ignores its control responses. +The repository draft includes the top-level decision and additional Gate work +merged in [PR #9](https://github.com/trendmicro/agent-hook-unity/pull/9). It also +implements the buffered response-delivery control described by +[draft RFC 0005](https://github.com/trendmicro/agent-hook-unity/blob/main/rfcs/0005-network-response-delivery-inspection.md): a `PostNetworkAccess` Gate may +allow or deny delivery after the request completes. RFC 0005's response-body +inspection and replacement fields remain unimplemented proposals. + +[Draft RFC 0007](https://github.com/trendmicro/agent-hook-unity/blob/main/rfcs/0007-core-draft-consolidation.md) +records this consolidation, the wire-identifier migration, and the review work +still required. It does not mark the specification or the earlier RFCs as +accepted. ## Get involved diff --git a/website/docs/participate.md b/website/docs/participate.md index a5c9a8c..ee6fc94 100644 --- a/website/docs/participate.md +++ b/website/docs/participate.md @@ -7,6 +7,11 @@ sidebar_position: 2 GitHub Discussions is the home for questions, ideas, use cases, and RFC proposals. Start there before opening a formal RFC pull request. +Discussions was enabled on 2026-09-21. The current consolidation candidate is +preparatory material and has not started formal review; see +[RFC 0007](https://github.com/trendmicro/agent-hook-unity/blob/main/rfcs/0007-core-draft-consolidation.md) +for its scope and pending decision record. + Use GitHub Issues for reproducible repository problems, such as a broken link, an invalid fixture, or a failing build. Sensitive reports belong in the private channel described by the repository security policy. diff --git a/website/docs/responses.md b/website/docs/responses.md index 1d59fea..bd5263e 100644 --- a/website/docs/responses.md +++ b/website/docs/responses.md @@ -22,21 +22,26 @@ remain authoritative. This page introduces no new wire fields or control semanti 4. Apply only the event-specific control semantics below. Hook permission never overrides native approval, sandbox, organization, managed-policy, or platform restrictions. -A response can pass the schema and still fail the request/response pairing checks. -An incorrect event ID or event name is invalid and follows the -existing fail-open rule: -there is no Agent Hook control result; a declared Gate continues unless independent -native policy blocks it. Missing responses, malformed JSON, schema errors, handler -errors, and timeouts follow that same rule. +A response can pass the schema and still fail the request/response pairing +checks. An incorrect event ID or event name supplies no control result from +that handler invocation. Missing responses, malformed JSON, schema errors, and +handler errors behave the same way. A timeout alone follows the +Core fail-open default, +subject to independent native policy, but no failure can erase another +accepted denial, rewrite, or approval requirement for the same pending action. ## Control by Gate 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`). +(payload mutations), a handler with control or mutation intent MUST provide a +top-level `decision` (`allow`, `deny`, `ask`, `defer`, or legacy `block`), except +when using supported legacy nested controls without a top-level decision. +Top-level `reason` is optional except for `deny` or `block`, which require it. +Passive observation or metadata responses need no control decision. When no rewrite is needed, `hookSpecificOutput` may be omitted entirely. For backward compatibility, `permissionDecision` inside `hookSpecificOutput` remains accepted. +It is a legacy fallback only when top-level `decision` is absent. | Gate | Canonical control path | Controlled boundary and effect | Defined rewrite support (`hookSpecificOutput`) | | --- | --- | --- | --- | @@ -45,7 +50,7 @@ compatibility, `permissionDecision` inside `hookSpecificOutput` remains accepted | `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`. | +| `PermissionRequest` | `decision: "allow"` or `"deny"` | Controls the native approval request. | Native-shaped `updatedInput` and `updatedPermissions` may be carried under `hookSpecificOutput.decision`; its `behavior` is a legacy fallback when top-level `decision` is absent. | | `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. | @@ -53,32 +58,34 @@ compatibility, `permissionDecision` inside `hookSpecificOutput` remains accepted | `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 +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; 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. +An `allow` supplies a permissive result from that handler invocation; it cannot +override another accepted denial or independent native policy. A response +requesting approval 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. 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 +For all event-supported rewrites and native approval updates listed in the +Core Gate table, 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. For `PreMemoryWrite`, `updatedContent` permits surgical content sanitization -before persistent storage. See the +For `PreNetworkAccess`, `PreMemoryWrite`, and `PreConfigChange`, the decision +covers the presented operation, target, and proposed values. A change before +dispatch or mutation requires Gate evaluation again. For `PreMemoryWrite`, +`updatedContent` permits surgical content sanitization before persistent +storage. See the mutation preconditions. For a declared `observe` capability, all response control fields are ignored for @@ -101,7 +108,7 @@ the stated portable behavior. These are documentation labels, not wire values. | Field | Schema shape | Current meaning or boundary | | --- | --- | --- | -| `spec` | Required; exactly `"agent-hooks/0.1"` | Identifies the contract; not native response compatibility. | +| `spec` | Required; exactly `"agent-hook-unity/0.1"` | Identifies this draft contract; not native response compatibility or an automatic negotiation token. | | `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` | `"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. | @@ -132,8 +139,8 @@ Paths here are relative to `hookSpecificOutput`. | `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. | +| `decision` | Object; requires `behavior` | Native-shaped approval details for `PermissionRequest`; its control is a legacy fallback when root `decision` is omitted. | +| `decision.behavior` | `allow` or `deny` | Legacy approval control when root `decision` is omitted; independent policy still applies. | | `decision.updatedInput` | Object; arbitrary properties | Optional approval-boundary input update; portable validation/application rules remain open. | | `decision.updatedPermissions` | Array; unconstrained items | Optional native permission updates; not a portable permission policy language. | | `decision.message` | String | Optional native approval message; consumer/display behavior remains open. | @@ -158,15 +165,15 @@ The repeated ID is for readability across independent examples; real deliveries have unique IDs. These response documents illustrate shape and existing semantics, not execution evidence for a particular host. -### Block an accepted prompt +### Deny an accepted prompt Request event: `UserPromptSubmit`. `hookSpecificOutput` is unnecessary for this control. ```json { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "11111111-1111-4111-8111-111111111111", - "decision": "block", + "decision": "deny", "reason": "This request exceeds the permitted task scope." } ``` @@ -177,29 +184,25 @@ Request event: `PreToolUse`. For a declared Gate, the denied invocation does not ```json { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "11111111-1111-4111-8111-111111111111", - "hookSpecificOutput": { - "hookEventName": "PreToolUse", - "permissionDecision": "deny", - "permissionDecisionReason": "The proposed tool action exceeds the permitted task scope." - } + "decision": "deny", + "reason": "The proposed tool action exceeds the permitted task scope." } ``` ### Deny a native approval request -Request event: `PermissionRequest`. The nested `behavior` belongs to this event; -it is not interchangeable with `permissionDecision`. +Request event: `PermissionRequest`. The canonical decision is top-level; the +older nested `hookSpecificOutput.decision.behavior` form remains a fallback +only when the top-level field is absent. ```json { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "11111111-1111-4111-8111-111111111111", - "hookSpecificOutput": { - "hookEventName": "PermissionRequest", - "decision": { "behavior": "deny" } - } + "decision": "deny", + "reason": "The requested operation is outside the approved scope." } ``` @@ -211,11 +214,11 @@ their structure. `allow` does not bypass independent native policy. ```json { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "11111111-1111-4111-8111-111111111111", + "decision": "allow", "hookSpecificOutput": { "hookEventName": "BeforeModelRequest", - "permissionDecision": "allow", "updatedMessages": [{ "role": "user", "content": "Summarize the public project overview." }] } } @@ -223,12 +226,13 @@ their structure. `allow` does not bypass independent native policy. ### Return metadata for an observation -Request event: `PostToolUse`. This response can support optional diagnostics; -it cannot change the completed invocation. +Request event: `PostToolUse`, delivered by a host that declares this registry +Gate as `observe`. This response can support optional diagnostics; it cannot +control context ingestion or change the completed invocation. ```json { - "spec": "agent-hooks/0.1", + "spec": "agent-hook-unity/0.1", "event_id": "11111111-1111-4111-8111-111111111111", "metadata": { "audit_id": "example-observation-1", @@ -239,10 +243,12 @@ it cannot change the completed invocation. ## Decisions still needed -The draft does not establish precedence for conflicting common and event-specific -controls, a universal default for every valid response that omits controls, -portable rewrite validation, or multi-handler composition. Schema acceptance is -not a substitute for those decisions. Native-shaped fields also do not imply -byte-for-byte Claude Code response compatibility. See +The draft does not establish precedence for conflicting common fields, a +universal default for every valid response that omits controls, handler +scheduling, or rewrite conflict resolution beyond preserving valid controls +for the same pending action. Native validation of a rewrite is defined: failure +is closed and cannot restore the original value. Schema acceptance is not a +substitute for runtime composition or enforcement. Native-shaped fields also +do not imply byte-for-byte Claude Code response compatibility. See boundaries still open in 0.1 for the distinction between host-specific behavior and changes requiring a future proposal. diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index 96848a3..57099b9 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -5,7 +5,7 @@ const [organizationName, projectName] = repository.split('/'); const repositoryUrl = `https://github.com/${repository}`; const config: Config = { - title: 'Agent Hook Spec', + title: 'Agent Hook Unity', tagline: 'A portable lifecycle-hook protocol for AI agents and tooling', favicon: 'img/favicon.svg', url: `https://${organizationName}.github.io`, @@ -56,7 +56,7 @@ const config: Config = { themeConfig: { image: 'img/social-card.svg', navbar: { - title: 'Agent Hook Spec', + title: 'Agent Hook Unity', items: [ { to: '/', label: 'Overview', position: 'left' }, { to: '/participate', label: 'Participate', position: 'left' }, @@ -86,7 +86,7 @@ const config: Config = { ] } ], - copyright: `Copyright © ${new Date().getFullYear()} Agent Hook Spec contributors. Documentation is CC BY 4.0; code is MIT.` + copyright: `Copyright © ${new Date().getFullYear()} Agent Hook Unity contributors. Documentation is CC BY 4.0; code is MIT.` }, prism: { theme: { plain: { color: '#f8fafc', backgroundColor: '#0f172a' }, styles: [] }, diff --git a/website/static/img/favicon.svg b/website/static/img/favicon.svg index d0d259d..7f2f769 100644 --- a/website/static/img/favicon.svg +++ b/website/static/img/favicon.svg @@ -1,4 +1,4 @@ - + diff --git a/website/static/img/social-card.svg b/website/static/img/social-card.svg index ebd00a9..5e7fae0 100644 --- a/website/static/img/social-card.svg +++ b/website/static/img/social-card.svg @@ -2,6 +2,6 @@ - Agent Hook Spec + Agent Hook Unity A portable lifecycle-hook protocol for AI agents diff --git a/website/static/schemas/0.1/hook-event.schema.json b/website/static/schemas/0.1/hook-event.schema.json index eb31e43..e4f5b98 100644 --- a/website/static/schemas/0.1/hook-event.schema.json +++ b/website/static/schemas/0.1/hook-event.schema.json @@ -1,8 +1,8 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://trendmicro.github.io/agent-hook-standard/schemas/0.1/hook-event.schema.json", - "title": "Agent Hook 0.1 Request", - "description": "A Claude Code-derived, security-telemetry request for an Agent Hook 0.1 event.", + "$id": "https://trendmicro.github.io/agent-hook-unity/schemas/0.1/hook-event.schema.json", + "title": "Agent Hook Unity 0.1 Request", + "description": "A Claude Code-derived, security-telemetry request for an Agent Hook Unity 0.1 event.", "$ref": "#/$defs/HookRequest", "$defs": { "Uuid": { @@ -66,7 +66,7 @@ "CommonRequest": { "type": "object", "properties": { - "spec": { "const": "agent-hooks/0.1" }, + "spec": { "const": "agent-hook-unity/0.1" }, "event_id": { "$ref": "#/$defs/Uuid" }, "hook_event_name": { "$ref": "#/$defs/EventName" }, "session_id": { "type": "string", "minLength": 1 }, diff --git a/website/static/schemas/0.1/hook-response.schema.json b/website/static/schemas/0.1/hook-response.schema.json index aacb7be..e4b083f 100644 --- a/website/static/schemas/0.1/hook-response.schema.json +++ b/website/static/schemas/0.1/hook-response.schema.json @@ -1,8 +1,8 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://trendmicro.github.io/agent-hook-standard/schemas/0.1/hook-response.schema.json", - "title": "Agent Hook 0.1 Response", - "description": "A correlated Claude Code-shaped response to an Agent Hook 0.1 request.", + "$id": "https://trendmicro.github.io/agent-hook-unity/schemas/0.1/hook-response.schema.json", + "title": "Agent Hook Unity 0.1 Response", + "description": "A correlated Claude Code-shaped response to an Agent Hook Unity 0.1 request.", "type": "object", "additionalProperties": false, "required": [ @@ -11,7 +11,7 @@ ], "properties": { "spec": { - "const": "agent-hooks/0.1" + "const": "agent-hook-unity/0.1" }, "event_id": { "$ref": "#/$defs/Uuid"