F2 (retransmit structural fix): move delivery-deadline check to after the first
attempt. The first publish is now unconditional — an already-expired deadline
supplied by the caller (e.g. ordinary-timeout path where entry_deadline was past
at resolution time) no longer silently skips the one relay write that resolves
the card. Retries still consult the deadline.
F2 (test fixes): replace the channel-closed bounded-exit test with two focused
tests:
- retransmit_resolved_edit_unconditional_first_attempt_on_expired_deadline:
supplies an already-expired deadline, verifies the kind-40003 event is still
published. Mutation: revert !first_attempt guard → zero publishes.
- retransmit_resolved_edit_always_uncertain_bounded_exit: uses test_pair_silent
(always Uncertain) under paused time, advances past the 4s delivery window,
verifies the loop exits. Mutation: large RESOLVED_DELIVERY_WINDOW_SECS → hangs.
F3 (interactive delivery-seam): add _deliveryFn testability seam to
PermissionDecisionButtons and thread it through LifecycleActivity. Add
test_f3_interactive_delivery_seam_allow_once_and_reject_once_fire_delivery:
builds an acp_read transcript card (four option kinds), renders via
LifecycleActivity with a mock _deliveryFn, clicks allow_once button, asserts
delivery called with opt-allow-once + nonce-interactive; clicks reject_once
button in a fresh render, asserts delivery called with opt-reject-once.
allow_always/reject_always produce no buttons (ACTIONABLE_KINDS contract).
JS suite: 10/10 pass. Mutation: remove kind from ACTIONABLE_KINDS → button
absent → delivery assertion fails.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
F1 — Combined ≤200 UTF-8 byte description with known-key extraction
and secret-bearing key redaction. summarize_raw_input extracts
command > file/path keys > cwd > reason > compact-JSON-fallback from
rawInput; keys matching SECRET_KEY_PREFIXES are replaced with
"<redacted>" in the fallback and excluded from named paths.
DESCRIPTION_COMBINED_MAX_BYTES (200) replaces the independent
DESCRIPTION_RAW_INPUT_BYTES (120) budget so the combined title+context
string always fits in one sentinel field. Truncation appends '…'.
New tests: scalar/empty-object rawInput → title only; token/password
redaction; command priority over path; combined-bound invariant;
production-seam sentinel-level bound verification.
F2 — RESOLVED_DELIVERY_WINDOW_SECS raised 60→300s (aligned with
relay card-maximum and PERMISSION_ASK_TIMEOUT_SECS admission window).
New tests: bounded-exit on channel-close (terminal path); same event
ID across clones verified.
F3 — Cross-layer test: acp_read event with all four adapter option
kinds driven through buildTranscript reducer → LifecycleActivity
render, asserting exactly 2 buttons (allow_once + reject_once).
Rust read-loop coverage: allow_always and reject_always decisions are
silently ignored by the card_actions allowlist; only allow_once is
accepted and resolves the entry.
F4 — Existing early_decision_first_wins test retained; read-loop
coverage now includes the allow_always/reject_always rejection path.
Routing hazard (same-channel multi-thread) — handle_permission_decision_control
now fans out to ALL tasks with matching channel_id instead of finding
the first one. The nonce is unguessable so the owning read loop accepts
it while siblings drop it on mismatch. Two regression tests:
two_threads_same_channel_fan_out_routes_to_owning_thread (Thread B
installed first, Thread A's decision still reaches A) and
two_threads_same_channel_thread_b_not_stranded (Thread B receives its
own decision independently after Thread A's is handled).
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
F1 (truthful operation context): `description_from_request_permission`
now appends a byte-truncated JSON summary of `rawInput` to the tool
name for buzz-agent v1/v2 paths, producing `"<tool>(<args>)"` so two
calls of the same tool with different arguments yield distinguishable
card descriptions. Codex v1.1.7 paths (command / reason) are unchanged.
New production-seam test drives `handle_permission_request` with a v2
payload and asserts the description field reaches the published kind-9
sentinel content. Tests cover file-path visibility, hostile markup,
control characters, byte truncation, and UTF-8 char-boundary truncation.
F2 (terminal-outcome delivery window): `finish_permission` now computes
`delivery_deadline = Instant::now() + RESOLVED_DELIVERY_WINDOW_SECS`
at resolution time instead of passing `entry_deadline` (already past
for ordinary timeouts). New test: ordinary-timeout path publishes at
least one kind-40003 resolved edit; would emit zero with old code.
F3 (observer surface two-option contract): removed `reject_always` from
`ACTIONABLE_KINDS` in `LifecycleActivity.tsx`; removed the
`hasPersistentGrant` badge and the "Permanent grant" advisory copy.
Updated all affected render tests and added a four-option contract test
proving only allow_once + reject_once render actionable buttons.
F4 (first-wins pre-ACK decision): guarded `entry.early_decision` with
`if entry.early_decision.is_none()` so only the first valid decision is
buffered. New regression test: Reject arrives first, Allow second, ACK
fires, observer confirms Reject is applied.
lib.rs: fixed three `TaskMeta` literal compile errors in test code
(missing `permission_decision_tx` / `scope` fields from main merge).
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
PermissionDecisionButtons classified actionable options via kind.startsWith("reject"), so an unknown or malformed reject-prefixed kind (e.g. reject_later_v2) still rendered a trusted Deny button whose semantics the UI does not understand. Replace prefix matching with an exact recognized-kind allowlist (allow_once, reject_once, reject_always); any kind outside it fails closed and renders no action. reject_always stays actionable (persistent denial only reduces authority) but its label defaults to "Always deny" when the harness omits one, so a user never silently chooses persistence.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
allow_always rendered as an ordinary green Allow button, giving the observer
feed a one-click path to a durable grant — the same UX concern Wes flagged in
B2. Unknown kinds also rendered as green Allow (Wes: should fail closed).
Tighten PermissionDecisionButtons:
- allow_once → actionable green Allow button (unchanged path)
- reject_* family → actionable red Deny button (unchanged path)
- allow_always → non-actionable amber badge: "Permanent grant — use request
card". The thread card (D5 disclosure) is the correct surface; the observer
feed is advanced/debug and should not offer one-click durable grants.
- Unknown kinds → silently omitted (fail closed); no button rendered.
Add LifecycleActivity.render.test.mjs with five cases covering allow_once,
reject_once, allow_always (badge present, no button), unknown kind (nothing
rendered), and a mixed allow_once + allow_always card.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>