switchroom 0.18.10 → 0.18.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-scheduler/index.js +29 -5
- package/dist/auth-broker/index.js +53 -13
- package/dist/cli/hindsight-mental-model-pretool.mjs +39 -0
- package/dist/cli/notion-write-pretool.mjs +29 -5
- package/dist/cli/switchroom.js +2636 -1369
- package/dist/cli/ui/apple-touch-icon.png +0 -0
- package/dist/cli/ui/favicon-32.png +0 -0
- package/dist/cli/ui/favicon.ico +0 -0
- package/dist/cli/ui/index.html +163 -17
- package/dist/host-control/main.js +1248 -342
- package/dist/vault/approvals/kernel-server.js +54 -13
- package/dist/vault/broker/server.js +163 -114
- package/package.json +3 -4
- package/profiles/_base/start.sh.hbs +65 -0
- package/profiles/_shared/vault-protocol.md.hbs +3 -1
- package/profiles/coding/CLAUDE.md.hbs +1 -1
- package/profiles/default/CLAUDE.md.hbs +2 -2
- package/profiles/executive-assistant/CLAUDE.md.hbs +1 -1
- package/profiles/health-coach/CLAUDE.md.hbs +1 -1
- package/telegram-plugin/bridge/bridge.ts +37 -0
- package/telegram-plugin/bridge/inbound-dedup.ts +101 -0
- package/telegram-plugin/dist/bridge/bridge.js +73 -1
- package/telegram-plugin/dist/gateway/gateway.js +3603 -1007
- package/telegram-plugin/dist/server.js +74 -2
- package/telegram-plugin/flood-circuit-breaker.ts +493 -21
- package/telegram-plugin/gateway/approval-hold.ts +583 -0
- package/telegram-plugin/gateway/auth-command.ts +92 -2
- package/telegram-plugin/gateway/auth-loopback-relay.ts +670 -0
- package/telegram-plugin/gateway/boot-card.ts +12 -5
- package/telegram-plugin/gateway/callback-query-handlers.ts +76 -1
- package/telegram-plugin/gateway/config-approval-handler.ts +6 -1
- package/telegram-plugin/gateway/disconnect-flush.ts +19 -0
- package/telegram-plugin/gateway/dm-pin-sweep.test.ts +251 -0
- package/telegram-plugin/gateway/dm-pin-sweep.ts +178 -0
- package/telegram-plugin/gateway/gateway.ts +1482 -165
- package/telegram-plugin/gateway/hostd-dispatch.ts +23 -0
- package/telegram-plugin/gateway/idle-clear.ts +90 -6
- package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +26 -5
- package/telegram-plugin/gateway/inject-handler.ts +8 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +46 -3
- package/telegram-plugin/gateway/ipc-server.ts +43 -0
- package/telegram-plugin/gateway/mental-model-propose-resolve.ts +145 -37
- package/telegram-plugin/gateway/model-command.ts +9 -3
- package/telegram-plugin/gateway/pending-session-command.ts +13 -1
- package/telegram-plugin/gateway/permission-ttl-sweep.ts +66 -0
- package/telegram-plugin/gateway/pre-approval-check.ts +74 -0
- package/telegram-plugin/gateway/queued-card-store.ts +217 -0
- package/telegram-plugin/gateway/session-model-file.ts +26 -1
- package/telegram-plugin/gateway/turn-end-gate-backstop.ts +59 -0
- package/telegram-plugin/gateway/turn-end-gate.ts +95 -0
- package/telegram-plugin/gateway/turn-typing-loop.ts +10 -2
- package/telegram-plugin/gateway/unhandled-rejection-policy.ts +13 -0
- package/telegram-plugin/hooks/dispatch-claim-scan.mjs +259 -0
- package/telegram-plugin/hooks/dispatch-claim-stop.mjs +129 -0
- package/telegram-plugin/hooks/hooks.json +9 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +209 -2
- package/telegram-plugin/operator-events.ts +23 -0
- package/telegram-plugin/package.json +0 -1
- package/telegram-plugin/permission-rule.ts +1 -0
- package/telegram-plugin/permission-title.ts +1 -0
- package/telegram-plugin/retry-api-call.ts +212 -2
- package/telegram-plugin/send-gate-degraded.test.ts +443 -0
- package/telegram-plugin/send-gate-observability.test.ts +470 -0
- package/telegram-plugin/send-gate-observability.ts +355 -0
- package/telegram-plugin/send-gate.test.ts +698 -0
- package/telegram-plugin/send-gate.ts +982 -0
- package/telegram-plugin/shared/bot-runtime.ts +17 -5
- package/telegram-plugin/shared/gw-trace-gate.ts +105 -0
- package/telegram-plugin/status-pin-driver.ts +52 -7
- package/telegram-plugin/status-pin.ts +81 -0
- package/telegram-plugin/subagent-watcher.ts +102 -2
- package/telegram-plugin/tests/activity-card-wiring.test.ts +18 -5
- package/telegram-plugin/tests/approval-hold-harness.ts +425 -0
- package/telegram-plugin/tests/approval-hold-outcome.test.ts +296 -0
- package/telegram-plugin/tests/approval-hold-record.test.ts +531 -0
- package/telegram-plugin/tests/approval-hold-redeliver.test.ts +602 -0
- package/telegram-plugin/tests/auth-loopback-relay.test.ts +533 -0
- package/telegram-plugin/tests/boot-card-flood-suppress.test.ts +53 -7
- package/telegram-plugin/tests/busy-key-reaper.test.ts +1 -0
- package/telegram-plugin/tests/dispatch-claim-scan.test.ts +250 -0
- package/telegram-plugin/tests/flood-breaker-blindness.test.ts +213 -0
- package/telegram-plugin/tests/flood-windows-persistence.test.ts +224 -0
- package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +29 -1
- package/telegram-plugin/tests/gateway-loopback-paste-redact.test.ts +66 -0
- package/telegram-plugin/tests/gw-trace-gate.test.ts +105 -0
- package/telegram-plugin/tests/idle-clear.test.ts +233 -3
- package/telegram-plugin/tests/inbound-dedup.test.ts +93 -0
- package/telegram-plugin/tests/inline-keyboard-callbacks.test.ts +284 -0
- package/telegram-plugin/tests/ipc-server-check-pre-approved.test.ts +194 -0
- package/telegram-plugin/tests/mental-model-propose-resolve.test.ts +123 -0
- package/telegram-plugin/tests/missed-approvals-wiring.test.ts +1 -1
- package/telegram-plugin/tests/model-command.test.ts +14 -0
- package/telegram-plugin/tests/pending-session-command.test.ts +21 -0
- package/telegram-plugin/tests/permission-card-routing.test.ts +30 -5
- package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +8 -7
- package/telegram-plugin/tests/permission-rearm-wiring.test.ts +1 -1
- package/telegram-plugin/tests/pre-approval-check.test.ts +148 -0
- package/telegram-plugin/tests/queued-card-store.test.ts +232 -0
- package/telegram-plugin/tests/reaction-flush-turn-gated.test.ts +100 -0
- package/telegram-plugin/tests/retry-api-call.test.ts +398 -0
- package/telegram-plugin/tests/session-model-file.test.ts +50 -0
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +35 -14
- package/telegram-plugin/tests/status-pin.test.ts +275 -1
- package/telegram-plugin/tests/subagent-watcher-deferral-log-ratelimit.test.ts +316 -0
- package/telegram-plugin/tests/turn-end-gate-backstop.test.ts +92 -0
- package/telegram-plugin/tests/turn-end-gate.test.ts +137 -0
- package/telegram-plugin/tests/typing-emitter.test.ts +586 -0
- package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +20 -0
- package/telegram-plugin/typing-emitter.ts +224 -0
- package/telegram-plugin/uat/scenarios/jtbd-feel-like-a-colleague-dm.test.ts +136 -0
- package/telegram-plugin/welcome-text.ts +42 -0
- package/vendor/hindsight-memory/scripts/drain_pending.py +22 -6
- package/vendor/hindsight-memory/scripts/lib/client.py +12 -5
- package/vendor/hindsight-memory/scripts/lib/directives.py +38 -3
- package/vendor/hindsight-memory/scripts/lib/pending.py +36 -9
- package/vendor/hindsight-memory/scripts/session_end.py +14 -3
- package/vendor/hindsight-memory/scripts/session_start.py +21 -0
- package/vendor/hindsight-memory/scripts/tests/test_directives.py +38 -0
- package/vendor/hindsight-memory/tests/test_drain_pending.py +68 -0
- package/vendor/hindsight-memory/tests/test_pending.py +44 -0
- package/vendor/hindsight-memory/tests/test_session_end_pending.py +38 -0
- package/vendor/hindsight-memory/tests/test_session_start_drain.py +155 -0
- package/telegram-plugin/channel-envelope-safety.test.ts +0 -56
- package/telegram-plugin/channel-envelope-safety.ts +0 -56
|
@@ -9966,7 +9966,7 @@ var require_util = __commonJS((exports) => {
|
|
|
9966
9966
|
return path;
|
|
9967
9967
|
}
|
|
9968
9968
|
exports.normalize = normalize;
|
|
9969
|
-
function
|
|
9969
|
+
function join3(aRoot, aPath) {
|
|
9970
9970
|
if (aRoot === "") {
|
|
9971
9971
|
aRoot = ".";
|
|
9972
9972
|
}
|
|
@@ -9998,7 +9998,7 @@ var require_util = __commonJS((exports) => {
|
|
|
9998
9998
|
}
|
|
9999
9999
|
return joined;
|
|
10000
10000
|
}
|
|
10001
|
-
exports.join =
|
|
10001
|
+
exports.join = join3;
|
|
10002
10002
|
exports.isAbsolute = function(aPath) {
|
|
10003
10003
|
return aPath.charAt(0) === "/" || urlRegexp.test(aPath);
|
|
10004
10004
|
};
|
|
@@ -10171,7 +10171,7 @@ var require_util = __commonJS((exports) => {
|
|
|
10171
10171
|
parsed.path = parsed.path.substring(0, index + 1);
|
|
10172
10172
|
}
|
|
10173
10173
|
}
|
|
10174
|
-
sourceURL =
|
|
10174
|
+
sourceURL = join3(urlGenerate(parsed), sourceURL);
|
|
10175
10175
|
}
|
|
10176
10176
|
return normalize(sourceURL);
|
|
10177
10177
|
}
|
|
@@ -15274,7 +15274,7 @@ var init_secretlint_source = __esm(() => {
|
|
|
15274
15274
|
});
|
|
15275
15275
|
|
|
15276
15276
|
// src/util/atomic.ts
|
|
15277
|
-
import { closeSync as closeSync2, constants, fsyncSync, openSync as openSync2, renameSync, rmSync, writeSync } from "node:fs";
|
|
15277
|
+
import { closeSync as closeSync2, constants, fsyncSync, openSync as openSync2, renameSync as renameSync2, rmSync, writeSync } from "node:fs";
|
|
15278
15278
|
var TMP_OPEN_FLAGS;
|
|
15279
15279
|
var init_atomic = __esm(() => {
|
|
15280
15280
|
TMP_OPEN_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
|
|
@@ -15989,7 +15989,7 @@ var require_command = __commonJS((exports2) => {
|
|
|
15989
15989
|
var EventEmitter2 = __require("node:events").EventEmitter;
|
|
15990
15990
|
var childProcess = __require("node:child_process");
|
|
15991
15991
|
var path2 = __require("node:path");
|
|
15992
|
-
var
|
|
15992
|
+
var fs3 = __require("node:fs");
|
|
15993
15993
|
var process2 = __require("node:process");
|
|
15994
15994
|
var { Argument, humanReadableArgName } = require_argument();
|
|
15995
15995
|
var { CommanderError } = require_error();
|
|
@@ -16510,7 +16510,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
16510
16510
|
this.processedArgs = [];
|
|
16511
16511
|
}
|
|
16512
16512
|
_checkForMissingExecutable(executableFile, executableDir, subcommandName) {
|
|
16513
|
-
if (
|
|
16513
|
+
if (fs3.existsSync(executableFile))
|
|
16514
16514
|
return;
|
|
16515
16515
|
const executableDirMessage = executableDir ? `searched for local subcommand relative to directory '${executableDir}'` : "no directory for search for local subcommand, use .executableDir() to supply a custom directory";
|
|
16516
16516
|
const executableMissing = `'${executableFile}' does not exist
|
|
@@ -16525,11 +16525,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
16525
16525
|
const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
|
|
16526
16526
|
function findFile(baseDir, baseName) {
|
|
16527
16527
|
const localBin = path2.resolve(baseDir, baseName);
|
|
16528
|
-
if (
|
|
16528
|
+
if (fs3.existsSync(localBin))
|
|
16529
16529
|
return localBin;
|
|
16530
16530
|
if (sourceExt.includes(path2.extname(baseName)))
|
|
16531
16531
|
return;
|
|
16532
|
-
const foundExt = sourceExt.find((ext) =>
|
|
16532
|
+
const foundExt = sourceExt.find((ext) => fs3.existsSync(`${localBin}${ext}`));
|
|
16533
16533
|
if (foundExt)
|
|
16534
16534
|
return `${localBin}${foundExt}`;
|
|
16535
16535
|
return;
|
|
@@ -16541,7 +16541,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
16541
16541
|
if (this._scriptPath) {
|
|
16542
16542
|
let resolvedScriptPath;
|
|
16543
16543
|
try {
|
|
16544
|
-
resolvedScriptPath =
|
|
16544
|
+
resolvedScriptPath = fs3.realpathSync(this._scriptPath);
|
|
16545
16545
|
} catch {
|
|
16546
16546
|
resolvedScriptPath = this._scriptPath;
|
|
16547
16547
|
}
|
|
@@ -17316,8 +17316,8 @@ var require_commander = __commonJS((exports2) => {
|
|
|
17316
17316
|
|
|
17317
17317
|
// src/host-control/main.ts
|
|
17318
17318
|
import { homedir as homedir6 } from "node:os";
|
|
17319
|
-
import { existsSync as
|
|
17320
|
-
import { join as
|
|
17319
|
+
import { existsSync as existsSync12, readdirSync as readdirSync4 } from "node:fs";
|
|
17320
|
+
import { join as join11, resolve as resolve9 } from "node:path";
|
|
17321
17321
|
|
|
17322
17322
|
// src/config/loader.ts
|
|
17323
17323
|
var import_yaml2 = __toESM(require_dist(), 1);
|
|
@@ -21399,7 +21399,12 @@ var ScheduleEntrySchema = exports_external.object({
|
|
|
21399
21399
|
var AgentSoulSchema = exports_external.object({
|
|
21400
21400
|
name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
|
|
21401
21401
|
style: exports_external.string().describe("Communication style description"),
|
|
21402
|
-
|
|
21402
|
+
creature: exports_external.string().optional().describe("Persona creature/form (e.g., 'owl', 'octopus')"),
|
|
21403
|
+
vibe: exports_external.string().optional().describe("One-line personality vibe (e.g., 'calm, precise')"),
|
|
21404
|
+
expertise: exports_external.string().optional().describe("Domain expertise summary rendered into the persona"),
|
|
21405
|
+
emoji: exports_external.string().optional().describe("Signature emoji for the persona (e.g., '\uD83E\uDD89')"),
|
|
21406
|
+
boundaries: exports_external.string().optional().describe("Behavioral boundaries and disclaimers"),
|
|
21407
|
+
shape: exports_external.enum(["executive-assistant", "developer", "coach", "generalist"]).default("generalist").describe("Persona-shape discriminator the per-agent CLAUDE.md template can " + "branch on (e.g., 'you are an executive assistant' vs 'a senior " + "engineer'). No runtime behavior yet — template work lands in a " + "later issue. Cascade: override (per-agent wins over default).")
|
|
21403
21408
|
}).optional();
|
|
21404
21409
|
var AgentToolsSchema = exports_external.object({
|
|
21405
21410
|
allow: exports_external.array(exports_external.string()).default([]).describe("Allowed tools (use ['all'] for unrestricted)"),
|
|
@@ -21564,6 +21569,11 @@ var TelegramChannelSchema = exports_external.object({
|
|
|
21564
21569
|
safe_boundary: exports_external.boolean().optional().describe("When true (the default), a `!`-prefix interrupt that arrives while " + "the agent is mid-tool-call is DEFERRED: the SIGINT and the " + "replacement turn wait until the in-flight tool call finishes (a " + "clean boundary) instead of C-c'ing the agent mid-write/mid-bash. If " + "no tool is in flight the interrupt still fires immediately. Bounded " + "by max_wait_ms so a long tool never strands the user. Set false to " + "fire synchronously the moment `!` is received (historical " + "behaviour). Rapid repeated `!` while one is pending coalesce into a " + "single deferred interrupt carrying the latest body."),
|
|
21565
21570
|
max_wait_ms: exports_external.number().int().positive().optional().describe("Upper bound (ms) the gateway waits for a safe boundary before firing " + "a deferred `!` interrupt anyway. Only consulted when safe_boundary is " + "true. Default 8000. Keep it short — the user explicitly asked to " + "interrupt, so a long in-flight tool shouldn't ghost them; the cap " + "trades a tiny risk of a mid-tool C-c for a guaranteed response.")
|
|
21566
21571
|
}).optional().describe("Interrupt timing — how a `!`-prefix interrupt behaves when it lands " + "mid-tool-call. Off by default (fire immediately). Cascades from " + "defaults.channels.telegram.interrupt."),
|
|
21572
|
+
button_choice_confirmation: exports_external.object({
|
|
21573
|
+
enabled: exports_external.boolean().default(false).describe("When true, tapping an agent-emitted inline_keyboard button annotates " + "the source message body with a '✅ You chose: <label> · HH:MM' line so " + "the chat surface is self-documenting. Only applies to single-use " + "keyboards (re-tappable keyboards are never annotated). Requires the " + "agent's parseMode to be the default 'html'. Opt-in (default false). " + "Limitation: the annotated body is rebuilt from the message's plain " + "text, so any formatting entities (bold, links, code, ...) on the " + "original message are lost when the annotation is applied."),
|
|
21574
|
+
format: exports_external.string().default("✅ You chose: {label} · {time}").describe("Template for the annotation line. `{label}` is replaced with the " + "tapped button's text (newlines stripped, HTML-escaped, trimmed to 60 " + "chars) and `{time}` with HH:MM in the chosen timezone. Note: retap " + "dedup (replacing a prior annotation instead of appending another " + "line) only recognizes the DEFAULT format shape — a custom format " + "accumulates one line per retap."),
|
|
21575
|
+
timezone: exports_external.enum(["gateway", "utc"]).default("gateway").describe("Timezone for the {time} placeholder. 'gateway' uses the gateway " + "process's local time; 'utc' uses UTC. Per-user timezones are out of " + "scope for v1.")
|
|
21576
|
+
}).optional().describe("Auto-confirm 'You chose: X' annotation on inline_keyboard taps (#789). " + "Cascades from defaults.channels.telegram.button_choice_confirmation."),
|
|
21567
21577
|
webhook_sources: exports_external.array(exports_external.enum(["github", "generic", "linear"])).optional().describe("External webhook sources allowed to ingest events into this agent's " + "log. POST /webhook/<agent>/<source> on the switchroom web server. " + "Each source has its own signature verification ('github' = " + "X-Hub-Signature-256 HMAC-SHA256, 'generic' = Bearer token, " + "'linear' = Linear-Signature bare-hex HMAC-SHA256 of the raw body). " + "Per-source secret read from ~/.switchroom/webhook-secrets.json " + "keyed by [agent][source]. Verified events append to " + "<agent>/telegram/webhook-events.jsonl for the agent to read on " + "demand. Off by default — webhook is the only untrusted-inbound " + "surface in the system, so opt-in is mandatory. " + "Cascades from defaults.channels.telegram.webhook_sources. " + "(Migrated from per-agent root in #596 — see #577.)"),
|
|
21568
21578
|
webhook_dispatch: exports_external.object({
|
|
21569
21579
|
github: exports_external.array(webhookDispatchRule).optional(),
|
|
@@ -21712,7 +21722,12 @@ var profileFields = {
|
|
|
21712
21722
|
soul: exports_external.object({
|
|
21713
21723
|
name: exports_external.string().optional(),
|
|
21714
21724
|
style: exports_external.string().optional(),
|
|
21715
|
-
|
|
21725
|
+
creature: exports_external.string().optional(),
|
|
21726
|
+
vibe: exports_external.string().optional(),
|
|
21727
|
+
expertise: exports_external.string().optional(),
|
|
21728
|
+
emoji: exports_external.string().optional(),
|
|
21729
|
+
boundaries: exports_external.string().optional(),
|
|
21730
|
+
shape: exports_external.enum(["executive-assistant", "developer", "coach", "generalist"]).optional()
|
|
21716
21731
|
}).optional(),
|
|
21717
21732
|
tools: exports_external.object({
|
|
21718
21733
|
allow: exports_external.array(exports_external.string()).optional(),
|
|
@@ -21909,7 +21924,8 @@ var VaultConfigSchema = exports_external.object({
|
|
|
21909
21924
|
autoUnlockCredentialPath: exports_external.string().default("~/.switchroom/vault-auto-unlock").describe("Path to the machine-bound auto-unlock blob (see " + "src/vault/auto-unlock.ts for the format). Default lives under " + "~/.switchroom so it can be bind-mounted into the vault-broker " + "container by docker compose. Tilde-expansion happens " + "at read time."),
|
|
21910
21925
|
approvalAuth: exports_external.enum(["passphrase", "telegram-id"]).default("passphrase").describe("Posture for tap-to-Approve on vault grant cards. `passphrase` " + "(default) prompts the operator to type the vault passphrase on " + "every Approve — two-factor (Telegram ID + passphrase). " + "`telegram-id` mints immediately on Approve with no passphrase " + "prompt — single-factor (Telegram ID only); REQUIRES " + "`autoUnlock: true` so the broker already holds the passphrase. " + "Trades a factor of security for smoother UX; opt-in only."),
|
|
21911
21926
|
postureMintAgents: exports_external.array(exports_external.string().min(1)).default([]).describe("Per-agent opt-in for posture-attested broker calls (`mint_grant` / " + "`list_grants` / `put` with `attest_via_posture: true`). Only agents " + "whose names are in this list can use the silent-mint path under " + "`approvalAuth: telegram-id`. Default `[]` — no agent can self-mint " + "until the operator explicitly opts it in. The request's `agent` " + "field must also equal the calling peer's resolved agent name " + "(broker rejects cross-agent posture mints). When `approvalAuth` is " + "`passphrase` this list is ignored — passphrase attestation still " + "works as before. Each entry is an agent slug exactly as it appears " + "under `agents:` in this config."),
|
|
21912
|
-
adminOnlyKeys: exports_external.array(exports_external.string().min(1)).default([]).describe("Vault keys held to a higher approval bar: only the admin operator " + "(`access.allowFrom[0]`) may approve a grant for them, and they can " + "NEVER be minted via posture attestation — granting one requires the " + "operator passphrase (so an agent, even one on `postureMintAgents`, " + "cannot self-grant it). Entries are exact key names or `*` globs, " + "e.g. `stripe/*`, `*/oauth-token`, `microsoft/ken-tokens` (`*` matches " + "any run of characters incl. `/`; case-sensitive). Default `[]` — no " + "key is admin-only. Posture may RETAIN an admin-only key across a " + "union re-mint but never ADD one. Takes effect on broker + gateway " + "restart (broker has no ACL hot-reload).")
|
|
21927
|
+
adminOnlyKeys: exports_external.array(exports_external.string().min(1)).default([]).describe("Vault keys held to a higher approval bar: only the admin operator " + "(`access.allowFrom[0]`) may approve a grant for them, and they can " + "NEVER be minted via posture attestation — granting one requires the " + "operator passphrase (so an agent, even one on `postureMintAgents`, " + "cannot self-grant it). Entries are exact key names or `*` globs, " + "e.g. `stripe/*`, `*/oauth-token`, `microsoft/ken-tokens` (`*` matches " + "any run of characters incl. `/`; case-sensitive). Default `[]` — no " + "key is admin-only. Posture may RETAIN an admin-only key across a " + "union re-mint but never ADD one. Takes effect on broker + gateway " + "restart (broker has no ACL hot-reload)."),
|
|
21928
|
+
auditFailClosed: exports_external.boolean().default(false).describe("sec WS10-F3 (#1420) — fail-CLOSED on an audit-append failure. When " + "`false` (default, backward-compatible) the broker fails OPEN: if a " + "vault-audit.log append fails it logs to stderr, bumps the durable " + "fail-open counter, and STILL releases the secret. When `true`, a " + "failed audit append DENIES the secret release with " + "`AUDIT_UNAVAILABLE` — no secret leaves the broker without a durable " + "audit row. The env var `SWITCHROOM_VAULT_AUDIT_FAIL_CLOSED` " + "(`1`/`true` or `0`/`false`) overrides this at runtime. Either way " + "the fail-open counter is surfaced by `switchroom doctor`. Takes " + "effect on broker restart.")
|
|
21913
21929
|
}).default({}).superRefine((broker, ctx) => {
|
|
21914
21930
|
if (broker.approvalAuth === "telegram-id" && broker.autoUnlock !== true) {
|
|
21915
21931
|
ctx.addIssue({
|
|
@@ -21948,7 +21964,9 @@ var FleetHealthConfigSchema = exports_external.object({
|
|
|
21948
21964
|
});
|
|
21949
21965
|
var HostdConfigSchema = exports_external.object({
|
|
21950
21966
|
config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit §3). Default false — the verb returns " + "`E_CONFIG_EDIT_DISABLED` until the operator explicitly flips " + "this to true. When true, admin agents can propose unified-diff " + "patches against " + "`/state/config/switchroom.yaml`, gated by an operator approval " + "card in the primary chat. Same trust posture as `update_apply` " + "and `agent_restart`: the human-in-the-loop tap is the security " + "boundary, not the agent's judgement."),
|
|
21951
|
-
config_edit_rate_per_hour: exports_external.number().int().min(1).max(20).default(3).describe("Per-requesting-agent rate cap for `config_propose_edit` cards " + "(RFC admin-agent-config-edit §5). Default 3 cards/hour; min 1, " + "max 20. ENFORCED server-side: a caller exceeding this in a sliding " + "1-hour window is rejected with `E_RATE_LIMITED` (carrying a " + "`retry_after` fix) instead of posting another operator approval " + "card — so a looping agent is throttled rather than spamming the chat.")
|
|
21967
|
+
config_edit_rate_per_hour: exports_external.number().int().min(1).max(20).default(3).describe("Per-requesting-agent rate cap for `config_propose_edit` cards " + "(RFC admin-agent-config-edit §5). Default 3 cards/hour; min 1, " + "max 20. ENFORCED server-side: a caller exceeding this in a sliding " + "1-hour window is rejected with `E_RATE_LIMITED` (carrying a " + "`retry_after` fix) instead of posting another operator approval " + "card — so a looping agent is throttled rather than spamming the chat."),
|
|
21968
|
+
operator_attest_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the operator-passphrase 2nd factor on hostd's " + "mutating verbs (#1841, RFC host-control-daemon.md §5.4). Default " + "false — attestation is ACCEPTED-and-audited whenever present, but " + "no verb REQUIRES it (behaviour is byte-identical to today, so " + "existing fleets and the Telegram approval-card flow are unchanged). " + "When true, the verbs in `operator_attest_required_verbs` demand a " + "valid operator-passphrase attestation IN ADDITION to admin: hostd " + "forwards the passphrase to the vault broker over its own admin-client " + "connection (`/run/switchroom/broker/hostd/sock`) and treats a broker " + "DENIED as a gate failure. hostd never holds the passphrase. Leave " + "this off until BOTH the gateway passphrase-forward and the " + "cross-compose broker socket are deployed, or gated verbs will fail."),
|
|
21969
|
+
operator_attest_required_verbs: exports_external.array(exports_external.string().min(1)).default(["update_apply", "apply", "rollout"]).describe("Which mutating hostd verbs REQUIRE a valid operator-passphrase " + "attestation when `operator_attest_enabled` is true (#1841). Default " + "is the RFC §5.4 fleet-mutation set (`update_apply`, `apply`, " + "`rollout`). Verbs NOT in this list still accept-and-audit an " + "attestation when one is supplied, but never require it. Ignored " + "entirely when `operator_attest_enabled` is false.")
|
|
21952
21970
|
});
|
|
21953
21971
|
var CronEgressSchema = exports_external.object({
|
|
21954
21972
|
allowed_hosts: exports_external.array(exports_external.string().min(1)).default([]).describe("Hosts a poll may reach (exact, https-only). loopback/private/IP-literal are always rejected."),
|
|
@@ -22371,6 +22389,11 @@ function mergeAgentConfig(defaultsIn, agentIn) {
|
|
|
22371
22389
|
if (v !== undefined)
|
|
22372
22390
|
combined[k] = v;
|
|
22373
22391
|
}
|
|
22392
|
+
const baseSoul = base;
|
|
22393
|
+
const overrideSoul = override;
|
|
22394
|
+
if (baseSoul.shape !== undefined && (overrideSoul.shape === undefined || overrideSoul.shape === "generalist")) {
|
|
22395
|
+
combined.shape = baseSoul.shape;
|
|
22396
|
+
}
|
|
22374
22397
|
merged.soul = combined;
|
|
22375
22398
|
}
|
|
22376
22399
|
if (defaults.memory || merged.memory) {
|
|
@@ -22797,11 +22820,391 @@ function resolveAgentsDir(config) {
|
|
|
22797
22820
|
return resolveDualPath(config.switchroom.agents_dir || "~/.switchroom/agents");
|
|
22798
22821
|
}
|
|
22799
22822
|
|
|
22823
|
+
// src/host-control/config-degraded.ts
|
|
22824
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, renameSync, unlinkSync, watchFile, unwatchFile, writeFileSync, mkdirSync } from "node:fs";
|
|
22825
|
+
import { connect } from "node:net";
|
|
22826
|
+
import { join } from "node:path";
|
|
22827
|
+
|
|
22828
|
+
// src/config/release-resolve.ts
|
|
22829
|
+
function parseSemverTag(tag) {
|
|
22830
|
+
if (!tag)
|
|
22831
|
+
return null;
|
|
22832
|
+
const m = /^v(\d+)\.(\d+)\.(\d+)$/.exec(tag.trim());
|
|
22833
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
22834
|
+
}
|
|
22835
|
+
function compareReleaseTags(a, b) {
|
|
22836
|
+
const pa = parseSemverTag(a);
|
|
22837
|
+
const pb = parseSemverTag(b);
|
|
22838
|
+
if (!pa || !pb)
|
|
22839
|
+
return null;
|
|
22840
|
+
for (let i = 0;i < 3; i++) {
|
|
22841
|
+
if (pa[i] !== pb[i])
|
|
22842
|
+
return pa[i] < pb[i] ? -1 : 1;
|
|
22843
|
+
}
|
|
22844
|
+
return 0;
|
|
22845
|
+
}
|
|
22846
|
+
|
|
22847
|
+
// src/host-control/self-bump.ts
|
|
22848
|
+
var SELF_BUMP_MARKER_FILENAME = "pending-rollout.json";
|
|
22849
|
+
var SELF_BUMP_HELPER_CONTAINER = "switchroom-hostd-selfbump";
|
|
22850
|
+
var SELF_BUMP_LOG_FILENAME = "selfbump.log";
|
|
22851
|
+
var SELF_BUMP_MARKER_MAX_AGE_MS = 15 * 60 * 1000;
|
|
22852
|
+
var SEMVER_PIN_RE = /^v\d+\.\d+\.\d+$/;
|
|
22853
|
+
function needsSelfBump(ownVersion, pin) {
|
|
22854
|
+
if (!ownVersion)
|
|
22855
|
+
return false;
|
|
22856
|
+
const cmp = compareReleaseTags(`v${ownVersion.trim().replace(/^v/, "")}`, `v${pin.trim().replace(/^v/, "")}`);
|
|
22857
|
+
return cmp !== null && cmp < 0;
|
|
22858
|
+
}
|
|
22859
|
+
function bumpHostdComposeImageTag(yaml, pin) {
|
|
22860
|
+
const re = /^(\s*image:\s*)(\S*switchroom-hostd):(\S+)\s*$/m;
|
|
22861
|
+
const m = yaml.match(re);
|
|
22862
|
+
if (!m)
|
|
22863
|
+
return null;
|
|
22864
|
+
const oldImageRef = `${m[2]}:${m[3]}`;
|
|
22865
|
+
const newImageRef = `${m[2]}:${pin}`;
|
|
22866
|
+
return {
|
|
22867
|
+
yaml: yaml.replace(re, `$1${newImageRef}`),
|
|
22868
|
+
oldImageRef,
|
|
22869
|
+
newImageRef
|
|
22870
|
+
};
|
|
22871
|
+
}
|
|
22872
|
+
function encodePendingRolloutMarker(m) {
|
|
22873
|
+
return JSON.stringify(m, null, 2) + `
|
|
22874
|
+
`;
|
|
22875
|
+
}
|
|
22876
|
+
function parsePendingRolloutMarker(raw) {
|
|
22877
|
+
let obj;
|
|
22878
|
+
try {
|
|
22879
|
+
obj = JSON.parse(raw);
|
|
22880
|
+
} catch {
|
|
22881
|
+
return null;
|
|
22882
|
+
}
|
|
22883
|
+
if (typeof obj !== "object" || obj === null)
|
|
22884
|
+
return null;
|
|
22885
|
+
const o = obj;
|
|
22886
|
+
if (o.v !== 1)
|
|
22887
|
+
return null;
|
|
22888
|
+
if (typeof o.request_id !== "string" || o.request_id.length === 0)
|
|
22889
|
+
return null;
|
|
22890
|
+
if (typeof o.pin !== "string" || !SEMVER_PIN_RE.test(o.pin))
|
|
22891
|
+
return null;
|
|
22892
|
+
if (typeof o.created_at !== "string" || !Number.isFinite(Date.parse(o.created_at))) {
|
|
22893
|
+
return null;
|
|
22894
|
+
}
|
|
22895
|
+
if (typeof o.prior_hostd_version !== "string")
|
|
22896
|
+
return null;
|
|
22897
|
+
const caller = o.caller;
|
|
22898
|
+
if (typeof caller !== "object" || caller === null)
|
|
22899
|
+
return null;
|
|
22900
|
+
if (caller.kind === "agent") {
|
|
22901
|
+
if (typeof caller.name !== "string" || caller.name.length === 0)
|
|
22902
|
+
return null;
|
|
22903
|
+
} else if (caller.kind !== "operator") {
|
|
22904
|
+
return null;
|
|
22905
|
+
}
|
|
22906
|
+
if (o.agents !== undefined) {
|
|
22907
|
+
if (!Array.isArray(o.agents) || o.agents.length === 0 || !o.agents.every((a) => typeof a === "string" && /^[\w-]+$/.test(a))) {
|
|
22908
|
+
return null;
|
|
22909
|
+
}
|
|
22910
|
+
}
|
|
22911
|
+
if (o.skip_web !== undefined && typeof o.skip_web !== "boolean")
|
|
22912
|
+
return null;
|
|
22913
|
+
if (o.allow_downgrade !== undefined && typeof o.allow_downgrade !== "boolean") {
|
|
22914
|
+
return null;
|
|
22915
|
+
}
|
|
22916
|
+
return {
|
|
22917
|
+
v: 1,
|
|
22918
|
+
request_id: o.request_id,
|
|
22919
|
+
pin: o.pin,
|
|
22920
|
+
...o.agents !== undefined ? { agents: o.agents } : {},
|
|
22921
|
+
...o.skip_web !== undefined ? { skip_web: o.skip_web } : {},
|
|
22922
|
+
...o.allow_downgrade !== undefined ? { allow_downgrade: o.allow_downgrade } : {},
|
|
22923
|
+
caller: caller.kind === "agent" ? { kind: "agent", name: caller.name } : { kind: "operator" },
|
|
22924
|
+
created_at: o.created_at,
|
|
22925
|
+
prior_hostd_version: o.prior_hostd_version
|
|
22926
|
+
};
|
|
22927
|
+
}
|
|
22928
|
+
function isMarkerFresh(marker, nowMs) {
|
|
22929
|
+
const created = Date.parse(marker.created_at);
|
|
22930
|
+
if (!Number.isFinite(created))
|
|
22931
|
+
return false;
|
|
22932
|
+
return nowMs - created < SELF_BUMP_MARKER_MAX_AGE_MS;
|
|
22933
|
+
}
|
|
22934
|
+
function selfBumpHelperArgs(opts) {
|
|
22935
|
+
const composePath = `${opts.hostdDirHostPath}/docker-compose.yml`;
|
|
22936
|
+
const logPath = `${opts.hostdDirHostPath}/${SELF_BUMP_LOG_FILENAME}`;
|
|
22937
|
+
const script = `sleep 2; ` + `echo "selfbump $(date -u) -> ${opts.targetImage}" >> "${logPath}" 2>&1; ` + `docker compose -p switchroom-hostd -f "${composePath}" pull >> "${logPath}" 2>&1 && ` + `docker compose -p switchroom-hostd -f "${composePath}" up -d >> "${logPath}" 2>&1; ` + `rc=$?; echo "selfbump done rc=$rc" >> "${logPath}" 2>&1; exit $rc`;
|
|
22938
|
+
return [
|
|
22939
|
+
"run",
|
|
22940
|
+
"-d",
|
|
22941
|
+
"--rm",
|
|
22942
|
+
"--name",
|
|
22943
|
+
SELF_BUMP_HELPER_CONTAINER,
|
|
22944
|
+
"--label",
|
|
22945
|
+
"switchroom.hostd-selfbump=true",
|
|
22946
|
+
"-v",
|
|
22947
|
+
"/var/run/docker.sock:/var/run/docker.sock",
|
|
22948
|
+
"-v",
|
|
22949
|
+
`${opts.hostdDirHostPath}:${opts.hostdDirHostPath}`,
|
|
22950
|
+
"--entrypoint",
|
|
22951
|
+
"/bin/sh",
|
|
22952
|
+
opts.helperImage,
|
|
22953
|
+
"-c",
|
|
22954
|
+
script
|
|
22955
|
+
];
|
|
22956
|
+
}
|
|
22957
|
+
|
|
22958
|
+
// src/host-control/config-degraded.ts
|
|
22959
|
+
var CONFIG_DEGRADED_MARKER_FILENAME = "config-degraded.json";
|
|
22960
|
+
var CONFIG_DEGRADED_RETRY_MS = 15000;
|
|
22961
|
+
var MAX_PRESERVED_DEGRADED_MS = 4 * 60 * 60 * 1000;
|
|
22962
|
+
function buildDegradedMarker(err, nowMs) {
|
|
22963
|
+
return {
|
|
22964
|
+
v: 1,
|
|
22965
|
+
since: new Date(nowMs).toISOString(),
|
|
22966
|
+
error: err.message,
|
|
22967
|
+
...err.details && err.details.length > 0 ? { details: err.details } : {}
|
|
22968
|
+
};
|
|
22969
|
+
}
|
|
22970
|
+
function readDegradedSince(markerPath) {
|
|
22971
|
+
try {
|
|
22972
|
+
if (!existsSync4(markerPath))
|
|
22973
|
+
return null;
|
|
22974
|
+
const parsed = JSON.parse(readFileSync3(markerPath, "utf8"));
|
|
22975
|
+
if (parsed?.v !== 1 || typeof parsed.since !== "string")
|
|
22976
|
+
return null;
|
|
22977
|
+
const since = Date.parse(parsed.since);
|
|
22978
|
+
return Number.isFinite(since) ? since : null;
|
|
22979
|
+
} catch {
|
|
22980
|
+
return null;
|
|
22981
|
+
}
|
|
22982
|
+
}
|
|
22983
|
+
function formatDegradedNotice(err) {
|
|
22984
|
+
const details = (err.details ?? []).map((d) => d.trim()).filter(Boolean).slice(0, 6).join(`
|
|
22985
|
+
`);
|
|
22986
|
+
return `⚠️ hostd: switchroom.yaml failed to load — running DEGRADED.
|
|
22987
|
+
` + `${err.message}
|
|
22988
|
+
` + (details ? details + `
|
|
22989
|
+
` : "") + `Retrying every ${Math.round(CONFIG_DEGRADED_RETRY_MS / 1000)}s and on file change. ` + "hostd's sockets are not bound yet, so fleet operations (rollouts, config " + "edits, restarts) will fail with connection errors until the config parses. " + "Fix the YAML (`switchroom config check` validates it) — no hostd restart " + "needed. A pending self-bump rollout, if any, will resume once the config " + "loads (its 15-min freshness budget is frozen while degraded, up to " + `${Math.round(MAX_PRESERVED_DEGRADED_MS / 3600000)}h).`;
|
|
22990
|
+
}
|
|
22991
|
+
function humanMins(ms) {
|
|
22992
|
+
const mins = Math.max(1, Math.round(ms / 60000));
|
|
22993
|
+
return mins >= 90 ? `${(mins / 60).toFixed(1)}h` : `${mins}m`;
|
|
22994
|
+
}
|
|
22995
|
+
function formatRecoveredNotice(degradedMs, pending) {
|
|
22996
|
+
let tail = "";
|
|
22997
|
+
if (pending?.preserved) {
|
|
22998
|
+
tail = ` Pending self-bump rollout to ${pending.pin} was preserved through the ` + `outage (marker now ${humanMins(pending.ageMs)} old) and will resume now.`;
|
|
22999
|
+
} else if (pending) {
|
|
23000
|
+
tail = ` NOTE: pending self-bump rollout to ${pending.pin} was NOT auto-resumed — ` + `degraded for ~${humanMins(degradedMs)}, past the ` + `${Math.round(MAX_PRESERVED_DEGRADED_MS / 3600000)}h preservation cap. Its ` + `marker (${humanMins(pending.ageMs)} old) will be treated as stale; re-run ` + `the rollout if it is still wanted.`;
|
|
23001
|
+
}
|
|
23002
|
+
return `✅ hostd: switchroom.yaml loads again after ~${humanMins(degradedMs)} degraded — ` + `normal service resumed.` + tail;
|
|
23003
|
+
}
|
|
23004
|
+
function shiftPendingRolloutMarker(raw, degradedMs) {
|
|
23005
|
+
if (degradedMs <= 0)
|
|
23006
|
+
return null;
|
|
23007
|
+
const marker = parsePendingRolloutMarker(raw);
|
|
23008
|
+
if (!marker)
|
|
23009
|
+
return null;
|
|
23010
|
+
const created = Date.parse(marker.created_at);
|
|
23011
|
+
if (!Number.isFinite(created))
|
|
23012
|
+
return null;
|
|
23013
|
+
return encodePendingRolloutMarker({
|
|
23014
|
+
...marker,
|
|
23015
|
+
created_at: new Date(created + degradedMs).toISOString()
|
|
23016
|
+
});
|
|
23017
|
+
}
|
|
23018
|
+
function clearStaleDegradedMarker(homeDir, log) {
|
|
23019
|
+
const markerPath = join(homeDir, ".switchroom", "hostd", CONFIG_DEGRADED_MARKER_FILENAME);
|
|
23020
|
+
try {
|
|
23021
|
+
if (!existsSync4(markerPath))
|
|
23022
|
+
return false;
|
|
23023
|
+
unlinkSync(markerPath);
|
|
23024
|
+
log(`config-degraded: clearing stale degraded marker from a prior crash ` + `(${markerPath}) — config loads cleanly on this boot`);
|
|
23025
|
+
return true;
|
|
23026
|
+
} catch (e) {
|
|
23027
|
+
log(`config-degraded: could not clear stale marker ${markerPath}: ${e.message}`);
|
|
23028
|
+
return false;
|
|
23029
|
+
}
|
|
23030
|
+
}
|
|
23031
|
+
function postOperatorNoticeViaGateways(candidates, text, log, connectTimeoutMs = 5000) {
|
|
23032
|
+
const tryOne = (c) => new Promise((resolve4) => {
|
|
23033
|
+
let done = false;
|
|
23034
|
+
const finish = (ok, why) => {
|
|
23035
|
+
if (done)
|
|
23036
|
+
return;
|
|
23037
|
+
done = true;
|
|
23038
|
+
if (why)
|
|
23039
|
+
log(`config-degraded: gateway ${c.agent} — ${why}`);
|
|
23040
|
+
try {
|
|
23041
|
+
client.destroy();
|
|
23042
|
+
} catch {}
|
|
23043
|
+
resolve4(ok);
|
|
23044
|
+
};
|
|
23045
|
+
let client;
|
|
23046
|
+
try {
|
|
23047
|
+
client = connect({ path: c.sock });
|
|
23048
|
+
} catch (e) {
|
|
23049
|
+
log(`config-degraded: gateway ${c.agent} — connect threw: ${e.message}`);
|
|
23050
|
+
resolve4(false);
|
|
23051
|
+
return;
|
|
23052
|
+
}
|
|
23053
|
+
client.setTimeout(connectTimeoutMs);
|
|
23054
|
+
client.on("connect", () => {
|
|
23055
|
+
try {
|
|
23056
|
+
client.write(JSON.stringify({
|
|
23057
|
+
type: "rollout_status_post",
|
|
23058
|
+
requestId: "config-degraded",
|
|
23059
|
+
agentName: c.agent,
|
|
23060
|
+
text
|
|
23061
|
+
}) + `
|
|
23062
|
+
`, () => {
|
|
23063
|
+
client.end();
|
|
23064
|
+
finish(true);
|
|
23065
|
+
});
|
|
23066
|
+
} catch (e) {
|
|
23067
|
+
finish(false, `write failed: ${e.message}`);
|
|
23068
|
+
}
|
|
23069
|
+
});
|
|
23070
|
+
client.on("timeout", () => finish(false, "connect timed out"));
|
|
23071
|
+
client.on("error", (e) => finish(false, `socket error: ${e.message}`));
|
|
23072
|
+
});
|
|
23073
|
+
return (async () => {
|
|
23074
|
+
for (const c of candidates) {
|
|
23075
|
+
if (await tryOne(c))
|
|
23076
|
+
return c.agent;
|
|
23077
|
+
}
|
|
23078
|
+
if (candidates.length > 0) {
|
|
23079
|
+
log("config-degraded: every candidate gateway socket failed; relying on marker file");
|
|
23080
|
+
}
|
|
23081
|
+
return null;
|
|
23082
|
+
})();
|
|
23083
|
+
}
|
|
23084
|
+
async function waitForConfigRecovery(opts) {
|
|
23085
|
+
const now = opts.nowFn ?? Date.now;
|
|
23086
|
+
const retryMs = opts.retryMs ?? CONFIG_DEGRADED_RETRY_MS;
|
|
23087
|
+
const hostdDir = join(opts.homeDir, ".switchroom", "hostd");
|
|
23088
|
+
const degradedMarkerPath = join(hostdDir, CONFIG_DEGRADED_MARKER_FILENAME);
|
|
23089
|
+
const pendingMarkerPath = join(hostdDir, SELF_BUMP_MARKER_FILENAME);
|
|
23090
|
+
const persistedSince = readDegradedSince(degradedMarkerPath);
|
|
23091
|
+
const enteredAt = Math.min(persistedSince ?? now(), now());
|
|
23092
|
+
if (persistedSince !== null) {
|
|
23093
|
+
opts.log(`config-degraded: resuming an existing degraded window (since ` + `${new Date(enteredAt).toISOString()}, persisted in ${degradedMarkerPath})`);
|
|
23094
|
+
}
|
|
23095
|
+
try {
|
|
23096
|
+
mkdirSync(hostdDir, { recursive: true, mode: 448 });
|
|
23097
|
+
writeFileSync(degradedMarkerPath + ".tmp", JSON.stringify(buildDegradedMarker(opts.initialError, enteredAt), null, 2) + `
|
|
23098
|
+
`, { mode: 384 });
|
|
23099
|
+
renameSync(degradedMarkerPath + ".tmp", degradedMarkerPath);
|
|
23100
|
+
} catch (e) {
|
|
23101
|
+
opts.log(`config-degraded: could not write marker ${degradedMarkerPath}: ${e.message}`);
|
|
23102
|
+
}
|
|
23103
|
+
try {
|
|
23104
|
+
opts.notify(formatDegradedNotice(opts.initialError));
|
|
23105
|
+
} catch (e) {
|
|
23106
|
+
opts.log(`config-degraded: notify threw: ${e.message}`);
|
|
23107
|
+
}
|
|
23108
|
+
opts.log(`config-degraded: entering degraded mode — ${opts.initialError.message}. ` + `Retrying every ${Math.round(retryMs / 1000)}s` + (opts.configPath ? ` and on change to ${opts.configPath}` : "") + `. Marker: ${degradedMarkerPath}`);
|
|
23109
|
+
const config = await new Promise((resolve4, reject) => {
|
|
23110
|
+
let timer = null;
|
|
23111
|
+
let settled = false;
|
|
23112
|
+
const cleanup = () => {
|
|
23113
|
+
if (timer)
|
|
23114
|
+
clearInterval(timer);
|
|
23115
|
+
if (opts.configPath) {
|
|
23116
|
+
try {
|
|
23117
|
+
unwatchFile(opts.configPath, onFileChange);
|
|
23118
|
+
} catch {}
|
|
23119
|
+
}
|
|
23120
|
+
};
|
|
23121
|
+
const attempt = (why) => {
|
|
23122
|
+
if (settled)
|
|
23123
|
+
return;
|
|
23124
|
+
try {
|
|
23125
|
+
const cfg = opts.loadFn();
|
|
23126
|
+
settled = true;
|
|
23127
|
+
cleanup();
|
|
23128
|
+
resolve4(cfg);
|
|
23129
|
+
} catch (err) {
|
|
23130
|
+
if (err instanceof ConfigError) {
|
|
23131
|
+
opts.log(`config-degraded: reload attempt (${why}) still failing — ${err.message}`);
|
|
23132
|
+
return;
|
|
23133
|
+
}
|
|
23134
|
+
settled = true;
|
|
23135
|
+
cleanup();
|
|
23136
|
+
reject(err);
|
|
23137
|
+
}
|
|
23138
|
+
};
|
|
23139
|
+
const onFileChange = () => attempt("file change");
|
|
23140
|
+
timer = setInterval(() => attempt("interval"), retryMs);
|
|
23141
|
+
if (opts.configPath) {
|
|
23142
|
+
try {
|
|
23143
|
+
watchFile(opts.configPath, { interval: 5000 }, onFileChange);
|
|
23144
|
+
} catch (e) {
|
|
23145
|
+
opts.log(`config-degraded: could not watch ${opts.configPath}: ${e.message}`);
|
|
23146
|
+
}
|
|
23147
|
+
}
|
|
23148
|
+
});
|
|
23149
|
+
const degradedMs = Math.max(0, now() - enteredAt);
|
|
23150
|
+
let pendingOutcome = null;
|
|
23151
|
+
try {
|
|
23152
|
+
if (existsSync4(pendingMarkerPath)) {
|
|
23153
|
+
const raw = readFileSync3(pendingMarkerPath, "utf8");
|
|
23154
|
+
const marker = parsePendingRolloutMarker(raw);
|
|
23155
|
+
if (marker && degradedMs <= 0) {
|
|
23156
|
+
pendingOutcome = {
|
|
23157
|
+
pin: marker.pin,
|
|
23158
|
+
ageMs: Math.max(0, now() - Date.parse(marker.created_at)),
|
|
23159
|
+
preserved: true
|
|
23160
|
+
};
|
|
23161
|
+
} else if (marker) {
|
|
23162
|
+
const withinCap = degradedMs <= MAX_PRESERVED_DEGRADED_MS;
|
|
23163
|
+
const shifted = withinCap ? shiftPendingRolloutMarker(raw, degradedMs) : null;
|
|
23164
|
+
if (shifted !== null) {
|
|
23165
|
+
writeFileSync(pendingMarkerPath + ".tmp", shifted, { mode: 384 });
|
|
23166
|
+
renameSync(pendingMarkerPath + ".tmp", pendingMarkerPath);
|
|
23167
|
+
pendingOutcome = {
|
|
23168
|
+
pin: marker.pin,
|
|
23169
|
+
ageMs: Math.max(0, now() - (Date.parse(marker.created_at) + degradedMs)),
|
|
23170
|
+
preserved: true
|
|
23171
|
+
};
|
|
23172
|
+
opts.log(`config-degraded: shifted pending-rollout marker (${marker.pin}) ` + `created_at by ${Math.round(degradedMs / 1000)}s so the degraded ` + `window does not count against the resume cutoff`);
|
|
23173
|
+
} else {
|
|
23174
|
+
pendingOutcome = {
|
|
23175
|
+
pin: marker.pin,
|
|
23176
|
+
ageMs: Math.max(0, now() - Date.parse(marker.created_at)),
|
|
23177
|
+
preserved: false
|
|
23178
|
+
};
|
|
23179
|
+
if (!withinCap) {
|
|
23180
|
+
opts.log(`config-degraded: NOT preserving pending-rollout marker ` + `(${marker.pin}) — degraded ${Math.round(degradedMs / 60000)}m, ` + `past the ${Math.round(MAX_PRESERVED_DEGRADED_MS / 3600000)}h ` + `preservation cap; the roll will not auto-resume`);
|
|
23181
|
+
}
|
|
23182
|
+
}
|
|
23183
|
+
}
|
|
23184
|
+
}
|
|
23185
|
+
} catch (e) {
|
|
23186
|
+
opts.log(`config-degraded: could not preserve pending-rollout marker: ${e.message}`);
|
|
23187
|
+
}
|
|
23188
|
+
try {
|
|
23189
|
+
if (existsSync4(degradedMarkerPath))
|
|
23190
|
+
unlinkSync(degradedMarkerPath);
|
|
23191
|
+
} catch (e) {
|
|
23192
|
+
opts.log(`config-degraded: could not remove marker ${degradedMarkerPath}: ${e.message}`);
|
|
23193
|
+
}
|
|
23194
|
+
try {
|
|
23195
|
+
opts.notify(formatRecoveredNotice(degradedMs, pendingOutcome));
|
|
23196
|
+
} catch (e) {
|
|
23197
|
+
opts.log(`config-degraded: recovery notify threw: ${e.message}`);
|
|
23198
|
+
}
|
|
23199
|
+
opts.log(`config-degraded: config loads again after ${Math.round(degradedMs / 1000)}s — resuming normal boot`);
|
|
23200
|
+
return config;
|
|
23201
|
+
}
|
|
23202
|
+
|
|
22800
23203
|
// src/agents/compose.ts
|
|
22801
23204
|
import { createHash } from "node:crypto";
|
|
22802
23205
|
|
|
22803
23206
|
// src/agents/scaffold.ts
|
|
22804
|
-
import { join as
|
|
23207
|
+
import { join as join5, resolve as resolve5 } from "node:path";
|
|
22805
23208
|
|
|
22806
23209
|
// src/config/timezone.ts
|
|
22807
23210
|
var CONTAINER_DEFAULT_UTC_ZONES = new Set([
|
|
@@ -22812,7 +23215,7 @@ var CONTAINER_DEFAULT_UTC_ZONES = new Set([
|
|
|
22812
23215
|
]);
|
|
22813
23216
|
|
|
22814
23217
|
// src/cli/agent-config.ts
|
|
22815
|
-
import { join } from "node:path";
|
|
23218
|
+
import { join as join2 } from "node:path";
|
|
22816
23219
|
import { homedir as homedir2 } from "node:os";
|
|
22817
23220
|
|
|
22818
23221
|
// src/vault/broker/acl.ts
|
|
@@ -22823,12 +23226,12 @@ var WEBKITE_VAULT_KEYS = new Set([
|
|
|
22823
23226
|
]);
|
|
22824
23227
|
|
|
22825
23228
|
// src/cli/agent-config.ts
|
|
22826
|
-
var AUDIT_ROOT =
|
|
23229
|
+
var AUDIT_ROOT = join2(homedir2(), ".switchroom", "audit");
|
|
22827
23230
|
|
|
22828
23231
|
// src/agents/profiles.ts
|
|
22829
23232
|
var import_handlebars = __toESM(require_lib(), 1);
|
|
22830
|
-
import { readFileSync as
|
|
22831
|
-
import { resolve as resolve4, join as
|
|
23233
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync5, readdirSync as readdirSync2, statSync as statSync2, copyFileSync, mkdirSync as mkdirSync2, realpathSync } from "node:fs";
|
|
23234
|
+
import { resolve as resolve4, join as join3, sep as pathSep } from "node:path";
|
|
22832
23235
|
var PROFILES_ROOT = resolve4(import.meta.dirname, "../../profiles");
|
|
22833
23236
|
import_handlebars.default.registerHelper("json", (value) => {
|
|
22834
23237
|
return new import_handlebars.default.SafeString(JSON.stringify(value, null, 2));
|
|
@@ -22839,9 +23242,9 @@ import_handlebars.default.registerHelper("isNumber", (value) => {
|
|
|
22839
23242
|
var SHARED_FRAGMENTS_DIR = resolve4(PROFILES_ROOT, "_shared");
|
|
22840
23243
|
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol"];
|
|
22841
23244
|
for (const name of SHARED_FRAGMENTS) {
|
|
22842
|
-
const fragPath =
|
|
22843
|
-
if (
|
|
22844
|
-
import_handlebars.default.registerPartial(name,
|
|
23245
|
+
const fragPath = join3(SHARED_FRAGMENTS_DIR, `${name}.md.hbs`);
|
|
23246
|
+
if (existsSync5(fragPath)) {
|
|
23247
|
+
import_handlebars.default.registerPartial(name, readFileSync4(fragPath, "utf-8"));
|
|
22845
23248
|
}
|
|
22846
23249
|
}
|
|
22847
23250
|
|
|
@@ -22850,18 +23253,22 @@ var tails = new Map;
|
|
|
22850
23253
|
|
|
22851
23254
|
// src/agents/inject.ts
|
|
22852
23255
|
var INJECT_COMMANDS = new Map([
|
|
22853
|
-
["/cost", { description: "Show session cost", expectsOutput: true, dialog: true }],
|
|
22854
|
-
["/status", { description: "Show session status", expectsOutput: true, dialog: true }],
|
|
22855
|
-
["/usage", { description: "Show plan quota", expectsOutput: true, dialog: true }],
|
|
22856
|
-
["/hooks", { description: "List configured hooks", expectsOutput: true, dialog: true }],
|
|
22857
|
-
["/memory", { description: "Open memory picker", expectsOutput: true, dialog: true }],
|
|
22858
|
-
["/
|
|
23256
|
+
["/cost", { description: "Show session cost", expectsOutput: true, dialog: true, argsAllowed: false }],
|
|
23257
|
+
["/status", { description: "Show session status", expectsOutput: true, dialog: true, argsAllowed: false }],
|
|
23258
|
+
["/usage", { description: "Show plan quota", expectsOutput: true, dialog: true, argsAllowed: false }],
|
|
23259
|
+
["/hooks", { description: "List configured hooks", expectsOutput: true, dialog: true, argsAllowed: false }],
|
|
23260
|
+
["/memory", { description: "Open memory picker", expectsOutput: true, dialog: true, argsAllowed: false }],
|
|
23261
|
+
["/help", { description: "Show help / command discovery", expectsOutput: true, dialog: true, argsAllowed: false }],
|
|
23262
|
+
["/context", { description: "Show context window usage", expectsOutput: true, argsAllowed: false }],
|
|
23263
|
+
["/release-notes", { description: "Show release-notes version list", expectsOutput: true, dialog: true, argsAllowed: false }],
|
|
23264
|
+
["/model", { description: "Open model picker", expectsOutput: true, argsAllowed: true }],
|
|
22859
23265
|
[
|
|
22860
23266
|
"/clear",
|
|
22861
23267
|
{
|
|
22862
23268
|
description: "Clear session screen",
|
|
22863
23269
|
expectsOutput: false,
|
|
22864
|
-
silentNote: "context cleared — fresh slate"
|
|
23270
|
+
silentNote: "context cleared — fresh slate",
|
|
23271
|
+
argsAllowed: false
|
|
22865
23272
|
}
|
|
22866
23273
|
],
|
|
22867
23274
|
[
|
|
@@ -22869,7 +23276,8 @@ var INJECT_COMMANDS = new Map([
|
|
|
22869
23276
|
{
|
|
22870
23277
|
description: "Compact conversation history",
|
|
22871
23278
|
expectsOutput: false,
|
|
22872
|
-
silentNote: "compaction runs silently"
|
|
23279
|
+
silentNote: "compaction runs silently",
|
|
23280
|
+
argsAllowed: false
|
|
22873
23281
|
}
|
|
22874
23282
|
]
|
|
22875
23283
|
]);
|
|
@@ -22879,6 +23287,15 @@ var INJECT_BLOCKED = new Map([
|
|
|
22879
23287
|
["/logout", { reason: "would terminate the agent's auth session" }],
|
|
22880
23288
|
["/exit", { reason: "would kill the agent process" }],
|
|
22881
23289
|
["/quit", { reason: "would kill the agent process" }],
|
|
23290
|
+
["/upgrade", { reason: "mutates the Claude Code installation" }],
|
|
23291
|
+
["/init", { reason: "generates/overwrites CLAUDE.md and runs a model turn" }],
|
|
23292
|
+
["/mcp", { reason: "opens an interactive MCP server management dialog" }],
|
|
23293
|
+
["/permissions", { reason: "opens an interactive permissions editor that mutates tool policy" }],
|
|
23294
|
+
["/install-github-app", { reason: "runs a network/OAuth install flow" }],
|
|
23295
|
+
["/add-dir", { reason: "mutates the session's working-directory set" }],
|
|
23296
|
+
["/terminal-setup", { reason: "mutates terminal keybinding configuration" }],
|
|
23297
|
+
["/privacy-settings", { reason: "opens an interactive privacy-settings dialog" }],
|
|
23298
|
+
["/bug", { reason: "submits a bug report over the network" }],
|
|
22882
23299
|
[
|
|
22883
23300
|
"/effort",
|
|
22884
23301
|
{
|
|
@@ -22896,6 +23313,7 @@ var HINDSIGHT_CONSUMER_NAME = "hindsight";
|
|
|
22896
23313
|
var HINDSIGHT_IMAGE_REPO = "ghcr.io/switchroom/switchroom-hindsight";
|
|
22897
23314
|
var HINDSIGHT_IMAGE = `${HINDSIGHT_IMAGE_REPO}:latest`;
|
|
22898
23315
|
var HINDSIGHT_BROKER_SOCK_VOLUME = `auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock`;
|
|
23316
|
+
var HINDSIGHT_CREDS_MIRROR_VOLUME = `consumer-creds-${HINDSIGHT_CONSUMER_NAME}`;
|
|
22899
23317
|
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)';
|
|
22900
23318
|
var HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
|
|
22901
23319
|
|
|
@@ -22928,8 +23346,10 @@ var KNOWN_VAULT_ARTIFACT_NAMES = new Set([
|
|
|
22928
23346
|
var SCRYPT_MAXMEM = 128 * 1024 * 1024;
|
|
22929
23347
|
|
|
22930
23348
|
// src/vault/broker/client.ts
|
|
23349
|
+
import * as net from "node:net";
|
|
23350
|
+
import * as fs from "node:fs";
|
|
22931
23351
|
import { homedir as homedir3 } from "node:os";
|
|
22932
|
-
import { join as
|
|
23352
|
+
import { join as join4 } from "node:path";
|
|
22933
23353
|
|
|
22934
23354
|
// src/broker-common/peercred-path.ts
|
|
22935
23355
|
function matchSocketName(socketPath, patterns, maxNameLen) {
|
|
@@ -23140,7 +23560,8 @@ var ErrorCode = exports_external.enum([
|
|
|
23140
23560
|
"DENIED",
|
|
23141
23561
|
"UNKNOWN_KEY",
|
|
23142
23562
|
"BAD_REQUEST",
|
|
23143
|
-
"INTERNAL"
|
|
23563
|
+
"INTERNAL",
|
|
23564
|
+
"AUDIT_UNAVAILABLE"
|
|
23144
23565
|
]);
|
|
23145
23566
|
var OkEntryResponseSchema = exports_external.object({
|
|
23146
23567
|
ok: exports_external.literal(true),
|
|
@@ -23277,16 +23698,123 @@ var ResponseSchema = exports_external.union([
|
|
|
23277
23698
|
OkApprovalConsumeRecordResponseSchema,
|
|
23278
23699
|
ErrorResponseSchema
|
|
23279
23700
|
]);
|
|
23701
|
+
function encodeRequest(req) {
|
|
23702
|
+
const json = JSON.stringify(req);
|
|
23703
|
+
if (Buffer.byteLength(json, "utf8") > MAX_FRAME_BYTES) {
|
|
23704
|
+
throw new Error(`Request frame too large (${Buffer.byteLength(json, "utf8")} bytes; max ${MAX_FRAME_BYTES})`);
|
|
23705
|
+
}
|
|
23706
|
+
return json + `
|
|
23707
|
+
`;
|
|
23708
|
+
}
|
|
23709
|
+
function decodeResponse(line) {
|
|
23710
|
+
if (Buffer.byteLength(line, "utf8") > MAX_FRAME_BYTES) {
|
|
23711
|
+
throw new RangeError(`Response frame too large (${Buffer.byteLength(line, "utf8")} bytes; max ${MAX_FRAME_BYTES})`);
|
|
23712
|
+
}
|
|
23713
|
+
const obj = JSON.parse(line);
|
|
23714
|
+
return ResponseSchema.parse(obj);
|
|
23715
|
+
}
|
|
23716
|
+
|
|
23717
|
+
// src/runtime-mode.ts
|
|
23718
|
+
function isDockerRuntime() {
|
|
23719
|
+
return process.env.SWITCHROOM_RUNTIME === "docker";
|
|
23720
|
+
}
|
|
23280
23721
|
|
|
23281
23722
|
// src/vault/broker/client.ts
|
|
23282
|
-
var
|
|
23283
|
-
var
|
|
23723
|
+
var DEFAULT_TIMEOUT_MS = 2000;
|
|
23724
|
+
var LEGACY_SOCKET_PATH = join4(homedir3(), ".switchroom", "vault-broker.sock");
|
|
23725
|
+
var OPERATOR_SOCKET_PATH = join4(homedir3(), ".switchroom", "broker-operator", "sock");
|
|
23726
|
+
function defaultBrokerSocketPath() {
|
|
23727
|
+
if (fs.existsSync(OPERATOR_SOCKET_PATH))
|
|
23728
|
+
return OPERATOR_SOCKET_PATH;
|
|
23729
|
+
if (isDockerRuntime())
|
|
23730
|
+
return OPERATOR_SOCKET_PATH;
|
|
23731
|
+
return LEGACY_SOCKET_PATH;
|
|
23732
|
+
}
|
|
23733
|
+
function resolveBrokerSocketPath(opts) {
|
|
23734
|
+
if (opts?.socket)
|
|
23735
|
+
return opts.socket;
|
|
23736
|
+
const env = process.env.SWITCHROOM_VAULT_BROKER_SOCK;
|
|
23737
|
+
if (env)
|
|
23738
|
+
return env;
|
|
23739
|
+
if (opts?.vaultBrokerSocket)
|
|
23740
|
+
return opts.vaultBrokerSocket;
|
|
23741
|
+
return defaultBrokerSocketPath();
|
|
23742
|
+
}
|
|
23743
|
+
async function rpcRaw(req, opts) {
|
|
23744
|
+
return rpc(req, opts);
|
|
23745
|
+
}
|
|
23746
|
+
async function rpc(req, opts) {
|
|
23747
|
+
const socketPath = resolveBrokerSocketPath(opts);
|
|
23748
|
+
const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
23749
|
+
return new Promise((resolve5) => {
|
|
23750
|
+
let settled = false;
|
|
23751
|
+
const settle = (val) => {
|
|
23752
|
+
if (settled)
|
|
23753
|
+
return;
|
|
23754
|
+
settled = true;
|
|
23755
|
+
resolve5(val);
|
|
23756
|
+
};
|
|
23757
|
+
const client = new net.Socket;
|
|
23758
|
+
const timer = setTimeout(() => {
|
|
23759
|
+
client.destroy();
|
|
23760
|
+
settle({ kind: "unreachable", msg: `broker did not respond within ${timeoutMs}ms` });
|
|
23761
|
+
}, timeoutMs);
|
|
23762
|
+
client.on("error", (err) => {
|
|
23763
|
+
clearTimeout(timer);
|
|
23764
|
+
const code = err.code ?? "ERR";
|
|
23765
|
+
let msg;
|
|
23766
|
+
if (code === "ENOENT")
|
|
23767
|
+
msg = "broker socket not found (is the daemon running?)";
|
|
23768
|
+
else if (code === "ECONNREFUSED")
|
|
23769
|
+
msg = "broker socket exists but refused connection";
|
|
23770
|
+
else if (code === "EACCES")
|
|
23771
|
+
msg = "broker socket access denied (wrong UID?)";
|
|
23772
|
+
else
|
|
23773
|
+
msg = `broker connection failed: ${err.message}`;
|
|
23774
|
+
settle({ kind: "unreachable", msg });
|
|
23775
|
+
});
|
|
23776
|
+
let buffer = "";
|
|
23777
|
+
client.on("data", (chunk) => {
|
|
23778
|
+
buffer += chunk.toString("utf8");
|
|
23779
|
+
const newlineIdx = buffer.indexOf(`
|
|
23780
|
+
`);
|
|
23781
|
+
if (newlineIdx !== -1) {
|
|
23782
|
+
const line = buffer.slice(0, newlineIdx).trimEnd();
|
|
23783
|
+
clearTimeout(timer);
|
|
23784
|
+
client.destroy();
|
|
23785
|
+
try {
|
|
23786
|
+
const resp = decodeResponse(line);
|
|
23787
|
+
settle({ kind: "response", resp });
|
|
23788
|
+
} catch (err) {
|
|
23789
|
+
settle({
|
|
23790
|
+
kind: "unreachable",
|
|
23791
|
+
msg: `unparseable broker response: ${err instanceof Error ? err.message : String(err)}`
|
|
23792
|
+
});
|
|
23793
|
+
}
|
|
23794
|
+
}
|
|
23795
|
+
});
|
|
23796
|
+
client.on("connect", () => {
|
|
23797
|
+
try {
|
|
23798
|
+
client.write(encodeRequest(req));
|
|
23799
|
+
} catch (err) {
|
|
23800
|
+
clearTimeout(timer);
|
|
23801
|
+
client.destroy();
|
|
23802
|
+
settle({
|
|
23803
|
+
kind: "unreachable",
|
|
23804
|
+
msg: `failed to send request: ${err instanceof Error ? err.message : String(err)}`
|
|
23805
|
+
});
|
|
23806
|
+
}
|
|
23807
|
+
});
|
|
23808
|
+
client.connect({ path: socketPath });
|
|
23809
|
+
});
|
|
23810
|
+
}
|
|
23284
23811
|
|
|
23285
23812
|
// src/vault/resolver.ts
|
|
23286
23813
|
var materializedDirs = new Set;
|
|
23287
23814
|
|
|
23288
23815
|
// src/agents/scaffold.ts
|
|
23289
23816
|
var REPO_ROOT = resolve5(import.meta.dirname, "../..");
|
|
23817
|
+
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.";
|
|
23290
23818
|
var SWITCHROOM_OWNED_SETTINGS_KEYS = new Set([
|
|
23291
23819
|
"permissions",
|
|
23292
23820
|
"mcpServers",
|
|
@@ -23317,20 +23845,20 @@ import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
|
|
|
23317
23845
|
import { mkdir, chmod, chown, unlink, appendFile } from "node:fs/promises";
|
|
23318
23846
|
import {
|
|
23319
23847
|
readdirSync as readdirSync3,
|
|
23320
|
-
existsSync as
|
|
23321
|
-
readFileSync as
|
|
23322
|
-
writeFileSync as
|
|
23323
|
-
renameSync as
|
|
23324
|
-
mkdirSync as
|
|
23848
|
+
existsSync as existsSync11,
|
|
23849
|
+
readFileSync as readFileSync9,
|
|
23850
|
+
writeFileSync as writeFileSync5,
|
|
23851
|
+
renameSync as renameSync3,
|
|
23852
|
+
mkdirSync as mkdirSync4,
|
|
23325
23853
|
openSync as openSync3,
|
|
23326
23854
|
ftruncateSync,
|
|
23327
23855
|
writeSync as writeSync2,
|
|
23328
23856
|
fsyncSync as fsyncSync2,
|
|
23329
23857
|
closeSync as closeSync3,
|
|
23330
23858
|
copyFileSync as copyFileSync2,
|
|
23331
|
-
unlinkSync
|
|
23859
|
+
unlinkSync as unlinkSync2
|
|
23332
23860
|
} from "node:fs";
|
|
23333
|
-
import { join as
|
|
23861
|
+
import { join as join10, dirname as dirname6, resolve as resolve8 } from "node:path";
|
|
23334
23862
|
import { createHash as createHash5, randomUUID as randomUUID2, randomBytes } from "node:crypto";
|
|
23335
23863
|
|
|
23336
23864
|
// src/host-control/protocol.ts
|
|
@@ -23339,7 +23867,8 @@ var IDEMPOTENCY_WINDOW_MS = 15000;
|
|
|
23339
23867
|
var RequestEnvelope = {
|
|
23340
23868
|
v: exports_external.literal(1),
|
|
23341
23869
|
request_id: exports_external.string().min(1).max(128),
|
|
23342
|
-
idempotency_key: exports_external.string().min(1).max(128).optional()
|
|
23870
|
+
idempotency_key: exports_external.string().min(1).max(128).optional(),
|
|
23871
|
+
operator_passphrase: exports_external.string().min(1).max(1024).optional()
|
|
23343
23872
|
};
|
|
23344
23873
|
var AgentRestartRequestSchema = exports_external.object({
|
|
23345
23874
|
...RequestEnvelope,
|
|
@@ -28019,10 +28548,10 @@ class PostHog extends PostHogBackendClient {
|
|
|
28019
28548
|
|
|
28020
28549
|
// src/analytics/posthog.ts
|
|
28021
28550
|
import {
|
|
28022
|
-
existsSync as
|
|
28023
|
-
mkdirSync as
|
|
28024
|
-
readFileSync as
|
|
28025
|
-
writeFileSync as
|
|
28551
|
+
existsSync as existsSync7,
|
|
28552
|
+
mkdirSync as mkdirSync3,
|
|
28553
|
+
readFileSync as readFileSync6,
|
|
28554
|
+
writeFileSync as writeFileSync3
|
|
28026
28555
|
} from "node:fs";
|
|
28027
28556
|
import { dirname as dirname2 } from "node:path";
|
|
28028
28557
|
import { randomUUID } from "node:crypto";
|
|
@@ -28040,8 +28569,8 @@ function getDistinctId() {
|
|
|
28040
28569
|
return cachedDistinctId;
|
|
28041
28570
|
const path = resolveStatePath("analytics-id");
|
|
28042
28571
|
try {
|
|
28043
|
-
if (
|
|
28044
|
-
const existing =
|
|
28572
|
+
if (existsSync7(path)) {
|
|
28573
|
+
const existing = readFileSync6(path, "utf-8").trim();
|
|
28045
28574
|
if (existing) {
|
|
28046
28575
|
cachedDistinctId = existing;
|
|
28047
28576
|
return existing;
|
|
@@ -28051,8 +28580,8 @@ function getDistinctId() {
|
|
|
28051
28580
|
const id = randomUUID();
|
|
28052
28581
|
cachedDistinctId = id;
|
|
28053
28582
|
try {
|
|
28054
|
-
|
|
28055
|
-
|
|
28583
|
+
mkdirSync3(dirname2(path), { recursive: true });
|
|
28584
|
+
writeFileSync3(path, id, "utf-8");
|
|
28056
28585
|
} catch {}
|
|
28057
28586
|
return id;
|
|
28058
28587
|
}
|
|
@@ -28660,7 +29189,7 @@ function redactedMarker(ruleId) {
|
|
|
28660
29189
|
return `[REDACTED:${trimmed}]`;
|
|
28661
29190
|
}
|
|
28662
29191
|
// src/cli/install-detect.ts
|
|
28663
|
-
import * as
|
|
29192
|
+
import * as fs2 from "node:fs";
|
|
28664
29193
|
import * as path from "node:path";
|
|
28665
29194
|
import * as os from "node:os";
|
|
28666
29195
|
var BIN_PATH = "/usr/local/bin/switchroom";
|
|
@@ -28674,12 +29203,12 @@ function detectInstallType() {
|
|
|
28674
29203
|
try {
|
|
28675
29204
|
const repoArtifact = sourceArtifactPath();
|
|
28676
29205
|
const distPrefix = sourceDistPrefix();
|
|
28677
|
-
const binExists =
|
|
28678
|
-
const repoExists =
|
|
29206
|
+
const binExists = fs2.existsSync(BIN_PATH);
|
|
29207
|
+
const repoExists = fs2.existsSync(repoArtifact);
|
|
28679
29208
|
if (binExists) {
|
|
28680
|
-
const lst =
|
|
29209
|
+
const lst = fs2.lstatSync(BIN_PATH);
|
|
28681
29210
|
if (lst.isSymbolicLink()) {
|
|
28682
|
-
const target =
|
|
29211
|
+
const target = fs2.readlinkSync(BIN_PATH);
|
|
28683
29212
|
const resolved = path.isAbsolute(target) ? target : path.resolve(path.dirname(BIN_PATH), target);
|
|
28684
29213
|
if (resolved.startsWith(distPrefix)) {
|
|
28685
29214
|
return {
|
|
@@ -28715,40 +29244,21 @@ var import_yaml3 = __toESM(require_dist(), 1);
|
|
|
28715
29244
|
// src/cli/rollout.ts
|
|
28716
29245
|
init_atomic();
|
|
28717
29246
|
|
|
28718
|
-
// src/config/release-resolve.ts
|
|
28719
|
-
function parseSemverTag(tag) {
|
|
28720
|
-
if (!tag)
|
|
28721
|
-
return null;
|
|
28722
|
-
const m = /^v(\d+)\.(\d+)\.(\d+)$/.exec(tag.trim());
|
|
28723
|
-
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
28724
|
-
}
|
|
28725
|
-
function compareReleaseTags(a, b) {
|
|
28726
|
-
const pa = parseSemverTag(a);
|
|
28727
|
-
const pb = parseSemverTag(b);
|
|
28728
|
-
if (!pa || !pb)
|
|
28729
|
-
return null;
|
|
28730
|
-
for (let i = 0;i < 3; i++) {
|
|
28731
|
-
if (pa[i] !== pb[i])
|
|
28732
|
-
return pa[i] < pb[i] ? -1 : 1;
|
|
28733
|
-
}
|
|
28734
|
-
return 0;
|
|
28735
|
-
}
|
|
28736
|
-
|
|
28737
29247
|
// src/cli/resolve-version.ts
|
|
28738
|
-
import { existsSync as
|
|
28739
|
-
import { dirname as dirname4, join as
|
|
29248
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
|
|
29249
|
+
import { dirname as dirname4, join as join7 } from "node:path";
|
|
28740
29250
|
|
|
28741
29251
|
// src/build-info.ts
|
|
28742
|
-
var VERSION = "0.18.
|
|
29252
|
+
var VERSION = "0.18.12";
|
|
28743
29253
|
|
|
28744
29254
|
// src/cli/resolve-version.ts
|
|
28745
29255
|
function readPackageVersion() {
|
|
28746
29256
|
let dir = import.meta.dirname;
|
|
28747
29257
|
for (let i = 0;i < 12 && dir && dir !== "/"; i++) {
|
|
28748
|
-
const p =
|
|
28749
|
-
if (
|
|
29258
|
+
const p = join7(dir, "package.json");
|
|
29259
|
+
if (existsSync9(p)) {
|
|
28750
29260
|
try {
|
|
28751
|
-
const pkg = JSON.parse(
|
|
29261
|
+
const pkg = JSON.parse(readFileSync7(p, "utf-8"));
|
|
28752
29262
|
if (pkg?.name === "switchroom" && typeof pkg.version === "string") {
|
|
28753
29263
|
return pkg.version;
|
|
28754
29264
|
}
|
|
@@ -28809,6 +29319,8 @@ function parseAuditLine(line) {
|
|
|
28809
29319
|
entry.error = o.error;
|
|
28810
29320
|
if (typeof o.phase === "string")
|
|
28811
29321
|
entry.phase = o.phase;
|
|
29322
|
+
if (typeof o.method === "string")
|
|
29323
|
+
entry.method = o.method;
|
|
28812
29324
|
if (typeof o.stdout_tail === "string")
|
|
28813
29325
|
entry.stdout_tail = o.stdout_tail;
|
|
28814
29326
|
if (typeof o.stderr_tail === "string")
|
|
@@ -28895,6 +29407,7 @@ function parseRolloutPhaseLine(line) {
|
|
|
28895
29407
|
"agent-start",
|
|
28896
29408
|
"agent-done",
|
|
28897
29409
|
"persist-pin",
|
|
29410
|
+
"web-refresh",
|
|
28898
29411
|
"hostd-web-deferred",
|
|
28899
29412
|
"self-bump",
|
|
28900
29413
|
"self-bump-done"
|
|
@@ -28905,149 +29418,38 @@ function parseRolloutPhaseLine(line) {
|
|
|
28905
29418
|
return null;
|
|
28906
29419
|
const out = {
|
|
28907
29420
|
phase: o.phase,
|
|
28908
|
-
target: o.target
|
|
28909
|
-
};
|
|
28910
|
-
if (typeof o.agent === "string")
|
|
28911
|
-
out.agent = o.agent;
|
|
28912
|
-
if (typeof o.n === "number")
|
|
28913
|
-
out.n = o.n;
|
|
28914
|
-
if (typeof o.m === "number")
|
|
28915
|
-
out.m = o.m;
|
|
28916
|
-
return out;
|
|
28917
|
-
}
|
|
28918
|
-
var ROLLOUT_RESULT_SENTINEL = "SWITCHROOM_ROLLOUT_RESULT:";
|
|
28919
|
-
function parseRolloutResultLine(stdout) {
|
|
28920
|
-
const lines = stdout.split(`
|
|
28921
|
-
`);
|
|
28922
|
-
for (let i = lines.length - 1;i >= 0; i--) {
|
|
28923
|
-
const line = lines[i].trim();
|
|
28924
|
-
if (line.startsWith(ROLLOUT_RESULT_SENTINEL)) {
|
|
28925
|
-
try {
|
|
28926
|
-
return JSON.parse(line.slice(ROLLOUT_RESULT_SENTINEL.length));
|
|
28927
|
-
} catch {
|
|
28928
|
-
return null;
|
|
28929
|
-
}
|
|
28930
|
-
}
|
|
28931
|
-
}
|
|
28932
|
-
return null;
|
|
28933
|
-
}
|
|
28934
|
-
|
|
28935
|
-
// src/host-control/self-bump.ts
|
|
28936
|
-
var SELF_BUMP_MARKER_FILENAME = "pending-rollout.json";
|
|
28937
|
-
var SELF_BUMP_HELPER_CONTAINER = "switchroom-hostd-selfbump";
|
|
28938
|
-
var SELF_BUMP_LOG_FILENAME = "selfbump.log";
|
|
28939
|
-
var SELF_BUMP_MARKER_MAX_AGE_MS = 15 * 60 * 1000;
|
|
28940
|
-
var SEMVER_PIN_RE = /^v\d+\.\d+\.\d+$/;
|
|
28941
|
-
function needsSelfBump(ownVersion, pin) {
|
|
28942
|
-
if (!ownVersion)
|
|
28943
|
-
return false;
|
|
28944
|
-
const cmp = compareReleaseTags(`v${ownVersion.trim().replace(/^v/, "")}`, `v${pin.trim().replace(/^v/, "")}`);
|
|
28945
|
-
return cmp !== null && cmp < 0;
|
|
28946
|
-
}
|
|
28947
|
-
function bumpHostdComposeImageTag(yaml, pin) {
|
|
28948
|
-
const re = /^(\s*image:\s*)(\S*switchroom-hostd):(\S+)\s*$/m;
|
|
28949
|
-
const m = yaml.match(re);
|
|
28950
|
-
if (!m)
|
|
28951
|
-
return null;
|
|
28952
|
-
const oldImageRef = `${m[2]}:${m[3]}`;
|
|
28953
|
-
const newImageRef = `${m[2]}:${pin}`;
|
|
28954
|
-
return {
|
|
28955
|
-
yaml: yaml.replace(re, `$1${newImageRef}`),
|
|
28956
|
-
oldImageRef,
|
|
28957
|
-
newImageRef
|
|
28958
|
-
};
|
|
28959
|
-
}
|
|
28960
|
-
function encodePendingRolloutMarker(m) {
|
|
28961
|
-
return JSON.stringify(m, null, 2) + `
|
|
28962
|
-
`;
|
|
28963
|
-
}
|
|
28964
|
-
function parsePendingRolloutMarker(raw) {
|
|
28965
|
-
let obj;
|
|
28966
|
-
try {
|
|
28967
|
-
obj = JSON.parse(raw);
|
|
28968
|
-
} catch {
|
|
28969
|
-
return null;
|
|
28970
|
-
}
|
|
28971
|
-
if (typeof obj !== "object" || obj === null)
|
|
28972
|
-
return null;
|
|
28973
|
-
const o = obj;
|
|
28974
|
-
if (o.v !== 1)
|
|
28975
|
-
return null;
|
|
28976
|
-
if (typeof o.request_id !== "string" || o.request_id.length === 0)
|
|
28977
|
-
return null;
|
|
28978
|
-
if (typeof o.pin !== "string" || !SEMVER_PIN_RE.test(o.pin))
|
|
28979
|
-
return null;
|
|
28980
|
-
if (typeof o.created_at !== "string" || !Number.isFinite(Date.parse(o.created_at))) {
|
|
28981
|
-
return null;
|
|
28982
|
-
}
|
|
28983
|
-
if (typeof o.prior_hostd_version !== "string")
|
|
28984
|
-
return null;
|
|
28985
|
-
const caller = o.caller;
|
|
28986
|
-
if (typeof caller !== "object" || caller === null)
|
|
28987
|
-
return null;
|
|
28988
|
-
if (caller.kind === "agent") {
|
|
28989
|
-
if (typeof caller.name !== "string" || caller.name.length === 0)
|
|
28990
|
-
return null;
|
|
28991
|
-
} else if (caller.kind !== "operator") {
|
|
28992
|
-
return null;
|
|
28993
|
-
}
|
|
28994
|
-
if (o.agents !== undefined) {
|
|
28995
|
-
if (!Array.isArray(o.agents) || o.agents.length === 0 || !o.agents.every((a) => typeof a === "string" && /^[\w-]+$/.test(a))) {
|
|
28996
|
-
return null;
|
|
28997
|
-
}
|
|
28998
|
-
}
|
|
28999
|
-
if (o.skip_web !== undefined && typeof o.skip_web !== "boolean")
|
|
29000
|
-
return null;
|
|
29001
|
-
if (o.allow_downgrade !== undefined && typeof o.allow_downgrade !== "boolean") {
|
|
29002
|
-
return null;
|
|
29003
|
-
}
|
|
29004
|
-
return {
|
|
29005
|
-
v: 1,
|
|
29006
|
-
request_id: o.request_id,
|
|
29007
|
-
pin: o.pin,
|
|
29008
|
-
...o.agents !== undefined ? { agents: o.agents } : {},
|
|
29009
|
-
...o.skip_web !== undefined ? { skip_web: o.skip_web } : {},
|
|
29010
|
-
...o.allow_downgrade !== undefined ? { allow_downgrade: o.allow_downgrade } : {},
|
|
29011
|
-
caller: caller.kind === "agent" ? { kind: "agent", name: caller.name } : { kind: "operator" },
|
|
29012
|
-
created_at: o.created_at,
|
|
29013
|
-
prior_hostd_version: o.prior_hostd_version
|
|
29421
|
+
target: o.target
|
|
29014
29422
|
};
|
|
29423
|
+
if (typeof o.agent === "string")
|
|
29424
|
+
out.agent = o.agent;
|
|
29425
|
+
if (typeof o.n === "number")
|
|
29426
|
+
out.n = o.n;
|
|
29427
|
+
if (typeof o.m === "number")
|
|
29428
|
+
out.m = o.m;
|
|
29429
|
+
return out;
|
|
29015
29430
|
}
|
|
29016
|
-
|
|
29017
|
-
|
|
29018
|
-
|
|
29019
|
-
|
|
29020
|
-
|
|
29021
|
-
|
|
29022
|
-
|
|
29023
|
-
|
|
29024
|
-
|
|
29025
|
-
|
|
29026
|
-
|
|
29027
|
-
|
|
29028
|
-
|
|
29029
|
-
|
|
29030
|
-
|
|
29031
|
-
SELF_BUMP_HELPER_CONTAINER,
|
|
29032
|
-
"--label",
|
|
29033
|
-
"switchroom.hostd-selfbump=true",
|
|
29034
|
-
"-v",
|
|
29035
|
-
"/var/run/docker.sock:/var/run/docker.sock",
|
|
29036
|
-
"-v",
|
|
29037
|
-
`${opts.hostdDirHostPath}:${opts.hostdDirHostPath}`,
|
|
29038
|
-
"--entrypoint",
|
|
29039
|
-
"/bin/sh",
|
|
29040
|
-
opts.helperImage,
|
|
29041
|
-
"-c",
|
|
29042
|
-
script
|
|
29043
|
-
];
|
|
29431
|
+
var ROLLOUT_RESULT_SENTINEL = "SWITCHROOM_ROLLOUT_RESULT:";
|
|
29432
|
+
function parseRolloutResultLine(stdout) {
|
|
29433
|
+
const lines = stdout.split(`
|
|
29434
|
+
`);
|
|
29435
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
29436
|
+
const line = lines[i].trim();
|
|
29437
|
+
if (line.startsWith(ROLLOUT_RESULT_SENTINEL)) {
|
|
29438
|
+
try {
|
|
29439
|
+
return JSON.parse(line.slice(ROLLOUT_RESULT_SENTINEL.length));
|
|
29440
|
+
} catch {
|
|
29441
|
+
return null;
|
|
29442
|
+
}
|
|
29443
|
+
}
|
|
29444
|
+
}
|
|
29445
|
+
return null;
|
|
29044
29446
|
}
|
|
29045
29447
|
|
|
29046
29448
|
// node_modules/.bun/commander@13.1.0/node_modules/commander/esm.mjs
|
|
29047
29449
|
var import__3 = __toESM(require_commander(), 1);
|
|
29048
29450
|
|
|
29049
29451
|
// src/cli/update.ts
|
|
29050
|
-
import { join as
|
|
29452
|
+
import { join as join8, dirname as dirname5, resolve as resolve7 } from "node:path";
|
|
29051
29453
|
import { homedir as homedir5 } from "node:os";
|
|
29052
29454
|
|
|
29053
29455
|
// src/agents/lifecycle.ts
|
|
@@ -29144,7 +29546,7 @@ function getAllAgentStatuses(config) {
|
|
|
29144
29546
|
|
|
29145
29547
|
// src/cli/update.ts
|
|
29146
29548
|
init_atomic();
|
|
29147
|
-
var DEFAULT_COMPOSE_PATH =
|
|
29549
|
+
var DEFAULT_COMPOSE_PATH = join8(homedir5(), ".switchroom", "compose", "docker-compose.yml");
|
|
29148
29550
|
var UPDATE_RESULT_SENTINEL = "SWITCHROOM_UPDATE_RESULT:";
|
|
29149
29551
|
function parseUpdateResultLine(stdout) {
|
|
29150
29552
|
const idx = stdout.lastIndexOf(UPDATE_RESULT_SENTINEL);
|
|
@@ -29167,9 +29569,9 @@ function parseUpdateResultLine(stdout) {
|
|
|
29167
29569
|
|
|
29168
29570
|
// src/host-control/config-edit-validator.ts
|
|
29169
29571
|
var import_yaml5 = __toESM(require_dist(), 1);
|
|
29170
|
-
import { mkdtempSync, writeFileSync as
|
|
29572
|
+
import { mkdtempSync, writeFileSync as writeFileSync4, rmSync as rmSync2, existsSync as existsSync10, readFileSync as readFileSync8 } from "node:fs";
|
|
29171
29573
|
import { tmpdir } from "node:os";
|
|
29172
|
-
import { join as
|
|
29574
|
+
import { join as join9, isAbsolute as isAbsolute2, normalize, basename as basename2 } from "node:path";
|
|
29173
29575
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
29174
29576
|
import { isDeepStrictEqual } from "node:util";
|
|
29175
29577
|
var MAX_PATCH_BYTES = 1024 * 1024;
|
|
@@ -29279,31 +29681,57 @@ function validateShape(unifiedDiff, targetPath) {
|
|
|
29279
29681
|
};
|
|
29280
29682
|
}
|
|
29281
29683
|
}
|
|
29684
|
+
{
|
|
29685
|
+
let inHunk = false;
|
|
29686
|
+
let hunkHasContext = false;
|
|
29687
|
+
const failZeroContext = {
|
|
29688
|
+
ok: false,
|
|
29689
|
+
code: "E_PATCH_INVALID_SHAPE",
|
|
29690
|
+
detail: "zero-context hunk not allowed; regenerate the diff with context " + "lines (git diff default of 3)"
|
|
29691
|
+
};
|
|
29692
|
+
const scanLines = lines.length > 0 && lines[lines.length - 1] === "" ? lines.slice(0, -1) : lines;
|
|
29693
|
+
for (const ln of scanLines) {
|
|
29694
|
+
if (ln.startsWith("@@")) {
|
|
29695
|
+
if (inHunk && !hunkHasContext)
|
|
29696
|
+
return failZeroContext;
|
|
29697
|
+
inHunk = true;
|
|
29698
|
+
hunkHasContext = false;
|
|
29699
|
+
} else if (ln.startsWith("--- ") || ln.startsWith("+++ ")) {
|
|
29700
|
+
if (inHunk && !hunkHasContext)
|
|
29701
|
+
return failZeroContext;
|
|
29702
|
+
inHunk = false;
|
|
29703
|
+
} else if (inHunk) {
|
|
29704
|
+
if (ln.startsWith(" ") || ln === "")
|
|
29705
|
+
hunkHasContext = true;
|
|
29706
|
+
}
|
|
29707
|
+
}
|
|
29708
|
+
if (inHunk && !hunkHasContext)
|
|
29709
|
+
return failZeroContext;
|
|
29710
|
+
}
|
|
29282
29711
|
return null;
|
|
29283
29712
|
}
|
|
29284
29713
|
function applyPatch(unifiedDiff, configPath, gitBin) {
|
|
29285
|
-
if (!
|
|
29714
|
+
if (!existsSync10(configPath)) {
|
|
29286
29715
|
return {
|
|
29287
29716
|
ok: false,
|
|
29288
29717
|
code: "E_PATCH_APPLY_FAILED",
|
|
29289
29718
|
detail: `target config not found at ${configPath}`
|
|
29290
29719
|
};
|
|
29291
29720
|
}
|
|
29292
|
-
const liveContent =
|
|
29293
|
-
const scratchDir = mkdtempSync(
|
|
29721
|
+
const liveContent = readFileSync8(configPath, "utf8");
|
|
29722
|
+
const scratchDir = mkdtempSync(join9(tmpdir(), "config-propose-edit-"));
|
|
29294
29723
|
try {
|
|
29295
29724
|
const targetBasename = configPath.split("/").pop() ?? "switchroom.yaml";
|
|
29296
|
-
const scratchFile =
|
|
29297
|
-
|
|
29725
|
+
const scratchFile = join9(scratchDir, targetBasename);
|
|
29726
|
+
writeFileSync4(scratchFile, liveContent);
|
|
29298
29727
|
const normalizedDiff = normalizeDiffHeadersToBasename(unifiedDiff, targetBasename);
|
|
29299
|
-
const patchFile =
|
|
29300
|
-
|
|
29728
|
+
const patchFile = join9(scratchDir, "proposal.patch");
|
|
29729
|
+
writeFileSync4(patchFile, normalizedDiff);
|
|
29301
29730
|
const bin = gitBin ?? "git";
|
|
29302
29731
|
const baseArgs = [
|
|
29303
29732
|
"apply",
|
|
29304
29733
|
"--whitespace=nowarn",
|
|
29305
|
-
"--recount"
|
|
29306
|
-
"--unidiff-zero"
|
|
29734
|
+
"--recount"
|
|
29307
29735
|
];
|
|
29308
29736
|
const checkP1 = spawnSync2(bin, [...baseArgs.slice(0, 1), "--check", ...baseArgs.slice(1), "-p1", patchFile], { cwd: scratchDir, encoding: "utf8", timeout: 1e4 });
|
|
29309
29737
|
let pStrip = "-p1";
|
|
@@ -29327,7 +29755,7 @@ function applyPatch(unifiedDiff, configPath, gitBin) {
|
|
|
29327
29755
|
detail: `git apply failed: ${(real.stderr || "").trim().slice(0, 500)}`
|
|
29328
29756
|
};
|
|
29329
29757
|
}
|
|
29330
|
-
const after =
|
|
29758
|
+
const after = readFileSync8(scratchFile, "utf8");
|
|
29331
29759
|
return { ok: true, after };
|
|
29332
29760
|
} finally {
|
|
29333
29761
|
rmSync2(scratchDir, { recursive: true, force: true });
|
|
@@ -29525,7 +29953,7 @@ function validateConfigEdit(opts) {
|
|
|
29525
29953
|
return schemaErr;
|
|
29526
29954
|
let beforeData = {};
|
|
29527
29955
|
try {
|
|
29528
|
-
const beforeRaw =
|
|
29956
|
+
const beforeRaw = existsSync10(opts.configPath) ? readFileSync8(opts.configPath, "utf8") : "";
|
|
29529
29957
|
const beforeDoc = import_yaml5.parseDocument(beforeRaw, { merge: false, strict: false });
|
|
29530
29958
|
beforeData = beforeDoc.toJS();
|
|
29531
29959
|
} catch {
|
|
@@ -29771,19 +30199,21 @@ function phaseLine(s) {
|
|
|
29771
30199
|
case "apply":
|
|
29772
30200
|
return "applying — regenerating compose";
|
|
29773
30201
|
case "canary-start":
|
|
29774
|
-
return `canary — restarting ${s.agent
|
|
30202
|
+
return `canary — restarting ${s.agent ? `\`${s.agent}\`` : "canary agent"}`;
|
|
29775
30203
|
case "canary-pass":
|
|
29776
|
-
return `canary passed (${s.agent
|
|
30204
|
+
return `canary passed (${s.agent ? `\`${s.agent}\`` : "canary"}) — rolling the rest`;
|
|
29777
30205
|
case "canary-fail":
|
|
29778
|
-
return `canary failed (${s.agent
|
|
30206
|
+
return `canary failed (${s.agent ? `\`${s.agent}\`` : "canary"})`;
|
|
29779
30207
|
case "agent-start":
|
|
29780
|
-
return `agent ${s.n ?? "?"}/${s.m ?? "?"} — restarting ${s.agent
|
|
30208
|
+
return `agent ${s.n ?? "?"}/${s.m ?? "?"} — restarting ${s.agent ? `\`${s.agent}\`` : ""}`.trim();
|
|
29781
30209
|
case "agent-done":
|
|
29782
|
-
return `agent ${s.n ?? "?"}/${s.m ?? "?"} — ${s.agent
|
|
30210
|
+
return `agent ${s.n ?? "?"}/${s.m ?? "?"} — ${s.agent ? `\`${s.agent}\`` : ""} done`.trim();
|
|
29783
30211
|
case "persist-pin":
|
|
29784
30212
|
return "persisting pin";
|
|
30213
|
+
case "web-refresh":
|
|
30214
|
+
return "refreshing web dashboard (webd install)";
|
|
29785
30215
|
case "hostd-web-deferred":
|
|
29786
|
-
return "hostd
|
|
30216
|
+
return "hostd refresh deferred (run host-side)";
|
|
29787
30217
|
case "self-bump":
|
|
29788
30218
|
return "hostd refreshing itself to the target (brief control blip)…";
|
|
29789
30219
|
case "self-bump-done":
|
|
@@ -29792,18 +30222,147 @@ function phaseLine(s) {
|
|
|
29792
30222
|
return "starting";
|
|
29793
30223
|
}
|
|
29794
30224
|
}
|
|
29795
|
-
function
|
|
29796
|
-
const
|
|
29797
|
-
|
|
30225
|
+
function formatDurationMs(ms) {
|
|
30226
|
+
const totalSec = Math.max(0, Math.round(ms / 1000));
|
|
30227
|
+
if (totalSec < 60)
|
|
30228
|
+
return `${totalSec}s`;
|
|
30229
|
+
const totalMin = Math.floor(totalSec / 60);
|
|
30230
|
+
const sec = totalSec % 60;
|
|
30231
|
+
if (totalMin < 60)
|
|
30232
|
+
return sec > 0 ? `${totalMin}m ${sec}s` : `${totalMin}m`;
|
|
30233
|
+
const hr = Math.floor(totalMin / 60);
|
|
30234
|
+
const min = totalMin % 60;
|
|
30235
|
+
return min > 0 ? `${hr}h ${min}m` : `${hr}h`;
|
|
30236
|
+
}
|
|
30237
|
+
function shortRequestId(id) {
|
|
30238
|
+
return id.length > 16 ? id.slice(0, 16) + "…" : id;
|
|
30239
|
+
}
|
|
30240
|
+
function headerLine(s, icon) {
|
|
30241
|
+
const arrow = s.fromVersion ? `\`${s.fromVersion}\` → \`${s.target}\`` : `→ \`${s.target}\``;
|
|
30242
|
+
const req = s.requestId ? ` · req \`${shortRequestId(s.requestId)}\`` : "";
|
|
30243
|
+
return `${icon} **Rollout** ${arrow}${req}`;
|
|
30244
|
+
}
|
|
30245
|
+
function checklistLines(s, compact) {
|
|
30246
|
+
const agents = s.agents ?? [];
|
|
30247
|
+
const lines = [];
|
|
30248
|
+
let doneCount = 0;
|
|
30249
|
+
let pendingNamed = 0;
|
|
30250
|
+
for (const a of agents) {
|
|
30251
|
+
const canary = a.canary ? " (canary)" : "";
|
|
30252
|
+
const dur = a.durationMs !== undefined ? ` — ${formatDurationMs(a.durationMs)}` : "";
|
|
30253
|
+
switch (a.status) {
|
|
30254
|
+
case "done":
|
|
30255
|
+
if (compact)
|
|
30256
|
+
doneCount += 1;
|
|
30257
|
+
else
|
|
30258
|
+
lines.push(`- ✓ \`${a.name}\`${canary}${dur}`);
|
|
30259
|
+
break;
|
|
30260
|
+
case "running":
|
|
30261
|
+
lines.push(`- ⏳ \`${a.name}\`${canary} — restarting…`);
|
|
30262
|
+
break;
|
|
30263
|
+
case "failed":
|
|
30264
|
+
lines.push(`- ✗ \`${a.name}\`${canary} — failed${dur}`);
|
|
30265
|
+
break;
|
|
30266
|
+
default:
|
|
30267
|
+
if (compact)
|
|
30268
|
+
pendingNamed += 1;
|
|
30269
|
+
else
|
|
30270
|
+
lines.push(`- · \`${a.name}\`${canary}`);
|
|
30271
|
+
}
|
|
30272
|
+
}
|
|
30273
|
+
if (doneCount > 0)
|
|
30274
|
+
lines.unshift(`- ✓ ${doneCount} done`);
|
|
30275
|
+
const unseen = s.m !== undefined ? Math.max(0, s.m - agents.length) : 0;
|
|
30276
|
+
const pendingTotal = unseen + pendingNamed;
|
|
30277
|
+
if (pendingTotal > 0 && agents.length > 0) {
|
|
30278
|
+
lines.push(`- · ${pendingTotal} more pending`);
|
|
30279
|
+
}
|
|
30280
|
+
return lines;
|
|
30281
|
+
}
|
|
30282
|
+
function etaLine(s) {
|
|
30283
|
+
if (s.m === undefined)
|
|
30284
|
+
return null;
|
|
30285
|
+
const done = (s.agents ?? []).filter((a) => a.status === "done" && a.durationMs !== undefined);
|
|
30286
|
+
if (done.length === 0)
|
|
30287
|
+
return null;
|
|
30288
|
+
const remaining = s.m - done.length;
|
|
30289
|
+
if (remaining <= 0)
|
|
30290
|
+
return null;
|
|
30291
|
+
const meanMs = done.reduce((t, a) => t + (a.durationMs ?? 0), 0) / done.length;
|
|
30292
|
+
return `~${formatDurationMs(meanMs * remaining)} left (rough est.)`;
|
|
30293
|
+
}
|
|
30294
|
+
function deferredLines(target) {
|
|
30295
|
+
const bare = target.trim().replace(/^v/, "");
|
|
30296
|
+
return [
|
|
30297
|
+
`**Deferred (still on the prior version — run host-side):**`,
|
|
30298
|
+
`- \`switchroom-web\` — \`switchroom webd install --tag ${target}\``,
|
|
30299
|
+
`- host operator CLI — \`sudo npm i -g switchroom@${bare}\``,
|
|
30300
|
+
`- hostd template regen (only if the release changed hostd mounts/env) — \`switchroom hostd install --tag ${target}\``
|
|
30301
|
+
];
|
|
30302
|
+
}
|
|
30303
|
+
var MAX_RENDER_CHARS = 3800;
|
|
30304
|
+
function rolledList(rolled, compact) {
|
|
30305
|
+
if (rolled.length === 0)
|
|
30306
|
+
return "none";
|
|
30307
|
+
if (compact)
|
|
30308
|
+
return `${rolled.length} agent(s)`;
|
|
30309
|
+
return rolled.map((r) => `\`${r}\``).join(", ");
|
|
30310
|
+
}
|
|
30311
|
+
function renderWith(s, compact) {
|
|
30312
|
+
const rolled = s.rolled ?? [];
|
|
30313
|
+
const rolledCount = rolled.length;
|
|
30314
|
+
const checklist = checklistLines(s, compact);
|
|
30315
|
+
const elapsedMs = s.elapsedMs ?? (s.startedAtMs !== undefined && s.nowMs !== undefined ? s.nowMs - s.startedAtMs : undefined);
|
|
29798
30316
|
if (s.terminal === "completed") {
|
|
29799
|
-
|
|
30317
|
+
const parts2 = [headerLine(s, "✅")];
|
|
30318
|
+
let summary = `**Done** — rolled ${rolledCount}${s.m !== undefined ? `/${s.m}` : ""} agent(s)`;
|
|
30319
|
+
if (elapsedMs !== undefined)
|
|
30320
|
+
summary += ` in ${formatDurationMs(elapsedMs)}`;
|
|
30321
|
+
summary += compact ? "." : `: ${rolledList(rolled, compact)}.`;
|
|
30322
|
+
parts2.push(summary);
|
|
30323
|
+
if (checklist.length > 0)
|
|
30324
|
+
parts2.push("", ...checklist);
|
|
30325
|
+
if (s.deferred !== false)
|
|
30326
|
+
parts2.push("", ...deferredLines(s.target));
|
|
30327
|
+
return parts2.join(`
|
|
30328
|
+
`);
|
|
29800
30329
|
}
|
|
29801
30330
|
if (s.terminal === "error") {
|
|
29802
|
-
const where = s.failedStep ? ` at
|
|
29803
|
-
|
|
30331
|
+
const where = s.failedStep ? ` at \`${s.failedStep}\`${s.failedAgent ? ` (\`${s.failedAgent}\` → ${s.got ?? "unreachable"})` : ""}` : "";
|
|
30332
|
+
const parts2 = [headerLine(s, "❌")];
|
|
30333
|
+
let summary = `**STOPPED**${where}`;
|
|
30334
|
+
if (elapsedMs !== undefined)
|
|
30335
|
+
summary += ` after ${formatDurationMs(elapsedMs)}`;
|
|
30336
|
+
summary += `. Rolled before stop: ${rolledList(rolled, compact)}.`;
|
|
30337
|
+
parts2.push(summary);
|
|
30338
|
+
if (checklist.length > 0)
|
|
30339
|
+
parts2.push("", ...checklist);
|
|
30340
|
+
return parts2.join(`
|
|
30341
|
+
`);
|
|
29804
30342
|
}
|
|
29805
|
-
const
|
|
29806
|
-
|
|
30343
|
+
const parts = [headerLine(s, "⏳"), `**Phase:** ${phaseLine(s)}`];
|
|
30344
|
+
if (checklist.length > 0)
|
|
30345
|
+
parts.push("", ...checklist);
|
|
30346
|
+
const footer = [];
|
|
30347
|
+
if (s.m !== undefined)
|
|
30348
|
+
footer.push(`${rolledCount}/${s.m} rolled`);
|
|
30349
|
+
else if (rolledCount > 0)
|
|
30350
|
+
footer.push(`${rolledCount} rolled`);
|
|
30351
|
+
if (elapsedMs !== undefined)
|
|
30352
|
+
footer.push(`elapsed ${formatDurationMs(elapsedMs)}`);
|
|
30353
|
+
const eta = etaLine(s);
|
|
30354
|
+
if (eta)
|
|
30355
|
+
footer.push(eta);
|
|
30356
|
+
if (footer.length > 0)
|
|
30357
|
+
parts.push("", footer.join(" · "));
|
|
30358
|
+
return parts.join(`
|
|
30359
|
+
`);
|
|
30360
|
+
}
|
|
30361
|
+
function renderRolloutStatus(s) {
|
|
30362
|
+
const full = renderWith(s, false);
|
|
30363
|
+
if (full.length <= MAX_RENDER_CHARS)
|
|
30364
|
+
return full;
|
|
30365
|
+
return renderWith(s, true);
|
|
29807
30366
|
}
|
|
29808
30367
|
|
|
29809
30368
|
// src/scheduler/dispatch.ts
|
|
@@ -29843,12 +30402,12 @@ function collectScheduleEntries(config) {
|
|
|
29843
30402
|
// src/agent-scheduler/replay.ts
|
|
29844
30403
|
var STALE_LOOKBACK_MAX_MIN = 14 * 24 * 60;
|
|
29845
30404
|
function readRecentFires(jsonlPath) {
|
|
29846
|
-
const
|
|
29847
|
-
if (!
|
|
30405
|
+
const fs3 = __require("node:fs");
|
|
30406
|
+
if (!fs3.existsSync(jsonlPath))
|
|
29848
30407
|
return [];
|
|
29849
30408
|
let raw;
|
|
29850
30409
|
try {
|
|
29851
|
-
raw =
|
|
30410
|
+
raw = fs3.readFileSync(jsonlPath, "utf8");
|
|
29852
30411
|
} catch {
|
|
29853
30412
|
return [];
|
|
29854
30413
|
}
|
|
@@ -29866,6 +30425,13 @@ function readRecentFires(jsonlPath) {
|
|
|
29866
30425
|
}
|
|
29867
30426
|
|
|
29868
30427
|
// src/host-control/server.ts
|
|
30428
|
+
var HOSTD_BROKER_SOCKET_PATH = "/run/switchroom/broker/hostd/sock";
|
|
30429
|
+
var DEFAULT_OPERATOR_ATTEST_VERBS = [
|
|
30430
|
+
"update_apply",
|
|
30431
|
+
"apply",
|
|
30432
|
+
"rollout"
|
|
30433
|
+
];
|
|
30434
|
+
var ATTEST_AUDIT_METHOD = "passphrase-attest";
|
|
29869
30435
|
function resolveDigests(imageRefs) {
|
|
29870
30436
|
const out = new Map;
|
|
29871
30437
|
for (const ref of imageRefs) {
|
|
@@ -29888,11 +30454,11 @@ function resolveDigests(imageRefs) {
|
|
|
29888
30454
|
return out;
|
|
29889
30455
|
}
|
|
29890
30456
|
function readCachedInstallType(bindRoot) {
|
|
29891
|
-
const cacheDir =
|
|
29892
|
-
const cachePath =
|
|
29893
|
-
if (
|
|
30457
|
+
const cacheDir = join10(bindRoot, ".switchroom");
|
|
30458
|
+
const cachePath = join10(cacheDir, "install-type.json");
|
|
30459
|
+
if (existsSync11(cachePath)) {
|
|
29894
30460
|
try {
|
|
29895
|
-
const raw =
|
|
30461
|
+
const raw = readFileSync9(cachePath, "utf-8");
|
|
29896
30462
|
const parsed = JSON.parse(raw);
|
|
29897
30463
|
if (parsed && typeof parsed.install_type === "string" && typeof parsed.detected_at === "string") {
|
|
29898
30464
|
return parsed;
|
|
@@ -29909,10 +30475,10 @@ function readCachedInstallType(bindRoot) {
|
|
|
29909
30475
|
source_paths: ctx.source_paths
|
|
29910
30476
|
};
|
|
29911
30477
|
try {
|
|
29912
|
-
|
|
30478
|
+
mkdirSync4(cacheDir, { recursive: true });
|
|
29913
30479
|
const tmp = `${cachePath}.tmp`;
|
|
29914
|
-
|
|
29915
|
-
|
|
30480
|
+
writeFileSync5(tmp, JSON.stringify(payload, null, 2), { mode: 420 });
|
|
30481
|
+
renameSync3(tmp, cachePath);
|
|
29916
30482
|
} catch {}
|
|
29917
30483
|
return payload;
|
|
29918
30484
|
}
|
|
@@ -29941,7 +30507,7 @@ function writeFileInPlacePreservingInode(targetPath, content) {
|
|
|
29941
30507
|
} finally {
|
|
29942
30508
|
closeSync3(fd);
|
|
29943
30509
|
}
|
|
29944
|
-
const readBack =
|
|
30510
|
+
const readBack = readFileSync9(targetPath);
|
|
29945
30511
|
if (readBack.length !== buf.length) {
|
|
29946
30512
|
throw new Error(`in-place write short: wrote ${buf.length} bytes but read back ${readBack.length}`);
|
|
29947
30513
|
}
|
|
@@ -29966,7 +30532,7 @@ class HostdServer {
|
|
|
29966
30532
|
this.opts = opts;
|
|
29967
30533
|
}
|
|
29968
30534
|
async start() {
|
|
29969
|
-
const hostdDir =
|
|
30535
|
+
const hostdDir = join10(this.opts.homeDir, ".switchroom", "hostd");
|
|
29970
30536
|
await mkdir(hostdDir, { recursive: true });
|
|
29971
30537
|
await chmod(hostdDir, 493).catch(() => {
|
|
29972
30538
|
return;
|
|
@@ -29981,13 +30547,13 @@ class HostdServer {
|
|
|
29981
30547
|
}
|
|
29982
30548
|
try {
|
|
29983
30549
|
for (const name of agentNames) {
|
|
29984
|
-
const dir =
|
|
29985
|
-
const sockPath =
|
|
30550
|
+
const dir = join10(hostdDir, name);
|
|
30551
|
+
const sockPath = join10(dir, "sock");
|
|
29986
30552
|
await mkdir(dir, { recursive: true });
|
|
29987
30553
|
await chmod(dir, 493).catch(() => {
|
|
29988
30554
|
return;
|
|
29989
30555
|
});
|
|
29990
|
-
if (
|
|
30556
|
+
if (existsSync11(sockPath))
|
|
29991
30557
|
await unlink(sockPath).catch(() => {
|
|
29992
30558
|
return;
|
|
29993
30559
|
});
|
|
@@ -30018,13 +30584,13 @@ class HostdServer {
|
|
|
30018
30584
|
process.stderr.write(`hostd: SWITCHROOM_HOSTD_OPERATOR_UID='${opUidStr}' is not a positive integer; skipping operator listener
|
|
30019
30585
|
`);
|
|
30020
30586
|
} else {
|
|
30021
|
-
const dir =
|
|
30022
|
-
const sockPath =
|
|
30587
|
+
const dir = join10(hostdDir, "operator");
|
|
30588
|
+
const sockPath = join10(dir, "sock");
|
|
30023
30589
|
await mkdir(dir, { recursive: true });
|
|
30024
30590
|
await chmod(dir, 493).catch(() => {
|
|
30025
30591
|
return;
|
|
30026
30592
|
});
|
|
30027
|
-
if (
|
|
30593
|
+
if (existsSync11(sockPath))
|
|
30028
30594
|
await unlink(sockPath).catch(() => {
|
|
30029
30595
|
return;
|
|
30030
30596
|
});
|
|
@@ -30060,11 +30626,11 @@ class HostdServer {
|
|
|
30060
30626
|
}
|
|
30061
30627
|
async reconcileOrphanedFleetMutations() {
|
|
30062
30628
|
const path2 = this.auditLogPath();
|
|
30063
|
-
if (!
|
|
30629
|
+
if (!existsSync11(path2))
|
|
30064
30630
|
return;
|
|
30065
30631
|
let raw;
|
|
30066
30632
|
try {
|
|
30067
|
-
raw =
|
|
30633
|
+
raw = readFileSync9(path2, "utf-8");
|
|
30068
30634
|
} catch {
|
|
30069
30635
|
return;
|
|
30070
30636
|
}
|
|
@@ -30202,6 +30768,15 @@ class HostdServer {
|
|
|
30202
30768
|
socket.end();
|
|
30203
30769
|
return;
|
|
30204
30770
|
}
|
|
30771
|
+
const attest = await this.checkOperatorAttest(req, caller);
|
|
30772
|
+
if (attest.denied !== null) {
|
|
30773
|
+
const resp2 = deniedResponse(req.request_id, attest.denied);
|
|
30774
|
+
await this.writeAudit({ caller, req, resp: resp2, method: attest.method });
|
|
30775
|
+
socket.write(encodeResponse(resp2));
|
|
30776
|
+
socket.end();
|
|
30777
|
+
return;
|
|
30778
|
+
}
|
|
30779
|
+
const attestMethod = attest.method;
|
|
30205
30780
|
const started = Date.now();
|
|
30206
30781
|
let resp;
|
|
30207
30782
|
try {
|
|
@@ -30260,10 +30835,70 @@ class HostdServer {
|
|
|
30260
30835
|
resp = err("E_DISPATCH_FAILED", "hostd dispatch failed").why(msg).op(req.op).caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).build(req.request_id, Date.now() - started);
|
|
30261
30836
|
resp = { ...resp, error: `hostd dispatch failed: ${msg}` };
|
|
30262
30837
|
}
|
|
30263
|
-
await this.writeAudit({ caller, req, resp });
|
|
30838
|
+
await this.writeAudit({ caller, req, resp, method: attestMethod });
|
|
30264
30839
|
socket.write(encodeResponse(resp));
|
|
30265
30840
|
socket.end();
|
|
30266
30841
|
}
|
|
30842
|
+
async checkOperatorAttest(req, caller) {
|
|
30843
|
+
const hostdCfg = this.opts.config.hostd;
|
|
30844
|
+
if (hostdCfg?.operator_attest_enabled !== true) {
|
|
30845
|
+
return { denied: null };
|
|
30846
|
+
}
|
|
30847
|
+
if (caller.kind === "operator")
|
|
30848
|
+
return { denied: null };
|
|
30849
|
+
const requiredVerbs = hostdCfg.operator_attest_required_verbs ?? DEFAULT_OPERATOR_ATTEST_VERBS;
|
|
30850
|
+
const required = requiredVerbs.includes(req.op);
|
|
30851
|
+
const passphrase = req.operator_passphrase;
|
|
30852
|
+
if (!required) {
|
|
30853
|
+
if (passphrase === undefined)
|
|
30854
|
+
return { denied: null };
|
|
30855
|
+
const v2 = await this.attestVerifier().verify(passphrase);
|
|
30856
|
+
return v2.ok ? { denied: null, method: ATTEST_AUDIT_METHOD } : {
|
|
30857
|
+
denied: `${req.op} operator-attest failed: ${v2.reason}`,
|
|
30858
|
+
method: ATTEST_AUDIT_METHOD
|
|
30859
|
+
};
|
|
30860
|
+
}
|
|
30861
|
+
if (passphrase === undefined) {
|
|
30862
|
+
return {
|
|
30863
|
+
denied: `${req.op} requires operator-passphrase attestation ` + `(hostd.operator_attest_enabled is on and ${req.op} is in ` + `operator_attest_required_verbs)`,
|
|
30864
|
+
method: ATTEST_AUDIT_METHOD
|
|
30865
|
+
};
|
|
30866
|
+
}
|
|
30867
|
+
const v = await this.attestVerifier().verify(passphrase);
|
|
30868
|
+
return v.ok ? { denied: null, method: ATTEST_AUDIT_METHOD } : {
|
|
30869
|
+
denied: `${req.op} operator-attest failed: ${v.reason}`,
|
|
30870
|
+
method: ATTEST_AUDIT_METHOD
|
|
30871
|
+
};
|
|
30872
|
+
}
|
|
30873
|
+
attestVerifier() {
|
|
30874
|
+
if (this.opts.attestVerifier)
|
|
30875
|
+
return this.opts.attestVerifier;
|
|
30876
|
+
if (!this._defaultAttestVerifier) {
|
|
30877
|
+
this._defaultAttestVerifier = {
|
|
30878
|
+
verify: (passphrase) => this.brokerAttestVerify(passphrase)
|
|
30879
|
+
};
|
|
30880
|
+
}
|
|
30881
|
+
return this._defaultAttestVerifier;
|
|
30882
|
+
}
|
|
30883
|
+
_defaultAttestVerifier;
|
|
30884
|
+
async brokerAttestVerify(passphrase) {
|
|
30885
|
+
const res = await rpcRaw({
|
|
30886
|
+
v: 1,
|
|
30887
|
+
op: "list_grants",
|
|
30888
|
+
agent: "hostd",
|
|
30889
|
+
passphrase
|
|
30890
|
+
}, { vaultBrokerSocket: HOSTD_BROKER_SOCKET_PATH, timeoutMs: 5000 });
|
|
30891
|
+
if (res.kind === "unreachable") {
|
|
30892
|
+
return { ok: false, reason: `broker unreachable: ${res.msg}` };
|
|
30893
|
+
}
|
|
30894
|
+
const resp = res.resp;
|
|
30895
|
+
if (resp.ok === true)
|
|
30896
|
+
return { ok: true };
|
|
30897
|
+
return {
|
|
30898
|
+
ok: false,
|
|
30899
|
+
reason: `broker denied attestation (${resp.code ?? "DENIED"})`
|
|
30900
|
+
};
|
|
30901
|
+
}
|
|
30267
30902
|
checkGate(req, caller) {
|
|
30268
30903
|
if (caller.kind === "operator")
|
|
30269
30904
|
return null;
|
|
@@ -30394,10 +31029,10 @@ class HostdServer {
|
|
|
30394
31029
|
missingApplyAssets() {
|
|
30395
31030
|
const root = this.opts.applyAssetsRoot ?? resolve8(import.meta.dirname, "../..");
|
|
30396
31031
|
return [
|
|
30397
|
-
|
|
30398
|
-
|
|
30399
|
-
|
|
30400
|
-
].filter((p) => !
|
|
31032
|
+
join10(root, "profiles"),
|
|
31033
|
+
join10(root, "profiles", "default"),
|
|
31034
|
+
join10(root, "vendor", "hindsight-memory")
|
|
31035
|
+
].filter((p) => !existsSync11(p));
|
|
30401
31036
|
}
|
|
30402
31037
|
applyAssetPreflight(request_id, started) {
|
|
30403
31038
|
const missing = this.missingApplyAssets();
|
|
@@ -30507,6 +31142,26 @@ class HostdServer {
|
|
|
30507
31142
|
const assetDenied = this.applyAssetPreflight(req.request_id, started);
|
|
30508
31143
|
if (assetDenied)
|
|
30509
31144
|
return assetDenied;
|
|
31145
|
+
if (req.args.agents !== undefined) {
|
|
31146
|
+
let validAgents = null;
|
|
31147
|
+
try {
|
|
31148
|
+
const cfg = loadConfig(this.opts.configPath);
|
|
31149
|
+
validAgents = Object.keys(cfg.agents ?? {});
|
|
31150
|
+
} catch {
|
|
31151
|
+
validAgents = null;
|
|
31152
|
+
}
|
|
31153
|
+
if (validAgents !== null) {
|
|
31154
|
+
const buildDenied = (human) => err("E_UNKNOWN_AGENT", human).fixBadInput("agents").op("rollout").caller(caller.kind).agentName(caller.kind === "agent" ? caller.name : undefined).asDenied().build(req.request_id, Date.now() - started);
|
|
31155
|
+
const requested = req.args.agents;
|
|
31156
|
+
if (requested.length === 0) {
|
|
31157
|
+
return buildDenied(`empty agents list — pass at least one agent, ` + `or omit "agents" to roll all. Valid agents: ${validAgents.join(", ")}`);
|
|
31158
|
+
}
|
|
31159
|
+
const unknown = requested.filter((a) => !validAgents.includes(a));
|
|
31160
|
+
if (unknown.length > 0) {
|
|
31161
|
+
return buildDenied(`unknown agent(s): ${unknown.join(", ")}. ` + `Valid agents: ${validAgents.join(", ")}`);
|
|
31162
|
+
}
|
|
31163
|
+
}
|
|
31164
|
+
}
|
|
30510
31165
|
const entry = this.launchRollout(req.args, req.request_id, caller, started);
|
|
30511
31166
|
return {
|
|
30512
31167
|
v: 1,
|
|
@@ -30561,19 +31216,19 @@ class HostdServer {
|
|
|
30561
31216
|
return entry;
|
|
30562
31217
|
}
|
|
30563
31218
|
hostdDirPath() {
|
|
30564
|
-
return
|
|
31219
|
+
return join10(this.opts.homeDir, ".switchroom", "hostd");
|
|
30565
31220
|
}
|
|
30566
31221
|
hostdDirHostPath() {
|
|
30567
31222
|
const hostHome = this.opts.hostHomeDir ?? process.env.SWITCHROOM_HOST_HOME?.trim() ?? this.opts.homeDir;
|
|
30568
|
-
return
|
|
31223
|
+
return join10(hostHome, ".switchroom", "hostd");
|
|
30569
31224
|
}
|
|
30570
31225
|
async beginSelfBump(req, caller, started) {
|
|
30571
31226
|
const ownVersion = this.opts.selfVersion ?? SWITCHROOM_VERSION;
|
|
30572
|
-
const composePath =
|
|
30573
|
-
if (!
|
|
31227
|
+
const composePath = join10(this.hostdDirPath(), "docker-compose.yml");
|
|
31228
|
+
if (!existsSync11(composePath)) {
|
|
30574
31229
|
return deniedResponse(req.request_id, `rollout: hostd's CLI (v${ownVersion}) is older than the target ` + `${req.args.pin} and needs a self-bump first, but its compose file ` + `is missing at ${composePath}. Run \`switchroom hostd install ` + `--tag ${req.args.pin}\` on the host, then re-run the roll. ` + `Nothing was changed.`, Date.now() - started);
|
|
30575
31230
|
}
|
|
30576
|
-
const before =
|
|
31231
|
+
const before = readFileSync9(composePath, "utf8");
|
|
30577
31232
|
const bumped = bumpHostdComposeImageTag(before, req.args.pin);
|
|
30578
31233
|
if (!bumped) {
|
|
30579
31234
|
return deniedResponse(req.request_id, `rollout: hostd needs a self-bump to ${req.args.pin} but no ` + `switchroom-hostd image line was found in ${composePath} ` + `(hand-edited compose?). Run \`switchroom hostd install --tag ` + `${req.args.pin}\` on the host, then re-run the roll. Nothing ` + `was changed.`, Date.now() - started);
|
|
@@ -30594,10 +31249,10 @@ class HostdServer {
|
|
|
30594
31249
|
try {
|
|
30595
31250
|
const baks = readdirSync3(this.hostdDirPath()).filter((f) => f.startsWith("docker-compose.yml.bak-selfbump-")).sort();
|
|
30596
31251
|
for (const f of baks.slice(0, Math.max(0, baks.length - 4))) {
|
|
30597
|
-
|
|
31252
|
+
unlinkSync2(join10(this.hostdDirPath(), f));
|
|
30598
31253
|
}
|
|
30599
31254
|
} catch {}
|
|
30600
|
-
const markerPath =
|
|
31255
|
+
const markerPath = join10(this.hostdDirPath(), SELF_BUMP_MARKER_FILENAME);
|
|
30601
31256
|
const marker = {
|
|
30602
31257
|
v: 1,
|
|
30603
31258
|
request_id: req.request_id,
|
|
@@ -30610,8 +31265,8 @@ class HostdServer {
|
|
|
30610
31265
|
prior_hostd_version: ownVersion
|
|
30611
31266
|
};
|
|
30612
31267
|
copyFileSync2(composePath, bakPath);
|
|
30613
|
-
|
|
30614
|
-
|
|
31268
|
+
writeFileSync5(composePath, bumped.yaml, "utf8");
|
|
31269
|
+
writeFileSync5(markerPath, encodePendingRolloutMarker(marker), {
|
|
30615
31270
|
mode: 384
|
|
30616
31271
|
});
|
|
30617
31272
|
const entry = {
|
|
@@ -30636,7 +31291,7 @@ class HostdServer {
|
|
|
30636
31291
|
const rollbackBump = () => {
|
|
30637
31292
|
try {
|
|
30638
31293
|
copyFileSync2(bakPath, composePath);
|
|
30639
|
-
|
|
31294
|
+
unlinkSync2(markerPath);
|
|
30640
31295
|
} catch (e) {
|
|
30641
31296
|
process.stderr.write(`hostd: self-bump rollback failed (compose/marker may need manual ` + `restore from ${bakPath}): ${e.message}
|
|
30642
31297
|
`);
|
|
@@ -30679,7 +31334,7 @@ class HostdServer {
|
|
|
30679
31334
|
rollbackBump();
|
|
30680
31335
|
entry.result = "error";
|
|
30681
31336
|
entry.finished_at = Date.now();
|
|
30682
|
-
entry.error = `self-bump helper failed (container exit ${Number.isFinite(code) ? code : "unknown"}). ` + `See ${
|
|
31337
|
+
entry.error = `self-bump helper failed (container exit ${Number.isFinite(code) ? code : "unknown"}). ` + `See ${join10(this.hostdDirHostPath(), SELF_BUMP_LOG_FILENAME)} on the host. ` + `Compose bump rolled back. Fallback: \`switchroom hostd install ` + `--tag ${req.args.pin}\` host-side, then re-run the roll.`;
|
|
30683
31338
|
if (this.fleetMutationInFlight && this.fleetMutationInFlight.request_id === entry.request_id) {
|
|
30684
31339
|
this.fleetMutationInFlight = null;
|
|
30685
31340
|
}
|
|
@@ -30703,15 +31358,15 @@ class HostdServer {
|
|
|
30703
31358
|
};
|
|
30704
31359
|
}
|
|
30705
31360
|
async resumePendingSelfBumpRollout() {
|
|
30706
|
-
const markerPath =
|
|
30707
|
-
if (!
|
|
31361
|
+
const markerPath = join10(this.hostdDirPath(), SELF_BUMP_MARKER_FILENAME);
|
|
31362
|
+
if (!existsSync11(markerPath))
|
|
30708
31363
|
return;
|
|
30709
31364
|
let raw;
|
|
30710
31365
|
try {
|
|
30711
|
-
raw =
|
|
31366
|
+
raw = readFileSync9(markerPath, "utf8");
|
|
30712
31367
|
} finally {
|
|
30713
31368
|
try {
|
|
30714
|
-
|
|
31369
|
+
unlinkSync2(markerPath);
|
|
30715
31370
|
} catch {}
|
|
30716
31371
|
}
|
|
30717
31372
|
const marker = parsePendingRolloutMarker(raw);
|
|
@@ -30748,7 +31403,7 @@ class HostdServer {
|
|
|
30748
31403
|
return;
|
|
30749
31404
|
}
|
|
30750
31405
|
if (needsSelfBump(ownVersion, marker.pin)) {
|
|
30751
|
-
await failResume(`hostd is still on v${ownVersion} after the self-bump to ` + `${marker.pin} (helper failed? see ` + `${
|
|
31406
|
+
await failResume(`hostd is still on v${ownVersion} after the self-bump to ` + `${marker.pin} (helper failed? see ` + `${join10(this.hostdDirHostPath(), SELF_BUMP_LOG_FILENAME)}). ` + `Fallback: \`switchroom hostd install --tag ${marker.pin}\` ` + `host-side, then re-issue the rollout.`);
|
|
30752
31407
|
return;
|
|
30753
31408
|
}
|
|
30754
31409
|
process.stderr.write(`hostd: resuming rollout ${marker.request_id} → ${marker.pin} after ` + `self-bump (v${marker.prior_hostd_version} → v${ownVersion})
|
|
@@ -30839,18 +31494,19 @@ class HostdServer {
|
|
|
30839
31494
|
if (!verdict.ok) {
|
|
30840
31495
|
return err(verdict.code, verdict.detail).fixBadInput("unified_diff").op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).build(req.request_id, Date.now() - started);
|
|
30841
31496
|
}
|
|
31497
|
+
let beforeContent;
|
|
31498
|
+
try {
|
|
31499
|
+
beforeContent = readFileSync9(configPath, "utf-8");
|
|
31500
|
+
} catch {
|
|
31501
|
+
beforeContent = "";
|
|
31502
|
+
}
|
|
30842
31503
|
if (caller.kind === "agent" && this.opts.config.agents[caller.name]?.admin !== true) {
|
|
30843
|
-
let beforeContent;
|
|
30844
|
-
try {
|
|
30845
|
-
beforeContent = readFileSync7(configPath, "utf-8");
|
|
30846
|
-
} catch {
|
|
30847
|
-
beforeContent = "";
|
|
30848
|
-
}
|
|
30849
31504
|
const admission = admitSelfScopedNonAdminEdit(beforeContent, verdict.postApplyContent, caller.name);
|
|
30850
31505
|
if (!admission.ok) {
|
|
30851
31506
|
return err("E_NOT_SELF_SCOPED", admission.detail).why("non-admin agents may only add rules to their own " + "agents.<self>.tools.allow OR append their own " + "agents.<self>.memory.mental_models via config_propose_edit " + `(mental-model check: ${admission.mentalModelDetail})`).fixBadInput("unified_diff").op("config_propose_edit").caller("agent").agentName(caller.name).asDenied().build(req.request_id, Date.now() - started);
|
|
30852
31507
|
}
|
|
30853
31508
|
}
|
|
31509
|
+
const proposedChangedPaths = classifyBlastRadius(beforeContent, verdict.postApplyContent).changedPaths;
|
|
30854
31510
|
if (!this.opts.approvalGateway) {
|
|
30855
31511
|
return err("E_NO_APPROVAL_GATEWAY", "validation passed but hostd was started without an approval-gateway wiring; the operator build is missing the telegram-plugin link").fixOperatorAction("infra", [
|
|
30856
31512
|
"ensure hostd was launched with --approval-gateway / telegram-plugin link"
|
|
@@ -30864,23 +31520,42 @@ class HostdServer {
|
|
|
30864
31520
|
`);
|
|
30865
31521
|
return await pending;
|
|
30866
31522
|
}
|
|
30867
|
-
const
|
|
30868
|
-
|
|
30869
|
-
|
|
30870
|
-
|
|
31523
|
+
const run = this.runConfigProposeRateThenApply(req, caller, callerName, configPath, started, proposedChangedPaths);
|
|
31524
|
+
this.inflightConfigProposals.set(dedupeKey, run);
|
|
31525
|
+
try {
|
|
31526
|
+
return await run;
|
|
31527
|
+
} finally {
|
|
31528
|
+
this.inflightConfigProposals.delete(dedupeKey);
|
|
31529
|
+
}
|
|
31530
|
+
}
|
|
31531
|
+
async runConfigProposeRateThenApply(req, caller, callerName, configPath, started, proposedChangedPaths) {
|
|
31532
|
+
let preApproved = false;
|
|
31533
|
+
try {
|
|
31534
|
+
if (this.opts.approvalGateway.checkPreApproved) {
|
|
31535
|
+
preApproved = await this.opts.approvalGateway.checkPreApproved(callerName, req.args.unified_diff);
|
|
31536
|
+
}
|
|
31537
|
+
} catch {
|
|
31538
|
+
preApproved = false;
|
|
31539
|
+
}
|
|
31540
|
+
if (!preApproved) {
|
|
31541
|
+
const rate = this.checkConfigEditRate(callerName, Date.now());
|
|
31542
|
+
if (!rate.ok) {
|
|
31543
|
+
const retryAtIso = new Date(rate.retryAtMs).toISOString();
|
|
31544
|
+
process.stderr.write(`hostd: config_propose_edit — RATE-LIMITED ${callerName} ` + `(>${rate.limit}/hour); next slot ${retryAtIso}
|
|
30871
31545
|
`);
|
|
30872
|
-
|
|
30873
|
-
|
|
30874
|
-
|
|
30875
|
-
|
|
30876
|
-
|
|
30877
|
-
|
|
30878
|
-
|
|
30879
|
-
|
|
30880
|
-
|
|
30881
|
-
|
|
30882
|
-
|
|
30883
|
-
|
|
31546
|
+
this.appendAuditRow({
|
|
31547
|
+
ts: new Date().toISOString(),
|
|
31548
|
+
op: "config_propose_edit",
|
|
31549
|
+
phase: "rate_limited",
|
|
31550
|
+
request_id: req.request_id,
|
|
31551
|
+
caller: caller.kind === "agent" ? { kind: "agent", name: caller.name } : { kind: "operator" },
|
|
31552
|
+
result: "denied",
|
|
31553
|
+
exit_code: null,
|
|
31554
|
+
duration_ms: Date.now() - started,
|
|
31555
|
+
error: `E_RATE_LIMITED: >${rate.limit} config_propose_edit cards/hour`
|
|
31556
|
+
});
|
|
31557
|
+
return err("E_RATE_LIMITED", `config_propose_edit rate limit exceeded (max ${rate.limit}/hour for this agent)`).why(`next slot opens at ${retryAtIso}`).fixRetryAfter(retryAtIso).op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).asDenied().build(req.request_id, Date.now() - started);
|
|
31558
|
+
}
|
|
30884
31559
|
}
|
|
30885
31560
|
this.appendAuditRow({
|
|
30886
31561
|
ts: new Date().toISOString(),
|
|
@@ -30890,15 +31565,10 @@ class HostdServer {
|
|
|
30890
31565
|
caller: caller.kind === "agent" ? { kind: "agent", name: caller.name } : { kind: "operator" },
|
|
30891
31566
|
result: "started",
|
|
30892
31567
|
exit_code: null,
|
|
30893
|
-
duration_ms: Date.now() - started
|
|
31568
|
+
duration_ms: Date.now() - started,
|
|
31569
|
+
...preApproved ? { pre_approved: true } : {}
|
|
30894
31570
|
});
|
|
30895
|
-
|
|
30896
|
-
this.inflightConfigProposals.set(dedupeKey, run);
|
|
30897
|
-
try {
|
|
30898
|
-
return await run;
|
|
30899
|
-
} finally {
|
|
30900
|
-
this.inflightConfigProposals.delete(dedupeKey);
|
|
30901
|
-
}
|
|
31571
|
+
return await this.runConfigProposeApprovalAndApply(req, caller, callerName, configPath, started, proposedChangedPaths);
|
|
30902
31572
|
}
|
|
30903
31573
|
inflightConfigProposals = new Map;
|
|
30904
31574
|
configEditPostTimes = new Map;
|
|
@@ -30917,7 +31587,7 @@ class HostdServer {
|
|
|
30917
31587
|
this.configEditPostTimes.set(callerName, prior);
|
|
30918
31588
|
return { ok: true };
|
|
30919
31589
|
}
|
|
30920
|
-
async runConfigProposeApprovalAndApply(req, caller, callerName, configPath,
|
|
31590
|
+
async runConfigProposeApprovalAndApply(req, caller, callerName, configPath, started, proposedChangedPaths) {
|
|
30921
31591
|
const approvalId = (this.opts.generateApprovalId ?? defaultApprovalId)();
|
|
30922
31592
|
const approval = await this.opts.approvalGateway.requestApproval({
|
|
30923
31593
|
requestId: approvalId,
|
|
@@ -30946,7 +31616,7 @@ class HostdServer {
|
|
|
30946
31616
|
try {
|
|
30947
31617
|
let snapshot;
|
|
30948
31618
|
try {
|
|
30949
|
-
snapshot =
|
|
31619
|
+
snapshot = readFileSync9(configPath, "utf-8");
|
|
30950
31620
|
} catch (e) {
|
|
30951
31621
|
await approval.finalize({
|
|
30952
31622
|
outcome: "reconcile_failed_rolled_back",
|
|
@@ -30954,8 +31624,43 @@ class HostdServer {
|
|
|
30954
31624
|
});
|
|
30955
31625
|
return this.reconcileFailedRolledBack(`snapshot read failed: ${e.message}`, req, caller, started);
|
|
30956
31626
|
}
|
|
31627
|
+
const reverdict = validateConfigEdit({
|
|
31628
|
+
configPath,
|
|
31629
|
+
targetPath: req.args.target_path,
|
|
31630
|
+
unifiedDiff: req.args.unified_diff
|
|
31631
|
+
});
|
|
31632
|
+
if (!reverdict.ok) {
|
|
31633
|
+
const why = `stored diff no longer applies against the current config` + `: ${reverdict.detail}`;
|
|
31634
|
+
await approval.finalize({
|
|
31635
|
+
outcome: "aborted_config_changed",
|
|
31636
|
+
detail: `config changed since proposal — re-propose (${why})`
|
|
31637
|
+
});
|
|
31638
|
+
return this.configChangedSinceProposal(why, req, caller, started);
|
|
31639
|
+
}
|
|
31640
|
+
const postApplyFresh = reverdict.postApplyContent;
|
|
31641
|
+
const applyChangedPaths = classifyBlastRadius(snapshot, postApplyFresh).changedPaths;
|
|
31642
|
+
const sameChangeSet = applyChangedPaths.length === proposedChangedPaths.length && applyChangedPaths.every((p, i) => p === proposedChangedPaths[i]);
|
|
31643
|
+
if (!sameChangeSet) {
|
|
31644
|
+
const why = `re-applied diff changes different config paths than approved ` + `(approved: [${proposedChangedPaths.join(", ")}]; ` + `would change: [${applyChangedPaths.join(", ")}])`;
|
|
31645
|
+
await approval.finalize({
|
|
31646
|
+
outcome: "aborted_config_changed",
|
|
31647
|
+
detail: `config changed since proposal — re-propose (${why})`
|
|
31648
|
+
});
|
|
31649
|
+
return this.configChangedSinceProposal(why, req, caller, started);
|
|
31650
|
+
}
|
|
31651
|
+
if (caller.kind === "agent" && this.opts.config.agents[caller.name]?.admin !== true) {
|
|
31652
|
+
const reAdmission = admitSelfScopedNonAdminEdit(snapshot, postApplyFresh, caller.name);
|
|
31653
|
+
if (!reAdmission.ok) {
|
|
31654
|
+
const why = `re-applied diff is no longer self-scoped for non-admin caller ` + `"${caller.name}": ${reAdmission.detail}`;
|
|
31655
|
+
await approval.finalize({
|
|
31656
|
+
outcome: "aborted_config_changed",
|
|
31657
|
+
detail: `config changed since proposal — re-propose (${why})`
|
|
31658
|
+
});
|
|
31659
|
+
return this.configChangedSinceProposal(why, req, caller, started);
|
|
31660
|
+
}
|
|
31661
|
+
}
|
|
30957
31662
|
try {
|
|
30958
|
-
writeFileInPlacePreservingInode(configPath,
|
|
31663
|
+
writeFileInPlacePreservingInode(configPath, postApplyFresh);
|
|
30959
31664
|
} catch (e) {
|
|
30960
31665
|
try {
|
|
30961
31666
|
writeFileInPlacePreservingInode(configPath, snapshot);
|
|
@@ -30969,7 +31674,7 @@ class HostdServer {
|
|
|
30969
31674
|
const runner = this.opts.runReconcile ?? (async () => this.runSwitchroom(caller.kind === "agent" ? ["apply", "--only", callerName, "--non-interactive"] : ["apply", "--non-interactive"]));
|
|
30970
31675
|
const recRes = await runner({ requestId: approvalId });
|
|
30971
31676
|
if (recRes.exit_code === 0) {
|
|
30972
|
-
const blast = classifyBlastRadius(snapshot,
|
|
31677
|
+
const blast = classifyBlastRadius(snapshot, postApplyFresh);
|
|
30973
31678
|
await approval.finalize({
|
|
30974
31679
|
outcome: "applied",
|
|
30975
31680
|
affectedAgents: blast.agents,
|
|
@@ -31007,6 +31712,11 @@ class HostdServer {
|
|
|
31007
31712
|
release();
|
|
31008
31713
|
}
|
|
31009
31714
|
}
|
|
31715
|
+
configChangedSinceProposal(why, req, caller, started) {
|
|
31716
|
+
const legacy = `E_CONFIG_CHANGED: config changed since proposal — re-propose (${why})`;
|
|
31717
|
+
const built = err("E_CONFIG_CHANGED", "config changed since the proposal was validated; re-propose against the current config").why(why).fixBadInput("unified_diff").op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).asDenied().build(req.request_id, Date.now() - started);
|
|
31718
|
+
return { ...built, error: legacy };
|
|
31719
|
+
}
|
|
31010
31720
|
reconcileFailedRolledBack(detail, req, caller, started, output) {
|
|
31011
31721
|
const legacy = `E_RECONCILE_FAILED_ROLLED_BACK: ${detail}`;
|
|
31012
31722
|
const built = err("E_RECONCILE_FAILED_ROLLED_BACK", "config write or reconcile failed; live file rolled back to snapshot").why(detail).fixOperatorAction("infra", [
|
|
@@ -31198,8 +31908,8 @@ ${output.recovery.stderr}` : "";
|
|
|
31198
31908
|
if (this.opts.imageRefsForDigests)
|
|
31199
31909
|
return this.opts.imageRefsForDigests();
|
|
31200
31910
|
try {
|
|
31201
|
-
const composePath =
|
|
31202
|
-
if (!
|
|
31911
|
+
const composePath = join10(this.opts.bindRoot ?? this.opts.homeDir, ".switchroom", "compose", "docker-compose.yml");
|
|
31912
|
+
if (!existsSync11(composePath))
|
|
31203
31913
|
return [];
|
|
31204
31914
|
const r = spawnSync3("docker", [
|
|
31205
31915
|
"compose",
|
|
@@ -31307,6 +32017,9 @@ ${output.recovery.stderr}` : "";
|
|
|
31307
32017
|
target: entry.pin ?? "",
|
|
31308
32018
|
rolled: entry.rolled,
|
|
31309
32019
|
terminal: entry.result === "completed" ? "completed" : "error",
|
|
32020
|
+
requestId: entry.request_id,
|
|
32021
|
+
...entry.prior_pin ? { fromVersion: entry.prior_pin } : {},
|
|
32022
|
+
...entry.finished_at !== null ? { elapsedMs: entry.finished_at - entry.started_at } : {},
|
|
31310
32023
|
...entry.failed_step ? { failedStep: entry.failed_step } : {},
|
|
31311
32024
|
...entry.failed_agent ? { failedAgent: entry.failed_agent } : {},
|
|
31312
32025
|
...entry.got !== undefined ? { got: entry.got } : {}
|
|
@@ -31335,10 +32048,10 @@ ${output.recovery.stderr}` : "";
|
|
|
31335
32048
|
}
|
|
31336
32049
|
rolloutRowExistsInLog(targetRequestId) {
|
|
31337
32050
|
const path2 = this.auditLogPath();
|
|
31338
|
-
if (!
|
|
32051
|
+
if (!existsSync11(path2))
|
|
31339
32052
|
return false;
|
|
31340
32053
|
try {
|
|
31341
|
-
const raw =
|
|
32054
|
+
const raw = readFileSync9(path2, "utf-8");
|
|
31342
32055
|
return latestRolloutRowForRequest(raw, targetRequestId) !== null;
|
|
31343
32056
|
} catch {
|
|
31344
32057
|
return false;
|
|
@@ -31346,11 +32059,11 @@ ${output.recovery.stderr}` : "";
|
|
|
31346
32059
|
}
|
|
31347
32060
|
rolloutStatusFromLog(responseRequestId, targetRequestId, started) {
|
|
31348
32061
|
const path2 = this.auditLogPath();
|
|
31349
|
-
if (!
|
|
32062
|
+
if (!existsSync11(path2))
|
|
31350
32063
|
return null;
|
|
31351
32064
|
let raw;
|
|
31352
32065
|
try {
|
|
31353
|
-
raw =
|
|
32066
|
+
raw = readFileSync9(path2, "utf-8");
|
|
31354
32067
|
} catch {
|
|
31355
32068
|
return null;
|
|
31356
32069
|
}
|
|
@@ -31429,7 +32142,7 @@ ${output.recovery.stderr}` : "";
|
|
|
31429
32142
|
}
|
|
31430
32143
|
}
|
|
31431
32144
|
auditLogPath() {
|
|
31432
|
-
return this.opts.auditLogPath ??
|
|
32145
|
+
return this.opts.auditLogPath ?? join10(this.opts.homeDir, ".switchroom", "host-control-audit.log");
|
|
31433
32146
|
}
|
|
31434
32147
|
appendAuditRow(row) {
|
|
31435
32148
|
const path2 = this.auditLogPath();
|
|
@@ -31455,6 +32168,7 @@ ${output.recovery.stderr}` : "";
|
|
|
31455
32168
|
await this.appendAuditRow({
|
|
31456
32169
|
ts: new Date().toISOString(),
|
|
31457
32170
|
op: args.req.op,
|
|
32171
|
+
...args.method ? { method: args.method } : {},
|
|
31458
32172
|
caller: args.caller.kind === "agent" ? { kind: "agent", name: args.caller.name } : { kind: "operator" },
|
|
31459
32173
|
request_id: args.req.request_id,
|
|
31460
32174
|
result: args.resp.result,
|
|
@@ -31682,13 +32396,84 @@ function isSafeExecArgvElement(s) {
|
|
|
31682
32396
|
}
|
|
31683
32397
|
|
|
31684
32398
|
// src/host-control/approval-gateway.ts
|
|
31685
|
-
import { connect } from "node:net";
|
|
32399
|
+
import { connect as connect2 } from "node:net";
|
|
32400
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
32401
|
+
var PRE_APPROVED_QUERY_TIMEOUT_MS = 2000;
|
|
31686
32402
|
|
|
31687
32403
|
class SocketApprovalGateway {
|
|
31688
32404
|
opts;
|
|
31689
32405
|
constructor(opts) {
|
|
31690
32406
|
this.opts = opts;
|
|
31691
32407
|
}
|
|
32408
|
+
async checkPreApproved(agentName, unifiedDiff) {
|
|
32409
|
+
const sockPath = this.opts.resolveGatewaySocket(agentName);
|
|
32410
|
+
if (sockPath === null)
|
|
32411
|
+
return false;
|
|
32412
|
+
const log = this.opts.log ?? (() => {});
|
|
32413
|
+
const correlationId = `preapp-${randomBytes2(8).toString("hex")}`;
|
|
32414
|
+
return await new Promise((resolve9) => {
|
|
32415
|
+
let settled = false;
|
|
32416
|
+
const client2 = connect2({ path: sockPath });
|
|
32417
|
+
let buffer = "";
|
|
32418
|
+
const done = (result) => {
|
|
32419
|
+
if (settled)
|
|
32420
|
+
return;
|
|
32421
|
+
settled = true;
|
|
32422
|
+
clearTimeout(timer);
|
|
32423
|
+
try {
|
|
32424
|
+
client2.destroy();
|
|
32425
|
+
} catch {}
|
|
32426
|
+
resolve9(result);
|
|
32427
|
+
};
|
|
32428
|
+
const timer = setTimeout(() => {
|
|
32429
|
+
log(`checkPreApproved timed out after ${PRE_APPROVED_QUERY_TIMEOUT_MS}ms (agent=${agentName}) — failing closed`);
|
|
32430
|
+
done(false);
|
|
32431
|
+
}, PRE_APPROVED_QUERY_TIMEOUT_MS);
|
|
32432
|
+
client2.on("connect", () => {
|
|
32433
|
+
try {
|
|
32434
|
+
client2.write(JSON.stringify({
|
|
32435
|
+
type: "check_pre_approved",
|
|
32436
|
+
agentName,
|
|
32437
|
+
correlationId,
|
|
32438
|
+
unifiedDiff
|
|
32439
|
+
}) + `
|
|
32440
|
+
`);
|
|
32441
|
+
} catch (err2) {
|
|
32442
|
+
log(`check_pre_approved write failed (agent=${agentName}): ${err2.message} — failing closed`);
|
|
32443
|
+
done(false);
|
|
32444
|
+
}
|
|
32445
|
+
});
|
|
32446
|
+
client2.on("data", (chunk2) => {
|
|
32447
|
+
buffer += chunk2.toString("utf8");
|
|
32448
|
+
const lines = buffer.split(`
|
|
32449
|
+
`);
|
|
32450
|
+
buffer = lines.pop() ?? "";
|
|
32451
|
+
for (const line of lines) {
|
|
32452
|
+
if (!line.trim())
|
|
32453
|
+
continue;
|
|
32454
|
+
let parsed;
|
|
32455
|
+
try {
|
|
32456
|
+
parsed = JSON.parse(line);
|
|
32457
|
+
} catch {
|
|
32458
|
+
done(false);
|
|
32459
|
+
return;
|
|
32460
|
+
}
|
|
32461
|
+
const obj = parsed;
|
|
32462
|
+
if (obj.type === "pre_approved_result" && obj.correlationId === correlationId) {
|
|
32463
|
+
done(obj.preApproved === true);
|
|
32464
|
+
return;
|
|
32465
|
+
}
|
|
32466
|
+
done(false);
|
|
32467
|
+
return;
|
|
32468
|
+
}
|
|
32469
|
+
});
|
|
32470
|
+
client2.on("error", (err2) => {
|
|
32471
|
+
log(`checkPreApproved socket error (agent=${agentName}): ${err2.message} — failing closed`);
|
|
32472
|
+
done(false);
|
|
32473
|
+
});
|
|
32474
|
+
client2.on("close", () => done(false));
|
|
32475
|
+
});
|
|
32476
|
+
}
|
|
31692
32477
|
async requestApproval(req) {
|
|
31693
32478
|
const sockPath = this.opts.resolveGatewaySocket(req.agentName);
|
|
31694
32479
|
if (sockPath === null) {
|
|
@@ -31700,7 +32485,7 @@ class SocketApprovalGateway {
|
|
|
31700
32485
|
};
|
|
31701
32486
|
}
|
|
31702
32487
|
return await new Promise((resolve9) => {
|
|
31703
|
-
const client2 =
|
|
32488
|
+
const client2 = connect2({ path: sockPath });
|
|
31704
32489
|
let buffer = "";
|
|
31705
32490
|
let resolved = false;
|
|
31706
32491
|
const log = this.opts.log ?? (() => {});
|
|
@@ -31804,7 +32589,7 @@ class SocketApprovalGateway {
|
|
|
31804
32589
|
}
|
|
31805
32590
|
|
|
31806
32591
|
// src/host-control/rollout-relay.ts
|
|
31807
|
-
import { connect as
|
|
32592
|
+
import { connect as connect3 } from "node:net";
|
|
31808
32593
|
|
|
31809
32594
|
class SocketRolloutRelay {
|
|
31810
32595
|
opts;
|
|
@@ -31825,7 +32610,7 @@ class SocketRolloutRelay {
|
|
|
31825
32610
|
return;
|
|
31826
32611
|
}
|
|
31827
32612
|
try {
|
|
31828
|
-
const client2 =
|
|
32613
|
+
const client2 = connect3({ path: sockPath });
|
|
31829
32614
|
client2.setTimeout(5000);
|
|
31830
32615
|
const done = () => {
|
|
31831
32616
|
try {
|
|
@@ -31863,7 +32648,7 @@ class SocketRolloutRelay {
|
|
|
31863
32648
|
}
|
|
31864
32649
|
|
|
31865
32650
|
// src/host-control/rollout-narration-relay.ts
|
|
31866
|
-
import { connect as
|
|
32651
|
+
import { connect as connect4 } from "node:net";
|
|
31867
32652
|
var DEFAULT_POST_REPLY_TIMEOUT_MS = 5000;
|
|
31868
32653
|
|
|
31869
32654
|
class SocketRolloutNarrationRelay {
|
|
@@ -31897,7 +32682,7 @@ class SocketRolloutNarrationRelay {
|
|
|
31897
32682
|
};
|
|
31898
32683
|
let client2;
|
|
31899
32684
|
try {
|
|
31900
|
-
client2 =
|
|
32685
|
+
client2 = connect4({ path: sockPath });
|
|
31901
32686
|
} catch (e) {
|
|
31902
32687
|
log(`narration post connect threw: ${e.message}`);
|
|
31903
32688
|
resolve9(null);
|
|
@@ -31967,7 +32752,7 @@ class SocketRolloutNarrationRelay {
|
|
|
31967
32752
|
if (sockPath === null)
|
|
31968
32753
|
return;
|
|
31969
32754
|
try {
|
|
31970
|
-
const client2 =
|
|
32755
|
+
const client2 = connect4({ path: sockPath });
|
|
31971
32756
|
client2.setTimeout(5000);
|
|
31972
32757
|
const done = () => {
|
|
31973
32758
|
try {
|
|
@@ -32014,6 +32799,47 @@ class LogTailRolloutNarrator {
|
|
|
32014
32799
|
this.relay = relay;
|
|
32015
32800
|
this.opts = opts;
|
|
32016
32801
|
}
|
|
32802
|
+
now() {
|
|
32803
|
+
return this.opts.now?.() ?? Date.now();
|
|
32804
|
+
}
|
|
32805
|
+
static upsertAgent(st, name, patch) {
|
|
32806
|
+
const existing = st.agents.find((a) => a.name === name);
|
|
32807
|
+
if (existing)
|
|
32808
|
+
Object.assign(existing, patch);
|
|
32809
|
+
else
|
|
32810
|
+
st.agents.push({ name, status: "pending", ...patch });
|
|
32811
|
+
}
|
|
32812
|
+
trackAgentProgress(st, phase) {
|
|
32813
|
+
const now = this.now();
|
|
32814
|
+
switch (phase.phase) {
|
|
32815
|
+
case "canary-start":
|
|
32816
|
+
case "agent-start":
|
|
32817
|
+
if (phase.agent) {
|
|
32818
|
+
LogTailRolloutNarrator.upsertAgent(st, phase.agent, {
|
|
32819
|
+
status: "running",
|
|
32820
|
+
startedAtMs: now,
|
|
32821
|
+
...phase.phase === "canary-start" ? { canary: true } : {}
|
|
32822
|
+
});
|
|
32823
|
+
}
|
|
32824
|
+
break;
|
|
32825
|
+
case "canary-pass":
|
|
32826
|
+
case "agent-done":
|
|
32827
|
+
case "canary-fail": {
|
|
32828
|
+
if (!phase.agent)
|
|
32829
|
+
break;
|
|
32830
|
+
const a = st.agents.find((x) => x.name === phase.agent);
|
|
32831
|
+
const durationMs = a?.startedAtMs !== undefined ? now - a.startedAtMs : undefined;
|
|
32832
|
+
LogTailRolloutNarrator.upsertAgent(st, phase.agent, {
|
|
32833
|
+
status: phase.phase === "canary-fail" ? "failed" : "done",
|
|
32834
|
+
...durationMs !== undefined ? { durationMs } : {},
|
|
32835
|
+
...phase.phase !== "agent-done" ? { canary: true } : {}
|
|
32836
|
+
});
|
|
32837
|
+
break;
|
|
32838
|
+
}
|
|
32839
|
+
default:
|
|
32840
|
+
break;
|
|
32841
|
+
}
|
|
32842
|
+
}
|
|
32017
32843
|
static seqFor(phase) {
|
|
32018
32844
|
switch (phase.phase) {
|
|
32019
32845
|
case "self-bump":
|
|
@@ -32032,6 +32858,8 @@ class LogTailRolloutNarrator {
|
|
|
32032
32858
|
return 2 * (phase.n ?? 1) + 0;
|
|
32033
32859
|
case "agent-done":
|
|
32034
32860
|
return 2 * (phase.n ?? 1) + 1;
|
|
32861
|
+
case "web-refresh":
|
|
32862
|
+
return Number.MAX_SAFE_INTEGER - 2;
|
|
32035
32863
|
case "hostd-web-deferred":
|
|
32036
32864
|
return Number.MAX_SAFE_INTEGER - 1;
|
|
32037
32865
|
default:
|
|
@@ -32047,6 +32875,7 @@ class LogTailRolloutNarrator {
|
|
|
32047
32875
|
if (seq < st.lastAppliedSeq)
|
|
32048
32876
|
return;
|
|
32049
32877
|
st.lastAppliedSeq = seq;
|
|
32878
|
+
this.trackAgentProgress(st, phase);
|
|
32050
32879
|
st.render = {
|
|
32051
32880
|
...st.render,
|
|
32052
32881
|
target: phase.target,
|
|
@@ -32054,7 +32883,11 @@ class LogTailRolloutNarrator {
|
|
|
32054
32883
|
...phase.n !== undefined ? { n: phase.n } : {},
|
|
32055
32884
|
...phase.m !== undefined ? { m: phase.m } : {},
|
|
32056
32885
|
...phase.agent !== undefined ? { agent: phase.agent } : {},
|
|
32057
|
-
rolled: entry.rolled
|
|
32886
|
+
rolled: entry.rolled,
|
|
32887
|
+
agents: st.agents,
|
|
32888
|
+
requestId: entry.request_id,
|
|
32889
|
+
...entry.prior_pin ? { fromVersion: entry.prior_pin } : {},
|
|
32890
|
+
startedAtMs: entry.started_at
|
|
32058
32891
|
};
|
|
32059
32892
|
this.scheduleRenderOrPost(entry.request_id);
|
|
32060
32893
|
} catch (e) {
|
|
@@ -32064,13 +32897,29 @@ class LogTailRolloutNarrator {
|
|
|
32064
32897
|
onTerminal(entry) {
|
|
32065
32898
|
try {
|
|
32066
32899
|
const st = this.ensureState(entry);
|
|
32900
|
+
const rolled = new Set(entry.rolled ?? []);
|
|
32901
|
+
for (const a of st.agents) {
|
|
32902
|
+
if (entry.failed_agent && a.name === entry.failed_agent) {
|
|
32903
|
+
a.status = "failed";
|
|
32904
|
+
} else if (rolled.has(a.name)) {
|
|
32905
|
+
a.status = "done";
|
|
32906
|
+
} else if (a.status === "running") {
|
|
32907
|
+
a.status = "pending";
|
|
32908
|
+
}
|
|
32909
|
+
}
|
|
32067
32910
|
st.render = {
|
|
32068
32911
|
target: entry.pin ?? st.render.target,
|
|
32069
32912
|
rolled: entry.rolled ?? st.render.rolled,
|
|
32070
32913
|
terminal: entry.result === "completed" ? "completed" : "error",
|
|
32071
32914
|
...entry.failed_step ? { failedStep: entry.failed_step } : {},
|
|
32072
32915
|
...entry.failed_agent ? { failedAgent: entry.failed_agent } : {},
|
|
32073
|
-
...entry.got !== undefined ? { got: entry.got } : {}
|
|
32916
|
+
...entry.got !== undefined ? { got: entry.got } : {},
|
|
32917
|
+
...st.agents.length > 0 ? { agents: st.agents } : {},
|
|
32918
|
+
...st.render.m !== undefined ? { m: st.render.m } : {},
|
|
32919
|
+
requestId: entry.request_id,
|
|
32920
|
+
...entry.prior_pin ?? st.render.fromVersion ? { fromVersion: entry.prior_pin ?? st.render.fromVersion } : {},
|
|
32921
|
+
elapsedMs: (entry.finished_at ?? this.now()) - entry.started_at,
|
|
32922
|
+
deferred: false
|
|
32074
32923
|
};
|
|
32075
32924
|
st.frozen = true;
|
|
32076
32925
|
if (st.timer) {
|
|
@@ -32095,6 +32944,7 @@ class LogTailRolloutNarrator {
|
|
|
32095
32944
|
postAttempts: 0,
|
|
32096
32945
|
postFailed: false,
|
|
32097
32946
|
render: { target: entry.pin ?? "" },
|
|
32947
|
+
agents: [],
|
|
32098
32948
|
frozen: false,
|
|
32099
32949
|
timer: null,
|
|
32100
32950
|
pendingEditAfterPost: false
|
|
@@ -32133,7 +32983,7 @@ class LogTailRolloutNarrator {
|
|
|
32133
32983
|
return false;
|
|
32134
32984
|
st.posting = true;
|
|
32135
32985
|
st.postAttempts += 1;
|
|
32136
|
-
const text = renderRolloutStatus(st.render);
|
|
32986
|
+
const text = renderRolloutStatus({ ...st.render, nowMs: this.now() });
|
|
32137
32987
|
this.relay.post({ requestId, agentName: st.agentName, text }).then((mid) => {
|
|
32138
32988
|
st.posting = false;
|
|
32139
32989
|
if (mid === null) {
|
|
@@ -32185,7 +33035,7 @@ class LogTailRolloutNarrator {
|
|
|
32185
33035
|
this.tryPost(requestId, true);
|
|
32186
33036
|
return;
|
|
32187
33037
|
}
|
|
32188
|
-
const text = renderRolloutStatus(st.render);
|
|
33038
|
+
const text = renderRolloutStatus({ ...st.render, nowMs: this.now() });
|
|
32189
33039
|
this.relay.edit({
|
|
32190
33040
|
requestId,
|
|
32191
33041
|
agentName: st.agentName,
|
|
@@ -32443,8 +33293,60 @@ function makeRestart(switchroomBin) {
|
|
|
32443
33293
|
|
|
32444
33294
|
// src/host-control/main.ts
|
|
32445
33295
|
import { appendFile as appendFile2 } from "node:fs/promises";
|
|
33296
|
+
async function loadConfigResilient() {
|
|
33297
|
+
try {
|
|
33298
|
+
const config = loadConfig();
|
|
33299
|
+
clearStaleDegradedMarker(homedir6(), (m) => process.stderr.write(`hostd: ${m}
|
|
33300
|
+
`));
|
|
33301
|
+
return config;
|
|
33302
|
+
} catch (err2) {
|
|
33303
|
+
if (!(err2 instanceof ConfigError))
|
|
33304
|
+
throw err2;
|
|
33305
|
+
let configPath;
|
|
33306
|
+
try {
|
|
33307
|
+
configPath = findConfigFile();
|
|
33308
|
+
} catch {
|
|
33309
|
+
configPath = undefined;
|
|
33310
|
+
}
|
|
33311
|
+
const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ?? join11(homedir6(), ".switchroom", "agents");
|
|
33312
|
+
const listGatewayCandidates = () => {
|
|
33313
|
+
const out = [];
|
|
33314
|
+
try {
|
|
33315
|
+
for (const name of readdirSync4(agentsDir).sort()) {
|
|
33316
|
+
const sock = resolve9(agentsDir, name, "telegram", "gateway.sock");
|
|
33317
|
+
if (existsSync12(sock))
|
|
33318
|
+
out.push({ agent: name, sock });
|
|
33319
|
+
}
|
|
33320
|
+
} catch {}
|
|
33321
|
+
return out;
|
|
33322
|
+
};
|
|
33323
|
+
const log = (m) => {
|
|
33324
|
+
process.stderr.write(`hostd: ${m}
|
|
33325
|
+
`);
|
|
33326
|
+
};
|
|
33327
|
+
const notify = (text) => {
|
|
33328
|
+
const candidates = listGatewayCandidates();
|
|
33329
|
+
if (candidates.length === 0) {
|
|
33330
|
+
log("config-degraded: no gateway socket paths found; relying on marker file");
|
|
33331
|
+
return;
|
|
33332
|
+
}
|
|
33333
|
+
postOperatorNoticeViaGateways(candidates, text, log).then((agent) => {
|
|
33334
|
+
if (agent !== null)
|
|
33335
|
+
log(`config-degraded: operator DM relayed via ${agent}`);
|
|
33336
|
+
});
|
|
33337
|
+
};
|
|
33338
|
+
return waitForConfigRecovery({
|
|
33339
|
+
initialError: err2,
|
|
33340
|
+
homeDir: homedir6(),
|
|
33341
|
+
loadFn: () => loadConfig(),
|
|
33342
|
+
configPath,
|
|
33343
|
+
notify,
|
|
33344
|
+
log
|
|
33345
|
+
});
|
|
33346
|
+
}
|
|
33347
|
+
}
|
|
32446
33348
|
async function main() {
|
|
32447
|
-
const config =
|
|
33349
|
+
const config = await loadConfigResilient();
|
|
32448
33350
|
if (config.host_control?.enabled !== true) {
|
|
32449
33351
|
process.stderr.write(`hostd: refusing to start — host_control.enabled is not true in switchroom.yaml
|
|
32450
33352
|
`);
|
|
@@ -32459,10 +33361,10 @@ async function main() {
|
|
|
32459
33361
|
`);
|
|
32460
33362
|
process.exit(2);
|
|
32461
33363
|
}
|
|
32462
|
-
const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ??
|
|
33364
|
+
const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ?? join11(homedir6(), ".switchroom", "agents");
|
|
32463
33365
|
const resolveGatewaySocket = (agentName) => {
|
|
32464
33366
|
const sock = resolve9(agentsDir, agentName, "telegram", "gateway.sock");
|
|
32465
|
-
return
|
|
33367
|
+
return existsSync12(sock) ? sock : null;
|
|
32466
33368
|
};
|
|
32467
33369
|
const approvalGateway = new SocketApprovalGateway({
|
|
32468
33370
|
resolveGatewaySocket,
|
|
@@ -32490,7 +33392,11 @@ async function main() {
|
|
|
32490
33392
|
])),
|
|
32491
33393
|
...config.hostd ? {
|
|
32492
33394
|
hostd: {
|
|
32493
|
-
...config.hostd.config_edit_enabled !== undefined ? { config_edit_enabled: config.hostd.config_edit_enabled } : {}
|
|
33395
|
+
...config.hostd.config_edit_enabled !== undefined ? { config_edit_enabled: config.hostd.config_edit_enabled } : {},
|
|
33396
|
+
...config.hostd.operator_attest_enabled !== undefined ? { operator_attest_enabled: config.hostd.operator_attest_enabled } : {},
|
|
33397
|
+
...config.hostd.operator_attest_required_verbs !== undefined ? {
|
|
33398
|
+
operator_attest_required_verbs: config.hostd.operator_attest_required_verbs
|
|
33399
|
+
} : {}
|
|
32494
33400
|
}
|
|
32495
33401
|
} : {}
|
|
32496
33402
|
},
|
|
@@ -32505,7 +33411,7 @@ async function main() {
|
|
|
32505
33411
|
let releaseWatcher = null;
|
|
32506
33412
|
const autoRel = config.host_control?.auto_release_check;
|
|
32507
33413
|
if (autoRel?.enabled === true) {
|
|
32508
|
-
const eventsLog =
|
|
33414
|
+
const eventsLog = join11(homedir6(), ".switchroom", "release-watcher-events.jsonl");
|
|
32509
33415
|
releaseWatcher = new ReleaseWatcher({
|
|
32510
33416
|
intervalMs: autoRel.interval_minutes * 60000,
|
|
32511
33417
|
checkFn: makeReleaseCheck({
|