switchroom 0.18.11 → 0.18.13

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 (150) hide show
  1. package/dist/agent-scheduler/index.js +37 -5
  2. package/dist/auth-broker/index.js +116 -78
  3. package/dist/cli/hindsight-mental-model-pretool.mjs +39 -0
  4. package/dist/cli/ms-365-write-pretool.mjs +31 -8
  5. package/dist/cli/notion-write-pretool.mjs +38 -6
  6. package/dist/cli/skill-validate-pretool.mjs +144 -2847
  7. package/dist/cli/switchroom.js +3529 -4543
  8. package/dist/cli/ui/index.html +163 -17
  9. package/dist/host-control/main.js +605 -2847
  10. package/dist/vault/approvals/kernel-server.js +120 -13
  11. package/dist/vault/broker/server.js +259 -157
  12. package/package.json +3 -4
  13. package/profiles/_base/start.sh.hbs +65 -0
  14. package/profiles/_shared/vault-protocol.md.hbs +3 -1
  15. package/profiles/coding/CLAUDE.md.hbs +1 -1
  16. package/profiles/default/CLAUDE.md.hbs +2 -2
  17. package/profiles/executive-assistant/CLAUDE.md.hbs +1 -1
  18. package/profiles/health-coach/CLAUDE.md.hbs +1 -1
  19. package/telegram-plugin/bridge/bridge.ts +37 -0
  20. package/telegram-plugin/bridge/inbound-dedup.ts +101 -0
  21. package/telegram-plugin/dist/bridge/bridge.js +122 -4
  22. package/telegram-plugin/dist/gateway/gateway.js +4213 -3288
  23. package/telegram-plugin/dist/server.js +139 -5
  24. package/telegram-plugin/flood-circuit-breaker.ts +493 -21
  25. package/telegram-plugin/format.ts +19 -0
  26. package/telegram-plugin/gateway/approval-hold.ts +602 -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 +88 -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 +1695 -230
  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/history.ts +51 -0
  54. package/telegram-plugin/hooks/dispatch-claim-scan.mjs +259 -0
  55. package/telegram-plugin/hooks/dispatch-claim-stop.mjs +129 -0
  56. package/telegram-plugin/hooks/hooks.json +9 -0
  57. package/telegram-plugin/inline-keyboard-callbacks.ts +303 -2
  58. package/telegram-plugin/model-unavailable.ts +41 -11
  59. package/telegram-plugin/operator-events.ts +23 -0
  60. package/telegram-plugin/outbound-field-redact.ts +69 -0
  61. package/telegram-plugin/package.json +0 -1
  62. package/telegram-plugin/permission-rule.ts +1 -0
  63. package/telegram-plugin/permission-title.ts +1 -0
  64. package/telegram-plugin/render/render.ts +32 -14
  65. package/telegram-plugin/retry-api-call.ts +212 -2
  66. package/telegram-plugin/scoped-approval.ts +11 -2
  67. package/telegram-plugin/secret-detect/chunker.ts +18 -4
  68. package/telegram-plugin/secret-detect/index.ts +12 -56
  69. package/telegram-plugin/send-gate-degraded.test.ts +574 -0
  70. package/telegram-plugin/send-gate-observability.test.ts +470 -0
  71. package/telegram-plugin/send-gate-observability.ts +355 -0
  72. package/telegram-plugin/send-gate.test.ts +717 -0
  73. package/telegram-plugin/send-gate.ts +1056 -0
  74. package/telegram-plugin/session-tail.ts +82 -7
  75. package/telegram-plugin/shared/bot-runtime.ts +17 -5
  76. package/telegram-plugin/shared/gw-trace-gate.ts +105 -0
  77. package/telegram-plugin/status-pin-driver.ts +52 -7
  78. package/telegram-plugin/status-pin.ts +81 -0
  79. package/telegram-plugin/subagent-watcher.ts +173 -18
  80. package/telegram-plugin/tests/activity-card-wiring.test.ts +18 -5
  81. package/telegram-plugin/tests/approval-hold-harness.ts +425 -0
  82. package/telegram-plugin/tests/approval-hold-outcome.test.ts +327 -0
  83. package/telegram-plugin/tests/approval-hold-record.test.ts +531 -0
  84. package/telegram-plugin/tests/approval-hold-redeliver.test.ts +602 -0
  85. package/telegram-plugin/tests/auth-loopback-relay.test.ts +533 -0
  86. package/telegram-plugin/tests/boot-card-flood-suppress.test.ts +53 -7
  87. package/telegram-plugin/tests/busy-key-reaper.test.ts +1 -0
  88. package/telegram-plugin/tests/callback-query-handlers.test.ts +65 -0
  89. package/telegram-plugin/tests/dispatch-claim-scan.test.ts +250 -0
  90. package/telegram-plugin/tests/flood-breaker-blindness.test.ts +213 -0
  91. package/telegram-plugin/tests/flood-windows-persistence.test.ts +224 -0
  92. package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
  93. package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +29 -1
  94. package/telegram-plugin/tests/gateway-loopback-paste-redact.test.ts +66 -0
  95. package/telegram-plugin/tests/gateway-outbound-redact.test.ts +57 -0
  96. package/telegram-plugin/tests/gw-trace-gate.test.ts +105 -0
  97. package/telegram-plugin/tests/history.test.ts +115 -0
  98. package/telegram-plugin/tests/idle-clear.test.ts +233 -3
  99. package/telegram-plugin/tests/inbound-dedup.test.ts +93 -0
  100. package/telegram-plugin/tests/inbound-message-types.test.ts +5 -1
  101. package/telegram-plugin/tests/inline-keyboard-callbacks.test.ts +448 -0
  102. package/telegram-plugin/tests/ipc-server-check-pre-approved.test.ts +194 -0
  103. package/telegram-plugin/tests/mental-model-propose-resolve.test.ts +123 -0
  104. package/telegram-plugin/tests/missed-approvals-wiring.test.ts +1 -1
  105. package/telegram-plugin/tests/model-command.test.ts +14 -0
  106. package/telegram-plugin/tests/operator-events-session-tail.test.ts +74 -0
  107. package/telegram-plugin/tests/outbound-field-redact.test.ts +107 -0
  108. package/telegram-plugin/tests/pending-session-command.test.ts +21 -0
  109. package/telegram-plugin/tests/permission-card-routing.test.ts +30 -5
  110. package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +8 -7
  111. package/telegram-plugin/tests/permission-rearm-wiring.test.ts +1 -1
  112. package/telegram-plugin/tests/pre-approval-check.test.ts +148 -0
  113. package/telegram-plugin/tests/queued-card-store.test.ts +232 -0
  114. package/telegram-plugin/tests/reaction-flush-turn-gated.test.ts +100 -0
  115. package/telegram-plugin/tests/reaction-gate-routing.test.ts +173 -0
  116. package/telegram-plugin/tests/render/render.test.ts +88 -0
  117. package/telegram-plugin/tests/retry-api-call.test.ts +398 -0
  118. package/telegram-plugin/tests/scoped-approval.test.ts +27 -0
  119. package/telegram-plugin/tests/secret-detect-chunk-overlap.test.ts +65 -0
  120. package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +5 -4
  121. package/telegram-plugin/tests/session-model-file.test.ts +50 -0
  122. package/telegram-plugin/tests/session-tail-sidecar-reap.test.ts +268 -0
  123. package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +35 -14
  124. package/telegram-plugin/tests/status-pin.test.ts +275 -1
  125. package/telegram-plugin/tests/subagent-watcher-deferral-log-ratelimit.test.ts +316 -0
  126. package/telegram-plugin/tests/subagent-watcher-fd-leak.test.ts +275 -0
  127. package/telegram-plugin/tests/turn-end-gate-backstop.test.ts +92 -0
  128. package/telegram-plugin/tests/turn-end-gate.test.ts +137 -0
  129. package/telegram-plugin/tests/typing-emitter.test.ts +586 -0
  130. package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +20 -0
  131. package/telegram-plugin/tests/worktree-watch-cwds.test.ts +215 -1
  132. package/telegram-plugin/typing-emitter.ts +224 -0
  133. package/telegram-plugin/uat/scenarios/jtbd-feel-like-a-colleague-dm.test.ts +136 -0
  134. package/telegram-plugin/welcome-text.ts +42 -0
  135. package/telegram-plugin/worktree-watch-cwds.ts +194 -5
  136. package/vendor/hindsight-memory/scripts/drain_pending.py +22 -6
  137. package/vendor/hindsight-memory/scripts/lib/client.py +12 -5
  138. package/vendor/hindsight-memory/scripts/lib/directives.py +38 -3
  139. package/vendor/hindsight-memory/scripts/lib/pending.py +36 -9
  140. package/vendor/hindsight-memory/scripts/session_end.py +14 -3
  141. package/vendor/hindsight-memory/scripts/session_start.py +21 -0
  142. package/vendor/hindsight-memory/scripts/tests/test_directives.py +38 -0
  143. package/vendor/hindsight-memory/tests/test_drain_pending.py +68 -0
  144. package/vendor/hindsight-memory/tests/test_pending.py +44 -0
  145. package/vendor/hindsight-memory/tests/test_session_end_pending.py +38 -0
  146. package/vendor/hindsight-memory/tests/test_session_start_drain.py +155 -0
  147. package/telegram-plugin/channel-envelope-safety.test.ts +0 -56
  148. package/telegram-plugin/channel-envelope-safety.ts +0 -56
  149. package/telegram-plugin/secret-detect/secretlint-source.ts +0 -95
  150. package/telegram-plugin/tests/secret-detect-secretlint.test.ts +0 -105
@@ -11059,7 +11059,12 @@ var ScheduleEntrySchema = exports_external.object({
11059
11059
  var AgentSoulSchema = exports_external.object({
11060
11060
  name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
11061
11061
  style: exports_external.string().describe("Communication style description"),
11062
- boundaries: exports_external.string().optional().describe("Behavioral boundaries and disclaimers")
11062
+ creature: exports_external.string().optional().describe("Persona creature/form (e.g., 'owl', 'octopus')"),
11063
+ vibe: exports_external.string().optional().describe("One-line personality vibe (e.g., 'calm, precise')"),
11064
+ expertise: exports_external.string().optional().describe("Domain expertise summary rendered into the persona"),
11065
+ emoji: exports_external.string().optional().describe("Signature emoji for the persona (e.g., '\uD83E\uDD89')"),
11066
+ boundaries: exports_external.string().optional().describe("Behavioral boundaries and disclaimers"),
11067
+ 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).")
11063
11068
  }).optional();
11064
11069
  var AgentToolsSchema = exports_external.object({
11065
11070
  allow: exports_external.array(exports_external.string()).default([]).describe("Allowed tools (use ['all'] for unrestricted)"),
@@ -11224,6 +11229,11 @@ var TelegramChannelSchema = exports_external.object({
11224
11229
  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."),
11225
11230
  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.")
11226
11231
  }).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."),
11232
+ button_choice_confirmation: exports_external.object({
11233
+ 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."),
11234
+ 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."),
11235
+ 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.")
11236
+ }).optional().describe("Auto-confirm 'You chose: X' annotation on inline_keyboard taps (#789). " + "Cascades from defaults.channels.telegram.button_choice_confirmation."),
11227
11237
  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.)"),
11228
11238
  webhook_dispatch: exports_external.object({
11229
11239
  github: exports_external.array(webhookDispatchRule).optional(),
@@ -11372,7 +11382,12 @@ var profileFields = {
11372
11382
  soul: exports_external.object({
11373
11383
  name: exports_external.string().optional(),
11374
11384
  style: exports_external.string().optional(),
11375
- boundaries: exports_external.string().optional()
11385
+ creature: exports_external.string().optional(),
11386
+ vibe: exports_external.string().optional(),
11387
+ expertise: exports_external.string().optional(),
11388
+ emoji: exports_external.string().optional(),
11389
+ boundaries: exports_external.string().optional(),
11390
+ shape: exports_external.enum(["executive-assistant", "developer", "coach", "generalist"]).optional()
11376
11391
  }).optional(),
11377
11392
  tools: exports_external.object({
11378
11393
  allow: exports_external.array(exports_external.string()).optional(),
@@ -11569,7 +11584,8 @@ var VaultConfigSchema = exports_external.object({
11569
11584
  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."),
11570
11585
  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."),
11571
11586
  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."),
11572
- 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).")
11587
+ 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)."),
11588
+ 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.")
11573
11589
  }).default({}).superRefine((broker, ctx) => {
11574
11590
  if (broker.approvalAuth === "telegram-id" && broker.autoUnlock !== true) {
11575
11591
  ctx.addIssue({
@@ -11608,7 +11624,9 @@ var FleetHealthConfigSchema = exports_external.object({
11608
11624
  });
11609
11625
  var HostdConfigSchema = exports_external.object({
11610
11626
  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."),
11611
- 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.")
11627
+ 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."),
11628
+ 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."),
11629
+ 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.")
11612
11630
  });
11613
11631
  var CronEgressSchema = exports_external.object({
11614
11632
  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."),
@@ -12020,6 +12038,11 @@ function mergeAgentConfig(defaultsIn, agentIn) {
12020
12038
  if (v !== undefined)
12021
12039
  combined[k] = v;
12022
12040
  }
12041
+ const baseSoul = base;
12042
+ const overrideSoul = override;
12043
+ if (baseSoul.shape !== undefined && (overrideSoul.shape === undefined || overrideSoul.shape === "generalist")) {
12044
+ combined.shape = baseSoul.shape;
12045
+ }
12023
12046
  merged.soul = combined;
12024
12047
  }
12025
12048
  if (defaults.memory || merged.memory) {
@@ -13097,6 +13120,13 @@ var ApprovalLookupRequestSchema = exports_external.object({
13097
13120
  action: exports_external.string().min(1),
13098
13121
  current_approver_set: exports_external.array(exports_external.string())
13099
13122
  });
13123
+ var ApprovalLookupByRequestRequestSchema = exports_external.object({
13124
+ v: exports_external.literal(1),
13125
+ op: exports_external.literal("approval_lookup_by_request"),
13126
+ agent_unit: exports_external.string().min(1),
13127
+ request_id: exports_external.string().regex(/^[0-9a-f]{32}$/),
13128
+ current_approver_set: exports_external.array(exports_external.string())
13129
+ });
13100
13130
  var ApprovalConsumeRequestSchema = exports_external.object({
13101
13131
  v: exports_external.literal(1),
13102
13132
  op: exports_external.literal("approval_consume"),
@@ -13151,6 +13181,7 @@ var RequestSchema = exports_external.discriminatedUnion("op", [
13151
13181
  RevokeGrantRequestSchema,
13152
13182
  ApprovalRequestRequestSchema,
13153
13183
  ApprovalLookupRequestSchema,
13184
+ ApprovalLookupByRequestRequestSchema,
13154
13185
  ApprovalConsumeRequestSchema,
13155
13186
  ApprovalRevokeRequestSchema,
13156
13187
  ApprovalListRequestSchema,
@@ -13173,7 +13204,8 @@ var ErrorCode = exports_external.enum([
13173
13204
  "DENIED",
13174
13205
  "UNKNOWN_KEY",
13175
13206
  "BAD_REQUEST",
13176
- "INTERNAL"
13207
+ "INTERNAL",
13208
+ "AUDIT_UNAVAILABLE"
13177
13209
  ]);
13178
13210
  var OkEntryResponseSchema = exports_external.object({
13179
13211
  ok: exports_external.literal(true),
@@ -7010,6 +7010,41 @@ var require_dist = __commonJS((exports) => {
7010
7010
  exports.visitAsync = visit.visitAsync;
7011
7011
  });
7012
7012
 
7013
+ // src/util/atomic.ts
7014
+ import { randomBytes } from "node:crypto";
7015
+ import { closeSync, constants, fsyncSync, openSync, renameSync, rmSync, writeSync } from "node:fs";
7016
+ function atomicWriteFileSync(destPath, contents, mode = 384) {
7017
+ const tmp = `${destPath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
7018
+ const buf = typeof contents === "string" ? Buffer.from(contents, "utf-8") : contents;
7019
+ let fd = null;
7020
+ try {
7021
+ fd = openSync(tmp, TMP_OPEN_FLAGS, mode);
7022
+ writeSync(fd, buf, 0, buf.length, 0);
7023
+ fsyncSync(fd);
7024
+ closeSync(fd);
7025
+ fd = null;
7026
+ renameSync(tmp, destPath);
7027
+ } catch (err) {
7028
+ if (fd !== null) {
7029
+ try {
7030
+ closeSync(fd);
7031
+ } catch {}
7032
+ }
7033
+ try {
7034
+ rmSync(tmp, { force: true });
7035
+ } catch {}
7036
+ throw err;
7037
+ }
7038
+ }
7039
+ function atomicWriteJsonSync(destPath, value, mode = 384) {
7040
+ atomicWriteFileSync(destPath, JSON.stringify(value, null, 2) + `
7041
+ `, mode);
7042
+ }
7043
+ var TMP_OPEN_FLAGS;
7044
+ var init_atomic = __esm(() => {
7045
+ TMP_OPEN_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
7046
+ });
7047
+
7013
7048
  // node_modules/.bun/handlebars@4.7.9/node_modules/handlebars/dist/cjs/handlebars/utils.js
7014
7049
  var require_utils = __commonJS((exports) => {
7015
7050
  exports.__esModule = true;
@@ -12570,41 +12605,6 @@ var require_lib = __commonJS((exports, module) => {
12570
12605
  }
12571
12606
  });
12572
12607
 
12573
- // src/util/atomic.ts
12574
- import { randomBytes } from "node:crypto";
12575
- import { closeSync, constants, fsyncSync, openSync, renameSync, rmSync, writeSync } from "node:fs";
12576
- function atomicWriteFileSync(destPath, contents, mode = 384) {
12577
- const tmp = `${destPath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
12578
- const buf = typeof contents === "string" ? Buffer.from(contents, "utf-8") : contents;
12579
- let fd = null;
12580
- try {
12581
- fd = openSync(tmp, TMP_OPEN_FLAGS, mode);
12582
- writeSync(fd, buf, 0, buf.length, 0);
12583
- fsyncSync(fd);
12584
- closeSync(fd);
12585
- fd = null;
12586
- renameSync(tmp, destPath);
12587
- } catch (err) {
12588
- if (fd !== null) {
12589
- try {
12590
- closeSync(fd);
12591
- } catch {}
12592
- }
12593
- try {
12594
- rmSync(tmp, { force: true });
12595
- } catch {}
12596
- throw err;
12597
- }
12598
- }
12599
- function atomicWriteJsonSync(destPath, value, mode = 384) {
12600
- atomicWriteFileSync(destPath, JSON.stringify(value, null, 2) + `
12601
- `, mode);
12602
- }
12603
- var TMP_OPEN_FLAGS;
12604
- var init_atomic = __esm(() => {
12605
- TMP_OPEN_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
12606
- });
12607
-
12608
12608
  // src/auth/broker/index.ts
12609
12609
  import { existsSync as existsSync9, readFileSync as readFileSync8 } from "node:fs";
12610
12610
 
@@ -16688,7 +16688,12 @@ var ScheduleEntrySchema = exports_external.object({
16688
16688
  var AgentSoulSchema = exports_external.object({
16689
16689
  name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
16690
16690
  style: exports_external.string().describe("Communication style description"),
16691
- boundaries: exports_external.string().optional().describe("Behavioral boundaries and disclaimers")
16691
+ creature: exports_external.string().optional().describe("Persona creature/form (e.g., 'owl', 'octopus')"),
16692
+ vibe: exports_external.string().optional().describe("One-line personality vibe (e.g., 'calm, precise')"),
16693
+ expertise: exports_external.string().optional().describe("Domain expertise summary rendered into the persona"),
16694
+ emoji: exports_external.string().optional().describe("Signature emoji for the persona (e.g., '\uD83E\uDD89')"),
16695
+ boundaries: exports_external.string().optional().describe("Behavioral boundaries and disclaimers"),
16696
+ 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).")
16692
16697
  }).optional();
16693
16698
  var AgentToolsSchema = exports_external.object({
16694
16699
  allow: exports_external.array(exports_external.string()).default([]).describe("Allowed tools (use ['all'] for unrestricted)"),
@@ -16853,6 +16858,11 @@ var TelegramChannelSchema = exports_external.object({
16853
16858
  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."),
16854
16859
  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.")
16855
16860
  }).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."),
16861
+ button_choice_confirmation: exports_external.object({
16862
+ 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."),
16863
+ 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."),
16864
+ 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.")
16865
+ }).optional().describe("Auto-confirm 'You chose: X' annotation on inline_keyboard taps (#789). " + "Cascades from defaults.channels.telegram.button_choice_confirmation."),
16856
16866
  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.)"),
16857
16867
  webhook_dispatch: exports_external.object({
16858
16868
  github: exports_external.array(webhookDispatchRule).optional(),
@@ -17001,7 +17011,12 @@ var profileFields = {
17001
17011
  soul: exports_external.object({
17002
17012
  name: exports_external.string().optional(),
17003
17013
  style: exports_external.string().optional(),
17004
- boundaries: exports_external.string().optional()
17014
+ creature: exports_external.string().optional(),
17015
+ vibe: exports_external.string().optional(),
17016
+ expertise: exports_external.string().optional(),
17017
+ emoji: exports_external.string().optional(),
17018
+ boundaries: exports_external.string().optional(),
17019
+ shape: exports_external.enum(["executive-assistant", "developer", "coach", "generalist"]).optional()
17005
17020
  }).optional(),
17006
17021
  tools: exports_external.object({
17007
17022
  allow: exports_external.array(exports_external.string()).optional(),
@@ -17198,7 +17213,8 @@ var VaultConfigSchema = exports_external.object({
17198
17213
  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."),
17199
17214
  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."),
17200
17215
  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."),
17201
- 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).")
17216
+ 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)."),
17217
+ 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.")
17202
17218
  }).default({}).superRefine((broker, ctx) => {
17203
17219
  if (broker.approvalAuth === "telegram-id" && broker.autoUnlock !== true) {
17204
17220
  ctx.addIssue({
@@ -17237,7 +17253,9 @@ var FleetHealthConfigSchema = exports_external.object({
17237
17253
  });
17238
17254
  var HostdConfigSchema = exports_external.object({
17239
17255
  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."),
17240
- 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.")
17256
+ 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."),
17257
+ 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."),
17258
+ 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.")
17241
17259
  });
17242
17260
  var CronEgressSchema = exports_external.object({
17243
17261
  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."),
@@ -17650,6 +17668,11 @@ function mergeAgentConfig(defaultsIn, agentIn) {
17650
17668
  if (v !== undefined)
17651
17669
  combined[k] = v;
17652
17670
  }
17671
+ const baseSoul = base;
17672
+ const overrideSoul = override;
17673
+ if (baseSoul.shape !== undefined && (overrideSoul.shape === undefined || overrideSoul.shape === "generalist")) {
17674
+ combined.shape = baseSoul.shape;
17675
+ }
17653
17676
  merged.soul = combined;
17654
17677
  }
17655
17678
  if (defaults.memory || merged.memory) {
@@ -18089,7 +18112,7 @@ import {
18089
18112
  rmSync as rmSync5,
18090
18113
  statSync as statSync6,
18091
18114
  unlinkSync,
18092
- writeFileSync as writeFileSync3
18115
+ writeFileSync as writeFileSync2
18093
18116
  } from "node:fs";
18094
18117
  import { closeSync as closeSync2, openSync as openSync2, writeSync as writeSync2 } from "node:fs";
18095
18118
  import * as constants2 from "node:constants";
@@ -18101,6 +18124,7 @@ import { createHash } from "node:crypto";
18101
18124
 
18102
18125
  // src/agents/scaffold.ts
18103
18126
  import { join as join4, resolve as resolve5 } from "node:path";
18127
+ init_atomic();
18104
18128
 
18105
18129
  // src/config/timezone.ts
18106
18130
  var CONTAINER_DEFAULT_UTC_ZONES = new Set([
@@ -18149,18 +18173,22 @@ var tails = new Map;
18149
18173
 
18150
18174
  // src/agents/inject.ts
18151
18175
  var INJECT_COMMANDS = new Map([
18152
- ["/cost", { description: "Show session cost", expectsOutput: true, dialog: true }],
18153
- ["/status", { description: "Show session status", expectsOutput: true, dialog: true }],
18154
- ["/usage", { description: "Show plan quota", expectsOutput: true, dialog: true }],
18155
- ["/hooks", { description: "List configured hooks", expectsOutput: true, dialog: true }],
18156
- ["/memory", { description: "Open memory picker", expectsOutput: true, dialog: true }],
18157
- ["/model", { description: "Open model picker", expectsOutput: true }],
18176
+ ["/cost", { description: "Show session cost", expectsOutput: true, dialog: true, argsAllowed: false }],
18177
+ ["/status", { description: "Show session status", expectsOutput: true, dialog: true, argsAllowed: false }],
18178
+ ["/usage", { description: "Show plan quota", expectsOutput: true, dialog: true, argsAllowed: false }],
18179
+ ["/hooks", { description: "List configured hooks", expectsOutput: true, dialog: true, argsAllowed: false }],
18180
+ ["/memory", { description: "Open memory picker", expectsOutput: true, dialog: true, argsAllowed: false }],
18181
+ ["/help", { description: "Show help / command discovery", expectsOutput: true, dialog: true, argsAllowed: false }],
18182
+ ["/context", { description: "Show context window usage", expectsOutput: true, argsAllowed: false }],
18183
+ ["/release-notes", { description: "Show release-notes version list", expectsOutput: true, dialog: true, argsAllowed: false }],
18184
+ ["/model", { description: "Open model picker", expectsOutput: true, argsAllowed: true }],
18158
18185
  [
18159
18186
  "/clear",
18160
18187
  {
18161
18188
  description: "Clear session screen",
18162
18189
  expectsOutput: false,
18163
- silentNote: "context cleared — fresh slate"
18190
+ silentNote: "context cleared — fresh slate",
18191
+ argsAllowed: false
18164
18192
  }
18165
18193
  ],
18166
18194
  [
@@ -18168,7 +18196,8 @@ var INJECT_COMMANDS = new Map([
18168
18196
  {
18169
18197
  description: "Compact conversation history",
18170
18198
  expectsOutput: false,
18171
- silentNote: "compaction runs silently"
18199
+ silentNote: "compaction runs silently",
18200
+ argsAllowed: false
18172
18201
  }
18173
18202
  ]
18174
18203
  ]);
@@ -18178,6 +18207,15 @@ var INJECT_BLOCKED = new Map([
18178
18207
  ["/logout", { reason: "would terminate the agent's auth session" }],
18179
18208
  ["/exit", { reason: "would kill the agent process" }],
18180
18209
  ["/quit", { reason: "would kill the agent process" }],
18210
+ ["/upgrade", { reason: "mutates the Claude Code installation" }],
18211
+ ["/init", { reason: "generates/overwrites CLAUDE.md and runs a model turn" }],
18212
+ ["/mcp", { reason: "opens an interactive MCP server management dialog" }],
18213
+ ["/permissions", { reason: "opens an interactive permissions editor that mutates tool policy" }],
18214
+ ["/install-github-app", { reason: "runs a network/OAuth install flow" }],
18215
+ ["/add-dir", { reason: "mutates the session's working-directory set" }],
18216
+ ["/terminal-setup", { reason: "mutates terminal keybinding configuration" }],
18217
+ ["/privacy-settings", { reason: "opens an interactive privacy-settings dialog" }],
18218
+ ["/bug", { reason: "submits a bug report over the network" }],
18181
18219
  [
18182
18220
  "/effort",
18183
18221
  {
@@ -18195,6 +18233,7 @@ var HINDSIGHT_CONSUMER_NAME = "hindsight";
18195
18233
  var HINDSIGHT_IMAGE_REPO = "ghcr.io/switchroom/switchroom-hindsight";
18196
18234
  var HINDSIGHT_IMAGE = `${HINDSIGHT_IMAGE_REPO}:latest`;
18197
18235
  var HINDSIGHT_BROKER_SOCK_VOLUME = `auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock`;
18236
+ var HINDSIGHT_CREDS_MIRROR_VOLUME = `consumer-creds-${HINDSIGHT_CONSUMER_NAME}`;
18198
18237
  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)';
18199
18238
  var HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
18200
18239
 
@@ -18352,6 +18391,13 @@ var ApprovalLookupRequestSchema = exports_external.object({
18352
18391
  action: exports_external.string().min(1),
18353
18392
  current_approver_set: exports_external.array(exports_external.string())
18354
18393
  });
18394
+ var ApprovalLookupByRequestRequestSchema = exports_external.object({
18395
+ v: exports_external.literal(1),
18396
+ op: exports_external.literal("approval_lookup_by_request"),
18397
+ agent_unit: exports_external.string().min(1),
18398
+ request_id: exports_external.string().regex(/^[0-9a-f]{32}$/),
18399
+ current_approver_set: exports_external.array(exports_external.string())
18400
+ });
18355
18401
  var ApprovalConsumeRequestSchema = exports_external.object({
18356
18402
  v: exports_external.literal(1),
18357
18403
  op: exports_external.literal("approval_consume"),
@@ -18406,6 +18452,7 @@ var RequestSchema = exports_external.discriminatedUnion("op", [
18406
18452
  RevokeGrantRequestSchema,
18407
18453
  ApprovalRequestRequestSchema,
18408
18454
  ApprovalLookupRequestSchema,
18455
+ ApprovalLookupByRequestRequestSchema,
18409
18456
  ApprovalConsumeRequestSchema,
18410
18457
  ApprovalRevokeRequestSchema,
18411
18458
  ApprovalListRequestSchema,
@@ -18428,7 +18475,8 @@ var ErrorCode = exports_external.enum([
18428
18475
  "DENIED",
18429
18476
  "UNKNOWN_KEY",
18430
18477
  "BAD_REQUEST",
18431
- "INTERNAL"
18478
+ "INTERNAL",
18479
+ "AUDIT_UNAVAILABLE"
18432
18480
  ]);
18433
18481
  var OkEntryResponseSchema = exports_external.object({
18434
18482
  ok: exports_external.literal(true),
@@ -18575,6 +18623,7 @@ var materializedDirs = new Set;
18575
18623
 
18576
18624
  // src/agents/scaffold.ts
18577
18625
  var REPO_ROOT = resolve5(import.meta.dirname, "../..");
18626
+ 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.";
18578
18627
  var SWITCHROOM_OWNED_SETTINGS_KEYS = new Set([
18579
18628
  "permissions",
18580
18629
  "mcpServers",
@@ -18603,9 +18652,6 @@ var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
18603
18652
  var OAUTH_BETA = "oauth-2025-04-20";
18604
18653
  var DEFAULT_USER_AGENT = "claude-cli/1.0.0 (external, cli)";
18605
18654
  var DEFAULT_PROBE_MODEL = "claude-haiku-4-5-20251001";
18606
- function isProbeThin(q) {
18607
- return q.fiveHourUtilPresent === false && q.sevenDayUtilPresent === false;
18608
- }
18609
18655
  function parseFloatHeader(headers, name) {
18610
18656
  const v = headers.get(name);
18611
18657
  if (v == null || v.trim().length === 0)
@@ -18702,6 +18748,9 @@ function snapshotFresh(s, now, maxAgeMs = SNAPSHOT_STALE_AGE_MS) {
18702
18748
  function snapshotWalled(s) {
18703
18749
  return s.fiveHourUtilizationPct >= WALL_PCT || s.sevenDayUtilizationPct >= WALL_PCT;
18704
18750
  }
18751
+ function snapshotComplete(s) {
18752
+ return s.fiveHourUtilPresent !== false && s.sevenDayUtilPresent !== false;
18753
+ }
18705
18754
  function overageLiftsWall(snapshot, inAllowList) {
18706
18755
  if (!inAllowList)
18707
18756
  return false;
@@ -18717,16 +18766,19 @@ function snapshotClearlyHealthy(s) {
18717
18766
  }
18718
18767
  function accountEligibility(opts) {
18719
18768
  const { mark, snapshot, now, allowOverage = false } = opts;
18769
+ if (snapshotFresh(snapshot, now) && snapshotWalled(snapshot) && !overageLiftsWall(snapshot, allowOverage)) {
18770
+ return "blocked";
18771
+ }
18720
18772
  if (snapshotFresh(snapshot, now)) {
18721
18773
  const markedAt = mark?.marked_at ?? 0;
18722
18774
  if (snapshot.capturedAt >= markedAt) {
18723
18775
  if (snapshotWalled(snapshot)) {
18724
- if (overageLiftsWall(snapshot, allowOverage)) {
18776
+ if (overageLiftsWall(snapshot, allowOverage))
18725
18777
  return "eligible";
18726
- }
18727
18778
  return "blocked";
18728
18779
  }
18729
- return "eligible";
18780
+ if (snapshotComplete(snapshot))
18781
+ return "eligible";
18730
18782
  }
18731
18783
  }
18732
18784
  if (mark !== undefined && mark.exhausted_until > now)
@@ -18743,7 +18795,7 @@ function snapshotShouldClearMark(snapshot, mark, now) {
18743
18795
  return false;
18744
18796
  if (snapshot.capturedAt < (mark.marked_at ?? 0))
18745
18797
  return false;
18746
- if (isProbeThin(snapshot))
18798
+ if (!snapshotComplete(snapshot))
18747
18799
  return false;
18748
18800
  return snapshotClearlyHealthy(snapshot);
18749
18801
  }
@@ -18752,7 +18804,7 @@ function clampMarkExpiry(opts) {
18752
18804
  const shortCeil = now + shortMs;
18753
18805
  if (proposedUntil <= shortCeil)
18754
18806
  return proposedUntil;
18755
- const liveContradictsWeeklyWall = snapshotFresh(snapshot, now) && snapshot.sevenDayUtilizationPct < WALL_PCT;
18807
+ const liveContradictsWeeklyWall = snapshotFresh(snapshot, now) && snapshot.sevenDayUtilPresent !== false && snapshot.sevenDayUtilizationPct < WALL_PCT;
18756
18808
  return liveContradictsWeeklyWall ? shortCeil : proposedUntil;
18757
18809
  }
18758
18810
  var SOFT_AVOID_FIVE_HOUR_CAP = 98;
@@ -18845,6 +18897,7 @@ function resolveConsumerProbeIntervalMs(env) {
18845
18897
  init_atomic();
18846
18898
 
18847
18899
  // src/auth/account-store.ts
18900
+ init_atomic();
18848
18901
  import {
18849
18902
  chownSync,
18850
18903
  existsSync as existsSync5,
@@ -18853,10 +18906,8 @@ import {
18853
18906
  readdirSync as readdirSync3,
18854
18907
  renameSync as renameSync2,
18855
18908
  rmSync as rmSync2,
18856
- statSync as statSync3,
18857
- writeFileSync as writeFileSync2
18909
+ statSync as statSync3
18858
18910
  } from "node:fs";
18859
- import { randomBytes as randomBytes2 } from "node:crypto";
18860
18911
  import { homedir as homedir4 } from "node:os";
18861
18912
  import { join as join5, resolve as resolve6 } from "node:path";
18862
18913
  var LABEL_MAX = 64;
@@ -18940,8 +18991,8 @@ function enrichClaudeCreds(value) {
18940
18991
  }
18941
18992
  function writeAccountCredentials(label, value, home2 = homedir4()) {
18942
18993
  validateAccountLabel(label);
18943
- mkdirSync2(accountDir(label, home2), { recursive: true });
18944
- atomicWriteJson(accountCredentialsPath(label, home2), enrichClaudeCreds(value));
18994
+ mkdirSync2(accountDir(label, home2), { recursive: true, mode: 448 });
18995
+ atomicWriteJsonSync(accountCredentialsPath(label, home2), enrichClaudeCreds(value));
18945
18996
  }
18946
18997
  function chownAccountFiles(label, uid, home2 = homedir4()) {
18947
18998
  validateAccountLabel(label);
@@ -18967,8 +19018,8 @@ function readAccountMeta(label, home2 = homedir4()) {
18967
19018
  }
18968
19019
  function writeAccountMeta(label, value, home2 = homedir4()) {
18969
19020
  validateAccountLabel(label);
18970
- mkdirSync2(accountDir(label, home2), { recursive: true });
18971
- atomicWriteJson(accountMetaPath(label, home2), value);
19021
+ mkdirSync2(accountDir(label, home2), { recursive: true, mode: 448 });
19022
+ atomicWriteJsonSync(accountMetaPath(label, home2), value);
18972
19023
  }
18973
19024
  function patchAccountMeta(label, patch, home2 = homedir4()) {
18974
19025
  const existing = readAccountMeta(label, home2) ?? { createdAt: Date.now() };
@@ -18976,19 +19027,6 @@ function patchAccountMeta(label, patch, home2 = homedir4()) {
18976
19027
  writeAccountMeta(label, merged, home2);
18977
19028
  return merged;
18978
19029
  }
18979
- function atomicWriteJson(destPath, value, mode = 384) {
18980
- const tmp = `${destPath}.tmp-${process.pid}-${randomBytes2(4).toString("hex")}`;
18981
- try {
18982
- writeFileSync2(tmp, JSON.stringify(value, null, 2) + `
18983
- `, { mode });
18984
- renameSync2(tmp, destPath);
18985
- } catch (err) {
18986
- try {
18987
- rmSync2(tmp, { force: true });
18988
- } catch {}
18989
- throw err;
18990
- }
18991
- }
18992
19030
 
18993
19031
  // src/auth/account-refresh.ts
18994
19032
  var REFRESH_THRESHOLD_MS = 60 * 60 * 1000;
@@ -20155,7 +20193,7 @@ class AuthBroker {
20155
20193
  if (!this.opts.skipHealthyMarker) {
20156
20194
  try {
20157
20195
  const healthyPath = join8(this.stateDir, "healthy");
20158
- writeFileSync3(healthyPath, String(this.now()) + `
20196
+ writeFileSync2(healthyPath, String(this.now()) + `
20159
20197
  `, { mode: 384 });
20160
20198
  } catch (err) {
20161
20199
  this.logErr(`failed to write healthy marker: ${err.message}`);
@@ -0,0 +1,39 @@
1
+ // src/cli/hindsight-mental-model-pretool.ts
2
+ import { readFileSync } from "node:fs";
3
+ var MENTAL_MODEL_WRITE_TOOLS = new Set([
4
+ "mcp__hindsight__create_mental_model",
5
+ "mcp__hindsight__update_mental_model",
6
+ "mcp__hindsight__delete_mental_model",
7
+ "mcp__hindsight__refresh_mental_model"
8
+ ]);
9
+ var DENY_MESSAGE = "Mental-model writes are operator-approved. Use " + "`mcp__switchroom-telegram__mental_model_propose` instead \u2014 it posts an " + "approval card and persists on approval.";
10
+ function readStdin() {
11
+ try {
12
+ return readFileSync(0, "utf8");
13
+ } catch {
14
+ return "";
15
+ }
16
+ }
17
+ function allow() {
18
+ process.exit(0);
19
+ }
20
+ function block(reason) {
21
+ process.stdout.write(JSON.stringify({ decision: "block", reason }));
22
+ process.exit(0);
23
+ }
24
+ function main() {
25
+ const raw = readStdin().trim();
26
+ if (!raw)
27
+ allow();
28
+ let event;
29
+ try {
30
+ event = JSON.parse(raw);
31
+ } catch {
32
+ allow();
33
+ }
34
+ const toolName = typeof event.tool_name === "string" ? event.tool_name : "";
35
+ if (MENTAL_MODEL_WRITE_TOOLS.has(toolName))
36
+ block(DENY_MESSAGE);
37
+ allow();
38
+ }
39
+ main();