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.
- package/dist/agent-scheduler/index.js +37 -5
- package/dist/auth-broker/index.js +116 -78
- package/dist/cli/hindsight-mental-model-pretool.mjs +39 -0
- package/dist/cli/ms-365-write-pretool.mjs +31 -8
- package/dist/cli/notion-write-pretool.mjs +38 -6
- package/dist/cli/skill-validate-pretool.mjs +144 -2847
- package/dist/cli/switchroom.js +3529 -4543
- package/dist/cli/ui/index.html +163 -17
- package/dist/host-control/main.js +605 -2847
- package/dist/vault/approvals/kernel-server.js +120 -13
- package/dist/vault/broker/server.js +259 -157
- package/package.json +3 -4
- package/profiles/_base/start.sh.hbs +65 -0
- package/profiles/_shared/vault-protocol.md.hbs +3 -1
- package/profiles/coding/CLAUDE.md.hbs +1 -1
- package/profiles/default/CLAUDE.md.hbs +2 -2
- package/profiles/executive-assistant/CLAUDE.md.hbs +1 -1
- package/profiles/health-coach/CLAUDE.md.hbs +1 -1
- package/telegram-plugin/bridge/bridge.ts +37 -0
- package/telegram-plugin/bridge/inbound-dedup.ts +101 -0
- package/telegram-plugin/dist/bridge/bridge.js +122 -4
- package/telegram-plugin/dist/gateway/gateway.js +4213 -3288
- package/telegram-plugin/dist/server.js +139 -5
- package/telegram-plugin/flood-circuit-breaker.ts +493 -21
- package/telegram-plugin/format.ts +19 -0
- package/telegram-plugin/gateway/approval-hold.ts +602 -0
- package/telegram-plugin/gateway/auth-command.ts +92 -2
- package/telegram-plugin/gateway/auth-loopback-relay.ts +670 -0
- package/telegram-plugin/gateway/boot-card.ts +12 -5
- package/telegram-plugin/gateway/callback-query-handlers.ts +88 -1
- package/telegram-plugin/gateway/config-approval-handler.ts +6 -1
- package/telegram-plugin/gateway/disconnect-flush.ts +19 -0
- package/telegram-plugin/gateway/dm-pin-sweep.test.ts +251 -0
- package/telegram-plugin/gateway/dm-pin-sweep.ts +178 -0
- package/telegram-plugin/gateway/gateway.ts +1695 -230
- package/telegram-plugin/gateway/hostd-dispatch.ts +23 -0
- package/telegram-plugin/gateway/idle-clear.ts +90 -6
- package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +26 -5
- package/telegram-plugin/gateway/inject-handler.ts +8 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +46 -3
- package/telegram-plugin/gateway/ipc-server.ts +43 -0
- package/telegram-plugin/gateway/mental-model-propose-resolve.ts +145 -37
- package/telegram-plugin/gateway/model-command.ts +9 -3
- package/telegram-plugin/gateway/pending-session-command.ts +13 -1
- package/telegram-plugin/gateway/permission-ttl-sweep.ts +66 -0
- package/telegram-plugin/gateway/pre-approval-check.ts +74 -0
- package/telegram-plugin/gateway/queued-card-store.ts +217 -0
- package/telegram-plugin/gateway/session-model-file.ts +26 -1
- package/telegram-plugin/gateway/turn-end-gate-backstop.ts +59 -0
- package/telegram-plugin/gateway/turn-end-gate.ts +95 -0
- package/telegram-plugin/gateway/turn-typing-loop.ts +10 -2
- package/telegram-plugin/gateway/unhandled-rejection-policy.ts +13 -0
- package/telegram-plugin/history.ts +51 -0
- package/telegram-plugin/hooks/dispatch-claim-scan.mjs +259 -0
- package/telegram-plugin/hooks/dispatch-claim-stop.mjs +129 -0
- package/telegram-plugin/hooks/hooks.json +9 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +303 -2
- package/telegram-plugin/model-unavailable.ts +41 -11
- package/telegram-plugin/operator-events.ts +23 -0
- package/telegram-plugin/outbound-field-redact.ts +69 -0
- package/telegram-plugin/package.json +0 -1
- package/telegram-plugin/permission-rule.ts +1 -0
- package/telegram-plugin/permission-title.ts +1 -0
- package/telegram-plugin/render/render.ts +32 -14
- package/telegram-plugin/retry-api-call.ts +212 -2
- package/telegram-plugin/scoped-approval.ts +11 -2
- package/telegram-plugin/secret-detect/chunker.ts +18 -4
- package/telegram-plugin/secret-detect/index.ts +12 -56
- package/telegram-plugin/send-gate-degraded.test.ts +574 -0
- package/telegram-plugin/send-gate-observability.test.ts +470 -0
- package/telegram-plugin/send-gate-observability.ts +355 -0
- package/telegram-plugin/send-gate.test.ts +717 -0
- package/telegram-plugin/send-gate.ts +1056 -0
- package/telegram-plugin/session-tail.ts +82 -7
- package/telegram-plugin/shared/bot-runtime.ts +17 -5
- package/telegram-plugin/shared/gw-trace-gate.ts +105 -0
- package/telegram-plugin/status-pin-driver.ts +52 -7
- package/telegram-plugin/status-pin.ts +81 -0
- package/telegram-plugin/subagent-watcher.ts +173 -18
- package/telegram-plugin/tests/activity-card-wiring.test.ts +18 -5
- package/telegram-plugin/tests/approval-hold-harness.ts +425 -0
- package/telegram-plugin/tests/approval-hold-outcome.test.ts +327 -0
- package/telegram-plugin/tests/approval-hold-record.test.ts +531 -0
- package/telegram-plugin/tests/approval-hold-redeliver.test.ts +602 -0
- package/telegram-plugin/tests/auth-loopback-relay.test.ts +533 -0
- package/telegram-plugin/tests/boot-card-flood-suppress.test.ts +53 -7
- package/telegram-plugin/tests/busy-key-reaper.test.ts +1 -0
- package/telegram-plugin/tests/callback-query-handlers.test.ts +65 -0
- package/telegram-plugin/tests/dispatch-claim-scan.test.ts +250 -0
- package/telegram-plugin/tests/flood-breaker-blindness.test.ts +213 -0
- package/telegram-plugin/tests/flood-windows-persistence.test.ts +224 -0
- package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +29 -1
- package/telegram-plugin/tests/gateway-loopback-paste-redact.test.ts +66 -0
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +57 -0
- package/telegram-plugin/tests/gw-trace-gate.test.ts +105 -0
- package/telegram-plugin/tests/history.test.ts +115 -0
- package/telegram-plugin/tests/idle-clear.test.ts +233 -3
- package/telegram-plugin/tests/inbound-dedup.test.ts +93 -0
- package/telegram-plugin/tests/inbound-message-types.test.ts +5 -1
- package/telegram-plugin/tests/inline-keyboard-callbacks.test.ts +448 -0
- package/telegram-plugin/tests/ipc-server-check-pre-approved.test.ts +194 -0
- package/telegram-plugin/tests/mental-model-propose-resolve.test.ts +123 -0
- package/telegram-plugin/tests/missed-approvals-wiring.test.ts +1 -1
- package/telegram-plugin/tests/model-command.test.ts +14 -0
- package/telegram-plugin/tests/operator-events-session-tail.test.ts +74 -0
- package/telegram-plugin/tests/outbound-field-redact.test.ts +107 -0
- package/telegram-plugin/tests/pending-session-command.test.ts +21 -0
- package/telegram-plugin/tests/permission-card-routing.test.ts +30 -5
- package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +8 -7
- package/telegram-plugin/tests/permission-rearm-wiring.test.ts +1 -1
- package/telegram-plugin/tests/pre-approval-check.test.ts +148 -0
- package/telegram-plugin/tests/queued-card-store.test.ts +232 -0
- package/telegram-plugin/tests/reaction-flush-turn-gated.test.ts +100 -0
- package/telegram-plugin/tests/reaction-gate-routing.test.ts +173 -0
- package/telegram-plugin/tests/render/render.test.ts +88 -0
- package/telegram-plugin/tests/retry-api-call.test.ts +398 -0
- package/telegram-plugin/tests/scoped-approval.test.ts +27 -0
- package/telegram-plugin/tests/secret-detect-chunk-overlap.test.ts +65 -0
- package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +5 -4
- package/telegram-plugin/tests/session-model-file.test.ts +50 -0
- package/telegram-plugin/tests/session-tail-sidecar-reap.test.ts +268 -0
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +35 -14
- package/telegram-plugin/tests/status-pin.test.ts +275 -1
- package/telegram-plugin/tests/subagent-watcher-deferral-log-ratelimit.test.ts +316 -0
- package/telegram-plugin/tests/subagent-watcher-fd-leak.test.ts +275 -0
- package/telegram-plugin/tests/turn-end-gate-backstop.test.ts +92 -0
- package/telegram-plugin/tests/turn-end-gate.test.ts +137 -0
- package/telegram-plugin/tests/typing-emitter.test.ts +586 -0
- package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +20 -0
- package/telegram-plugin/tests/worktree-watch-cwds.test.ts +215 -1
- package/telegram-plugin/typing-emitter.ts +224 -0
- package/telegram-plugin/uat/scenarios/jtbd-feel-like-a-colleague-dm.test.ts +136 -0
- package/telegram-plugin/welcome-text.ts +42 -0
- package/telegram-plugin/worktree-watch-cwds.ts +194 -5
- package/vendor/hindsight-memory/scripts/drain_pending.py +22 -6
- package/vendor/hindsight-memory/scripts/lib/client.py +12 -5
- package/vendor/hindsight-memory/scripts/lib/directives.py +38 -3
- package/vendor/hindsight-memory/scripts/lib/pending.py +36 -9
- package/vendor/hindsight-memory/scripts/session_end.py +14 -3
- package/vendor/hindsight-memory/scripts/session_start.py +21 -0
- package/vendor/hindsight-memory/scripts/tests/test_directives.py +38 -0
- package/vendor/hindsight-memory/tests/test_drain_pending.py +68 -0
- package/vendor/hindsight-memory/tests/test_pending.py +44 -0
- package/vendor/hindsight-memory/tests/test_session_end_pending.py +38 -0
- package/vendor/hindsight-memory/tests/test_session_start_drain.py +155 -0
- package/telegram-plugin/channel-envelope-safety.test.ts +0 -56
- package/telegram-plugin/channel-envelope-safety.ts +0 -56
- package/telegram-plugin/secret-detect/secretlint-source.ts +0 -95
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
["/
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|