Repository navigation
Warden core: deterministic decision layer, scoped grants, audit and approval inbox (W2a) - #402
Merged
Merged
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
âĶants Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
âĶnts in audit previews Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
âĶpprovals and warden cli Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
âĶlient Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
âĶwarden state Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
âĶize approver trust Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
oratis
marked this pull request as ready for review
October 2, 2026 11:39
Keeps both the reach-out routes (W3) and the warden routes in server.ts and in the api contract. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Brings in the credential broker and inbound hygiene (W2b, #405); keeps both the secret and the approvals/warden subcommands in cli.ts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
âĶpts in (review A) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
review_diff passed its target to git as a raw argv, so --output=<path> overwrote any file while classified read. Reject a leading dash and add --end-of-options. Same class fixed in npm_info view, speak (say -o) and the yt-dlp adapter; github numbers must be positive integers. The audit of every other read/self tool that spawns a process is recorded as tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
âĶiew 3, part 1) A sandboxed shell could reach the approval API on loopback and read or write <lisaHome>/warden. Seatbelt profiles now end with denials for both; bwrap hides the directory behind a tmpfs. The server registers its bound port. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
âĶs being updated) Own-property rule lookups and unknown-behaviour strictness (1), target-rule coverage (12), unsandboxed exec and writes ask by default (3), tainted egress and credential-path reads (4), full payload held for the card with a required digest (5), incomplete targets (6), wider taint sources and a persisted taint store (7), grant narrowing under taint and explicit ask rules (8), tainted skill writes (9), stricter MCP naming rules (10), symlink- and case-aware protected paths (11), redaction and HMAC digest (13), bounded regexes and the inbox abort race (lows). Server wiring, API, client, CLI and tests follow. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
âĶ 10, 11, 13, lows) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
âĶtaint, target coverage (review 3, 4, 6, 8, 9, 11) Also holds writes to credential locations to the same bar as reads: a tool-wide grant on write does not reach ~/.ssh. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
âĶrace (review 5, 6, 8, low) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
âĶ, 13, lows) The session loads the per-home digest key, tracks URLs seen in the conversation, flags commands that name warden state or its cli, and hands the full payload to the inbox. The api serves one item with its whole payload to approvers. The web server passes who the caller is (a caller who cannot approve gets the remote column), the user text, attachments, and persists conversation taint. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
âĶon taint in memory and on disk Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
âĶdigest (review 5) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
âĶs digest (review 5)
The web card fetches GET /api/approvals/{id}, renders every field in the
server's order with long values scrolling, and only then offers the scopes
the server allows. lisa approvals gains show, and approve requires --digest.
The route and the required digest are in the api contract.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
âĶround Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
âĶat the string guard is not a boundary Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
âĶopt-in The threat model still described taint as lasting one turn and Warden as planned. Taint now covers the whole conversation and survives a restart, tainted egress to an address not seen verbatim asks, and Warden, taint and new-recipient PII ship opt-in behind --approval warden / LISA_APPROVAL. Only the web client answers approvals today. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
W2a of
docs/PLAN_ALWAYS_ON_UPGRADE_2026-09-30.md: a deterministic layer that decides every tool call on the web surface, with scoped grants, an audit log and an approval inbox.lisa serve --webwith no flag behaves exactly as before (auto).--approval warden, orLISA_APPROVAL=wardenfor an app-launched backend, turns it on. The CLI REPL is unchanged.Why it stays opt-in
Only the web client renders an approval card, and over HTTP on loopback the server cannot tell the user's browser from a local process. A process the user runs unsandboxed can read an approval and approve it. Until approvals are signed by a native approver (the Mac app with a Keychain-bound key, or a paired phone), Warden protects against a model that has not been given an unconfined shell, not against one that has. That approver, and a real workspace root under launchd and Lisa.app, gate flipping the default.
Design
src/warden/â no model calls, no I/O in the policy.types.tsclassify.ts,paths.tspreview.tspolicy.tsevaluate(req, ctx)â purerules.ts,grants.ts,taint.ts,audit.ts,store.ts<lisaHome>/warden/, mode 0600inbox.tssession.tscreateWardenSessionâ the integration pointEvaluation order: system invariants â a
handoffrule â grants (narrowed by what the request is) â forced asks â user rules â default matrix â context floor.System invariants (not overridable by rules, grants or envelopes)
purchaseandcredentialare always handed back to the user.autonomymay onlyreadand write Lisa's own home; everything else is denied and never raises an approval.execor a host file write.Default matrix
Unsandboxed exec asks for every origin, the local owner included; a user who wants the old behaviour sets
tools.bash = auto.Forced asks
~/.ssh,~/.aws,<lisaHome>/warden, âĶ).skill_managewrite.Grants and the card
once,task,target,24h,always. Exact match on tool, category, method and trust column. Atargetgrant needs every target covered, and is not offered when the targets cannot be enumerated.alwaysor24h. A user rule stricter than a grant wins.GET /api/approvals/{id}), in the classifier's order, and approving requires the digest of what was shown.Full detail, the taint sources and the known limits are in
docs/DESIGN_WARDEN.md.Integration points
Inbox emitter:
new WardenInbox({ emit: (event, uid) => void }). The host must deliver each event only to subscribers of thatuid.approval_requested{ type, id, kind: "approval"|"handoff", at, tool, category, targets, preview, purpose?, digest, expiresAt, taskId?, origin, reason, scopes }approval_resolved{ type, id, verdict: "approved"|"denied"|"expired"|"dismissed", scope? }HTTP API (in
contracts/lisa-api-v1.openapi.json, additive)GET /api/approvalscanApproveGET /api/approvals/{id}POST /api/approvals/{id}/approve{ digest, scope? };digestis requiredPOST /api/approvals/{id}/deny{ reason? }; dismisses a handoffGET/PUT /api/warden/rulesGET /api/warden/grants,DELETE /api/warden/grants/{id}GET /api/warden/audit?limit=State-changing routes require the loopback owner (Mac edition) or a signed-in per-user session, a JSON content type, and a same-origin request.
CLI:
lisa approvals [list | show <id> | approve <id> --digest <digest> [--scope âĶ] | deny <id>],lisa warden rules|grants|audit.Review round (findings against
29b21a5)All thirteen findings and the four lows are addressed; each has a regression test named
review N. Summary:LISA_APPROVALaccepted.ask.review_diffno longer passes its target as an option. The same class was fixed innpm_info,speakand the yt-dlp adapter; the audit of the other read/self tools is recorded as tests.skill_manageasks in a tainted run.Changes outside
src/warden/src/web/server.ts: the import,createWebWarden(...), thehandleWardenApihook, the per-turn session, the sandbox port registration, inbox shutdown, and an optionalprovideroption used by the wiring test.src/sandbox/:protect.ts, and the denials in the seatbelt and bwrap profiles.src/tools/review_diff.ts,npm_info.ts,github.ts,src/voice/speak.ts,src/kb/ingest/adapters/ytdlp.ts: option-injection fixes.src/cli.ts,src/cli-args.ts,src/runtime-policy.ts,src/approval.ts: the mode and the subcommands.src/types.ts,src/subagent.ts,src/tools/task.ts: theapprovalhand-down to subagents.Tests run
All local gates pass on Node 26:
typecheck,typecheck:client,lint(0 errors),format:check,build,check:api-contract,check-md-links, andnpm testâ 2521 pass, 0 fail, 1 skipped (the existing node-pty test). 183 of those are this PR's.Not done / known limits
/under launchd and Lisa.app.memory,soul_patch,kb_writeandkb_addare stillauto; the audit log marks them.read,grepandkb_readinside the workspace do not taint.--approval wardenin the CLI REPL leaves thetasksubagent ungated.reachOut()in the integration PR.ðĪ Generated with Claude Code