Repository navigation
feat(warden): credential broker (secret:// handles) + inbound hygiene for mail [W2b] - #405
Merged
Merged
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…viour change) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…st reads Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…limits Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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:38
The pair merge spliced the run array in place, once per merged pair, which made a text of mostly "123 456" pairs cost O(n^2). Build a new array in one pass instead; semantics are unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
oratis
added a commit
that referenced
this pull request
Oct 2, 2026
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>
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
Workstream W2b of
docs/PLAN_ALWAYS_ON_UPGRADE_2026-09-30.md(Warden: "凭据代理" + "入站卫生"). Two independent pieces undersrc/warden/:secret://gmail/work). ASecretStoreholds the values (macOS Keychain, an encrypted file, or a per-tenant encrypted file in the cloud edition);resolveSecretRefsswaps handles for values inside a tool executor;redactKnownSecretsscrubs them from whatever comes back. Plus thelisa secret set|list|rmCLI.stripSensitiveTokensremoves one-time codes and sign-in / password-reset links from untrusted text. It is wired into every path where mail text reaches a model, the digest, an alert or the chat.Nothing in this PR is called by a tool executor yet, and no existing credential is migrated — see Not done.
Design
Full write-up:
docs/DESIGN_SECRETS_AND_HYGIENE.md.Secret store
/usr/bin/securityLISA_HOMEoverride<home>/warden/secrets.enc.json<home>/warden/secret.key(0600)[a-z0-9][a-z0-9._-]{0,63}, one optional/namespace.SecretStore:set / get / has / list / remove(async).list()returns names + timestamps only.resolveSecretRefs(value, store, { allow? })returns{ value, names, redact(text) }. It deep-copies, resolves whole-string and embedded handles, and fails closed on an unknown, malformed or disallowed handle.allow(name)is the seam for Warden policy.corrupt; nothing is overwritten to repair it.LISA_SECRETS_BACKEND=file|keychainforces the local backend.Inbound hygiene
stripSensitiveTokens(text, { truncated? })→{ text, removed: { otp, signInLinks, resetLinks } }. Deterministic (regex + URL parsing), idempotent, counts only.123 456,123-456,G-123456, full-width) or 5–10 letters-and-digits, near a keyword in EN / ZH / JA / KO / ES / FR / DE / PT / IT.Wired at:
cleanInbound()insrc/mail/service.tsMailItems kept from itbuildClassifyPrompt,parseClassificationinsrc/mail/classify.tsformatAlertinsrc/mail/alerts.tslatestDigest()insrc/mail/store.tsSubject and snippet are cleaned together (a code is often announced in one and printed in the other). The classifier's system prompt explains the placeholders. Two heuristic signals keep working after the code is gone:
security-code(now also keyed on the placeholder) and a newauth-link.There is no KB ingest path for e-mail-like content, so nothing was wired there.
Security notes
security -i; the spawned argv is only["-i"]. Tests assert no argv element ever contains the value.security -ifindings (measured with the harmlesshelpcommand only): lines are read through a 4096-byte buffer and the overflow is executed as a second command and echoed to stderr; the exit status after an interactive session is not reliable. Hence: Keychain values are capped at 2560 bytes, every write is verified by read-back, and stderr is never captured.node --test. A non-defaultLISA_HOMEnever selects the Keychain.SecretStoreErrorcarries a code and the handle. The CLI has nogetand refuses a value on the command line. The hygiene log line is counts only.Deviations from the plan / threat model, for the record (those documents are not edited here):
apple-authorization.ts— no KMS. Rotating or losing that secret makes stored secrets undecryptable (fail closed).v1.<base64url>, so Keychain Access shows the encoded form.Tests
106 new tests across
src/warden/*.test.ts,src/cli/secret.test.ts,src/mail/hygiene-wiring.test.ts:list()and files on disk never contain a value;Local gates on the final tree:
typecheck✓ ·lint0 errors (70 warnings, none in a file or line this PR adds) ✓ ·format:check✓ ·build✓ ·check:api-contract✓ ·check-md-links✓ ·npm test2178 tests, 2177 pass, 0 fail, 1 skipped (the node-pty test, which cannot spawn on local Node 26).Review fix (
717be06): the split-code merge inscanRunsspliced the run array in place and was O(n²) on input made of123 456pairs. It is now a single pass that builds a new array, with the same semantics (checked byte-for-byte against the old version on 60,000 random inputs).stripSensitiveTokenson"123 456 x ".repeat(k), best of 5: 80 KB 17.0 ms → 6.2 ms, 320 KB 206.7 ms → 24.8 ms, 1280 KB 8369 ms → 101.7 ms.One unrelated flake seen once in three full runs:
src/web/email-deliverability.test.ts("a slow resolver admits the address…") asserts a 120 ms wall-clock bound and missed it by ~5 ms while the machine was busy. It passes in isolation and on the other two runs; the file is untouched here.Not done / follow-ups
resolveSecretRefsyet, and nothing is migrated into the store (mail passwords / OAuth tokens inmail/secrets.json, provider keys inconfig.env).lisa secret set x,list,rm x) before anything depends on it.web_fetchcontent or channel messages.otp-482913); scheme-less links; tracker-wrapped links with no auth wording nearby; other languages.secretyet.Shared files:
src/cli.ts(+10: dispatch block and three help lines) andsrc/cli-args.ts(+3/−1: the subcommand name).src/mail/classify.ts,service.tsandstore.tswere prettier-formatted in a separate commit before the behaviour change, as the format gate requires for touched files.🤖 Generated with Claude Code