Skip to content

Guard managed-hook contracts against agent binary drift (version attestation + headless contract tests) #726

Description

@onevcat

Why

Managed hooks (docs-ai 064, S3a/S3b) are a contract with external agent binaries, and that contract moves. While closing S3b, three drifts surfaced that no unit test could have caught, because the stubs encode exactly the assumptions that rotted:

Layer What drifted Where it was found
Process shape Droid ≥ 0.202 forks a droid exec engine child; hooks descend from it 009-s3b-action.md
Preflight protocol Codex 0.149.1 app-server exits on stdin EOF and drops queued config/read 007-s3a-action.md follow-up
Injection surface Qoder flag hooks are folder-trust gated (research doc had said otherwise) 009-s3b-action.md

The unit suite holds Prowl-side invariants well (argv/settings rendering, payload decoding, the observation store state machine, caller attribution). It structurally cannot detect binary drift. The only test touching a real binary today is CodexConfigReadLiveContractTests, env-gated and Codex-only — and it silently reports success when the variable is passed without the TEST_RUNNER_ prefix, so it may never have run in earlier gates.

When

After S3c lands. S3c adds Pi, Oh My Pi, and OpenCode to the tier-A set, so the drift guard should be designed once for all eight runtimes rather than retrofitted per slice.

Scope

T0 — Version attestation (cheap, zero runtime cost)

  • An attestation record (e.g. agent-attestation.json next to the research matrix) listing, per runtime, the version the contract last passed against and the date.
  • make agent-versions: compares installed versions (--version of each binary) with the attestation; newer-than-attested → warning, with a hint to run T1.
  • Optional: a scheduled check of npm/brew latest versions so a new release is noticed before someone upgrades.

T1 — Headless contract tests (real binaries, no TUI, deterministic assertions)

One test per runtime, using its one-shot mode (claude -p, codex exec, droid exec, qodercli -p, Copilot's headless mode, Pi/OMP/OpenCode equivalents):

  • Build argv and settings through the production renderers (ManagedHookRendering, AgentManagedHookPreparer) — the test must exercise the same injection the app performs.
  • Point the hook command at the real bundled CLI against a stub Unix socket (the fakesock approach used during S3b), or at a capture script.
  • Assert the received frame decodes through the production AgentNativeHookPayload decoder with the expected runtime, native event → signal mapping, cwd, and session id.
  • Assert SessionStart / SessionEnd without a model turn; assert Stop / agent-turn-complete with one turn on the cheapest model when credentials are present.
  • Include the Codex preflight (config/read) as it exists today.
  • make test-agent-contracts bakes the TEST_RUNNER_ prefix in so the gate can never be silently skipped; passing runs update the T0 attestation.
  • Run locally on demand: before a release, and whenever a runtime is upgraded. Not a CI job (credentials, spend).

Non-goals

  • Interactive E2E through the app (isolated Debug instance + prowl create tab --profile + trust prompts). It is the only layer that covers process shape, but it drifts on its own (TUI prompts, trust dialogs, flaky deferred surface creation) and costs GUI time per run. Keep the S3b sweep script as a manual release-runbook aid; do not productize it now.
  • Field telemetry ("hook registered but never verified") — deferred.

Acceptance

  • make agent-versions reports the attestation state for every tier-A runtime.
  • make test-agent-contracts runs one real-binary contract per tier-A runtime and fails loudly when a binary is missing, unauthenticated, or drifts.
  • The research matrix's baseline versions are derived from the attestation record instead of being hand-edited.

Refs

  • PR Add managed completion hooks for Copilot, Droid, and Qoder #725 (S3b) — Droid launch-process fix and the Codex 0.149.1 pipe fix
  • docs-ai/064-agent-completion-signals/007-s3a-action.md, 009-s3b-action.md, research-agent-completion-signals.md
  • supacodeTests/CodexConfigReadLiveContractTests.swift — the pattern to generalize

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions