feat(safety): freeze/unfreeze kill switch, init→setup-policy chain, spend line - #73
Merged
Merged
Conversation
…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
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>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 switchselat freeze [--note "<why>"]writes the flag file ($XDG_STATE_HOME/selat-pay/frozen.json,SELAT_PAY_FREEZE_PATHoverride; schema owner is selat-pay); every paid call is refused before signing untilselat unfreezeremoves it. Local and instant — no Circle round-trip. Probes and free discovery keep working.selat budgetshows the FROZEN state prominently while active — first line of the human display (bold red), plus afrozenfield and prefixeduser_summaryin--json.bin/selat.mjs+ top-level help; both commands have--help.selat init→setup-policychainingAt 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, thenpromptYesNo"Set spending caps now?" (default YES) → runs the existingsetup-policyflow (Circle's pattern: the email-OTP prompt belongs to the user's own terminal,inherit: true)selat setup-policycommand instead of hanging on a promptThe 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 runpre-payment output (non-json) — both the x402 path and the Apify prepaid-token pathselat fund's plan block (aligned as a plan row)Reuses
budget.mjs'sreadSessionConfig/sessionSpentvia a new exportedsessionSpendLine()helper.Tests
test/freeze-and-policy-chain.test.mjs:freezeFilePath/readFreeze(fail closed on malformed flag)/freezeStatusLine/noteArg, thefreeze/unfreezecommands against a temp flag path (0600, schema, re-freeze refresh, idempotent unfreeze),policyChainDecision(prompt / print / silent-when-capped / silent-when-unreadable), andsessionSpendLinenpm run checkcleanLive verification (no money moved)
Drove
bin/selat.mjswith fakeXDG_*/SELAT_PAY_*paths, a stubCIRCLE_BIN, and an armed fake session ledger:freeze --note→ flag written (schema v1) →budgetshows the FROZEN line +session $0.0150 spent of $2→--jsoncarriesfrozen→unfreezeremoves the flag. Cross-repo: the flag written byselat freezetrips selat-pay'senforceNotFrozen()with the exact refusal message, and readsnullafterselat unfreeze.🤖 Generated with Claude Code