fix(acp): extend description extraction to codex v1.1.7; protect prose gate with integrated render test

B1 (F2 — codex v1.1.7 command/file-change approvals):
- Add two extraction paths to `description_from_request_permission`:
  4. `params.toolCall.rawInput.command` — codex v1.1.7 command execution
     (no title; command string in rawInput). Wire shape cited from tag v1.1.7.
  5. `params._meta.codex.params.reason` — codex v1.1.7 file-change
     (no title or rawInput; reason in codex metadata). Wire shape cited.
- Two new Rust tests use verbatim v1.1.7 wire shapes from
  `buildCommandPermissionRequest` / `buildFileChangePermissionRequest`
  in `codex-acp tag v1.1.7 CodexApprovalHandler.ts`. Both go red if the
  extraction pointers are changed back.
- Three new PermissionRequestCardBlock component tests (Carl's F2 bar):
  hostile markup renders as inert text (no <script>/<b> created),
  control characters render safely, description accessible to AT
  (not aria-hidden).

B2 (F3 — MessageRow call-site mutation protection):
- Replace `hasPermissionRequestCard` with `selectPermissionRequest` in
  `permissionRequestAuthPubkey.ts`: computes the full card payload ONCE,
  folding in channelId + isAgent + signer trust + computePermissionRequest.
  Returns `PermissionRequestSelection | null` so prose suppression and the
  card block share a single result — prose suppressed iff card renders,
  by construction.
- MessageRow now calls `selectPermissionRequest(message, isKnownAgentPubkey,
  channelId)` once (line 382), uses `permReq !== null` as the prose gate
  (line 431), and passes `permReq` to `PermissionRequestCardBlock`.
- Add `MessageRowProseGate.test.mjs`: renders the REAL MessageRow with
  a forged-signer sentinel (signerPubkey ≠ registered agent). Asserts
  the sentinel body nonce appears in DOM prose. Goes red when line 431
  is mutated from `permReq !== null` to `isPermissionRequestSentinel(body)`
  (Thufir's exact mutation). Stubs `useAppNavigation` (used by markdown.tsx
  and SentFromThreadLine via router hooks) and `MessageActionBar` so the
  test runs in JSDOM without a live RouterProvider.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
Duncan
2026-08-29 19:48:24 -04:00
parent 05246f65a9
commit 05b496eda4
9 changed files with 861 additions and 109 deletions

View File

@@ -354,9 +354,11 @@ additional round trip.
The event content is a compact JSON object that matches the D6 frozen schema
(`requestNonce`, `optionIds`, `labels`, `expiresAt`, `description`, …). `description`
is sourced from `params.title` (buzz-agent v2), `params.subject.toolCall.title`
(v2 fallback), or `params.toolCall.title` (buzz-agent v1 / codex-acp) of the
(v2 fallback), `params.toolCall.title` (buzz-agent v1 / codex-acp permissions-request),
`params.toolCall.rawInput.command` (codex-acp v1.1.7 command requests), or
`params._meta.codex.params.reason` (codex-acp v1.1.7 file-change requests) of the
`session/request_permission` message — the first non-empty value wins. It is
`null` when no adapter-provided title is found. `optionIds` is a
`null` when no adapter-provided description is found. `optionIds` is a
**two-action contract**: exactly one `allow_once` and one `reject_once`, in that
order — the harness selects them via `select_card_actions` and fails closed if
either is absent or ambiguous, so no other option (e.g. `allow_always`) can ever
@@ -426,9 +428,11 @@ an unrenderable card. Labels and `description` are lossy display strings and are
only fields truncated rather than rejected; truncation lands on a UTF-8 char boundary
so the result is always valid UTF-8 within the byte limit the Desktop parser accepts.
Label values in the sentinel come directly from the ACP options' `name` fields;
render them verbatim. `description` is sourced from the adapter's title field —
render them verbatim. `description` is sourced from the adapter's operation field —
`params.title` (buzz-agent v2), `params.subject.toolCall.title` (v2 fallback),
or `params.toolCall.title` (v1 / codex-acp) — and describes the operation being
`params.toolCall.title` (v1 / codex-acp permissions-request),
`params.toolCall.rawInput.command` (codex-acp v1.1.7 command), or
`params._meta.codex.params.reason` (codex-acp v1.1.7 file-change) — and describes the operation being
authorized.
### Sentinel authenticity