switchroom 0.18.11 → 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 +2453 -1293
- package/dist/cli/ui/index.html +163 -17
- package/dist/host-control/main.js +504 -100
- package/dist/vault/approvals/kernel-server.js +53 -13
- package/dist/vault/broker/server.js +162 -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 +3602 -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";
|
|
@@ -18154,22 +18177,6 @@ function isWebkiteCredentialKeyForAgent(agentConfig, key) {
|
|
|
18154
18177
|
return false;
|
|
18155
18178
|
return true;
|
|
18156
18179
|
}
|
|
18157
|
-
function parseCronUnit(unitName) {
|
|
18158
|
-
const m = unitName.match(/^switchroom-([a-zA-Z0-9_-]+)-cron-(\d+)\.service$/);
|
|
18159
|
-
if (!m)
|
|
18160
|
-
return null;
|
|
18161
|
-
const agentName = m[1];
|
|
18162
|
-
const index = parseInt(m[2], 10);
|
|
18163
|
-
if (!agentName)
|
|
18164
|
-
return null;
|
|
18165
|
-
return { agentName, index };
|
|
18166
|
-
}
|
|
18167
|
-
function agentSlugFromPeer(peer) {
|
|
18168
|
-
if (peer.systemdUnit === null)
|
|
18169
|
-
return null;
|
|
18170
|
-
const parsed = parseCronUnit(peer.systemdUnit);
|
|
18171
|
-
return parsed?.agentName ?? null;
|
|
18172
|
-
}
|
|
18173
18180
|
function checkEntryScope(scope, agentSlug) {
|
|
18174
18181
|
if (scope === undefined || scope === null) {
|
|
18175
18182
|
return { allow: true };
|
|
@@ -18192,42 +18199,6 @@ function checkEntryScope(scope, agentSlug) {
|
|
|
18192
18199
|
}
|
|
18193
18200
|
return { allow: true };
|
|
18194
18201
|
}
|
|
18195
|
-
function checkAcl(peer, config, key) {
|
|
18196
|
-
if (peer.systemdUnit !== null) {
|
|
18197
|
-
const parsed = parseCronUnit(peer.systemdUnit);
|
|
18198
|
-
if (parsed === null) {
|
|
18199
|
-
return {
|
|
18200
|
-
allow: false,
|
|
18201
|
-
reason: `systemd unit '${peer.systemdUnit}' does not match switchroom cron unit naming convention`
|
|
18202
|
-
};
|
|
18203
|
-
}
|
|
18204
|
-
const { agentName, index } = parsed;
|
|
18205
|
-
const agentConfig = config.agents?.[agentName];
|
|
18206
|
-
if (!agentConfig) {
|
|
18207
|
-
return { allow: false, reason: `agent '${agentName}' not found in config` };
|
|
18208
|
-
}
|
|
18209
|
-
const schedule = agentConfig.schedule ?? [];
|
|
18210
|
-
if (index >= schedule.length || index < 0) {
|
|
18211
|
-
return {
|
|
18212
|
-
allow: false,
|
|
18213
|
-
reason: `schedule index ${index} out of range for agent '${agentName}' (${schedule.length} entries)`
|
|
18214
|
-
};
|
|
18215
|
-
}
|
|
18216
|
-
const entry = schedule[index];
|
|
18217
|
-
const allowedKeys = entry.secrets ?? [];
|
|
18218
|
-
if (!allowedKeys.includes(key)) {
|
|
18219
|
-
return {
|
|
18220
|
-
allow: false,
|
|
18221
|
-
reason: `key '${key}' not in ACL for ${agentName}/schedule[${index}]`
|
|
18222
|
-
};
|
|
18223
|
-
}
|
|
18224
|
-
return { allow: true };
|
|
18225
|
-
}
|
|
18226
|
-
return {
|
|
18227
|
-
allow: false,
|
|
18228
|
-
reason: "caller is not a switchroom cron unit; use 'switchroom vault get --no-broker' for interactive access"
|
|
18229
|
-
};
|
|
18230
|
-
}
|
|
18231
18202
|
function checkAclByAgent(config, agentName, key) {
|
|
18232
18203
|
if (!agentName) {
|
|
18233
18204
|
return { allow: false, reason: "agent name unresolved" };
|
|
@@ -18361,18 +18332,22 @@ var tails = new Map;
|
|
|
18361
18332
|
|
|
18362
18333
|
// src/agents/inject.ts
|
|
18363
18334
|
var INJECT_COMMANDS = new Map([
|
|
18364
|
-
["/cost", { description: "Show session cost", expectsOutput: true, dialog: true }],
|
|
18365
|
-
["/status", { description: "Show session status", expectsOutput: true, dialog: true }],
|
|
18366
|
-
["/usage", { description: "Show plan quota", expectsOutput: true, dialog: true }],
|
|
18367
|
-
["/hooks", { description: "List configured hooks", expectsOutput: true, dialog: true }],
|
|
18368
|
-
["/memory", { description: "Open memory picker", expectsOutput: true, dialog: true }],
|
|
18369
|
-
["/
|
|
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 }],
|
|
18370
18344
|
[
|
|
18371
18345
|
"/clear",
|
|
18372
18346
|
{
|
|
18373
18347
|
description: "Clear session screen",
|
|
18374
18348
|
expectsOutput: false,
|
|
18375
|
-
silentNote: "context cleared — fresh slate"
|
|
18349
|
+
silentNote: "context cleared — fresh slate",
|
|
18350
|
+
argsAllowed: false
|
|
18376
18351
|
}
|
|
18377
18352
|
],
|
|
18378
18353
|
[
|
|
@@ -18380,7 +18355,8 @@ var INJECT_COMMANDS = new Map([
|
|
|
18380
18355
|
{
|
|
18381
18356
|
description: "Compact conversation history",
|
|
18382
18357
|
expectsOutput: false,
|
|
18383
|
-
silentNote: "compaction runs silently"
|
|
18358
|
+
silentNote: "compaction runs silently",
|
|
18359
|
+
argsAllowed: false
|
|
18384
18360
|
}
|
|
18385
18361
|
]
|
|
18386
18362
|
]);
|
|
@@ -18390,6 +18366,15 @@ var INJECT_BLOCKED = new Map([
|
|
|
18390
18366
|
["/logout", { reason: "would terminate the agent's auth session" }],
|
|
18391
18367
|
["/exit", { reason: "would kill the agent process" }],
|
|
18392
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" }],
|
|
18393
18378
|
[
|
|
18394
18379
|
"/effort",
|
|
18395
18380
|
{
|
|
@@ -18407,6 +18392,7 @@ var HINDSIGHT_CONSUMER_NAME = "hindsight";
|
|
|
18407
18392
|
var HINDSIGHT_IMAGE_REPO = "ghcr.io/switchroom/switchroom-hindsight";
|
|
18408
18393
|
var HINDSIGHT_IMAGE = `${HINDSIGHT_IMAGE_REPO}:latest`;
|
|
18409
18394
|
var HINDSIGHT_BROKER_SOCK_VOLUME = `auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock`;
|
|
18395
|
+
var HINDSIGHT_CREDS_MIRROR_VOLUME = `consumer-creds-${HINDSIGHT_CONSUMER_NAME}`;
|
|
18410
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)';
|
|
18411
18397
|
var HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
|
|
18412
18398
|
|
|
@@ -19354,7 +19340,8 @@ var ErrorCode = exports_external.enum([
|
|
|
19354
19340
|
"DENIED",
|
|
19355
19341
|
"UNKNOWN_KEY",
|
|
19356
19342
|
"BAD_REQUEST",
|
|
19357
|
-
"INTERNAL"
|
|
19343
|
+
"INTERNAL",
|
|
19344
|
+
"AUDIT_UNAVAILABLE"
|
|
19358
19345
|
]);
|
|
19359
19346
|
var OkEntryResponseSchema = exports_external.object({
|
|
19360
19347
|
ok: exports_external.literal(true),
|
|
@@ -19542,6 +19529,7 @@ init_paths();
|
|
|
19542
19529
|
|
|
19543
19530
|
// src/agents/scaffold.ts
|
|
19544
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.";
|
|
19545
19533
|
var SWITCHROOM_OWNED_SETTINGS_KEYS = new Set([
|
|
19546
19534
|
"permissions",
|
|
19547
19535
|
"mcpServers",
|
|
@@ -19929,6 +19917,23 @@ function safeAuditLogPath(requestedPath) {
|
|
|
19929
19917
|
}
|
|
19930
19918
|
|
|
19931
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
|
+
}
|
|
19932
19937
|
var DEFAULT_AUDIT_MAX_BYTES = 32 * 1024 * 1024;
|
|
19933
19938
|
var DEFAULT_AUDIT_MAX_FILES = 5;
|
|
19934
19939
|
function resolveRotation(opts) {
|
|
@@ -20009,7 +20014,27 @@ function createAuditLogger(opts = {}) {
|
|
|
20009
20014
|
const logPath = safeAuditLogPath(opts.path ?? defaultAuditLogPath());
|
|
20010
20015
|
let chain = seedChain(logPath);
|
|
20011
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
|
+
};
|
|
20012
20034
|
return {
|
|
20035
|
+
failOpenCount() {
|
|
20036
|
+
return failOpen;
|
|
20037
|
+
},
|
|
20013
20038
|
write(entry) {
|
|
20014
20039
|
if (rotation.maxBytes > 0) {
|
|
20015
20040
|
try {
|
|
@@ -20026,14 +20051,18 @@ function createAuditLogger(opts = {}) {
|
|
|
20026
20051
|
} catch (err) {
|
|
20027
20052
|
process.stderr.write(`[vault-audit] ERROR: could not open audit log ${logPath}: ${err.message}
|
|
20028
20053
|
`);
|
|
20029
|
-
|
|
20054
|
+
recordFailOpen(err);
|
|
20055
|
+
return false;
|
|
20030
20056
|
}
|
|
20057
|
+
let durable = false;
|
|
20031
20058
|
try {
|
|
20032
20059
|
fs.writeSync(fd, line);
|
|
20033
20060
|
chain = next;
|
|
20061
|
+
durable = true;
|
|
20034
20062
|
} catch (err) {
|
|
20035
20063
|
process.stderr.write(`[vault-audit] ERROR: could not write to audit log ${logPath}: ${err.message}
|
|
20036
20064
|
`);
|
|
20065
|
+
recordFailOpen(err);
|
|
20037
20066
|
} finally {
|
|
20038
20067
|
try {
|
|
20039
20068
|
fs.closeSync(fd);
|
|
@@ -20042,6 +20071,7 @@ function createAuditLogger(opts = {}) {
|
|
|
20042
20071
|
`);
|
|
20043
20072
|
}
|
|
20044
20073
|
}
|
|
20074
|
+
return durable;
|
|
20045
20075
|
}
|
|
20046
20076
|
};
|
|
20047
20077
|
}
|
|
@@ -22375,6 +22405,21 @@ class VaultBroker {
|
|
|
22375
22405
|
this.grantsDb = openGrantsDb();
|
|
22376
22406
|
}
|
|
22377
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
|
+
}
|
|
22378
22423
|
async start(socketPath, configPath, vaultPath) {
|
|
22379
22424
|
if (process.platform !== "linux" && process.env.SWITCHROOM_BROKER_ALLOW_NON_LINUX !== "1") {
|
|
22380
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.`);
|
|
@@ -22442,7 +22487,7 @@ class VaultBroker {
|
|
|
22442
22487
|
return;
|
|
22443
22488
|
try {
|
|
22444
22489
|
if (ready) {
|
|
22445
|
-
|
|
22490
|
+
writeFileSync3(p, "", { mode: 384 });
|
|
22446
22491
|
} else if (existsSync11(p)) {
|
|
22447
22492
|
unlinkSync6(p);
|
|
22448
22493
|
}
|
|
@@ -22741,7 +22786,7 @@ class VaultBroker {
|
|
|
22741
22786
|
const auditPeerUid = peer?.uid;
|
|
22742
22787
|
const auditAgentName = agentName ?? undefined;
|
|
22743
22788
|
const writeAudit = (entry) => {
|
|
22744
|
-
this.auditLogger.write({
|
|
22789
|
+
return this.auditLogger.write({
|
|
22745
22790
|
...entry,
|
|
22746
22791
|
peer_uid: entry.peer_uid ?? auditPeerUid,
|
|
22747
22792
|
agent_name: entry.agent_name ?? auditAgentName
|
|
@@ -22850,8 +22895,8 @@ class VaultBroker {
|
|
|
22850
22895
|
return;
|
|
22851
22896
|
}
|
|
22852
22897
|
}
|
|
22853
|
-
if (!isOperator && agentName === null && process.platform === "linux"
|
|
22854
|
-
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";
|
|
22855
22900
|
this.auditLogger.write({
|
|
22856
22901
|
ts: new Date().toISOString(),
|
|
22857
22902
|
op: "list",
|
|
@@ -22863,14 +22908,12 @@ class VaultBroker {
|
|
|
22863
22908
|
socket.write(encodeResponse(errorResponse("DENIED", reason)));
|
|
22864
22909
|
return;
|
|
22865
22910
|
}
|
|
22866
|
-
const listAgentSlug = agentName
|
|
22911
|
+
const listAgentSlug = agentName;
|
|
22867
22912
|
let visibleKeys;
|
|
22868
22913
|
if (isOperator) {
|
|
22869
22914
|
visibleKeys = Object.entries(this.secrets).filter(([, entry]) => checkEntryScope(entry.scope, "operator").allow).map(([k]) => k);
|
|
22870
22915
|
} else if (agentName !== null && this.config !== null) {
|
|
22871
22916
|
visibleKeys = Object.entries(this.secrets).filter(([key, entry]) => checkAclByAgent(this.config, agentName, key).allow && checkEntryScope(entry.scope, agentName).allow).map(([k]) => k);
|
|
22872
|
-
} else if (peer !== null && this.config !== null) {
|
|
22873
|
-
visibleKeys = Object.entries(this.secrets).filter(([key, entry]) => checkAcl(peer, this.config, key).allow && checkEntryScope(entry.scope, listAgentSlug).allow).map(([k]) => k);
|
|
22874
22917
|
} else {
|
|
22875
22918
|
visibleKeys = Object.entries(this.secrets).filter(([, entry]) => checkEntryScope(entry.scope, listAgentSlug).allow).map(([k]) => k);
|
|
22876
22919
|
}
|
|
@@ -22894,6 +22937,21 @@ class VaultBroker {
|
|
|
22894
22937
|
const grantResult = await validateGrant(this.grantsDb, req.token, req.key);
|
|
22895
22938
|
if (grantResult.ok) {
|
|
22896
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
|
+
}
|
|
22897
22955
|
const entry2 = this.secrets[req.key];
|
|
22898
22956
|
if (entry2 === undefined) {
|
|
22899
22957
|
this.auditLogger.write({
|
|
@@ -22910,7 +22968,23 @@ class VaultBroker {
|
|
|
22910
22968
|
socket.write(encodeResponse(errorResponse("UNKNOWN_KEY", `Key not found: ${req.key}`)));
|
|
22911
22969
|
return;
|
|
22912
22970
|
}
|
|
22913
|
-
|
|
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, {
|
|
22914
22988
|
ts: new Date().toISOString(),
|
|
22915
22989
|
op: "get",
|
|
22916
22990
|
key: req.key,
|
|
@@ -22920,7 +22994,9 @@ class VaultBroker {
|
|
|
22920
22994
|
result: "allowed",
|
|
22921
22995
|
method: "grant",
|
|
22922
22996
|
grant_id: grantId
|
|
22923
|
-
})
|
|
22997
|
+
})) {
|
|
22998
|
+
return;
|
|
22999
|
+
}
|
|
22924
23000
|
socket.write(encodeResponse(entryResponse(entry2)));
|
|
22925
23001
|
return;
|
|
22926
23002
|
} else if (grantResult.reason === "grant-revoked") {
|
|
@@ -22974,23 +23050,8 @@ class VaultBroker {
|
|
|
22974
23050
|
socket.write(encodeResponse(errorResponse("DENIED", aclResult.reason)));
|
|
22975
23051
|
return;
|
|
22976
23052
|
}
|
|
22977
|
-
} else if (
|
|
22978
|
-
const
|
|
22979
|
-
if (!aclResult.allow) {
|
|
22980
|
-
writeAudit({
|
|
22981
|
-
ts: new Date().toISOString(),
|
|
22982
|
-
op: "get",
|
|
22983
|
-
key: req.key,
|
|
22984
|
-
caller: auditCaller,
|
|
22985
|
-
pid: auditPid,
|
|
22986
|
-
cgroup: auditCgroup,
|
|
22987
|
-
result: `denied:${aclResult.reason}`
|
|
22988
|
-
});
|
|
22989
|
-
socket.write(encodeResponse(errorResponse("DENIED", aclResult.reason)));
|
|
22990
|
-
return;
|
|
22991
|
-
}
|
|
22992
|
-
} else if (process.platform === "linux" && peer === null) {
|
|
22993
|
-
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";
|
|
22994
23055
|
this.auditLogger.write({
|
|
22995
23056
|
ts: new Date().toISOString(),
|
|
22996
23057
|
op: "get",
|
|
@@ -23017,7 +23078,7 @@ class VaultBroker {
|
|
|
23017
23078
|
socket.write(encodeResponse(errorResponse("UNKNOWN_KEY", `Key not found: ${req.key}`)));
|
|
23018
23079
|
return;
|
|
23019
23080
|
}
|
|
23020
|
-
const getAgentSlug = agentName
|
|
23081
|
+
const getAgentSlug = agentName;
|
|
23021
23082
|
const scopeResult = checkEntryScope(entry.scope, getAgentSlug);
|
|
23022
23083
|
if (!scopeResult.allow) {
|
|
23023
23084
|
this.auditLogger.write({
|
|
@@ -23032,7 +23093,7 @@ class VaultBroker {
|
|
|
23032
23093
|
socket.write(encodeResponse(errorResponse("DENIED", scopeResult.reason)));
|
|
23033
23094
|
return;
|
|
23034
23095
|
}
|
|
23035
|
-
this.
|
|
23096
|
+
if (!this.auditedReleaseOk(socket, writeAudit, {
|
|
23036
23097
|
ts: new Date().toISOString(),
|
|
23037
23098
|
op: "get",
|
|
23038
23099
|
key: req.key,
|
|
@@ -23040,7 +23101,9 @@ class VaultBroker {
|
|
|
23040
23101
|
pid: auditPid,
|
|
23041
23102
|
cgroup: auditCgroup,
|
|
23042
23103
|
result: "allowed"
|
|
23043
|
-
})
|
|
23104
|
+
})) {
|
|
23105
|
+
return;
|
|
23106
|
+
}
|
|
23044
23107
|
socket.write(encodeResponse(entryResponse(entry)));
|
|
23045
23108
|
return;
|
|
23046
23109
|
}
|
|
@@ -23448,21 +23511,6 @@ class VaultBroker {
|
|
|
23448
23511
|
socket.write(encodeResponse(errorResponse("DENIED", "peercred unavailable; cannot verify operator identity")));
|
|
23449
23512
|
return;
|
|
23450
23513
|
}
|
|
23451
|
-
if (peer !== null && peer.systemdUnit !== null) {
|
|
23452
|
-
const parsed = parseCronUnit(peer.systemdUnit);
|
|
23453
|
-
if (parsed !== null) {
|
|
23454
|
-
this.auditLogger.write({
|
|
23455
|
-
ts: new Date().toISOString(),
|
|
23456
|
-
op: req.op,
|
|
23457
|
-
caller: auditCaller,
|
|
23458
|
-
pid: auditPid,
|
|
23459
|
-
cgroup: auditCgroup,
|
|
23460
|
-
result: "denied:cron-cannot-manage-grants"
|
|
23461
|
-
});
|
|
23462
|
-
socket.write(encodeResponse(errorResponse("DENIED", "Grant management ops are operator-only; cron units cannot mint, list, or revoke grants")));
|
|
23463
|
-
return;
|
|
23464
|
-
}
|
|
23465
|
-
}
|
|
23466
23514
|
}
|
|
23467
23515
|
}
|
|
23468
23516
|
if (req.op === "mint_grant") {
|
|
@@ -23501,7 +23549,7 @@ class VaultBroker {
|
|
|
23501
23549
|
mkdirSync6(tokenDir, { recursive: true });
|
|
23502
23550
|
const tokenPath = path3.join(tokenDir, ".vault-token");
|
|
23503
23551
|
const tmpPath = `${tokenPath}.tmp.${process.pid}`;
|
|
23504
|
-
|
|
23552
|
+
writeFileSync3(tmpPath, mintResult.token, { mode: 384 });
|
|
23505
23553
|
renameSync6(tmpPath, tokenPath);
|
|
23506
23554
|
try {
|
|
23507
23555
|
const uid = allocateAgentUid(agent);
|
|
@@ -23788,7 +23836,7 @@ class VaultBroker {
|
|
|
23788
23836
|
_writePidFile() {
|
|
23789
23837
|
try {
|
|
23790
23838
|
const pidPath = resolvePath(PID_FILE_DEFAULT);
|
|
23791
|
-
|
|
23839
|
+
writeFileSync3(pidPath, String(process.pid) + `
|
|
23792
23840
|
`, { mode: 384 });
|
|
23793
23841
|
} catch {}
|
|
23794
23842
|
}
|
|
@@ -23844,7 +23892,7 @@ class VaultBroker {
|
|
|
23844
23892
|
const credPath = `${dir}/vault-passphrase`;
|
|
23845
23893
|
let passphrase;
|
|
23846
23894
|
try {
|
|
23847
|
-
passphrase =
|
|
23895
|
+
passphrase = readFileSync10(credPath, "utf8").replace(/\n+$/, "");
|
|
23848
23896
|
} catch (err) {
|
|
23849
23897
|
const code = err.code;
|
|
23850
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",
|