switchroom 0.18.9 → 0.18.11
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 +1 -0
- package/dist/auth-broker/index.js +198 -13
- package/dist/cli/notion-write-pretool.mjs +1 -0
- package/dist/cli/switchroom.js +214 -83
- 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/host-control/main.js +760 -257
- package/dist/vault/approvals/kernel-server.js +3 -1
- package/dist/vault/broker/server.js +3 -1
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +119 -37
- package/profiles/_shared/dev-protocol.md.hbs +42 -0
- package/skills/dev-protocol/SKILL.md +131 -0
- package/telegram-plugin/README.md +2 -1
- package/telegram-plugin/admin-commands/dispatch.test.ts +40 -2
- package/telegram-plugin/admin-commands/index.ts +6 -1
- package/telegram-plugin/bridge/bridge.ts +23 -1
- package/telegram-plugin/bridge/crash-breadcrumb.ts +42 -0
- package/telegram-plugin/chat-lock.ts +13 -0
- package/telegram-plugin/dist/bridge/bridge.js +24 -1
- package/telegram-plugin/dist/gateway/gateway.js +1832 -263
- package/telegram-plugin/dist/server.js +29 -2
- package/telegram-plugin/fallback-card-collapse.ts +131 -0
- package/telegram-plugin/gateway/bridge-dead-watchdog.ts +546 -0
- package/telegram-plugin/gateway/effort-command.ts +47 -3
- package/telegram-plugin/gateway/gateway.ts +1435 -211
- package/telegram-plugin/gateway/model-command.ts +94 -8
- package/telegram-plugin/gateway/pending-session-command.ts +365 -0
- package/telegram-plugin/gateway/permission-timeout.ts +25 -0
- package/telegram-plugin/gateway/resume-inbound-builder.ts +23 -3
- package/telegram-plugin/gateway/session-model-file.ts +166 -23
- package/telegram-plugin/gateway/stop-command.ts +56 -0
- package/telegram-plugin/photo-precheck.ts +201 -0
- package/telegram-plugin/quota-watch.ts +141 -2
- package/telegram-plugin/registry/subagents-schema.ts +26 -3
- package/telegram-plugin/registry/subagents.test.ts +67 -0
- package/telegram-plugin/retry-api-call.ts +31 -0
- package/telegram-plugin/subagent-watcher.ts +392 -1
- package/telegram-plugin/tests/bridge-dead-watchdog.test.ts +576 -0
- package/telegram-plugin/tests/buffer-gate-broadened.test.ts +11 -5
- package/telegram-plugin/tests/chat-lock-unhandled-rejection.test.ts +101 -0
- package/telegram-plugin/tests/crash-breadcrumb.test.ts +57 -0
- package/telegram-plugin/tests/effort-command.test.ts +59 -2
- package/telegram-plugin/tests/fallback-card-collapse.test.ts +104 -0
- package/telegram-plugin/tests/gateway-pending-command-wiring.test.ts +124 -0
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +7 -1
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +19 -11
- package/telegram-plugin/tests/model-command.test.ts +46 -3
- package/telegram-plugin/tests/pending-session-command.test.ts +322 -0
- package/telegram-plugin/tests/permission-timeout.test.ts +26 -0
- package/telegram-plugin/tests/permission-verdict-resume-guard.test.ts +16 -0
- package/telegram-plugin/tests/photo-dimension-fallback.test.ts +129 -0
- package/telegram-plugin/tests/photo-precheck.test.ts +240 -0
- package/telegram-plugin/tests/photo-reroute-wiring.test.ts +85 -0
- package/telegram-plugin/tests/quota-watch.test.ts +225 -0
- package/telegram-plugin/tests/session-model-file.test.ts +101 -2
- package/telegram-plugin/tests/stop-command.test.ts +234 -0
- package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +27 -9
- package/telegram-plugin/tests/subagent-watcher-resurrection.test.ts +398 -0
- package/telegram-plugin/tests/subagent-watcher-stall-terminal.test.ts +172 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +37 -0
- package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +18 -4
- package/telegram-plugin/welcome-text.ts +4 -3
- package/telegram-plugin/worker-activity-feed.ts +27 -0
package/dist/cli/switchroom.js
CHANGED
|
@@ -14232,6 +14232,7 @@ var init_schema = __esm(() => {
|
|
|
14232
14232
|
uid: exports_external.number().int().nonnegative().optional().describe("Optional UID to chown the consumer socket to (defaults to 0 = root, " + "suitable for sibling containers running as root)."),
|
|
14233
14233
|
mirror_dir: exports_external.string().optional().describe("Optional host-side directory path. When set, the broker actively " + "writes the consumer's effective-account `.credentials.json` mirror " + "here \u2014 in addition to serving creds on demand via `get-credentials`. " + "Use this to eliminate the pull-latency gap: without a mirror the " + "consumer only gets failover creds at its next scheduled re-fetch " + "(up to 30 min). With a mirror the broker pushes failover creds " + "immediately when it detects exhaustion (consumer-quota-sensor tick, " + "or a mark-exhausted RPC on the pinned account). The directory must " + "be accessible to the broker container (bind-mounted from the host) " + "and to the consumer container; the broker writes " + "`<mirror_dir>/.credentials.json` atomically. Chown is attempted to " + "`uid` (default 0) \u2014 swallowed when CAP_CHOWN is absent.")
|
|
14234
14234
|
})).optional().describe("Non-agent peers that hold a broker socket (RFC H \u00a74.8). Each gets " + "its own `/run/switchroom/auth-broker/<name>/sock` chowned to its UID. " + "Consumers cannot be admins; a consumer name that collides with an " + "agent (whether that agent has `admin: true` or not) is a config " + "error caught at schema validation."),
|
|
14235
|
+
proactive_failover_pct: exports_external.number().min(1).max(99).optional().describe("Soft-avoid threshold (percent) for proactive auth failover (#3031). " + "When set (e.g. 95), an account whose fresh 7-day utilization is at/" + "above this value \u2014 or whose 5h utilization is at/above min(pct+3, " + "98) \u2014 enters the SOFT-AVOID preference tier: the broker prefers a " + "fully-eligible fallback account on the serving path, without ever " + "blocking the account (the hard wall stays 99.5). Enter/exit uses " + "hysteresis (exit below pct-5 or on window reset) so the preference " + "does not flap between probe ticks. Accounts serving past the wall " + "via `allow_overage_accounts` are never soft-avoided. UNSET (the " + "default) disables the tier entirely \u2014 behavior is identical to a " + "fleet without this field."),
|
|
14235
14236
|
allow_overage_accounts: exports_external.array(exports_external.string().min(1)).optional().describe("Opt-in list of account labels (bare strings matching `auth.active` / " + "`auth.fallback_order` entries) that may be served PAST the weekly " + "utilization wall when Anthropic overage billing is available for the " + "account (`overageStatus === 'allowed'`). Overage is REAL MONEY \u2014 " + "default is empty (no account gets this). An account in this list is " + "only kept eligible when its fresh quota snapshot reports " + "`overageStatus: 'allowed'` AND `overageDisabledReason` is NOT " + "'out_of_credits' (i.e. the overage credit has not been exhausted). " + "As soon as `overageDisabledReason` becomes 'out_of_credits', the " + "account is blocked immediately regardless of this flag. Overage lifts " + "ONLY the utilization wall \u2014 it cannot lift an active exhaustion mark " + "written by a real 429 (`mark-exhausted`).")
|
|
14236
14237
|
}).optional().describe("Switchroom-auth-broker configuration (RFC H). Fleet-wide active account, " + "fallback order, admin-agent ACL, and ephemeral-consumer surface. " + "Required from the v0.8+ schema onwards; pre-v0.8 fleets are migrated " + "in-place by `switchroom apply` (see src/auth/migrate-schema.ts)."),
|
|
14237
14238
|
drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key \u2014 use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
|
|
@@ -17427,6 +17428,40 @@ function registerAgentConfigCommands(program2) {
|
|
|
17427
17428
|
process.exit(1);
|
|
17428
17429
|
}
|
|
17429
17430
|
}));
|
|
17431
|
+
config.command("check").description("Validate a switchroom.yaml (YAML parse incl. duplicate keys, schema, cross-field checks) without starting anything").option("--file <path>", "YAML file to validate (defaults to the discovered switchroom.yaml)").action(async (opts) => {
|
|
17432
|
+
let path;
|
|
17433
|
+
try {
|
|
17434
|
+
path = opts.file ?? findConfigFile();
|
|
17435
|
+
} catch (err) {
|
|
17436
|
+
if (!(err instanceof ConfigError))
|
|
17437
|
+
throw err;
|
|
17438
|
+
process.stderr.write(`INVALID: switchroom.yaml not found
|
|
17439
|
+
${err.message}
|
|
17440
|
+
`);
|
|
17441
|
+
for (const d of err.details ?? []) {
|
|
17442
|
+
process.stderr.write(`${d}
|
|
17443
|
+
`);
|
|
17444
|
+
}
|
|
17445
|
+
process.exit(1);
|
|
17446
|
+
}
|
|
17447
|
+
try {
|
|
17448
|
+
loadConfig(path);
|
|
17449
|
+
} catch (err) {
|
|
17450
|
+
if (err instanceof ConfigError) {
|
|
17451
|
+
process.stderr.write(`INVALID: ${path}
|
|
17452
|
+
${err.message}
|
|
17453
|
+
`);
|
|
17454
|
+
for (const d of err.details ?? []) {
|
|
17455
|
+
process.stderr.write(`${d}
|
|
17456
|
+
`);
|
|
17457
|
+
}
|
|
17458
|
+
process.exit(1);
|
|
17459
|
+
}
|
|
17460
|
+
throw err;
|
|
17461
|
+
}
|
|
17462
|
+
process.stdout.write(`OK: ${path} is a valid switchroom config
|
|
17463
|
+
`);
|
|
17464
|
+
});
|
|
17430
17465
|
config.command("whoami").description("Emit what this agent is allowed to do (tools, MCP, vault key-names, powers) as JSON \u2014 its own sandbox, computed from live enforcement").option("--agent <name>", "Target agent (defaults to $SWITCHROOM_AGENT_NAME)").action(withConfigError(async (opts) => {
|
|
17431
17466
|
let agent;
|
|
17432
17467
|
try {
|
|
@@ -17587,6 +17622,7 @@ function registerAgentConfigCommands(program2) {
|
|
|
17587
17622
|
var AUDIT_ROOT;
|
|
17588
17623
|
var init_agent_config = __esm(() => {
|
|
17589
17624
|
init_helpers();
|
|
17625
|
+
init_loader();
|
|
17590
17626
|
init_merge();
|
|
17591
17627
|
init_acl();
|
|
17592
17628
|
init_cron_introspect();
|
|
@@ -23256,6 +23292,14 @@ function renderExecutionDisciplineFragment(context = {}, profilesRoot = PROFILES
|
|
|
23256
23292
|
const template = import_handlebars.default.compile(source, { noEscape: true });
|
|
23257
23293
|
return template(context).trimEnd();
|
|
23258
23294
|
}
|
|
23295
|
+
function renderDevProtocolFragment(context = {}, profilesRoot = PROFILES_ROOT) {
|
|
23296
|
+
const fragPath = join4(resolve4(profilesRoot, "_shared"), "dev-protocol.md.hbs");
|
|
23297
|
+
if (!existsSync6(fragPath))
|
|
23298
|
+
return "";
|
|
23299
|
+
const source = readFileSync6(fragPath, "utf-8");
|
|
23300
|
+
const template = import_handlebars.default.compile(source, { noEscape: true });
|
|
23301
|
+
return template(context).trimEnd();
|
|
23302
|
+
}
|
|
23259
23303
|
function renderReplyDisciplineFragment(context = {}, profilesRoot = PROFILES_ROOT) {
|
|
23260
23304
|
const fragPath = join4(resolve4(profilesRoot, "_shared"), "reply-discipline.md.hbs");
|
|
23261
23305
|
if (!existsSync6(fragPath))
|
|
@@ -23297,7 +23341,7 @@ var init_profiles = __esm(() => {
|
|
|
23297
23341
|
return typeof value === "number" && Number.isFinite(value);
|
|
23298
23342
|
});
|
|
23299
23343
|
SHARED_FRAGMENTS_DIR = resolve4(PROFILES_ROOT, "_shared");
|
|
23300
|
-
SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline"];
|
|
23344
|
+
SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol"];
|
|
23301
23345
|
for (const name of SHARED_FRAGMENTS) {
|
|
23302
23346
|
const fragPath = join4(SHARED_FRAGMENTS_DIR, `${name}.md.hbs`);
|
|
23303
23347
|
if (existsSync6(fragPath)) {
|
|
@@ -23383,7 +23427,8 @@ function getBuiltinDefaultSkillEntries() {
|
|
|
23383
23427
|
"switchroom-status",
|
|
23384
23428
|
"switchroom-health",
|
|
23385
23429
|
"switchroom-runtime",
|
|
23386
|
-
"mental-model-curator"
|
|
23430
|
+
"mental-model-curator",
|
|
23431
|
+
"dev-protocol"
|
|
23387
23432
|
];
|
|
23388
23433
|
return [
|
|
23389
23434
|
...anthropic.map((key) => ({ key, optOutKey: key, source: "anthropic" })),
|
|
@@ -27277,6 +27322,13 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
|
|
|
27277
27322
|
rendered = rendered.trimEnd() + `
|
|
27278
27323
|
|
|
27279
27324
|
` + executionDiscipline + `
|
|
27325
|
+
`;
|
|
27326
|
+
}
|
|
27327
|
+
const devProtocol = renderDevProtocolFragment(context);
|
|
27328
|
+
if (devProtocol) {
|
|
27329
|
+
rendered = rendered.trimEnd() + `
|
|
27330
|
+
|
|
27331
|
+
` + devProtocol + `
|
|
27280
27332
|
`;
|
|
27281
27333
|
}
|
|
27282
27334
|
}
|
|
@@ -28111,6 +28163,13 @@ ${baseAppend}` : TELEGRAM_FORMATTING_FLOOR_CARD;
|
|
|
28111
28163
|
rendered = rendered.trimEnd() + `
|
|
28112
28164
|
|
|
28113
28165
|
` + executionDiscipline + `
|
|
28166
|
+
`;
|
|
28167
|
+
}
|
|
28168
|
+
const devProtocol = renderDevProtocolFragment(claudeContext);
|
|
28169
|
+
if (devProtocol) {
|
|
28170
|
+
rendered = rendered.trimEnd() + `
|
|
28171
|
+
|
|
28172
|
+
` + devProtocol + `
|
|
28114
28173
|
`;
|
|
28115
28174
|
}
|
|
28116
28175
|
let composed = composeWithSidecar(rendered, claudeCustomPath);
|
|
@@ -63375,9 +63434,9 @@ __export(exports_server, {
|
|
|
63375
63434
|
dispatchTool: () => dispatchTool,
|
|
63376
63435
|
TOOLS: () => TOOLS
|
|
63377
63436
|
});
|
|
63378
|
-
import { spawnSync as
|
|
63437
|
+
import { spawnSync as spawnSync18 } from "node:child_process";
|
|
63379
63438
|
function execCli(args, stdin) {
|
|
63380
|
-
const r =
|
|
63439
|
+
const r = spawnSync18(CLI_BIN, args, {
|
|
63381
63440
|
encoding: "utf-8",
|
|
63382
63441
|
env: process.env,
|
|
63383
63442
|
timeout: 15000,
|
|
@@ -63881,6 +63940,7 @@ __export(exports_server2, {
|
|
|
63881
63940
|
resolveAuditLogPath: () => resolveAuditLogPath,
|
|
63882
63941
|
getLastUpdateApplyStatus: () => getLastUpdateApplyStatus,
|
|
63883
63942
|
dispatchTool: () => dispatchTool2,
|
|
63943
|
+
UPDATE_APPLY_SEMVER_PIN_ADVISORY: () => UPDATE_APPLY_SEMVER_PIN_ADVISORY,
|
|
63884
63944
|
TOOLS: () => TOOLS2
|
|
63885
63945
|
});
|
|
63886
63946
|
import { randomBytes as randomBytes16 } from "node:crypto";
|
|
@@ -63895,7 +63955,7 @@ function wireTimeoutForOp(op) {
|
|
|
63895
63955
|
return WIRE_TIMEOUT_MS_BY_OP[op] ?? DEFAULT_WIRE_TIMEOUT_MS;
|
|
63896
63956
|
}
|
|
63897
63957
|
async function dispatchTool2(name, args) {
|
|
63898
|
-
if (name === "get_status") {
|
|
63958
|
+
if (name === "get_status" && args.request_id === undefined) {
|
|
63899
63959
|
return getLastUpdateApplyStatus();
|
|
63900
63960
|
}
|
|
63901
63961
|
if (!SELF_AGENT) {
|
|
@@ -63906,6 +63966,7 @@ async function dispatchTool2(name, args) {
|
|
|
63906
63966
|
return errorText2(`hostd MCP: socket not bound at ${sockPath}. The host-control ` + `daemon is either not installed (run \`switchroom hostd install\`) ` + `or this agent isn't admin-flagged in switchroom.yaml. RFC C ` + `bind-mounts the per-agent socket only when host_control.enabled ` + `is true AND the agent has admin: true.`);
|
|
63907
63967
|
}
|
|
63908
63968
|
let req;
|
|
63969
|
+
let advisory;
|
|
63909
63970
|
switch (name) {
|
|
63910
63971
|
case "agent_restart": {
|
|
63911
63972
|
if (!args.name)
|
|
@@ -63998,6 +64059,9 @@ async function dispatchTool2(name, args) {
|
|
|
63998
64059
|
if (args.pin && !/^(sha-[0-9a-f]{7,40}|v\d+\.\d+\.\d+)$/.test(args.pin)) {
|
|
63999
64060
|
return errorText2(`update_apply: pin "${args.pin}" is invalid. Expected sha-<7-40 hex> or v<semver>.`);
|
|
64000
64061
|
}
|
|
64062
|
+
if (args.pin && ROLLOUT_SEMVER_PIN_RE.test(args.pin)) {
|
|
64063
|
+
advisory = UPDATE_APPLY_SEMVER_PIN_ADVISORY;
|
|
64064
|
+
}
|
|
64001
64065
|
req = {
|
|
64002
64066
|
v: 1,
|
|
64003
64067
|
op: "update_apply",
|
|
@@ -64036,6 +64100,18 @@ async function dispatchTool2(name, args) {
|
|
|
64036
64100
|
};
|
|
64037
64101
|
break;
|
|
64038
64102
|
}
|
|
64103
|
+
case "get_status": {
|
|
64104
|
+
if (typeof args.request_id !== "string" || !args.request_id) {
|
|
64105
|
+
return errorText2("get_status: request_id must be a non-empty string");
|
|
64106
|
+
}
|
|
64107
|
+
req = {
|
|
64108
|
+
v: 1,
|
|
64109
|
+
op: "get_status",
|
|
64110
|
+
request_id: makeRequestId("mcp-get-status"),
|
|
64111
|
+
args: { target_request_id: args.request_id }
|
|
64112
|
+
};
|
|
64113
|
+
break;
|
|
64114
|
+
}
|
|
64039
64115
|
case "config_propose_edit": {
|
|
64040
64116
|
if (!args.unified_diff || typeof args.unified_diff !== "string") {
|
|
64041
64117
|
return errorText2("config_propose_edit: unified_diff is required (non-empty string).");
|
|
@@ -64071,7 +64147,11 @@ async function dispatchTool2(name, args) {
|
|
|
64071
64147
|
return errorText2(`hostd wire error (request_id=${req.request_id}): ` + `${err2.message}`);
|
|
64072
64148
|
}
|
|
64073
64149
|
if (resp.result === "started" || resp.result === "completed") {
|
|
64074
|
-
|
|
64150
|
+
const out = jsonText2(resp);
|
|
64151
|
+
if (advisory) {
|
|
64152
|
+
out.content.push({ type: "text", text: advisory });
|
|
64153
|
+
}
|
|
64154
|
+
return out;
|
|
64075
64155
|
}
|
|
64076
64156
|
const content = [
|
|
64077
64157
|
{ type: "text", text: JSON.stringify(resp) }
|
|
@@ -64143,7 +64223,7 @@ async function runHostdMcpServer() {
|
|
|
64143
64223
|
const transport = new StdioServerTransport;
|
|
64144
64224
|
await server.connect(transport);
|
|
64145
64225
|
}
|
|
64146
|
-
var SELF_AGENT, DEFAULT_WIRE_TIMEOUT_MS = 1e4, WIRE_TIMEOUT_MS_BY_OP, ROLLOUT_SEMVER_PIN_RE, TOOLS2;
|
|
64226
|
+
var SELF_AGENT, DEFAULT_WIRE_TIMEOUT_MS = 1e4, WIRE_TIMEOUT_MS_BY_OP, ROLLOUT_SEMVER_PIN_RE, UPDATE_APPLY_SEMVER_PIN_ADVISORY, TOOLS2;
|
|
64147
64227
|
var init_server4 = __esm(() => {
|
|
64148
64228
|
init_server2();
|
|
64149
64229
|
init_stdio2();
|
|
@@ -64155,6 +64235,7 @@ var init_server4 = __esm(() => {
|
|
|
64155
64235
|
config_propose_edit: 61 * 60 * 1000
|
|
64156
64236
|
};
|
|
64157
64237
|
ROLLOUT_SEMVER_PIN_RE = /^v\d+\.\d+\.\d+$/;
|
|
64238
|
+
UPDATE_APPLY_SEMVER_PIN_ADVISORY = "\u26a0\ufe0f advisory: update_apply with a semver pin is the BLUNT path \u2014 all " + "containers recreate at once, with no canary, no per-agent version " + "assert, and no stop-on-mismatch. For fleet version rolls prefer the " + "`rollout` tool (staggered, canary-gated, version-asserted; also " + "refreshes the web + hindsight singletons). This request was still " + "dispatched \u2014 this is a steer, not a refusal.";
|
|
64158
64239
|
TOOLS2 = [
|
|
64159
64240
|
{
|
|
64160
64241
|
name: "agent_restart",
|
|
@@ -64278,7 +64359,7 @@ var init_server4 = __esm(() => {
|
|
|
64278
64359
|
},
|
|
64279
64360
|
{
|
|
64280
64361
|
name: "update_apply",
|
|
64281
|
-
description: "Execute a fleet-wide update: pull images, regenerate " + "scaffolds, recreate containers. Admin-only at the wire layer. " + "Returns `started` once dispatched \u2014 the actual work runs " + "async on the host and the caller's own agent container will " + "be recreated as part of the cycle. This is operator-gated \u2014 every " + "call surfaces a Telegram approval card. ALWAYS pass a one-line " + "`reason` explaining why you're applying this update; it renders on " + "the card's `why:` line so the operator can decide in context.",
|
|
64362
|
+
description: "Execute a fleet-wide update: pull images, regenerate " + "scaffolds, recreate containers. This is the BLUNT all-at-once " + "path \u2014 no canary, no per-agent version assert, no stop-on-" + "mismatch. For rolling the fleet to a specific VERSION (a semver " + "pin), prefer the `rollout` tool instead: staggered, canary-gated, " + "version-asserted, and it also refreshes the web + hindsight " + "singletons. Calling update_apply with a semver pin still works " + "(the operator may want the blunt path deliberately) but the " + "result includes an advisory warning. Admin-only at the wire layer. " + "Returns `started` once dispatched \u2014 the actual work runs " + "async on the host and the caller's own agent container will " + "be recreated as part of the cycle. This is operator-gated \u2014 every " + "call surfaces a Telegram approval card. ALWAYS pass a one-line " + "`reason` explaining why you're applying this update; it renders on " + "the card's `why:` line so the operator can decide in context.",
|
|
64282
64363
|
inputSchema: {
|
|
64283
64364
|
type: "object",
|
|
64284
64365
|
properties: {
|
|
@@ -64310,7 +64391,7 @@ var init_server4 = __esm(() => {
|
|
|
64310
64391
|
},
|
|
64311
64392
|
{
|
|
64312
64393
|
name: "rollout",
|
|
64313
|
-
description: "SAFELY roll the fleet to a pinned SEMVER version, staggered and " + "canary-gated (#2487). Unlike update_apply (a blunt all-at-once " + "recreate), this restarts agents one at a time canary-first, asserts " + "each agent's in-container `switchroom --version` matches the target, " + "and STOPS on the first mismatch \u2014 so a bad build fails on the canary " + "before touching the rest of the fleet. The durable release.pin is " + "persisted only AFTER the canary confirms (a failed canary never " + "strands a bad pin). `pin` MUST be a tagged semver (vX.Y.Z) \u2014 sha " + "pins are rejected because the version assert needs a semver to " + "compare against. When the target pin is NEWER than hostd's own CLI " + "(every freshly tagged release), hostd SELF-BUMPS first (#2645): its " + "container restarts on the target image (~30-60s blip on this MCP " + "socket), then the roll resumes AUTOMATICALLY under the SAME " + "request_id \u2014 do NOT re-issue the rollout during the blip; poll " + "get_status
|
|
64394
|
+
description: "SAFELY roll the fleet to a pinned SEMVER version, staggered and " + "canary-gated (#2487). Unlike update_apply (a blunt all-at-once " + "recreate), this restarts agents one at a time canary-first, asserts " + "each agent's in-container `switchroom --version` matches the target, " + "and STOPS on the first mismatch \u2014 so a bad build fails on the canary " + "before touching the rest of the fleet. The durable release.pin is " + "persisted only AFTER the canary confirms (a failed canary never " + "strands a bad pin). `pin` MUST be a tagged semver (vX.Y.Z) \u2014 sha " + "pins are rejected because the version assert needs a semver to " + "compare against. When the target pin is NEWER than hostd's own CLI " + "(every freshly tagged release), hostd SELF-BUMPS first (#2645): its " + "container restarts on the target image (~30-60s blip on this MCP " + "socket), then the roll resumes AUTOMATICALLY under the SAME " + "request_id \u2014 do NOT re-issue the rollout during the blip; poll " + "get_status (pass the returned request_id as its `request_id` " + "argument) once the socket is back. " + "The web + hindsight singletons are refreshed in-plan on this path " + "(only the hostd template regen + host operator CLI stay host-side; " + "the terminal warnings name exactly what is still on the prior " + "version and the commands to finish). By default downgrade pins " + "are rejected \u2014 pass `allow_downgrade: true` for the operator-approved " + "rollback path to a known-good earlier tag; all other safety rails " + "(canary order, version-assert, stop-on-mismatch) apply unchanged. " + "Admin-only at the wire layer AND deliberately NOT pre-approved \u2014 every " + "call surfaces a Telegram approval card for the operator to tap. " + "ALWAYS pass a one-line `reason` explaining why you're rolling the " + "fleet to this pin; it renders on the card's `why:` line so the " + "operator can decide in context. " + "Returns `started`; poll get_status with `request_id` for the " + "structured outcome (which agents rolled / where it stopped).",
|
|
64314
64395
|
inputSchema: {
|
|
64315
64396
|
type: "object",
|
|
64316
64397
|
required: ["pin"],
|
|
@@ -64333,11 +64414,11 @@ var init_server4 = __esm(() => {
|
|
|
64333
64414
|
},
|
|
64334
64415
|
skip_web: {
|
|
64335
64416
|
type: "boolean",
|
|
64336
|
-
description: "Skip the web
|
|
64417
|
+
description: "Skip the web refresh step (leaves switchroom-web on the prior " + "version \u2014 the terminal warning names the host-side command to " + "finish). The hostd self-refresh is deferred on this path " + "regardless of this flag."
|
|
64337
64418
|
},
|
|
64338
64419
|
allow_downgrade: {
|
|
64339
64420
|
type: "boolean",
|
|
64340
|
-
description: "Operator-approved rollback to a known-good earlier tag (#2487 PR2). " + "When true, the downgrade guard is relaxed so `pin` may be older " + "than the current release.pin. All other safety rails (canary order, " + "version-assert, stop-on-mismatch, persist-after-canary, " + "hostd
|
|
64421
|
+
description: "Operator-approved rollback to a known-good earlier tag (#2487 PR2). " + "When true, the downgrade guard is relaxed so `pin` may be older " + "than the current release.pin. All other safety rails (canary order, " + "version-assert, stop-on-mismatch, persist-after-canary, " + "hostd deferral) apply unchanged. Still gated by the operator " + "approval card \u2014 not pre-approved."
|
|
64341
64422
|
}
|
|
64342
64423
|
}
|
|
64343
64424
|
}
|
|
@@ -64370,10 +64451,17 @@ var init_server4 = __esm(() => {
|
|
|
64370
64451
|
},
|
|
64371
64452
|
{
|
|
64372
64453
|
name: "get_status",
|
|
64373
|
-
description: "
|
|
64454
|
+
description: "Look up the status of a prior async fleet mutation. With " + "`request_id` (the id echoed by rollout / update_apply / " + "agent_restart etc.), asks the host-control daemon for that " + "specific request's live status \u2014 for an in-flight `rollout` " + "this is the ONLY way to see progress: the response payload " + "carries the current phase, n/m counters, rolled[] agents, " + "failedStep/failedAgent and pin, and it keeps working across a " + "hostd self-bump/restart via the durable audit-log fallback. " + "This is the tool the `rollout` description tells you to poll " + "with the returned request_id. Without `request_id`, falls back " + "to reading the most recent terminal `update_apply` audit row " + "(channel, pin, resolved_sha, install_context, result, " + "exit_code, stderr_tail) \u2014 use that no-arg form only to report " + "the last blunt update on demand; it CANNOT see rollouts or " + "in-flight work. Read-only; returns JSON.",
|
|
64374
64455
|
inputSchema: {
|
|
64375
64456
|
type: "object",
|
|
64376
|
-
properties: {
|
|
64457
|
+
properties: {
|
|
64458
|
+
request_id: {
|
|
64459
|
+
type: "string",
|
|
64460
|
+
minLength: 1,
|
|
64461
|
+
maxLength: 128,
|
|
64462
|
+
description: "The request_id returned by a prior mutating verb " + "(e.g. `mcp-rollout-\u2026`). When set, the daemon returns " + "that request's rich per-request status (phase, n/m, " + "rolled agents, outcome). Omit to read the last " + "terminal update_apply audit row instead."
|
|
64463
|
+
}
|
|
64464
|
+
},
|
|
64377
64465
|
additionalProperties: false
|
|
64378
64466
|
}
|
|
64379
64467
|
}
|
|
@@ -64889,8 +64977,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
64889
64977
|
import { dirname, join } from "node:path";
|
|
64890
64978
|
|
|
64891
64979
|
// src/build-info.ts
|
|
64892
|
-
var VERSION = "0.18.
|
|
64893
|
-
var COMMIT_SHA = "
|
|
64980
|
+
var VERSION = "0.18.11";
|
|
64981
|
+
var COMMIT_SHA = "8fbd9574";
|
|
64894
64982
|
|
|
64895
64983
|
// src/cli/resolve-version.ts
|
|
64896
64984
|
function readPackageVersion() {
|
|
@@ -85030,13 +85118,52 @@ function registerUpdateCommand(program3) {
|
|
|
85030
85118
|
|
|
85031
85119
|
// src/cli/rollout.ts
|
|
85032
85120
|
init_helpers();
|
|
85033
|
-
import { spawnSync as
|
|
85121
|
+
import { spawnSync as spawnSync14 } from "node:child_process";
|
|
85034
85122
|
import { readFileSync as readFileSync58, chownSync as chownSync6, statSync as statSync30 } from "node:fs";
|
|
85035
85123
|
import { homedir as homedir40 } from "node:os";
|
|
85036
85124
|
init_operator_uid();
|
|
85037
85125
|
init_atomic();
|
|
85038
85126
|
init_audit_reader();
|
|
85039
85127
|
init_hindsight();
|
|
85128
|
+
|
|
85129
|
+
// src/cli/deploy-version-guard.ts
|
|
85130
|
+
import { spawnSync as spawnSync13 } from "node:child_process";
|
|
85131
|
+
var defaultRunner3 = (args) => {
|
|
85132
|
+
const r = spawnSync13("docker", args, { encoding: "utf8" });
|
|
85133
|
+
return { ok: r.status === 0, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
85134
|
+
};
|
|
85135
|
+
function deployedImageTag(container, run = defaultRunner3) {
|
|
85136
|
+
const r = run(["inspect", "-f", "{{.Config.Image}}", container]);
|
|
85137
|
+
if (!r.ok)
|
|
85138
|
+
return null;
|
|
85139
|
+
const ref = r.stdout.trim();
|
|
85140
|
+
if (!ref)
|
|
85141
|
+
return null;
|
|
85142
|
+
const noDigest = ref.split("@")[0];
|
|
85143
|
+
const colon = noDigest.lastIndexOf(":");
|
|
85144
|
+
if (colon < 0)
|
|
85145
|
+
return null;
|
|
85146
|
+
const tag = noDigest.slice(colon + 1);
|
|
85147
|
+
return tag && !tag.includes("/") ? tag : null;
|
|
85148
|
+
}
|
|
85149
|
+
function checkDowngrade(opts) {
|
|
85150
|
+
const running = deployedImageTag(opts.container, opts.run);
|
|
85151
|
+
if (opts.allowDowngrade)
|
|
85152
|
+
return { skip: false, running };
|
|
85153
|
+
const cmp = compareReleaseTags(opts.targetTag, running);
|
|
85154
|
+
if (cmp !== null && cmp < 0) {
|
|
85155
|
+
return {
|
|
85156
|
+
skip: true,
|
|
85157
|
+
running,
|
|
85158
|
+
message: `Skipping ${opts.container} deploy \u2014 it is already on ${running}, newer than the ` + `requested ${opts.targetTag}.
|
|
85159
|
+
` + ` Refusing to downgrade: this guards against a concurrent rollout/update ` + `reverting a newer build.
|
|
85160
|
+
` + ` To force the downgrade, re-run with --allow-downgrade.`
|
|
85161
|
+
};
|
|
85162
|
+
}
|
|
85163
|
+
return { skip: false, running };
|
|
85164
|
+
}
|
|
85165
|
+
|
|
85166
|
+
// src/cli/rollout.ts
|
|
85040
85167
|
function normalizeVersion(v) {
|
|
85041
85168
|
return v.trim().replace(/^v/, "");
|
|
85042
85169
|
}
|
|
@@ -85065,6 +85192,9 @@ function planRollout(agents, opts = {}) {
|
|
|
85065
85192
|
} else if (opts.pinToPersist) {
|
|
85066
85193
|
steps.push({ kind: "persist-pin", pin: opts.pinToPersist });
|
|
85067
85194
|
}
|
|
85195
|
+
if (!opts.skipWeb) {
|
|
85196
|
+
steps.push({ kind: "refresh-web" });
|
|
85197
|
+
}
|
|
85068
85198
|
steps.push({ kind: "refresh-hindsight" });
|
|
85069
85199
|
steps.push({ kind: "sweep" });
|
|
85070
85200
|
return steps;
|
|
@@ -85216,15 +85346,24 @@ function executeRollout(steps, target, deps, execOpts = {}) {
|
|
|
85216
85346
|
break;
|
|
85217
85347
|
}
|
|
85218
85348
|
case "refresh-web": {
|
|
85219
|
-
|
|
85220
|
-
|
|
85221
|
-
|
|
85222
|
-
|
|
85349
|
+
const downgradeArgs = execOpts.allowDowngrade ? ["--allow-downgrade"] : [];
|
|
85350
|
+
deps.log(`ROLL_STEP refresh-web \u2014 webd install --tag ${target}` + (execOpts.allowDowngrade ? " --allow-downgrade" : ""));
|
|
85351
|
+
emit({ phase: "web-refresh", target });
|
|
85352
|
+
const r = deps.run(["webd", "install", "--tag", target, ...downgradeArgs]);
|
|
85353
|
+
if (r.status !== 0) {
|
|
85354
|
+
warnings.push(`web refresh FAILED (non-fatal \u2014 agents already rolled) so ` + `switchroom-web is still on the PRIOR version. Finish it ` + `host-side: \`switchroom webd install --tag ${target}\`.`);
|
|
85355
|
+
break;
|
|
85356
|
+
}
|
|
85357
|
+
const webTag = deps.webImageTag?.();
|
|
85358
|
+
if (webTag !== undefined && webTag !== null && isVersionAssertable(webTag) && normalizeVersion(webTag) !== targetNorm) {
|
|
85359
|
+
warnings.push(`web refresh completed but switchroom-web is running ${webTag}, ` + `NOT the roll target ${target} (most likely its downgrade guard ` + `skipped the install). Finish it host-side: \`switchroom webd ` + `install --tag ${target} --allow-downgrade\`.`);
|
|
85360
|
+
}
|
|
85223
85361
|
break;
|
|
85224
85362
|
}
|
|
85225
85363
|
case "refresh-hostd": {
|
|
85226
|
-
|
|
85227
|
-
|
|
85364
|
+
const downgradeArgs = execOpts.allowDowngrade ? ["--allow-downgrade"] : [];
|
|
85365
|
+
deps.log(`ROLL_STEP refresh-hostd \u2014 hostd install --tag ${target}` + (execOpts.allowDowngrade ? " --allow-downgrade" : ""));
|
|
85366
|
+
const r = deps.run(["hostd", "install", "--tag", target, ...downgradeArgs]);
|
|
85228
85367
|
if (r.status !== 0)
|
|
85229
85368
|
warnings.push(`hostd refresh failed (non-fatal); agents already rolled`);
|
|
85230
85369
|
break;
|
|
@@ -85279,7 +85418,7 @@ function resolveRollbackTarget(auditLogPath) {
|
|
|
85279
85418
|
}
|
|
85280
85419
|
function registerRolloutCommand(program3) {
|
|
85281
85420
|
const hostdCtx = isHostdContext2();
|
|
85282
|
-
program3.command("rollout").description("Deploy a pinned version to the fleet, safely (staggered restart + " + "per-agent version assert + web/hostd refresh)." + (hostdCtx ? "" : " Run with sudo.")).option("--pin <version>", "Version to roll (e.g. v0.15.18). Defaults to release.pin from config.").option("--agents <list>", "Comma-separated subset of agents to roll (default: all configured).").option("--skip-web", "Skip the web
|
|
85421
|
+
program3.command("rollout").description("Deploy a pinned version to the fleet, safely (staggered restart + " + "per-agent version assert + web/hostd refresh)." + (hostdCtx ? "" : " Run with sudo.")).option("--pin <version>", "Version to roll (e.g. v0.15.18). Defaults to release.pin from config.").option("--agents <list>", "Comma-separated subset of agents to roll (default: all configured).").option("--skip-web", "Skip the web refresh (host-shell path: also the hostd + hindsight " + "refresh). On the hostd/agent path only the web refresh is skipped \u2014 " + "hindsight is still recreated and hostd stays deferred regardless.").option("--allow-downgrade", "Permit rolling to an older semver tag (operator-approved rollback path). " + "When set, the downgrade guard is relaxed so --pin may be older than the " + "current release.pin. All other safety rails apply unchanged.").option("--dry-run", "Print the plan and exit without changing anything.").action(async (opts) => {
|
|
85283
85422
|
const config = getConfig(program3);
|
|
85284
85423
|
let resolvedPin = opts.pin;
|
|
85285
85424
|
if (opts.allowDowngrade && !resolvedPin) {
|
|
@@ -85363,11 +85502,11 @@ function registerRolloutCommand(program3) {
|
|
|
85363
85502
|
const scriptPath = process.argv[1] ?? "switchroom";
|
|
85364
85503
|
const deps = {
|
|
85365
85504
|
run: (args) => {
|
|
85366
|
-
const r =
|
|
85505
|
+
const r = spawnSync14(process.execPath, [scriptPath, ...args], { stdio: "inherit" });
|
|
85367
85506
|
return { status: r.status ?? 1 };
|
|
85368
85507
|
},
|
|
85369
85508
|
probeVersion: (agent) => {
|
|
85370
|
-
const r =
|
|
85509
|
+
const r = spawnSync14("docker", ["exec", `switchroom-${agent}`, "sh", "-lc", "switchroom --version"], { encoding: "utf8" });
|
|
85371
85510
|
if (r.status !== 0)
|
|
85372
85511
|
return null;
|
|
85373
85512
|
return (r.stdout ?? "").trim().split(`
|
|
@@ -85377,6 +85516,7 @@ function registerRolloutCommand(program3) {
|
|
|
85377
85516
|
`),
|
|
85378
85517
|
emitPhase: (phase) => process.stdout.write(encodeRolloutPhaseLine(phase) + `
|
|
85379
85518
|
`),
|
|
85519
|
+
webImageTag: () => deployedImageTag("switchroom-web"),
|
|
85380
85520
|
persistPin: (pin) => {
|
|
85381
85521
|
const before = readFileSync58(configPath, "utf8");
|
|
85382
85522
|
const after = setReleasePinInConfig(before, pin);
|
|
@@ -85398,9 +85538,12 @@ function registerRolloutCommand(program3) {
|
|
|
85398
85538
|
};
|
|
85399
85539
|
process.stdout.write(`${opts.allowDowngrade ? "Rolling back" : "Rolling"} ${requested.length} agent(s) to ${target}\u2026
|
|
85400
85540
|
`);
|
|
85401
|
-
const result = executeRollout(steps, target, deps, {
|
|
85541
|
+
const result = executeRollout(steps, target, deps, {
|
|
85542
|
+
hostdContext: hostdCtx,
|
|
85543
|
+
allowDowngrade: opts.allowDowngrade
|
|
85544
|
+
});
|
|
85402
85545
|
if (hostdCtx) {
|
|
85403
|
-
result.warnings.push(`still on the PRIOR version after this roll:
|
|
85546
|
+
result.warnings.push(`still on the PRIOR version after this roll: the host operator ` + `CLI (\`sudo npm i -g switchroom@${normalizeVersion(target)}\`). ` + `hostd's compose was tag-bumped by the self-bump when one ran; a ` + `full hostd template regen (only needed when a release changes ` + `hostd's mounts/env) is \`switchroom hostd install --tag ${target}\` ` + `host-side. An agent-invoked rollout cannot recreate its own hostd ` + `container mid-roll without killing itself.` + (opts.skipWeb ? ` --skip-web was set, so switchroom-web is ALSO still on the ` + `prior version (host-side: \`switchroom webd install --tag ${target}\`).` : ""));
|
|
85404
85547
|
process.stdout.write(encodeRolloutPhaseLine({ phase: "hostd-web-deferred", target }) + `
|
|
85405
85548
|
`);
|
|
85406
85549
|
}
|
|
@@ -85426,7 +85569,7 @@ function registerRolloutCommand(program3) {
|
|
|
85426
85569
|
});
|
|
85427
85570
|
}
|
|
85428
85571
|
function registerRollbackCommand(program3) {
|
|
85429
|
-
program3.command("rollback").description("Roll the fleet back to the previous version (operator-approved downgrade). " + "Defaults to the version captured in the last completed rollout's prior_pin. " + "Equivalent to `rollout --allow-downgrade [--pin <version>]`.").option("--to <version>", "Explicit version to roll back to (e.g. v0.15.17). " + "Omit to use the last completed rollout's prior_pin.").option("--agents <list>", "Comma-separated subset of agents to roll back (default: all configured).").option("--skip-web", "Skip the web
|
|
85572
|
+
program3.command("rollback").description("Roll the fleet back to the previous version (operator-approved downgrade). " + "Defaults to the version captured in the last completed rollout's prior_pin. " + "Equivalent to `rollout --allow-downgrade [--pin <version>]`.").option("--to <version>", "Explicit version to roll back to (e.g. v0.15.17). " + "Omit to use the last completed rollout's prior_pin.").option("--agents <list>", "Comma-separated subset of agents to roll back (default: all configured).").option("--skip-web", "Skip the web refresh (host-shell path: also the hostd + hindsight " + "refresh). On the hostd/agent path only the web refresh is skipped \u2014 " + "hindsight is still recreated and hostd stays deferred regardless.").option("--dry-run", "Print the plan and exit without changing anything.").action(async (opts) => {
|
|
85430
85573
|
const rolloutArgs = ["--allow-downgrade"];
|
|
85431
85574
|
if (opts.to)
|
|
85432
85575
|
rolloutArgs.push("--pin", opts.to);
|
|
@@ -86989,7 +87132,7 @@ init_helpers();
|
|
|
86989
87132
|
init_loader();
|
|
86990
87133
|
import { existsSync as existsSync71 } from "node:fs";
|
|
86991
87134
|
import { resolve as resolve43, sep as sep3 } from "node:path";
|
|
86992
|
-
import { spawnSync as
|
|
87135
|
+
import { spawnSync as spawnSync15 } from "node:child_process";
|
|
86993
87136
|
|
|
86994
87137
|
// src/agents/workspace.ts
|
|
86995
87138
|
import { readFile as readFile2, stat } from "node:fs/promises";
|
|
@@ -87702,7 +87845,7 @@ function registerWorkspaceCommand(program3) {
|
|
|
87702
87845
|
process.exit(1);
|
|
87703
87846
|
}
|
|
87704
87847
|
const editor = process.env["EDITOR"] ?? process.env["VISUAL"] ?? "vi";
|
|
87705
|
-
const child =
|
|
87848
|
+
const child = spawnSync15(editor, [target], { stdio: "inherit" });
|
|
87706
87849
|
if (child.status !== 0 && child.status !== null) {
|
|
87707
87850
|
process.exit(child.status);
|
|
87708
87851
|
}
|
|
@@ -87769,7 +87912,7 @@ function registerWorkspaceCommand(program3) {
|
|
|
87769
87912
|
`);
|
|
87770
87913
|
return;
|
|
87771
87914
|
}
|
|
87772
|
-
const statusResult =
|
|
87915
|
+
const statusResult = spawnSync15("git", ["status", "--short"], {
|
|
87773
87916
|
cwd: dir,
|
|
87774
87917
|
encoding: "utf-8"
|
|
87775
87918
|
});
|
|
@@ -87784,7 +87927,7 @@ function registerWorkspaceCommand(program3) {
|
|
|
87784
87927
|
return;
|
|
87785
87928
|
}
|
|
87786
87929
|
const message = opts.message || `checkpoint: ${new Date().toISOString()}`;
|
|
87787
|
-
const addResult =
|
|
87930
|
+
const addResult = spawnSync15("git", ["add", "-A"], {
|
|
87788
87931
|
cwd: dir,
|
|
87789
87932
|
encoding: "utf-8"
|
|
87790
87933
|
});
|
|
@@ -87793,7 +87936,7 @@ function registerWorkspaceCommand(program3) {
|
|
|
87793
87936
|
`);
|
|
87794
87937
|
process.exit(1);
|
|
87795
87938
|
}
|
|
87796
|
-
const commitResult =
|
|
87939
|
+
const commitResult = spawnSync15("git", ["commit", "-m", message], {
|
|
87797
87940
|
cwd: dir,
|
|
87798
87941
|
encoding: "utf-8"
|
|
87799
87942
|
});
|
|
@@ -87802,7 +87945,7 @@ function registerWorkspaceCommand(program3) {
|
|
|
87802
87945
|
`);
|
|
87803
87946
|
process.exit(1);
|
|
87804
87947
|
}
|
|
87805
|
-
const shaResult =
|
|
87948
|
+
const shaResult = spawnSync15("git", ["rev-parse", "--short", "HEAD"], {
|
|
87806
87949
|
cwd: dir,
|
|
87807
87950
|
encoding: "utf-8"
|
|
87808
87951
|
});
|
|
@@ -87823,7 +87966,7 @@ function registerWorkspaceCommand(program3) {
|
|
|
87823
87966
|
`);
|
|
87824
87967
|
return;
|
|
87825
87968
|
}
|
|
87826
|
-
const child =
|
|
87969
|
+
const child = spawnSync15("git", ["status", "--short"], {
|
|
87827
87970
|
cwd: dir,
|
|
87828
87971
|
stdio: "inherit"
|
|
87829
87972
|
});
|
|
@@ -92200,7 +92343,7 @@ import {
|
|
|
92200
92343
|
} from "node:fs";
|
|
92201
92344
|
import { tmpdir as tmpdir5, homedir as homedir50 } from "node:os";
|
|
92202
92345
|
import { dirname as dirname30, join as join86, relative as relative2, resolve as resolve52 } from "node:path";
|
|
92203
|
-
import { spawnSync as
|
|
92346
|
+
import { spawnSync as spawnSync16 } from "node:child_process";
|
|
92204
92347
|
|
|
92205
92348
|
// src/cli/skill-common.ts
|
|
92206
92349
|
var import_yaml24 = __toESM(require_dist(), 1);
|
|
@@ -92493,7 +92636,7 @@ function loadFromDir(dir) {
|
|
|
92493
92636
|
function loadFromTarball(tarPath) {
|
|
92494
92637
|
const isGz = tarPath.endsWith(".gz") || tarPath.endsWith(".tgz");
|
|
92495
92638
|
const listFlags = isGz ? ["-tzf"] : ["-tf"];
|
|
92496
|
-
const list2 =
|
|
92639
|
+
const list2 = spawnSync16("tar", [...listFlags, tarPath], {
|
|
92497
92640
|
encoding: "utf-8",
|
|
92498
92641
|
stdio: ["ignore", "pipe", "pipe"]
|
|
92499
92642
|
});
|
|
@@ -92510,7 +92653,7 @@ function loadFromTarball(tarPath) {
|
|
|
92510
92653
|
const staging = mkdtempSync5(join86(tmpdir5(), "skill-apply-extract-"));
|
|
92511
92654
|
try {
|
|
92512
92655
|
const flags = isGz ? ["-xzf"] : ["-xf"];
|
|
92513
|
-
const r =
|
|
92656
|
+
const r = spawnSync16("tar", [
|
|
92514
92657
|
...flags,
|
|
92515
92658
|
tarPath,
|
|
92516
92659
|
"-C",
|
|
@@ -92585,7 +92728,7 @@ function validatePayload(name, files) {
|
|
|
92585
92728
|
if (errors2.length === 0) {
|
|
92586
92729
|
for (const [path8, content] of Object.entries(files)) {
|
|
92587
92730
|
if (SH_SCRIPT_RE2.test(path8)) {
|
|
92588
|
-
const r =
|
|
92731
|
+
const r = spawnSync16("bash", ["-n"], {
|
|
92589
92732
|
input: content,
|
|
92590
92733
|
encoding: "utf-8"
|
|
92591
92734
|
});
|
|
@@ -92597,7 +92740,7 @@ function validatePayload(name, files) {
|
|
|
92597
92740
|
const tmpPy = join86(tmp, "check.py");
|
|
92598
92741
|
try {
|
|
92599
92742
|
writeFileSync41(tmpPy, content);
|
|
92600
|
-
const r =
|
|
92743
|
+
const r = spawnSync16("python3", ["-m", "py_compile", tmpPy], {
|
|
92601
92744
|
encoding: "utf-8"
|
|
92602
92745
|
});
|
|
92603
92746
|
if (r.status !== 0) {
|
|
@@ -92760,7 +92903,7 @@ function registerSkillCommand(program3) {
|
|
|
92760
92903
|
\u2713 Wrote ${name} to ${currentDir}`));
|
|
92761
92904
|
const applyBin = process.argv[1] ?? "switchroom";
|
|
92762
92905
|
console.log(source_default.gray(`Running \`switchroom apply --non-interactive\`...`));
|
|
92763
|
-
const r =
|
|
92906
|
+
const r = spawnSync16(process.argv0, [applyBin, "apply", "--non-interactive"], { stdio: "inherit" });
|
|
92764
92907
|
if (r.status !== 0) {
|
|
92765
92908
|
console.error(source_default.yellow(`(warning: \`switchroom apply\` exited ${r.status} \u2014 skill is ` + `in the pool but symlinks may not be refreshed. Re-run manually.)`));
|
|
92766
92909
|
}
|
|
@@ -92793,7 +92936,7 @@ import {
|
|
|
92793
92936
|
} from "node:fs";
|
|
92794
92937
|
import { dirname as dirname31, join as join87, relative as relative3, resolve as resolve53 } from "node:path";
|
|
92795
92938
|
import { homedir as homedir51, tmpdir as tmpdir6 } from "node:os";
|
|
92796
|
-
import { spawnSync as
|
|
92939
|
+
import { spawnSync as spawnSync17 } from "node:child_process";
|
|
92797
92940
|
init_helpers();
|
|
92798
92941
|
init_agent_config();
|
|
92799
92942
|
init_source();
|
|
@@ -92994,7 +93137,7 @@ function behavioralValidate(files) {
|
|
|
92994
93137
|
const errors2 = [];
|
|
92995
93138
|
for (const [path8, content] of Object.entries(files)) {
|
|
92996
93139
|
if (SH_SCRIPT_RE.test(path8)) {
|
|
92997
|
-
const r =
|
|
93140
|
+
const r = spawnSync17("bash", ["-n"], { input: content, encoding: "utf-8" });
|
|
92998
93141
|
if (r.status !== 0) {
|
|
92999
93142
|
errors2.push(`${path8} fails \`bash -n\`: ${(r.stderr ?? "").trim()}`);
|
|
93000
93143
|
}
|
|
@@ -93003,7 +93146,7 @@ function behavioralValidate(files) {
|
|
|
93003
93146
|
const tmpPy = join87(tmp, "check.py");
|
|
93004
93147
|
try {
|
|
93005
93148
|
writeFileSync42(tmpPy, content);
|
|
93006
|
-
const r =
|
|
93149
|
+
const r = spawnSync17("python3", ["-m", "py_compile", tmpPy], {
|
|
93007
93150
|
encoding: "utf-8"
|
|
93008
93151
|
});
|
|
93009
93152
|
if (r.status !== 0) {
|
|
@@ -93799,43 +93942,6 @@ import { homedir as homedir54 } from "node:os";
|
|
|
93799
93942
|
import { join as join90 } from "node:path";
|
|
93800
93943
|
import { spawnSync as spawnSync20 } from "node:child_process";
|
|
93801
93944
|
|
|
93802
|
-
// src/cli/deploy-version-guard.ts
|
|
93803
|
-
import { spawnSync as spawnSync18 } from "node:child_process";
|
|
93804
|
-
var defaultRunner3 = (args) => {
|
|
93805
|
-
const r = spawnSync18("docker", args, { encoding: "utf8" });
|
|
93806
|
-
return { ok: r.status === 0, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
93807
|
-
};
|
|
93808
|
-
function deployedImageTag(container, run = defaultRunner3) {
|
|
93809
|
-
const r = run(["inspect", "-f", "{{.Config.Image}}", container]);
|
|
93810
|
-
if (!r.ok)
|
|
93811
|
-
return null;
|
|
93812
|
-
const ref = r.stdout.trim();
|
|
93813
|
-
if (!ref)
|
|
93814
|
-
return null;
|
|
93815
|
-
const noDigest = ref.split("@")[0];
|
|
93816
|
-
const colon = noDigest.lastIndexOf(":");
|
|
93817
|
-
if (colon < 0)
|
|
93818
|
-
return null;
|
|
93819
|
-
const tag = noDigest.slice(colon + 1);
|
|
93820
|
-
return tag && !tag.includes("/") ? tag : null;
|
|
93821
|
-
}
|
|
93822
|
-
function checkDowngrade(opts) {
|
|
93823
|
-
const running = deployedImageTag(opts.container, opts.run);
|
|
93824
|
-
if (opts.allowDowngrade)
|
|
93825
|
-
return { skip: false, running };
|
|
93826
|
-
const cmp = compareReleaseTags(opts.targetTag, running);
|
|
93827
|
-
if (cmp !== null && cmp < 0) {
|
|
93828
|
-
return {
|
|
93829
|
-
skip: true,
|
|
93830
|
-
running,
|
|
93831
|
-
message: `Skipping ${opts.container} deploy \u2014 it is already on ${running}, newer than the ` + `requested ${opts.targetTag}.
|
|
93832
|
-
` + ` Refusing to downgrade: this guards against a concurrent rollout/update ` + `reverting a newer build.
|
|
93833
|
-
` + ` To force the downgrade, re-run with --allow-downgrade.`
|
|
93834
|
-
};
|
|
93835
|
-
}
|
|
93836
|
-
return { skip: false, running };
|
|
93837
|
-
}
|
|
93838
|
-
|
|
93839
93945
|
// src/cli/singleton-stale-cleanup.ts
|
|
93840
93946
|
import { spawnSync as spawnSync19 } from "node:child_process";
|
|
93841
93947
|
function makeDockerRunner() {
|
|
@@ -94277,7 +94383,7 @@ The log is created when hostd handles its first privileged-verb request.`));
|
|
|
94277
94383
|
init_source();
|
|
94278
94384
|
init_helpers();
|
|
94279
94385
|
init_operator_uid();
|
|
94280
|
-
import { existsSync as existsSync94, mkdirSync as mkdirSync53, writeFileSync as writeFileSync44, copyFileSync as copyFileSync14 } from "node:fs";
|
|
94386
|
+
import { chownSync as chownSync9, existsSync as existsSync94, mkdirSync as mkdirSync53, writeFileSync as writeFileSync44, copyFileSync as copyFileSync14 } from "node:fs";
|
|
94281
94387
|
import { homedir as homedir55 } from "node:os";
|
|
94282
94388
|
import { join as join91 } from "node:path";
|
|
94283
94389
|
import { spawnSync as spawnSync21 } from "node:child_process";
|
|
@@ -94286,6 +94392,16 @@ function resolveWebImageTag(explicitTag, release) {
|
|
|
94286
94392
|
return explicitTag;
|
|
94287
94393
|
return resolveImageTag(resolveRelease({ root: release }));
|
|
94288
94394
|
}
|
|
94395
|
+
function resolveWebHostHome(env2 = process.env, home2 = homedir55()) {
|
|
94396
|
+
const fromEnv = env2.SWITCHROOM_HOST_HOME?.trim();
|
|
94397
|
+
const resolved = fromEnv && fromEnv.length > 0 ? fromEnv : home2;
|
|
94398
|
+
if (resolved === "/host-home" || resolved.startsWith("/host-home/")) {
|
|
94399
|
+
throw new Error(`switchroom webd install: refusing to generate \u2014 the host home resolved to ` + `"${resolved}", the in-container mount point of the operator home (never a ` + `valid host bind source). Emitting it would make Docker create empty ` + `/host-home dirs on the host and break every webhook forward + secret read.
|
|
94400
|
+
|
|
94401
|
+
` + `Recovery: run \`switchroom webd install\` from the HOST shell, or set ` + `SWITCHROOM_HOST_HOME to the real host home first.`);
|
|
94402
|
+
}
|
|
94403
|
+
return resolved;
|
|
94404
|
+
}
|
|
94289
94405
|
var WEB_COMPOSE_PROJECT = "switchroom-web";
|
|
94290
94406
|
function renderWebComposeFile(opts) {
|
|
94291
94407
|
const { hostHome, imageTag, operatorUid } = opts;
|
|
@@ -94408,8 +94524,15 @@ async function doInstall2(opts, program3) {
|
|
|
94408
94524
|
console.log(source_default.yellow(` \u23ed ${opts.dryRun ? "[dry-run] " : ""}${guard.message}`));
|
|
94409
94525
|
return;
|
|
94410
94526
|
}
|
|
94527
|
+
let hostHome;
|
|
94528
|
+
try {
|
|
94529
|
+
hostHome = resolveWebHostHome();
|
|
94530
|
+
} catch (err2) {
|
|
94531
|
+
console.error(source_default.red(err2.message));
|
|
94532
|
+
process.exit(1);
|
|
94533
|
+
}
|
|
94411
94534
|
const yaml = renderWebComposeFile({
|
|
94412
|
-
hostHome
|
|
94535
|
+
hostHome,
|
|
94413
94536
|
imageTag,
|
|
94414
94537
|
operatorUid
|
|
94415
94538
|
});
|
|
@@ -94423,6 +94546,14 @@ async function doInstall2(opts, program3) {
|
|
|
94423
94546
|
if (bak)
|
|
94424
94547
|
console.log(source_default.dim(` Backed up existing compose to ${bak}`));
|
|
94425
94548
|
writeFileSync44(composePath, yaml, "utf8");
|
|
94549
|
+
try {
|
|
94550
|
+
if (typeof process.geteuid === "function" && process.geteuid() === 0) {
|
|
94551
|
+
chownSync9(dir, operatorUid, operatorUid);
|
|
94552
|
+
chownSync9(composePath, operatorUid, operatorUid);
|
|
94553
|
+
if (bak)
|
|
94554
|
+
chownSync9(bak, operatorUid, operatorUid);
|
|
94555
|
+
}
|
|
94556
|
+
} catch {}
|
|
94426
94557
|
console.log(source_default.green(` \u2713 Wrote ${composePath}`));
|
|
94427
94558
|
console.log(source_default.dim(` running as uid ${operatorUid} (operator), network_mode: host`));
|
|
94428
94559
|
console.log(source_default.dim(` Pulling ghcr.io/switchroom/switchroom-web:${imageTag}\u2026`));
|