Repository navigation
feat(vault): bridge-level secret interception + vault CLI skill - #65
bassilkhilo-ag2 wants to merge 7 commits into
Conversation
…ored to Keychain before task file write Adds src/vault_intercept.py: intercepts 'vault set KEY VALUE' patterns in incoming Slack/Discord messages BEFORE writing to task files on disk. Secrets go to macOS Keychain (security add-generic-password); task file gets 'vault set KEY [STORED-IN-KEYCHAIN]' placeholder. Secret lifecycle: Slack/Discord API → bridge (in-memory, SSL in transit) → Keychain (encrypted at rest). Disk never sees plaintext. - src/vault_intercept.py: shared intercept module (regex, Keychain write, NamedTuple result). Supports bare, double-quoted, and single-quoted values. Multiple vault-set commands in one message all intercepted in a single pass. - src/slack-bridge.py: calls intercept_vault_commands() in _write_task() before user_task_text is built. Logs stored key names; logs but does not crash on Keychain failure (task still goes through sanitized). - src/discord-bridge.py: same pattern before user_task_text is built. - tests/vault-intercept.test.py: 15 tests (no real Keychain writes — mocked). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extends vault_intercept.py with:
- Key manifest at ~/.sutando-vault/keys.json (names only, no values)
updated on every store so keys are enumerable without Keychain dump
- get_vault_key(key) helper for scripts and skills that need a secret
- list_vault_keys() returns sorted key names from the manifest
Adds skills/vault/vault.py — a CLI with three subcommands:
vault list — show all stored key names
vault get KEY — retrieve value from Keychain
vault env KEY... -- CMD — run CMD with vault keys as env vars
Use case: Bassil passes an API key via Slack, bridge intercepts it to
Keychain, then any skill can call `get_vault_key('KEY')` or spawn a
subprocess with `vault env KEY -- python3 my_script.py` — key never
touches a config file or task file.
16 tests in tests/vault-skill.test.py, all passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents the vault secret store so every future session knows to call
get_vault_key("KEY") instead of asking for .env entries or hardcoding
secrets. Covers list, get, and env CLI subcommands.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…iew) - _replacer now catches RuntimeError internally — plaintext is always redacted from the returned text even when Keychain write fails. On failure, inserts [VAULT-STORE-FAILED] placeholder instead of [STORED-IN-KEYCHAIN] — secret never reaches disk either way. - InterceptResult gains a third field: failed: list[str] — keys that could not be stored. Bridges log these without re-exposing values. - _register_key uses temp-file + os.replace for atomic manifest writes — concurrent Slack + Discord intercepts can no longer corrupt keys.json. - Added ps-visibility comment on security argv call (known limitation). - Updated bridges to use attribute access (result.text / result.stored / result.failed) instead of tuple unpacking. - Replaced test_keychain_failure_raises_runtime_error with two new tests: test_store_failure_still_redacts and test_partial_failure_redacts_all. All 32 vault tests pass.
… + telegram bridge Addresses Maddy's review on sutando OSS PR sonichi#1084 (blocking + 3 non-blocking): BLOCKING fixed: - discord-bridge: vault interception now guarded by `access_tier == "owner"` check. Previously intercept_vault_commands() fired before the team/other sandbox gate, letting untrusted Discord senders write arbitrary keys to the owner's Keychain. Non-owner messages get redact_vault_commands() instead (pattern scrubbed, no write). - slack-bridge: vault interception moved to AFTER access_tier is resolved from tierMap (was called 40+ lines too early, before tier_map lookup). Same owner-only gate applied. Non-blocking fixed: - vault_intercept: regex extended to match backtick-quoted values (`secret`). Discord users typing code-formatted secrets no longer get backticks stored verbatim. - vault_intercept: empty value rejected with [VAULT-EMPTY-VALUE] placeholder. - telegram-bridge: vault interception added (was the only bridge without it). All Telegram senders are owner-tier (bot token allowlist); no tier gate needed yet. - redact_vault_commands() added to vault_intercept.py for non-owner scrubbing. Tests: 26/26 passing (10 new tests covering backtick, empty value, redact variants). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Security review addressed (f52525c cherry-picked from sonichi#1084 review by @liususan091219): Blocking fixed:
Non-blocking also fixed:
Tests: 26/26 passing, 10 new tests. |
|
Mirrors sutando OSS sonichi#1084 — same implementation. LGTM:
|
…lt intercept block
The vault PR adds ~1.2k chars to _write_task. The {0,6000}? capture budget
no longer reaches the terminating double-blank-line, causing
'_write_task function not found'. Bump to 8000.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Fixed the CI failure. Root cause:
Fix: committed |
|
Tagging @VasiliyRad for review — this is the Stando-side counterpart to sonichi#1084 (vault bridge interception). CI is passing. The key security invariant: vault values never touch disk in task files — bridge intercepts |
Summary
2 commits only — vault feature on top of stando's current main.
vault set KEY VALUEcommands sent via Slack or Discord are intercepted before the task file is written. The actual value goes straight to macOS Keychain. Task files only ever see[STORED-IN-KEYCHAIN].~/.sutando-vault/keys.json): tracks stored key names (never values) so keys are enumerable.vaultCLI skill (skills/vault/vault.py): three subcommands:vault list— show all stored key namesvault get KEY— retrieve value from Keychainvault env KEY... -- CMD— run CMD with vault keys injected as env varsget_vault_key(KEY)helper invault_intercept.py— for any skill that needs a secret at runtime.Use case
Bassil is on a walk. Opens Apollo on his iPhone, generates an API key, sends it to Sutando via Slack. The bridge intercepts it to Keychain. Sutando builds the GTM agent using
get_vault_key('APOLLO_API_KEY'). He passes his Railway key, Sutando deploys — he can see the app live while sitting by the lake.Secret lifecycle: Slack/Discord API → bridge (in-memory, SSL) → Keychain (encrypted at rest). Disk never sees plaintext.
Test plan
tests/vault-skill.test.py— all passingtests/vault-intercept.test.py— all passingvault setshow[STORED-IN-KEYCHAIN]placeholdervault listshows stored key namesvault env KEY -- CMDinjects key as env var🤖 Generated with Claude Code