Skip to content

feat(safety): freeze/unfreeze kill switch, init→setup-policy chain, spend line - #73

Merged
SELAT-DEV merged 1 commit into
mainfrom
feat/freeze-and-policy-chain
Jul 15, 2026
Merged

feat(safety): freeze/unfreeze kill switch, init→setup-policy chain, spend line#73
SELAT-DEV merged 1 commit into
mainfrom
feat/freeze-and-policy-chain

Conversation

@SELAT-DEV

Copy link
Copy Markdown
Contributor

Summary

Tester feedback WS4 remainder, layered on the merged session-budget chain (#68). Pairs with selat-pay feat/freeze (#23), which enforces the flag pre-signature.

selat freeze / selat unfreeze — instant local kill switch

  • selat freeze [--note "<why>"] writes the flag file ($XDG_STATE_HOME/selat-pay/frozen.json, SELAT_PAY_FREEZE_PATH override; schema owner is selat-pay); every paid call is refused before signing until selat unfreeze removes it. Local and instant — no Circle round-trip. Probes and free discovery keep working.
  • selat budget shows the FROZEN state prominently while active — first line of the human display (bold red), plus a frozen field and prefixed user_summary in --json.
  • Wired into bin/selat.mjs + top-level help; both commands have --help.

selat initsetup-policy chaining

At the end of init's success path, when the wallet has no custom spending policy (best-effort spendingPolicy() read — an unreadable policy stays silent instead of nagging on a Circle hiccup): a strong recommendation block, then

  • interactive stdin: promptYesNo "Set spending caps now?" (default YES) → runs the existing setup-policy flow (Circle's pattern: the email-OTP prompt belongs to the user's own terminal, inherit: true)
  • non-TTY: prints the verbatim selat setup-policy command instead of hanging on a prompt

The closing "Set a spending policy before depositing > $20" hint is skipped when caps exist or were just set.

Confirmation-time spend line

When a session budget is armed, the moment money is about to move now shows session: $X spent of $Y:

  • selat run pre-payment output (non-json) — both the x402 path and the Apify prepaid-token path
  • selat fund's plan block (aligned as a plan row)

Reuses budget.mjs's readSessionConfig/sessionSpent via a new exported sessionSpendLine() helper.

Tests

  • New test/freeze-and-policy-chain.test.mjs: freezeFilePath/readFreeze (fail closed on malformed flag)/freezeStatusLine/noteArg, the freeze/unfreeze commands against a temp flag path (0600, schema, re-freeze refresh, idempotent unfreeze), policyChainDecision (prompt / print / silent-when-capped / silent-when-unreadable), and sessionSpendLine
  • Full suite: 112/112 pass (was 105 on main); npm run check clean

Live verification (no money moved)

Drove bin/selat.mjs with fake XDG_*/SELAT_PAY_* paths, a stub CIRCLE_BIN, and an armed fake session ledger: freeze --note → flag written (schema v1) → budget shows the FROZEN line + session $0.0150 spent of $2--json carries frozenunfreeze removes the flag. Cross-repo: the flag written by selat freeze trips selat-pay's enforceNotFrozen() with the exact refusal message, and reads null after selat unfreeze.

🤖 Generated with Claude Code

…pend line

Tester feedback WS4 remainder, on top of the session-budget chain (#68):

- `selat freeze [--note "<why>"]` / `selat unfreeze`: instant local kill
  switch. Freeze writes the flag file selat-pay checks pre-signature
  (schema owner: selat-pay feat/freeze); unfreeze removes it. Local and
  instant — no Circle round-trip. `selat budget` (human + --json) shows
  the FROZEN state prominently while active.
- `selat init` now chains into `setup-policy`: when the wallet has no
  custom spending policy (best-effort spendingPolicy() read), init's
  success path prints a strong recommendation and — interactive shells
  only — asks "Set spending caps now?" (default YES) and runs the
  existing setup-policy flow. Non-TTY prints the verbatim command
  instead: the Circle OTP prompt belongs to the user's own terminal.
- Confirmation-time spend line: when a session budget is armed,
  `selat run` (both x402 and Apify-prepaid paths, non-json) and
  `selat fund`'s plan print "session: $X spent of $Y" at the moment
  money is about to move (sessionSpendLine(), reusing budget.mjs's
  readSessionConfig/sessionSpent).

Pure decision helpers (readFreeze/freezeStatusLine/noteArg,
policyChainDecision, sessionSpendLine) are exported and unit-tested.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@SELAT-DEV
SELAT-DEV merged commit 59c4f02 into main Jul 15, 2026
SELAT-DEV added a commit that referenced this pull request Jul 15, 2026
…e, caps ladder) (#74)

Tester feedback round 2, WS7: a submission claimed the wallet is
'custodial only' and another believed the wallet expires in 14 days.

- Trust model section decodes 'self-custody' against Circle's published
  docs: 2-of-2 MPC, both shares required, shares never exposed to the
  agent, Circle cannot unilaterally move funds; where each share
  physically lives is not published, so the section says so instead of
  inventing specifics. States SELAT holds no keys, funds, or login
  credentials (signs only through the Circle CLI).
- FAQ: wallets never expire (14 days = Apify prepaid token; 7-day OTP
  re-login is authentication, not expiry); one unified Gateway balance
  with per-chain rows as routing detail (aligned with the #69 fund
  copy); guardrails ladder per call (--max-amount) -> per session
  (selat budget start) -> per window (setup-policy) + freeze kill
  switch marked in flight (#73).

Docs-only; npm test 105/105.

Co-authored-by: Karen Sheng <ksherlocked@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@SELAT-DEV SELAT-DEV mentioned this pull request Jul 15, 2026
@SELAT-DEV
SELAT-DEV deleted the feat/freeze-and-policy-chain branch July 31, 2026 17:52
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.

2 participants