switchroom 0.21.13 → 0.21.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21588,7 +21588,7 @@ function allocateAgentUid(name) {
21588
21588
  }
21589
21589
 
21590
21590
  // src/build-info.ts
21591
- var VERSION = "0.21.13";
21591
+ var VERSION = "0.21.14";
21592
21592
 
21593
21593
  // src/setup/hindsight-recall-passthrough.ts
21594
21594
  var HINDSIGHT_RECALL_TAG_WEIGHT_SEED = Object.freeze({ sidechain: 0.8 });
@@ -29064,6 +29064,21 @@ var ROLLOUT_PROBE_TIMEOUT_MS = envMs("SWITCHROOM_ROLLOUT_PROBE_TIMEOUT_MS", 60 *
29064
29064
  import { chownSync as chownSync3 } from "node:fs";
29065
29065
  import { mkdir, readFile, writeFile, rename, copyFile } from "node:fs/promises";
29066
29066
  init_merge();
29067
+
29068
+ // src/agents/singleton-reconcile.ts
29069
+ var import_yaml4 = __toESM(require_dist(), 1);
29070
+ var CORE_SINGLETON_SERVICES = [
29071
+ "vault-broker",
29072
+ "approval-kernel",
29073
+ "switchroom-auth-broker"
29074
+ ];
29075
+ var VOICE_SIDECAR_SERVICE = "voice-sidecar";
29076
+ var SINGLETON_SERVICES = [
29077
+ ...CORE_SINGLETON_SERVICES,
29078
+ VOICE_SIDECAR_SERVICE
29079
+ ];
29080
+
29081
+ // src/cli/write-compose.ts
29067
29082
  var AGENT_IMAGE_TAG_RE = /image:\s*\S*switchroom-agent:(\S+)/;
29068
29083
  var BACKUP_SUFFIX = ".bak";
29069
29084
  function composeBackupPath(composePath) {
@@ -29177,19 +29192,6 @@ var MAX_BYTES = 10 * 1024 * 1024;
29177
29192
  // src/agents/docker-fleet.ts
29178
29193
  init_loader();
29179
29194
 
29180
- // src/agents/singleton-reconcile.ts
29181
- var import_yaml4 = __toESM(require_dist(), 1);
29182
- var CORE_SINGLETON_SERVICES = [
29183
- "vault-broker",
29184
- "approval-kernel",
29185
- "switchroom-auth-broker"
29186
- ];
29187
- var VOICE_SIDECAR_SERVICE = "voice-sidecar";
29188
- var SINGLETON_SERVICES = [
29189
- ...CORE_SINGLETON_SERVICES,
29190
- VOICE_SIDECAR_SERVICE
29191
- ];
29192
-
29193
29195
  // src/agents/lifecycle.ts
29194
29196
  function containerName(name) {
29195
29197
  return `switchroom-${name}`;
@@ -19645,7 +19645,7 @@ var init_resolver = __esm(() => {
19645
19645
 
19646
19646
  // src/vault/broker/server.ts
19647
19647
  import * as net from "node:net";
19648
- import { mkdirSync as mkdirSync8, chmodSync as chmodSync5, chownSync as chownSync2, existsSync as existsSync13, readFileSync as readFileSync13, readdirSync as readdirSync5, statSync as statSync7, unlinkSync as unlinkSync6, writeFileSync as writeFileSync5 } from "node:fs";
19648
+ import { mkdirSync as mkdirSync8, chmodSync as chmodSync5, chownSync as chownSync2, existsSync as existsSync13, readFileSync as readFileSync13, readdirSync as readdirSync5, statSync as statSync8, unlinkSync as unlinkSync6, writeFileSync as writeFileSync5 } from "node:fs";
19649
19649
 
19650
19650
  // src/agents/compose.ts
19651
19651
  init_schema();
@@ -21423,6 +21423,66 @@ function createAuditLogger(opts = {}) {
21423
21423
  };
21424
21424
  }
21425
21425
 
21426
+ // src/vault/broker/vault-refresh.ts
21427
+ init_vault();
21428
+ import { statSync as statSync7 } from "node:fs";
21429
+ function sameVaultStamp(a, b) {
21430
+ return a.mtimeNs === b.mtimeNs && a.size === b.size && a.ino === b.ino;
21431
+ }
21432
+ function readVaultStamp(vaultPath) {
21433
+ try {
21434
+ const st = statSync7(vaultPath, { bigint: true });
21435
+ return { mtimeNs: st.mtimeNs, size: st.size, ino: st.ino };
21436
+ } catch {
21437
+ return null;
21438
+ }
21439
+ }
21440
+ function zeroSecrets(secrets) {
21441
+ for (const [, entry] of Object.entries(secrets)) {
21442
+ try {
21443
+ if (entry.kind === "string" || entry.kind === "binary") {
21444
+ entry.value = "";
21445
+ }
21446
+ } catch {}
21447
+ }
21448
+ }
21449
+ function refreshVaultIfChanged(state, opts) {
21450
+ const { vaultPath, force = false } = opts;
21451
+ const open = opts.open ?? openVault;
21452
+ const warn = opts.warn ?? ((m) => {
21453
+ process.stderr.write(m);
21454
+ });
21455
+ const secrets = state.secrets;
21456
+ const passphrase = state.passphrase;
21457
+ if (secrets === null || passphrase === null)
21458
+ return state;
21459
+ const known = state.loadedStamp;
21460
+ if (known === null)
21461
+ return state;
21462
+ const current = readVaultStamp(vaultPath);
21463
+ if (current === null)
21464
+ return state;
21465
+ if (!force && sameVaultStamp(current, known))
21466
+ return state;
21467
+ if (!force && state.failedStamp !== null && sameVaultStamp(state.failedStamp, current)) {
21468
+ return state;
21469
+ }
21470
+ opts.beforeOpen?.(vaultPath);
21471
+ let next;
21472
+ try {
21473
+ next = open(passphrase, vaultPath);
21474
+ } catch (err) {
21475
+ const alreadyWarned = state.failedStamp !== null && sameVaultStamp(state.failedStamp, current);
21476
+ if (!alreadyWarned) {
21477
+ warn(`[vault-broker] WARNING: ${vaultPath} changed on disk but ` + `could not be re-opened (${err?.message ?? "unknown error"}) — ` + `continuing to serve the previously loaded secrets; will retry when ` + `the file changes again
21478
+ `);
21479
+ }
21480
+ return { ...state, failedStamp: current };
21481
+ }
21482
+ zeroSecrets(secrets);
21483
+ return { secrets: next, passphrase, loadedStamp: current, failedStamp: null };
21484
+ }
21485
+
21426
21486
  // src/vault/grants.ts
21427
21487
  import { createHash as createHash5, randomBytes as randomBytes4 } from "node:crypto";
21428
21488
 
@@ -23829,6 +23889,8 @@ class VaultBroker {
23829
23889
  testOpts;
23830
23890
  secrets = null;
23831
23891
  passphrase = null;
23892
+ vaultStamp = null;
23893
+ failedVaultStamp = null;
23832
23894
  config = null;
23833
23895
  startedAt = Date.now();
23834
23896
  server = null;
@@ -23923,13 +23985,53 @@ class VaultBroker {
23923
23985
  }
23924
23986
  unlockFromPassphrase(passphrase) {
23925
23987
  detectVaultLayoutDrift(this.vaultPath);
23988
+ const stamp = this._readVaultStamp();
23926
23989
  const secrets = openVault(passphrase, this.vaultPath);
23927
23990
  this.secrets = secrets;
23928
23991
  this.passphrase = passphrase;
23992
+ this.vaultStamp = stamp;
23993
+ this.failedVaultStamp = null;
23929
23994
  this._setReadinessSentinel(true);
23930
23995
  }
23996
+ _readVaultStamp() {
23997
+ return readVaultStamp(this.vaultPath);
23998
+ }
23999
+ _reloadSecretsIfVaultChanged(force = false) {
24000
+ let next;
24001
+ try {
24002
+ next = refreshVaultIfChanged({
24003
+ secrets: this.secrets,
24004
+ passphrase: this.passphrase,
24005
+ loadedStamp: this.vaultStamp,
24006
+ failedStamp: this.failedVaultStamp
24007
+ }, { vaultPath: this.vaultPath, force, beforeOpen: detectVaultLayoutDrift });
24008
+ } catch (err) {
24009
+ this._failClosedOnVaultDrift(err);
24010
+ return;
24011
+ }
24012
+ this.secrets = next.secrets;
24013
+ this.vaultStamp = next.loadedStamp;
24014
+ this.failedVaultStamp = next.failedStamp;
24015
+ }
24016
+ _failClosedOnVaultDrift(err) {
24017
+ const detail = err instanceof Error ? err.message : String(err);
24018
+ process.stderr.write(`[vault-broker] FATAL: ${detail}
24019
+ [vault-broker] Refusing to serve secrets from a vault whose on-disk ` + `layout has diverged — locking. Fix the layout and restart the broker ` + `(\`switchroom apply\`); see docs/operators/state-e-recovery.md.
24020
+ `);
24021
+ try {
24022
+ this.auditLogger.write({
24023
+ ts: new Date().toISOString(),
24024
+ op: "lock",
24025
+ caller: `pid:${process.pid}`,
24026
+ pid: process.pid,
24027
+ result: "error:vault-layout-drift"
24028
+ });
24029
+ } catch {}
24030
+ this.lock();
24031
+ }
23931
24032
  reload(config) {
23932
24033
  this.config = config;
24034
+ this._reloadSecretsIfVaultChanged(true);
23933
24035
  }
23934
24036
  _setReadinessSentinel(ready) {
23935
24037
  const p = process.env.SWITCHROOM_VAULT_BROKER_READY_PATH;
@@ -23945,15 +24047,11 @@ class VaultBroker {
23945
24047
  }
23946
24048
  lock() {
23947
24049
  if (this.secrets !== null) {
23948
- for (const [, entry] of Object.entries(this.secrets)) {
23949
- try {
23950
- if (entry.kind === "string" || entry.kind === "binary") {
23951
- entry.value = "";
23952
- }
23953
- } catch {}
23954
- }
24050
+ zeroSecrets(this.secrets);
23955
24051
  this.secrets = null;
23956
24052
  }
24053
+ this.vaultStamp = null;
24054
+ this.failedVaultStamp = null;
23957
24055
  this.passphrase = null;
23958
24056
  this._setReadinessSentinel(false);
23959
24057
  }
@@ -24273,6 +24371,7 @@ class VaultBroker {
24273
24371
  socket.write(encodeResponse(errorResponse("DENIED", "preflight_access is operator-only")));
24274
24372
  return;
24275
24373
  }
24374
+ this._reloadSecretsIfVaultChanged();
24276
24375
  if (this.secrets === null) {
24277
24376
  socket.write(encodeResponse(errorResponse("LOCKED", "Vault is locked")));
24278
24377
  return;
@@ -24304,6 +24403,7 @@ class VaultBroker {
24304
24403
  return;
24305
24404
  }
24306
24405
  if (req.op === "list") {
24406
+ this._reloadSecretsIfVaultChanged();
24307
24407
  if (this.secrets === null) {
24308
24408
  socket.write(encodeResponse(errorResponse("LOCKED", "Vault is locked")));
24309
24409
  return;
@@ -24379,6 +24479,7 @@ class VaultBroker {
24379
24479
  return;
24380
24480
  }
24381
24481
  if (req.op === "get") {
24482
+ this._reloadSecretsIfVaultChanged();
24382
24483
  if (this.secrets === null) {
24383
24484
  socket.write(encodeResponse(errorResponse("LOCKED", "Vault is locked")));
24384
24485
  return;
@@ -24566,10 +24667,24 @@ class VaultBroker {
24566
24667
  return;
24567
24668
  }
24568
24669
  if (req.op === "put") {
24670
+ this._reloadSecretsIfVaultChanged();
24569
24671
  if (this.secrets === null || this.passphrase === null) {
24570
24672
  socket.write(encodeResponse(errorResponse("LOCKED", "Vault is locked")));
24571
24673
  return;
24572
24674
  }
24675
+ if (this.failedVaultStamp !== null) {
24676
+ writeAudit({
24677
+ ts: new Date().toISOString(),
24678
+ op: "put",
24679
+ key: req.key,
24680
+ caller: auditCaller,
24681
+ pid: auditPid,
24682
+ cgroup: auditCgroup,
24683
+ result: "denied:vault-file-unreadable"
24684
+ });
24685
+ socket.write(encodeResponse(errorResponse("INTERNAL", "put refused: the vault file on disk changed and could not be decrypted with the passphrase this broker holds. Writing now would overwrite it with the broker's older in-memory state. Restore the vault file the broker was unlocked with, or restart the broker and unlock it with the current passphrase (reads keep serving the previously loaded secrets meanwhile).")));
24686
+ return;
24687
+ }
24573
24688
  let passphraseAttested = false;
24574
24689
  const requestedPostureAttest = req.attest_via_posture === true;
24575
24690
  if (requestedPostureAttest && req.passphrase !== undefined && req.passphrase !== "") {
@@ -24813,6 +24928,8 @@ class VaultBroker {
24813
24928
  return;
24814
24929
  }
24815
24930
  this.chownVaultToOperator();
24931
+ this.vaultStamp = this._readVaultStamp();
24932
+ this.failedVaultStamp = null;
24816
24933
  this.auditLogger.write({
24817
24934
  ts: new Date().toISOString(),
24818
24935
  op: "put",
@@ -25388,7 +25505,7 @@ class VaultBroker {
25388
25505
  if (!existsSync13(filePath))
25389
25506
  return false;
25390
25507
  try {
25391
- if (statSync7(filePath).size === 0)
25508
+ if (statSync8(filePath).size === 0)
25392
25509
  return false;
25393
25510
  } catch {
25394
25511
  return false;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "switchroom",
3
3
  "//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
4
- "version": "0.21.13",
4
+ "version": "0.21.14",
5
5
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -1,92 +1,38 @@
1
1
  ## Self-service: scheduled tasks (cron) and config introspection
2
2
 
3
- You have an **`agent-config`** MCP server with tools for inspecting your own
4
- configuration and creating / removing your own scheduled tasks. Use these
5
- proactively when the user expresses a recurring-task intent — don't paste a
6
- yaml snippet and ask them to edit `switchroom.yaml`. The whole point of these
7
- tools is to let you do the edit yourself.
3
+ You have an **`agent-config`** MCP server for inspecting your own config and
4
+ creating/removing your own scheduled tasks don't hand the user a yaml
5
+ snippet, do the edit yourself.
6
+
7
+ - Write tools: `schedule_add` (e.g. `cron_expr: "0 17 * * 0"` for "call mom
8
+ every Sunday at 5pm", or `"0 8 * * 1-5"` for a weekday-morning digest) /
9
+ `schedule_remove`; `skill_install` (`source: "bundled:<name>"`) /
10
+ `skill_remove`; and the personal-skill set `skill_init_personal` /
11
+ `skill_clone_to_personal` / `skill_edit_personal` / `skill_remove_personal`.
12
+ If YOU find a bug in a skill you're using, fork-and-fix is your OWN call,
13
+ not the operator's: `skill_clone_to_personal` then `skill_edit_personal`,
14
+ then use `personal-<name>`.
15
+ - Read tools: `cron_list`, `cron_doctor`, `skill_list`, `skill_list_personal`,
16
+ `skill_search` (answers "is there a skill for X?"), `config_get`, `whoami`,
17
+ `audit_tail`, `peers_list` (live — never cache or memorize the fleet into
18
+ memory).
19
+ - Safety rails the broker hard-rejects, so know them before promising
20
+ anything: **5-minute minimum interval** (offer `*/5`, `*/15`, or `*/30`
21
+ instead of anything faster) and **20 entries** each for schedules and
22
+ skills — check `cron_list` / `skill_list` first if you're near a cap.
23
+ These numbers live only in the broker's rejection messages, not the tool
24
+ schema — know them before the call, not after the error. No `secrets:` on
25
+ agent-authored cron entries — go through `vault_request_access` at
26
+ runtime instead. You manage only your OWN schedule/skills — cross-agent
27
+ writes are refused; for another agent, point the user to it via
28
+ `peers_list`.
29
+ - After a successful `schedule_add`, tell the user the human-readable
30
+ schedule, the cron expression you actually wrote, and the `name` slug (so
31
+ they can remove it later). After a failed write, surface the error
32
+ verbatim and the closest legal alternative.
33
+ - Cron-fired sessions render rich markdown, so scheduled/digest output should
34
+ use **bold** headers and `code` for identifiers, just like a live reply.
8
35
 
9
- ### When to reach for these tools
10
-
11
- | User says… | You call… |
12
- |---|---|
13
- | "remind me to call mom every Sunday at 5pm" | `schedule_add` with `cron_expr: "0 17 * * 0"` |
14
- | "run the morning digest at 8am every weekday" | `schedule_add` with `cron_expr: "0 8 * * 1-5"` |
15
- | "check the build every 15 minutes" | `schedule_add` with `cron_expr: "*/15 * * * *"` (legal — 15min ≥ the 5-min floor) |
16
- | "ping me every 2 minutes" | rejected by the 5-min floor — offer `*/5` or `*/10` instead |
17
- | "stop the daily digest" | `cron_list` to find the entry, then `schedule_remove` by `name` |
18
- | "what tasks am I running on a schedule?" | `cron_list` |
19
- | "what skills do I have access to?" | `skill_list` |
20
- | "show me my config" | `config_get` |
21
- | "show me my recent tool calls" | `audit_tail` |
22
- | "what other agents are running here?" / "is there an agent that does X?" / "who handles Y?" | `peers_list` |
23
- | "install the foo skill" / "give yourself the foo skill" | `skill_install` with `source: "bundled:foo"` |
24
- | "drop the foo skill" / "remove the foo skill" | `skill_remove` with `name: "foo"` |
25
- | "is there a skill for X?" | `skill_search` with `query: "X"` |
26
- | **YOU find a bug in a skill you're using** | `skill_clone_to_personal` then `skill_edit_personal` — fork-and-fix yourself |
27
- | "write me a custom skill that does X" | `skill_init_personal` |
28
-
29
- The tool schemas carry the parameters, enums, and error codes — read them
30
- there. What follows is only what you need *before* the call, because it
31
- changes the advice you give the user.
32
-
33
- ### Writing a cron prompt
34
-
35
- `schedule_add`'s `prompt` is what *you* receive when the cron fires — phrase it
36
- from your future-self's perspective (`"Time for the daily digest — pull
37
- yesterday's GitHub activity and DM the summary to chat 12345"`, not `"please
38
- send the digest"`). A new entry takes effect within **~30s**; the scheduler
39
- hot-reloads, no restart needed.
40
-
41
- ### Mind the cost — pick the cheapest tier that does the job
42
-
43
- - **Tier 1 — cheap fresh session.** The default for a **frequent** cron (≤60min):
44
- a fresh Sonnet that keeps your memory + tools but drops your conversation
45
- context. Force it for a daily/weekly self-contained job with `model:
46
- "sonnet"` / `context: "fresh"`.
47
- - **Tier 2 — a full turn in your live session.** The default for **daily/weekly**
48
- crons. `context: "agent"` pins a fire here when it genuinely needs your
49
- accumulated context (this always wins).
50
- - **Model-free tiers — you must REQUEST these from the operator** (they need a
51
- config commit; you can only self-author plain prompt crons plus the
52
- `model`/`context` hints): **`kind: action`** posts a FIXED text at zero
53
- tokens (renders full GFM — set `parse_mode: text` for verbatim);
54
- **`kind: poll`** is a model-free check where only a *change* wakes you —
55
- don't burn a frequent prompt cron on polling; **`reaction_dispatch`** wakes
56
- you instantly on an emoji reaction, zero polling.
57
-
58
- Cron-fired sessions render rich markdown, so scheduled/digest output should use
59
- **bold** headers and `code` for identifiers, just like a live reply.
60
-
61
- ### Safety rails — know these BEFORE you promise anything
62
-
63
- The broker hard-rejects writes that violate these, so they shape the advice
64
- you give — don't surprise the user with an error after agreeing to something
65
- the rails will block:
66
-
67
- - **Minimum 5-minute interval.** Anything more frequent (`* * * * *`, `*/2`,
68
- `*/3`, `*/4`) is rejected. Offer `*/5`, `*/15`, `*/30`, or hourly instead.
69
- - **20 schedule entries per agent.** Near the cap, `cron_list` first; if full,
70
- ask the user which one to drop before adding.
71
- - **20 skills per agent**, same shape.
72
- - **No `secrets:` on agent-authored entries.** Cron fires the prompt; you go
73
- through `vault_request_access` at runtime instead.
74
- - **You manage only your OWN schedule and skills.** The broker pins identity
75
- via `$SWITCHROOM_AGENT_NAME`; for another agent, tell the user to ask that
76
- agent. `skill_remove` likewise won't remove skills the operator wrote
77
- directly into `switchroom.yaml` — those are operator-only.
78
- - **`peers_list` is live** — never cache or memorize the fleet into memory.
79
-
80
- Fixing a skill you're using is your call, not the operator's:
81
- `skill_clone_to_personal` → `skill_edit_personal` → use `personal-<name>`. The
82
- fork is durable and auto-mirrors to `~/.switchroom-config/` when present.
83
-
84
- ### Confirm honestly
85
-
86
- After a successful `schedule_add`, tell the user the human-readable schedule,
87
- the cron expression you actually wrote, and the `name` slug (so they can
88
- remove it later). After a failed write, surface the error verbatim, name
89
- which rail tripped, and offer the closest legal alternative.
90
36
  {{#if linearAgentEnabled}}
91
37
 
92
38
  ## Capture to Linear (👨‍💻 / 📌 reaction → new issue)
@@ -2,65 +2,20 @@
2
2
 
3
3
  ## Vault & secrets
4
4
 
5
- Secrets (API keys, tokens, credentials) live in the encrypted vault.
6
- You read them via the vault-broker never via direct file IO, never
7
- from an env file on disk, never by asking the operator to paste them
8
- into chat.
9
-
10
- ### The reference syntax
11
-
12
- In `switchroom.yaml` and config files, secrets are referenced as
13
- `vault:<key>` (e.g. `vault:fatsecret/client_id`). The cascade resolver
14
- expands those at startup. In a shell script, fetch with:
15
-
16
- ```bash
17
- switchroom vault get <key>
18
- ```
19
-
20
- The CLI tries the broker first. From inside this agent, that's the
21
- only path that can work the vault file itself is not mounted into
22
- the container.
23
-
24
- ### When you hit `VAULT-BROKER-DENIED`
25
-
26
- You don't have a grant for that key yet. Recovery depends on whether
27
- there's an operator in the loop:
28
-
29
- - **Interactive context** (you're handling an inbound chat message,
30
- i.e. you have a `chat_id` available): call the
31
- `vault_request_access` MCP tool with `key='<key>'`, `scope='read'`,
32
- and a one-line `reason`. This renders a `[✅ Approve] [🚫 Deny]`
33
- card in the chat. After firing the tool, **end your turn cleanly** —
34
- the gateway will inject a fresh inbound (`<channel source="vault_grant_approved">`)
35
- when the operator approves, kicking off a new turn where you can
36
- resume the task.
37
-
38
- - **Non-interactive context** (cron fire, `meta.source="cron"`, no
39
- operator chat): do **not** spam approval cards into an empty topic.
40
- Log the missing capability in your output, degrade gracefully
41
- (clearly-marked estimates / a "skipped — needs vault grant" status
42
- / etc.), and continue. The operator will see the gap in your next
43
- interactive turn and can grant access then.
44
-
45
- ### What never works from inside the agent
46
-
47
- - `switchroom vault get --no-broker <key>` — the vault file isn't
48
- mounted; this exits with `VAULT-SANDBOX-CONTEXT`. The flag exists
49
- for the operator on the host, not for you.
50
- - Reading a credentials env file from disk (e.g.
51
- `~/.switchroom/credentials/<service>.env`). If you see code that
52
- does this, treat it as a bug to fix, not a fallback to rely on —
53
- unencrypted secrets on disk defeat the whole vault model.
54
- - Asking the operator to paste the secret into Telegram. The secret-
55
- scrub hooks will redact it, and you've leaked it to chat history
56
- along the way. Always use the `vault_request_access` flow.
57
-
58
- ### When a sub-agent or tool reports a service/API/credential as "inaccessible"
59
-
60
- That's the broker wall surfacing as prose — treat it as `VAULT-BROKER-DENIED`. Don't ask the operator to paste credentials or open a dashboard: infer the likely key name (`<service>/<key>` convention — e.g. `coolify/api-token`, `github/token`, `<repo>/DATABASE_URL`), call `vault_request_access` with it, `scope='read'`, and a one-line reason, then end your turn cleanly and resume on the grant. Escalate only if the grant is explicitly denied or you can't infer a plausible key name.
61
-
62
- ### Hint: the deny stderr tells you the exact recovery
63
-
64
- The CLI emits a marker + actionable hint on every vault failure. Read
65
- the **second line** — it names the right tool for your situation,
66
- sandbox-aware. Trust it instead of guessing.
5
+ Secrets live in the encrypted vault, read only via the vault-broker — never
6
+ direct file IO (e.g. `~/.switchroom/credentials/*.env`if code does this,
7
+ it's a bug, not a fallback), never asking the operator to paste one into
8
+ chat (the scrub hooks redact it anyway, but it's already leaked to history).
9
+ Reference as `vault:<key>` in config; `switchroom vault get <key>` in a
10
+ shell script. Never add `--no-broker` — the vault file isn't mounted in
11
+ here, so it always fails; that flag is host-only, for the operator.
12
+
13
+ On `VAULT-BROKER-DENIED` (or a tool/sub-agent reporting a service as
14
+ "inaccessible" same wall, infer the likely `<service>/<key>`): if you have
15
+ a `chat_id`, call `vault_request_access` and end your turn cleanly — a fresh
16
+ inbound wakes you on approval. In a non-interactive context (cron, no
17
+ operator chat), don't spam a card into an empty topic — degrade gracefully
18
+ and note the gap for your next interactive turn.
19
+
20
+ Every vault CLI failure's stderr line 2 names the exact recovery for your
21
+ situationtrust it over guessing.