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