Skip to content

spec(hooks): define canonical session Intent when sessionStart omits intent - #340

Open
drk-ram wants to merge 1 commit into
GenAI-Security-Project:integrationfrom
drk-ram:spec/canonical-intent-149
Open

drk-ram wants to merge 1 commit into
GenAI-Security-Project:integrationfrom
drk-ram:spec/canonical-intent-149

Conversation

@drk-ram

@drk-ram drk-ram commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

What changed

Aligns specification/v0.1.0/hooks/session-start.json, specification/v0.1.0/hooks/agent-trigger.json, docs/spec/instrument/hooks.md, and docs/spec/instrument/specification.md (§8.4) with docs/concepts/intent.md: when sessionStart omits intent (for example on unattended scheduled, external_event, a2a_inbound, or system activations), the first agentTrigger carrying intent establishes the canonical session Intent, and any subsequent agentTrigger proposing a different intent MUST be rejected unless widened via ask-details.json's intent_extension.

Which issue does this implement

Fixes #149

  • This is an editorial correction (typo, grammar, link, formatting) with no change in
    meaning, so it needs no issue

Base branch

  • integration, because this touches the specification, schemas, a reference
    implementation, an adapter, tests, or CI
  • main, because every changed path is on the documentation lane allowlist in
    CONTRIBUTING.md

Type of change

  • Specification change (schema, hooks, events, AgBOM)
  • Reference implementation or adapter
  • Documentation
  • Tooling or CI
  • Governance (licensing, security policy, contributor docs)

Specification changes

  • I opened a Discussion before this PR
  • Schema changes validate against the JSON Schema spec
  • I described the impact on downstream implementers below

Breaking for implementers? No wire shape changes. Clarifies the description fields in session-start.json and agent-trigger.json and aligns hooks.md and specification.md §8.4 with the existing rule in docs/concepts/intent.md.

I tested this

  • I synced my branch with the base branch before opening this
  • uv run pytest -v passes on my machine
  • uv run mkdocs build --strict passes on my machine

Checklist

  • Commits are signed off with git commit -s (required by the DCO)
  • Prose follows STYLE.md
  • No secrets, tokens, or internal URLs in the diff

Security

  • This change has no security impact

Closes the unattended-activation gap on scheduled, external_event, a2a_inbound, and system triggers by ensuring the first agentTrigger carrying intent locks Intent.parsed as the canonical session Intent when sessionStart omitted it.

@github-actions

Copy link
Copy Markdown

This pull request's description says it closes #149, which is on the project roadmap. When it merges, GitHub closes that issue and the roadmap counts it as delivered. Please confirm the change delivers the whole issue before merging.

@rocklambros rocklambros left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raghuvar, first wins is what docs/concepts/intent.md:9 already says. Your schema edits change only description text, so the wire shape stays the same (I compared both files). The merged tree on integration 19b1197 passes every suite.

@bar-capsule @afogel, this needs no ruling on first wins. It does add a MUST at specification.md:297, so it needs one of you. Today only session-start.json says a different Intent is rejected, and only when sessionStart carries one. The new line covers every session.

That line leaves three questions open. Two Guardians could answer each one differently, which is what issue #149 reports.

  1. What does "rejected" mean on the wire? §17.1 has no error code for it. The next sentence in §8.4 says "ignored or rejected", so an a2a_inbound trigger gets both answers. Do you want deny on the step, and with which reason code? Or may a Guardian keep the first Intent and drop the new one? The reference store drops it (session-context-store.ts:272), though nothing calls that code yet.

  2. What counts as "different"? An intent has four fields, and §8.4 fixes only parsed. Is a new raw or provenance_id different? Is a shorter parsed list different? I'd compare parsed as a set, but that's your decision.

  3. Which Intent can establish, and what happens when none does? §8.4 requires parser_provenance.origin to be user_input, so an honest Intent on an a2a_inbound or external_event trigger can't establish anything. A second trigger can't either, because hooks.md:330 calls agentTrigger the first content-bearing hook. Does an Intent on a trigger the Guardian denied count? When no Intent is established, does the session run without one, and is a late Intent rejected?

This line also works only inside one session. If each scheduled run opens a new session, there's no earlier Intent to compare with, which is the last case in issue #149. I'm not asking for a change there.

Once you answer, Raghuvar can push and one of you can approve and merge. That closes issue #149 and lets PR #341 go next.

Raghuvar, please wait for those answers, so you only push once. I have four asks. I applied them to a scratch copy, and pytest and mkdocs build --strict pass. If Bar or Ariel answer differently, change ask 1 to match.

  1. specification.md:297 needs two small changes. First, "sessionStart when present" can mean the hook, and then a sessionStart with no intent never reaches "otherwise". Change it to "sessionStart when it carries intent".

    Second, the reject sentence has no exception for an approved extension, but your PR description says "unless widened". After an extension with scope: session, Intent.parsed is larger (§9.1). A later trigger then differs from the first Intent or from the larger one, so this line rejects it. Change the sentence to:

    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`.
    
  2. Please write the rule only in §8.4. The two schemas and hooks.md drop "before any content-bearing step", so those three let a late Intent establish and §8.4 doesn't. agent-trigger.json:19 also adds a second new MUST ("MUST match it"). SPEC_REVIEW_PRINCIPLES.md section 1 asks us to define a rule once. Use this text:

    • agent-trigger.json:19, the whole 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.
    • session-start.json:24, the last sentence: When sessionStart carries no Intent, Specification §8.4 defines what establishes the canonical session Intent.
    • hooks.md:104, from optional to the end: 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.
  3. Your test at tests/test_hook_taxonomy.py:330 checks two phrases and reads neither doc. I changed agent-trigger.json to say a first trigger "does not establish" the Intent, then changed MUST to MAY, then deleted the reject clause. The test passed all three times. Replace it with this one, which also tests asks 1 and 2. It failed on all 7 ways I broke the new text. End the file with one newline.

    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
  4. Please rebase after PR #338 lands. Both PRs add a test at the end of tests/test_hook_taxonomy.py, so Git will show a conflict in that file. The conflict is only about position, so keep both tests. I tried this on a scratch copy: 926 tests collected, 925 passed, 1 skipped (the usual one). PR #341 has the same conflict, so keep every test there too.

    Test: uv run pytest tests/test_hook_taxonomy.py passes, and both test names are in the file.

Issue #149 has no assignee yet, because GitHub still won't let me assign you. It's the same step as on your other PRs, so please leave a comment on the issue. After you push, I'll run all of this again.

…intent

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 GenAI-Security-Project#149

Signed-off-by: Raghuvar Karthik Durga <177662415+drk-ram@users.noreply.github.com>
@drk-ram

drk-ram commented Oct 31, 2026

Copy link
Copy Markdown
Collaborator Author

@rocklambros Rebased onto integration (1b8f970) after PR #338 merged and applied asks 1 to 4 in a7227da (pytest tests/test_hook_taxonomy.py passes with both the PR #338 and PR #340 tests).

@drk-ram
drk-ram force-pushed the spec/canonical-intent-149 branch from 931f598 to a7227da Compare October 31, 2026 23:38

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

[Bug] Canonical Intent is undefined when sessionStart carries none, which is the unattended activation path

2 participants