Skip to content

feat(focus): fail-close send-safety gate on approve (C-SEND-1)#659

Open
Victor "David" Medina (Victor-David-Medina) wants to merge 4 commits into
mainfrom
claude/c-send-1-sendsafety
Open

feat(focus): fail-close send-safety gate on approve (C-SEND-1)#659
Victor "David" Medina (Victor-David-Medina) wants to merge 4 commits into
mainfrom
claude/c-send-1-sendsafety

Conversation

@Victor-David-Medina

Copy link
Copy Markdown
Collaborator

C-SEND-1 — fail-close send-safety gate on approve

Designed by an 8-agent understand→design→adversarial-verify workflow before any code. The key finding reframed the task:

The dry-run preview already exists and is correct (WouldSendPreview + resolveProvenance + TrustStrip + silent-pilot.test.tsx). So C-SEND-1 is not a preview build — do not rebuild those.

The real gap = a safety hole

app/api/v1/focus/decide/route.ts queued a real outreach send gated only by the OUTREACH_CONSUMER_ENABLED env var — with zero awareness of card provenance or PILOT_LOCK. Approving a sample/shadow card could queue a real send if that one flag flipped. That is unsafe-by-config, not safe-by-contract — and it means the eventual First-Light send is not yet a tested one-click.

The fix (smallest honest change, reuses every existing primitive)

  • lib/outreach/safety.ts — new pure resolveSendDecision({ sends, pilotLocked, consumerEnabled }). An external send is queued ONLY when live AND pilot-unlocked AND consumer-enabled; any missing condition holds it. Precedence pilot_lock → not_live_provenance → consumer_paused. It can never become true by a single env flip.
  • decide route — computes sends by reusing the pure provenance guards (isDataProvenance/isSideEffectMode, mirroring resolveProvenance's live-rule) read defensively off the row (recovery_items has no provenance column today → absent → not live → held = fail-close), plus isPilotLockEnabled(). The approval (outcome_receipts) is always recorded even when the send is held. Adds structured would_send + send_blocked_reason to the response (assert on fields, not prose).
  • No new lock/provenance definition, no new tableoutcome_receipts already IS the approval log; proof_events stays Pulse-owned.

Tests

  • lib/outreach/safety.test.ts — full 8-combo truth table (exactly one allowed; pilot lock always dominates).
  • __tests__/focus-decide-route.test.ts — live-queues / sample-held (not_live_provenance) / pilot-held (pilot_lock) / consumer-held (consumer_paused); approval receipt recorded in every held case.
  • __tests__/csend-proof-invariant.test.ts — approve never writes proof_events; empty ledger = 0 proof_events / 0 wins. Codifies the proof=0 honesty invariant.

Honesty / safety

Makes the console consistent with Pulse's PILOT_LOCK fail-close boundary (defense in depth). At proof_events=0 with HRC pilot-locked, nothing queues — by contract, not by luck. Built off fresh origin/main in an isolated worktree; the parked console dev branch was untouched. node_modules absent in the worktree → console CI (Build+Lint+Test, Quality Gate) is the test gate.

Generated with Claude Code by RelayLaunch

The dry-run preview (WouldSendPreview + resolveProvenance) already exists and is
correct - so C-SEND-1 is not a preview build. The real gap was a safety hole:
the decide route queued a real outreach send gated ONLY by the
OUTREACH_CONSUMER_ENABLED env var, with zero awareness of card provenance or
PILOT_LOCK. Approving a sample/shadow card could queue a real send if that one
flag flipped - unsafe-by-config, not safe-by-contract.

This adds a fail-close send-safety contract (resolveSendDecision): an approved
outreach is queued ONLY when the item is genuinely live AND the tenant is
pilot-unlocked AND the consumer is enabled. Any missing condition HOLDS the send
while still recording the approval (outcome_receipts). Reuses the pure
provenance guards + isPilotLockEnabled; no new lock/provenance definition.
proof_events is never written by the console (Pulse-owned) - locked by an
invariant test.

Tests: 8-combo truth table for resolveSendDecision; decide-route cases for
live-queues / sample-held / pilot-held / consumer-held (+ approval always
recorded); proof=0 invariant (approve never writes proof_events; empty ledger = 0/0).

Makes the console consistent with Pulse's PILOT_LOCK fail-close boundary
(defense in depth) so the eventual First-Light send is safe-by-contract.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

🛡️ Cascade Quality Score: 100/100

Category Score Status
TypeScript 20/20
ESLint 20/20
Brand Compliance 15/15
Test Suite 25/25
Build 20/20

Threshold: 85/100 | Result: PASS ✅

Victor "David" Medina and others added 2 commits July 1, 2026 23:27
…urce-guard)

outreach-consumer-safety.test.ts asserts every send-capable route references
the consumer-pause safety by that literal phrase. The C-SEND-1 reword dropped it
from the decide route; restore it (with a comment) so the guard passes. Behavior
unchanged - still fail-close held.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ent)

Completes the C-SEND-1 arc: the decide route now returns send_blocked_reason when
the fail-close gate holds an approved send, but the Deck ignored it - an
approved-but-held card just showed 'Approved', implying it was delivered. While
PILOT_LOCK holds every send (First Light), the owner would think approvals went out.

Now handleDecision (single + bulk) captures the held reason and the card shows an
honest 'Prepared, not sent' chip with a plain reason on hover (pilot lock on /
preview only / delivery paused). Neutral stone chip + amber-300 accent (held is
informational, not interactive/error). No new component - extends the existing
card status row next to the 'Queued' chip. No fabricated proof; nothing sent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Victor-David-Medina

Copy link
Copy Markdown
Collaborator Author

Extended: added the owner-visible held-state that completes C-SEND-1. The gate now returns send_blocked_reason, but the Deck was ignoring it - an approved-but-held card just showed "Approved", which (while PILOT_LOCK holds every send at First Light) implies it was delivered. Now handleDecision (single + bulk-approve) captures the reason and the card shows an honest "Prepared, not sent" chip with a plain reason on hover (pilot lock on / preview only / delivery paused). Neutral stone chip + amber-300 accent (held = informational, not interactive/error); extends the existing status row next to "Queued", no new component. This makes the send-safety gate visible so an approval is never mistaken for a send.

…p jargon (rank-2)

Pre-First-Light gate rank-2: the held-state reason lived only in a title= tooltip
(invisible on Elaine's touch device), used jargon ('Pilot lock on, nothing sends
yet'), and gave no 'what happens next'. Now: (1) plain reassuring labels that say
the approval is SAVED + when it sends ('Saved. It sends once testing wraps up;
nothing has gone out yet.'), no 'pilot lock' jargon; (2) rendered as a VISIBLE
inline line under the card (works on touch), in addition to the glanceable
'Prepared, not sent' chip. Closes the C-SEND-1 arc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Victor-David-Medina

Copy link
Copy Markdown
Collaborator Author

Extended (rank-2 from the pre-First-Light gate): the held-state reason was tooltip-only (invisible on touch), jargon, and gave no next-step. Now plain-English reassuring labels ("Saved. It sends once testing wraps up; nothing has gone out yet" - no "pilot lock" jargon) rendered as a visible inline line under the card (works on Elaine's touch device), alongside the glanceable "Prepared, not sent" chip. This closes the C-SEND-1 arc + the last Claude gate item.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant