switchroom 0.18.10 → 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/cli/switchroom.js +188 -81
- 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 +758 -256
- package/dist/vault/approvals/kernel-server.js +1 -0
- package/dist/vault/broker/server.js +1 -0
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +5 -4
|
@@ -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);
|
|
@@ -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 existsSync11, 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);
|
|
@@ -22797,11 +22797,391 @@ function resolveAgentsDir(config) {
|
|
|
22797
22797
|
return resolveDualPath(config.switchroom.agents_dir || "~/.switchroom/agents");
|
|
22798
22798
|
}
|
|
22799
22799
|
|
|
22800
|
+
// src/host-control/config-degraded.ts
|
|
22801
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, renameSync, unlinkSync, watchFile, unwatchFile, writeFileSync, mkdirSync } from "node:fs";
|
|
22802
|
+
import { connect } from "node:net";
|
|
22803
|
+
import { join } from "node:path";
|
|
22804
|
+
|
|
22805
|
+
// src/config/release-resolve.ts
|
|
22806
|
+
function parseSemverTag(tag) {
|
|
22807
|
+
if (!tag)
|
|
22808
|
+
return null;
|
|
22809
|
+
const m = /^v(\d+)\.(\d+)\.(\d+)$/.exec(tag.trim());
|
|
22810
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
22811
|
+
}
|
|
22812
|
+
function compareReleaseTags(a, b) {
|
|
22813
|
+
const pa = parseSemverTag(a);
|
|
22814
|
+
const pb = parseSemverTag(b);
|
|
22815
|
+
if (!pa || !pb)
|
|
22816
|
+
return null;
|
|
22817
|
+
for (let i = 0;i < 3; i++) {
|
|
22818
|
+
if (pa[i] !== pb[i])
|
|
22819
|
+
return pa[i] < pb[i] ? -1 : 1;
|
|
22820
|
+
}
|
|
22821
|
+
return 0;
|
|
22822
|
+
}
|
|
22823
|
+
|
|
22824
|
+
// src/host-control/self-bump.ts
|
|
22825
|
+
var SELF_BUMP_MARKER_FILENAME = "pending-rollout.json";
|
|
22826
|
+
var SELF_BUMP_HELPER_CONTAINER = "switchroom-hostd-selfbump";
|
|
22827
|
+
var SELF_BUMP_LOG_FILENAME = "selfbump.log";
|
|
22828
|
+
var SELF_BUMP_MARKER_MAX_AGE_MS = 15 * 60 * 1000;
|
|
22829
|
+
var SEMVER_PIN_RE = /^v\d+\.\d+\.\d+$/;
|
|
22830
|
+
function needsSelfBump(ownVersion, pin) {
|
|
22831
|
+
if (!ownVersion)
|
|
22832
|
+
return false;
|
|
22833
|
+
const cmp = compareReleaseTags(`v${ownVersion.trim().replace(/^v/, "")}`, `v${pin.trim().replace(/^v/, "")}`);
|
|
22834
|
+
return cmp !== null && cmp < 0;
|
|
22835
|
+
}
|
|
22836
|
+
function bumpHostdComposeImageTag(yaml, pin) {
|
|
22837
|
+
const re = /^(\s*image:\s*)(\S*switchroom-hostd):(\S+)\s*$/m;
|
|
22838
|
+
const m = yaml.match(re);
|
|
22839
|
+
if (!m)
|
|
22840
|
+
return null;
|
|
22841
|
+
const oldImageRef = `${m[2]}:${m[3]}`;
|
|
22842
|
+
const newImageRef = `${m[2]}:${pin}`;
|
|
22843
|
+
return {
|
|
22844
|
+
yaml: yaml.replace(re, `$1${newImageRef}`),
|
|
22845
|
+
oldImageRef,
|
|
22846
|
+
newImageRef
|
|
22847
|
+
};
|
|
22848
|
+
}
|
|
22849
|
+
function encodePendingRolloutMarker(m) {
|
|
22850
|
+
return JSON.stringify(m, null, 2) + `
|
|
22851
|
+
`;
|
|
22852
|
+
}
|
|
22853
|
+
function parsePendingRolloutMarker(raw) {
|
|
22854
|
+
let obj;
|
|
22855
|
+
try {
|
|
22856
|
+
obj = JSON.parse(raw);
|
|
22857
|
+
} catch {
|
|
22858
|
+
return null;
|
|
22859
|
+
}
|
|
22860
|
+
if (typeof obj !== "object" || obj === null)
|
|
22861
|
+
return null;
|
|
22862
|
+
const o = obj;
|
|
22863
|
+
if (o.v !== 1)
|
|
22864
|
+
return null;
|
|
22865
|
+
if (typeof o.request_id !== "string" || o.request_id.length === 0)
|
|
22866
|
+
return null;
|
|
22867
|
+
if (typeof o.pin !== "string" || !SEMVER_PIN_RE.test(o.pin))
|
|
22868
|
+
return null;
|
|
22869
|
+
if (typeof o.created_at !== "string" || !Number.isFinite(Date.parse(o.created_at))) {
|
|
22870
|
+
return null;
|
|
22871
|
+
}
|
|
22872
|
+
if (typeof o.prior_hostd_version !== "string")
|
|
22873
|
+
return null;
|
|
22874
|
+
const caller = o.caller;
|
|
22875
|
+
if (typeof caller !== "object" || caller === null)
|
|
22876
|
+
return null;
|
|
22877
|
+
if (caller.kind === "agent") {
|
|
22878
|
+
if (typeof caller.name !== "string" || caller.name.length === 0)
|
|
22879
|
+
return null;
|
|
22880
|
+
} else if (caller.kind !== "operator") {
|
|
22881
|
+
return null;
|
|
22882
|
+
}
|
|
22883
|
+
if (o.agents !== undefined) {
|
|
22884
|
+
if (!Array.isArray(o.agents) || o.agents.length === 0 || !o.agents.every((a) => typeof a === "string" && /^[\w-]+$/.test(a))) {
|
|
22885
|
+
return null;
|
|
22886
|
+
}
|
|
22887
|
+
}
|
|
22888
|
+
if (o.skip_web !== undefined && typeof o.skip_web !== "boolean")
|
|
22889
|
+
return null;
|
|
22890
|
+
if (o.allow_downgrade !== undefined && typeof o.allow_downgrade !== "boolean") {
|
|
22891
|
+
return null;
|
|
22892
|
+
}
|
|
22893
|
+
return {
|
|
22894
|
+
v: 1,
|
|
22895
|
+
request_id: o.request_id,
|
|
22896
|
+
pin: o.pin,
|
|
22897
|
+
...o.agents !== undefined ? { agents: o.agents } : {},
|
|
22898
|
+
...o.skip_web !== undefined ? { skip_web: o.skip_web } : {},
|
|
22899
|
+
...o.allow_downgrade !== undefined ? { allow_downgrade: o.allow_downgrade } : {},
|
|
22900
|
+
caller: caller.kind === "agent" ? { kind: "agent", name: caller.name } : { kind: "operator" },
|
|
22901
|
+
created_at: o.created_at,
|
|
22902
|
+
prior_hostd_version: o.prior_hostd_version
|
|
22903
|
+
};
|
|
22904
|
+
}
|
|
22905
|
+
function isMarkerFresh(marker, nowMs) {
|
|
22906
|
+
const created = Date.parse(marker.created_at);
|
|
22907
|
+
if (!Number.isFinite(created))
|
|
22908
|
+
return false;
|
|
22909
|
+
return nowMs - created < SELF_BUMP_MARKER_MAX_AGE_MS;
|
|
22910
|
+
}
|
|
22911
|
+
function selfBumpHelperArgs(opts) {
|
|
22912
|
+
const composePath = `${opts.hostdDirHostPath}/docker-compose.yml`;
|
|
22913
|
+
const logPath = `${opts.hostdDirHostPath}/${SELF_BUMP_LOG_FILENAME}`;
|
|
22914
|
+
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`;
|
|
22915
|
+
return [
|
|
22916
|
+
"run",
|
|
22917
|
+
"-d",
|
|
22918
|
+
"--rm",
|
|
22919
|
+
"--name",
|
|
22920
|
+
SELF_BUMP_HELPER_CONTAINER,
|
|
22921
|
+
"--label",
|
|
22922
|
+
"switchroom.hostd-selfbump=true",
|
|
22923
|
+
"-v",
|
|
22924
|
+
"/var/run/docker.sock:/var/run/docker.sock",
|
|
22925
|
+
"-v",
|
|
22926
|
+
`${opts.hostdDirHostPath}:${opts.hostdDirHostPath}`,
|
|
22927
|
+
"--entrypoint",
|
|
22928
|
+
"/bin/sh",
|
|
22929
|
+
opts.helperImage,
|
|
22930
|
+
"-c",
|
|
22931
|
+
script
|
|
22932
|
+
];
|
|
22933
|
+
}
|
|
22934
|
+
|
|
22935
|
+
// src/host-control/config-degraded.ts
|
|
22936
|
+
var CONFIG_DEGRADED_MARKER_FILENAME = "config-degraded.json";
|
|
22937
|
+
var CONFIG_DEGRADED_RETRY_MS = 15000;
|
|
22938
|
+
var MAX_PRESERVED_DEGRADED_MS = 4 * 60 * 60 * 1000;
|
|
22939
|
+
function buildDegradedMarker(err, nowMs) {
|
|
22940
|
+
return {
|
|
22941
|
+
v: 1,
|
|
22942
|
+
since: new Date(nowMs).toISOString(),
|
|
22943
|
+
error: err.message,
|
|
22944
|
+
...err.details && err.details.length > 0 ? { details: err.details } : {}
|
|
22945
|
+
};
|
|
22946
|
+
}
|
|
22947
|
+
function readDegradedSince(markerPath) {
|
|
22948
|
+
try {
|
|
22949
|
+
if (!existsSync4(markerPath))
|
|
22950
|
+
return null;
|
|
22951
|
+
const parsed = JSON.parse(readFileSync3(markerPath, "utf8"));
|
|
22952
|
+
if (parsed?.v !== 1 || typeof parsed.since !== "string")
|
|
22953
|
+
return null;
|
|
22954
|
+
const since = Date.parse(parsed.since);
|
|
22955
|
+
return Number.isFinite(since) ? since : null;
|
|
22956
|
+
} catch {
|
|
22957
|
+
return null;
|
|
22958
|
+
}
|
|
22959
|
+
}
|
|
22960
|
+
function formatDegradedNotice(err) {
|
|
22961
|
+
const details = (err.details ?? []).map((d) => d.trim()).filter(Boolean).slice(0, 6).join(`
|
|
22962
|
+
`);
|
|
22963
|
+
return `⚠️ hostd: switchroom.yaml failed to load — running DEGRADED.
|
|
22964
|
+
` + `${err.message}
|
|
22965
|
+
` + (details ? details + `
|
|
22966
|
+
` : "") + `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).`;
|
|
22967
|
+
}
|
|
22968
|
+
function humanMins(ms) {
|
|
22969
|
+
const mins = Math.max(1, Math.round(ms / 60000));
|
|
22970
|
+
return mins >= 90 ? `${(mins / 60).toFixed(1)}h` : `${mins}m`;
|
|
22971
|
+
}
|
|
22972
|
+
function formatRecoveredNotice(degradedMs, pending) {
|
|
22973
|
+
let tail = "";
|
|
22974
|
+
if (pending?.preserved) {
|
|
22975
|
+
tail = ` Pending self-bump rollout to ${pending.pin} was preserved through the ` + `outage (marker now ${humanMins(pending.ageMs)} old) and will resume now.`;
|
|
22976
|
+
} else if (pending) {
|
|
22977
|
+
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.`;
|
|
22978
|
+
}
|
|
22979
|
+
return `✅ hostd: switchroom.yaml loads again after ~${humanMins(degradedMs)} degraded — ` + `normal service resumed.` + tail;
|
|
22980
|
+
}
|
|
22981
|
+
function shiftPendingRolloutMarker(raw, degradedMs) {
|
|
22982
|
+
if (degradedMs <= 0)
|
|
22983
|
+
return null;
|
|
22984
|
+
const marker = parsePendingRolloutMarker(raw);
|
|
22985
|
+
if (!marker)
|
|
22986
|
+
return null;
|
|
22987
|
+
const created = Date.parse(marker.created_at);
|
|
22988
|
+
if (!Number.isFinite(created))
|
|
22989
|
+
return null;
|
|
22990
|
+
return encodePendingRolloutMarker({
|
|
22991
|
+
...marker,
|
|
22992
|
+
created_at: new Date(created + degradedMs).toISOString()
|
|
22993
|
+
});
|
|
22994
|
+
}
|
|
22995
|
+
function clearStaleDegradedMarker(homeDir, log) {
|
|
22996
|
+
const markerPath = join(homeDir, ".switchroom", "hostd", CONFIG_DEGRADED_MARKER_FILENAME);
|
|
22997
|
+
try {
|
|
22998
|
+
if (!existsSync4(markerPath))
|
|
22999
|
+
return false;
|
|
23000
|
+
unlinkSync(markerPath);
|
|
23001
|
+
log(`config-degraded: clearing stale degraded marker from a prior crash ` + `(${markerPath}) — config loads cleanly on this boot`);
|
|
23002
|
+
return true;
|
|
23003
|
+
} catch (e) {
|
|
23004
|
+
log(`config-degraded: could not clear stale marker ${markerPath}: ${e.message}`);
|
|
23005
|
+
return false;
|
|
23006
|
+
}
|
|
23007
|
+
}
|
|
23008
|
+
function postOperatorNoticeViaGateways(candidates, text, log, connectTimeoutMs = 5000) {
|
|
23009
|
+
const tryOne = (c) => new Promise((resolve4) => {
|
|
23010
|
+
let done = false;
|
|
23011
|
+
const finish = (ok, why) => {
|
|
23012
|
+
if (done)
|
|
23013
|
+
return;
|
|
23014
|
+
done = true;
|
|
23015
|
+
if (why)
|
|
23016
|
+
log(`config-degraded: gateway ${c.agent} — ${why}`);
|
|
23017
|
+
try {
|
|
23018
|
+
client.destroy();
|
|
23019
|
+
} catch {}
|
|
23020
|
+
resolve4(ok);
|
|
23021
|
+
};
|
|
23022
|
+
let client;
|
|
23023
|
+
try {
|
|
23024
|
+
client = connect({ path: c.sock });
|
|
23025
|
+
} catch (e) {
|
|
23026
|
+
log(`config-degraded: gateway ${c.agent} — connect threw: ${e.message}`);
|
|
23027
|
+
resolve4(false);
|
|
23028
|
+
return;
|
|
23029
|
+
}
|
|
23030
|
+
client.setTimeout(connectTimeoutMs);
|
|
23031
|
+
client.on("connect", () => {
|
|
23032
|
+
try {
|
|
23033
|
+
client.write(JSON.stringify({
|
|
23034
|
+
type: "rollout_status_post",
|
|
23035
|
+
requestId: "config-degraded",
|
|
23036
|
+
agentName: c.agent,
|
|
23037
|
+
text
|
|
23038
|
+
}) + `
|
|
23039
|
+
`, () => {
|
|
23040
|
+
client.end();
|
|
23041
|
+
finish(true);
|
|
23042
|
+
});
|
|
23043
|
+
} catch (e) {
|
|
23044
|
+
finish(false, `write failed: ${e.message}`);
|
|
23045
|
+
}
|
|
23046
|
+
});
|
|
23047
|
+
client.on("timeout", () => finish(false, "connect timed out"));
|
|
23048
|
+
client.on("error", (e) => finish(false, `socket error: ${e.message}`));
|
|
23049
|
+
});
|
|
23050
|
+
return (async () => {
|
|
23051
|
+
for (const c of candidates) {
|
|
23052
|
+
if (await tryOne(c))
|
|
23053
|
+
return c.agent;
|
|
23054
|
+
}
|
|
23055
|
+
if (candidates.length > 0) {
|
|
23056
|
+
log("config-degraded: every candidate gateway socket failed; relying on marker file");
|
|
23057
|
+
}
|
|
23058
|
+
return null;
|
|
23059
|
+
})();
|
|
23060
|
+
}
|
|
23061
|
+
async function waitForConfigRecovery(opts) {
|
|
23062
|
+
const now = opts.nowFn ?? Date.now;
|
|
23063
|
+
const retryMs = opts.retryMs ?? CONFIG_DEGRADED_RETRY_MS;
|
|
23064
|
+
const hostdDir = join(opts.homeDir, ".switchroom", "hostd");
|
|
23065
|
+
const degradedMarkerPath = join(hostdDir, CONFIG_DEGRADED_MARKER_FILENAME);
|
|
23066
|
+
const pendingMarkerPath = join(hostdDir, SELF_BUMP_MARKER_FILENAME);
|
|
23067
|
+
const persistedSince = readDegradedSince(degradedMarkerPath);
|
|
23068
|
+
const enteredAt = Math.min(persistedSince ?? now(), now());
|
|
23069
|
+
if (persistedSince !== null) {
|
|
23070
|
+
opts.log(`config-degraded: resuming an existing degraded window (since ` + `${new Date(enteredAt).toISOString()}, persisted in ${degradedMarkerPath})`);
|
|
23071
|
+
}
|
|
23072
|
+
try {
|
|
23073
|
+
mkdirSync(hostdDir, { recursive: true, mode: 448 });
|
|
23074
|
+
writeFileSync(degradedMarkerPath + ".tmp", JSON.stringify(buildDegradedMarker(opts.initialError, enteredAt), null, 2) + `
|
|
23075
|
+
`, { mode: 384 });
|
|
23076
|
+
renameSync(degradedMarkerPath + ".tmp", degradedMarkerPath);
|
|
23077
|
+
} catch (e) {
|
|
23078
|
+
opts.log(`config-degraded: could not write marker ${degradedMarkerPath}: ${e.message}`);
|
|
23079
|
+
}
|
|
23080
|
+
try {
|
|
23081
|
+
opts.notify(formatDegradedNotice(opts.initialError));
|
|
23082
|
+
} catch (e) {
|
|
23083
|
+
opts.log(`config-degraded: notify threw: ${e.message}`);
|
|
23084
|
+
}
|
|
23085
|
+
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}`);
|
|
23086
|
+
const config = await new Promise((resolve4, reject) => {
|
|
23087
|
+
let timer = null;
|
|
23088
|
+
let settled = false;
|
|
23089
|
+
const cleanup = () => {
|
|
23090
|
+
if (timer)
|
|
23091
|
+
clearInterval(timer);
|
|
23092
|
+
if (opts.configPath) {
|
|
23093
|
+
try {
|
|
23094
|
+
unwatchFile(opts.configPath, onFileChange);
|
|
23095
|
+
} catch {}
|
|
23096
|
+
}
|
|
23097
|
+
};
|
|
23098
|
+
const attempt = (why) => {
|
|
23099
|
+
if (settled)
|
|
23100
|
+
return;
|
|
23101
|
+
try {
|
|
23102
|
+
const cfg = opts.loadFn();
|
|
23103
|
+
settled = true;
|
|
23104
|
+
cleanup();
|
|
23105
|
+
resolve4(cfg);
|
|
23106
|
+
} catch (err) {
|
|
23107
|
+
if (err instanceof ConfigError) {
|
|
23108
|
+
opts.log(`config-degraded: reload attempt (${why}) still failing — ${err.message}`);
|
|
23109
|
+
return;
|
|
23110
|
+
}
|
|
23111
|
+
settled = true;
|
|
23112
|
+
cleanup();
|
|
23113
|
+
reject(err);
|
|
23114
|
+
}
|
|
23115
|
+
};
|
|
23116
|
+
const onFileChange = () => attempt("file change");
|
|
23117
|
+
timer = setInterval(() => attempt("interval"), retryMs);
|
|
23118
|
+
if (opts.configPath) {
|
|
23119
|
+
try {
|
|
23120
|
+
watchFile(opts.configPath, { interval: 5000 }, onFileChange);
|
|
23121
|
+
} catch (e) {
|
|
23122
|
+
opts.log(`config-degraded: could not watch ${opts.configPath}: ${e.message}`);
|
|
23123
|
+
}
|
|
23124
|
+
}
|
|
23125
|
+
});
|
|
23126
|
+
const degradedMs = Math.max(0, now() - enteredAt);
|
|
23127
|
+
let pendingOutcome = null;
|
|
23128
|
+
try {
|
|
23129
|
+
if (existsSync4(pendingMarkerPath)) {
|
|
23130
|
+
const raw = readFileSync3(pendingMarkerPath, "utf8");
|
|
23131
|
+
const marker = parsePendingRolloutMarker(raw);
|
|
23132
|
+
if (marker && degradedMs <= 0) {
|
|
23133
|
+
pendingOutcome = {
|
|
23134
|
+
pin: marker.pin,
|
|
23135
|
+
ageMs: Math.max(0, now() - Date.parse(marker.created_at)),
|
|
23136
|
+
preserved: true
|
|
23137
|
+
};
|
|
23138
|
+
} else if (marker) {
|
|
23139
|
+
const withinCap = degradedMs <= MAX_PRESERVED_DEGRADED_MS;
|
|
23140
|
+
const shifted = withinCap ? shiftPendingRolloutMarker(raw, degradedMs) : null;
|
|
23141
|
+
if (shifted !== null) {
|
|
23142
|
+
writeFileSync(pendingMarkerPath + ".tmp", shifted, { mode: 384 });
|
|
23143
|
+
renameSync(pendingMarkerPath + ".tmp", pendingMarkerPath);
|
|
23144
|
+
pendingOutcome = {
|
|
23145
|
+
pin: marker.pin,
|
|
23146
|
+
ageMs: Math.max(0, now() - (Date.parse(marker.created_at) + degradedMs)),
|
|
23147
|
+
preserved: true
|
|
23148
|
+
};
|
|
23149
|
+
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`);
|
|
23150
|
+
} else {
|
|
23151
|
+
pendingOutcome = {
|
|
23152
|
+
pin: marker.pin,
|
|
23153
|
+
ageMs: Math.max(0, now() - Date.parse(marker.created_at)),
|
|
23154
|
+
preserved: false
|
|
23155
|
+
};
|
|
23156
|
+
if (!withinCap) {
|
|
23157
|
+
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`);
|
|
23158
|
+
}
|
|
23159
|
+
}
|
|
23160
|
+
}
|
|
23161
|
+
}
|
|
23162
|
+
} catch (e) {
|
|
23163
|
+
opts.log(`config-degraded: could not preserve pending-rollout marker: ${e.message}`);
|
|
23164
|
+
}
|
|
23165
|
+
try {
|
|
23166
|
+
if (existsSync4(degradedMarkerPath))
|
|
23167
|
+
unlinkSync(degradedMarkerPath);
|
|
23168
|
+
} catch (e) {
|
|
23169
|
+
opts.log(`config-degraded: could not remove marker ${degradedMarkerPath}: ${e.message}`);
|
|
23170
|
+
}
|
|
23171
|
+
try {
|
|
23172
|
+
opts.notify(formatRecoveredNotice(degradedMs, pendingOutcome));
|
|
23173
|
+
} catch (e) {
|
|
23174
|
+
opts.log(`config-degraded: recovery notify threw: ${e.message}`);
|
|
23175
|
+
}
|
|
23176
|
+
opts.log(`config-degraded: config loads again after ${Math.round(degradedMs / 1000)}s — resuming normal boot`);
|
|
23177
|
+
return config;
|
|
23178
|
+
}
|
|
23179
|
+
|
|
22800
23180
|
// src/agents/compose.ts
|
|
22801
23181
|
import { createHash } from "node:crypto";
|
|
22802
23182
|
|
|
22803
23183
|
// src/agents/scaffold.ts
|
|
22804
|
-
import { join as
|
|
23184
|
+
import { join as join5, resolve as resolve5 } from "node:path";
|
|
22805
23185
|
|
|
22806
23186
|
// src/config/timezone.ts
|
|
22807
23187
|
var CONTAINER_DEFAULT_UTC_ZONES = new Set([
|
|
@@ -22812,7 +23192,7 @@ var CONTAINER_DEFAULT_UTC_ZONES = new Set([
|
|
|
22812
23192
|
]);
|
|
22813
23193
|
|
|
22814
23194
|
// src/cli/agent-config.ts
|
|
22815
|
-
import { join } from "node:path";
|
|
23195
|
+
import { join as join2 } from "node:path";
|
|
22816
23196
|
import { homedir as homedir2 } from "node:os";
|
|
22817
23197
|
|
|
22818
23198
|
// src/vault/broker/acl.ts
|
|
@@ -22823,12 +23203,12 @@ var WEBKITE_VAULT_KEYS = new Set([
|
|
|
22823
23203
|
]);
|
|
22824
23204
|
|
|
22825
23205
|
// src/cli/agent-config.ts
|
|
22826
|
-
var AUDIT_ROOT =
|
|
23206
|
+
var AUDIT_ROOT = join2(homedir2(), ".switchroom", "audit");
|
|
22827
23207
|
|
|
22828
23208
|
// src/agents/profiles.ts
|
|
22829
23209
|
var import_handlebars = __toESM(require_lib(), 1);
|
|
22830
|
-
import { readFileSync as
|
|
22831
|
-
import { resolve as resolve4, join as
|
|
23210
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync5, readdirSync as readdirSync2, statSync as statSync2, copyFileSync, mkdirSync as mkdirSync2, realpathSync } from "node:fs";
|
|
23211
|
+
import { resolve as resolve4, join as join3, sep as pathSep } from "node:path";
|
|
22832
23212
|
var PROFILES_ROOT = resolve4(import.meta.dirname, "../../profiles");
|
|
22833
23213
|
import_handlebars.default.registerHelper("json", (value) => {
|
|
22834
23214
|
return new import_handlebars.default.SafeString(JSON.stringify(value, null, 2));
|
|
@@ -22839,9 +23219,9 @@ import_handlebars.default.registerHelper("isNumber", (value) => {
|
|
|
22839
23219
|
var SHARED_FRAGMENTS_DIR = resolve4(PROFILES_ROOT, "_shared");
|
|
22840
23220
|
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol"];
|
|
22841
23221
|
for (const name of SHARED_FRAGMENTS) {
|
|
22842
|
-
const fragPath =
|
|
22843
|
-
if (
|
|
22844
|
-
import_handlebars.default.registerPartial(name,
|
|
23222
|
+
const fragPath = join3(SHARED_FRAGMENTS_DIR, `${name}.md.hbs`);
|
|
23223
|
+
if (existsSync5(fragPath)) {
|
|
23224
|
+
import_handlebars.default.registerPartial(name, readFileSync4(fragPath, "utf-8"));
|
|
22845
23225
|
}
|
|
22846
23226
|
}
|
|
22847
23227
|
|
|
@@ -22929,7 +23309,7 @@ var SCRYPT_MAXMEM = 128 * 1024 * 1024;
|
|
|
22929
23309
|
|
|
22930
23310
|
// src/vault/broker/client.ts
|
|
22931
23311
|
import { homedir as homedir3 } from "node:os";
|
|
22932
|
-
import { join as
|
|
23312
|
+
import { join as join4 } from "node:path";
|
|
22933
23313
|
|
|
22934
23314
|
// src/broker-common/peercred-path.ts
|
|
22935
23315
|
function matchSocketName(socketPath, patterns, maxNameLen) {
|
|
@@ -23279,8 +23659,8 @@ var ResponseSchema = exports_external.union([
|
|
|
23279
23659
|
]);
|
|
23280
23660
|
|
|
23281
23661
|
// src/vault/broker/client.ts
|
|
23282
|
-
var LEGACY_SOCKET_PATH =
|
|
23283
|
-
var OPERATOR_SOCKET_PATH =
|
|
23662
|
+
var LEGACY_SOCKET_PATH = join4(homedir3(), ".switchroom", "vault-broker.sock");
|
|
23663
|
+
var OPERATOR_SOCKET_PATH = join4(homedir3(), ".switchroom", "broker-operator", "sock");
|
|
23284
23664
|
|
|
23285
23665
|
// src/vault/resolver.ts
|
|
23286
23666
|
var materializedDirs = new Set;
|
|
@@ -23317,20 +23697,20 @@ import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
|
|
|
23317
23697
|
import { mkdir, chmod, chown, unlink, appendFile } from "node:fs/promises";
|
|
23318
23698
|
import {
|
|
23319
23699
|
readdirSync as readdirSync3,
|
|
23320
|
-
existsSync as
|
|
23321
|
-
readFileSync as
|
|
23322
|
-
writeFileSync as
|
|
23323
|
-
renameSync as
|
|
23324
|
-
mkdirSync as
|
|
23700
|
+
existsSync as existsSync10,
|
|
23701
|
+
readFileSync as readFileSync8,
|
|
23702
|
+
writeFileSync as writeFileSync5,
|
|
23703
|
+
renameSync as renameSync3,
|
|
23704
|
+
mkdirSync as mkdirSync4,
|
|
23325
23705
|
openSync as openSync3,
|
|
23326
23706
|
ftruncateSync,
|
|
23327
23707
|
writeSync as writeSync2,
|
|
23328
23708
|
fsyncSync as fsyncSync2,
|
|
23329
23709
|
closeSync as closeSync3,
|
|
23330
23710
|
copyFileSync as copyFileSync2,
|
|
23331
|
-
unlinkSync
|
|
23711
|
+
unlinkSync as unlinkSync2
|
|
23332
23712
|
} from "node:fs";
|
|
23333
|
-
import { join as
|
|
23713
|
+
import { join as join10, dirname as dirname6, resolve as resolve8 } from "node:path";
|
|
23334
23714
|
import { createHash as createHash5, randomUUID as randomUUID2, randomBytes } from "node:crypto";
|
|
23335
23715
|
|
|
23336
23716
|
// src/host-control/protocol.ts
|
|
@@ -28019,10 +28399,10 @@ class PostHog extends PostHogBackendClient {
|
|
|
28019
28399
|
|
|
28020
28400
|
// src/analytics/posthog.ts
|
|
28021
28401
|
import {
|
|
28022
|
-
existsSync as
|
|
28023
|
-
mkdirSync as
|
|
28024
|
-
readFileSync as
|
|
28025
|
-
writeFileSync as
|
|
28402
|
+
existsSync as existsSync6,
|
|
28403
|
+
mkdirSync as mkdirSync3,
|
|
28404
|
+
readFileSync as readFileSync5,
|
|
28405
|
+
writeFileSync as writeFileSync3
|
|
28026
28406
|
} from "node:fs";
|
|
28027
28407
|
import { dirname as dirname2 } from "node:path";
|
|
28028
28408
|
import { randomUUID } from "node:crypto";
|
|
@@ -28040,8 +28420,8 @@ function getDistinctId() {
|
|
|
28040
28420
|
return cachedDistinctId;
|
|
28041
28421
|
const path = resolveStatePath("analytics-id");
|
|
28042
28422
|
try {
|
|
28043
|
-
if (
|
|
28044
|
-
const existing =
|
|
28423
|
+
if (existsSync6(path)) {
|
|
28424
|
+
const existing = readFileSync5(path, "utf-8").trim();
|
|
28045
28425
|
if (existing) {
|
|
28046
28426
|
cachedDistinctId = existing;
|
|
28047
28427
|
return existing;
|
|
@@ -28051,8 +28431,8 @@ function getDistinctId() {
|
|
|
28051
28431
|
const id = randomUUID();
|
|
28052
28432
|
cachedDistinctId = id;
|
|
28053
28433
|
try {
|
|
28054
|
-
|
|
28055
|
-
|
|
28434
|
+
mkdirSync3(dirname2(path), { recursive: true });
|
|
28435
|
+
writeFileSync3(path, id, "utf-8");
|
|
28056
28436
|
} catch {}
|
|
28057
28437
|
return id;
|
|
28058
28438
|
}
|
|
@@ -28715,40 +29095,21 @@ var import_yaml3 = __toESM(require_dist(), 1);
|
|
|
28715
29095
|
// src/cli/rollout.ts
|
|
28716
29096
|
init_atomic();
|
|
28717
29097
|
|
|
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
29098
|
// src/cli/resolve-version.ts
|
|
28738
|
-
import { existsSync as
|
|
28739
|
-
import { dirname as dirname4, join as
|
|
29099
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
|
|
29100
|
+
import { dirname as dirname4, join as join7 } from "node:path";
|
|
28740
29101
|
|
|
28741
29102
|
// src/build-info.ts
|
|
28742
|
-
var VERSION = "0.18.
|
|
29103
|
+
var VERSION = "0.18.11";
|
|
28743
29104
|
|
|
28744
29105
|
// src/cli/resolve-version.ts
|
|
28745
29106
|
function readPackageVersion() {
|
|
28746
29107
|
let dir = import.meta.dirname;
|
|
28747
29108
|
for (let i = 0;i < 12 && dir && dir !== "/"; i++) {
|
|
28748
|
-
const p =
|
|
28749
|
-
if (
|
|
29109
|
+
const p = join7(dir, "package.json");
|
|
29110
|
+
if (existsSync8(p)) {
|
|
28750
29111
|
try {
|
|
28751
|
-
const pkg = JSON.parse(
|
|
29112
|
+
const pkg = JSON.parse(readFileSync6(p, "utf-8"));
|
|
28752
29113
|
if (pkg?.name === "switchroom" && typeof pkg.version === "string") {
|
|
28753
29114
|
return pkg.version;
|
|
28754
29115
|
}
|
|
@@ -28895,6 +29256,7 @@ function parseRolloutPhaseLine(line) {
|
|
|
28895
29256
|
"agent-start",
|
|
28896
29257
|
"agent-done",
|
|
28897
29258
|
"persist-pin",
|
|
29259
|
+
"web-refresh",
|
|
28898
29260
|
"hostd-web-deferred",
|
|
28899
29261
|
"self-bump",
|
|
28900
29262
|
"self-bump-done"
|
|
@@ -28932,122 +29294,11 @@ function parseRolloutResultLine(stdout) {
|
|
|
28932
29294
|
return null;
|
|
28933
29295
|
}
|
|
28934
29296
|
|
|
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
|
|
29014
|
-
};
|
|
29015
|
-
}
|
|
29016
|
-
function isMarkerFresh(marker, nowMs) {
|
|
29017
|
-
const created = Date.parse(marker.created_at);
|
|
29018
|
-
if (!Number.isFinite(created))
|
|
29019
|
-
return false;
|
|
29020
|
-
return nowMs - created < SELF_BUMP_MARKER_MAX_AGE_MS;
|
|
29021
|
-
}
|
|
29022
|
-
function selfBumpHelperArgs(opts) {
|
|
29023
|
-
const composePath = `${opts.hostdDirHostPath}/docker-compose.yml`;
|
|
29024
|
-
const logPath = `${opts.hostdDirHostPath}/${SELF_BUMP_LOG_FILENAME}`;
|
|
29025
|
-
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`;
|
|
29026
|
-
return [
|
|
29027
|
-
"run",
|
|
29028
|
-
"-d",
|
|
29029
|
-
"--rm",
|
|
29030
|
-
"--name",
|
|
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
|
-
];
|
|
29044
|
-
}
|
|
29045
|
-
|
|
29046
29297
|
// node_modules/.bun/commander@13.1.0/node_modules/commander/esm.mjs
|
|
29047
29298
|
var import__3 = __toESM(require_commander(), 1);
|
|
29048
29299
|
|
|
29049
29300
|
// src/cli/update.ts
|
|
29050
|
-
import { join as
|
|
29301
|
+
import { join as join8, dirname as dirname5, resolve as resolve7 } from "node:path";
|
|
29051
29302
|
import { homedir as homedir5 } from "node:os";
|
|
29052
29303
|
|
|
29053
29304
|
// src/agents/lifecycle.ts
|
|
@@ -29144,7 +29395,7 @@ function getAllAgentStatuses(config) {
|
|
|
29144
29395
|
|
|
29145
29396
|
// src/cli/update.ts
|
|
29146
29397
|
init_atomic();
|
|
29147
|
-
var DEFAULT_COMPOSE_PATH =
|
|
29398
|
+
var DEFAULT_COMPOSE_PATH = join8(homedir5(), ".switchroom", "compose", "docker-compose.yml");
|
|
29148
29399
|
var UPDATE_RESULT_SENTINEL = "SWITCHROOM_UPDATE_RESULT:";
|
|
29149
29400
|
function parseUpdateResultLine(stdout) {
|
|
29150
29401
|
const idx = stdout.lastIndexOf(UPDATE_RESULT_SENTINEL);
|
|
@@ -29167,9 +29418,9 @@ function parseUpdateResultLine(stdout) {
|
|
|
29167
29418
|
|
|
29168
29419
|
// src/host-control/config-edit-validator.ts
|
|
29169
29420
|
var import_yaml5 = __toESM(require_dist(), 1);
|
|
29170
|
-
import { mkdtempSync, writeFileSync as
|
|
29421
|
+
import { mkdtempSync, writeFileSync as writeFileSync4, rmSync as rmSync2, existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
|
|
29171
29422
|
import { tmpdir } from "node:os";
|
|
29172
|
-
import { join as
|
|
29423
|
+
import { join as join9, isAbsolute as isAbsolute2, normalize, basename as basename2 } from "node:path";
|
|
29173
29424
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
29174
29425
|
import { isDeepStrictEqual } from "node:util";
|
|
29175
29426
|
var MAX_PATCH_BYTES = 1024 * 1024;
|
|
@@ -29282,22 +29533,22 @@ function validateShape(unifiedDiff, targetPath) {
|
|
|
29282
29533
|
return null;
|
|
29283
29534
|
}
|
|
29284
29535
|
function applyPatch(unifiedDiff, configPath, gitBin) {
|
|
29285
|
-
if (!
|
|
29536
|
+
if (!existsSync9(configPath)) {
|
|
29286
29537
|
return {
|
|
29287
29538
|
ok: false,
|
|
29288
29539
|
code: "E_PATCH_APPLY_FAILED",
|
|
29289
29540
|
detail: `target config not found at ${configPath}`
|
|
29290
29541
|
};
|
|
29291
29542
|
}
|
|
29292
|
-
const liveContent =
|
|
29293
|
-
const scratchDir = mkdtempSync(
|
|
29543
|
+
const liveContent = readFileSync7(configPath, "utf8");
|
|
29544
|
+
const scratchDir = mkdtempSync(join9(tmpdir(), "config-propose-edit-"));
|
|
29294
29545
|
try {
|
|
29295
29546
|
const targetBasename = configPath.split("/").pop() ?? "switchroom.yaml";
|
|
29296
|
-
const scratchFile =
|
|
29297
|
-
|
|
29547
|
+
const scratchFile = join9(scratchDir, targetBasename);
|
|
29548
|
+
writeFileSync4(scratchFile, liveContent);
|
|
29298
29549
|
const normalizedDiff = normalizeDiffHeadersToBasename(unifiedDiff, targetBasename);
|
|
29299
|
-
const patchFile =
|
|
29300
|
-
|
|
29550
|
+
const patchFile = join9(scratchDir, "proposal.patch");
|
|
29551
|
+
writeFileSync4(patchFile, normalizedDiff);
|
|
29301
29552
|
const bin = gitBin ?? "git";
|
|
29302
29553
|
const baseArgs = [
|
|
29303
29554
|
"apply",
|
|
@@ -29327,7 +29578,7 @@ function applyPatch(unifiedDiff, configPath, gitBin) {
|
|
|
29327
29578
|
detail: `git apply failed: ${(real.stderr || "").trim().slice(0, 500)}`
|
|
29328
29579
|
};
|
|
29329
29580
|
}
|
|
29330
|
-
const after =
|
|
29581
|
+
const after = readFileSync7(scratchFile, "utf8");
|
|
29331
29582
|
return { ok: true, after };
|
|
29332
29583
|
} finally {
|
|
29333
29584
|
rmSync2(scratchDir, { recursive: true, force: true });
|
|
@@ -29525,7 +29776,7 @@ function validateConfigEdit(opts) {
|
|
|
29525
29776
|
return schemaErr;
|
|
29526
29777
|
let beforeData = {};
|
|
29527
29778
|
try {
|
|
29528
|
-
const beforeRaw =
|
|
29779
|
+
const beforeRaw = existsSync9(opts.configPath) ? readFileSync7(opts.configPath, "utf8") : "";
|
|
29529
29780
|
const beforeDoc = import_yaml5.parseDocument(beforeRaw, { merge: false, strict: false });
|
|
29530
29781
|
beforeData = beforeDoc.toJS();
|
|
29531
29782
|
} catch {
|
|
@@ -29771,19 +30022,21 @@ function phaseLine(s) {
|
|
|
29771
30022
|
case "apply":
|
|
29772
30023
|
return "applying — regenerating compose";
|
|
29773
30024
|
case "canary-start":
|
|
29774
|
-
return `canary — restarting ${s.agent
|
|
30025
|
+
return `canary — restarting ${s.agent ? `\`${s.agent}\`` : "canary agent"}`;
|
|
29775
30026
|
case "canary-pass":
|
|
29776
|
-
return `canary passed (${s.agent
|
|
30027
|
+
return `canary passed (${s.agent ? `\`${s.agent}\`` : "canary"}) — rolling the rest`;
|
|
29777
30028
|
case "canary-fail":
|
|
29778
|
-
return `canary failed (${s.agent
|
|
30029
|
+
return `canary failed (${s.agent ? `\`${s.agent}\`` : "canary"})`;
|
|
29779
30030
|
case "agent-start":
|
|
29780
|
-
return `agent ${s.n ?? "?"}/${s.m ?? "?"} — restarting ${s.agent
|
|
30031
|
+
return `agent ${s.n ?? "?"}/${s.m ?? "?"} — restarting ${s.agent ? `\`${s.agent}\`` : ""}`.trim();
|
|
29781
30032
|
case "agent-done":
|
|
29782
|
-
return `agent ${s.n ?? "?"}/${s.m ?? "?"} — ${s.agent
|
|
30033
|
+
return `agent ${s.n ?? "?"}/${s.m ?? "?"} — ${s.agent ? `\`${s.agent}\`` : ""} done`.trim();
|
|
29783
30034
|
case "persist-pin":
|
|
29784
30035
|
return "persisting pin";
|
|
30036
|
+
case "web-refresh":
|
|
30037
|
+
return "refreshing web dashboard (webd install)";
|
|
29785
30038
|
case "hostd-web-deferred":
|
|
29786
|
-
return "hostd
|
|
30039
|
+
return "hostd refresh deferred (run host-side)";
|
|
29787
30040
|
case "self-bump":
|
|
29788
30041
|
return "hostd refreshing itself to the target (brief control blip)…";
|
|
29789
30042
|
case "self-bump-done":
|
|
@@ -29792,18 +30045,147 @@ function phaseLine(s) {
|
|
|
29792
30045
|
return "starting";
|
|
29793
30046
|
}
|
|
29794
30047
|
}
|
|
29795
|
-
function
|
|
29796
|
-
const
|
|
29797
|
-
|
|
30048
|
+
function formatDurationMs(ms) {
|
|
30049
|
+
const totalSec = Math.max(0, Math.round(ms / 1000));
|
|
30050
|
+
if (totalSec < 60)
|
|
30051
|
+
return `${totalSec}s`;
|
|
30052
|
+
const totalMin = Math.floor(totalSec / 60);
|
|
30053
|
+
const sec = totalSec % 60;
|
|
30054
|
+
if (totalMin < 60)
|
|
30055
|
+
return sec > 0 ? `${totalMin}m ${sec}s` : `${totalMin}m`;
|
|
30056
|
+
const hr = Math.floor(totalMin / 60);
|
|
30057
|
+
const min = totalMin % 60;
|
|
30058
|
+
return min > 0 ? `${hr}h ${min}m` : `${hr}h`;
|
|
30059
|
+
}
|
|
30060
|
+
function shortRequestId(id) {
|
|
30061
|
+
return id.length > 16 ? id.slice(0, 16) + "…" : id;
|
|
30062
|
+
}
|
|
30063
|
+
function headerLine(s, icon) {
|
|
30064
|
+
const arrow = s.fromVersion ? `\`${s.fromVersion}\` → \`${s.target}\`` : `→ \`${s.target}\``;
|
|
30065
|
+
const req = s.requestId ? ` · req \`${shortRequestId(s.requestId)}\`` : "";
|
|
30066
|
+
return `${icon} **Rollout** ${arrow}${req}`;
|
|
30067
|
+
}
|
|
30068
|
+
function checklistLines(s, compact) {
|
|
30069
|
+
const agents = s.agents ?? [];
|
|
30070
|
+
const lines = [];
|
|
30071
|
+
let doneCount = 0;
|
|
30072
|
+
let pendingNamed = 0;
|
|
30073
|
+
for (const a of agents) {
|
|
30074
|
+
const canary = a.canary ? " (canary)" : "";
|
|
30075
|
+
const dur = a.durationMs !== undefined ? ` — ${formatDurationMs(a.durationMs)}` : "";
|
|
30076
|
+
switch (a.status) {
|
|
30077
|
+
case "done":
|
|
30078
|
+
if (compact)
|
|
30079
|
+
doneCount += 1;
|
|
30080
|
+
else
|
|
30081
|
+
lines.push(`- ✓ \`${a.name}\`${canary}${dur}`);
|
|
30082
|
+
break;
|
|
30083
|
+
case "running":
|
|
30084
|
+
lines.push(`- ⏳ \`${a.name}\`${canary} — restarting…`);
|
|
30085
|
+
break;
|
|
30086
|
+
case "failed":
|
|
30087
|
+
lines.push(`- ✗ \`${a.name}\`${canary} — failed${dur}`);
|
|
30088
|
+
break;
|
|
30089
|
+
default:
|
|
30090
|
+
if (compact)
|
|
30091
|
+
pendingNamed += 1;
|
|
30092
|
+
else
|
|
30093
|
+
lines.push(`- · \`${a.name}\`${canary}`);
|
|
30094
|
+
}
|
|
30095
|
+
}
|
|
30096
|
+
if (doneCount > 0)
|
|
30097
|
+
lines.unshift(`- ✓ ${doneCount} done`);
|
|
30098
|
+
const unseen = s.m !== undefined ? Math.max(0, s.m - agents.length) : 0;
|
|
30099
|
+
const pendingTotal = unseen + pendingNamed;
|
|
30100
|
+
if (pendingTotal > 0 && agents.length > 0) {
|
|
30101
|
+
lines.push(`- · ${pendingTotal} more pending`);
|
|
30102
|
+
}
|
|
30103
|
+
return lines;
|
|
30104
|
+
}
|
|
30105
|
+
function etaLine(s) {
|
|
30106
|
+
if (s.m === undefined)
|
|
30107
|
+
return null;
|
|
30108
|
+
const done = (s.agents ?? []).filter((a) => a.status === "done" && a.durationMs !== undefined);
|
|
30109
|
+
if (done.length === 0)
|
|
30110
|
+
return null;
|
|
30111
|
+
const remaining = s.m - done.length;
|
|
30112
|
+
if (remaining <= 0)
|
|
30113
|
+
return null;
|
|
30114
|
+
const meanMs = done.reduce((t, a) => t + (a.durationMs ?? 0), 0) / done.length;
|
|
30115
|
+
return `~${formatDurationMs(meanMs * remaining)} left (rough est.)`;
|
|
30116
|
+
}
|
|
30117
|
+
function deferredLines(target) {
|
|
30118
|
+
const bare = target.trim().replace(/^v/, "");
|
|
30119
|
+
return [
|
|
30120
|
+
`**Deferred (still on the prior version — run host-side):**`,
|
|
30121
|
+
`- \`switchroom-web\` — \`switchroom webd install --tag ${target}\``,
|
|
30122
|
+
`- host operator CLI — \`sudo npm i -g switchroom@${bare}\``,
|
|
30123
|
+
`- hostd template regen (only if the release changed hostd mounts/env) — \`switchroom hostd install --tag ${target}\``
|
|
30124
|
+
];
|
|
30125
|
+
}
|
|
30126
|
+
var MAX_RENDER_CHARS = 3800;
|
|
30127
|
+
function rolledList(rolled, compact) {
|
|
30128
|
+
if (rolled.length === 0)
|
|
30129
|
+
return "none";
|
|
30130
|
+
if (compact)
|
|
30131
|
+
return `${rolled.length} agent(s)`;
|
|
30132
|
+
return rolled.map((r) => `\`${r}\``).join(", ");
|
|
30133
|
+
}
|
|
30134
|
+
function renderWith(s, compact) {
|
|
30135
|
+
const rolled = s.rolled ?? [];
|
|
30136
|
+
const rolledCount = rolled.length;
|
|
30137
|
+
const checklist = checklistLines(s, compact);
|
|
30138
|
+
const elapsedMs = s.elapsedMs ?? (s.startedAtMs !== undefined && s.nowMs !== undefined ? s.nowMs - s.startedAtMs : undefined);
|
|
29798
30139
|
if (s.terminal === "completed") {
|
|
29799
|
-
|
|
30140
|
+
const parts2 = [headerLine(s, "✅")];
|
|
30141
|
+
let summary = `**Done** — rolled ${rolledCount}${s.m !== undefined ? `/${s.m}` : ""} agent(s)`;
|
|
30142
|
+
if (elapsedMs !== undefined)
|
|
30143
|
+
summary += ` in ${formatDurationMs(elapsedMs)}`;
|
|
30144
|
+
summary += compact ? "." : `: ${rolledList(rolled, compact)}.`;
|
|
30145
|
+
parts2.push(summary);
|
|
30146
|
+
if (checklist.length > 0)
|
|
30147
|
+
parts2.push("", ...checklist);
|
|
30148
|
+
if (s.deferred !== false)
|
|
30149
|
+
parts2.push("", ...deferredLines(s.target));
|
|
30150
|
+
return parts2.join(`
|
|
30151
|
+
`);
|
|
29800
30152
|
}
|
|
29801
30153
|
if (s.terminal === "error") {
|
|
29802
|
-
const where = s.failedStep ? ` at
|
|
29803
|
-
|
|
30154
|
+
const where = s.failedStep ? ` at \`${s.failedStep}\`${s.failedAgent ? ` (\`${s.failedAgent}\` → ${s.got ?? "unreachable"})` : ""}` : "";
|
|
30155
|
+
const parts2 = [headerLine(s, "❌")];
|
|
30156
|
+
let summary = `**STOPPED**${where}`;
|
|
30157
|
+
if (elapsedMs !== undefined)
|
|
30158
|
+
summary += ` after ${formatDurationMs(elapsedMs)}`;
|
|
30159
|
+
summary += `. Rolled before stop: ${rolledList(rolled, compact)}.`;
|
|
30160
|
+
parts2.push(summary);
|
|
30161
|
+
if (checklist.length > 0)
|
|
30162
|
+
parts2.push("", ...checklist);
|
|
30163
|
+
return parts2.join(`
|
|
30164
|
+
`);
|
|
29804
30165
|
}
|
|
29805
|
-
const
|
|
29806
|
-
|
|
30166
|
+
const parts = [headerLine(s, "⏳"), `**Phase:** ${phaseLine(s)}`];
|
|
30167
|
+
if (checklist.length > 0)
|
|
30168
|
+
parts.push("", ...checklist);
|
|
30169
|
+
const footer = [];
|
|
30170
|
+
if (s.m !== undefined)
|
|
30171
|
+
footer.push(`${rolledCount}/${s.m} rolled`);
|
|
30172
|
+
else if (rolledCount > 0)
|
|
30173
|
+
footer.push(`${rolledCount} rolled`);
|
|
30174
|
+
if (elapsedMs !== undefined)
|
|
30175
|
+
footer.push(`elapsed ${formatDurationMs(elapsedMs)}`);
|
|
30176
|
+
const eta = etaLine(s);
|
|
30177
|
+
if (eta)
|
|
30178
|
+
footer.push(eta);
|
|
30179
|
+
if (footer.length > 0)
|
|
30180
|
+
parts.push("", footer.join(" · "));
|
|
30181
|
+
return parts.join(`
|
|
30182
|
+
`);
|
|
30183
|
+
}
|
|
30184
|
+
function renderRolloutStatus(s) {
|
|
30185
|
+
const full = renderWith(s, false);
|
|
30186
|
+
if (full.length <= MAX_RENDER_CHARS)
|
|
30187
|
+
return full;
|
|
30188
|
+
return renderWith(s, true);
|
|
29807
30189
|
}
|
|
29808
30190
|
|
|
29809
30191
|
// src/scheduler/dispatch.ts
|
|
@@ -29888,11 +30270,11 @@ function resolveDigests(imageRefs) {
|
|
|
29888
30270
|
return out;
|
|
29889
30271
|
}
|
|
29890
30272
|
function readCachedInstallType(bindRoot) {
|
|
29891
|
-
const cacheDir =
|
|
29892
|
-
const cachePath =
|
|
29893
|
-
if (
|
|
30273
|
+
const cacheDir = join10(bindRoot, ".switchroom");
|
|
30274
|
+
const cachePath = join10(cacheDir, "install-type.json");
|
|
30275
|
+
if (existsSync10(cachePath)) {
|
|
29894
30276
|
try {
|
|
29895
|
-
const raw =
|
|
30277
|
+
const raw = readFileSync8(cachePath, "utf-8");
|
|
29896
30278
|
const parsed = JSON.parse(raw);
|
|
29897
30279
|
if (parsed && typeof parsed.install_type === "string" && typeof parsed.detected_at === "string") {
|
|
29898
30280
|
return parsed;
|
|
@@ -29909,10 +30291,10 @@ function readCachedInstallType(bindRoot) {
|
|
|
29909
30291
|
source_paths: ctx.source_paths
|
|
29910
30292
|
};
|
|
29911
30293
|
try {
|
|
29912
|
-
|
|
30294
|
+
mkdirSync4(cacheDir, { recursive: true });
|
|
29913
30295
|
const tmp = `${cachePath}.tmp`;
|
|
29914
|
-
|
|
29915
|
-
|
|
30296
|
+
writeFileSync5(tmp, JSON.stringify(payload, null, 2), { mode: 420 });
|
|
30297
|
+
renameSync3(tmp, cachePath);
|
|
29916
30298
|
} catch {}
|
|
29917
30299
|
return payload;
|
|
29918
30300
|
}
|
|
@@ -29941,7 +30323,7 @@ function writeFileInPlacePreservingInode(targetPath, content) {
|
|
|
29941
30323
|
} finally {
|
|
29942
30324
|
closeSync3(fd);
|
|
29943
30325
|
}
|
|
29944
|
-
const readBack =
|
|
30326
|
+
const readBack = readFileSync8(targetPath);
|
|
29945
30327
|
if (readBack.length !== buf.length) {
|
|
29946
30328
|
throw new Error(`in-place write short: wrote ${buf.length} bytes but read back ${readBack.length}`);
|
|
29947
30329
|
}
|
|
@@ -29966,7 +30348,7 @@ class HostdServer {
|
|
|
29966
30348
|
this.opts = opts;
|
|
29967
30349
|
}
|
|
29968
30350
|
async start() {
|
|
29969
|
-
const hostdDir =
|
|
30351
|
+
const hostdDir = join10(this.opts.homeDir, ".switchroom", "hostd");
|
|
29970
30352
|
await mkdir(hostdDir, { recursive: true });
|
|
29971
30353
|
await chmod(hostdDir, 493).catch(() => {
|
|
29972
30354
|
return;
|
|
@@ -29981,13 +30363,13 @@ class HostdServer {
|
|
|
29981
30363
|
}
|
|
29982
30364
|
try {
|
|
29983
30365
|
for (const name of agentNames) {
|
|
29984
|
-
const dir =
|
|
29985
|
-
const sockPath =
|
|
30366
|
+
const dir = join10(hostdDir, name);
|
|
30367
|
+
const sockPath = join10(dir, "sock");
|
|
29986
30368
|
await mkdir(dir, { recursive: true });
|
|
29987
30369
|
await chmod(dir, 493).catch(() => {
|
|
29988
30370
|
return;
|
|
29989
30371
|
});
|
|
29990
|
-
if (
|
|
30372
|
+
if (existsSync10(sockPath))
|
|
29991
30373
|
await unlink(sockPath).catch(() => {
|
|
29992
30374
|
return;
|
|
29993
30375
|
});
|
|
@@ -30018,13 +30400,13 @@ class HostdServer {
|
|
|
30018
30400
|
process.stderr.write(`hostd: SWITCHROOM_HOSTD_OPERATOR_UID='${opUidStr}' is not a positive integer; skipping operator listener
|
|
30019
30401
|
`);
|
|
30020
30402
|
} else {
|
|
30021
|
-
const dir =
|
|
30022
|
-
const sockPath =
|
|
30403
|
+
const dir = join10(hostdDir, "operator");
|
|
30404
|
+
const sockPath = join10(dir, "sock");
|
|
30023
30405
|
await mkdir(dir, { recursive: true });
|
|
30024
30406
|
await chmod(dir, 493).catch(() => {
|
|
30025
30407
|
return;
|
|
30026
30408
|
});
|
|
30027
|
-
if (
|
|
30409
|
+
if (existsSync10(sockPath))
|
|
30028
30410
|
await unlink(sockPath).catch(() => {
|
|
30029
30411
|
return;
|
|
30030
30412
|
});
|
|
@@ -30060,11 +30442,11 @@ class HostdServer {
|
|
|
30060
30442
|
}
|
|
30061
30443
|
async reconcileOrphanedFleetMutations() {
|
|
30062
30444
|
const path2 = this.auditLogPath();
|
|
30063
|
-
if (!
|
|
30445
|
+
if (!existsSync10(path2))
|
|
30064
30446
|
return;
|
|
30065
30447
|
let raw;
|
|
30066
30448
|
try {
|
|
30067
|
-
raw =
|
|
30449
|
+
raw = readFileSync8(path2, "utf-8");
|
|
30068
30450
|
} catch {
|
|
30069
30451
|
return;
|
|
30070
30452
|
}
|
|
@@ -30394,10 +30776,10 @@ class HostdServer {
|
|
|
30394
30776
|
missingApplyAssets() {
|
|
30395
30777
|
const root = this.opts.applyAssetsRoot ?? resolve8(import.meta.dirname, "../..");
|
|
30396
30778
|
return [
|
|
30397
|
-
|
|
30398
|
-
|
|
30399
|
-
|
|
30400
|
-
].filter((p) => !
|
|
30779
|
+
join10(root, "profiles"),
|
|
30780
|
+
join10(root, "profiles", "default"),
|
|
30781
|
+
join10(root, "vendor", "hindsight-memory")
|
|
30782
|
+
].filter((p) => !existsSync10(p));
|
|
30401
30783
|
}
|
|
30402
30784
|
applyAssetPreflight(request_id, started) {
|
|
30403
30785
|
const missing = this.missingApplyAssets();
|
|
@@ -30561,19 +30943,19 @@ class HostdServer {
|
|
|
30561
30943
|
return entry;
|
|
30562
30944
|
}
|
|
30563
30945
|
hostdDirPath() {
|
|
30564
|
-
return
|
|
30946
|
+
return join10(this.opts.homeDir, ".switchroom", "hostd");
|
|
30565
30947
|
}
|
|
30566
30948
|
hostdDirHostPath() {
|
|
30567
30949
|
const hostHome = this.opts.hostHomeDir ?? process.env.SWITCHROOM_HOST_HOME?.trim() ?? this.opts.homeDir;
|
|
30568
|
-
return
|
|
30950
|
+
return join10(hostHome, ".switchroom", "hostd");
|
|
30569
30951
|
}
|
|
30570
30952
|
async beginSelfBump(req, caller, started) {
|
|
30571
30953
|
const ownVersion = this.opts.selfVersion ?? SWITCHROOM_VERSION;
|
|
30572
|
-
const composePath =
|
|
30573
|
-
if (!
|
|
30954
|
+
const composePath = join10(this.hostdDirPath(), "docker-compose.yml");
|
|
30955
|
+
if (!existsSync10(composePath)) {
|
|
30574
30956
|
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
30957
|
}
|
|
30576
|
-
const before =
|
|
30958
|
+
const before = readFileSync8(composePath, "utf8");
|
|
30577
30959
|
const bumped = bumpHostdComposeImageTag(before, req.args.pin);
|
|
30578
30960
|
if (!bumped) {
|
|
30579
30961
|
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 +30976,10 @@ class HostdServer {
|
|
|
30594
30976
|
try {
|
|
30595
30977
|
const baks = readdirSync3(this.hostdDirPath()).filter((f) => f.startsWith("docker-compose.yml.bak-selfbump-")).sort();
|
|
30596
30978
|
for (const f of baks.slice(0, Math.max(0, baks.length - 4))) {
|
|
30597
|
-
|
|
30979
|
+
unlinkSync2(join10(this.hostdDirPath(), f));
|
|
30598
30980
|
}
|
|
30599
30981
|
} catch {}
|
|
30600
|
-
const markerPath =
|
|
30982
|
+
const markerPath = join10(this.hostdDirPath(), SELF_BUMP_MARKER_FILENAME);
|
|
30601
30983
|
const marker = {
|
|
30602
30984
|
v: 1,
|
|
30603
30985
|
request_id: req.request_id,
|
|
@@ -30610,8 +30992,8 @@ class HostdServer {
|
|
|
30610
30992
|
prior_hostd_version: ownVersion
|
|
30611
30993
|
};
|
|
30612
30994
|
copyFileSync2(composePath, bakPath);
|
|
30613
|
-
|
|
30614
|
-
|
|
30995
|
+
writeFileSync5(composePath, bumped.yaml, "utf8");
|
|
30996
|
+
writeFileSync5(markerPath, encodePendingRolloutMarker(marker), {
|
|
30615
30997
|
mode: 384
|
|
30616
30998
|
});
|
|
30617
30999
|
const entry = {
|
|
@@ -30636,7 +31018,7 @@ class HostdServer {
|
|
|
30636
31018
|
const rollbackBump = () => {
|
|
30637
31019
|
try {
|
|
30638
31020
|
copyFileSync2(bakPath, composePath);
|
|
30639
|
-
|
|
31021
|
+
unlinkSync2(markerPath);
|
|
30640
31022
|
} catch (e) {
|
|
30641
31023
|
process.stderr.write(`hostd: self-bump rollback failed (compose/marker may need manual ` + `restore from ${bakPath}): ${e.message}
|
|
30642
31024
|
`);
|
|
@@ -30679,7 +31061,7 @@ class HostdServer {
|
|
|
30679
31061
|
rollbackBump();
|
|
30680
31062
|
entry.result = "error";
|
|
30681
31063
|
entry.finished_at = Date.now();
|
|
30682
|
-
entry.error = `self-bump helper failed (container exit ${Number.isFinite(code) ? code : "unknown"}). ` + `See ${
|
|
31064
|
+
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
31065
|
if (this.fleetMutationInFlight && this.fleetMutationInFlight.request_id === entry.request_id) {
|
|
30684
31066
|
this.fleetMutationInFlight = null;
|
|
30685
31067
|
}
|
|
@@ -30703,15 +31085,15 @@ class HostdServer {
|
|
|
30703
31085
|
};
|
|
30704
31086
|
}
|
|
30705
31087
|
async resumePendingSelfBumpRollout() {
|
|
30706
|
-
const markerPath =
|
|
30707
|
-
if (!
|
|
31088
|
+
const markerPath = join10(this.hostdDirPath(), SELF_BUMP_MARKER_FILENAME);
|
|
31089
|
+
if (!existsSync10(markerPath))
|
|
30708
31090
|
return;
|
|
30709
31091
|
let raw;
|
|
30710
31092
|
try {
|
|
30711
|
-
raw =
|
|
31093
|
+
raw = readFileSync8(markerPath, "utf8");
|
|
30712
31094
|
} finally {
|
|
30713
31095
|
try {
|
|
30714
|
-
|
|
31096
|
+
unlinkSync2(markerPath);
|
|
30715
31097
|
} catch {}
|
|
30716
31098
|
}
|
|
30717
31099
|
const marker = parsePendingRolloutMarker(raw);
|
|
@@ -30748,7 +31130,7 @@ class HostdServer {
|
|
|
30748
31130
|
return;
|
|
30749
31131
|
}
|
|
30750
31132
|
if (needsSelfBump(ownVersion, marker.pin)) {
|
|
30751
|
-
await failResume(`hostd is still on v${ownVersion} after the self-bump to ` + `${marker.pin} (helper failed? see ` + `${
|
|
31133
|
+
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
31134
|
return;
|
|
30753
31135
|
}
|
|
30754
31136
|
process.stderr.write(`hostd: resuming rollout ${marker.request_id} → ${marker.pin} after ` + `self-bump (v${marker.prior_hostd_version} → v${ownVersion})
|
|
@@ -30842,7 +31224,7 @@ class HostdServer {
|
|
|
30842
31224
|
if (caller.kind === "agent" && this.opts.config.agents[caller.name]?.admin !== true) {
|
|
30843
31225
|
let beforeContent;
|
|
30844
31226
|
try {
|
|
30845
|
-
beforeContent =
|
|
31227
|
+
beforeContent = readFileSync8(configPath, "utf-8");
|
|
30846
31228
|
} catch {
|
|
30847
31229
|
beforeContent = "";
|
|
30848
31230
|
}
|
|
@@ -30946,7 +31328,7 @@ class HostdServer {
|
|
|
30946
31328
|
try {
|
|
30947
31329
|
let snapshot;
|
|
30948
31330
|
try {
|
|
30949
|
-
snapshot =
|
|
31331
|
+
snapshot = readFileSync8(configPath, "utf-8");
|
|
30950
31332
|
} catch (e) {
|
|
30951
31333
|
await approval.finalize({
|
|
30952
31334
|
outcome: "reconcile_failed_rolled_back",
|
|
@@ -31198,8 +31580,8 @@ ${output.recovery.stderr}` : "";
|
|
|
31198
31580
|
if (this.opts.imageRefsForDigests)
|
|
31199
31581
|
return this.opts.imageRefsForDigests();
|
|
31200
31582
|
try {
|
|
31201
|
-
const composePath =
|
|
31202
|
-
if (!
|
|
31583
|
+
const composePath = join10(this.opts.bindRoot ?? this.opts.homeDir, ".switchroom", "compose", "docker-compose.yml");
|
|
31584
|
+
if (!existsSync10(composePath))
|
|
31203
31585
|
return [];
|
|
31204
31586
|
const r = spawnSync3("docker", [
|
|
31205
31587
|
"compose",
|
|
@@ -31307,6 +31689,9 @@ ${output.recovery.stderr}` : "";
|
|
|
31307
31689
|
target: entry.pin ?? "",
|
|
31308
31690
|
rolled: entry.rolled,
|
|
31309
31691
|
terminal: entry.result === "completed" ? "completed" : "error",
|
|
31692
|
+
requestId: entry.request_id,
|
|
31693
|
+
...entry.prior_pin ? { fromVersion: entry.prior_pin } : {},
|
|
31694
|
+
...entry.finished_at !== null ? { elapsedMs: entry.finished_at - entry.started_at } : {},
|
|
31310
31695
|
...entry.failed_step ? { failedStep: entry.failed_step } : {},
|
|
31311
31696
|
...entry.failed_agent ? { failedAgent: entry.failed_agent } : {},
|
|
31312
31697
|
...entry.got !== undefined ? { got: entry.got } : {}
|
|
@@ -31335,10 +31720,10 @@ ${output.recovery.stderr}` : "";
|
|
|
31335
31720
|
}
|
|
31336
31721
|
rolloutRowExistsInLog(targetRequestId) {
|
|
31337
31722
|
const path2 = this.auditLogPath();
|
|
31338
|
-
if (!
|
|
31723
|
+
if (!existsSync10(path2))
|
|
31339
31724
|
return false;
|
|
31340
31725
|
try {
|
|
31341
|
-
const raw =
|
|
31726
|
+
const raw = readFileSync8(path2, "utf-8");
|
|
31342
31727
|
return latestRolloutRowForRequest(raw, targetRequestId) !== null;
|
|
31343
31728
|
} catch {
|
|
31344
31729
|
return false;
|
|
@@ -31346,11 +31731,11 @@ ${output.recovery.stderr}` : "";
|
|
|
31346
31731
|
}
|
|
31347
31732
|
rolloutStatusFromLog(responseRequestId, targetRequestId, started) {
|
|
31348
31733
|
const path2 = this.auditLogPath();
|
|
31349
|
-
if (!
|
|
31734
|
+
if (!existsSync10(path2))
|
|
31350
31735
|
return null;
|
|
31351
31736
|
let raw;
|
|
31352
31737
|
try {
|
|
31353
|
-
raw =
|
|
31738
|
+
raw = readFileSync8(path2, "utf-8");
|
|
31354
31739
|
} catch {
|
|
31355
31740
|
return null;
|
|
31356
31741
|
}
|
|
@@ -31429,7 +31814,7 @@ ${output.recovery.stderr}` : "";
|
|
|
31429
31814
|
}
|
|
31430
31815
|
}
|
|
31431
31816
|
auditLogPath() {
|
|
31432
|
-
return this.opts.auditLogPath ??
|
|
31817
|
+
return this.opts.auditLogPath ?? join10(this.opts.homeDir, ".switchroom", "host-control-audit.log");
|
|
31433
31818
|
}
|
|
31434
31819
|
appendAuditRow(row) {
|
|
31435
31820
|
const path2 = this.auditLogPath();
|
|
@@ -31682,7 +32067,7 @@ function isSafeExecArgvElement(s) {
|
|
|
31682
32067
|
}
|
|
31683
32068
|
|
|
31684
32069
|
// src/host-control/approval-gateway.ts
|
|
31685
|
-
import { connect } from "node:net";
|
|
32070
|
+
import { connect as connect2 } from "node:net";
|
|
31686
32071
|
|
|
31687
32072
|
class SocketApprovalGateway {
|
|
31688
32073
|
opts;
|
|
@@ -31700,7 +32085,7 @@ class SocketApprovalGateway {
|
|
|
31700
32085
|
};
|
|
31701
32086
|
}
|
|
31702
32087
|
return await new Promise((resolve9) => {
|
|
31703
|
-
const client2 =
|
|
32088
|
+
const client2 = connect2({ path: sockPath });
|
|
31704
32089
|
let buffer = "";
|
|
31705
32090
|
let resolved = false;
|
|
31706
32091
|
const log = this.opts.log ?? (() => {});
|
|
@@ -31804,7 +32189,7 @@ class SocketApprovalGateway {
|
|
|
31804
32189
|
}
|
|
31805
32190
|
|
|
31806
32191
|
// src/host-control/rollout-relay.ts
|
|
31807
|
-
import { connect as
|
|
32192
|
+
import { connect as connect3 } from "node:net";
|
|
31808
32193
|
|
|
31809
32194
|
class SocketRolloutRelay {
|
|
31810
32195
|
opts;
|
|
@@ -31825,7 +32210,7 @@ class SocketRolloutRelay {
|
|
|
31825
32210
|
return;
|
|
31826
32211
|
}
|
|
31827
32212
|
try {
|
|
31828
|
-
const client2 =
|
|
32213
|
+
const client2 = connect3({ path: sockPath });
|
|
31829
32214
|
client2.setTimeout(5000);
|
|
31830
32215
|
const done = () => {
|
|
31831
32216
|
try {
|
|
@@ -31863,7 +32248,7 @@ class SocketRolloutRelay {
|
|
|
31863
32248
|
}
|
|
31864
32249
|
|
|
31865
32250
|
// src/host-control/rollout-narration-relay.ts
|
|
31866
|
-
import { connect as
|
|
32251
|
+
import { connect as connect4 } from "node:net";
|
|
31867
32252
|
var DEFAULT_POST_REPLY_TIMEOUT_MS = 5000;
|
|
31868
32253
|
|
|
31869
32254
|
class SocketRolloutNarrationRelay {
|
|
@@ -31897,7 +32282,7 @@ class SocketRolloutNarrationRelay {
|
|
|
31897
32282
|
};
|
|
31898
32283
|
let client2;
|
|
31899
32284
|
try {
|
|
31900
|
-
client2 =
|
|
32285
|
+
client2 = connect4({ path: sockPath });
|
|
31901
32286
|
} catch (e) {
|
|
31902
32287
|
log(`narration post connect threw: ${e.message}`);
|
|
31903
32288
|
resolve9(null);
|
|
@@ -31967,7 +32352,7 @@ class SocketRolloutNarrationRelay {
|
|
|
31967
32352
|
if (sockPath === null)
|
|
31968
32353
|
return;
|
|
31969
32354
|
try {
|
|
31970
|
-
const client2 =
|
|
32355
|
+
const client2 = connect4({ path: sockPath });
|
|
31971
32356
|
client2.setTimeout(5000);
|
|
31972
32357
|
const done = () => {
|
|
31973
32358
|
try {
|
|
@@ -32014,6 +32399,47 @@ class LogTailRolloutNarrator {
|
|
|
32014
32399
|
this.relay = relay;
|
|
32015
32400
|
this.opts = opts;
|
|
32016
32401
|
}
|
|
32402
|
+
now() {
|
|
32403
|
+
return this.opts.now?.() ?? Date.now();
|
|
32404
|
+
}
|
|
32405
|
+
static upsertAgent(st, name, patch) {
|
|
32406
|
+
const existing = st.agents.find((a) => a.name === name);
|
|
32407
|
+
if (existing)
|
|
32408
|
+
Object.assign(existing, patch);
|
|
32409
|
+
else
|
|
32410
|
+
st.agents.push({ name, status: "pending", ...patch });
|
|
32411
|
+
}
|
|
32412
|
+
trackAgentProgress(st, phase) {
|
|
32413
|
+
const now = this.now();
|
|
32414
|
+
switch (phase.phase) {
|
|
32415
|
+
case "canary-start":
|
|
32416
|
+
case "agent-start":
|
|
32417
|
+
if (phase.agent) {
|
|
32418
|
+
LogTailRolloutNarrator.upsertAgent(st, phase.agent, {
|
|
32419
|
+
status: "running",
|
|
32420
|
+
startedAtMs: now,
|
|
32421
|
+
...phase.phase === "canary-start" ? { canary: true } : {}
|
|
32422
|
+
});
|
|
32423
|
+
}
|
|
32424
|
+
break;
|
|
32425
|
+
case "canary-pass":
|
|
32426
|
+
case "agent-done":
|
|
32427
|
+
case "canary-fail": {
|
|
32428
|
+
if (!phase.agent)
|
|
32429
|
+
break;
|
|
32430
|
+
const a = st.agents.find((x) => x.name === phase.agent);
|
|
32431
|
+
const durationMs = a?.startedAtMs !== undefined ? now - a.startedAtMs : undefined;
|
|
32432
|
+
LogTailRolloutNarrator.upsertAgent(st, phase.agent, {
|
|
32433
|
+
status: phase.phase === "canary-fail" ? "failed" : "done",
|
|
32434
|
+
...durationMs !== undefined ? { durationMs } : {},
|
|
32435
|
+
...phase.phase !== "agent-done" ? { canary: true } : {}
|
|
32436
|
+
});
|
|
32437
|
+
break;
|
|
32438
|
+
}
|
|
32439
|
+
default:
|
|
32440
|
+
break;
|
|
32441
|
+
}
|
|
32442
|
+
}
|
|
32017
32443
|
static seqFor(phase) {
|
|
32018
32444
|
switch (phase.phase) {
|
|
32019
32445
|
case "self-bump":
|
|
@@ -32032,6 +32458,8 @@ class LogTailRolloutNarrator {
|
|
|
32032
32458
|
return 2 * (phase.n ?? 1) + 0;
|
|
32033
32459
|
case "agent-done":
|
|
32034
32460
|
return 2 * (phase.n ?? 1) + 1;
|
|
32461
|
+
case "web-refresh":
|
|
32462
|
+
return Number.MAX_SAFE_INTEGER - 2;
|
|
32035
32463
|
case "hostd-web-deferred":
|
|
32036
32464
|
return Number.MAX_SAFE_INTEGER - 1;
|
|
32037
32465
|
default:
|
|
@@ -32047,6 +32475,7 @@ class LogTailRolloutNarrator {
|
|
|
32047
32475
|
if (seq < st.lastAppliedSeq)
|
|
32048
32476
|
return;
|
|
32049
32477
|
st.lastAppliedSeq = seq;
|
|
32478
|
+
this.trackAgentProgress(st, phase);
|
|
32050
32479
|
st.render = {
|
|
32051
32480
|
...st.render,
|
|
32052
32481
|
target: phase.target,
|
|
@@ -32054,7 +32483,11 @@ class LogTailRolloutNarrator {
|
|
|
32054
32483
|
...phase.n !== undefined ? { n: phase.n } : {},
|
|
32055
32484
|
...phase.m !== undefined ? { m: phase.m } : {},
|
|
32056
32485
|
...phase.agent !== undefined ? { agent: phase.agent } : {},
|
|
32057
|
-
rolled: entry.rolled
|
|
32486
|
+
rolled: entry.rolled,
|
|
32487
|
+
agents: st.agents,
|
|
32488
|
+
requestId: entry.request_id,
|
|
32489
|
+
...entry.prior_pin ? { fromVersion: entry.prior_pin } : {},
|
|
32490
|
+
startedAtMs: entry.started_at
|
|
32058
32491
|
};
|
|
32059
32492
|
this.scheduleRenderOrPost(entry.request_id);
|
|
32060
32493
|
} catch (e) {
|
|
@@ -32064,13 +32497,29 @@ class LogTailRolloutNarrator {
|
|
|
32064
32497
|
onTerminal(entry) {
|
|
32065
32498
|
try {
|
|
32066
32499
|
const st = this.ensureState(entry);
|
|
32500
|
+
const rolled = new Set(entry.rolled ?? []);
|
|
32501
|
+
for (const a of st.agents) {
|
|
32502
|
+
if (entry.failed_agent && a.name === entry.failed_agent) {
|
|
32503
|
+
a.status = "failed";
|
|
32504
|
+
} else if (rolled.has(a.name)) {
|
|
32505
|
+
a.status = "done";
|
|
32506
|
+
} else if (a.status === "running") {
|
|
32507
|
+
a.status = "pending";
|
|
32508
|
+
}
|
|
32509
|
+
}
|
|
32067
32510
|
st.render = {
|
|
32068
32511
|
target: entry.pin ?? st.render.target,
|
|
32069
32512
|
rolled: entry.rolled ?? st.render.rolled,
|
|
32070
32513
|
terminal: entry.result === "completed" ? "completed" : "error",
|
|
32071
32514
|
...entry.failed_step ? { failedStep: entry.failed_step } : {},
|
|
32072
32515
|
...entry.failed_agent ? { failedAgent: entry.failed_agent } : {},
|
|
32073
|
-
...entry.got !== undefined ? { got: entry.got } : {}
|
|
32516
|
+
...entry.got !== undefined ? { got: entry.got } : {},
|
|
32517
|
+
...st.agents.length > 0 ? { agents: st.agents } : {},
|
|
32518
|
+
...st.render.m !== undefined ? { m: st.render.m } : {},
|
|
32519
|
+
requestId: entry.request_id,
|
|
32520
|
+
...entry.prior_pin ?? st.render.fromVersion ? { fromVersion: entry.prior_pin ?? st.render.fromVersion } : {},
|
|
32521
|
+
elapsedMs: (entry.finished_at ?? this.now()) - entry.started_at,
|
|
32522
|
+
deferred: false
|
|
32074
32523
|
};
|
|
32075
32524
|
st.frozen = true;
|
|
32076
32525
|
if (st.timer) {
|
|
@@ -32095,6 +32544,7 @@ class LogTailRolloutNarrator {
|
|
|
32095
32544
|
postAttempts: 0,
|
|
32096
32545
|
postFailed: false,
|
|
32097
32546
|
render: { target: entry.pin ?? "" },
|
|
32547
|
+
agents: [],
|
|
32098
32548
|
frozen: false,
|
|
32099
32549
|
timer: null,
|
|
32100
32550
|
pendingEditAfterPost: false
|
|
@@ -32133,7 +32583,7 @@ class LogTailRolloutNarrator {
|
|
|
32133
32583
|
return false;
|
|
32134
32584
|
st.posting = true;
|
|
32135
32585
|
st.postAttempts += 1;
|
|
32136
|
-
const text = renderRolloutStatus(st.render);
|
|
32586
|
+
const text = renderRolloutStatus({ ...st.render, nowMs: this.now() });
|
|
32137
32587
|
this.relay.post({ requestId, agentName: st.agentName, text }).then((mid) => {
|
|
32138
32588
|
st.posting = false;
|
|
32139
32589
|
if (mid === null) {
|
|
@@ -32185,7 +32635,7 @@ class LogTailRolloutNarrator {
|
|
|
32185
32635
|
this.tryPost(requestId, true);
|
|
32186
32636
|
return;
|
|
32187
32637
|
}
|
|
32188
|
-
const text = renderRolloutStatus(st.render);
|
|
32638
|
+
const text = renderRolloutStatus({ ...st.render, nowMs: this.now() });
|
|
32189
32639
|
this.relay.edit({
|
|
32190
32640
|
requestId,
|
|
32191
32641
|
agentName: st.agentName,
|
|
@@ -32443,8 +32893,60 @@ function makeRestart(switchroomBin) {
|
|
|
32443
32893
|
|
|
32444
32894
|
// src/host-control/main.ts
|
|
32445
32895
|
import { appendFile as appendFile2 } from "node:fs/promises";
|
|
32896
|
+
async function loadConfigResilient() {
|
|
32897
|
+
try {
|
|
32898
|
+
const config = loadConfig();
|
|
32899
|
+
clearStaleDegradedMarker(homedir6(), (m) => process.stderr.write(`hostd: ${m}
|
|
32900
|
+
`));
|
|
32901
|
+
return config;
|
|
32902
|
+
} catch (err2) {
|
|
32903
|
+
if (!(err2 instanceof ConfigError))
|
|
32904
|
+
throw err2;
|
|
32905
|
+
let configPath;
|
|
32906
|
+
try {
|
|
32907
|
+
configPath = findConfigFile();
|
|
32908
|
+
} catch {
|
|
32909
|
+
configPath = undefined;
|
|
32910
|
+
}
|
|
32911
|
+
const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ?? join11(homedir6(), ".switchroom", "agents");
|
|
32912
|
+
const listGatewayCandidates = () => {
|
|
32913
|
+
const out = [];
|
|
32914
|
+
try {
|
|
32915
|
+
for (const name of readdirSync4(agentsDir).sort()) {
|
|
32916
|
+
const sock = resolve9(agentsDir, name, "telegram", "gateway.sock");
|
|
32917
|
+
if (existsSync11(sock))
|
|
32918
|
+
out.push({ agent: name, sock });
|
|
32919
|
+
}
|
|
32920
|
+
} catch {}
|
|
32921
|
+
return out;
|
|
32922
|
+
};
|
|
32923
|
+
const log = (m) => {
|
|
32924
|
+
process.stderr.write(`hostd: ${m}
|
|
32925
|
+
`);
|
|
32926
|
+
};
|
|
32927
|
+
const notify = (text) => {
|
|
32928
|
+
const candidates = listGatewayCandidates();
|
|
32929
|
+
if (candidates.length === 0) {
|
|
32930
|
+
log("config-degraded: no gateway socket paths found; relying on marker file");
|
|
32931
|
+
return;
|
|
32932
|
+
}
|
|
32933
|
+
postOperatorNoticeViaGateways(candidates, text, log).then((agent) => {
|
|
32934
|
+
if (agent !== null)
|
|
32935
|
+
log(`config-degraded: operator DM relayed via ${agent}`);
|
|
32936
|
+
});
|
|
32937
|
+
};
|
|
32938
|
+
return waitForConfigRecovery({
|
|
32939
|
+
initialError: err2,
|
|
32940
|
+
homeDir: homedir6(),
|
|
32941
|
+
loadFn: () => loadConfig(),
|
|
32942
|
+
configPath,
|
|
32943
|
+
notify,
|
|
32944
|
+
log
|
|
32945
|
+
});
|
|
32946
|
+
}
|
|
32947
|
+
}
|
|
32446
32948
|
async function main() {
|
|
32447
|
-
const config =
|
|
32949
|
+
const config = await loadConfigResilient();
|
|
32448
32950
|
if (config.host_control?.enabled !== true) {
|
|
32449
32951
|
process.stderr.write(`hostd: refusing to start — host_control.enabled is not true in switchroom.yaml
|
|
32450
32952
|
`);
|
|
@@ -32459,10 +32961,10 @@ async function main() {
|
|
|
32459
32961
|
`);
|
|
32460
32962
|
process.exit(2);
|
|
32461
32963
|
}
|
|
32462
|
-
const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ??
|
|
32964
|
+
const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ?? join11(homedir6(), ".switchroom", "agents");
|
|
32463
32965
|
const resolveGatewaySocket = (agentName) => {
|
|
32464
32966
|
const sock = resolve9(agentsDir, agentName, "telegram", "gateway.sock");
|
|
32465
|
-
return
|
|
32967
|
+
return existsSync11(sock) ? sock : null;
|
|
32466
32968
|
};
|
|
32467
32969
|
const approvalGateway = new SocketApprovalGateway({
|
|
32468
32970
|
resolveGatewaySocket,
|
|
@@ -32505,7 +33007,7 @@ async function main() {
|
|
|
32505
33007
|
let releaseWatcher = null;
|
|
32506
33008
|
const autoRel = config.host_control?.auto_release_check;
|
|
32507
33009
|
if (autoRel?.enabled === true) {
|
|
32508
|
-
const eventsLog =
|
|
33010
|
+
const eventsLog = join11(homedir6(), ".switchroom", "release-watcher-events.jsonl");
|
|
32509
33011
|
releaseWatcher = new ReleaseWatcher({
|
|
32510
33012
|
intervalMs: autoRel.interval_minutes * 60000,
|
|
32511
33013
|
checkFn: makeReleaseCheck({
|