Standalone module — no wiring yet. Lands the substrate for issue #13:
secrets.load(path) — vault file loader; refuses non-0600
secrets.make_session(vault) — per-conversation scrub/rehydrate state
session:scrub(text, mode) — substitute literals (+ autodetect)
session:rehydrate(text) — restore placeholders
secrets.streaming_rehydrator — chunk-boundary-tolerant streaming wrapper
Mode semantics (chosen per call by the caller):
"off" — identity, no mapping
"vault" — vault literals only, placeholders, rehydratable
"vault+autodetect" — + heuristic regexes, placeholders, rehydratable
"stealth" — + heuristic regexes, opaque decoys, one-way
Placeholders are stable across the session: the same literal always
maps to the same $AISH_SECRET_NNN slot, so re-scrubbing the same
context is idempotent and the model sees a consistent vocabulary.
AUTODETECT_PATTERNS (ordered; longer prefixes first):
sk-or-v<N>-... OpenRouter
ghp_/gho_/ghs_ GitHub PATs
AKIA<16> AWS access keys
eyJ...x.y.z JWTs
sk-... OpenAI (generic; matched after openrouter)
-----BEGIN ... PRIVATE KEY----- SSH/GPG key headers
Streaming rehydrator: tolerates a placeholder split across SSE chunks
($AISH_SE then CRET_001). It holds back the trailing partial-match
in a buffer, emits the rest, and resolves on the next push or flush.
Verified with 20 unit cases (vault sub, stable mapping, autodetect
across all label kinds, stealth decoys, mode=off, streaming with
mid-placeholder splits, non-placeholder $-prose pass-through).
Vault file mode enforcement: 0600 only — matches ssh's behavior for
~/.ssh/id_rsa. Loud failure (status + skip) if mode is wider.
Next commit (issue #13 follow-up): wire into broker / tool dispatch
/ display, add per-broker `redact` policy, :secrets meta, config
example block.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>