switchroom 0.19.24 → 0.19.26
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 +20 -7
- package/dist/auth-broker/index.js +93 -28
- package/dist/cli/autoaccept-poll.js +0 -1
- package/dist/cli/drive-write-pretool.mjs +5 -0
- package/dist/cli/ms-365-write-pretool.mjs +5 -0
- package/dist/cli/notion-write-pretool.mjs +20 -6
- package/dist/cli/switchroom.js +3091 -1435
- package/dist/host-control/main.js +92 -29
- package/dist/vault/approvals/kernel-server.js +92 -28
- package/dist/vault/broker/server.js +258 -71
- package/examples/switchroom.yaml +1 -1
- package/package.json +1 -1
- package/profiles/_base/cron-session.sh.hbs +6 -0
- package/profiles/_base/start.sh.hbs +92 -17
- package/skills/switchroom-health/SKILL.md +19 -0
- package/skills/switchroom-status/SKILL.md +1 -1
- package/telegram-plugin/auth-snapshot-format.ts +9 -2
- package/telegram-plugin/dist/gateway/gateway.js +6981 -6747
- package/telegram-plugin/gateway/gateway.ts +53 -52
- package/telegram-plugin/gateway/periodic-sweep-guard.ts +86 -0
- package/telegram-plugin/gateway/status-pin-retarget.ts +144 -0
- package/telegram-plugin/quota-bar-format.ts +4 -1
- package/telegram-plugin/status-no-truncate.ts +49 -0
- package/telegram-plugin/status-pin-driver.ts +28 -0
- package/telegram-plugin/status-pin.ts +33 -4
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +42 -0
- package/telegram-plugin/tests/card-type-distinguishability.test.ts +268 -0
- package/telegram-plugin/tests/periodic-sweep-guard.test.ts +151 -0
- package/telegram-plugin/tests/pinned-card-collapse.test.ts +29 -18
- package/telegram-plugin/tests/quota-bar-format.test.ts +50 -0
- package/telegram-plugin/tests/secret-detect-false-positives.test.ts +1 -1
- package/telegram-plugin/tests/status-pin-retarget.test.ts +216 -0
- package/telegram-plugin/tests/status-pin-shutdown-wiring.test.ts +94 -0
- package/telegram-plugin/tests/status-pin-store.test.ts +87 -21
- package/telegram-plugin/tests/status-pin.test.ts +128 -2
- package/telegram-plugin/tests/worker-activity-feed.test.ts +10 -10
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +37 -21
- package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +1 -1
- package/telegram-plugin/tier-downgrade.ts +3 -2
- package/telegram-plugin/tool-activity-summary.ts +61 -18
- package/telegram-plugin/uat/assertions.ts +21 -2
- package/telegram-plugin/uat/feed-matcher.test.ts +29 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-channel.test.ts +9 -2
- package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-dm.test.ts +9 -2
- package/telegram-plugin/worker-activity-feed.ts +38 -17
- package/vendor/hindsight-memory/scripts/lib/config.py +61 -19
- package/vendor/hindsight-memory/scripts/lib/content.py +376 -1
- package/vendor/hindsight-memory/scripts/lib/english_words.txt +10799 -0
- package/vendor/hindsight-memory/scripts/recall.py +503 -252
- package/vendor/hindsight-memory/scripts/tests/test_recall_bank_slots.py +509 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_envelope_strip_telemetry.py +22 -5
- package/vendor/hindsight-memory/scripts/tests/test_recall_error_text.py +147 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_hook_budget.py +266 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +0 -401
- package/vendor/hindsight-memory/scripts/tests/test_recall_no_lexical_gate.py +261 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_query_shaping.py +473 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_request_timeout.py +241 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_transcript_fallback.py +25 -8
- package/vendor/hindsight-memory/tests/test_content.py +218 -0
|
@@ -4164,9 +4164,15 @@ 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
|
-
|
|
4167
|
+
hook_timeout_seconds: exports_external.number().int().min(1).optional().describe("Ceiling (seconds) Claude Code gives the UserPromptSubmit recall " + "hook before killing it. Stamped into the installed plugin's " + "hooks/hooks.json, so it survives `switchroom apply` reinstalling " + "the plugin. Default 12. Raising it lets slow banks finish at the " + "cost of pre-turn dead air; `parallel_deadline_seconds` and " + "`request_timeout_seconds` are both kept under it. A value below " + "3s is raised to 3s and reported: the fan-out deadline must be at " + "least 1s AND still leave 2s of post-deadline headroom, so a " + "lower ceiling admits no usable envelope at all."),
|
|
4168
|
+
parallel_deadline_seconds: exports_external.number().int().min(1).optional().describe("Shared deadline (seconds) for the whole parallel multi-bank " + "recall fan-out. Slots unfinished when it elapses are abandoned " + "and reported as timed out. Defaults to `hook_timeout_seconds` " + "minus 2s of headroom for block formatting, cache write and " + "stdout flush, so a straggler bank can never push the hook past " + "its ceiling. Set explicitly to override that derivation; a value " + "that would leave less than 2s under the hook ceiling — including " + "one set EQUAL to it — is clamped back to `hook_timeout_seconds` " + "minus 2, and the clamp is reported. Equality is not allowed: at " + "zero headroom the hook is killed mid-write and the turn loses " + "both the memories and the recall_log row explaining why."),
|
|
4169
|
+
query_max_tokens: exports_external.number().int().min(0).optional().describe("Cap on the number of DISTINCT BM25 terms the recall hook may " + "put on the wire. `recallMaxQueryChars` bounds characters, which " + "is not the cost driver: Hindsight OR-joins every query token " + "into one tsquery and Postgres native FTS ranks the entire " + "matched set before the top-60 heapsort, so cost tracks TERMS. " + "An 800-char composed query is ~96 distinct terms and matched " + "119,510 of 135,565 rows on the `overlord` bank (14.0s for the " + "3-arm " + "BM25 UNION), past the per-bank timeout — 96.8% of that agent's " + "own-bank recalls returned nothing. Plugin default is 24 " + "(measured 48,433 rows / 2.7s on the same bank). Terms are " + "chosen recency-first (the latest turn beats prior context), " + "then by selectivity. 0 disables shaping (rollback lever)."),
|
|
4170
|
+
query_stop_terms: exports_external.array(exports_external.string().min(1).regex(/^[\w./-]+$/)).optional().describe("Extra terms dropped from the BM25 recall query, on top of the " + "built-in English stopword list. For BANK-SPECIFIC " + "high-document-frequency words a generic stoplist cannot know " + "about: on `overlord`, `switchroom` matches 20% of the bank and " + "`agent` another 20%, purely because that is what the corpus is " + "about, and each such term drags tens of thousands of rows into " + "the ranking. Defaults to []."),
|
|
4171
|
+
request_timeout_seconds: exports_external.number().int().min(1).optional().describe("Per-bank HTTP read timeout, in seconds, for one recall " + "request. Even parallelised, each bank carries its own deadline " + "so ONE hung bank returns empty instead of consuming the shared " + "deadline and starving its siblings. The plugin default is 12 " + "(raised from a hardcoded 8 in #3757, which fired on 96.8% of " + "one agent's own-bank recalls). Switchroom defaults it to the " + "effective `parallel_deadline_seconds` instead — 10 at the " + "shipped ceiling — because the shared fan-out deadline is " + "already the tighter outer guard, so a per-bank value above it " + "can never bind. An explicitly configured value above the " + "effective deadline is clamped down to it, and the clamp is " + "reported."),
|
|
4172
|
+
own_bank_min_slots: exports_external.number().int().min(0).optional().describe("Slots inside `max_memories` reserved as a FLOOR for the agent's " + "own bank when recall fans out to more than one bank. The merged " + "set is sorted globally by relevance and head-sliced, which is " + "winner-take-all across banks: when both banks return more " + "candidates than the cap, one bank's score distribution can fill " + "every slot and the agent gets a dossier about its operator with " + "none of its own session memory. A floor, not a quota: at most " + "this many slots, only if the own bank returned that many, and " + "only up to HALF the cap shared with `additional_bank_min_slots` " + "— the rest is always won on pure relevance, so composition still " + "moves with the scores. Fixes score-based crowd-out only; a " + "timed-out bank returns no candidates and reservation is a no-op " + "there. 0 disables (default). Switchroom-managed agents use 2 " + "against the fleet-deployed cap of 6."),
|
|
4173
|
+
additional_bank_min_slots: exports_external.number().int().min(0).optional().describe("Slots inside `max_memories` reserved as a FLOOR for the " + "additional (profile / shared / sender) banks. Symmetric with " + "`own_bank_min_slots` — same floor-not-quota semantics, and the " + "two share the same half-of-cap reservation budget. When they sum " + "above that budget the own-bank floor is honoured first. 0 " + "disables (default). Switchroom-managed agents use 1 against the " + "fleet-deployed cap of 6. Observe `injected_own_bank_count` / " + "`injected_additional_bank_count` via " + "`switchroom memory recall-log`."),
|
|
4168
4174
|
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
|
-
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
|
|
4175
|
+
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 the `request_timeout_seconds` per-bank timeout " + "(defaults to the effective `parallel_deadline_seconds`, 10s at " + "the shipped ceiling) 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
4176
|
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`."),
|
|
4171
4177
|
skip_trivial: exports_external.boolean().optional().describe("Skip recall on plausibly-stateless trivial turns (time/date/" + "greeting). Switchroom default true — saves the recall arm + " + "injected tokens on turns that never need memory, guarded so it " + "never skips a turn that references user/project/session state. " + "Set false to always run recall."),
|
|
4172
4178
|
topic_filter_mode: exports_external.enum(["soft-preamble", "hard-filter"]).optional().describe("Supergroup-mode cross-topic memory behaviour. Default " + "(unset) → soft-preamble: recall returns memories from all " + "topics, and a 'Current topic: …' preamble tells the model " + "to self-scope. hard-filter: drop any recalled memory whose " + "metadata.thread_id differs from the active inbound's topic. " + "Flip to hard-filter when the recall_log shows binding " + "failures (model surfacing the right memory but applying " + "it to the wrong topic).")
|
|
@@ -4389,7 +4395,7 @@ var init_schema = __esm(() => {
|
|
|
4389
4395
|
reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent → uses global."),
|
|
4390
4396
|
consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent → global.")
|
|
4391
4397
|
}).optional().describe("LLM knob for the hindsight container. The flat `provider`/`model` set " + "the global default (backward-compatible); optional `retain`/`reflect`/" + "`consolidation` blocks override individual ops. All fields optional; " + "unset fields fall back to the hard-coded defaults."),
|
|
4392
|
-
env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY — a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS — a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED — a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
|
|
4398
|
+
env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_MAX_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY — a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP — the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS — a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED — a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
|
|
4393
4399
|
});
|
|
4394
4400
|
MicrosoftWorkspaceConfigSchema = exports_external.object({
|
|
4395
4401
|
microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL — omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
|
|
@@ -4525,7 +4531,13 @@ var init_schema = __esm(() => {
|
|
|
4525
4531
|
recall: exports_external.object({
|
|
4526
4532
|
max_memories: exports_external.number().int().min(0).optional(),
|
|
4527
4533
|
cache_ttl_secs: exports_external.number().int().min(0).optional(),
|
|
4528
|
-
|
|
4534
|
+
hook_timeout_seconds: exports_external.number().int().min(1).optional(),
|
|
4535
|
+
parallel_deadline_seconds: exports_external.number().int().min(1).optional(),
|
|
4536
|
+
query_max_tokens: exports_external.number().int().min(0).optional(),
|
|
4537
|
+
query_stop_terms: exports_external.array(exports_external.string().min(1).regex(/^[\w./-]+$/)).optional(),
|
|
4538
|
+
request_timeout_seconds: exports_external.number().int().min(1).optional(),
|
|
4539
|
+
own_bank_min_slots: exports_external.number().int().min(0).optional(),
|
|
4540
|
+
additional_bank_min_slots: exports_external.number().int().min(0).optional(),
|
|
4529
4541
|
additional_banks: exports_external.array(exports_external.string()).optional(),
|
|
4530
4542
|
sender_banks: exports_external.record(exports_external.string(), exports_external.string()).optional()
|
|
4531
4543
|
}).optional()
|
|
@@ -4559,7 +4571,8 @@ var init_schema = __esm(() => {
|
|
|
4559
4571
|
memory: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/, "memory must be a Docker size string like '6g', '512m', '1.5g'").optional().describe("Hard memory cap (Docker `mem_limit` → cgroup memory.max). When the " + "container exceeds this, the kernel OOM-kills processes in the cgroup. " + "Format: '6g', '1.5g', '512m'. When unset at every cascade layer the " + "compose generator falls back to the hard-coded per-profile defaults " + "in src/agents/compose.ts (klanker 6g, coding 2g, conversational 1.5g, " + "lightweight 1g, default 1.5g)."),
|
|
4560
4572
|
memory_reservation: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/, "memory_reservation must be a Docker size string like '4g', '256m'").optional().describe("Soft memory floor (Docker `mem_reservation` → cgroup memory.low). " + "Under host-wide memory pressure, the kernel protects at least this " + "much from being reclaimed from the cgroup. Must be ≤ memory. Use to " + "keep an agent RAM-resident when the host has other tenants that " + "might push the box (Coolify apps, build jobs). Default: unset."),
|
|
4561
4573
|
pids_limit: exports_external.number().int().positive().optional().describe("Max processes the cgroup can spawn (cgroup pids.max). Prevents " + "fork bombs and runaway test runners. Counts every process in the " + "cgroup including bash subprocesses, claude itself, sidecars, and " + "any test/build worker. A typical agent at idle uses ~30 PIDs; " + "`npm test`-style workloads can spike to 200+. Set generously " + "(2000 is a comfortable cap for test-running agents). Default: " + "unset (no cgroup pid cap)."),
|
|
4562
|
-
cpus: exports_external.number().positive().optional().describe("CPU quota (Docker `cpus`). Fractional values OK (e.g. 0.5, 2.0). " + "When unset at every cascade layer the compose generator falls " + "back to the per-profile default (klanker/coding 2.0, default 1.0, " + "lightweight 0.5).")
|
|
4574
|
+
cpus: exports_external.number().positive().optional().describe("CPU quota (Docker `cpus`). Fractional values OK (e.g. 0.5, 2.0). " + "When unset at every cascade layer the compose generator falls " + "back to the per-profile default (klanker/coding 2.0, default 1.0, " + "lightweight 0.5)."),
|
|
4575
|
+
tmp_size: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/, "tmp_size must be a Docker size string like '1g', '4g', '512m'").refine((v) => parseFloat(v) > 0, "tmp_size must be greater than zero").optional().describe("Size of the agent container's RAM-backed `/tmp` (Docker " + "`tmpfs: - /tmp:size=<this>,mode=1777`). The container root FS is " + "read-only, so /tmp is the only scratch space the agent and its " + "sub-agents get for repo clones and toolchain caches (bunx/npx/pip) " + "— a fan-out of several sub-agents exhausts the 1g default. A tmpfs " + "only consumes host RAM for the pages actually written, so raising " + "the ceiling costs nothing until it is used; those pages are still " + "charged to the container's `memory` cap, so raise both together. " + "Format: '1g', '4g', '512m'. Default when unset at every cascade " + "layer: '1g'.")
|
|
4563
4576
|
}).optional().describe("Per-agent resource limits. Cascades through defaults → profile → " + "per-agent with per-field merge (agent wins on each field independently). " + "Any field left unset at every layer falls back to the hard-coded " + "per-profile defaults in src/agents/compose.ts."),
|
|
4564
4577
|
experimental: exports_external.object({
|
|
4565
4578
|
legacy_pty: exports_external.boolean().optional().describe("Opt out of the default tmux supervisor (#725) and run the agent under " + "the legacy PTY supervisor instead. Default: false (tmux is the default)."),
|
|
@@ -4652,7 +4665,8 @@ var init_schema = __esm(() => {
|
|
|
4652
4665
|
memory: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/).optional(),
|
|
4653
4666
|
memory_reservation: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/).optional(),
|
|
4654
4667
|
pids_limit: exports_external.number().int().positive().optional(),
|
|
4655
|
-
cpus: exports_external.number().positive().optional()
|
|
4668
|
+
cpus: exports_external.number().positive().optional(),
|
|
4669
|
+
tmp_size: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/, "tmp_size must be a Docker size string like '1g', '4g', '512m'").refine((v) => parseFloat(v) > 0, "tmp_size must be greater than zero").optional()
|
|
4656
4670
|
}).optional()
|
|
4657
4671
|
}).superRefine((agent, ctx) => {
|
|
4658
4672
|
if (agent.dm_only !== true)
|
|
@@ -18225,7 +18239,7 @@ var require_lib = __commonJS((exports, module) => {
|
|
|
18225
18239
|
|
|
18226
18240
|
// src/vault/broker/server.ts
|
|
18227
18241
|
import * as net from "node:net";
|
|
18228
|
-
import { mkdirSync as
|
|
18242
|
+
import { mkdirSync as mkdirSync8, chmodSync as chmodSync5, chownSync as chownSync2, existsSync as existsSync12, readFileSync as readFileSync12, readdirSync as readdirSync5, statSync as statSync7, unlinkSync as unlinkSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
18229
18243
|
|
|
18230
18244
|
// src/agents/compose.ts
|
|
18231
18245
|
init_schema();
|
|
@@ -18581,6 +18595,10 @@ function allocateAgentUid(name) {
|
|
|
18581
18595
|
return AGENT_UID_MIN + u32 % range;
|
|
18582
18596
|
}
|
|
18583
18597
|
|
|
18598
|
+
// src/setup/hindsight-recall-tunables.ts
|
|
18599
|
+
var RECALL_DEADLINE_HEADROOM_SECONDS = 2;
|
|
18600
|
+
var MIN_RECALL_HOOK_TIMEOUT_SECONDS = RECALL_DEADLINE_HEADROOM_SECONDS + 1;
|
|
18601
|
+
|
|
18584
18602
|
// src/agents/scaffold.ts
|
|
18585
18603
|
init_schema();
|
|
18586
18604
|
|
|
@@ -18867,6 +18885,27 @@ function assertPositive(value, label) {
|
|
|
18867
18885
|
// src/setup/host-capabilities.ts
|
|
18868
18886
|
init_paths();
|
|
18869
18887
|
|
|
18888
|
+
// src/setup/hindsight-pg-defaults.ts
|
|
18889
|
+
var HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION = 8 * 1024;
|
|
18890
|
+
var HINDSIGHT_PG_APP_ANON_MIB = 2560;
|
|
18891
|
+
var HINDSIGHT_PG_PAGE_CACHE_FLOOR_MIB = 2048;
|
|
18892
|
+
var HINDSIGHT_PG_SHARED_BUFFERS_BUDGET_MIB = HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION - HINDSIGHT_PG_APP_ANON_MIB - HINDSIGHT_PG_PAGE_CACHE_FLOOR_MIB;
|
|
18893
|
+
var HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB = 1536;
|
|
18894
|
+
var HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB = 4096;
|
|
18895
|
+
function pgMib(mib) {
|
|
18896
|
+
return `${mib}MB`;
|
|
18897
|
+
}
|
|
18898
|
+
var HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE_ENV = "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE";
|
|
18899
|
+
var HINDSIGHT_PG_SHARED_BUFFERS_ENV = "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS";
|
|
18900
|
+
var HINDSIGHT_PG_DEFAULTS = [
|
|
18901
|
+
[
|
|
18902
|
+
HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE_ENV,
|
|
18903
|
+
pgMib(HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB)
|
|
18904
|
+
],
|
|
18905
|
+
[HINDSIGHT_PG_SHARED_BUFFERS_ENV, pgMib(HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB)]
|
|
18906
|
+
];
|
|
18907
|
+
var HINDSIGHT_PG_ENV_KEYS = new Set(HINDSIGHT_PG_DEFAULTS.map(([k]) => k));
|
|
18908
|
+
|
|
18870
18909
|
// src/setup/hindsight-perf-defaults.ts
|
|
18871
18910
|
var HINDSIGHT_RERANKER_MAX_CANDIDATES_FOR_DERIVATION = 150;
|
|
18872
18911
|
var HINDSIGHT_DEFAULT_RECALL_MAX_CANDIDATES_PER_SOURCE = Math.ceil(HINDSIGHT_RERANKER_MAX_CANDIDATES_FOR_DERIVATION * 0.4);
|
|
@@ -18881,6 +18920,15 @@ var HINDSIGHT_DEFAULT_RERANKER_LOCAL_BATCH_SIZE = 128;
|
|
|
18881
18920
|
var HINDSIGHT_DEFAULT_LLM_STRICT_SCHEMA = "true";
|
|
18882
18921
|
var HINDSIGHT_DEFAULT_LLM_MAX_RETRIES = 2;
|
|
18883
18922
|
var HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM = 2;
|
|
18923
|
+
var HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE = 1000;
|
|
18924
|
+
var HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING = "true";
|
|
18925
|
+
var HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES = 150;
|
|
18926
|
+
var HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT = 4;
|
|
18927
|
+
var HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8;
|
|
18928
|
+
var HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S = 600;
|
|
18929
|
+
var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND = 500;
|
|
18930
|
+
var HINDSIGHT_DEFAULT_CONSOLIDATION_SLOT_LIMIT = 6;
|
|
18931
|
+
var HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS = 1;
|
|
18884
18932
|
var HINDSIGHT_PERF_DEFAULTS_UNGATED = [
|
|
18885
18933
|
[
|
|
18886
18934
|
"HINDSIGHT_API_RECALL_MAX_CANDIDATES_PER_SOURCE",
|
|
@@ -18898,6 +18946,42 @@ var HINDSIGHT_PERF_DEFAULTS_UNGATED = [
|
|
|
18898
18946
|
[
|
|
18899
18947
|
"HINDSIGHT_API_CONSOLIDATION_LLM_PARALLELISM",
|
|
18900
18948
|
String(HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_PARALLELISM)
|
|
18949
|
+
],
|
|
18950
|
+
[
|
|
18951
|
+
"HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE",
|
|
18952
|
+
String(HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE)
|
|
18953
|
+
],
|
|
18954
|
+
[
|
|
18955
|
+
"HINDSIGHT_API_RERANKER_LOCAL_BUCKET_BATCHING",
|
|
18956
|
+
HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING
|
|
18957
|
+
],
|
|
18958
|
+
[
|
|
18959
|
+
"HINDSIGHT_API_RERANKER_MAX_CANDIDATES",
|
|
18960
|
+
String(HINDSIGHT_DEFAULT_RERANKER_MAX_CANDIDATES)
|
|
18961
|
+
],
|
|
18962
|
+
[
|
|
18963
|
+
"HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT",
|
|
18964
|
+
String(HINDSIGHT_DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT)
|
|
18965
|
+
],
|
|
18966
|
+
[
|
|
18967
|
+
"HINDSIGHT_API_RECALL_MAX_CONCURRENT",
|
|
18968
|
+
String(HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT)
|
|
18969
|
+
],
|
|
18970
|
+
[
|
|
18971
|
+
"HINDSIGHT_API_REFLECT_WALL_TIMEOUT",
|
|
18972
|
+
String(HINDSIGHT_DEFAULT_REFLECT_WALL_TIMEOUT_S)
|
|
18973
|
+
],
|
|
18974
|
+
[
|
|
18975
|
+
"HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLOTS",
|
|
18976
|
+
String(HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_SLOTS)
|
|
18977
|
+
],
|
|
18978
|
+
[
|
|
18979
|
+
"HINDSIGHT_API_WORKER_CONSOLIDATION_SLOT_LIMIT",
|
|
18980
|
+
String(HINDSIGHT_DEFAULT_CONSOLIDATION_SLOT_LIMIT)
|
|
18981
|
+
],
|
|
18982
|
+
[
|
|
18983
|
+
"HINDSIGHT_API_CONSOLIDATION_MAX_MEMORIES_PER_ROUND",
|
|
18984
|
+
String(HINDSIGHT_DEFAULT_CONSOLIDATION_MAX_MEMORIES_PER_ROUND)
|
|
18901
18985
|
]
|
|
18902
18986
|
];
|
|
18903
18987
|
var HINDSIGHT_PERF_DEFAULTS_GPU = [
|
|
@@ -18921,7 +19005,8 @@ var HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM = [
|
|
|
18921
19005
|
["HINDSIGHT_API_LLM_MAX_RETRIES", String(HINDSIGHT_DEFAULT_LLM_MAX_RETRIES)]
|
|
18922
19006
|
];
|
|
18923
19007
|
var HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
|
|
18924
|
-
"HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY"
|
|
19008
|
+
"HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY",
|
|
19009
|
+
"HINDSIGHT_CE_DECISIVE_RELATIVE_GAP"
|
|
18925
19010
|
]);
|
|
18926
19011
|
var HINDSIGHT_PERF_ENV_KEYS = new Set([
|
|
18927
19012
|
...[
|
|
@@ -18932,27 +19017,6 @@ var HINDSIGHT_PERF_ENV_KEYS = new Set([
|
|
|
18932
19017
|
...HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS
|
|
18933
19018
|
]);
|
|
18934
19019
|
|
|
18935
|
-
// src/setup/hindsight-pg-defaults.ts
|
|
18936
|
-
var HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION = 8 * 1024;
|
|
18937
|
-
var HINDSIGHT_PG_APP_ANON_MIB = 2560;
|
|
18938
|
-
var HINDSIGHT_PG_PAGE_CACHE_FLOOR_MIB = 2048;
|
|
18939
|
-
var HINDSIGHT_PG_SHARED_BUFFERS_BUDGET_MIB = HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION - HINDSIGHT_PG_APP_ANON_MIB - HINDSIGHT_PG_PAGE_CACHE_FLOOR_MIB;
|
|
18940
|
-
var HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB = 1536;
|
|
18941
|
-
var HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB = 4096;
|
|
18942
|
-
function pgMib(mib) {
|
|
18943
|
-
return `${mib}MB`;
|
|
18944
|
-
}
|
|
18945
|
-
var HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE_ENV = "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE";
|
|
18946
|
-
var HINDSIGHT_PG_SHARED_BUFFERS_ENV = "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS";
|
|
18947
|
-
var HINDSIGHT_PG_DEFAULTS = [
|
|
18948
|
-
[
|
|
18949
|
-
HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE_ENV,
|
|
18950
|
-
pgMib(HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB)
|
|
18951
|
-
],
|
|
18952
|
-
[HINDSIGHT_PG_SHARED_BUFFERS_ENV, pgMib(HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB)]
|
|
18953
|
-
];
|
|
18954
|
-
var HINDSIGHT_PG_ENV_KEYS = new Set(HINDSIGHT_PG_DEFAULTS.map(([k]) => k));
|
|
18955
|
-
|
|
18956
19020
|
// src/setup/hindsight-context-budget.ts
|
|
18957
19021
|
var HINDSIGHT_UPSTREAM_RETAIN_CHUNK_SIZE = 3000;
|
|
18958
19022
|
var HINDSIGHT_RETAIN_MAX_COMPLETION_FLOOR = HINDSIGHT_UPSTREAM_RETAIN_CHUNK_SIZE + 72;
|
|
@@ -19892,6 +19956,73 @@ function migrateLegacyGrantsDbLocation(newDbPath = getGrantsDbPath(), deps) {
|
|
|
19892
19956
|
// src/agents/compose.ts
|
|
19893
19957
|
var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
|
|
19894
19958
|
|
|
19959
|
+
// src/vault/broker/write-token-file.ts
|
|
19960
|
+
import { chownSync, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
|
|
19961
|
+
import { join as join6 } from "node:path";
|
|
19962
|
+
class TokenFileWriteError extends Error {
|
|
19963
|
+
tokenPath;
|
|
19964
|
+
constructor(tokenPath, message, options) {
|
|
19965
|
+
super(message, options);
|
|
19966
|
+
this.name = "TokenFileWriteError";
|
|
19967
|
+
this.tokenPath = tokenPath;
|
|
19968
|
+
}
|
|
19969
|
+
}
|
|
19970
|
+
function writeAgentTokenFile(args, deps = {}) {
|
|
19971
|
+
const { agentsDir, agent, token } = args;
|
|
19972
|
+
const mkdir = deps.mkdir ?? ((p) => {
|
|
19973
|
+
mkdirSync4(p, { recursive: true });
|
|
19974
|
+
});
|
|
19975
|
+
const writeFile = deps.writeFile ?? ((p, d) => {
|
|
19976
|
+
writeFileSync2(p, d, { mode: 384 });
|
|
19977
|
+
});
|
|
19978
|
+
const readFile = deps.readFile ?? ((p) => readFileSync7(p, "utf8"));
|
|
19979
|
+
const chown = deps.chown ?? ((p, uid, gid) => {
|
|
19980
|
+
chownSync(p, uid, gid);
|
|
19981
|
+
});
|
|
19982
|
+
const agentUid = deps.agentUid ?? allocateAgentUid;
|
|
19983
|
+
const tokenDir = join6(agentsDir, agent);
|
|
19984
|
+
const tokenPath = join6(tokenDir, ".vault-token");
|
|
19985
|
+
try {
|
|
19986
|
+
mkdir(tokenDir);
|
|
19987
|
+
} catch (err) {
|
|
19988
|
+
throw new TokenFileWriteError(tokenPath, `cannot create agent token dir ${tokenDir}: ${errText(err)}`, { cause: err });
|
|
19989
|
+
}
|
|
19990
|
+
let preExisting;
|
|
19991
|
+
try {
|
|
19992
|
+
readFile(tokenPath);
|
|
19993
|
+
preExisting = true;
|
|
19994
|
+
} catch {
|
|
19995
|
+
preExisting = false;
|
|
19996
|
+
}
|
|
19997
|
+
try {
|
|
19998
|
+
writeFile(tokenPath, token);
|
|
19999
|
+
} catch (err) {
|
|
20000
|
+
throw new TokenFileWriteError(tokenPath, `cannot write token file ${tokenPath}: ${errText(err)}`, { cause: err });
|
|
20001
|
+
}
|
|
20002
|
+
let onDisk;
|
|
20003
|
+
try {
|
|
20004
|
+
onDisk = readFile(tokenPath);
|
|
20005
|
+
} catch (err) {
|
|
20006
|
+
throw new TokenFileWriteError(tokenPath, `token file ${tokenPath} is unreadable after write: ${errText(err)}`, { cause: err });
|
|
20007
|
+
}
|
|
20008
|
+
if (onDisk !== token) {
|
|
20009
|
+
throw new TokenFileWriteError(tokenPath, `token file ${tokenPath} did not verify after write ` + `(${onDisk.length} bytes on disk, ${token.length} expected)`);
|
|
20010
|
+
}
|
|
20011
|
+
let chownWarning = null;
|
|
20012
|
+
if (!preExisting) {
|
|
20013
|
+
try {
|
|
20014
|
+
const uid = agentUid(agent);
|
|
20015
|
+
chown(tokenPath, uid, uid);
|
|
20016
|
+
} catch (err) {
|
|
20017
|
+
chownWarning = `token file ${tokenPath} created but chown to the agent UID failed: ` + `${errText(err)} (CAP_CHOWN missing?)`;
|
|
20018
|
+
}
|
|
20019
|
+
}
|
|
20020
|
+
return { tokenPath, chownWarning };
|
|
20021
|
+
}
|
|
20022
|
+
function errText(err) {
|
|
20023
|
+
return err instanceof Error ? err.message : String(err);
|
|
20024
|
+
}
|
|
20025
|
+
|
|
19895
20026
|
// src/vault/broker/server.ts
|
|
19896
20027
|
import { dirname as dirname7, resolve as resolve9, basename as basename5 } from "node:path";
|
|
19897
20028
|
import * as os3 from "node:os";
|
|
@@ -19904,23 +20035,23 @@ import {
|
|
|
19904
20035
|
existsSync as existsSync8,
|
|
19905
20036
|
fsyncSync as fsyncSync4,
|
|
19906
20037
|
lstatSync as lstatSync2,
|
|
19907
|
-
mkdirSync as
|
|
20038
|
+
mkdirSync as mkdirSync5,
|
|
19908
20039
|
openSync as openSync4,
|
|
19909
20040
|
closeSync as closeSync4,
|
|
19910
|
-
readFileSync as
|
|
20041
|
+
readFileSync as readFileSync8,
|
|
19911
20042
|
renameSync as renameSync4,
|
|
19912
20043
|
statSync as statSync4,
|
|
19913
20044
|
symlinkSync,
|
|
19914
20045
|
unlinkSync as unlinkSync3
|
|
19915
20046
|
} from "node:fs";
|
|
19916
20047
|
import { createHash as createHash2 } from "node:crypto";
|
|
19917
|
-
import { basename as basename3, dirname as dirname4, join as
|
|
20048
|
+
import { basename as basename3, dirname as dirname4, join as join7 } from "node:path";
|
|
19918
20049
|
function vaultLayoutPaths(home2) {
|
|
19919
|
-
const switchroomRoot =
|
|
20050
|
+
const switchroomRoot = join7(home2, ".switchroom");
|
|
19920
20051
|
return {
|
|
19921
|
-
oldPath:
|
|
19922
|
-
newPath:
|
|
19923
|
-
parent:
|
|
20052
|
+
oldPath: join7(switchroomRoot, "vault.enc"),
|
|
20053
|
+
newPath: join7(switchroomRoot, "vault", "vault.enc"),
|
|
20054
|
+
parent: join7(switchroomRoot, "vault"),
|
|
19924
20055
|
switchroomRoot
|
|
19925
20056
|
};
|
|
19926
20057
|
}
|
|
@@ -19969,7 +20100,7 @@ function runMigration(home2, opts) {
|
|
|
19969
20100
|
if (oldStat?.isFile() && !newExists) {
|
|
19970
20101
|
if (opts.dryRun)
|
|
19971
20102
|
return { kind: "migrated" };
|
|
19972
|
-
|
|
20103
|
+
mkdirSync5(parent, { recursive: true, mode: 448 });
|
|
19973
20104
|
const tempNew = `${newPath}.tmp`;
|
|
19974
20105
|
copyFileSync2(oldPath, tempNew);
|
|
19975
20106
|
chmodSync2(tempNew, 384);
|
|
@@ -19997,11 +20128,11 @@ function lstatSyncOrNull(path2) {
|
|
|
19997
20128
|
}
|
|
19998
20129
|
}
|
|
19999
20130
|
function sha256File(path2) {
|
|
20000
|
-
const data =
|
|
20131
|
+
const data = readFileSync8(path2);
|
|
20001
20132
|
return createHash2("sha256").update(data).digest("hex");
|
|
20002
20133
|
}
|
|
20003
20134
|
function atomicReplaceWithSymlink(target, linkTarget) {
|
|
20004
|
-
const tmp =
|
|
20135
|
+
const tmp = join7(dirname4(target), `.${basename3(target)}.symlink-tmp`);
|
|
20005
20136
|
if (existsSync8(tmp)) {
|
|
20006
20137
|
try {
|
|
20007
20138
|
unlinkSync3(tmp);
|
|
@@ -20037,9 +20168,9 @@ import {
|
|
|
20037
20168
|
closeSync as closeSync5,
|
|
20038
20169
|
existsSync as existsSync9,
|
|
20039
20170
|
fsyncSync as fsyncSync5,
|
|
20040
|
-
mkdirSync as
|
|
20171
|
+
mkdirSync as mkdirSync6,
|
|
20041
20172
|
openSync as openSync5,
|
|
20042
|
-
readFileSync as
|
|
20173
|
+
readFileSync as readFileSync9,
|
|
20043
20174
|
renameSync as renameSync5,
|
|
20044
20175
|
unlinkSync as unlinkSync4,
|
|
20045
20176
|
writeSync as writeSync4
|
|
@@ -20078,7 +20209,7 @@ function readMachineId() {
|
|
|
20078
20209
|
}
|
|
20079
20210
|
for (const path2 of [MACHINE_ID_PRIMARY, MACHINE_ID_FALLBACK]) {
|
|
20080
20211
|
try {
|
|
20081
|
-
const id =
|
|
20212
|
+
const id = readFileSync9(path2, "utf8").trim();
|
|
20082
20213
|
if (id.length > 0)
|
|
20083
20214
|
return id;
|
|
20084
20215
|
} catch {}
|
|
@@ -20120,7 +20251,7 @@ function readAutoUnlockFile(filePath) {
|
|
|
20120
20251
|
}
|
|
20121
20252
|
let blob;
|
|
20122
20253
|
try {
|
|
20123
|
-
blob =
|
|
20254
|
+
blob = readFileSync9(filePath);
|
|
20124
20255
|
} catch {
|
|
20125
20256
|
throw new AutoUnlockDecryptError("io");
|
|
20126
20257
|
}
|
|
@@ -20201,7 +20332,7 @@ function seedChain(path2) {
|
|
|
20201
20332
|
// src/vault/broker/test-isolation-guard.ts
|
|
20202
20333
|
import { mkdtempSync } from "node:fs";
|
|
20203
20334
|
import { homedir as homedir5, tmpdir } from "node:os";
|
|
20204
|
-
import { join as
|
|
20335
|
+
import { join as join8, resolve as resolve8, sep } from "node:path";
|
|
20205
20336
|
function isTestRuntime() {
|
|
20206
20337
|
return process.env.VITEST !== undefined;
|
|
20207
20338
|
}
|
|
@@ -20222,7 +20353,7 @@ function safeVaultPath(requestedPath) {
|
|
|
20222
20353
|
return requestedPath;
|
|
20223
20354
|
if (!isUnderRealSwitchroomHome(requestedPath))
|
|
20224
20355
|
return requestedPath;
|
|
20225
|
-
const redirected =
|
|
20356
|
+
const redirected = join8(mkdtempSync(join8(tmpdir(), "sw-test-vault-")), "vault.enc");
|
|
20226
20357
|
if (!warnedVault) {
|
|
20227
20358
|
warnedVault = true;
|
|
20228
20359
|
process.stderr.write(`[test-isolation guard] vault path redirected away from the ` + `production tree to an isolated tmpdir — a vault/broker test ` + `resolved its vault path inside ~/.switchroom. See CLAUDE.md ` + `"Vault & shared-state test discipline".
|
|
@@ -20238,7 +20369,7 @@ function safeAuditLogPath(requestedPath) {
|
|
|
20238
20369
|
if (!isUnderRealSwitchroomHome(requestedPath))
|
|
20239
20370
|
return requestedPath;
|
|
20240
20371
|
if (!cachedTmpAuditLog) {
|
|
20241
|
-
cachedTmpAuditLog =
|
|
20372
|
+
cachedTmpAuditLog = join8(mkdtempSync(join8(tmpdir(), "sw-test-audit-")), "vault-audit.log");
|
|
20242
20373
|
}
|
|
20243
20374
|
if (!warnedAudit) {
|
|
20244
20375
|
warnedAudit = true;
|
|
@@ -22384,6 +22515,37 @@ function listGrants(db, agent_slug) {
|
|
|
22384
22515
|
return rows.map(rowToGrant);
|
|
22385
22516
|
}
|
|
22386
22517
|
|
|
22518
|
+
// src/vault/broker/reap-token-file.ts
|
|
22519
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "node:fs";
|
|
22520
|
+
import { join as join11 } from "node:path";
|
|
22521
|
+
var REAPABLE_REASONS = new Set(["grant-invalid", "grant-expired"]);
|
|
22522
|
+
function reapUnusableGrantToken(args, deps = {}) {
|
|
22523
|
+
const { agentsDir, agent, presentedToken, reason } = args;
|
|
22524
|
+
if (!REAPABLE_REASONS.has(reason))
|
|
22525
|
+
return "kept:reason-not-reapable";
|
|
22526
|
+
if (!agent)
|
|
22527
|
+
return "kept:no-agent-identity";
|
|
22528
|
+
const readFile = deps.readFile ?? ((p) => readFileSync11(p, "utf8"));
|
|
22529
|
+
const writeFile = deps.writeFile ?? ((p, d) => writeFileSync4(p, d, { mode: 384 }));
|
|
22530
|
+
const tokenPath = join11(agentsDir, agent, ".vault-token");
|
|
22531
|
+
let onDisk;
|
|
22532
|
+
try {
|
|
22533
|
+
onDisk = readFile(tokenPath);
|
|
22534
|
+
} catch {
|
|
22535
|
+
return "kept:nothing-on-disk";
|
|
22536
|
+
}
|
|
22537
|
+
if (onDisk.trim() === "")
|
|
22538
|
+
return "kept:nothing-on-disk";
|
|
22539
|
+
if (onDisk.trim() !== presentedToken.trim())
|
|
22540
|
+
return "kept:superseded";
|
|
22541
|
+
try {
|
|
22542
|
+
writeFile(tokenPath, "");
|
|
22543
|
+
} catch {
|
|
22544
|
+
return "kept:io-error";
|
|
22545
|
+
}
|
|
22546
|
+
return "reaped";
|
|
22547
|
+
}
|
|
22548
|
+
|
|
22387
22549
|
// src/vault/admin-only-keys.ts
|
|
22388
22550
|
function globToRegExp(pattern) {
|
|
22389
22551
|
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
@@ -22945,7 +23107,7 @@ class VaultBroker {
|
|
|
22945
23107
|
}
|
|
22946
23108
|
process.umask(63);
|
|
22947
23109
|
const parentDir = dirname7(this.socketPath);
|
|
22948
|
-
|
|
23110
|
+
mkdirSync8(parentDir, { recursive: true, mode: 448 });
|
|
22949
23111
|
try {
|
|
22950
23112
|
chmodSync5(parentDir, 448);
|
|
22951
23113
|
} catch {}
|
|
@@ -22985,7 +23147,7 @@ class VaultBroker {
|
|
|
22985
23147
|
return;
|
|
22986
23148
|
try {
|
|
22987
23149
|
if (ready) {
|
|
22988
|
-
|
|
23150
|
+
writeFileSync5(p, "", { mode: 384 });
|
|
22989
23151
|
} else if (existsSync12(p)) {
|
|
22990
23152
|
unlinkSync6(p);
|
|
22991
23153
|
}
|
|
@@ -23063,7 +23225,7 @@ class VaultBroker {
|
|
|
23063
23225
|
const dir = abs.slice(0, -"/sock".length);
|
|
23064
23226
|
if (existsSync12(dir)) {
|
|
23065
23227
|
try {
|
|
23066
|
-
|
|
23228
|
+
chownSync2(dir, 0, 0);
|
|
23067
23229
|
} catch {}
|
|
23068
23230
|
try {
|
|
23069
23231
|
chmodSync5(dir, 448);
|
|
@@ -23091,12 +23253,12 @@ class VaultBroker {
|
|
|
23091
23253
|
try {
|
|
23092
23254
|
agentUid = allocateAgentUid(agentName);
|
|
23093
23255
|
try {
|
|
23094
|
-
|
|
23256
|
+
chownSync2(abs, agentUid, agentUid);
|
|
23095
23257
|
} catch {}
|
|
23096
23258
|
if (abs.endsWith("/sock")) {
|
|
23097
23259
|
const dir = abs.slice(0, -"/sock".length);
|
|
23098
23260
|
try {
|
|
23099
|
-
|
|
23261
|
+
chownSync2(dir, agentUid, agentUid);
|
|
23100
23262
|
} catch {}
|
|
23101
23263
|
}
|
|
23102
23264
|
} catch {}
|
|
@@ -23123,7 +23285,7 @@ class VaultBroker {
|
|
|
23123
23285
|
} catch {}
|
|
23124
23286
|
if (agentUid !== null) {
|
|
23125
23287
|
try {
|
|
23126
|
-
|
|
23288
|
+
chownSync2(unlockAbs, agentUid, agentUid);
|
|
23127
23289
|
} catch {}
|
|
23128
23290
|
}
|
|
23129
23291
|
this.agentServers.set(unlockAbs, { server: unlockServer, agentName });
|
|
@@ -23158,7 +23320,7 @@ class VaultBroker {
|
|
|
23158
23320
|
return;
|
|
23159
23321
|
try {
|
|
23160
23322
|
if (existsSync12(this.vaultPath))
|
|
23161
|
-
|
|
23323
|
+
chownSync2(this.vaultPath, uid, uid);
|
|
23162
23324
|
} catch {}
|
|
23163
23325
|
}
|
|
23164
23326
|
bindOperatorListener(socketPath, operatorUid) {
|
|
@@ -23172,7 +23334,7 @@ class VaultBroker {
|
|
|
23172
23334
|
const dir = abs.slice(0, -"/sock".length);
|
|
23173
23335
|
if (existsSync12(dir)) {
|
|
23174
23336
|
try {
|
|
23175
|
-
|
|
23337
|
+
chownSync2(dir, 0, 0);
|
|
23176
23338
|
} catch {}
|
|
23177
23339
|
try {
|
|
23178
23340
|
chmodSync5(dir, 448);
|
|
@@ -23196,7 +23358,7 @@ class VaultBroker {
|
|
|
23196
23358
|
chmodSync5(abs, 384);
|
|
23197
23359
|
} catch {}
|
|
23198
23360
|
try {
|
|
23199
|
-
|
|
23361
|
+
chownSync2(abs, operatorUid, operatorUid);
|
|
23200
23362
|
} catch {}
|
|
23201
23363
|
const unlockServer = net.createServer((sock) => {
|
|
23202
23364
|
this._handleUnlockConnection(sock, true);
|
|
@@ -23207,12 +23369,12 @@ class VaultBroker {
|
|
|
23207
23369
|
chmodSync5(unlockAbs, 384);
|
|
23208
23370
|
} catch {}
|
|
23209
23371
|
try {
|
|
23210
|
-
|
|
23372
|
+
chownSync2(unlockAbs, operatorUid, operatorUid);
|
|
23211
23373
|
} catch {}
|
|
23212
23374
|
if (abs.endsWith("/sock")) {
|
|
23213
23375
|
const dir = abs.slice(0, -"/sock".length);
|
|
23214
23376
|
try {
|
|
23215
|
-
|
|
23377
|
+
chownSync2(dir, operatorUid, operatorUid);
|
|
23216
23378
|
} catch {}
|
|
23217
23379
|
try {
|
|
23218
23380
|
chmodSync5(dir, 448);
|
|
@@ -23514,6 +23676,14 @@ class VaultBroker {
|
|
|
23514
23676
|
socket.write(encodeResponse(errorResponse("DENIED", "grant-revoked")));
|
|
23515
23677
|
return;
|
|
23516
23678
|
}
|
|
23679
|
+
if (!grantResult.ok) {
|
|
23680
|
+
reapUnusableGrantToken({
|
|
23681
|
+
agentsDir: this.config ? resolveAgentsDir(this.config) : path5.join(os3.homedir(), ".switchroom", "agents"),
|
|
23682
|
+
agent: agentName,
|
|
23683
|
+
presentedToken: req.token,
|
|
23684
|
+
reason: grantResult.reason
|
|
23685
|
+
});
|
|
23686
|
+
}
|
|
23517
23687
|
}
|
|
23518
23688
|
if (isOperator) {
|
|
23519
23689
|
const entry2 = this.secrets[req.key];
|
|
@@ -24104,22 +24274,39 @@ class VaultBroker {
|
|
|
24104
24274
|
}
|
|
24105
24275
|
try {
|
|
24106
24276
|
const agentsDir = this.config ? resolveAgentsDir(this.config) : path5.join(os3.homedir(), ".switchroom", "agents");
|
|
24107
|
-
const
|
|
24108
|
-
|
|
24109
|
-
|
|
24110
|
-
|
|
24111
|
-
|
|
24112
|
-
|
|
24113
|
-
|
|
24114
|
-
const uid = allocateAgentUid(agent);
|
|
24115
|
-
chownSync(tokenPath, uid, uid);
|
|
24116
|
-
} catch (chownErr) {
|
|
24117
|
-
process.stderr.write(`[vault-broker] mint_grant: token written but chown failed for agent ${agent}: ${chownErr.message} (CAP_CHOWN missing?)
|
|
24277
|
+
const { chownWarning } = writeAgentTokenFile({
|
|
24278
|
+
agentsDir,
|
|
24279
|
+
agent,
|
|
24280
|
+
token: mintResult.token
|
|
24281
|
+
});
|
|
24282
|
+
if (chownWarning !== null) {
|
|
24283
|
+
process.stderr.write(`[vault-broker] mint_grant: ${chownWarning} (agent ${agent})
|
|
24118
24284
|
`);
|
|
24119
24285
|
}
|
|
24120
24286
|
} catch (err) {
|
|
24121
|
-
|
|
24287
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
24288
|
+
let rolledBack = false;
|
|
24289
|
+
let rollbackErr = null;
|
|
24290
|
+
try {
|
|
24291
|
+
rolledBack = revokeGrant(this.grantsDb, mintResult.id);
|
|
24292
|
+
} catch (revokeError) {
|
|
24293
|
+
rollbackErr = revokeError instanceof Error ? revokeError.message : String(revokeError);
|
|
24294
|
+
}
|
|
24295
|
+
const rollbackNote = rolledBack ? "grant rolled back (revoked)" : `GRANT ROLLBACK FAILED — revoke ${mintResult.id} manually${rollbackErr ? `: ${rollbackErr}` : ""}`;
|
|
24296
|
+
process.stderr.write(`[vault-broker] mint_grant: failed to write token file for agent ${agent}: ${msg}; ${rollbackNote}
|
|
24122
24297
|
`);
|
|
24298
|
+
this.auditLogger.write({
|
|
24299
|
+
ts: new Date().toISOString(),
|
|
24300
|
+
op: "mint_grant",
|
|
24301
|
+
caller: auditCaller,
|
|
24302
|
+
pid: auditPid,
|
|
24303
|
+
cgroup: auditCgroup,
|
|
24304
|
+
result: `error:token-write-failed:${msg}`,
|
|
24305
|
+
method: "grant",
|
|
24306
|
+
grant_id: mintResult.id
|
|
24307
|
+
});
|
|
24308
|
+
socket.write(encodeResponse(errorResponse("INTERNAL", `Failed to write token file for agent ${agent}: ${msg} (${rollbackNote})`)));
|
|
24309
|
+
return;
|
|
24123
24310
|
}
|
|
24124
24311
|
this.auditLogger.write({
|
|
24125
24312
|
ts: new Date().toISOString(),
|
|
@@ -24395,7 +24582,7 @@ class VaultBroker {
|
|
|
24395
24582
|
_writePidFile() {
|
|
24396
24583
|
try {
|
|
24397
24584
|
const pidPath = resolvePath(PID_FILE_DEFAULT);
|
|
24398
|
-
|
|
24585
|
+
writeFileSync5(pidPath, String(process.pid) + `
|
|
24399
24586
|
`, { mode: 384 });
|
|
24400
24587
|
} catch {}
|
|
24401
24588
|
}
|
|
@@ -24451,7 +24638,7 @@ class VaultBroker {
|
|
|
24451
24638
|
const credPath = `${dir}/vault-passphrase`;
|
|
24452
24639
|
let passphrase;
|
|
24453
24640
|
try {
|
|
24454
|
-
passphrase =
|
|
24641
|
+
passphrase = readFileSync12(credPath, "utf8").replace(/\n+$/, "");
|
|
24455
24642
|
} catch (err) {
|
|
24456
24643
|
const code = err.code;
|
|
24457
24644
|
if (code === "ENOENT") {
|
package/examples/switchroom.yaml
CHANGED
|
@@ -249,7 +249,7 @@ agents:
|
|
|
249
249
|
# topic_emoji: "💻"
|
|
250
250
|
# bot_token: "vault:telegram-dev-bot-token" # its own bot
|
|
251
251
|
# extends: coder # inherits from inline profile above
|
|
252
|
-
# model:
|
|
252
|
+
# model: opus # override defaults.model for this agent
|
|
253
253
|
# memory:
|
|
254
254
|
# collection: coding
|
|
255
255
|
# cli_args: ["--effort", "high"] # escape hatch: extra exec claude flags
|
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.19.
|
|
4
|
+
"version": "0.19.26",
|
|
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": {
|