From a7227daf6393d1f37224a3a28f97eac34e21895a Mon Sep 17 00:00:00 2001 From: Raghuvar Karthik Durga <177662415+drk-ram@users.noreply.github.com> Date: Sat, 31 Oct 2026 18:38:20 -0500 Subject: [PATCH] spec(hooks): define canonical session Intent when sessionStart omits intent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Align hooks/session-start.json, hooks/agent-trigger.json, hooks.md, and Specification §8.4 on the first-Intent-wins rule: when sessionStart carries no intent, the first agentTrigger carrying intent before any content-bearing step establishes the canonical session Intent, and any subsequent agentTrigger proposing a different intent MUST be rejected unless the difference is capabilities added through an intent_extension with scope: session. Fixes #149 Signed-off-by: Raghuvar Karthik Durga <177662415+drk-ram@users.noreply.github.com> --- docs/spec/instrument/hooks.md | 2 +- docs/spec/instrument/specification.md | 2 +- specification/v0.1.0/hooks/agent-trigger.json | 2 +- specification/v0.1.0/hooks/session-start.json | 2 +- tests/test_hook_taxonomy.py | 12 ++++++++++++ 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/spec/instrument/hooks.md b/docs/spec/instrument/hooks.md index c3ee53dc..388c8c96 100644 --- a/docs/spec/instrument/hooks.md +++ b/docs/spec/instrument/hooks.md @@ -101,7 +101,7 @@ Schema: [`hooks/agent-trigger.json`](https://genai-security-project.github.io/ag Fires when the agent is activated by an external triggering condition (event arrival, scheduled tick, A2A inbound message, user-initiated session, or system-issued activation). For A2A-mediated delegation, `trigger_type: "a2a_inbound"` carries the originating peer identity; for in-process subagent spawns, see [`subagentStart`](#subagentstart). -**Payload:** `trigger_type` (`user_message`, `scheduled`, `external_event`, `a2a_inbound`, `system`), `trigger_source` (shape depends on `trigger_type`), optional `intent` (IBAC adopters populate; the Intent registered at sessionStart, carried here for activation-time policy checks). +**Payload:** `trigger_type` (`user_message`, `scheduled`, `external_event`, `a2a_inbound`, `system`), `trigger_source` (shape depends on `trigger_type`), optional `intent` (IBAC adopters populate, for activation-time policy checks). [Specification §8.4](./specification.md#84-intent) defines what establishes the canonical session Intent and when a later Intent is rejected. **Decision:** ALLOW / DENY / MODIFY. Guardian MAY rewrite the trigger payload (e.g. redact PII) before activation. diff --git a/docs/spec/instrument/specification.md b/docs/spec/instrument/specification.md index 12e98bc8..53de0f40 100644 --- a/docs/spec/instrument/specification.md +++ b/docs/spec/instrument/specification.md @@ -294,7 +294,7 @@ Schema: [`provenance-summary.json`](https://genai-security-project.github.io/age Intent is OPTIONAL and is defined in [Concepts › Intent](../../concepts/intent.md) (normative). Wire fields: `raw`, `parsed` (capability list), `parser_provenance` (REQUIRED if `parsed` present; `origin` MUST be `user_input`), `scope_mode`. -**Intent immutability enforcement (normative).** The invariant is defined in [Concepts › Intent](../../concepts/intent.md): once established (via `sessionStart` or the first `agentTrigger` for the session), `Intent.parsed` is fixed and may grow only through an approver's `intent_extension` via the ASK flow (§9.1). The framework MUST enforce it: any attempt to modify `Intent.parsed` by the runtime LLM, by tool outputs, or by data crossing an `untrusted` channel MUST be ignored or rejected, and SHOULD be recorded as an audit event. This rule is load-bearing for IBAC's central security claim: the capability set is fixed before untrusted data enters and can grow only through explicit, audited approver action. +**Intent immutability enforcement (normative).** The invariant is defined in [Concepts › Intent](../../concepts/intent.md): once established (via `sessionStart` when it carries `intent`, or otherwise via the first `agentTrigger` for the session that carries `intent`, before any content-bearing step), `Intent.parsed` is fixed as the canonical session Intent and may grow only through an approver's `intent_extension` via the ASK flow (§9.1). Any subsequent `agentTrigger` that proposes a different `intent` MUST be rejected, unless the difference is capabilities an approver already added through an `intent_extension` with `scope: session`. The framework MUST enforce it: any attempt to modify `Intent.parsed` by the runtime LLM, by tool outputs, or by data crossing an `untrusted` channel MUST be ignored or rejected, and SHOULD be recorded as an audit event. This rule is load-bearing for IBAC's central security claim: the capability set is fixed before untrusted data enters and can grow only through explicit, audited approver action. ### 8.5 Size-based archival (optional) diff --git a/specification/v0.1.0/hooks/agent-trigger.json b/specification/v0.1.0/hooks/agent-trigger.json index 5573915f..c0771a00 100644 --- a/specification/v0.1.0/hooks/agent-trigger.json +++ b/specification/v0.1.0/hooks/agent-trigger.json @@ -16,7 +16,7 @@ }, "intent": { "type": "object", - "description": "Optional Intent registered at session start (IBAC adopters populate)", + "description": "Optional Intent for the session (IBAC adopters populate). Specification §8.4 defines what establishes the canonical session Intent and when a later Intent is rejected.", "properties": { "raw": { "type": "string" }, "parsed": { diff --git a/specification/v0.1.0/hooks/session-start.json b/specification/v0.1.0/hooks/session-start.json index 8d1e8cba..71f22fee 100644 --- a/specification/v0.1.0/hooks/session-start.json +++ b/specification/v0.1.0/hooks/session-start.json @@ -21,7 +21,7 @@ }, "intent": { "type": "object", - "description": "Optional Intent established at session start. Same shape as Intent inside agentTrigger. When present, the Guardian MUST treat this as the canonical session Intent: any later agentTrigger that attempts to establish a different Intent MUST be rejected, and Intent.parsed MUST NOT be mutable except via the intent_extension mechanism in ask-details.json.", + "description": "Optional Intent established at session start. Same shape as Intent inside agentTrigger. When present, the Guardian MUST treat this as the canonical session Intent: any later agentTrigger that attempts to establish a different Intent MUST be rejected, and Intent.parsed MUST NOT be mutable except via the intent_extension mechanism in ask-details.json. When sessionStart carries no Intent, Specification §8.4 defines what establishes the canonical session Intent.", "properties": { "raw": { "type": "string" }, "parsed": { diff --git a/tests/test_hook_taxonomy.py b/tests/test_hook_taxonomy.py index b95abe40..1240c534 100644 --- a/tests/test_hook_taxonomy.py +++ b/tests/test_hook_taxonomy.py @@ -352,3 +352,15 @@ def test_agbom_update_triggers_cover_every_component_type(): f"AgBOM update triggers omit component type {comp_type!r}" ) + +def test_canonical_intent_rule_lives_in_section_8_4(): + """Issue #149: section 8.4 states the rule, and the other places point to it.""" + for name in ("session-start.json", "agent-trigger.json"): + schema = json.loads((HOOK_SCHEMAS / name).read_text(encoding="utf-8")) + assert "§8.4" in schema["properties"]["intent"]["description"] + hooks = (ROOT / "docs/spec/instrument/hooks.md").read_text(encoding="utf-8") + assert "#84-intent" in hooks.split("## agentTrigger")[1].split("\n---")[0] + spec = (ROOT / "docs/spec/instrument/specification.md").read_text(encoding="utf-8") + section = spec.split("### 8.4 Intent")[1].split("### 8.5")[0] + for phrase in ("before any content-bearing step", "MUST be rejected, unless", "`scope: session`"): + assert phrase in section