switchroom 0.18.10 → 0.18.12

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.
Files changed (125) hide show
  1. package/dist/agent-scheduler/index.js +29 -5
  2. package/dist/auth-broker/index.js +53 -13
  3. package/dist/cli/hindsight-mental-model-pretool.mjs +39 -0
  4. package/dist/cli/notion-write-pretool.mjs +29 -5
  5. package/dist/cli/switchroom.js +2636 -1369
  6. package/dist/cli/ui/apple-touch-icon.png +0 -0
  7. package/dist/cli/ui/favicon-32.png +0 -0
  8. package/dist/cli/ui/favicon.ico +0 -0
  9. package/dist/cli/ui/index.html +163 -17
  10. package/dist/host-control/main.js +1248 -342
  11. package/dist/vault/approvals/kernel-server.js +54 -13
  12. package/dist/vault/broker/server.js +163 -114
  13. package/package.json +3 -4
  14. package/profiles/_base/start.sh.hbs +65 -0
  15. package/profiles/_shared/vault-protocol.md.hbs +3 -1
  16. package/profiles/coding/CLAUDE.md.hbs +1 -1
  17. package/profiles/default/CLAUDE.md.hbs +2 -2
  18. package/profiles/executive-assistant/CLAUDE.md.hbs +1 -1
  19. package/profiles/health-coach/CLAUDE.md.hbs +1 -1
  20. package/telegram-plugin/bridge/bridge.ts +37 -0
  21. package/telegram-plugin/bridge/inbound-dedup.ts +101 -0
  22. package/telegram-plugin/dist/bridge/bridge.js +73 -1
  23. package/telegram-plugin/dist/gateway/gateway.js +3603 -1007
  24. package/telegram-plugin/dist/server.js +74 -2
  25. package/telegram-plugin/flood-circuit-breaker.ts +493 -21
  26. package/telegram-plugin/gateway/approval-hold.ts +583 -0
  27. package/telegram-plugin/gateway/auth-command.ts +92 -2
  28. package/telegram-plugin/gateway/auth-loopback-relay.ts +670 -0
  29. package/telegram-plugin/gateway/boot-card.ts +12 -5
  30. package/telegram-plugin/gateway/callback-query-handlers.ts +76 -1
  31. package/telegram-plugin/gateway/config-approval-handler.ts +6 -1
  32. package/telegram-plugin/gateway/disconnect-flush.ts +19 -0
  33. package/telegram-plugin/gateway/dm-pin-sweep.test.ts +251 -0
  34. package/telegram-plugin/gateway/dm-pin-sweep.ts +178 -0
  35. package/telegram-plugin/gateway/gateway.ts +1482 -165
  36. package/telegram-plugin/gateway/hostd-dispatch.ts +23 -0
  37. package/telegram-plugin/gateway/idle-clear.ts +90 -6
  38. package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +26 -5
  39. package/telegram-plugin/gateway/inject-handler.ts +8 -0
  40. package/telegram-plugin/gateway/ipc-protocol.ts +46 -3
  41. package/telegram-plugin/gateway/ipc-server.ts +43 -0
  42. package/telegram-plugin/gateway/mental-model-propose-resolve.ts +145 -37
  43. package/telegram-plugin/gateway/model-command.ts +9 -3
  44. package/telegram-plugin/gateway/pending-session-command.ts +13 -1
  45. package/telegram-plugin/gateway/permission-ttl-sweep.ts +66 -0
  46. package/telegram-plugin/gateway/pre-approval-check.ts +74 -0
  47. package/telegram-plugin/gateway/queued-card-store.ts +217 -0
  48. package/telegram-plugin/gateway/session-model-file.ts +26 -1
  49. package/telegram-plugin/gateway/turn-end-gate-backstop.ts +59 -0
  50. package/telegram-plugin/gateway/turn-end-gate.ts +95 -0
  51. package/telegram-plugin/gateway/turn-typing-loop.ts +10 -2
  52. package/telegram-plugin/gateway/unhandled-rejection-policy.ts +13 -0
  53. package/telegram-plugin/hooks/dispatch-claim-scan.mjs +259 -0
  54. package/telegram-plugin/hooks/dispatch-claim-stop.mjs +129 -0
  55. package/telegram-plugin/hooks/hooks.json +9 -0
  56. package/telegram-plugin/inline-keyboard-callbacks.ts +209 -2
  57. package/telegram-plugin/operator-events.ts +23 -0
  58. package/telegram-plugin/package.json +0 -1
  59. package/telegram-plugin/permission-rule.ts +1 -0
  60. package/telegram-plugin/permission-title.ts +1 -0
  61. package/telegram-plugin/retry-api-call.ts +212 -2
  62. package/telegram-plugin/send-gate-degraded.test.ts +443 -0
  63. package/telegram-plugin/send-gate-observability.test.ts +470 -0
  64. package/telegram-plugin/send-gate-observability.ts +355 -0
  65. package/telegram-plugin/send-gate.test.ts +698 -0
  66. package/telegram-plugin/send-gate.ts +982 -0
  67. package/telegram-plugin/shared/bot-runtime.ts +17 -5
  68. package/telegram-plugin/shared/gw-trace-gate.ts +105 -0
  69. package/telegram-plugin/status-pin-driver.ts +52 -7
  70. package/telegram-plugin/status-pin.ts +81 -0
  71. package/telegram-plugin/subagent-watcher.ts +102 -2
  72. package/telegram-plugin/tests/activity-card-wiring.test.ts +18 -5
  73. package/telegram-plugin/tests/approval-hold-harness.ts +425 -0
  74. package/telegram-plugin/tests/approval-hold-outcome.test.ts +296 -0
  75. package/telegram-plugin/tests/approval-hold-record.test.ts +531 -0
  76. package/telegram-plugin/tests/approval-hold-redeliver.test.ts +602 -0
  77. package/telegram-plugin/tests/auth-loopback-relay.test.ts +533 -0
  78. package/telegram-plugin/tests/boot-card-flood-suppress.test.ts +53 -7
  79. package/telegram-plugin/tests/busy-key-reaper.test.ts +1 -0
  80. package/telegram-plugin/tests/dispatch-claim-scan.test.ts +250 -0
  81. package/telegram-plugin/tests/flood-breaker-blindness.test.ts +213 -0
  82. package/telegram-plugin/tests/flood-windows-persistence.test.ts +224 -0
  83. package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
  84. package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +29 -1
  85. package/telegram-plugin/tests/gateway-loopback-paste-redact.test.ts +66 -0
  86. package/telegram-plugin/tests/gw-trace-gate.test.ts +105 -0
  87. package/telegram-plugin/tests/idle-clear.test.ts +233 -3
  88. package/telegram-plugin/tests/inbound-dedup.test.ts +93 -0
  89. package/telegram-plugin/tests/inline-keyboard-callbacks.test.ts +284 -0
  90. package/telegram-plugin/tests/ipc-server-check-pre-approved.test.ts +194 -0
  91. package/telegram-plugin/tests/mental-model-propose-resolve.test.ts +123 -0
  92. package/telegram-plugin/tests/missed-approvals-wiring.test.ts +1 -1
  93. package/telegram-plugin/tests/model-command.test.ts +14 -0
  94. package/telegram-plugin/tests/pending-session-command.test.ts +21 -0
  95. package/telegram-plugin/tests/permission-card-routing.test.ts +30 -5
  96. package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +8 -7
  97. package/telegram-plugin/tests/permission-rearm-wiring.test.ts +1 -1
  98. package/telegram-plugin/tests/pre-approval-check.test.ts +148 -0
  99. package/telegram-plugin/tests/queued-card-store.test.ts +232 -0
  100. package/telegram-plugin/tests/reaction-flush-turn-gated.test.ts +100 -0
  101. package/telegram-plugin/tests/retry-api-call.test.ts +398 -0
  102. package/telegram-plugin/tests/session-model-file.test.ts +50 -0
  103. package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +35 -14
  104. package/telegram-plugin/tests/status-pin.test.ts +275 -1
  105. package/telegram-plugin/tests/subagent-watcher-deferral-log-ratelimit.test.ts +316 -0
  106. package/telegram-plugin/tests/turn-end-gate-backstop.test.ts +92 -0
  107. package/telegram-plugin/tests/turn-end-gate.test.ts +137 -0
  108. package/telegram-plugin/tests/typing-emitter.test.ts +586 -0
  109. package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +20 -0
  110. package/telegram-plugin/typing-emitter.ts +224 -0
  111. package/telegram-plugin/uat/scenarios/jtbd-feel-like-a-colleague-dm.test.ts +136 -0
  112. package/telegram-plugin/welcome-text.ts +42 -0
  113. package/vendor/hindsight-memory/scripts/drain_pending.py +22 -6
  114. package/vendor/hindsight-memory/scripts/lib/client.py +12 -5
  115. package/vendor/hindsight-memory/scripts/lib/directives.py +38 -3
  116. package/vendor/hindsight-memory/scripts/lib/pending.py +36 -9
  117. package/vendor/hindsight-memory/scripts/session_end.py +14 -3
  118. package/vendor/hindsight-memory/scripts/session_start.py +21 -0
  119. package/vendor/hindsight-memory/scripts/tests/test_directives.py +38 -0
  120. package/vendor/hindsight-memory/tests/test_drain_pending.py +68 -0
  121. package/vendor/hindsight-memory/tests/test_pending.py +44 -0
  122. package/vendor/hindsight-memory/tests/test_session_end_pending.py +38 -0
  123. package/vendor/hindsight-memory/tests/test_session_start_drain.py +155 -0
  124. package/telegram-plugin/channel-envelope-safety.test.ts +0 -56
  125. package/telegram-plugin/channel-envelope-safety.ts +0 -56
@@ -4116,7 +4116,12 @@ var init_schema = __esm(() => {
4116
4116
  AgentSoulSchema = exports_external.object({
4117
4117
  name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
4118
4118
  style: exports_external.string().describe("Communication style description"),
4119
- boundaries: exports_external.string().optional().describe("Behavioral boundaries and disclaimers")
4119
+ creature: exports_external.string().optional().describe("Persona creature/form (e.g., 'owl', 'octopus')"),
4120
+ vibe: exports_external.string().optional().describe("One-line personality vibe (e.g., 'calm, precise')"),
4121
+ expertise: exports_external.string().optional().describe("Domain expertise summary rendered into the persona"),
4122
+ emoji: exports_external.string().optional().describe("Signature emoji for the persona (e.g., '\uD83E\uDD89')"),
4123
+ boundaries: exports_external.string().optional().describe("Behavioral boundaries and disclaimers"),
4124
+ shape: exports_external.enum(["executive-assistant", "developer", "coach", "generalist"]).default("generalist").describe("Persona-shape discriminator the per-agent CLAUDE.md template can " + "branch on (e.g., 'you are an executive assistant' vs 'a senior " + "engineer'). No runtime behavior yet — template work lands in a " + "later issue. Cascade: override (per-agent wins over default).")
4120
4125
  }).optional();
4121
4126
  AgentToolsSchema = exports_external.object({
4122
4127
  allow: exports_external.array(exports_external.string()).default([]).describe("Allowed tools (use ['all'] for unrestricted)"),
@@ -4281,6 +4286,11 @@ var init_schema = __esm(() => {
4281
4286
  safe_boundary: exports_external.boolean().optional().describe("When true (the default), a `!`-prefix interrupt that arrives while " + "the agent is mid-tool-call is DEFERRED: the SIGINT and the " + "replacement turn wait until the in-flight tool call finishes (a " + "clean boundary) instead of C-c'ing the agent mid-write/mid-bash. If " + "no tool is in flight the interrupt still fires immediately. Bounded " + "by max_wait_ms so a long tool never strands the user. Set false to " + "fire synchronously the moment `!` is received (historical " + "behaviour). Rapid repeated `!` while one is pending coalesce into a " + "single deferred interrupt carrying the latest body."),
4282
4287
  max_wait_ms: exports_external.number().int().positive().optional().describe("Upper bound (ms) the gateway waits for a safe boundary before firing " + "a deferred `!` interrupt anyway. Only consulted when safe_boundary is " + "true. Default 8000. Keep it short — the user explicitly asked to " + "interrupt, so a long in-flight tool shouldn't ghost them; the cap " + "trades a tiny risk of a mid-tool C-c for a guaranteed response.")
4283
4288
  }).optional().describe("Interrupt timing — how a `!`-prefix interrupt behaves when it lands " + "mid-tool-call. Off by default (fire immediately). Cascades from " + "defaults.channels.telegram.interrupt."),
4289
+ button_choice_confirmation: exports_external.object({
4290
+ enabled: exports_external.boolean().default(false).describe("When true, tapping an agent-emitted inline_keyboard button annotates " + "the source message body with a '✅ You chose: <label> · HH:MM' line so " + "the chat surface is self-documenting. Only applies to single-use " + "keyboards (re-tappable keyboards are never annotated). Requires the " + "agent's parseMode to be the default 'html'. Opt-in (default false). " + "Limitation: the annotated body is rebuilt from the message's plain " + "text, so any formatting entities (bold, links, code, ...) on the " + "original message are lost when the annotation is applied."),
4291
+ format: exports_external.string().default("✅ You chose: {label} · {time}").describe("Template for the annotation line. `{label}` is replaced with the " + "tapped button's text (newlines stripped, HTML-escaped, trimmed to 60 " + "chars) and `{time}` with HH:MM in the chosen timezone. Note: retap " + "dedup (replacing a prior annotation instead of appending another " + "line) only recognizes the DEFAULT format shape — a custom format " + "accumulates one line per retap."),
4292
+ timezone: exports_external.enum(["gateway", "utc"]).default("gateway").describe("Timezone for the {time} placeholder. 'gateway' uses the gateway " + "process's local time; 'utc' uses UTC. Per-user timezones are out of " + "scope for v1.")
4293
+ }).optional().describe("Auto-confirm 'You chose: X' annotation on inline_keyboard taps (#789). " + "Cascades from defaults.channels.telegram.button_choice_confirmation."),
4284
4294
  webhook_sources: exports_external.array(exports_external.enum(["github", "generic", "linear"])).optional().describe("External webhook sources allowed to ingest events into this agent's " + "log. POST /webhook/<agent>/<source> on the switchroom web server. " + "Each source has its own signature verification ('github' = " + "X-Hub-Signature-256 HMAC-SHA256, 'generic' = Bearer token, " + "'linear' = Linear-Signature bare-hex HMAC-SHA256 of the raw body). " + "Per-source secret read from ~/.switchroom/webhook-secrets.json " + "keyed by [agent][source]. Verified events append to " + "<agent>/telegram/webhook-events.jsonl for the agent to read on " + "demand. Off by default — webhook is the only untrusted-inbound " + "surface in the system, so opt-in is mandatory. " + "Cascades from defaults.channels.telegram.webhook_sources. " + "(Migrated from per-agent root in #596 — see #577.)"),
4285
4295
  webhook_dispatch: exports_external.object({
4286
4296
  github: exports_external.array(webhookDispatchRule).optional(),
@@ -4429,7 +4439,12 @@ var init_schema = __esm(() => {
4429
4439
  soul: exports_external.object({
4430
4440
  name: exports_external.string().optional(),
4431
4441
  style: exports_external.string().optional(),
4432
- boundaries: exports_external.string().optional()
4442
+ creature: exports_external.string().optional(),
4443
+ vibe: exports_external.string().optional(),
4444
+ expertise: exports_external.string().optional(),
4445
+ emoji: exports_external.string().optional(),
4446
+ boundaries: exports_external.string().optional(),
4447
+ shape: exports_external.enum(["executive-assistant", "developer", "coach", "generalist"]).optional()
4433
4448
  }).optional(),
4434
4449
  tools: exports_external.object({
4435
4450
  allow: exports_external.array(exports_external.string()).optional(),
@@ -4626,7 +4641,8 @@ var init_schema = __esm(() => {
4626
4641
  autoUnlockCredentialPath: exports_external.string().default("~/.switchroom/vault-auto-unlock").describe("Path to the machine-bound auto-unlock blob (see " + "src/vault/auto-unlock.ts for the format). Default lives under " + "~/.switchroom so it can be bind-mounted into the vault-broker " + "container by docker compose. Tilde-expansion happens " + "at read time."),
4627
4642
  approvalAuth: exports_external.enum(["passphrase", "telegram-id"]).default("passphrase").describe("Posture for tap-to-Approve on vault grant cards. `passphrase` " + "(default) prompts the operator to type the vault passphrase on " + "every Approve — two-factor (Telegram ID + passphrase). " + "`telegram-id` mints immediately on Approve with no passphrase " + "prompt — single-factor (Telegram ID only); REQUIRES " + "`autoUnlock: true` so the broker already holds the passphrase. " + "Trades a factor of security for smoother UX; opt-in only."),
4628
4643
  postureMintAgents: exports_external.array(exports_external.string().min(1)).default([]).describe("Per-agent opt-in for posture-attested broker calls (`mint_grant` / " + "`list_grants` / `put` with `attest_via_posture: true`). Only agents " + "whose names are in this list can use the silent-mint path under " + "`approvalAuth: telegram-id`. Default `[]` — no agent can self-mint " + "until the operator explicitly opts it in. The request's `agent` " + "field must also equal the calling peer's resolved agent name " + "(broker rejects cross-agent posture mints). When `approvalAuth` is " + "`passphrase` this list is ignored — passphrase attestation still " + "works as before. Each entry is an agent slug exactly as it appears " + "under `agents:` in this config."),
4629
- adminOnlyKeys: exports_external.array(exports_external.string().min(1)).default([]).describe("Vault keys held to a higher approval bar: only the admin operator " + "(`access.allowFrom[0]`) may approve a grant for them, and they can " + "NEVER be minted via posture attestation — granting one requires the " + "operator passphrase (so an agent, even one on `postureMintAgents`, " + "cannot self-grant it). Entries are exact key names or `*` globs, " + "e.g. `stripe/*`, `*/oauth-token`, `microsoft/ken-tokens` (`*` matches " + "any run of characters incl. `/`; case-sensitive). Default `[]` — no " + "key is admin-only. Posture may RETAIN an admin-only key across a " + "union re-mint but never ADD one. Takes effect on broker + gateway " + "restart (broker has no ACL hot-reload).")
4644
+ adminOnlyKeys: exports_external.array(exports_external.string().min(1)).default([]).describe("Vault keys held to a higher approval bar: only the admin operator " + "(`access.allowFrom[0]`) may approve a grant for them, and they can " + "NEVER be minted via posture attestation — granting one requires the " + "operator passphrase (so an agent, even one on `postureMintAgents`, " + "cannot self-grant it). Entries are exact key names or `*` globs, " + "e.g. `stripe/*`, `*/oauth-token`, `microsoft/ken-tokens` (`*` matches " + "any run of characters incl. `/`; case-sensitive). Default `[]` — no " + "key is admin-only. Posture may RETAIN an admin-only key across a " + "union re-mint but never ADD one. Takes effect on broker + gateway " + "restart (broker has no ACL hot-reload)."),
4645
+ auditFailClosed: exports_external.boolean().default(false).describe("sec WS10-F3 (#1420) — fail-CLOSED on an audit-append failure. When " + "`false` (default, backward-compatible) the broker fails OPEN: if a " + "vault-audit.log append fails it logs to stderr, bumps the durable " + "fail-open counter, and STILL releases the secret. When `true`, a " + "failed audit append DENIES the secret release with " + "`AUDIT_UNAVAILABLE` — no secret leaves the broker without a durable " + "audit row. The env var `SWITCHROOM_VAULT_AUDIT_FAIL_CLOSED` " + "(`1`/`true` or `0`/`false`) overrides this at runtime. Either way " + "the fail-open counter is surfaced by `switchroom doctor`. Takes " + "effect on broker restart.")
4630
4646
  }).default({}).superRefine((broker, ctx) => {
4631
4647
  if (broker.approvalAuth === "telegram-id" && broker.autoUnlock !== true) {
4632
4648
  ctx.addIssue({
@@ -4665,7 +4681,9 @@ var init_schema = __esm(() => {
4665
4681
  });
4666
4682
  HostdConfigSchema = exports_external.object({
4667
4683
  config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit §3). Default false — the verb returns " + "`E_CONFIG_EDIT_DISABLED` until the operator explicitly flips " + "this to true. When true, admin agents can propose unified-diff " + "patches against " + "`/state/config/switchroom.yaml`, gated by an operator approval " + "card in the primary chat. Same trust posture as `update_apply` " + "and `agent_restart`: the human-in-the-loop tap is the security " + "boundary, not the agent's judgement."),
4668
- config_edit_rate_per_hour: exports_external.number().int().min(1).max(20).default(3).describe("Per-requesting-agent rate cap for `config_propose_edit` cards " + "(RFC admin-agent-config-edit §5). Default 3 cards/hour; min 1, " + "max 20. ENFORCED server-side: a caller exceeding this in a sliding " + "1-hour window is rejected with `E_RATE_LIMITED` (carrying a " + "`retry_after` fix) instead of posting another operator approval " + "card — so a looping agent is throttled rather than spamming the chat.")
4684
+ config_edit_rate_per_hour: exports_external.number().int().min(1).max(20).default(3).describe("Per-requesting-agent rate cap for `config_propose_edit` cards " + "(RFC admin-agent-config-edit §5). Default 3 cards/hour; min 1, " + "max 20. ENFORCED server-side: a caller exceeding this in a sliding " + "1-hour window is rejected with `E_RATE_LIMITED` (carrying a " + "`retry_after` fix) instead of posting another operator approval " + "card — so a looping agent is throttled rather than spamming the chat."),
4685
+ operator_attest_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the operator-passphrase 2nd factor on hostd's " + "mutating verbs (#1841, RFC host-control-daemon.md §5.4). Default " + "false — attestation is ACCEPTED-and-audited whenever present, but " + "no verb REQUIRES it (behaviour is byte-identical to today, so " + "existing fleets and the Telegram approval-card flow are unchanged). " + "When true, the verbs in `operator_attest_required_verbs` demand a " + "valid operator-passphrase attestation IN ADDITION to admin: hostd " + "forwards the passphrase to the vault broker over its own admin-client " + "connection (`/run/switchroom/broker/hostd/sock`) and treats a broker " + "DENIED as a gate failure. hostd never holds the passphrase. Leave " + "this off until BOTH the gateway passphrase-forward and the " + "cross-compose broker socket are deployed, or gated verbs will fail."),
4686
+ operator_attest_required_verbs: exports_external.array(exports_external.string().min(1)).default(["update_apply", "apply", "rollout"]).describe("Which mutating hostd verbs REQUIRE a valid operator-passphrase " + "attestation when `operator_attest_enabled` is true (#1841). Default " + "is the RFC §5.4 fleet-mutation set (`update_apply`, `apply`, " + "`rollout`). Verbs NOT in this list still accept-and-audit an " + "attestation when one is supplied, but never require it. Ignored " + "entirely when `operator_attest_enabled` is false.")
4669
4687
  });
4670
4688
  CronEgressSchema = exports_external.object({
4671
4689
  allowed_hosts: exports_external.array(exports_external.string().min(1)).default([]).describe("Hosts a poll may reach (exact, https-only). loopback/private/IP-literal are always rejected."),
@@ -4897,6 +4915,11 @@ function mergeAgentConfig(defaultsIn, agentIn) {
4897
4915
  if (v !== undefined)
4898
4916
  combined[k] = v;
4899
4917
  }
4918
+ const baseSoul = base;
4919
+ const overrideSoul = override;
4920
+ if (baseSoul.shape !== undefined && (overrideSoul.shape === undefined || overrideSoul.shape === "generalist")) {
4921
+ combined.shape = baseSoul.shape;
4922
+ }
4900
4923
  merged.soul = combined;
4901
4924
  }
4902
4925
  if (defaults.memory || merged.memory) {
@@ -18064,7 +18087,7 @@ var require_lib = __commonJS((exports, module) => {
18064
18087
 
18065
18088
  // src/vault/broker/server.ts
18066
18089
  import * as net from "node:net";
18067
- import { mkdirSync as mkdirSync6, chmodSync as chmodSync4, chownSync, existsSync as existsSync11, readFileSync as readFileSync9, readdirSync as readdirSync4, statSync as statSync6, unlinkSync as unlinkSync6, writeFileSync as writeFileSync2, renameSync as renameSync6 } from "node:fs";
18090
+ import { mkdirSync as mkdirSync6, chmodSync as chmodSync4, chownSync, existsSync as existsSync11, readFileSync as readFileSync10, readdirSync as readdirSync4, statSync as statSync6, unlinkSync as unlinkSync6, writeFileSync as writeFileSync3, renameSync as renameSync6 } from "node:fs";
18068
18091
 
18069
18092
  // src/agents/compose.ts
18070
18093
  import { createHash } from "node:crypto";
@@ -18096,6 +18119,7 @@ import { homedir as homedir2 } from "node:os";
18096
18119
  init_loader();
18097
18120
 
18098
18121
  // src/cli/agent-config.ts
18122
+ init_loader();
18099
18123
  init_merge();
18100
18124
 
18101
18125
  // src/config/google-workspace-acl.ts
@@ -18153,22 +18177,6 @@ function isWebkiteCredentialKeyForAgent(agentConfig, key) {
18153
18177
  return false;
18154
18178
  return true;
18155
18179
  }
18156
- function parseCronUnit(unitName) {
18157
- const m = unitName.match(/^switchroom-([a-zA-Z0-9_-]+)-cron-(\d+)\.service$/);
18158
- if (!m)
18159
- return null;
18160
- const agentName = m[1];
18161
- const index = parseInt(m[2], 10);
18162
- if (!agentName)
18163
- return null;
18164
- return { agentName, index };
18165
- }
18166
- function agentSlugFromPeer(peer) {
18167
- if (peer.systemdUnit === null)
18168
- return null;
18169
- const parsed = parseCronUnit(peer.systemdUnit);
18170
- return parsed?.agentName ?? null;
18171
- }
18172
18180
  function checkEntryScope(scope, agentSlug) {
18173
18181
  if (scope === undefined || scope === null) {
18174
18182
  return { allow: true };
@@ -18191,42 +18199,6 @@ function checkEntryScope(scope, agentSlug) {
18191
18199
  }
18192
18200
  return { allow: true };
18193
18201
  }
18194
- function checkAcl(peer, config, key) {
18195
- if (peer.systemdUnit !== null) {
18196
- const parsed = parseCronUnit(peer.systemdUnit);
18197
- if (parsed === null) {
18198
- return {
18199
- allow: false,
18200
- reason: `systemd unit '${peer.systemdUnit}' does not match switchroom cron unit naming convention`
18201
- };
18202
- }
18203
- const { agentName, index } = parsed;
18204
- const agentConfig = config.agents?.[agentName];
18205
- if (!agentConfig) {
18206
- return { allow: false, reason: `agent '${agentName}' not found in config` };
18207
- }
18208
- const schedule = agentConfig.schedule ?? [];
18209
- if (index >= schedule.length || index < 0) {
18210
- return {
18211
- allow: false,
18212
- reason: `schedule index ${index} out of range for agent '${agentName}' (${schedule.length} entries)`
18213
- };
18214
- }
18215
- const entry = schedule[index];
18216
- const allowedKeys = entry.secrets ?? [];
18217
- if (!allowedKeys.includes(key)) {
18218
- return {
18219
- allow: false,
18220
- reason: `key '${key}' not in ACL for ${agentName}/schedule[${index}]`
18221
- };
18222
- }
18223
- return { allow: true };
18224
- }
18225
- return {
18226
- allow: false,
18227
- reason: "caller is not a switchroom cron unit; use 'switchroom vault get --no-broker' for interactive access"
18228
- };
18229
- }
18230
18202
  function checkAclByAgent(config, agentName, key) {
18231
18203
  if (!agentName) {
18232
18204
  return { allow: false, reason: "agent name unresolved" };
@@ -18360,18 +18332,22 @@ var tails = new Map;
18360
18332
 
18361
18333
  // src/agents/inject.ts
18362
18334
  var INJECT_COMMANDS = new Map([
18363
- ["/cost", { description: "Show session cost", expectsOutput: true, dialog: true }],
18364
- ["/status", { description: "Show session status", expectsOutput: true, dialog: true }],
18365
- ["/usage", { description: "Show plan quota", expectsOutput: true, dialog: true }],
18366
- ["/hooks", { description: "List configured hooks", expectsOutput: true, dialog: true }],
18367
- ["/memory", { description: "Open memory picker", expectsOutput: true, dialog: true }],
18368
- ["/model", { description: "Open model picker", expectsOutput: true }],
18335
+ ["/cost", { description: "Show session cost", expectsOutput: true, dialog: true, argsAllowed: false }],
18336
+ ["/status", { description: "Show session status", expectsOutput: true, dialog: true, argsAllowed: false }],
18337
+ ["/usage", { description: "Show plan quota", expectsOutput: true, dialog: true, argsAllowed: false }],
18338
+ ["/hooks", { description: "List configured hooks", expectsOutput: true, dialog: true, argsAllowed: false }],
18339
+ ["/memory", { description: "Open memory picker", expectsOutput: true, dialog: true, argsAllowed: false }],
18340
+ ["/help", { description: "Show help / command discovery", expectsOutput: true, dialog: true, argsAllowed: false }],
18341
+ ["/context", { description: "Show context window usage", expectsOutput: true, argsAllowed: false }],
18342
+ ["/release-notes", { description: "Show release-notes version list", expectsOutput: true, dialog: true, argsAllowed: false }],
18343
+ ["/model", { description: "Open model picker", expectsOutput: true, argsAllowed: true }],
18369
18344
  [
18370
18345
  "/clear",
18371
18346
  {
18372
18347
  description: "Clear session screen",
18373
18348
  expectsOutput: false,
18374
- silentNote: "context cleared — fresh slate"
18349
+ silentNote: "context cleared — fresh slate",
18350
+ argsAllowed: false
18375
18351
  }
18376
18352
  ],
18377
18353
  [
@@ -18379,7 +18355,8 @@ var INJECT_COMMANDS = new Map([
18379
18355
  {
18380
18356
  description: "Compact conversation history",
18381
18357
  expectsOutput: false,
18382
- silentNote: "compaction runs silently"
18358
+ silentNote: "compaction runs silently",
18359
+ argsAllowed: false
18383
18360
  }
18384
18361
  ]
18385
18362
  ]);
@@ -18389,6 +18366,15 @@ var INJECT_BLOCKED = new Map([
18389
18366
  ["/logout", { reason: "would terminate the agent's auth session" }],
18390
18367
  ["/exit", { reason: "would kill the agent process" }],
18391
18368
  ["/quit", { reason: "would kill the agent process" }],
18369
+ ["/upgrade", { reason: "mutates the Claude Code installation" }],
18370
+ ["/init", { reason: "generates/overwrites CLAUDE.md and runs a model turn" }],
18371
+ ["/mcp", { reason: "opens an interactive MCP server management dialog" }],
18372
+ ["/permissions", { reason: "opens an interactive permissions editor that mutates tool policy" }],
18373
+ ["/install-github-app", { reason: "runs a network/OAuth install flow" }],
18374
+ ["/add-dir", { reason: "mutates the session's working-directory set" }],
18375
+ ["/terminal-setup", { reason: "mutates terminal keybinding configuration" }],
18376
+ ["/privacy-settings", { reason: "opens an interactive privacy-settings dialog" }],
18377
+ ["/bug", { reason: "submits a bug report over the network" }],
18392
18378
  [
18393
18379
  "/effort",
18394
18380
  {
@@ -18406,6 +18392,7 @@ var HINDSIGHT_CONSUMER_NAME = "hindsight";
18406
18392
  var HINDSIGHT_IMAGE_REPO = "ghcr.io/switchroom/switchroom-hindsight";
18407
18393
  var HINDSIGHT_IMAGE = `${HINDSIGHT_IMAGE_REPO}:latest`;
18408
18394
  var HINDSIGHT_BROKER_SOCK_VOLUME = `auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock`;
18395
+ var HINDSIGHT_CREDS_MIRROR_VOLUME = `consumer-creds-${HINDSIGHT_CONSUMER_NAME}`;
18409
18396
  var HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)';
18410
18397
  var HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
18411
18398
 
@@ -19353,7 +19340,8 @@ var ErrorCode = exports_external.enum([
19353
19340
  "DENIED",
19354
19341
  "UNKNOWN_KEY",
19355
19342
  "BAD_REQUEST",
19356
- "INTERNAL"
19343
+ "INTERNAL",
19344
+ "AUDIT_UNAVAILABLE"
19357
19345
  ]);
19358
19346
  var OkEntryResponseSchema = exports_external.object({
19359
19347
  ok: exports_external.literal(true),
@@ -19541,6 +19529,7 @@ init_paths();
19541
19529
 
19542
19530
  // src/agents/scaffold.ts
19543
19531
  var REPO_ROOT = resolve6(import.meta.dirname, "../..");
19532
+ var CLAUDE_MD_YOURS_PLACEHOLDER = "This space is yours. Add per-agent rules, exceptions, or context the " + "Switchroom template doesn't capture. Everything above the marker line is " + "regenerated on every apply; this section is preserved.";
19544
19533
  var SWITCHROOM_OWNED_SETTINGS_KEYS = new Set([
19545
19534
  "permissions",
19546
19535
  "mcpServers",
@@ -19928,6 +19917,23 @@ function safeAuditLogPath(requestedPath) {
19928
19917
  }
19929
19918
 
19930
19919
  // src/vault/broker/audit-log.ts
19920
+ function failOpenStatePath(logPath) {
19921
+ return `${logPath}.failopen`;
19922
+ }
19923
+ function readFailOpenState(statePath) {
19924
+ try {
19925
+ const raw = fs.readFileSync(statePath, "utf8");
19926
+ const parsed = JSON.parse(raw);
19927
+ const count = Number(parsed.failOpenCount);
19928
+ return {
19929
+ failOpenCount: Number.isFinite(count) && count > 0 ? count : 0,
19930
+ lastFailureTs: typeof parsed.lastFailureTs === "string" ? parsed.lastFailureTs : undefined,
19931
+ lastError: typeof parsed.lastError === "string" ? parsed.lastError : undefined
19932
+ };
19933
+ } catch {
19934
+ return { failOpenCount: 0 };
19935
+ }
19936
+ }
19931
19937
  var DEFAULT_AUDIT_MAX_BYTES = 32 * 1024 * 1024;
19932
19938
  var DEFAULT_AUDIT_MAX_FILES = 5;
19933
19939
  function resolveRotation(opts) {
@@ -20008,7 +20014,27 @@ function createAuditLogger(opts = {}) {
20008
20014
  const logPath = safeAuditLogPath(opts.path ?? defaultAuditLogPath());
20009
20015
  let chain = seedChain(logPath);
20010
20016
  const rotation = resolveRotation(opts);
20017
+ const statePath = opts.statePath ?? failOpenStatePath(logPath);
20018
+ let failOpen = readFailOpenState(statePath).failOpenCount;
20019
+ const recordFailOpen = (err) => {
20020
+ failOpen += 1;
20021
+ const state = {
20022
+ failOpenCount: failOpen,
20023
+ lastFailureTs: new Date().toISOString(),
20024
+ lastError: String(err.message).slice(0, 300)
20025
+ };
20026
+ try {
20027
+ fs.writeFileSync(statePath, `${JSON.stringify(state)}
20028
+ `, { mode: 384 });
20029
+ } catch (persistErr) {
20030
+ process.stderr.write(`[vault-audit] ERROR: could not persist fail-open counter ${statePath}: ${persistErr.message}
20031
+ `);
20032
+ }
20033
+ };
20011
20034
  return {
20035
+ failOpenCount() {
20036
+ return failOpen;
20037
+ },
20012
20038
  write(entry) {
20013
20039
  if (rotation.maxBytes > 0) {
20014
20040
  try {
@@ -20025,14 +20051,18 @@ function createAuditLogger(opts = {}) {
20025
20051
  } catch (err) {
20026
20052
  process.stderr.write(`[vault-audit] ERROR: could not open audit log ${logPath}: ${err.message}
20027
20053
  `);
20028
- return;
20054
+ recordFailOpen(err);
20055
+ return false;
20029
20056
  }
20057
+ let durable = false;
20030
20058
  try {
20031
20059
  fs.writeSync(fd, line);
20032
20060
  chain = next;
20061
+ durable = true;
20033
20062
  } catch (err) {
20034
20063
  process.stderr.write(`[vault-audit] ERROR: could not write to audit log ${logPath}: ${err.message}
20035
20064
  `);
20065
+ recordFailOpen(err);
20036
20066
  } finally {
20037
20067
  try {
20038
20068
  fs.closeSync(fd);
@@ -20041,6 +20071,7 @@ function createAuditLogger(opts = {}) {
20041
20071
  `);
20042
20072
  }
20043
20073
  }
20074
+ return durable;
20044
20075
  }
20045
20076
  };
20046
20077
  }
@@ -22374,6 +22405,21 @@ class VaultBroker {
22374
22405
  this.grantsDb = openGrantsDb();
22375
22406
  }
22376
22407
  }
22408
+ auditFailClosedEnabled() {
22409
+ const env = process.env.SWITCHROOM_VAULT_AUDIT_FAIL_CLOSED;
22410
+ if (env !== undefined && env !== "") {
22411
+ return env === "1" || env.toLowerCase() === "true";
22412
+ }
22413
+ return this.config?.vault?.broker?.auditFailClosed === true;
22414
+ }
22415
+ auditedReleaseOk(socket, writeAudit, entry) {
22416
+ const durable = writeAudit(entry);
22417
+ if (!durable && this.auditFailClosedEnabled()) {
22418
+ socket.write(encodeResponse(errorResponse("AUDIT_UNAVAILABLE", "secret release denied: audit append failed and broker is in fail-closed mode (vault.broker.auditFailClosed)")));
22419
+ return false;
22420
+ }
22421
+ return true;
22422
+ }
22377
22423
  async start(socketPath, configPath, vaultPath) {
22378
22424
  if (process.platform !== "linux" && process.env.SWITCHROOM_BROKER_ALLOW_NON_LINUX !== "1") {
22379
22425
  throw new Error(`vault-broker is Linux-only (running on ${process.platform}). ` + `The broker's ACL relies on cgroup-based systemd unit identification, ` + `which is not available on this platform. ` + `Use 'switchroom vault get --no-broker' for direct vault access. ` + `If you need to run the broker for development on this platform, ` + `set SWITCHROOM_BROKER_ALLOW_NON_LINUX=1 — but understand that the ` + `broker will accept any same-user caller without per-cron ACL enforcement.`);
@@ -22441,7 +22487,7 @@ class VaultBroker {
22441
22487
  return;
22442
22488
  try {
22443
22489
  if (ready) {
22444
- writeFileSync2(p, "", { mode: 384 });
22490
+ writeFileSync3(p, "", { mode: 384 });
22445
22491
  } else if (existsSync11(p)) {
22446
22492
  unlinkSync6(p);
22447
22493
  }
@@ -22740,7 +22786,7 @@ class VaultBroker {
22740
22786
  const auditPeerUid = peer?.uid;
22741
22787
  const auditAgentName = agentName ?? undefined;
22742
22788
  const writeAudit = (entry) => {
22743
- this.auditLogger.write({
22789
+ return this.auditLogger.write({
22744
22790
  ...entry,
22745
22791
  peer_uid: entry.peer_uid ?? auditPeerUid,
22746
22792
  agent_name: entry.agent_name ?? auditAgentName
@@ -22849,8 +22895,8 @@ class VaultBroker {
22849
22895
  return;
22850
22896
  }
22851
22897
  }
22852
- if (!isOperator && agentName === null && process.platform === "linux" && peer === null) {
22853
- const reason = "Unable to identify caller (peercred unavailable); denying on Linux";
22898
+ if (!isOperator && agentName === null && process.platform === "linux") {
22899
+ const reason = "Unable to identify caller (no per-agent socket identity); denying on Linux";
22854
22900
  this.auditLogger.write({
22855
22901
  ts: new Date().toISOString(),
22856
22902
  op: "list",
@@ -22862,14 +22908,12 @@ class VaultBroker {
22862
22908
  socket.write(encodeResponse(errorResponse("DENIED", reason)));
22863
22909
  return;
22864
22910
  }
22865
- const listAgentSlug = agentName ?? (peer !== null ? agentSlugFromPeer(peer) : null);
22911
+ const listAgentSlug = agentName;
22866
22912
  let visibleKeys;
22867
22913
  if (isOperator) {
22868
22914
  visibleKeys = Object.entries(this.secrets).filter(([, entry]) => checkEntryScope(entry.scope, "operator").allow).map(([k]) => k);
22869
22915
  } else if (agentName !== null && this.config !== null) {
22870
22916
  visibleKeys = Object.entries(this.secrets).filter(([key, entry]) => checkAclByAgent(this.config, agentName, key).allow && checkEntryScope(entry.scope, agentName).allow).map(([k]) => k);
22871
- } else if (peer !== null && this.config !== null) {
22872
- visibleKeys = Object.entries(this.secrets).filter(([key, entry]) => checkAcl(peer, this.config, key).allow && checkEntryScope(entry.scope, listAgentSlug).allow).map(([k]) => k);
22873
22917
  } else {
22874
22918
  visibleKeys = Object.entries(this.secrets).filter(([, entry]) => checkEntryScope(entry.scope, listAgentSlug).allow).map(([k]) => k);
22875
22919
  }
@@ -22893,6 +22937,21 @@ class VaultBroker {
22893
22937
  const grantResult = await validateGrant(this.grantsDb, req.token, req.key);
22894
22938
  if (grantResult.ok) {
22895
22939
  const grantId = grantResult.grant.id;
22940
+ if (agentName !== null && grantResult.grant.agent_slug !== agentName) {
22941
+ this.auditLogger.write({
22942
+ ts: new Date().toISOString(),
22943
+ op: "get",
22944
+ key: req.key,
22945
+ caller: auditCaller,
22946
+ pid: auditPid,
22947
+ cgroup: auditCgroup,
22948
+ result: "denied:grant-identity-mismatch",
22949
+ method: "grant",
22950
+ grant_id: grantId
22951
+ });
22952
+ socket.write(encodeResponse(errorResponse("DENIED", "grant-identity-mismatch")));
22953
+ return;
22954
+ }
22896
22955
  const entry2 = this.secrets[req.key];
22897
22956
  if (entry2 === undefined) {
22898
22957
  this.auditLogger.write({
@@ -22909,7 +22968,23 @@ class VaultBroker {
22909
22968
  socket.write(encodeResponse(errorResponse("UNKNOWN_KEY", `Key not found: ${req.key}`)));
22910
22969
  return;
22911
22970
  }
22912
- this.auditLogger.write({
22971
+ const tokenScope = checkEntryScope(entry2.scope, grantResult.grant.agent_slug);
22972
+ if (!tokenScope.allow) {
22973
+ this.auditLogger.write({
22974
+ ts: new Date().toISOString(),
22975
+ op: "get",
22976
+ key: req.key,
22977
+ caller: auditCaller,
22978
+ pid: auditPid,
22979
+ cgroup: auditCgroup,
22980
+ result: `denied:${tokenScope.reason}`,
22981
+ method: "grant",
22982
+ grant_id: grantId
22983
+ });
22984
+ socket.write(encodeResponse(errorResponse("DENIED", tokenScope.reason)));
22985
+ return;
22986
+ }
22987
+ if (!this.auditedReleaseOk(socket, writeAudit, {
22913
22988
  ts: new Date().toISOString(),
22914
22989
  op: "get",
22915
22990
  key: req.key,
@@ -22919,7 +22994,9 @@ class VaultBroker {
22919
22994
  result: "allowed",
22920
22995
  method: "grant",
22921
22996
  grant_id: grantId
22922
- });
22997
+ })) {
22998
+ return;
22999
+ }
22923
23000
  socket.write(encodeResponse(entryResponse(entry2)));
22924
23001
  return;
22925
23002
  } else if (grantResult.reason === "grant-revoked") {
@@ -22973,23 +23050,8 @@ class VaultBroker {
22973
23050
  socket.write(encodeResponse(errorResponse("DENIED", aclResult.reason)));
22974
23051
  return;
22975
23052
  }
22976
- } else if (peer !== null && this.config !== null) {
22977
- const aclResult = checkAcl(peer, this.config, req.key);
22978
- if (!aclResult.allow) {
22979
- writeAudit({
22980
- ts: new Date().toISOString(),
22981
- op: "get",
22982
- key: req.key,
22983
- caller: auditCaller,
22984
- pid: auditPid,
22985
- cgroup: auditCgroup,
22986
- result: `denied:${aclResult.reason}`
22987
- });
22988
- socket.write(encodeResponse(errorResponse("DENIED", aclResult.reason)));
22989
- return;
22990
- }
22991
- } else if (process.platform === "linux" && peer === null) {
22992
- const reason = "Unable to identify caller (peercred unavailable); denying on Linux";
23053
+ } else if (process.platform === "linux" && agentName === null) {
23054
+ const reason = "Unable to identify caller (no per-agent socket identity); denying on Linux";
22993
23055
  this.auditLogger.write({
22994
23056
  ts: new Date().toISOString(),
22995
23057
  op: "get",
@@ -23016,7 +23078,7 @@ class VaultBroker {
23016
23078
  socket.write(encodeResponse(errorResponse("UNKNOWN_KEY", `Key not found: ${req.key}`)));
23017
23079
  return;
23018
23080
  }
23019
- const getAgentSlug = agentName ?? (peer !== null ? agentSlugFromPeer(peer) : null);
23081
+ const getAgentSlug = agentName;
23020
23082
  const scopeResult = checkEntryScope(entry.scope, getAgentSlug);
23021
23083
  if (!scopeResult.allow) {
23022
23084
  this.auditLogger.write({
@@ -23031,7 +23093,7 @@ class VaultBroker {
23031
23093
  socket.write(encodeResponse(errorResponse("DENIED", scopeResult.reason)));
23032
23094
  return;
23033
23095
  }
23034
- this.auditLogger.write({
23096
+ if (!this.auditedReleaseOk(socket, writeAudit, {
23035
23097
  ts: new Date().toISOString(),
23036
23098
  op: "get",
23037
23099
  key: req.key,
@@ -23039,7 +23101,9 @@ class VaultBroker {
23039
23101
  pid: auditPid,
23040
23102
  cgroup: auditCgroup,
23041
23103
  result: "allowed"
23042
- });
23104
+ })) {
23105
+ return;
23106
+ }
23043
23107
  socket.write(encodeResponse(entryResponse(entry)));
23044
23108
  return;
23045
23109
  }
@@ -23447,21 +23511,6 @@ class VaultBroker {
23447
23511
  socket.write(encodeResponse(errorResponse("DENIED", "peercred unavailable; cannot verify operator identity")));
23448
23512
  return;
23449
23513
  }
23450
- if (peer !== null && peer.systemdUnit !== null) {
23451
- const parsed = parseCronUnit(peer.systemdUnit);
23452
- if (parsed !== null) {
23453
- this.auditLogger.write({
23454
- ts: new Date().toISOString(),
23455
- op: req.op,
23456
- caller: auditCaller,
23457
- pid: auditPid,
23458
- cgroup: auditCgroup,
23459
- result: "denied:cron-cannot-manage-grants"
23460
- });
23461
- socket.write(encodeResponse(errorResponse("DENIED", "Grant management ops are operator-only; cron units cannot mint, list, or revoke grants")));
23462
- return;
23463
- }
23464
- }
23465
23514
  }
23466
23515
  }
23467
23516
  if (req.op === "mint_grant") {
@@ -23500,7 +23549,7 @@ class VaultBroker {
23500
23549
  mkdirSync6(tokenDir, { recursive: true });
23501
23550
  const tokenPath = path3.join(tokenDir, ".vault-token");
23502
23551
  const tmpPath = `${tokenPath}.tmp.${process.pid}`;
23503
- writeFileSync2(tmpPath, mintResult.token, { mode: 384 });
23552
+ writeFileSync3(tmpPath, mintResult.token, { mode: 384 });
23504
23553
  renameSync6(tmpPath, tokenPath);
23505
23554
  try {
23506
23555
  const uid = allocateAgentUid(agent);
@@ -23787,7 +23836,7 @@ class VaultBroker {
23787
23836
  _writePidFile() {
23788
23837
  try {
23789
23838
  const pidPath = resolvePath(PID_FILE_DEFAULT);
23790
- writeFileSync2(pidPath, String(process.pid) + `
23839
+ writeFileSync3(pidPath, String(process.pid) + `
23791
23840
  `, { mode: 384 });
23792
23841
  } catch {}
23793
23842
  }
@@ -23843,7 +23892,7 @@ class VaultBroker {
23843
23892
  const credPath = `${dir}/vault-passphrase`;
23844
23893
  let passphrase;
23845
23894
  try {
23846
- passphrase = readFileSync9(credPath, "utf8").replace(/\n+$/, "");
23895
+ passphrase = readFileSync10(credPath, "utf8").replace(/\n+$/, "");
23847
23896
  } catch (err) {
23848
23897
  const code = err.code;
23849
23898
  if (code === "ENOENT") {
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.18.10",
4
+ "version": "0.18.12",
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": {
@@ -24,9 +24,9 @@
24
24
  "build": "node scripts/build.mjs",
25
25
  "build:cli": "node scripts/build.mjs && bun build --compile --target=bun-linux-x64 --minify bin/switchroom.ts --outfile switchroom-linux-amd64",
26
26
  "pretest": "npm run build",
27
- "test": "vitest run && bun test telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/registry/api-registry.test.ts telegram-plugin/registry/turns-schema.test.ts telegram-plugin/tests/idle-footer-wiring.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
27
+ "test": "vitest run && bun test telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/registry/api-registry.test.ts telegram-plugin/registry/turns-schema.test.ts telegram-plugin/tests/idle-footer-wiring.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
28
28
  "test:vitest": "vitest run",
29
- "test:bun": "bun test src/vault/grants.test.ts src/vault/grants-db.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
29
+ "test:bun": "bun test src/vault/grants.test.ts src/vault/grants-db.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
30
30
  "test:watch": "vitest",
31
31
  "lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-web-subscription-honest.mjs",
32
32
  "lint:tsc": "tsc --noEmit",
@@ -43,7 +43,6 @@
43
43
  "@modelcontextprotocol/sdk": "^1.0.0",
44
44
  "@secretlint/core": "^12.2.0",
45
45
  "@secretlint/secretlint-rule-preset-recommend": "^12.2.0",
46
- "@secretlint/types": "^12.2.0",
47
46
  "@xterm/headless": "^6.0.0",
48
47
  "bcryptjs": "^3.0.3",
49
48
  "chalk": "^5.4.0",