<!-- source: openspec/changes/close-untrusted-input-dispatch-chain/verification.md -->

---
audience:
  - executor
---
# Verification — close-untrusted-input-dispatch-chain

> Companion to proposal.md / design.md / tasks.md.

## Spec alignment

"Done" means: the three read-only tools (`linear_get_issue`, `fetch_url`, `web_search`) return author-/web-controlled content inside a `wrapUntrustedInput` boundary; the Forge MCP server spawns its CLI with an allowlist-derived environment (no blanket `process.env`); and `agents-threat-model.md` carries `AT-16` + `AT-17` covering the web-fetch ingest path and the MCP-spawn credential-inheritance chain. The chain is closed — injected instructions in Linear/web content no longer arrive as trusted text, and the spawned process no longer carries the full secret set.

## Hallucination risks

MUST appear:

- the four openspec files under `openspec/changes/close-untrusted-input-dispatch-chain/` (new).
- edits to `linear_get_issue.ts`, `fetch_url.ts`, `web_search.ts` (wrapping).
- edits to `mcp-server/spawn.ts` + `mcp-server/tools/dispatch.ts` (env scoping).
- two new rows `AT-16` + `AT-17` in `agents-threat-model.md`.

MUST NOT appear:

- No new boundary primitive — only `wrapUntrustedInput` is used (`D-UID-1`).
- No schema change to any review/audit/build `Finding` type — that is `add-review-confidence-gate`.
- No new MCP server, no new tool registration, no allowlist-membership lint (F-12 is the follow-on detector).
- No `--auto-*` confirmation gating (`Q-UID-1` deferred).
- No edits under `openspec/current/`.
- No `git push`, no PR opened, no `--auto-push`/`--auto-pr` dispatch run.

## Pattern references

- `openspec/_research/2026-06-13-agent-harness-hardening-audit.md` — source findings + the falsifiable acceptance test.
- `architect/repo-tools.ts:274`, `run_headless.ts:139` — already-wrapped paths; the pattern to copy, must stay untouched (zero false positives).
- `agents-threat-model.md` `AT-01..15` — row format for `AT-16`/`AT-17`.

## Acceptance

### Wave 2 — input wrapping (falsifiable)

- Done when: an injection-boundary scan over `packages/muse/src/tools/*.ts` flags **zero** untrusted-result tools returning content without `wrapUntrustedInput` — i.e. `linear_get_issue.ts`, `fetch_url.ts`, `web_search.ts` are now wrapped — with **zero false positives** on the already-wrapped `architect/repo-tools.ts` and `run_headless.ts`. (This is the audit's exact acceptance test, now expected to pass.)
- Done when: a unit test asserts the `<untrusted_input>` marker wraps the Linear description + comment bodies + title and the web content/results, and does **not** wrap Linear structural metadata.

### Wave 3 — env scoping (falsifiable)

- Done when: `buildSpawnEnv(process.env)` returns a set ⊆ (`SPAWN_ENV_ALLOWLIST` ∪ system vars); a unit test asserts an off-allowlist token is absent from the result.
- Done when: a real `forge` dispatch **without** `--auto-push`/`--auto-pr` completes successfully against the scoped env (proves no over-scrub).

### Wave 4 — threat model

- Reviewer confirms: `AT-16` + `AT-17` present, in the `AT-01..15` format, each with a post-mitigation RPN ≤ 10 consistent with the register.

### Wave 5 — gates

- Done when: `node tools/check-openspec-refs.mjs` exits 0.
- Done when: touched-package tests are green.

### Sign-off

- Pavel confirms the diff before any merge/push (agents prepare, humans sign).
