switchroom 0.19.17 → 0.19.19
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/bin/run-hook.sh +148 -0
- package/bin/workspace-dynamic-hook.sh +147 -38
- package/dist/agent-scheduler/index.js +13 -4
- package/dist/auth-broker/index.js +32 -5
- package/dist/cli/drive-write-pretool.mjs +48 -5
- package/dist/cli/ms-365-write-pretool.mjs +40 -2
- package/dist/cli/notion-write-pretool.mjs +13 -4
- package/dist/cli/switchroom.js +10614 -8104
- package/dist/host-control/main.js +12849 -11446
- package/dist/vault/approvals/kernel-server.js +90 -12
- package/dist/vault/broker/server.js +277 -94
- package/package.json +5 -3
- package/profiles/_base/start.sh.hbs +69 -5
- package/profiles/coding/CLAUDE.md.hbs +1 -1
- package/profiles/default/CLAUDE.md.hbs +3 -3
- package/profiles/executive-assistant/CLAUDE.md.hbs +1 -1
- package/profiles/health-coach/CLAUDE.md.hbs +1 -1
- package/skills/mental-model-curator/SKILL.md +8 -6
- package/telegram-plugin/bridge/bridge.ts +25 -19
- package/telegram-plugin/bridge/mcp-instructions.ts +87 -0
- package/telegram-plugin/dist/bridge/bridge.js +28 -20
- package/telegram-plugin/dist/gateway/gateway.js +2077 -1087
- package/telegram-plugin/dist/server.js +32 -20
- package/telegram-plugin/gateway/always-allow-persist-queue.ts +97 -11
- package/telegram-plugin/gateway/boot-card.ts +5 -1
- package/telegram-plugin/gateway/boot-probes.ts +113 -0
- package/telegram-plugin/gateway/config-approval-handler.test.ts +54 -0
- package/telegram-plugin/gateway/config-approval-handler.ts +16 -1
- package/telegram-plugin/gateway/disconnect-flush.ts +17 -0
- package/telegram-plugin/gateway/gateway.ts +43 -1
- package/telegram-plugin/gateway/handback-preturn-signal.ts +61 -7
- package/telegram-plugin/gateway/ipc-protocol.ts +5 -0
- package/telegram-plugin/gateway/ipc-server.ts +13 -0
- package/telegram-plugin/gateway/liveness-wiring.ts +125 -5
- package/telegram-plugin/gateway/missed-approvals-store.ts +66 -17
- package/telegram-plugin/gateway/obligation-ledger.ts +84 -4
- package/telegram-plugin/gateway/pending-card-store.ts +46 -16
- package/telegram-plugin/gateway/resume-inbound-builder.ts +13 -4
- package/telegram-plugin/gateway/scoped-grant-store.ts +39 -14
- package/telegram-plugin/gateway/store-file.ts +244 -0
- package/telegram-plugin/gateway/stream-render.ts +24 -5
- package/telegram-plugin/hooks/secret-guard-pretool.mjs +249 -76
- package/telegram-plugin/hooks/tool-label-pretool.mjs +88 -2
- package/telegram-plugin/registry/turns-schema.test.ts +8 -3
- package/telegram-plugin/registry/turns-schema.ts +40 -12
- package/telegram-plugin/runtime-metrics.ts +14 -0
- package/telegram-plugin/silence-poke.ts +138 -0
- package/telegram-plugin/tests/boot-probe-drift.test.ts +152 -0
- package/telegram-plugin/tests/bridge-tool-parity.test.ts +95 -0
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +32 -0
- package/telegram-plugin/tests/handback-preturn-signal.test.ts +62 -0
- package/telegram-plugin/tests/helpers/liveness-wiring-fixture.ts +178 -0
- package/telegram-plugin/tests/ipc-server-validate-config-approval.test.ts +95 -0
- package/telegram-plugin/tests/mcp-instructions-budget.test.ts +184 -0
- package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +22 -2
- package/telegram-plugin/tests/obligation-determinism.test.ts +114 -3
- package/telegram-plugin/tests/obligation-ledger.test.ts +310 -0
- package/telegram-plugin/tests/registry-turns.test.ts +13 -0
- package/telegram-plugin/tests/resume-inbound-builder.test.ts +15 -0
- package/telegram-plugin/tests/secret-guard-pretool.test.ts +347 -16
- package/telegram-plugin/tests/silence-poke-orphan-reap.test.ts +392 -0
- package/telegram-plugin/tests/silence-poke-teardown-notice.test.ts +301 -0
- package/telegram-plugin/tests/store-atomic-write.test.ts +411 -0
- package/telegram-plugin/tests/stream-render-golden.test.ts +103 -1
- package/telegram-plugin/tests/tool-activity-summary.test.ts +9 -2
- package/telegram-plugin/tests/tool-label-pretool.test.ts +94 -0
- package/telegram-plugin/tests/tts-normalize.test.ts +43 -0
- package/telegram-plugin/tests/voice-normalize-text.test.ts +212 -3
- package/telegram-plugin/tests/worker-feed-repeat-steps.test.ts +147 -0
- package/telegram-plugin/tts-normalize.ts +6 -4
- package/telegram-plugin/voice-normalize-text.ts +168 -11
- package/telegram-plugin/worker-activity-feed.ts +51 -1
- package/vendor/hindsight-memory/CHANGELOG.md +73 -0
- package/vendor/hindsight-memory/scripts/drain_pending.py +668 -56
- package/vendor/hindsight-memory/scripts/lib/client.py +124 -0
- package/vendor/hindsight-memory/scripts/lib/config.py +8 -3
- package/vendor/hindsight-memory/scripts/lib/directives.py +62 -4
- package/vendor/hindsight-memory/scripts/lib/pending.py +865 -33
- package/vendor/hindsight-memory/scripts/lib/retain_split.py +449 -0
- package/vendor/hindsight-memory/scripts/recall.py +257 -12
- package/vendor/hindsight-memory/scripts/retain.py +12 -6
- package/vendor/hindsight-memory/scripts/session_start.py +48 -0
- package/vendor/hindsight-memory/scripts/tests/test_client_document_exists.py +470 -0
- package/vendor/hindsight-memory/scripts/tests/test_directives.py +80 -9
- package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +2121 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +362 -18
- package/vendor/hindsight-memory/scripts/tests/test_retain_split.py +430 -0
- package/vendor/hindsight-memory/scripts/tests/test_session_start_version_skew.py +204 -0
- package/vendor/hindsight-memory/settings.json +1 -1
- package/vendor/hindsight-memory/tests/test_drain_pending.py +102 -6
- package/vendor/hindsight-memory/tests/test_pending.py +32 -7
|
@@ -4013,7 +4013,7 @@ var init_zod = __esm(() => {
|
|
|
4013
4013
|
});
|
|
4014
4014
|
|
|
4015
4015
|
// src/config/schema.ts
|
|
4016
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
|
|
4016
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
|
|
4017
4017
|
var init_schema = __esm(() => {
|
|
4018
4018
|
init_zod();
|
|
4019
4019
|
CodeRepoEntrySchema = exports_external.object({
|
|
@@ -4164,7 +4164,7 @@ var init_schema = __esm(() => {
|
|
|
4164
4164
|
recall: exports_external.object({
|
|
4165
4165
|
max_memories: exports_external.number().int().min(0).optional().describe("Cap on the number of memories injected into the prompt by " + "auto-recall, regardless of token budget. Plugin default is 12. " + "0 disables the cap (all memories Hindsight returns are injected)."),
|
|
4166
4166
|
cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
|
|
4167
|
-
min_overlap: exports_external.number().min(0).max(1).optional().describe("Minimum
|
|
4167
|
+
min_overlap: exports_external.number().min(0).max(1).optional().describe("Minimum containment token overlap [0.0–1.0] between the user " + "prompt and a memory's text for the memory to be injected. " + "A cheap FLOOR that removes candidates with (near-)zero lexical " + "relationship to the prompt — NOT a precision control: at the " + "recommended 0.10 the gate is near-passthrough, and the " + "effective precision control is the engine rerank plus the " + "max_memories head-slice. 0.0 disables (default — current " + "behaviour). Use 0.10; values at or above 0.20 measurably " + "starve recall — on production replay 0.20 leaves ~41.9% of " + "turns with NO memories at all, re-creating the bug #3541 " + "fixed. Observe the `overlap_dropped` field via " + "`switchroom memory recall-log`."),
|
|
4168
4168
|
types: exports_external.array(exports_external.string()).optional().describe("Hindsight fact types to recall. Switchroom default is " + '["world", "experience", "observation"] — the synthesized ' + "`observation` tier is on by default. Set to " + '["world", "experience"] to opt out of observation-backed ' + "recall for this agent (or fleet-wide under defaults)."),
|
|
4169
4169
|
additional_banks: exports_external.array(exports_external.string()).optional().describe("Extra Hindsight banks to recall from on every turn, merged into " + "the agent's own bank results — e.g. a shared operator/household " + "profile bank authored via `switchroom memory profile`. Each is " + "recalled with an 8s timeout and is non-fatal on failure. Stays " + "within the single tenant: all banks are the operator's data, in " + "the operator's Hindsight instance (see the `single-tenant` " + "invariant). Defaults to [] (no extra banks)."),
|
|
4170
4170
|
sender_banks: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Per-speaker recall routing: a map of Telegram sender → extra " + "recall bank. When a message arrives, the agent also recalls the " + "speaker's bank (matched by Telegram username — a leading @ is " + "optional — or numeric user_id), merged " + "into its own results — so each trusted user gets their own " + "profile context. Additive recall scoping within the single " + "tenant: never an access boundary (who may drive an agent stays " + "the per-agent user assignment in `access.allowFrom`). Author the " + "banks via `switchroom memory profile`."),
|
|
@@ -4476,9 +4476,16 @@ var init_schema = __esm(() => {
|
|
|
4476
4476
|
enabled: exports_external.boolean().optional().describe("Master switch for the reaction-dispatch path. Default false — " + "with no reaction_dispatch block, reactions are persisted (and may " + "feed the `reactions` feedback path) but are NEVER dispatched as " + "event-driven inbound turns."),
|
|
4477
4477
|
emojis: exports_external.array(exports_external.string()).optional().describe('Emoji allowlist that triggers a `<channel event="reaction">` ' + "inbound turn when reacted to any message. Default [] (nothing " + "fires). Cascade mode: REPLACE (not union) — a layer's list " + "replaces lower layers entirely so an operator can narrow per-agent.")
|
|
4478
4478
|
}).optional();
|
|
4479
|
-
|
|
4479
|
+
releaseBlockFields = {
|
|
4480
4480
|
channel: exports_external.enum(["dev", "rc", "latest"]).optional(),
|
|
4481
4481
|
pin: exports_external.string().regex(/^(sha-[0-9a-f]{7,40}|v\d+\.\d+\.\d+)$/).optional()
|
|
4482
|
+
};
|
|
4483
|
+
ReleaseBlock = exports_external.object(releaseBlockFields).strict().refine((r) => !(r.channel && r.pin), {
|
|
4484
|
+
message: "release.channel and release.pin are mutually exclusive"
|
|
4485
|
+
});
|
|
4486
|
+
RootReleaseBlock = exports_external.object({
|
|
4487
|
+
...releaseBlockFields,
|
|
4488
|
+
auto_update: exports_external.boolean().optional().describe("Opt-in unattended fleet auto-update (KEN-131). When true, hostd's " + "release watcher polls the published release version and, on a new " + "release, drives the EXISTING staggered canary rollout " + "(`switchroom rollout --pin vX.Y.Z`) unattended: canary-first, " + "per-agent version assert, durable pin persisted only after the " + "canary is green, abort + operator alert card on canary failure, " + "and compose rollback on a failed apply. Default false — with it " + "unset/false, behaviour is unchanged.")
|
|
4482
4489
|
}).strict().refine((r) => !(r.channel && r.pin), {
|
|
4483
4490
|
message: "release.channel and release.pin are mutually exclusive"
|
|
4484
4491
|
});
|
|
@@ -4722,6 +4729,7 @@ var init_schema = __esm(() => {
|
|
|
4722
4729
|
enabled: exports_external.boolean().default(false).describe("When true, hostd polls the remote release tag every " + "`interval_minutes` and applies + restarts the fleet when a new " + "release is detected. Default false — opt-in."),
|
|
4723
4730
|
interval_minutes: exports_external.number().int().min(5).max(1440).default(5).describe("Poll interval in minutes. Floor of 5m matches the agent-config " + "cron rate limit; ceiling of 1440m (24h) is a sanity cap."),
|
|
4724
4731
|
apply_on_detect: exports_external.boolean().default(true).describe("When false, hostd logs `release_detected` but does NOT call " + "update_apply / restart all. Useful for dogfooding the detector " + "without rolling the fleet."),
|
|
4732
|
+
notify_on_detect: exports_external.boolean().default(false).describe("KEN-129 — operator-in-the-loop update prompt. Only consulted " + "when apply_on_detect is false (auto-apply supersedes notify): " + "a newly detected release posts ONE operator approval card " + "('fleet is behind — tap to apply') via an admin agent's " + "gateway; Approve runs hostd's update_apply path (fleet-" + "mutation-locked, durable status rows, get_status-pollable). " + "Dedup on release id: the last-notified id persists in " + "~/.switchroom/release-notify-state.json, so a card that " + "reached the operator is never re-posted for the same release."),
|
|
4725
4733
|
image_ref: exports_external.string().default("ghcr.io/switchroom/switchroom-agent:latest").describe("Image reference whose remote digest is compared to the local " + "image digest. Defaults to the agent image's :latest tag, which " + "is the canonical signal that a release has been promoted.")
|
|
4726
4734
|
});
|
|
4727
4735
|
HostControlConfigSchema = exports_external.object({
|
|
@@ -4764,7 +4772,7 @@ var init_schema = __esm(() => {
|
|
|
4764
4772
|
timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC'").optional().describe("Global default IANA timezone applied to every agent unless the " + "agent (or its profile) declares its own. See the per-agent " + "timezone field for the full cascade and auto-detection fallback.")
|
|
4765
4773
|
}),
|
|
4766
4774
|
telegram: TelegramConfigSchema,
|
|
4767
|
-
release:
|
|
4775
|
+
release: RootReleaseBlock.optional().describe("Fleet-wide default release-channel pin / pointer for the update " + "flow. Either `channel` (dev|rc|latest) or `pin` (sha-<hex>|v<semver>) " + "— mutually exclusive. Per-agent `release` REPLACES this entirely " + "(except `auto_update`, which is root-only — a fleet property read by " + "hostd's release watcher, never per-agent)."),
|
|
4768
4776
|
memory: MemoryBackendConfigSchema.optional(),
|
|
4769
4777
|
hindsight: HindsightConfigSchema.optional().describe("Fleet-singleton Hindsight (memory backend) configuration. Currently " + "just the LLM knob (provider + model) used for retain/reflect/" + "consolidation. Both fields optional; when unset the container falls " + "back to provider=claude-code + the hard-coded HINDSIGHT_DEFAULT_MODEL " + "so nothing changes for operators who don't set it. Read at container " + "launch by startHindsight() (src/setup/hindsight.ts) — takes effect on " + "the next `switchroom apply` / `memory setup --recreate`."),
|
|
4770
4778
|
vault: VaultConfigSchema.optional(),
|
|
@@ -18214,7 +18222,7 @@ var require_lib = __commonJS((exports, module) => {
|
|
|
18214
18222
|
|
|
18215
18223
|
// src/vault/broker/server.ts
|
|
18216
18224
|
import * as net from "node:net";
|
|
18217
|
-
import { mkdirSync as mkdirSync7, chmodSync as chmodSync5, chownSync, existsSync as existsSync12, readFileSync as readFileSync10, readdirSync as
|
|
18225
|
+
import { mkdirSync as mkdirSync7, chmodSync as chmodSync5, chownSync, existsSync as existsSync12, readFileSync as readFileSync10, readdirSync as readdirSync5, statSync as statSync7, unlinkSync as unlinkSync6, writeFileSync as writeFileSync3, renameSync as renameSync8 } from "node:fs";
|
|
18218
18226
|
|
|
18219
18227
|
// src/agents/compose.ts
|
|
18220
18228
|
init_schema();
|
|
@@ -18823,9 +18831,26 @@ var HINDSIGHT_BROKER_SOCK_VOLUME = `auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock`
|
|
|
18823
18831
|
var HINDSIGHT_CREDS_MIRROR_VOLUME = `consumer-creds-${HINDSIGHT_CONSUMER_NAME}`;
|
|
18824
18832
|
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)';
|
|
18825
18833
|
var HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
|
|
18834
|
+
var DOCKER_PROBE_TIMEOUT_MS = 60 * 1000;
|
|
18826
18835
|
|
|
18827
18836
|
// src/memory/hindsight.ts
|
|
18828
|
-
var DEFAULT_RETAIN_MISSION = "Extract
|
|
18837
|
+
var DEFAULT_RETAIN_MISSION = "Extract durable facts that will still be true and useful weeks from now: " + "user preferences and standing rules, ongoing projects and recurring " + "commitments, technical and architectural decisions with their rationale, " + "and people/tool relationships. A preference revealed by a request is " + "durable — record the preference (what the user likes, wants, or always " + `does), not the request itself.
|
|
18838
|
+
|
|
18839
|
+
` + `NEVER extract:
|
|
18840
|
+
` + "- Agent tool-use traces or narration of what the assistant did (e.g. " + `"the assistant used X to query Y", "ran a search", "sent the message").
|
|
18841
|
+
` + "- In-flight workflow/process narration (a sub-task started, paused, or is " + "still running) — retain the outcome only once the task completes or a " + `decision is made.
|
|
18842
|
+
` + `- Operation, request, batch or session IDs, UUIDs, hashes, or error codes.
|
|
18843
|
+
` + "- Hindsight's own errors, retries, backlogs, or internal state — the " + `memory system's self-reports are not memories.
|
|
18844
|
+
` + `- Restatements of the user's current request or the task in progress.
|
|
18845
|
+
` + "- Transient state (unread counts, build status, what is running right now) " + "unless the fact is explicitly dated, in which case record it as a dated " + `observation.
|
|
18846
|
+
` + `- Greetings, acknowledgements, and routine operational chatter.
|
|
18847
|
+
|
|
18848
|
+
` + "If a candidate fact matches an exclusion, drop it rather than rewording " + "it. If nothing durable remains, return an empty facts list.";
|
|
18849
|
+
var SUPERSEDED_RETAIN_MISSIONS = [
|
|
18850
|
+
"Extract technical decisions, architectural choices, user preferences, project context, and people/tool relationships. Ignore routine greetings and transient operational details.",
|
|
18851
|
+
"Extract user preferences, ongoing projects, recurring commitments, " + "important context, and durable facts that should help across future " + "conversations. Skip one-off chatter and temporary task noise.",
|
|
18852
|
+
"Extract user preferences, ongoing projects, recurring commitments, " + "important context, and durable facts that should help across future " + "conversations. Skip one-off chatter and temporary task noise, " + "including in-flight workflow/process narration (a sub-task started, " + "paused, or is still running) — only retain the outcome once a task " + "actually completes or a decision is made."
|
|
18853
|
+
];
|
|
18829
18854
|
var PROFILE_MEMORY_DEFAULTS = {
|
|
18830
18855
|
"health-coach": {
|
|
18831
18856
|
disposition: { skepticism: 2, literalism: 2, empathy: 5 },
|
|
@@ -19522,7 +19547,8 @@ var ApprovalDecisionMetaSchema = exports_external.object({
|
|
|
19522
19547
|
ttl_expires_at: exports_external.number().nullable(),
|
|
19523
19548
|
last_used_at: exports_external.number().nullable(),
|
|
19524
19549
|
revoked_at: exports_external.number().nullable(),
|
|
19525
|
-
revoke_reason: exports_external.string().nullable()
|
|
19550
|
+
revoke_reason: exports_external.string().nullable(),
|
|
19551
|
+
origin: exports_external.enum(["agent", "operator"]).optional()
|
|
19526
19552
|
});
|
|
19527
19553
|
var OkApprovalLookupResponseSchema = exports_external.object({
|
|
19528
19554
|
ok: exports_external.literal(true),
|
|
@@ -19634,6 +19660,7 @@ init_paths();
|
|
|
19634
19660
|
// src/agents/scaffold.ts
|
|
19635
19661
|
var REPO_ROOT = resolve6(import.meta.dirname, "../..");
|
|
19636
19662
|
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.";
|
|
19663
|
+
var pendingBankOps = new Set;
|
|
19637
19664
|
var SWITCHROOM_OWNED_SETTINGS_KEYS = new Set([
|
|
19638
19665
|
"permissions",
|
|
19639
19666
|
"mcpServers",
|
|
@@ -19720,9 +19747,9 @@ function migrateLegacyGrantsDbLocation(newDbPath = getGrantsDbPath(), deps) {
|
|
|
19720
19747
|
var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
|
|
19721
19748
|
|
|
19722
19749
|
// src/vault/broker/server.ts
|
|
19723
|
-
import { dirname as dirname7, resolve as resolve9, basename as
|
|
19750
|
+
import { dirname as dirname7, resolve as resolve9, basename as basename5 } from "node:path";
|
|
19724
19751
|
import * as os3 from "node:os";
|
|
19725
|
-
import * as
|
|
19752
|
+
import * as path5 from "node:path";
|
|
19726
19753
|
|
|
19727
19754
|
// src/vault/migrate-layout.ts
|
|
19728
19755
|
import {
|
|
@@ -19956,9 +19983,9 @@ function readAutoUnlockFile(filePath) {
|
|
|
19956
19983
|
var DEFAULT_AUTO_UNLOCK_PATH = "~/.switchroom/vault-auto-unlock";
|
|
19957
19984
|
|
|
19958
19985
|
// src/vault/broker/audit-log.ts
|
|
19959
|
-
import * as
|
|
19986
|
+
import * as fs3 from "node:fs";
|
|
19960
19987
|
import * as os2 from "node:os";
|
|
19961
|
-
import * as
|
|
19988
|
+
import * as path3 from "node:path";
|
|
19962
19989
|
|
|
19963
19990
|
// src/util/audit-hashchain.ts
|
|
19964
19991
|
import { createHash as createHash4 } from "node:crypto";
|
|
@@ -20075,76 +20102,107 @@ function safeAuditLogPath(requestedPath) {
|
|
|
20075
20102
|
return cachedTmpAuditLog;
|
|
20076
20103
|
}
|
|
20077
20104
|
|
|
20078
|
-
// src/
|
|
20079
|
-
|
|
20080
|
-
|
|
20081
|
-
|
|
20082
|
-
|
|
20105
|
+
// src/util/log-rotation.ts
|
|
20106
|
+
import * as fs2 from "node:fs";
|
|
20107
|
+
import * as path2 from "node:path";
|
|
20108
|
+
function fsyncAndMeasure(snapshotPath, logPath, tag) {
|
|
20109
|
+
let step = "open";
|
|
20083
20110
|
try {
|
|
20084
|
-
const
|
|
20085
|
-
|
|
20086
|
-
|
|
20087
|
-
|
|
20088
|
-
|
|
20089
|
-
|
|
20090
|
-
|
|
20091
|
-
|
|
20092
|
-
|
|
20093
|
-
|
|
20111
|
+
const fd = fs2.openSync(snapshotPath, "r");
|
|
20112
|
+
try {
|
|
20113
|
+
step = "fsync";
|
|
20114
|
+
fs2.fsyncSync(fd);
|
|
20115
|
+
step = "measure";
|
|
20116
|
+
const size = fs2.fstatSync(fd).size;
|
|
20117
|
+
step = "close";
|
|
20118
|
+
return size;
|
|
20119
|
+
} finally {
|
|
20120
|
+
fs2.closeSync(fd);
|
|
20121
|
+
}
|
|
20122
|
+
} catch (err) {
|
|
20123
|
+
process.stderr.write(`[${tag}] ERROR: could not ${step} snapshot ${snapshotPath}; leaving active log ${logPath} intact to avoid data loss: ${err.message}
|
|
20124
|
+
`);
|
|
20125
|
+
return null;
|
|
20094
20126
|
}
|
|
20095
20127
|
}
|
|
20096
|
-
|
|
20097
|
-
|
|
20098
|
-
|
|
20099
|
-
|
|
20100
|
-
|
|
20101
|
-
|
|
20102
|
-
|
|
20103
|
-
|
|
20104
|
-
}
|
|
20105
|
-
|
|
20106
|
-
|
|
20128
|
+
function rotateLogFile(logPath, maxFiles, tag, stillHeld) {
|
|
20129
|
+
const keep = Math.max(1, Math.floor(maxFiles));
|
|
20130
|
+
const held = (what) => {
|
|
20131
|
+
if (!stillHeld || stillHeld())
|
|
20132
|
+
return true;
|
|
20133
|
+
process.stderr.write(`[${tag}] WARN: rotation lock for ${logPath} was reclaimed while we were inside it; declining to ${what}
|
|
20134
|
+
`);
|
|
20135
|
+
return false;
|
|
20136
|
+
};
|
|
20137
|
+
let entryBytes;
|
|
20138
|
+
try {
|
|
20139
|
+
entryBytes = fs2.statSync(logPath).size;
|
|
20140
|
+
} catch (err) {
|
|
20141
|
+
process.stderr.write(`[${tag}] ERROR: could not stat active log ${logPath} before rotating it: ${err.message}
|
|
20142
|
+
`);
|
|
20143
|
+
return false;
|
|
20144
|
+
}
|
|
20145
|
+
const notRotatedUnderUs = (what) => {
|
|
20146
|
+
let live;
|
|
20147
|
+
try {
|
|
20148
|
+
live = fs2.statSync(logPath).size;
|
|
20149
|
+
} catch (err) {
|
|
20150
|
+
process.stderr.write(`[${tag}] ERROR: could not re-stat active log ${logPath}; declining to ${what}: ${err.message}
|
|
20151
|
+
`);
|
|
20152
|
+
return false;
|
|
20153
|
+
}
|
|
20154
|
+
if (live < entryBytes) {
|
|
20155
|
+
process.stderr.write(`[${tag}] WARN: active log ${logPath} shrank from ${entryBytes} to ${live} bytes while we were rotating it; another rotator snapshotted it and these generations are its, not ours — declining to ${what}
|
|
20156
|
+
`);
|
|
20157
|
+
return false;
|
|
20158
|
+
}
|
|
20159
|
+
return true;
|
|
20160
|
+
};
|
|
20161
|
+
const oldest = `${logPath}.${keep}`;
|
|
20107
20162
|
if (fs2.existsSync(oldest)) {
|
|
20163
|
+
if (!held(`drop ${oldest}`))
|
|
20164
|
+
return false;
|
|
20165
|
+
if (!notRotatedUnderUs(`drop ${oldest}`))
|
|
20166
|
+
return false;
|
|
20108
20167
|
try {
|
|
20109
20168
|
fs2.unlinkSync(oldest);
|
|
20110
20169
|
} catch (err) {
|
|
20111
|
-
process.stderr.write(`[
|
|
20170
|
+
process.stderr.write(`[${tag}] ERROR: could not drop oldest rotation ${oldest}: ${err.message}
|
|
20112
20171
|
`);
|
|
20113
20172
|
}
|
|
20114
20173
|
}
|
|
20115
|
-
for (let n =
|
|
20174
|
+
for (let n = keep - 1;n >= 1; n--) {
|
|
20116
20175
|
const from = `${logPath}.${n}`;
|
|
20117
20176
|
const to = `${logPath}.${n + 1}`;
|
|
20118
20177
|
if (!fs2.existsSync(from))
|
|
20119
20178
|
continue;
|
|
20179
|
+
if (!held(`shift ${from} → ${to}`))
|
|
20180
|
+
return false;
|
|
20181
|
+
if (!notRotatedUnderUs(`shift ${from} → ${to}`))
|
|
20182
|
+
return false;
|
|
20120
20183
|
try {
|
|
20121
20184
|
fs2.renameSync(from, to);
|
|
20122
20185
|
} catch (err) {
|
|
20123
|
-
process.stderr.write(`[
|
|
20186
|
+
process.stderr.write(`[${tag}] ERROR: could not rotate ${from} → ${to}: ${err.message}
|
|
20124
20187
|
`);
|
|
20125
|
-
return;
|
|
20188
|
+
return false;
|
|
20126
20189
|
}
|
|
20127
20190
|
}
|
|
20128
20191
|
const snapshotPath = `${logPath}.1`;
|
|
20192
|
+
if (!held(`overwrite ${snapshotPath}`))
|
|
20193
|
+
return false;
|
|
20194
|
+
if (!notRotatedUnderUs(`overwrite ${snapshotPath}`))
|
|
20195
|
+
return false;
|
|
20129
20196
|
try {
|
|
20130
20197
|
fs2.copyFileSync(logPath, snapshotPath);
|
|
20131
20198
|
} catch (err) {
|
|
20132
|
-
process.stderr.write(`[
|
|
20133
|
-
`);
|
|
20134
|
-
return;
|
|
20135
|
-
}
|
|
20136
|
-
try {
|
|
20137
|
-
const fd = fs2.openSync(snapshotPath, "r");
|
|
20138
|
-
try {
|
|
20139
|
-
fs2.fsyncSync(fd);
|
|
20140
|
-
} finally {
|
|
20141
|
-
fs2.closeSync(fd);
|
|
20142
|
-
}
|
|
20143
|
-
} catch (err) {
|
|
20144
|
-
process.stderr.write(`[vault-audit] ERROR: could not fsync audit snapshot ${snapshotPath}; leaving active log intact to avoid data loss: ${err.message}
|
|
20199
|
+
process.stderr.write(`[${tag}] ERROR: could not snapshot active log ${logPath} → ${snapshotPath}: ${err.message}
|
|
20145
20200
|
`);
|
|
20146
|
-
return;
|
|
20201
|
+
return false;
|
|
20147
20202
|
}
|
|
20203
|
+
const snapshotBytes = fsyncAndMeasure(snapshotPath, logPath, tag);
|
|
20204
|
+
if (snapshotBytes === null)
|
|
20205
|
+
return false;
|
|
20148
20206
|
try {
|
|
20149
20207
|
const dirFd = fs2.openSync(path2.dirname(snapshotPath), "r");
|
|
20150
20208
|
try {
|
|
@@ -20153,15 +20211,78 @@ function rotateAuditLog(logPath, maxFiles) {
|
|
|
20153
20211
|
fs2.closeSync(dirFd);
|
|
20154
20212
|
}
|
|
20155
20213
|
} catch {}
|
|
20214
|
+
if (!held(`truncate ${logPath}`))
|
|
20215
|
+
return false;
|
|
20216
|
+
let liveBytes;
|
|
20217
|
+
try {
|
|
20218
|
+
liveBytes = fs2.statSync(logPath).size;
|
|
20219
|
+
} catch (err) {
|
|
20220
|
+
process.stderr.write(`[${tag}] ERROR: could not re-stat active log ${logPath} before truncating; leaving it intact to avoid data loss: ${err.message}
|
|
20221
|
+
`);
|
|
20222
|
+
return false;
|
|
20223
|
+
}
|
|
20224
|
+
if (liveBytes < snapshotBytes) {
|
|
20225
|
+
process.stderr.write(`[${tag}] WARN: active log ${logPath} shrank from ${snapshotBytes} to ${liveBytes} bytes after we snapshotted it; another rotator truncated it and these rows are its, not ours — declining to truncate
|
|
20226
|
+
`);
|
|
20227
|
+
return false;
|
|
20228
|
+
}
|
|
20156
20229
|
try {
|
|
20157
20230
|
fs2.truncateSync(logPath, 0);
|
|
20158
20231
|
} catch (err) {
|
|
20159
|
-
process.stderr.write(`[
|
|
20232
|
+
process.stderr.write(`[${tag}] ERROR: could not truncate active log ${logPath}: ${err.message}
|
|
20160
20233
|
`);
|
|
20234
|
+
return false;
|
|
20235
|
+
}
|
|
20236
|
+
if (stillHeld && !stillHeld()) {
|
|
20237
|
+
process.stderr.write(`[${tag}] ERROR: truncated ${logPath} after our rotation lock was reclaimed; rows another rotator wrote between our size check and our truncate are LOST
|
|
20238
|
+
`);
|
|
20239
|
+
}
|
|
20240
|
+
return true;
|
|
20241
|
+
}
|
|
20242
|
+
function resolveRotationConfig(args) {
|
|
20243
|
+
const env = args.env ?? process.env;
|
|
20244
|
+
const envBytes = Number(env[args.envBytesVar]);
|
|
20245
|
+
const envFiles = Number(env[args.envFilesVar]);
|
|
20246
|
+
const maxBytes = args.maxBytes !== undefined && args.maxBytes !== 0 ? args.maxBytes : Number.isFinite(envBytes) && envBytes !== 0 ? envBytes : args.defaultBytes;
|
|
20247
|
+
const maxFiles = args.maxFiles !== undefined && args.maxFiles > 0 ? args.maxFiles : Number.isFinite(envFiles) && envFiles > 0 ? envFiles : args.defaultFiles;
|
|
20248
|
+
return { maxBytes, maxFiles };
|
|
20249
|
+
}
|
|
20250
|
+
|
|
20251
|
+
// src/vault/broker/audit-log.ts
|
|
20252
|
+
function failOpenStatePath(logPath) {
|
|
20253
|
+
return `${logPath}.failopen`;
|
|
20254
|
+
}
|
|
20255
|
+
function readFailOpenState(statePath) {
|
|
20256
|
+
try {
|
|
20257
|
+
const raw = fs3.readFileSync(statePath, "utf8");
|
|
20258
|
+
const parsed = JSON.parse(raw);
|
|
20259
|
+
const count = Number(parsed.failOpenCount);
|
|
20260
|
+
return {
|
|
20261
|
+
failOpenCount: Number.isFinite(count) && count > 0 ? count : 0,
|
|
20262
|
+
lastFailureTs: typeof parsed.lastFailureTs === "string" ? parsed.lastFailureTs : undefined,
|
|
20263
|
+
lastError: typeof parsed.lastError === "string" ? parsed.lastError : undefined
|
|
20264
|
+
};
|
|
20265
|
+
} catch {
|
|
20266
|
+
return { failOpenCount: 0 };
|
|
20161
20267
|
}
|
|
20162
20268
|
}
|
|
20269
|
+
var DEFAULT_AUDIT_MAX_BYTES = 32 * 1024 * 1024;
|
|
20270
|
+
var DEFAULT_AUDIT_MAX_FILES = 5;
|
|
20271
|
+
function resolveRotation(opts) {
|
|
20272
|
+
return resolveRotationConfig({
|
|
20273
|
+
maxBytes: opts.maxBytes,
|
|
20274
|
+
maxFiles: opts.maxFiles,
|
|
20275
|
+
envBytesVar: "SWITCHROOM_VAULT_AUDIT_MAX_BYTES",
|
|
20276
|
+
envFilesVar: "SWITCHROOM_VAULT_AUDIT_MAX_FILES",
|
|
20277
|
+
defaultBytes: DEFAULT_AUDIT_MAX_BYTES,
|
|
20278
|
+
defaultFiles: DEFAULT_AUDIT_MAX_FILES
|
|
20279
|
+
});
|
|
20280
|
+
}
|
|
20281
|
+
function rotateAuditLog(logPath, maxFiles) {
|
|
20282
|
+
rotateLogFile(logPath, maxFiles, "vault-audit");
|
|
20283
|
+
}
|
|
20163
20284
|
function defaultAuditLogPath() {
|
|
20164
|
-
return
|
|
20285
|
+
return path3.join(os2.homedir(), ".switchroom", "vault-audit.log");
|
|
20165
20286
|
}
|
|
20166
20287
|
function callerFromPeer(peer) {
|
|
20167
20288
|
if (peer.systemdUnit !== null && peer.systemdUnit.length > 0) {
|
|
@@ -20183,7 +20304,7 @@ function createAuditLogger(opts = {}) {
|
|
|
20183
20304
|
lastError: String(err.message).slice(0, 300)
|
|
20184
20305
|
};
|
|
20185
20306
|
try {
|
|
20186
|
-
|
|
20307
|
+
fs3.writeFileSync(statePath, `${JSON.stringify(state)}
|
|
20187
20308
|
`, { mode: 384 });
|
|
20188
20309
|
} catch (persistErr) {
|
|
20189
20310
|
process.stderr.write(`[vault-audit] ERROR: could not persist fail-open counter ${statePath}: ${persistErr.message}
|
|
@@ -20197,7 +20318,7 @@ function createAuditLogger(opts = {}) {
|
|
|
20197
20318
|
write(entry) {
|
|
20198
20319
|
if (rotation.maxBytes > 0) {
|
|
20199
20320
|
try {
|
|
20200
|
-
const size =
|
|
20321
|
+
const size = fs3.statSync(logPath).size;
|
|
20201
20322
|
if (size >= rotation.maxBytes) {
|
|
20202
20323
|
rotateAuditLog(logPath, rotation.maxFiles);
|
|
20203
20324
|
}
|
|
@@ -20206,7 +20327,7 @@ function createAuditLogger(opts = {}) {
|
|
|
20206
20327
|
const { line, next } = chainRow(chain, entry);
|
|
20207
20328
|
let fd;
|
|
20208
20329
|
try {
|
|
20209
|
-
fd =
|
|
20330
|
+
fd = fs3.openSync(logPath, "a", 384);
|
|
20210
20331
|
} catch (err) {
|
|
20211
20332
|
process.stderr.write(`[vault-audit] ERROR: could not open audit log ${logPath}: ${err.message}
|
|
20212
20333
|
`);
|
|
@@ -20215,7 +20336,7 @@ function createAuditLogger(opts = {}) {
|
|
|
20215
20336
|
}
|
|
20216
20337
|
let durable = false;
|
|
20217
20338
|
try {
|
|
20218
|
-
|
|
20339
|
+
fs3.writeSync(fd, line);
|
|
20219
20340
|
chain = next;
|
|
20220
20341
|
durable = true;
|
|
20221
20342
|
} catch (err) {
|
|
@@ -20224,7 +20345,7 @@ function createAuditLogger(opts = {}) {
|
|
|
20224
20345
|
recordFailOpen(err);
|
|
20225
20346
|
} finally {
|
|
20226
20347
|
try {
|
|
20227
|
-
|
|
20348
|
+
fs3.closeSync(fd);
|
|
20228
20349
|
} catch (closeErr) {
|
|
20229
20350
|
process.stderr.write(`[vault-audit] ERROR: could not close audit log fd: ${closeErr.message}
|
|
20230
20351
|
`);
|
|
@@ -20236,7 +20357,7 @@ function createAuditLogger(opts = {}) {
|
|
|
20236
20357
|
}
|
|
20237
20358
|
|
|
20238
20359
|
// src/vault/grants.ts
|
|
20239
|
-
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
20360
|
+
import { createHash as createHash5, randomBytes as randomBytes4 } from "node:crypto";
|
|
20240
20361
|
|
|
20241
20362
|
// node_modules/.bun/bcryptjs@3.0.3/node_modules/bcryptjs/index.js
|
|
20242
20363
|
import nodeCrypto from "crypto";
|
|
@@ -21930,6 +22051,44 @@ function migrateGrantsSchema(db) {
|
|
|
21930
22051
|
}
|
|
21931
22052
|
}
|
|
21932
22053
|
var BCRYPT_COST = 10;
|
|
22054
|
+
var COMPARE_MEMO_TTL_MS = 60000;
|
|
22055
|
+
var COMPARE_MEMO_MAX_ENTRIES = 512;
|
|
22056
|
+
var compareMemo = new Map;
|
|
22057
|
+
function memoKey(id, secretHash, secret) {
|
|
22058
|
+
return `${id}:${secretHash}:${createHash5("sha256").update(secret).digest("hex")}`;
|
|
22059
|
+
}
|
|
22060
|
+
async function compareSecret(id, secret, secretHash) {
|
|
22061
|
+
const k = memoKey(id, secretHash, secret);
|
|
22062
|
+
const now = Date.now();
|
|
22063
|
+
const expiry = compareMemo.get(k);
|
|
22064
|
+
if (expiry !== undefined) {
|
|
22065
|
+
if (expiry > now)
|
|
22066
|
+
return true;
|
|
22067
|
+
compareMemo.delete(k);
|
|
22068
|
+
}
|
|
22069
|
+
let hashMatches;
|
|
22070
|
+
try {
|
|
22071
|
+
hashMatches = await compare(secret, secretHash);
|
|
22072
|
+
} catch {
|
|
22073
|
+
hashMatches = false;
|
|
22074
|
+
}
|
|
22075
|
+
if (hashMatches) {
|
|
22076
|
+
if (compareMemo.size >= COMPARE_MEMO_MAX_ENTRIES) {
|
|
22077
|
+
for (const [ck, cexp] of compareMemo) {
|
|
22078
|
+
if (cexp <= now)
|
|
22079
|
+
compareMemo.delete(ck);
|
|
22080
|
+
}
|
|
22081
|
+
while (compareMemo.size >= COMPARE_MEMO_MAX_ENTRIES) {
|
|
22082
|
+
const oldest = compareMemo.keys().next();
|
|
22083
|
+
if (oldest.done)
|
|
22084
|
+
break;
|
|
22085
|
+
compareMemo.delete(oldest.value);
|
|
22086
|
+
}
|
|
22087
|
+
}
|
|
22088
|
+
compareMemo.set(k, now + COMPARE_MEMO_TTL_MS);
|
|
22089
|
+
}
|
|
22090
|
+
return hashMatches;
|
|
22091
|
+
}
|
|
21933
22092
|
function generateId() {
|
|
21934
22093
|
return "vg_" + randomBytes4(3).toString("hex");
|
|
21935
22094
|
}
|
|
@@ -22012,12 +22171,7 @@ async function validateGrant(db, token, key) {
|
|
|
22012
22171
|
return { ok: false, reason: "grant-invalid" };
|
|
22013
22172
|
}
|
|
22014
22173
|
const secretHash = row.secret_hash;
|
|
22015
|
-
|
|
22016
|
-
try {
|
|
22017
|
-
hashMatches = await compare(secret, secretHash);
|
|
22018
|
-
} catch {
|
|
22019
|
-
hashMatches = false;
|
|
22020
|
-
}
|
|
22174
|
+
const hashMatches = await compareSecret(id, secret, secretHash);
|
|
22021
22175
|
if (!hashMatches) {
|
|
22022
22176
|
return { ok: false, reason: "grant-invalid" };
|
|
22023
22177
|
}
|
|
@@ -22049,12 +22203,7 @@ async function validateGrantForWrite(db, token, key) {
|
|
|
22049
22203
|
if (!row)
|
|
22050
22204
|
return { ok: false, reason: "grant-invalid" };
|
|
22051
22205
|
const secretHash = row.secret_hash;
|
|
22052
|
-
|
|
22053
|
-
try {
|
|
22054
|
-
hashMatches = await compare(secret, secretHash);
|
|
22055
|
-
} catch {
|
|
22056
|
-
hashMatches = false;
|
|
22057
|
-
}
|
|
22206
|
+
const hashMatches = await compareSecret(id, secret, secretHash);
|
|
22058
22207
|
if (!hashMatches)
|
|
22059
22208
|
return { ok: false, reason: "grant-invalid" };
|
|
22060
22209
|
const grant = rowToGrant(row);
|
|
@@ -22115,8 +22264,8 @@ function adminOnlyKeysBeingAdded(requestedKeys, existingKeys, patterns) {
|
|
|
22115
22264
|
}
|
|
22116
22265
|
|
|
22117
22266
|
// src/vault/grants-db.ts
|
|
22118
|
-
import * as
|
|
22119
|
-
import * as
|
|
22267
|
+
import * as path4 from "node:path";
|
|
22268
|
+
import * as fs4 from "node:fs";
|
|
22120
22269
|
import { Database } from "bun:sqlite";
|
|
22121
22270
|
|
|
22122
22271
|
// src/vault/approvals/schema.ts
|
|
@@ -22209,12 +22358,12 @@ function isGrantsDbCorruption(err) {
|
|
|
22209
22358
|
function quarantineGrantsDb(dbPath) {
|
|
22210
22359
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
22211
22360
|
const quarantinePath = `${dbPath}.corrupt-${stamp}`;
|
|
22212
|
-
|
|
22361
|
+
fs4.renameSync(dbPath, quarantinePath);
|
|
22213
22362
|
for (const suffix of WAL_SIDECAR_SUFFIXES2) {
|
|
22214
22363
|
const sidecar = `${dbPath}${suffix}`;
|
|
22215
|
-
if (
|
|
22364
|
+
if (fs4.existsSync(sidecar)) {
|
|
22216
22365
|
try {
|
|
22217
|
-
|
|
22366
|
+
fs4.renameSync(sidecar, `${quarantinePath}${suffix}`);
|
|
22218
22367
|
} catch {}
|
|
22219
22368
|
}
|
|
22220
22369
|
}
|
|
@@ -22224,7 +22373,7 @@ function openAndMigrate(dbPath) {
|
|
|
22224
22373
|
const db = new Database(dbPath, { create: true });
|
|
22225
22374
|
try {
|
|
22226
22375
|
try {
|
|
22227
|
-
|
|
22376
|
+
fs4.chmodSync(dbPath, 384);
|
|
22228
22377
|
} catch {}
|
|
22229
22378
|
db.run("PRAGMA journal_mode=WAL");
|
|
22230
22379
|
db.run("PRAGMA busy_timeout=5000");
|
|
@@ -22240,8 +22389,8 @@ function openAndMigrate(dbPath) {
|
|
|
22240
22389
|
return db;
|
|
22241
22390
|
}
|
|
22242
22391
|
function openGrantsDb(dbPath = DEFAULT_GRANTS_DB_PATH, opener = openAndMigrate) {
|
|
22243
|
-
const dir =
|
|
22244
|
-
|
|
22392
|
+
const dir = path4.dirname(dbPath);
|
|
22393
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
22245
22394
|
migrateLegacyGrantsDbLocation(dbPath);
|
|
22246
22395
|
try {
|
|
22247
22396
|
return opener(dbPath);
|
|
@@ -22265,6 +22414,7 @@ function canonicalizeApproverSet(approvers) {
|
|
|
22265
22414
|
}
|
|
22266
22415
|
|
|
22267
22416
|
// src/vault/approvals/kernel.ts
|
|
22417
|
+
var ALLOW_ONCE_CONSUMED_REASON = "allow_once_consumed";
|
|
22268
22418
|
function generateRequestId() {
|
|
22269
22419
|
return randomBytes5(16).toString("hex");
|
|
22270
22420
|
}
|
|
@@ -22391,6 +22541,38 @@ function evaluateDecisionRow(db, row, currentCanonical, opts, now) {
|
|
|
22391
22541
|
if (decision.decision === "deny") {
|
|
22392
22542
|
return { state: "denied", decision };
|
|
22393
22543
|
}
|
|
22544
|
+
if (decision.decision === "allow_once") {
|
|
22545
|
+
const burn = db.run(`UPDATE approval_decisions
|
|
22546
|
+
SET last_used_at = ?, revoked_at = ?, revoke_reason = ?
|
|
22547
|
+
WHERE id = ? AND revoked_at IS NULL`, [now, now, ALLOW_ONCE_CONSUMED_REASON, decision.id]);
|
|
22548
|
+
if ((burn.changes ?? 0) === 0) {
|
|
22549
|
+
audit(db, "deny", {
|
|
22550
|
+
agent_unit: decision.agent_unit,
|
|
22551
|
+
scope: decision.scope,
|
|
22552
|
+
action: decision.action,
|
|
22553
|
+
decision_id: decision.id,
|
|
22554
|
+
context: { reason: ALLOW_ONCE_CONSUMED_REASON }
|
|
22555
|
+
});
|
|
22556
|
+
return { state: "denied", decision };
|
|
22557
|
+
}
|
|
22558
|
+
decision.last_used_at = now;
|
|
22559
|
+
decision.revoked_at = now;
|
|
22560
|
+
decision.revoke_reason = ALLOW_ONCE_CONSUMED_REASON;
|
|
22561
|
+
audit(db, "match", {
|
|
22562
|
+
agent_unit: decision.agent_unit,
|
|
22563
|
+
scope: decision.scope,
|
|
22564
|
+
action: decision.action,
|
|
22565
|
+
decision_id: decision.id
|
|
22566
|
+
});
|
|
22567
|
+
audit(db, "revoke", {
|
|
22568
|
+
agent_unit: decision.agent_unit,
|
|
22569
|
+
scope: decision.scope,
|
|
22570
|
+
action: decision.action,
|
|
22571
|
+
decision_id: decision.id,
|
|
22572
|
+
context: { actor: "kernel", reason: ALLOW_ONCE_CONSUMED_REASON }
|
|
22573
|
+
});
|
|
22574
|
+
return { state: "granted", decision };
|
|
22575
|
+
}
|
|
22394
22576
|
if (decision.decision === "allow_ttl") {
|
|
22395
22577
|
const maxLifetime = opts.max_ttl_lifetime_ms ?? DEFAULT_MAX_TTL_LIFETIME_MS;
|
|
22396
22578
|
const hardCap = decision.granted_at + maxLifetime;
|
|
@@ -22459,7 +22641,8 @@ function consumeNonce(db, request_id, now = Date.now()) {
|
|
|
22459
22641
|
}
|
|
22460
22642
|
function recordDecision(db, input, now = Date.now()) {
|
|
22461
22643
|
const id = randomUUID();
|
|
22462
|
-
const
|
|
22644
|
+
const maxLifetime = input.max_ttl_lifetime_ms ?? DEFAULT_MAX_TTL_LIFETIME_MS;
|
|
22645
|
+
const ttl_expires_at = input.decision === "allow_ttl" && input.ttl_ms ? now + Math.min(input.ttl_ms, maxLifetime) : null;
|
|
22463
22646
|
const canonical = canonicalizeApproverSet(input.approver_set);
|
|
22464
22647
|
db.run(`INSERT INTO approval_decisions
|
|
22465
22648
|
(id, agent_unit, scope, action, decision,
|
|
@@ -23774,10 +23957,10 @@ class VaultBroker {
|
|
|
23774
23957
|
return;
|
|
23775
23958
|
}
|
|
23776
23959
|
try {
|
|
23777
|
-
const agentsDir = this.config ? resolveAgentsDir(this.config) :
|
|
23778
|
-
const tokenDir =
|
|
23960
|
+
const agentsDir = this.config ? resolveAgentsDir(this.config) : path5.join(os3.homedir(), ".switchroom", "agents");
|
|
23961
|
+
const tokenDir = path5.join(agentsDir, agent);
|
|
23779
23962
|
mkdirSync7(tokenDir, { recursive: true });
|
|
23780
|
-
const tokenPath =
|
|
23963
|
+
const tokenPath = path5.join(tokenDir, ".vault-token");
|
|
23781
23964
|
const tmpPath = `${tokenPath}.tmp.${process.pid}`;
|
|
23782
23965
|
writeFileSync3(tmpPath, mintResult.token, { mode: 384 });
|
|
23783
23966
|
renameSync8(tmpPath, tokenPath);
|
|
@@ -23839,8 +24022,8 @@ class VaultBroker {
|
|
|
23839
24022
|
try {
|
|
23840
24023
|
const row = this.grantsDb.query("SELECT agent_slug FROM vault_grants WHERE id = ?").get(id);
|
|
23841
24024
|
if (row && AgentNameSchema.safeParse(row.agent_slug).success) {
|
|
23842
|
-
const agentsDir = this.config ? resolveAgentsDir(this.config) :
|
|
23843
|
-
const tokenPath =
|
|
24025
|
+
const agentsDir = this.config ? resolveAgentsDir(this.config) : path5.join(os3.homedir(), ".switchroom", "agents");
|
|
24026
|
+
const tokenPath = path5.join(agentsDir, row.agent_slug, ".vault-token");
|
|
23844
24027
|
if (existsSync12(tokenPath)) {
|
|
23845
24028
|
try {
|
|
23846
24029
|
unlinkSync6(tokenPath);
|
|
@@ -24082,7 +24265,7 @@ class VaultBroker {
|
|
|
24082
24265
|
if (!existsSync12(filePath))
|
|
24083
24266
|
return false;
|
|
24084
24267
|
try {
|
|
24085
|
-
if (
|
|
24268
|
+
if (statSync7(filePath).size === 0)
|
|
24086
24269
|
return false;
|
|
24087
24270
|
} catch {
|
|
24088
24271
|
return false;
|
|
@@ -24161,12 +24344,12 @@ class VaultBroker {
|
|
|
24161
24344
|
}
|
|
24162
24345
|
function detectVaultLayoutDrift(vaultPath) {
|
|
24163
24346
|
const dir = dirname7(vaultPath);
|
|
24164
|
-
if (
|
|
24347
|
+
if (basename5(dir) !== "vault")
|
|
24165
24348
|
return;
|
|
24166
|
-
if (
|
|
24349
|
+
if (basename5(vaultPath) !== "vault.enc")
|
|
24167
24350
|
return;
|
|
24168
24351
|
const switchroomDir = dirname7(dir);
|
|
24169
|
-
if (
|
|
24352
|
+
if (basename5(switchroomDir) !== ".switchroom")
|
|
24170
24353
|
return;
|
|
24171
24354
|
const home2 = dirname7(switchroomDir);
|
|
24172
24355
|
const result = inspectVaultLayout(home2);
|
|
@@ -24195,7 +24378,7 @@ async function main() {
|
|
|
24195
24378
|
let perAgentTargets = [];
|
|
24196
24379
|
try {
|
|
24197
24380
|
if (existsSync12(perAgentDir)) {
|
|
24198
|
-
const entries =
|
|
24381
|
+
const entries = readdirSync5(perAgentDir, { withFileTypes: true });
|
|
24199
24382
|
const flat = [];
|
|
24200
24383
|
const subdirs = [];
|
|
24201
24384
|
for (const e of entries) {
|