switchroom 0.12.0 → 0.12.2
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/README.md +26 -11
- package/dist/auth-broker/index.js +1 -1
- package/dist/cli/skill-validate-pretool.mjs +7209 -0
- package/dist/cli/switchroom.js +891 -434
- package/dist/vault/broker/server.js +31 -22
- package/package.json +3 -2
- package/profiles/_shared/agent-self-service.md.hbs +1 -1
- package/skills/skill-creator/SKILL.md +52 -0
- package/telegram-plugin/auth-snapshot-format.ts +5 -5
- package/telegram-plugin/dist/gateway/gateway.js +62 -8
- package/telegram-plugin/gateway/access-validator.test.ts +8 -8
- package/telegram-plugin/gateway/access-validator.ts +1 -1
- package/telegram-plugin/gateway/boot-probes.ts +43 -3
- package/telegram-plugin/gateway/gateway.ts +72 -0
- package/telegram-plugin/recent-outbound-dedup.ts +1 -1
- package/telegram-plugin/registry/turns-schema.ts +1 -1
- package/telegram-plugin/tests/auth-add-flow.test.ts +1 -1
- package/telegram-plugin/tests/auth-command-format2.test.ts +4 -4
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +17 -17
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +10 -10
- package/telegram-plugin/tests/boot-probes.test.ts +37 -2
- package/telegram-plugin/tests/fixtures/service-log-current-claude-code.bin +1 -1
- package/telegram-plugin/tests/fleet-state.test.ts +3 -2
- package/telegram-plugin/tests/secret-detect-audit.test.ts +1 -1
- package/telegram-plugin/tests/secret-detect-pipeline.test.ts +7 -6
- package/telegram-plugin/tests/secret-detect-suppressor-no-silent-allow.test.ts +6 -5
- package/telegram-plugin/tests/secret-detect.test.ts +8 -8
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +8 -8
- package/telegram-plugin/tests/vault-request-access-tool.test.ts +51 -0
package/dist/cli/switchroom.js
CHANGED
|
@@ -21988,6 +21988,7 @@ __export(exports_client, {
|
|
|
21988
21988
|
listGrantsViaBroker: () => listGrantsViaBroker,
|
|
21989
21989
|
getViaBrokerStructured: () => getViaBrokerStructured,
|
|
21990
21990
|
getViaBroker: () => getViaBroker,
|
|
21991
|
+
defaultBrokerSocketPath: () => defaultBrokerSocketPath,
|
|
21991
21992
|
createBrokerClient: () => createBrokerClient,
|
|
21992
21993
|
brokerIsComposeManaged: () => brokerIsComposeManaged,
|
|
21993
21994
|
VaultTokenRejectedError: () => VaultTokenRejectedError
|
|
@@ -22252,7 +22253,7 @@ async function lockViaBroker(opts) {
|
|
|
22252
22253
|
async function unlockViaBroker(passphrase, opts) {
|
|
22253
22254
|
const dataSocketPath = resolveBrokerSocketPath(opts);
|
|
22254
22255
|
const unlockSocketPath = unlockSocketFor(dataSocketPath);
|
|
22255
|
-
const timeoutMs = opts?.timeoutMs ??
|
|
22256
|
+
const timeoutMs = opts?.timeoutMs ?? UNLOCK_TIMEOUT_MS;
|
|
22256
22257
|
return new Promise((resolve8) => {
|
|
22257
22258
|
let settled = false;
|
|
22258
22259
|
const settle = (val) => {
|
|
@@ -22354,7 +22355,7 @@ async function revokeGrantViaBroker(id, opts) {
|
|
|
22354
22355
|
return { kind: "error", msg: resp.msg };
|
|
22355
22356
|
return { kind: "error", msg: "unexpected broker response" };
|
|
22356
22357
|
}
|
|
22357
|
-
var DEFAULT_TIMEOUT_MS = 2000, LEGACY_SOCKET_PATH, OPERATOR_SOCKET_PATH, VaultTokenRejectedError;
|
|
22358
|
+
var DEFAULT_TIMEOUT_MS = 2000, UNLOCK_TIMEOUT_MS = 30000, LEGACY_SOCKET_PATH, OPERATOR_SOCKET_PATH, VaultTokenRejectedError;
|
|
22358
22359
|
var init_client = __esm(() => {
|
|
22359
22360
|
init_protocol();
|
|
22360
22361
|
init_peercred();
|
|
@@ -23085,6 +23086,8 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
|
|
|
23085
23086
|
NPM_CONFIG_PREFIX: "/state/agent/home/.npm-global",
|
|
23086
23087
|
PIP_BREAK_SYSTEM_PACKAGES: "1",
|
|
23087
23088
|
PIP_USER: "1",
|
|
23089
|
+
DISABLE_AUTOUPDATER: "1",
|
|
23090
|
+
CLAUDE_CODE_ATTRIBUTION_HEADER: "0",
|
|
23088
23091
|
SWITCHROOM_AGENT_NAME: a.name,
|
|
23089
23092
|
SWITCHROOM_CONTAINER: "1",
|
|
23090
23093
|
SWITCHROOM_VAULT_BROKER_SOCK: `/run/switchroom/broker/sock`,
|
|
@@ -27070,6 +27073,151 @@ var init_via_claude = __esm(() => {
|
|
|
27070
27073
|
];
|
|
27071
27074
|
});
|
|
27072
27075
|
|
|
27076
|
+
// src/vault/broker/acl.ts
|
|
27077
|
+
function parseCronUnit(unitName) {
|
|
27078
|
+
const m = unitName.match(/^switchroom-([a-zA-Z0-9_-]+)-cron-(\d+)\.service$/);
|
|
27079
|
+
if (!m)
|
|
27080
|
+
return null;
|
|
27081
|
+
const agentName = m[1];
|
|
27082
|
+
const index = parseInt(m[2], 10);
|
|
27083
|
+
if (!agentName)
|
|
27084
|
+
return null;
|
|
27085
|
+
return { agentName, index };
|
|
27086
|
+
}
|
|
27087
|
+
function agentSlugFromPeer(peer) {
|
|
27088
|
+
if (peer.systemdUnit === null)
|
|
27089
|
+
return null;
|
|
27090
|
+
const parsed = parseCronUnit(peer.systemdUnit);
|
|
27091
|
+
return parsed?.agentName ?? null;
|
|
27092
|
+
}
|
|
27093
|
+
function checkEntryScope(scope, agentSlug) {
|
|
27094
|
+
if (scope === undefined || scope === null) {
|
|
27095
|
+
return { allow: true };
|
|
27096
|
+
}
|
|
27097
|
+
const deny = scope.deny ?? [];
|
|
27098
|
+
const allow = scope.allow ?? [];
|
|
27099
|
+
if (agentSlug !== null && deny.includes(agentSlug)) {
|
|
27100
|
+
return {
|
|
27101
|
+
allow: false,
|
|
27102
|
+
reason: `agent '${agentSlug}' is in the entry's deny list (scope-deny)`
|
|
27103
|
+
};
|
|
27104
|
+
}
|
|
27105
|
+
if (allow.length > 0) {
|
|
27106
|
+
if (agentSlug === null || !allow.includes(agentSlug)) {
|
|
27107
|
+
return {
|
|
27108
|
+
allow: false,
|
|
27109
|
+
reason: agentSlug === null ? "caller agent slug could not be determined; entry has a non-empty allow list (scope-allow)" : `agent '${agentSlug}' is not in the entry's allow list (scope-allow)`
|
|
27110
|
+
};
|
|
27111
|
+
}
|
|
27112
|
+
}
|
|
27113
|
+
return { allow: true };
|
|
27114
|
+
}
|
|
27115
|
+
function checkAcl(peer, config, key) {
|
|
27116
|
+
if (peer.systemdUnit !== null) {
|
|
27117
|
+
const parsed = parseCronUnit(peer.systemdUnit);
|
|
27118
|
+
if (parsed === null) {
|
|
27119
|
+
return {
|
|
27120
|
+
allow: false,
|
|
27121
|
+
reason: `systemd unit '${peer.systemdUnit}' does not match switchroom cron unit naming convention`
|
|
27122
|
+
};
|
|
27123
|
+
}
|
|
27124
|
+
const { agentName, index } = parsed;
|
|
27125
|
+
const agentConfig = config.agents?.[agentName];
|
|
27126
|
+
if (!agentConfig) {
|
|
27127
|
+
return { allow: false, reason: `agent '${agentName}' not found in config` };
|
|
27128
|
+
}
|
|
27129
|
+
const schedule = agentConfig.schedule ?? [];
|
|
27130
|
+
if (index >= schedule.length || index < 0) {
|
|
27131
|
+
return {
|
|
27132
|
+
allow: false,
|
|
27133
|
+
reason: `schedule index ${index} out of range for agent '${agentName}' (${schedule.length} entries)`
|
|
27134
|
+
};
|
|
27135
|
+
}
|
|
27136
|
+
const entry = schedule[index];
|
|
27137
|
+
const allowedKeys = entry.secrets ?? [];
|
|
27138
|
+
if (!allowedKeys.includes(key)) {
|
|
27139
|
+
return {
|
|
27140
|
+
allow: false,
|
|
27141
|
+
reason: `key '${key}' not in ACL for ${agentName}/schedule[${index}]`
|
|
27142
|
+
};
|
|
27143
|
+
}
|
|
27144
|
+
return { allow: true };
|
|
27145
|
+
}
|
|
27146
|
+
return {
|
|
27147
|
+
allow: false,
|
|
27148
|
+
reason: "caller is not a switchroom cron unit; use 'switchroom vault get --no-broker' for interactive access"
|
|
27149
|
+
};
|
|
27150
|
+
}
|
|
27151
|
+
function checkAclByAgent(config, agentName, key) {
|
|
27152
|
+
if (!agentName) {
|
|
27153
|
+
return { allow: false, reason: "agent name unresolved" };
|
|
27154
|
+
}
|
|
27155
|
+
const agentConfig = config.agents?.[agentName];
|
|
27156
|
+
if (!agentConfig) {
|
|
27157
|
+
return { allow: false, reason: `agent '${agentName}' not found in config` };
|
|
27158
|
+
}
|
|
27159
|
+
const googleSlot = parseGoogleAccountSlotKey(key);
|
|
27160
|
+
if (googleSlot !== null) {
|
|
27161
|
+
return checkGoogleAccountAcl(config, agentName, googleSlot.account, key);
|
|
27162
|
+
}
|
|
27163
|
+
const agentBot = agentConfig.bot_token;
|
|
27164
|
+
const botRef = agentBot && agentBot.length > 0 ? agentBot : config.telegram?.bot_token;
|
|
27165
|
+
if (typeof botRef === "string" && botRef.startsWith("vault:")) {
|
|
27166
|
+
const botKey = botRef.slice("vault:".length).split("#")[0];
|
|
27167
|
+
if (botKey.length > 0 && botKey === key) {
|
|
27168
|
+
return { allow: true };
|
|
27169
|
+
}
|
|
27170
|
+
}
|
|
27171
|
+
const schedule = agentConfig.schedule ?? [];
|
|
27172
|
+
if (schedule.length === 0) {
|
|
27173
|
+
return {
|
|
27174
|
+
allow: false,
|
|
27175
|
+
reason: `agent '${agentName}' has no schedule entries declaring 'secrets'; nothing is broker-accessible`
|
|
27176
|
+
};
|
|
27177
|
+
}
|
|
27178
|
+
for (const entry of schedule) {
|
|
27179
|
+
const allowed = entry?.secrets ?? [];
|
|
27180
|
+
if (allowed.includes(key)) {
|
|
27181
|
+
return { allow: true };
|
|
27182
|
+
}
|
|
27183
|
+
}
|
|
27184
|
+
return {
|
|
27185
|
+
allow: false,
|
|
27186
|
+
reason: `key '${key}' not in ACL for agent '${agentName}'`
|
|
27187
|
+
};
|
|
27188
|
+
}
|
|
27189
|
+
function parseGoogleAccountSlotKey(key) {
|
|
27190
|
+
const match = key.match(/^google:([^:]+):([a-z_]+)$/);
|
|
27191
|
+
if (!match)
|
|
27192
|
+
return null;
|
|
27193
|
+
return { account: match[1], field: match[2] };
|
|
27194
|
+
}
|
|
27195
|
+
function checkGoogleAccountAcl(config, agentName, account, key) {
|
|
27196
|
+
const accounts = config.google_accounts ?? {};
|
|
27197
|
+
const accountKey = account.toLowerCase();
|
|
27198
|
+
const accountEntry = accounts[accountKey] ?? accounts[account];
|
|
27199
|
+
if (!accountEntry) {
|
|
27200
|
+
return {
|
|
27201
|
+
allow: false,
|
|
27202
|
+
reason: `google_accounts['${account}'] not configured (key '${key}')`
|
|
27203
|
+
};
|
|
27204
|
+
}
|
|
27205
|
+
const enabled = accountEntry.enabled_for ?? [];
|
|
27206
|
+
if (enabled.length === 0) {
|
|
27207
|
+
return {
|
|
27208
|
+
allow: false,
|
|
27209
|
+
reason: `google_accounts['${account}'].enabled_for is empty (key '${key}')`
|
|
27210
|
+
};
|
|
27211
|
+
}
|
|
27212
|
+
if (!enabled.includes(agentName)) {
|
|
27213
|
+
return {
|
|
27214
|
+
allow: false,
|
|
27215
|
+
reason: `agent '${agentName}' not in google_accounts['${account}'].enabled_for (key '${key}')`
|
|
27216
|
+
};
|
|
27217
|
+
}
|
|
27218
|
+
return { allow: true };
|
|
27219
|
+
}
|
|
27220
|
+
|
|
27073
27221
|
// src/util/audit-hashchain.ts
|
|
27074
27222
|
import { createHash as createHash6 } from "node:crypto";
|
|
27075
27223
|
import { openSync as openSync7, readSync as readSync2, fstatSync as fstatSync2, closeSync as closeSync7 } from "node:fs";
|
|
@@ -27916,6 +28064,108 @@ var init_doctor_auth_broker = __esm(() => {
|
|
|
27916
28064
|
init_paths();
|
|
27917
28065
|
});
|
|
27918
28066
|
|
|
28067
|
+
// src/cli/doctor-hostd.ts
|
|
28068
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
28069
|
+
function realDockerInspect(ref, format) {
|
|
28070
|
+
try {
|
|
28071
|
+
const r = spawnSync6("docker", ["inspect", ref, "--format", format], {
|
|
28072
|
+
encoding: "utf-8",
|
|
28073
|
+
timeout: 5000
|
|
28074
|
+
});
|
|
28075
|
+
if (r.status !== 0)
|
|
28076
|
+
return null;
|
|
28077
|
+
const out = (r.stdout ?? "").trim();
|
|
28078
|
+
return out.length > 0 ? out : null;
|
|
28079
|
+
} catch {
|
|
28080
|
+
return null;
|
|
28081
|
+
}
|
|
28082
|
+
}
|
|
28083
|
+
function imageCreatedAt(container, dockerInspect) {
|
|
28084
|
+
const imageId = dockerInspect(container, "{{.Image}}");
|
|
28085
|
+
if (!imageId)
|
|
28086
|
+
return null;
|
|
28087
|
+
return dockerInspect(imageId, "{{.Created}}");
|
|
28088
|
+
}
|
|
28089
|
+
function runHostdChecks(config, deps = {}) {
|
|
28090
|
+
const dockerInspect = deps.dockerInspect ?? realDockerInspect;
|
|
28091
|
+
const enabled = config.host_control?.enabled === true;
|
|
28092
|
+
if (!enabled) {
|
|
28093
|
+
return [
|
|
28094
|
+
{
|
|
28095
|
+
name: "hostd: configured",
|
|
28096
|
+
status: "warn",
|
|
28097
|
+
detail: "host_control.enabled is not true \u2014 /restart and /update apply " + "use the legacy in-agent fallback, which fails on docker installs (#926)",
|
|
28098
|
+
fix: "Set `host_control: { enabled: true }` in switchroom.yaml and run `switchroom hostd install`"
|
|
28099
|
+
}
|
|
28100
|
+
];
|
|
28101
|
+
}
|
|
28102
|
+
const results = [
|
|
28103
|
+
{
|
|
28104
|
+
name: "hostd: configured",
|
|
28105
|
+
status: "ok",
|
|
28106
|
+
detail: "host_control.enabled: true"
|
|
28107
|
+
}
|
|
28108
|
+
];
|
|
28109
|
+
const status = dockerInspect(HOSTD_CONTAINER, "{{.State.Status}}");
|
|
28110
|
+
if (status !== "running") {
|
|
28111
|
+
results.push({
|
|
28112
|
+
name: "hostd: running",
|
|
28113
|
+
status: "fail",
|
|
28114
|
+
detail: status === null ? `${HOSTD_CONTAINER} container not found` : `${HOSTD_CONTAINER} is ${status}, not running`,
|
|
28115
|
+
fix: "Run `switchroom hostd install` on the host to (re)create the daemon"
|
|
28116
|
+
});
|
|
28117
|
+
return results;
|
|
28118
|
+
}
|
|
28119
|
+
results.push({
|
|
28120
|
+
name: "hostd: running",
|
|
28121
|
+
status: "ok",
|
|
28122
|
+
detail: `${HOSTD_CONTAINER} running`
|
|
28123
|
+
});
|
|
28124
|
+
const hostdCreated = imageCreatedAt(HOSTD_CONTAINER, dockerInspect);
|
|
28125
|
+
let agentCreated = null;
|
|
28126
|
+
for (const name of Object.keys(config.agents ?? {})) {
|
|
28127
|
+
agentCreated = imageCreatedAt(`switchroom-${name}`, dockerInspect);
|
|
28128
|
+
if (agentCreated)
|
|
28129
|
+
break;
|
|
28130
|
+
}
|
|
28131
|
+
if (!hostdCreated || !agentCreated) {
|
|
28132
|
+
results.push({
|
|
28133
|
+
name: "hostd: image drift",
|
|
28134
|
+
status: "ok",
|
|
28135
|
+
detail: "skipped \u2014 no running agent image to compare against"
|
|
28136
|
+
});
|
|
28137
|
+
return results;
|
|
28138
|
+
}
|
|
28139
|
+
const hostdMs = Date.parse(hostdCreated);
|
|
28140
|
+
const agentMs = Date.parse(agentCreated);
|
|
28141
|
+
if (Number.isNaN(hostdMs) || Number.isNaN(agentMs)) {
|
|
28142
|
+
results.push({
|
|
28143
|
+
name: "hostd: image drift",
|
|
28144
|
+
status: "ok",
|
|
28145
|
+
detail: "skipped \u2014 unparseable image timestamps"
|
|
28146
|
+
});
|
|
28147
|
+
return results;
|
|
28148
|
+
}
|
|
28149
|
+
const lagHours = (agentMs - hostdMs) / 3600000;
|
|
28150
|
+
if (lagHours > HOSTD_DRIFT_HOURS) {
|
|
28151
|
+
results.push({
|
|
28152
|
+
name: "hostd: image drift",
|
|
28153
|
+
status: "warn",
|
|
28154
|
+
detail: `hostd image is ~${lagHours.toFixed(1)}h older than the agent fleet ` + `image \u2014 likely a \`switchroom update --skip-images\` left it behind ` + `(that flag skips the refresh-hostd step)`,
|
|
28155
|
+
fix: "Run `switchroom hostd install` (or `switchroom update` without `--skip-images`)"
|
|
28156
|
+
});
|
|
28157
|
+
} else {
|
|
28158
|
+
results.push({
|
|
28159
|
+
name: "hostd: image drift",
|
|
28160
|
+
status: "ok",
|
|
28161
|
+
detail: `in sync with the agent fleet image (\u00b1${HOSTD_DRIFT_HOURS}h)`
|
|
28162
|
+
});
|
|
28163
|
+
}
|
|
28164
|
+
return results;
|
|
28165
|
+
}
|
|
28166
|
+
var HOSTD_CONTAINER = "switchroom-hostd", HOSTD_DRIFT_HOURS = 2;
|
|
28167
|
+
var init_doctor_hostd = () => {};
|
|
28168
|
+
|
|
27919
28169
|
// src/cli/doctor-drive.ts
|
|
27920
28170
|
import {
|
|
27921
28171
|
existsSync as realExistsSync,
|
|
@@ -28205,6 +28455,181 @@ function runCredentialsMigrationChecks(config, deps = {}) {
|
|
|
28205
28455
|
}
|
|
28206
28456
|
var init_doctor_credentials_migration = () => {};
|
|
28207
28457
|
|
|
28458
|
+
// src/cli/doctor-secret-access.ts
|
|
28459
|
+
import {
|
|
28460
|
+
accessSync,
|
|
28461
|
+
constants as fsConstants4,
|
|
28462
|
+
existsSync as existsSync46,
|
|
28463
|
+
realpathSync as realpathSync4,
|
|
28464
|
+
statSync as statSync20
|
|
28465
|
+
} from "node:fs";
|
|
28466
|
+
import { userInfo } from "node:os";
|
|
28467
|
+
function resolveVaultPath2(config) {
|
|
28468
|
+
return config.vault?.path ? config.vault.path.replace(/^~/, process.env.HOME ?? "") : resolveStatePath("vault.enc");
|
|
28469
|
+
}
|
|
28470
|
+
function defaultStatVault(path4) {
|
|
28471
|
+
if (!existsSync46(path4)) {
|
|
28472
|
+
return { exists: false, readable: false, uid: -1, mode: 0, realPath: path4 };
|
|
28473
|
+
}
|
|
28474
|
+
let real = path4;
|
|
28475
|
+
try {
|
|
28476
|
+
real = realpathSync4(path4);
|
|
28477
|
+
} catch {}
|
|
28478
|
+
let uid = -1;
|
|
28479
|
+
let mode = 0;
|
|
28480
|
+
try {
|
|
28481
|
+
const s = statSync20(real);
|
|
28482
|
+
uid = s.uid;
|
|
28483
|
+
mode = s.mode & 511;
|
|
28484
|
+
} catch {
|
|
28485
|
+
return { exists: true, readable: false, uid, mode, realPath: real };
|
|
28486
|
+
}
|
|
28487
|
+
let readable = false;
|
|
28488
|
+
try {
|
|
28489
|
+
accessSync(real, fsConstants4.R_OK);
|
|
28490
|
+
readable = true;
|
|
28491
|
+
} catch {
|
|
28492
|
+
readable = false;
|
|
28493
|
+
}
|
|
28494
|
+
return { exists: true, readable, uid, mode, realPath: real };
|
|
28495
|
+
}
|
|
28496
|
+
function collectVaultRefs2(value, out) {
|
|
28497
|
+
if (typeof value === "string") {
|
|
28498
|
+
if (value.startsWith("vault:")) {
|
|
28499
|
+
const key = value.slice("vault:".length).split("#")[0].trim();
|
|
28500
|
+
if (key)
|
|
28501
|
+
out.add(key);
|
|
28502
|
+
}
|
|
28503
|
+
return;
|
|
28504
|
+
}
|
|
28505
|
+
if (Array.isArray(value)) {
|
|
28506
|
+
for (const v of value)
|
|
28507
|
+
collectVaultRefs2(v, out);
|
|
28508
|
+
return;
|
|
28509
|
+
}
|
|
28510
|
+
if (value && typeof value === "object") {
|
|
28511
|
+
for (const v of Object.values(value)) {
|
|
28512
|
+
collectVaultRefs2(v, out);
|
|
28513
|
+
}
|
|
28514
|
+
}
|
|
28515
|
+
}
|
|
28516
|
+
function runSecretAccessChecks(config, deps = {}) {
|
|
28517
|
+
const results = [];
|
|
28518
|
+
const vaultPath = deps.vaultPath ?? resolveVaultPath2(config);
|
|
28519
|
+
const statVault = deps.statVault ?? defaultStatVault;
|
|
28520
|
+
const selfUid = deps.selfUid ?? (typeof process.getuid === "function" ? process.getuid() : -1);
|
|
28521
|
+
let selfUser = deps.selfUser;
|
|
28522
|
+
if (selfUser === undefined) {
|
|
28523
|
+
try {
|
|
28524
|
+
selfUser = userInfo().username;
|
|
28525
|
+
} catch {
|
|
28526
|
+
selfUser = "<you>";
|
|
28527
|
+
}
|
|
28528
|
+
}
|
|
28529
|
+
const vf = statVault(vaultPath);
|
|
28530
|
+
if (!vf.exists) {
|
|
28531
|
+
results.push({
|
|
28532
|
+
name: "vault: operator readable",
|
|
28533
|
+
status: "ok",
|
|
28534
|
+
detail: `vault file not present at ${vaultPath} \u2014 see the Vault section`
|
|
28535
|
+
});
|
|
28536
|
+
} else if (!vf.readable) {
|
|
28537
|
+
results.push({
|
|
28538
|
+
name: "vault: operator readable",
|
|
28539
|
+
status: "fail",
|
|
28540
|
+
detail: `${vf.realPath} is owned by uid ${vf.uid} (mode 0${vf.mode.toString(8)}) \u2014 ` + `the operator (uid ${selfUid} ${selfUser}) cannot read it, so every ` + `\`switchroom vault \u2026\` fails. The broker still works (CAP_DAC_READ_SEARCH), ` + `which masks this until you touch the vault directly.`,
|
|
28541
|
+
fix: `sudo chown ${selfUser}:${selfUser} ${vf.realPath}`
|
|
28542
|
+
});
|
|
28543
|
+
} else {
|
|
28544
|
+
results.push({
|
|
28545
|
+
name: "vault: operator readable",
|
|
28546
|
+
status: "ok",
|
|
28547
|
+
detail: `operator can read ${vf.realPath}`
|
|
28548
|
+
});
|
|
28549
|
+
}
|
|
28550
|
+
const passphrase = deps.passphrase ?? process.env.SWITCHROOM_VAULT_PASSPHRASE;
|
|
28551
|
+
if (!passphrase) {
|
|
28552
|
+
results.push({
|
|
28553
|
+
name: "agent secret access",
|
|
28554
|
+
status: "warn",
|
|
28555
|
+
detail: "SWITCHROOM_VAULT_PASSPHRASE not set \u2014 cannot enumerate vault keys/ACLs " + "to verify per-agent secret access",
|
|
28556
|
+
fix: "Export SWITCHROOM_VAULT_PASSPHRASE and re-run `switchroom doctor`"
|
|
28557
|
+
});
|
|
28558
|
+
return results;
|
|
28559
|
+
}
|
|
28560
|
+
let entries;
|
|
28561
|
+
try {
|
|
28562
|
+
entries = (deps.openVault ?? openVault)(passphrase, vaultPath);
|
|
28563
|
+
} catch (err) {
|
|
28564
|
+
results.push({
|
|
28565
|
+
name: "agent secret access",
|
|
28566
|
+
status: vf.readable ? "fail" : "warn",
|
|
28567
|
+
detail: `cannot open the vault: ${err.message}`,
|
|
28568
|
+
fix: vf.readable ? "SWITCHROOM_VAULT_PASSPHRASE may be wrong, or the vault is corrupt" : "fix the vault file ownership above first (operator cannot read it)"
|
|
28569
|
+
});
|
|
28570
|
+
return results;
|
|
28571
|
+
}
|
|
28572
|
+
const agents = Object.keys(config.agents ?? {});
|
|
28573
|
+
for (const name of agents) {
|
|
28574
|
+
const resolved = resolveAgentConfig(config.defaults, config.profiles, config.agents[name]);
|
|
28575
|
+
const cronKeys = new Set;
|
|
28576
|
+
for (const entry of resolved.schedule ?? []) {
|
|
28577
|
+
for (const s of entry.secrets ?? [])
|
|
28578
|
+
cronKeys.add(s);
|
|
28579
|
+
}
|
|
28580
|
+
const refKeys = new Set;
|
|
28581
|
+
collectVaultRefs2(resolved, refKeys);
|
|
28582
|
+
const needed = new Set([...cronKeys, ...refKeys]);
|
|
28583
|
+
if (needed.size === 0) {
|
|
28584
|
+
results.push({
|
|
28585
|
+
name: `secret access: ${name}`,
|
|
28586
|
+
status: "ok",
|
|
28587
|
+
detail: "no declared vault secrets"
|
|
28588
|
+
});
|
|
28589
|
+
continue;
|
|
28590
|
+
}
|
|
28591
|
+
const gaps = [];
|
|
28592
|
+
for (const key of [...needed].sort()) {
|
|
28593
|
+
const isGoogleSlot = key.startsWith("google:");
|
|
28594
|
+
if (!isGoogleSlot && !(key in entries)) {
|
|
28595
|
+
gaps.push(`'${key}' missing from the vault`);
|
|
28596
|
+
continue;
|
|
28597
|
+
}
|
|
28598
|
+
const byScope = checkEntryScope(entries[key]?.scope, name);
|
|
28599
|
+
if (cronKeys.has(key)) {
|
|
28600
|
+
const byAgent = checkAclByAgent(config, name, key);
|
|
28601
|
+
if (!byAgent.allow) {
|
|
28602
|
+
gaps.push(`'${key}' (cron) \u2014 no static ACL grants read (${byAgent.reason})`);
|
|
28603
|
+
continue;
|
|
28604
|
+
}
|
|
28605
|
+
}
|
|
28606
|
+
if (!byScope.allow) {
|
|
28607
|
+
gaps.push(`'${key}' \u2014 per-key scope denies read (${byScope.reason})`);
|
|
28608
|
+
}
|
|
28609
|
+
}
|
|
28610
|
+
if (gaps.length === 0) {
|
|
28611
|
+
results.push({
|
|
28612
|
+
name: `secret access: ${name}`,
|
|
28613
|
+
status: "ok",
|
|
28614
|
+
detail: `${needed.size} secret(s): all present + ACL ok`
|
|
28615
|
+
});
|
|
28616
|
+
} else {
|
|
28617
|
+
results.push({
|
|
28618
|
+
name: `secret access: ${name}`,
|
|
28619
|
+
status: "fail",
|
|
28620
|
+
detail: `${gaps.length}/${needed.size} unreachable \u2014 ${gaps.join("; ")}`,
|
|
28621
|
+
fix: "`switchroom vault set <key>` for missing keys; " + "`switchroom vault set <key> --allow " + name + "` to grant this agent read access"
|
|
28622
|
+
});
|
|
28623
|
+
}
|
|
28624
|
+
}
|
|
28625
|
+
return results;
|
|
28626
|
+
}
|
|
28627
|
+
var init_doctor_secret_access = __esm(() => {
|
|
28628
|
+
init_paths();
|
|
28629
|
+
init_merge();
|
|
28630
|
+
init_vault();
|
|
28631
|
+
});
|
|
28632
|
+
|
|
28208
28633
|
// src/cli/doctor-inlined-secrets.ts
|
|
28209
28634
|
import { readFileSync as fsReadFileSync } from "node:fs";
|
|
28210
28635
|
function isSecretShapedKey(key) {
|
|
@@ -28394,7 +28819,9 @@ __export(exports_doctor, {
|
|
|
28394
28819
|
parsePythonVersion: () => parsePythonVersion,
|
|
28395
28820
|
parseNodeVersion: () => parseNodeVersion,
|
|
28396
28821
|
parseEnvFile: () => parseEnvFile,
|
|
28822
|
+
mffEnvState: () => mffEnvState,
|
|
28397
28823
|
mffEnvPath: () => mffEnvPath,
|
|
28824
|
+
mffAgentName: () => mffAgentName,
|
|
28398
28825
|
isSwitchroomCheckout: () => isSwitchroomCheckout,
|
|
28399
28826
|
findChromium: () => findChromium,
|
|
28400
28827
|
deriveEd25519PublicKeyBytes: () => deriveEd25519PublicKeyBytes,
|
|
@@ -28420,18 +28847,18 @@ __export(exports_doctor, {
|
|
|
28420
28847
|
checkAgents: () => checkAgents,
|
|
28421
28848
|
MFF_VAULT_KEY: () => MFF_VAULT_KEY
|
|
28422
28849
|
});
|
|
28423
|
-
import { execSync as execSync3, spawnSync as
|
|
28850
|
+
import { execSync as execSync3, spawnSync as spawnSync7 } from "node:child_process";
|
|
28424
28851
|
import {
|
|
28425
|
-
accessSync,
|
|
28426
|
-
constants as
|
|
28427
|
-
existsSync as
|
|
28852
|
+
accessSync as accessSync2,
|
|
28853
|
+
constants as fsConstants5,
|
|
28854
|
+
existsSync as existsSync47,
|
|
28428
28855
|
lstatSync as lstatSync5,
|
|
28429
28856
|
mkdirSync as mkdirSync26,
|
|
28430
28857
|
readFileSync as readFileSync44,
|
|
28431
28858
|
readdirSync as readdirSync17,
|
|
28432
|
-
statSync as
|
|
28859
|
+
statSync as statSync21
|
|
28433
28860
|
} from "node:fs";
|
|
28434
|
-
import { join as join41, resolve as resolve29 } from "node:path";
|
|
28861
|
+
import { dirname as dirname12, join as join41, resolve as resolve29 } from "node:path";
|
|
28435
28862
|
import { createPublicKey, createPrivateKey } from "node:crypto";
|
|
28436
28863
|
function statusGlyph(status) {
|
|
28437
28864
|
switch (status) {
|
|
@@ -28445,14 +28872,14 @@ function statusGlyph(status) {
|
|
|
28445
28872
|
}
|
|
28446
28873
|
function findInNvm(bin) {
|
|
28447
28874
|
const nvmRoot = join41(process.env.HOME ?? "", ".nvm", "versions", "node");
|
|
28448
|
-
if (!
|
|
28875
|
+
if (!existsSync47(nvmRoot))
|
|
28449
28876
|
return null;
|
|
28450
28877
|
try {
|
|
28451
28878
|
const versions = readdirSync17(nvmRoot).sort().reverse();
|
|
28452
28879
|
for (const v of versions) {
|
|
28453
28880
|
const candidate = join41(nvmRoot, v, "bin", bin);
|
|
28454
28881
|
try {
|
|
28455
|
-
const s =
|
|
28882
|
+
const s = statSync21(candidate);
|
|
28456
28883
|
if (s.isFile() || s.isSymbolicLink()) {
|
|
28457
28884
|
return candidate;
|
|
28458
28885
|
}
|
|
@@ -28617,7 +29044,7 @@ function findChromium(homeDir = process.env.HOME ?? "", envBrowsersPath = proces
|
|
|
28617
29044
|
}
|
|
28618
29045
|
cacheLocations.push(join41(homeDir, ".cache", "ms-playwright"));
|
|
28619
29046
|
for (const cacheDir of cacheLocations) {
|
|
28620
|
-
if (!
|
|
29047
|
+
if (!existsSync47(cacheDir))
|
|
28621
29048
|
continue;
|
|
28622
29049
|
try {
|
|
28623
29050
|
const entries = readdirSync17(cacheDir).filter((e) => e.startsWith("chromium"));
|
|
@@ -28629,7 +29056,7 @@ function findChromium(homeDir = process.env.HOME ?? "", envBrowsersPath = proces
|
|
|
28629
29056
|
join41(cacheDir, entry, "chrome-linux", "headless_shell")
|
|
28630
29057
|
];
|
|
28631
29058
|
for (const path4 of candidates2) {
|
|
28632
|
-
if (
|
|
29059
|
+
if (existsSync47(path4))
|
|
28633
29060
|
return path4;
|
|
28634
29061
|
}
|
|
28635
29062
|
}
|
|
@@ -28652,7 +29079,7 @@ function checkChromium() {
|
|
|
28652
29079
|
function checkDepsCacheWritable(depsRoot = resolvePath("~/.switchroom/deps")) {
|
|
28653
29080
|
try {
|
|
28654
29081
|
mkdirSync26(depsRoot, { recursive: true });
|
|
28655
|
-
|
|
29082
|
+
accessSync2(depsRoot, fsConstants5.W_OK);
|
|
28656
29083
|
return {
|
|
28657
29084
|
name: "~/.switchroom/deps writable",
|
|
28658
29085
|
status: "ok",
|
|
@@ -28710,7 +29137,7 @@ function checkLegacyState() {
|
|
|
28710
29137
|
const results = [];
|
|
28711
29138
|
const h = process.env.HOME ?? "/root";
|
|
28712
29139
|
const clerkDir = join41(h, LEGACY_STATE_DIR);
|
|
28713
|
-
const clerkPresent =
|
|
29140
|
+
const clerkPresent = existsSync47(clerkDir);
|
|
28714
29141
|
results.push({
|
|
28715
29142
|
name: "legacy ~/.clerk state",
|
|
28716
29143
|
status: clerkPresent ? "warn" : "ok",
|
|
@@ -28752,7 +29179,7 @@ function checkVault(config) {
|
|
|
28752
29179
|
status: "ok",
|
|
28753
29180
|
detail: "Approval auth: passphrase (two-factor)"
|
|
28754
29181
|
};
|
|
28755
|
-
if (!
|
|
29182
|
+
if (!existsSync47(vaultPath)) {
|
|
28756
29183
|
return [
|
|
28757
29184
|
postureResult,
|
|
28758
29185
|
{
|
|
@@ -28845,7 +29272,7 @@ function checkHindsightConsumer(config, opts) {
|
|
|
28845
29272
|
}
|
|
28846
29273
|
function probeAuthBrokerSocket(consumerName) {
|
|
28847
29274
|
const containerPath = `/run/switchroom/auth-broker/${consumerName}/sock`;
|
|
28848
|
-
const r =
|
|
29275
|
+
const r = spawnSync7("docker", ["exec", "switchroom-auth-broker", "test", "-S", containerPath], { stdio: "pipe", timeout: 3000 });
|
|
28849
29276
|
if (r.error || r.status === null)
|
|
28850
29277
|
return "unreachable";
|
|
28851
29278
|
if (r.status === 0)
|
|
@@ -28924,7 +29351,7 @@ async function checkHindsight(config) {
|
|
|
28924
29351
|
function checkPendingRetainsQueue(dir) {
|
|
28925
29352
|
const home2 = process.env.HOME ?? "";
|
|
28926
29353
|
const pendingDir = dir ?? process.env.HINDSIGHT_PENDING_DIR ?? join41(home2, ".hindsight", "pending-retains");
|
|
28927
|
-
if (!
|
|
29354
|
+
if (!existsSync47(pendingDir)) {
|
|
28928
29355
|
return {
|
|
28929
29356
|
name: "pending-retains queue",
|
|
28930
29357
|
status: "ok",
|
|
@@ -28995,7 +29422,7 @@ function tryReadHostFile(path4) {
|
|
|
28995
29422
|
}
|
|
28996
29423
|
}
|
|
28997
29424
|
function parseEnvFile(path4) {
|
|
28998
|
-
if (!
|
|
29425
|
+
if (!existsSync47(path4))
|
|
28999
29426
|
return {};
|
|
29000
29427
|
let content;
|
|
29001
29428
|
try {
|
|
@@ -29105,7 +29532,7 @@ async function checkTelegram(config) {
|
|
|
29105
29532
|
}
|
|
29106
29533
|
function checkStartShStale(agentName, startShPath) {
|
|
29107
29534
|
const label = `${agentName}: start.sh scheduler block`;
|
|
29108
|
-
if (!
|
|
29535
|
+
if (!existsSync47(startShPath)) {
|
|
29109
29536
|
return {
|
|
29110
29537
|
name: label,
|
|
29111
29538
|
status: "warn",
|
|
@@ -29173,7 +29600,7 @@ function checkLeakedHomeSwitchroom(agentName, agentDir) {
|
|
|
29173
29600
|
function checkRepoHygiene(repoRoot) {
|
|
29174
29601
|
const results = [];
|
|
29175
29602
|
const exportDir = join41(repoRoot, "clerk-export");
|
|
29176
|
-
if (
|
|
29603
|
+
if (existsSync47(exportDir)) {
|
|
29177
29604
|
results.push({
|
|
29178
29605
|
name: "repo hygiene: clerk-export/ on disk (#1072)",
|
|
29179
29606
|
status: "warn",
|
|
@@ -29182,7 +29609,7 @@ function checkRepoHygiene(repoRoot) {
|
|
|
29182
29609
|
});
|
|
29183
29610
|
}
|
|
29184
29611
|
const knownTarball = join41(repoRoot, "clerk-export-with-secrets.tar.gz");
|
|
29185
|
-
if (
|
|
29612
|
+
if (existsSync47(knownTarball)) {
|
|
29186
29613
|
results.push({
|
|
29187
29614
|
name: "repo hygiene: clerk-export-with-secrets.tar.gz on disk (#1072)",
|
|
29188
29615
|
status: "warn",
|
|
@@ -29222,10 +29649,10 @@ function checkRepoHygiene(repoRoot) {
|
|
|
29222
29649
|
}
|
|
29223
29650
|
function isSwitchroomCheckout(dir) {
|
|
29224
29651
|
try {
|
|
29225
|
-
if (!
|
|
29652
|
+
if (!existsSync47(join41(dir, ".git")))
|
|
29226
29653
|
return false;
|
|
29227
29654
|
const pkgPath = join41(dir, "package.json");
|
|
29228
|
-
if (!
|
|
29655
|
+
if (!existsSync47(pkgPath))
|
|
29229
29656
|
return false;
|
|
29230
29657
|
const pkg = JSON.parse(readFileSync44(pkgPath, "utf-8"));
|
|
29231
29658
|
return pkg.name === "switchroom";
|
|
@@ -29240,7 +29667,7 @@ function checkAgents(config, configPath) {
|
|
|
29240
29667
|
const authStatuses = getAllAuthStatuses(config);
|
|
29241
29668
|
for (const [name, agentConfig] of Object.entries(config.agents)) {
|
|
29242
29669
|
const agentDir = resolve29(agentsDir, name);
|
|
29243
|
-
if (!
|
|
29670
|
+
if (!existsSync47(agentDir)) {
|
|
29244
29671
|
results.push({
|
|
29245
29672
|
name: `${name}: scaffold`,
|
|
29246
29673
|
status: "fail",
|
|
@@ -29292,7 +29719,7 @@ function checkAgents(config, configPath) {
|
|
|
29292
29719
|
name: `${name}: auth`,
|
|
29293
29720
|
status: "fail",
|
|
29294
29721
|
detail: auth?.pendingAuth ? "pending (auth flow in progress)" : "not authenticated",
|
|
29295
|
-
fix: `Run \`switchroom auth add <label> --
|
|
29722
|
+
fix: `Run \`switchroom auth add <label> --via-claude\` then \`switchroom auth use <label>\` (RFC H \u2014 see docs/auth.md)`
|
|
29296
29723
|
});
|
|
29297
29724
|
}
|
|
29298
29725
|
} else {
|
|
@@ -29332,13 +29759,13 @@ function checkAgents(config, configPath) {
|
|
|
29332
29759
|
name: `${name}: auth slots`,
|
|
29333
29760
|
status: status2,
|
|
29334
29761
|
detail,
|
|
29335
|
-
fix: quotaOut > 0 ? `Quota-exhausted account(s) will auto-recover when the window resets; broker auto-rotates per \`auth.fallback_order\` (see \`switchroom auth show\`).` : expired > 0 ? `Expired account(s) \u2014 add a fresh one via \`switchroom auth add <label> --
|
|
29762
|
+
fix: quotaOut > 0 ? `Quota-exhausted account(s) will auto-recover when the window resets; broker auto-rotates per \`auth.fallback_order\` (see \`switchroom auth show\`).` : expired > 0 ? `Expired account(s) \u2014 add a fresh one via \`switchroom auth add <label> --via-claude\` and \`switchroom auth use <label>\` (RFC H).` : undefined
|
|
29336
29763
|
});
|
|
29337
29764
|
}
|
|
29338
29765
|
}
|
|
29339
29766
|
if (agentConfig.channels?.telegram?.plugin === "switchroom") {
|
|
29340
29767
|
const mcpJsonPath = join41(agentDir, ".mcp.json");
|
|
29341
|
-
if (!
|
|
29768
|
+
if (!existsSync47(mcpJsonPath)) {
|
|
29342
29769
|
results.push({
|
|
29343
29770
|
name: `${name}: .mcp.json`,
|
|
29344
29771
|
status: "fail",
|
|
@@ -29406,8 +29833,28 @@ ${title}`));
|
|
|
29406
29833
|
}
|
|
29407
29834
|
return { oks, warns, fails };
|
|
29408
29835
|
}
|
|
29409
|
-
function
|
|
29410
|
-
|
|
29836
|
+
function mffAgentName(config) {
|
|
29837
|
+
for (const [name, ac] of Object.entries(config?.agents ?? {})) {
|
|
29838
|
+
if ((ac?.skills ?? []).includes("my-family-finance")) {
|
|
29839
|
+
return name;
|
|
29840
|
+
}
|
|
29841
|
+
}
|
|
29842
|
+
return;
|
|
29843
|
+
}
|
|
29844
|
+
function mffEnvPath(config) {
|
|
29845
|
+
const home2 = process.env.HOME ?? "/root";
|
|
29846
|
+
const agent = mffAgentName(config);
|
|
29847
|
+
return agent ? resolve29(home2, ".switchroom/credentials", agent, "my-family-finance/.env") : resolve29(home2, ".switchroom/credentials/my-family-finance/.env");
|
|
29848
|
+
}
|
|
29849
|
+
function mffEnvState(envPath) {
|
|
29850
|
+
if (!existsSync47(envPath))
|
|
29851
|
+
return "absent";
|
|
29852
|
+
try {
|
|
29853
|
+
accessSync2(envPath, fsConstants5.R_OK);
|
|
29854
|
+
return "readable";
|
|
29855
|
+
} catch {
|
|
29856
|
+
return "agent-private";
|
|
29857
|
+
}
|
|
29411
29858
|
}
|
|
29412
29859
|
function checkMffVaultKeyPresent(passphrase, vaultPath) {
|
|
29413
29860
|
if (!passphrase) {
|
|
@@ -29418,7 +29865,7 @@ function checkMffVaultKeyPresent(passphrase, vaultPath) {
|
|
|
29418
29865
|
fix: "Export SWITCHROOM_VAULT_PASSPHRASE to enable MFF vault probes"
|
|
29419
29866
|
};
|
|
29420
29867
|
}
|
|
29421
|
-
if (!
|
|
29868
|
+
if (!existsSync47(vaultPath)) {
|
|
29422
29869
|
return {
|
|
29423
29870
|
name: "mff: vault key present",
|
|
29424
29871
|
status: "fail",
|
|
@@ -29471,7 +29918,7 @@ function deriveEd25519PublicKeyBytes(keyMaterial) {
|
|
|
29471
29918
|
}
|
|
29472
29919
|
}
|
|
29473
29920
|
function checkMffVaultKeyFormat(passphrase, vaultPath) {
|
|
29474
|
-
if (!passphrase || !
|
|
29921
|
+
if (!passphrase || !existsSync47(vaultPath)) {
|
|
29475
29922
|
return {
|
|
29476
29923
|
name: "mff: vault key format",
|
|
29477
29924
|
status: "warn",
|
|
@@ -29513,12 +29960,20 @@ function checkMffVaultKeyFormat(passphrase, vaultPath) {
|
|
|
29513
29960
|
}
|
|
29514
29961
|
}
|
|
29515
29962
|
function checkMffEnvFile(envPath = mffEnvPath()) {
|
|
29516
|
-
|
|
29963
|
+
const state = mffEnvState(envPath);
|
|
29964
|
+
if (state === "absent") {
|
|
29517
29965
|
return {
|
|
29518
29966
|
name: "mff: .env present",
|
|
29519
29967
|
status: "fail",
|
|
29520
29968
|
detail: `${envPath} not found`,
|
|
29521
|
-
fix: "
|
|
29969
|
+
fix: "Place the MFF skill's .env in the per-agent credentials dir, e.g. `~/.switchroom/credentials/<agent>/my-family-finance/.env` (MFF_API_URL=https://...), then `switchroom update`"
|
|
29970
|
+
};
|
|
29971
|
+
}
|
|
29972
|
+
if (state === "agent-private") {
|
|
29973
|
+
return {
|
|
29974
|
+
name: "mff: .env present",
|
|
29975
|
+
status: "ok",
|
|
29976
|
+
detail: `present, agent-private (${envPath}) \u2014 per-agent since WS6-F2; the operator-run doctor cannot read it by design`
|
|
29522
29977
|
};
|
|
29523
29978
|
}
|
|
29524
29979
|
const env2 = parseEnvFile(envPath);
|
|
@@ -29537,6 +29992,14 @@ function checkMffEnvFile(envPath = mffEnvPath()) {
|
|
|
29537
29992
|
};
|
|
29538
29993
|
}
|
|
29539
29994
|
async function checkMffApiReachable(envPath = mffEnvPath(), timeoutMs = 5000) {
|
|
29995
|
+
const state = mffEnvState(envPath);
|
|
29996
|
+
if (state !== "readable") {
|
|
29997
|
+
return {
|
|
29998
|
+
name: "mff: API reachable",
|
|
29999
|
+
status: "warn",
|
|
30000
|
+
detail: state === "agent-private" ? "skipped \u2014 MFF creds are per-agent/agent-private since WS6-F2; deep probe must run in-agent" : "skipped (MFF .env not found \u2014 see 'mff: .env present')"
|
|
30001
|
+
};
|
|
30002
|
+
}
|
|
29540
30003
|
const env2 = parseEnvFile(envPath);
|
|
29541
30004
|
const apiUrl = env2.MFF_API_URL?.trim();
|
|
29542
30005
|
if (!apiUrl) {
|
|
@@ -29581,6 +30044,14 @@ async function checkMffApiReachable(envPath = mffEnvPath(), timeoutMs = 5000) {
|
|
|
29581
30044
|
}
|
|
29582
30045
|
}
|
|
29583
30046
|
async function checkMffAuthFlow(envPath = mffEnvPath(), timeoutMs = 8000) {
|
|
30047
|
+
const state = mffEnvState(envPath);
|
|
30048
|
+
if (state !== "readable") {
|
|
30049
|
+
return {
|
|
30050
|
+
name: "mff: auth flow",
|
|
30051
|
+
status: "warn",
|
|
30052
|
+
detail: state === "agent-private" ? "skipped \u2014 MFF creds are per-agent/agent-private since WS6-F2; deep probe must run in-agent" : "skipped (MFF .env not found \u2014 see 'mff: .env present')"
|
|
30053
|
+
};
|
|
30054
|
+
}
|
|
29584
30055
|
const env2 = parseEnvFile(envPath);
|
|
29585
30056
|
const apiUrl = env2.MFF_API_URL?.trim();
|
|
29586
30057
|
if (!apiUrl) {
|
|
@@ -29590,9 +30061,9 @@ async function checkMffAuthFlow(envPath = mffEnvPath(), timeoutMs = 8000) {
|
|
|
29590
30061
|
detail: "skipped (MFF_API_URL not set)"
|
|
29591
30062
|
};
|
|
29592
30063
|
}
|
|
29593
|
-
const credDir =
|
|
30064
|
+
const credDir = dirname12(envPath);
|
|
29594
30065
|
const authScript = join41(credDir, "claude-auth.py");
|
|
29595
|
-
if (!
|
|
30066
|
+
if (!existsSync47(authScript)) {
|
|
29596
30067
|
return {
|
|
29597
30068
|
name: "mff: auth flow",
|
|
29598
30069
|
status: "warn",
|
|
@@ -29603,7 +30074,7 @@ async function checkMffAuthFlow(envPath = mffEnvPath(), timeoutMs = 8000) {
|
|
|
29603
30074
|
const python3 = which("python3") ?? "python3";
|
|
29604
30075
|
let token;
|
|
29605
30076
|
try {
|
|
29606
|
-
const result =
|
|
30077
|
+
const result = spawnSync7(python3, [authScript, "--quiet"], {
|
|
29607
30078
|
timeout: timeoutMs,
|
|
29608
30079
|
encoding: "utf-8",
|
|
29609
30080
|
env: { ...process.env, ...env2 }
|
|
@@ -29671,6 +30142,14 @@ async function checkMffAuthFlow(envPath = mffEnvPath(), timeoutMs = 8000) {
|
|
|
29671
30142
|
}
|
|
29672
30143
|
}
|
|
29673
30144
|
async function checkMffCloudflareUa(envPath = mffEnvPath(), timeoutMs = 5000) {
|
|
30145
|
+
const state = mffEnvState(envPath);
|
|
30146
|
+
if (state !== "readable") {
|
|
30147
|
+
return {
|
|
30148
|
+
name: "mff: Cloudflare UA bypass",
|
|
30149
|
+
status: "warn",
|
|
30150
|
+
detail: state === "agent-private" ? "skipped \u2014 MFF creds are per-agent/agent-private since WS6-F2; deep probe must run in-agent" : "skipped (MFF .env not found \u2014 see 'mff: .env present')"
|
|
30151
|
+
};
|
|
30152
|
+
}
|
|
29674
30153
|
const env2 = parseEnvFile(envPath);
|
|
29675
30154
|
const apiUrl = env2.MFF_API_URL?.trim();
|
|
29676
30155
|
if (!apiUrl) {
|
|
@@ -29732,7 +30211,7 @@ async function checkMffCloudflareUa(envPath = mffEnvPath(), timeoutMs = 5000) {
|
|
|
29732
30211
|
fix: "Check MFF_API_URL and whether the /api/health endpoint is publicly accessible"
|
|
29733
30212
|
};
|
|
29734
30213
|
}
|
|
29735
|
-
async function checkMff(passphrase, vaultPath, envPath = mffEnvPath()) {
|
|
30214
|
+
async function checkMff(passphrase, vaultPath, config, envPath = mffEnvPath(config)) {
|
|
29736
30215
|
const results = [];
|
|
29737
30216
|
results.push(checkMffVaultKeyPresent(passphrase, vaultPath));
|
|
29738
30217
|
results.push(checkMffVaultKeyFormat(passphrase, vaultPath));
|
|
@@ -29835,7 +30314,7 @@ function registerDoctorCommand(program3) {
|
|
|
29835
30314
|
const passphrase = process.env.SWITCHROOM_VAULT_PASSPHRASE;
|
|
29836
30315
|
const vaultPath = config.vault?.path ? config.vault.path.replace(/^~/, process.env.HOME ?? "") : resolveStatePath("vault.enc");
|
|
29837
30316
|
if (opts.skill === "mff") {
|
|
29838
|
-
const mffResults = await checkMff(passphrase, vaultPath);
|
|
30317
|
+
const mffResults = await checkMff(passphrase, vaultPath, config);
|
|
29839
30318
|
if (opts.json) {
|
|
29840
30319
|
console.log(JSON.stringify({ sections: [{ title: "MFF Skill", results: mffResults }] }, null, 2));
|
|
29841
30320
|
} else {
|
|
@@ -29862,6 +30341,7 @@ function registerDoctorCommand(program3) {
|
|
|
29862
30341
|
},
|
|
29863
30342
|
{ title: "Legacy State", results: checkLegacyState() },
|
|
29864
30343
|
{ title: "Vault", results: checkVault(config) },
|
|
30344
|
+
{ title: "Vault access", results: runSecretAccessChecks(config) },
|
|
29865
30345
|
{ title: "Memory (Hindsight)", results: await checkHindsight(config) },
|
|
29866
30346
|
{ title: "Telegram", results: await checkTelegram(config) },
|
|
29867
30347
|
{ title: "Agents", results: checkAgents(config, configPath) },
|
|
@@ -29869,8 +30349,9 @@ function registerDoctorCommand(program3) {
|
|
|
29869
30349
|
{ title: "Audit integrity (WS10-F4)", results: runAuditIntegrityChecks() },
|
|
29870
30350
|
{ title: "Docker (Phase 1a)", results: runDockerSection(config) },
|
|
29871
30351
|
{ title: "Auth Broker", results: runAuthBrokerChecks(config) },
|
|
30352
|
+
{ title: "Host control (hostd)", results: runHostdChecks(config) },
|
|
29872
30353
|
{ title: "Google Drive", results: runDriveChecks(config) },
|
|
29873
|
-
{ title: "MFF Skill", results: await checkMff(passphrase, vaultPath) }
|
|
30354
|
+
{ title: "MFF Skill", results: await checkMff(passphrase, vaultPath, config) }
|
|
29874
30355
|
];
|
|
29875
30356
|
const cwd = process.cwd();
|
|
29876
30357
|
if (isSwitchroomCheckout(cwd)) {
|
|
@@ -29923,8 +30404,10 @@ var init_doctor = __esm(() => {
|
|
|
29923
30404
|
init_hindsight();
|
|
29924
30405
|
init_doctor_docker();
|
|
29925
30406
|
init_doctor_auth_broker();
|
|
30407
|
+
init_doctor_hostd();
|
|
29926
30408
|
init_doctor_drive();
|
|
29927
30409
|
init_doctor_credentials_migration();
|
|
30410
|
+
init_doctor_secret_access();
|
|
29928
30411
|
init_doctor_inlined_secrets();
|
|
29929
30412
|
init_doctor_audit_integrity();
|
|
29930
30413
|
MANIFEST_WARN_ONLY = new Set([
|
|
@@ -45564,9 +46047,9 @@ __export(exports_server, {
|
|
|
45564
46047
|
dispatchTool: () => dispatchTool,
|
|
45565
46048
|
TOOLS: () => TOOLS
|
|
45566
46049
|
});
|
|
45567
|
-
import { spawnSync as
|
|
46050
|
+
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
45568
46051
|
function execCli(args) {
|
|
45569
|
-
const r =
|
|
46052
|
+
const r = spawnSync10(CLI_BIN, args, {
|
|
45570
46053
|
encoding: "utf-8",
|
|
45571
46054
|
env: process.env,
|
|
45572
46055
|
timeout: 15000
|
|
@@ -46059,7 +46542,7 @@ __export(exports_server2, {
|
|
|
46059
46542
|
TOOLS: () => TOOLS2
|
|
46060
46543
|
});
|
|
46061
46544
|
import { randomBytes as randomBytes13 } from "node:crypto";
|
|
46062
|
-
import { existsSync as
|
|
46545
|
+
import { existsSync as existsSync71, readFileSync as readFileSync59 } from "node:fs";
|
|
46063
46546
|
function selfSocketPath() {
|
|
46064
46547
|
return `/run/switchroom/hostd/${SELF_AGENT}/sock`;
|
|
46065
46548
|
}
|
|
@@ -46074,7 +46557,7 @@ async function dispatchTool2(name, args) {
|
|
|
46074
46557
|
return errorText2("hostd MCP: SWITCHROOM_AGENT_NAME env var is not set \u2014 cannot " + "determine which per-agent socket to talk to.");
|
|
46075
46558
|
}
|
|
46076
46559
|
const sockPath = selfSocketPath();
|
|
46077
|
-
if (!
|
|
46560
|
+
if (!existsSync71(sockPath)) {
|
|
46078
46561
|
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.`);
|
|
46079
46562
|
}
|
|
46080
46563
|
let req;
|
|
@@ -46193,13 +46676,13 @@ function resolveAuditLogPath() {
|
|
|
46193
46676
|
if (process.env.HOSTD_AUDIT_LOG_PATH)
|
|
46194
46677
|
return process.env.HOSTD_AUDIT_LOG_PATH;
|
|
46195
46678
|
const bindMounted = "/host-home/.switchroom/host-control-audit.log";
|
|
46196
|
-
if (
|
|
46679
|
+
if (existsSync71(bindMounted))
|
|
46197
46680
|
return bindMounted;
|
|
46198
46681
|
return defaultAuditLogPath2();
|
|
46199
46682
|
}
|
|
46200
46683
|
function getLastUpdateApplyStatus() {
|
|
46201
46684
|
const path8 = resolveAuditLogPath();
|
|
46202
|
-
if (!
|
|
46685
|
+
if (!existsSync71(path8)) {
|
|
46203
46686
|
return errorText2(`get_status: audit log not found at ${path8}. No update_apply has run yet?`);
|
|
46204
46687
|
}
|
|
46205
46688
|
let raw;
|
|
@@ -46411,8 +46894,8 @@ var {
|
|
|
46411
46894
|
} = import__.default;
|
|
46412
46895
|
|
|
46413
46896
|
// src/build-info.ts
|
|
46414
|
-
var VERSION = "0.12.
|
|
46415
|
-
var COMMIT_SHA = "
|
|
46897
|
+
var VERSION = "0.12.2";
|
|
46898
|
+
var COMMIT_SHA = "1eeb64be";
|
|
46416
46899
|
|
|
46417
46900
|
// src/cli/agent.ts
|
|
46418
46901
|
init_source();
|
|
@@ -48496,6 +48979,16 @@ function buildSettingsHooksBlock(p) {
|
|
|
48496
48979
|
}
|
|
48497
48980
|
]
|
|
48498
48981
|
},
|
|
48982
|
+
{
|
|
48983
|
+
matcher: "^(Write|Edit|MultiEdit)$",
|
|
48984
|
+
hooks: [
|
|
48985
|
+
{
|
|
48986
|
+
type: "command",
|
|
48987
|
+
command: wrap("hook:skill-validate-pretool", `node "${join8(DOCKER_BUNDLED_HOOKS_PATH, "skill-validate-pretool.mjs")}"`),
|
|
48988
|
+
timeout: 10
|
|
48989
|
+
}
|
|
48990
|
+
]
|
|
48991
|
+
},
|
|
48499
48992
|
{
|
|
48500
48993
|
hooks: [
|
|
48501
48994
|
{
|
|
@@ -55184,145 +55677,6 @@ var DEFAULT_AUTO_UNLOCK_PATH = "~/.switchroom/vault-auto-unlock";
|
|
|
55184
55677
|
|
|
55185
55678
|
// src/vault/broker/server.ts
|
|
55186
55679
|
init_peercred();
|
|
55187
|
-
|
|
55188
|
-
// src/vault/broker/acl.ts
|
|
55189
|
-
function parseCronUnit(unitName) {
|
|
55190
|
-
const m = unitName.match(/^switchroom-([a-zA-Z0-9_-]+)-cron-(\d+)\.service$/);
|
|
55191
|
-
if (!m)
|
|
55192
|
-
return null;
|
|
55193
|
-
const agentName = m[1];
|
|
55194
|
-
const index = parseInt(m[2], 10);
|
|
55195
|
-
if (!agentName)
|
|
55196
|
-
return null;
|
|
55197
|
-
return { agentName, index };
|
|
55198
|
-
}
|
|
55199
|
-
function agentSlugFromPeer(peer) {
|
|
55200
|
-
if (peer.systemdUnit === null)
|
|
55201
|
-
return null;
|
|
55202
|
-
const parsed = parseCronUnit(peer.systemdUnit);
|
|
55203
|
-
return parsed?.agentName ?? null;
|
|
55204
|
-
}
|
|
55205
|
-
function checkEntryScope(scope, agentSlug) {
|
|
55206
|
-
if (scope === undefined || scope === null) {
|
|
55207
|
-
return { allow: true };
|
|
55208
|
-
}
|
|
55209
|
-
const deny = scope.deny ?? [];
|
|
55210
|
-
const allow = scope.allow ?? [];
|
|
55211
|
-
if (agentSlug !== null && deny.includes(agentSlug)) {
|
|
55212
|
-
return {
|
|
55213
|
-
allow: false,
|
|
55214
|
-
reason: `agent '${agentSlug}' is in the entry's deny list (scope-deny)`
|
|
55215
|
-
};
|
|
55216
|
-
}
|
|
55217
|
-
if (allow.length > 0) {
|
|
55218
|
-
if (agentSlug === null || !allow.includes(agentSlug)) {
|
|
55219
|
-
return {
|
|
55220
|
-
allow: false,
|
|
55221
|
-
reason: agentSlug === null ? "caller agent slug could not be determined; entry has a non-empty allow list (scope-allow)" : `agent '${agentSlug}' is not in the entry's allow list (scope-allow)`
|
|
55222
|
-
};
|
|
55223
|
-
}
|
|
55224
|
-
}
|
|
55225
|
-
return { allow: true };
|
|
55226
|
-
}
|
|
55227
|
-
function checkAcl(peer, config, key) {
|
|
55228
|
-
if (peer.systemdUnit !== null) {
|
|
55229
|
-
const parsed = parseCronUnit(peer.systemdUnit);
|
|
55230
|
-
if (parsed === null) {
|
|
55231
|
-
return {
|
|
55232
|
-
allow: false,
|
|
55233
|
-
reason: `systemd unit '${peer.systemdUnit}' does not match switchroom cron unit naming convention`
|
|
55234
|
-
};
|
|
55235
|
-
}
|
|
55236
|
-
const { agentName, index } = parsed;
|
|
55237
|
-
const agentConfig = config.agents?.[agentName];
|
|
55238
|
-
if (!agentConfig) {
|
|
55239
|
-
return { allow: false, reason: `agent '${agentName}' not found in config` };
|
|
55240
|
-
}
|
|
55241
|
-
const schedule = agentConfig.schedule ?? [];
|
|
55242
|
-
if (index >= schedule.length || index < 0) {
|
|
55243
|
-
return {
|
|
55244
|
-
allow: false,
|
|
55245
|
-
reason: `schedule index ${index} out of range for agent '${agentName}' (${schedule.length} entries)`
|
|
55246
|
-
};
|
|
55247
|
-
}
|
|
55248
|
-
const entry = schedule[index];
|
|
55249
|
-
const allowedKeys = entry.secrets ?? [];
|
|
55250
|
-
if (!allowedKeys.includes(key)) {
|
|
55251
|
-
return {
|
|
55252
|
-
allow: false,
|
|
55253
|
-
reason: `key '${key}' not in ACL for ${agentName}/schedule[${index}]`
|
|
55254
|
-
};
|
|
55255
|
-
}
|
|
55256
|
-
return { allow: true };
|
|
55257
|
-
}
|
|
55258
|
-
return {
|
|
55259
|
-
allow: false,
|
|
55260
|
-
reason: "caller is not a switchroom cron unit; use 'switchroom vault get --no-broker' for interactive access"
|
|
55261
|
-
};
|
|
55262
|
-
}
|
|
55263
|
-
function checkAclByAgent(config, agentName, key) {
|
|
55264
|
-
if (!agentName) {
|
|
55265
|
-
return { allow: false, reason: "agent name unresolved" };
|
|
55266
|
-
}
|
|
55267
|
-
const agentConfig = config.agents?.[agentName];
|
|
55268
|
-
if (!agentConfig) {
|
|
55269
|
-
return { allow: false, reason: `agent '${agentName}' not found in config` };
|
|
55270
|
-
}
|
|
55271
|
-
const googleSlot = parseGoogleAccountSlotKey(key);
|
|
55272
|
-
if (googleSlot !== null) {
|
|
55273
|
-
return checkGoogleAccountAcl(config, agentName, googleSlot.account, key);
|
|
55274
|
-
}
|
|
55275
|
-
const schedule = agentConfig.schedule ?? [];
|
|
55276
|
-
if (schedule.length === 0) {
|
|
55277
|
-
return {
|
|
55278
|
-
allow: false,
|
|
55279
|
-
reason: `agent '${agentName}' has no schedule entries declaring 'secrets'; nothing is broker-accessible`
|
|
55280
|
-
};
|
|
55281
|
-
}
|
|
55282
|
-
for (const entry of schedule) {
|
|
55283
|
-
const allowed = entry?.secrets ?? [];
|
|
55284
|
-
if (allowed.includes(key)) {
|
|
55285
|
-
return { allow: true };
|
|
55286
|
-
}
|
|
55287
|
-
}
|
|
55288
|
-
return {
|
|
55289
|
-
allow: false,
|
|
55290
|
-
reason: `key '${key}' not in ACL for agent '${agentName}'`
|
|
55291
|
-
};
|
|
55292
|
-
}
|
|
55293
|
-
function parseGoogleAccountSlotKey(key) {
|
|
55294
|
-
const match = key.match(/^google:([^:]+):([a-z_]+)$/);
|
|
55295
|
-
if (!match)
|
|
55296
|
-
return null;
|
|
55297
|
-
return { account: match[1], field: match[2] };
|
|
55298
|
-
}
|
|
55299
|
-
function checkGoogleAccountAcl(config, agentName, account, key) {
|
|
55300
|
-
const accounts = config.google_accounts ?? {};
|
|
55301
|
-
const accountKey = account.toLowerCase();
|
|
55302
|
-
const accountEntry = accounts[accountKey] ?? accounts[account];
|
|
55303
|
-
if (!accountEntry) {
|
|
55304
|
-
return {
|
|
55305
|
-
allow: false,
|
|
55306
|
-
reason: `google_accounts['${account}'] not configured (key '${key}')`
|
|
55307
|
-
};
|
|
55308
|
-
}
|
|
55309
|
-
const enabled = accountEntry.enabled_for ?? [];
|
|
55310
|
-
if (enabled.length === 0) {
|
|
55311
|
-
return {
|
|
55312
|
-
allow: false,
|
|
55313
|
-
reason: `google_accounts['${account}'].enabled_for is empty (key '${key}')`
|
|
55314
|
-
};
|
|
55315
|
-
}
|
|
55316
|
-
if (!enabled.includes(agentName)) {
|
|
55317
|
-
return {
|
|
55318
|
-
allow: false,
|
|
55319
|
-
reason: `agent '${agentName}' not in google_accounts['${account}'].enabled_for (key '${key}')`
|
|
55320
|
-
};
|
|
55321
|
-
}
|
|
55322
|
-
return { allow: true };
|
|
55323
|
-
}
|
|
55324
|
-
|
|
55325
|
-
// src/vault/broker/server.ts
|
|
55326
55680
|
init_protocol();
|
|
55327
55681
|
|
|
55328
55682
|
// src/vault/broker/audit-log.ts
|
|
@@ -57962,35 +58316,37 @@ class VaultBroker {
|
|
|
57962
58316
|
const grantId = dotIdx !== -1 ? req.token.slice(0, dotIdx) : undefined;
|
|
57963
58317
|
const sentinelKey = Object.keys(this.secrets)[0] ?? "__list_check__";
|
|
57964
58318
|
const tokenCheck = await validateGrant(this.grantsDb, req.token, sentinelKey);
|
|
57965
|
-
if (!tokenCheck.ok && tokenCheck.reason
|
|
58319
|
+
if (!tokenCheck.ok && tokenCheck.reason === "grant-revoked") {
|
|
57966
58320
|
this.auditLogger.write({
|
|
57967
58321
|
ts: new Date().toISOString(),
|
|
57968
58322
|
op: "list",
|
|
57969
58323
|
caller: auditCaller,
|
|
57970
58324
|
pid: auditPid,
|
|
57971
58325
|
cgroup: auditCgroup,
|
|
57972
|
-
result: `denied
|
|
58326
|
+
result: `denied:grant-revoked`,
|
|
57973
58327
|
method: "grant",
|
|
57974
58328
|
grant_id: grantId
|
|
57975
58329
|
});
|
|
57976
|
-
socket.write(encodeResponse(errorResponse("DENIED",
|
|
58330
|
+
socket.write(encodeResponse(errorResponse("DENIED", "grant-revoked")));
|
|
58331
|
+
return;
|
|
58332
|
+
}
|
|
58333
|
+
if (tokenCheck.ok || tokenCheck.reason === "grant-key-not-allowed") {
|
|
58334
|
+
const grantRow = tokenCheck.ok ? tokenCheck.grant : this.grantsDb.query("SELECT key_allow FROM vault_grants WHERE id = ?").get(grantId ?? "");
|
|
58335
|
+
const allowedKeys = grantRow ? typeof grantRow.key_allow === "string" ? JSON.parse(grantRow.key_allow) : grantRow.key_allow : [];
|
|
58336
|
+
const visibleKeys2 = allowedKeys.filter((k) => (k in this.secrets));
|
|
58337
|
+
this.auditLogger.write({
|
|
58338
|
+
ts: new Date().toISOString(),
|
|
58339
|
+
op: "list",
|
|
58340
|
+
caller: auditCaller,
|
|
58341
|
+
pid: auditPid,
|
|
58342
|
+
cgroup: auditCgroup,
|
|
58343
|
+
result: `allowed:${visibleKeys2.length}`,
|
|
58344
|
+
method: "grant",
|
|
58345
|
+
grant_id: grantId
|
|
58346
|
+
});
|
|
58347
|
+
socket.write(encodeResponse({ ok: true, keys: visibleKeys2 }));
|
|
57977
58348
|
return;
|
|
57978
58349
|
}
|
|
57979
|
-
const grantRow = tokenCheck.ok ? tokenCheck.grant : this.grantsDb.query("SELECT key_allow FROM vault_grants WHERE id = ?").get(grantId ?? "");
|
|
57980
|
-
const allowedKeys = grantRow ? typeof grantRow.key_allow === "string" ? JSON.parse(grantRow.key_allow) : grantRow.key_allow : [];
|
|
57981
|
-
const visibleKeys2 = allowedKeys.filter((k) => (k in this.secrets));
|
|
57982
|
-
this.auditLogger.write({
|
|
57983
|
-
ts: new Date().toISOString(),
|
|
57984
|
-
op: "list",
|
|
57985
|
-
caller: auditCaller,
|
|
57986
|
-
pid: auditPid,
|
|
57987
|
-
cgroup: auditCgroup,
|
|
57988
|
-
result: `allowed:${visibleKeys2.length}`,
|
|
57989
|
-
method: "grant",
|
|
57990
|
-
grant_id: grantId
|
|
57991
|
-
});
|
|
57992
|
-
socket.write(encodeResponse({ ok: true, keys: visibleKeys2 }));
|
|
57993
|
-
return;
|
|
57994
58350
|
}
|
|
57995
58351
|
if (!isOperator && agentName === null && process.platform === "linux" && peer === null) {
|
|
57996
58352
|
const reason = "Unable to identify caller (peercred unavailable); denying on Linux";
|
|
@@ -58065,10 +58421,9 @@ class VaultBroker {
|
|
|
58065
58421
|
});
|
|
58066
58422
|
socket.write(encodeResponse(entryResponse(entry2)));
|
|
58067
58423
|
return;
|
|
58068
|
-
} else {
|
|
58424
|
+
} else if (grantResult.reason === "grant-revoked") {
|
|
58069
58425
|
const dotIdx = req.token.indexOf(".");
|
|
58070
58426
|
const grantId = dotIdx !== -1 ? req.token.slice(0, dotIdx) : undefined;
|
|
58071
|
-
const denyReason = grantResult.reason;
|
|
58072
58427
|
this.auditLogger.write({
|
|
58073
58428
|
ts: new Date().toISOString(),
|
|
58074
58429
|
op: "get",
|
|
@@ -58076,11 +58431,11 @@ class VaultBroker {
|
|
|
58076
58431
|
caller: auditCaller,
|
|
58077
58432
|
pid: auditPid,
|
|
58078
58433
|
cgroup: auditCgroup,
|
|
58079
|
-
result: `denied
|
|
58434
|
+
result: `denied:grant-revoked`,
|
|
58080
58435
|
method: "grant",
|
|
58081
58436
|
grant_id: grantId
|
|
58082
58437
|
});
|
|
58083
|
-
socket.write(encodeResponse(errorResponse("DENIED",
|
|
58438
|
+
socket.write(encodeResponse(errorResponse("DENIED", "grant-revoked")));
|
|
58084
58439
|
return;
|
|
58085
58440
|
}
|
|
58086
58441
|
}
|
|
@@ -68131,7 +68486,7 @@ async function stepScaffoldAgents(config, agentBots, userId, nonInteractive, swi
|
|
|
68131
68486
|
if (switchroomConfigPath && !config.auth?.active) {
|
|
68132
68487
|
try {
|
|
68133
68488
|
await ensureAuthActiveDefault(switchroomConfigPath);
|
|
68134
|
-
console.log(source_default.gray(" Set auth.active: default \u2014 run `switchroom auth add default --
|
|
68489
|
+
console.log(source_default.gray(" Set auth.active: default \u2014 run `switchroom auth add default --via-claude` to log in"));
|
|
68135
68490
|
} catch (err) {
|
|
68136
68491
|
console.log(source_default.yellow(` \u26a0 Could not set auth.active default: ${err.message}`));
|
|
68137
68492
|
}
|
|
@@ -68410,27 +68765,36 @@ init_doctor();
|
|
|
68410
68765
|
init_source();
|
|
68411
68766
|
init_loader();
|
|
68412
68767
|
init_lifecycle();
|
|
68413
|
-
import { cpSync as cpSync2, existsSync as
|
|
68414
|
-
import { spawnSync as
|
|
68415
|
-
import { join as join42, dirname as
|
|
68768
|
+
import { cpSync as cpSync2, existsSync as existsSync48, mkdirSync as mkdirSync27, readFileSync as readFileSync45, realpathSync as realpathSync5, rmSync as rmSync12, statSync as statSync22 } from "node:fs";
|
|
68769
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
68770
|
+
import { join as join42, dirname as dirname13, resolve as resolve30 } from "node:path";
|
|
68416
68771
|
import { homedir as homedir22 } from "node:os";
|
|
68417
68772
|
var DEFAULT_COMPOSE_PATH = join42(homedir22(), ".switchroom", "compose", "docker-compose.yml");
|
|
68418
68773
|
function isGitCheckout(scriptPath) {
|
|
68419
|
-
let dir =
|
|
68774
|
+
let dir = dirname13(scriptPath);
|
|
68420
68775
|
for (let i = 0;i < 10; i++) {
|
|
68421
|
-
if (
|
|
68776
|
+
if (existsSync48(join42(dir, ".git")))
|
|
68422
68777
|
return true;
|
|
68423
|
-
const parent =
|
|
68778
|
+
const parent = dirname13(dir);
|
|
68424
68779
|
if (parent === dir)
|
|
68425
68780
|
return false;
|
|
68426
68781
|
dir = parent;
|
|
68427
68782
|
}
|
|
68428
68783
|
return false;
|
|
68429
68784
|
}
|
|
68785
|
+
function rebuildRefusalMessage(scriptPath) {
|
|
68786
|
+
if (isGitCheckout(scriptPath))
|
|
68787
|
+
return null;
|
|
68788
|
+
return `--rebuild builds the CLI from a git checkout, but switchroom is ` + `running from "${scriptPath}" \u2014 a published install (no .git ` + `ancestor). Rebuilding from source here would drift this host off ` + `the reviewed, CI-published release. Use the published path:
|
|
68789
|
+
` + `
|
|
68790
|
+
` + ` npm i -g switchroom@latest && switchroom update
|
|
68791
|
+
` + `
|
|
68792
|
+
` + `(\`--rebuild\` is for switchroom maintainers iterating on a source ` + `checkout \u2014 not for published installs.)`;
|
|
68793
|
+
}
|
|
68430
68794
|
function planUpdate(opts) {
|
|
68431
68795
|
const composePath = opts.composePath ?? DEFAULT_COMPOSE_PATH;
|
|
68432
68796
|
const runner = opts.runner ?? defaultRunner;
|
|
68433
|
-
const scriptPath = process.argv[1] ?? "";
|
|
68797
|
+
const scriptPath = opts.scriptPath ?? process.argv[1] ?? "";
|
|
68434
68798
|
const steps = [];
|
|
68435
68799
|
const releaseOverrideArgs = [];
|
|
68436
68800
|
if (opts.channel)
|
|
@@ -68458,7 +68822,7 @@ function planUpdate(opts) {
|
|
|
68458
68822
|
steps.push({
|
|
68459
68823
|
name: "pull-images",
|
|
68460
68824
|
description: "Pull broker / kernel / agent images from GHCR",
|
|
68461
|
-
skipReason: opts.skipImages ? "--skip-images flag set" : !
|
|
68825
|
+
skipReason: opts.skipImages ? "--skip-images flag set" : !existsSync48(composePath) ? `compose file not found at ${composePath} (run \`switchroom apply --compose-only\` first)` : undefined,
|
|
68462
68826
|
run: () => {
|
|
68463
68827
|
const r = runner("docker", [
|
|
68464
68828
|
"compose",
|
|
@@ -68477,8 +68841,9 @@ function planUpdate(opts) {
|
|
|
68477
68841
|
name: "rebuild-source",
|
|
68478
68842
|
description: "git pull upstream main + bun install + npm run build",
|
|
68479
68843
|
run: () => {
|
|
68480
|
-
|
|
68481
|
-
|
|
68844
|
+
const refusal = rebuildRefusalMessage(scriptPath);
|
|
68845
|
+
if (refusal) {
|
|
68846
|
+
throw new Error(refusal);
|
|
68482
68847
|
}
|
|
68483
68848
|
const pull = runner("git", ["pull", "--ff-only", "upstream", "main"]);
|
|
68484
68849
|
if (pull.status !== 0)
|
|
@@ -68537,16 +68902,16 @@ function planUpdate(opts) {
|
|
|
68537
68902
|
}
|
|
68538
68903
|
const source = resolve30(import.meta.dirname, "../../skills");
|
|
68539
68904
|
const dest = join42(homedir22(), ".switchroom", "skills", "_bundled");
|
|
68540
|
-
if (!
|
|
68905
|
+
if (!existsSync48(source)) {
|
|
68541
68906
|
process.stderr.write(`switchroom update: sync-bundled-skills \u2014 CLI bundle has no adjacent skills/ at ${source}; skipping.
|
|
68542
68907
|
`);
|
|
68543
68908
|
return;
|
|
68544
68909
|
}
|
|
68545
68910
|
try {
|
|
68546
|
-
if (
|
|
68911
|
+
if (existsSync48(dest)) {
|
|
68547
68912
|
rmSync12(dest, { recursive: true, force: true });
|
|
68548
68913
|
}
|
|
68549
|
-
mkdirSync27(
|
|
68914
|
+
mkdirSync27(dirname13(dest), { recursive: true });
|
|
68550
68915
|
cpSync2(source, dest, { recursive: true, dereference: false });
|
|
68551
68916
|
} catch (err) {
|
|
68552
68917
|
throw new Error(`sync-bundled-skills failed: ${err.message}`);
|
|
@@ -68605,7 +68970,7 @@ function planUpdate(opts) {
|
|
|
68605
68970
|
return steps;
|
|
68606
68971
|
}
|
|
68607
68972
|
function defaultRunner(cmd, args) {
|
|
68608
|
-
const r =
|
|
68973
|
+
const r = spawnSync8(cmd, args, { stdio: "inherit" });
|
|
68609
68974
|
return { status: r.status ?? 1 };
|
|
68610
68975
|
}
|
|
68611
68976
|
function writeMarkerInPreferredLocation(agent, reason, runner) {
|
|
@@ -68642,16 +69007,16 @@ function defaultStatusProbe(composePath) {
|
|
|
68642
69007
|
let scriptPath = rawScriptPath;
|
|
68643
69008
|
try {
|
|
68644
69009
|
if (rawScriptPath)
|
|
68645
|
-
scriptPath =
|
|
69010
|
+
scriptPath = realpathSync5(rawScriptPath);
|
|
68646
69011
|
} catch {}
|
|
68647
69012
|
if (scriptPath) {
|
|
68648
69013
|
try {
|
|
68649
|
-
cliBuiltAt = new Date(
|
|
69014
|
+
cliBuiltAt = new Date(statSync22(scriptPath).mtimeMs).toISOString();
|
|
68650
69015
|
} catch {}
|
|
68651
|
-
let dir =
|
|
69016
|
+
let dir = dirname13(scriptPath);
|
|
68652
69017
|
for (let i = 0;i < 8; i++) {
|
|
68653
69018
|
const pkgPath = join42(dir, "package.json");
|
|
68654
|
-
if (
|
|
69019
|
+
if (existsSync48(pkgPath)) {
|
|
68655
69020
|
try {
|
|
68656
69021
|
const pkg = JSON.parse(readFileSync45(pkgPath, "utf-8"));
|
|
68657
69022
|
if (typeof pkg.version === "string")
|
|
@@ -68661,7 +69026,7 @@ function defaultStatusProbe(composePath) {
|
|
|
68661
69026
|
}
|
|
68662
69027
|
break;
|
|
68663
69028
|
}
|
|
68664
|
-
const parent =
|
|
69029
|
+
const parent = dirname13(dir);
|
|
68665
69030
|
if (parent === dir)
|
|
68666
69031
|
break;
|
|
68667
69032
|
dir = parent;
|
|
@@ -68674,13 +69039,13 @@ function defaultStatusProbe(composePath) {
|
|
|
68674
69039
|
warnings.push("could not resolve CLI version (no package.json found above the resolved script path)");
|
|
68675
69040
|
}
|
|
68676
69041
|
const services = [];
|
|
68677
|
-
if (!
|
|
69042
|
+
if (!existsSync48(composePath)) {
|
|
68678
69043
|
warnings.push(`compose file not found at ${composePath}; service status unknown`);
|
|
68679
69044
|
return { cliVersion, cliBuiltAt, services, warnings };
|
|
68680
69045
|
}
|
|
68681
69046
|
let serviceList = [];
|
|
68682
69047
|
try {
|
|
68683
|
-
const r =
|
|
69048
|
+
const r = spawnSync8("docker", ["compose", "-p", "switchroom", "-f", composePath, "config", "--services"], { encoding: "utf-8", timeout: 1e4 });
|
|
68684
69049
|
if (r.status !== 0) {
|
|
68685
69050
|
warnings.push(`docker compose config --services failed: ${r.stderr?.trim() ?? r.error?.message ?? "unknown"}`);
|
|
68686
69051
|
return { cliVersion, cliBuiltAt, services, warnings };
|
|
@@ -68697,7 +69062,7 @@ function defaultStatusProbe(composePath) {
|
|
|
68697
69062
|
let containerCreatedAt = null;
|
|
68698
69063
|
let status = "<unknown>";
|
|
68699
69064
|
try {
|
|
68700
|
-
const r =
|
|
69065
|
+
const r = spawnSync8("docker", ["inspect", "-f", "{{.Config.Image}}|{{.Created}}|{{.State.Status}}", containerName2], { encoding: "utf-8", timeout: 5000 });
|
|
68701
69066
|
if (r.status === 0) {
|
|
68702
69067
|
const [img, created, st] = r.stdout.trim().split("|");
|
|
68703
69068
|
image = img ?? null;
|
|
@@ -68713,7 +69078,7 @@ function defaultStatusProbe(composePath) {
|
|
|
68713
69078
|
let imagePulledAt = null;
|
|
68714
69079
|
if (image) {
|
|
68715
69080
|
try {
|
|
68716
|
-
const r =
|
|
69081
|
+
const r = spawnSync8("docker", ["image", "inspect", "-f", "{{.Id}}|{{.Created}}|{{.Metadata.LastTagTime}}", image], { encoding: "utf-8", timeout: 5000 });
|
|
68717
69082
|
if (r.status === 0) {
|
|
68718
69083
|
const [id, created, lastTag] = r.stdout.trim().split("|");
|
|
68719
69084
|
imageDigestShort = id?.replace(/^sha256:/, "").slice(0, 12) ?? null;
|
|
@@ -68798,6 +69163,14 @@ async function runUpdate(opts) {
|
|
|
68798
69163
|
`));
|
|
68799
69164
|
return 2;
|
|
68800
69165
|
}
|
|
69166
|
+
if (opts.rebuild) {
|
|
69167
|
+
const refusal = rebuildRefusalMessage(opts.scriptPath ?? process.argv[1] ?? "");
|
|
69168
|
+
if (refusal) {
|
|
69169
|
+
stderr(source_default.red(refusal + `
|
|
69170
|
+
`));
|
|
69171
|
+
return 2;
|
|
69172
|
+
}
|
|
69173
|
+
}
|
|
68801
69174
|
const steps = planUpdate(opts);
|
|
68802
69175
|
if (opts.check) {
|
|
68803
69176
|
stdout(source_default.bold(`switchroom update --check (dry-run)
|
|
@@ -68835,7 +69208,7 @@ Dry-run only; nothing was changed. Re-run without --check to apply.
|
|
|
68835
69208
|
return 0;
|
|
68836
69209
|
}
|
|
68837
69210
|
function registerUpdateCommand(program3) {
|
|
68838
|
-
program3.command("update").description("Update switchroom on this host: pull images, refresh scaffolds, recreate containers. Wraps the full `pull && apply && up -d` flow.").option("--check", "Dry-run: print the steps that would execute, exit 0.").option("--skip-images", "Skip the docker image pull (offline mode).").option("--rebuild", "Source-checkout
|
|
69211
|
+
program3.command("update").description("Update switchroom on this host: pull images, refresh scaffolds, recreate containers. Wraps the full `pull && apply && up -d` flow.").option("--check", "Dry-run: print the steps that would execute, exit 0.").option("--skip-images", "Skip the docker image pull (offline mode).").option("--rebuild", "Source-checkout / maintainer only: git pull + bun install + npm run build before applying. REFUSED on a published install \u2014 use `npm i -g switchroom@latest && switchroom update` there.").option("--status", "Read-only snapshot: report local CLI version, image digest + pull time, container creation time per service. Does NOT invoke any update steps. Wired by Telegram /upgrade-status (#927).").option("--json", "Output as JSON (currently only honored under --status; other modes ignore).").addOption(new Option("--channel <c>", "Override the resolved release block for this update run: follow the named channel (dev|rc|latest). Mutually exclusive with --pin.").choices(["dev", "rc", "latest"]).conflicts("pin")).addOption(new Option("--pin <p>", "Override the resolved release block for this update run: pin to a specific build (sha-<7-40 hex> or v<semver>). Mutually exclusive with --channel.").conflicts("channel")).option("--force", "[legacy v0.6 no-op]").option("--no-restart", "[legacy v0.6 no-op]").option("--resume <file>", "[legacy v0.6 no-op]").option("--phase <phase>", "[legacy v0.6 no-op]").action(async (opts) => {
|
|
68839
69212
|
if (opts.pin && !/^(sha-[0-9a-f]{7,40}|v\d+\.\d+\.\d+)$/.test(opts.pin)) {
|
|
68840
69213
|
console.error(source_default.red(`--pin "${opts.pin}" is invalid. Expected sha-<7-40 hex> or v<semver>.`));
|
|
68841
69214
|
process.exit(2);
|
|
@@ -68861,8 +69234,8 @@ init_source();
|
|
|
68861
69234
|
init_helpers();
|
|
68862
69235
|
init_lifecycle();
|
|
68863
69236
|
import { execSync as execSync4 } from "node:child_process";
|
|
68864
|
-
import { existsSync as
|
|
68865
|
-
import { dirname as
|
|
69237
|
+
import { existsSync as existsSync49, readFileSync as readFileSync46 } from "node:fs";
|
|
69238
|
+
import { dirname as dirname14, join as join43 } from "node:path";
|
|
68866
69239
|
function getClaudeCodeVersion() {
|
|
68867
69240
|
try {
|
|
68868
69241
|
const out = execSync4("claude --version 2>/dev/null", {
|
|
@@ -68913,15 +69286,15 @@ function locateSwitchroomInstallDir() {
|
|
|
68913
69286
|
let dir = import.meta.dirname;
|
|
68914
69287
|
for (let i = 0;i < 10 && dir && dir !== "/"; i++) {
|
|
68915
69288
|
const pkgPath = join43(dir, "package.json");
|
|
68916
|
-
if (
|
|
69289
|
+
if (existsSync49(pkgPath)) {
|
|
68917
69290
|
try {
|
|
68918
69291
|
const pkg = JSON.parse(readFileSync46(pkgPath, "utf-8"));
|
|
68919
|
-
if (pkg.name === "switchroom" &&
|
|
69292
|
+
if (pkg.name === "switchroom" && existsSync49(join43(dir, ".git"))) {
|
|
68920
69293
|
return dir;
|
|
68921
69294
|
}
|
|
68922
69295
|
} catch {}
|
|
68923
69296
|
}
|
|
68924
|
-
dir =
|
|
69297
|
+
dir = dirname14(dir);
|
|
68925
69298
|
}
|
|
68926
69299
|
return null;
|
|
68927
69300
|
}
|
|
@@ -69135,13 +69508,13 @@ function registerHandoffCommand(program3) {
|
|
|
69135
69508
|
// src/issues/store.ts
|
|
69136
69509
|
import {
|
|
69137
69510
|
closeSync as closeSync10,
|
|
69138
|
-
existsSync as
|
|
69511
|
+
existsSync as existsSync50,
|
|
69139
69512
|
mkdirSync as mkdirSync28,
|
|
69140
69513
|
openSync as openSync10,
|
|
69141
69514
|
readdirSync as readdirSync18,
|
|
69142
69515
|
readFileSync as readFileSync47,
|
|
69143
69516
|
renameSync as renameSync10,
|
|
69144
|
-
statSync as
|
|
69517
|
+
statSync as statSync23,
|
|
69145
69518
|
unlinkSync as unlinkSync10,
|
|
69146
69519
|
writeFileSync as writeFileSync25,
|
|
69147
69520
|
writeSync as writeSync6
|
|
@@ -69467,7 +69840,7 @@ var ISSUES_FILE = "issues.jsonl";
|
|
|
69467
69840
|
var ISSUES_LOCK = "issues.lock";
|
|
69468
69841
|
function readAll(stateDir) {
|
|
69469
69842
|
const path4 = join44(stateDir, ISSUES_FILE);
|
|
69470
|
-
if (!
|
|
69843
|
+
if (!existsSync50(path4))
|
|
69471
69844
|
return [];
|
|
69472
69845
|
let raw;
|
|
69473
69846
|
try {
|
|
@@ -69544,7 +69917,7 @@ function record(stateDir, input, nowFn = Date.now) {
|
|
|
69544
69917
|
});
|
|
69545
69918
|
}
|
|
69546
69919
|
function resolve33(stateDir, fingerprint, nowFn = Date.now) {
|
|
69547
|
-
if (!
|
|
69920
|
+
if (!existsSync50(join44(stateDir, ISSUES_FILE)))
|
|
69548
69921
|
return 0;
|
|
69549
69922
|
return withLock(stateDir, () => {
|
|
69550
69923
|
const all = readAll(stateDir);
|
|
@@ -69562,7 +69935,7 @@ function resolve33(stateDir, fingerprint, nowFn = Date.now) {
|
|
|
69562
69935
|
});
|
|
69563
69936
|
}
|
|
69564
69937
|
function resolveAllBySource(stateDir, source, nowFn = Date.now) {
|
|
69565
|
-
if (!
|
|
69938
|
+
if (!existsSync50(join44(stateDir, ISSUES_FILE)))
|
|
69566
69939
|
return 0;
|
|
69567
69940
|
return withLock(stateDir, () => {
|
|
69568
69941
|
const all = readAll(stateDir);
|
|
@@ -69580,7 +69953,7 @@ function resolveAllBySource(stateDir, source, nowFn = Date.now) {
|
|
|
69580
69953
|
});
|
|
69581
69954
|
}
|
|
69582
69955
|
function prune(stateDir, opts = {}) {
|
|
69583
|
-
if (!
|
|
69956
|
+
if (!existsSync50(join44(stateDir, ISSUES_FILE)))
|
|
69584
69957
|
return 0;
|
|
69585
69958
|
return withLock(stateDir, () => {
|
|
69586
69959
|
const all = readAll(stateDir);
|
|
@@ -69637,7 +70010,7 @@ function sweepOrphanTmpFiles(stateDir) {
|
|
|
69637
70010
|
continue;
|
|
69638
70011
|
const tmpPath = join44(stateDir, entry);
|
|
69639
70012
|
try {
|
|
69640
|
-
const stat =
|
|
70013
|
+
const stat = statSync23(tmpPath);
|
|
69641
70014
|
if (stat.mtimeMs < cutoff) {
|
|
69642
70015
|
unlinkSync10(tmpPath);
|
|
69643
70016
|
}
|
|
@@ -69930,20 +70303,20 @@ function relTime(deltaMs) {
|
|
|
69930
70303
|
|
|
69931
70304
|
// src/cli/deps.ts
|
|
69932
70305
|
init_source();
|
|
69933
|
-
import { existsSync as
|
|
70306
|
+
import { existsSync as existsSync53 } from "node:fs";
|
|
69934
70307
|
import { homedir as homedir25 } from "node:os";
|
|
69935
70308
|
import { join as join47, resolve as resolve34 } from "node:path";
|
|
69936
70309
|
|
|
69937
70310
|
// src/deps/python.ts
|
|
69938
70311
|
import { createHash as createHash9 } from "node:crypto";
|
|
69939
70312
|
import {
|
|
69940
|
-
existsSync as
|
|
70313
|
+
existsSync as existsSync51,
|
|
69941
70314
|
mkdirSync as mkdirSync29,
|
|
69942
70315
|
readFileSync as readFileSync48,
|
|
69943
70316
|
rmSync as rmSync13,
|
|
69944
70317
|
writeFileSync as writeFileSync26
|
|
69945
70318
|
} from "node:fs";
|
|
69946
|
-
import { dirname as
|
|
70319
|
+
import { dirname as dirname15, join as join45 } from "node:path";
|
|
69947
70320
|
import { homedir as homedir23 } from "node:os";
|
|
69948
70321
|
import { execFileSync as execFileSync14 } from "node:child_process";
|
|
69949
70322
|
|
|
@@ -69965,7 +70338,7 @@ function ensurePythonEnv(opts) {
|
|
|
69965
70338
|
const { skillName, requirementsPath, force = false } = opts;
|
|
69966
70339
|
const cacheRoot = opts.cacheRoot ?? defaultPythonCacheRoot();
|
|
69967
70340
|
const hostPython = opts.pythonBin ?? "python3";
|
|
69968
|
-
if (!
|
|
70341
|
+
if (!existsSync51(requirementsPath)) {
|
|
69969
70342
|
throw new PythonEnvError(`requirements file not found: ${requirementsPath}`);
|
|
69970
70343
|
}
|
|
69971
70344
|
const venvDir = join45(cacheRoot, skillName);
|
|
@@ -69974,7 +70347,7 @@ function ensurePythonEnv(opts) {
|
|
|
69974
70347
|
const pythonBin = join45(binDir, "python");
|
|
69975
70348
|
const pipBin = join45(binDir, "pip");
|
|
69976
70349
|
const targetHash = hashFile(requirementsPath);
|
|
69977
|
-
if (!force &&
|
|
70350
|
+
if (!force && existsSync51(stampPath) && existsSync51(pythonBin)) {
|
|
69978
70351
|
const existingHash = readFileSync48(stampPath, "utf8").trim();
|
|
69979
70352
|
if (existingHash === targetHash) {
|
|
69980
70353
|
return {
|
|
@@ -69987,10 +70360,10 @@ function ensurePythonEnv(opts) {
|
|
|
69987
70360
|
};
|
|
69988
70361
|
}
|
|
69989
70362
|
}
|
|
69990
|
-
if (
|
|
70363
|
+
if (existsSync51(venvDir)) {
|
|
69991
70364
|
rmSync13(venvDir, { recursive: true, force: true });
|
|
69992
70365
|
}
|
|
69993
|
-
mkdirSync29(
|
|
70366
|
+
mkdirSync29(dirname15(venvDir), { recursive: true });
|
|
69994
70367
|
try {
|
|
69995
70368
|
execFileSync14(hostPython, ["-m", "venv", venvDir], { stdio: "pipe" });
|
|
69996
70369
|
} catch (err) {
|
|
@@ -70025,13 +70398,13 @@ function ensurePythonEnv(opts) {
|
|
|
70025
70398
|
import { createHash as createHash10 } from "node:crypto";
|
|
70026
70399
|
import {
|
|
70027
70400
|
copyFileSync as copyFileSync9,
|
|
70028
|
-
existsSync as
|
|
70401
|
+
existsSync as existsSync52,
|
|
70029
70402
|
mkdirSync as mkdirSync30,
|
|
70030
70403
|
readFileSync as readFileSync49,
|
|
70031
70404
|
rmSync as rmSync14,
|
|
70032
70405
|
writeFileSync as writeFileSync27
|
|
70033
70406
|
} from "node:fs";
|
|
70034
|
-
import { dirname as
|
|
70407
|
+
import { dirname as dirname16, join as join46 } from "node:path";
|
|
70035
70408
|
import { homedir as homedir24 } from "node:os";
|
|
70036
70409
|
import { execFileSync as execFileSync15 } from "node:child_process";
|
|
70037
70410
|
|
|
@@ -70058,14 +70431,14 @@ function defaultNodeCacheRoot() {
|
|
|
70058
70431
|
return join46(homedir24(), ".switchroom", "deps", "node");
|
|
70059
70432
|
}
|
|
70060
70433
|
function hashDepInputs(packageJsonPath) {
|
|
70061
|
-
const sourceDir =
|
|
70434
|
+
const sourceDir = dirname16(packageJsonPath);
|
|
70062
70435
|
const hasher = createHash10("sha256");
|
|
70063
70436
|
hasher.update(`package.json
|
|
70064
70437
|
`);
|
|
70065
70438
|
hasher.update(readFileSync49(packageJsonPath));
|
|
70066
70439
|
for (const lockName of ALL_LOCKFILES) {
|
|
70067
70440
|
const lockPath = join46(sourceDir, lockName);
|
|
70068
|
-
if (
|
|
70441
|
+
if (existsSync52(lockPath)) {
|
|
70069
70442
|
hasher.update(`
|
|
70070
70443
|
`);
|
|
70071
70444
|
hasher.update(lockName);
|
|
@@ -70080,16 +70453,16 @@ function ensureNodeEnv(opts) {
|
|
|
70080
70453
|
const { skillName, packageJsonPath, force = false } = opts;
|
|
70081
70454
|
const cacheRoot = opts.cacheRoot ?? defaultNodeCacheRoot();
|
|
70082
70455
|
const installer = opts.installer ?? "bun";
|
|
70083
|
-
if (!
|
|
70456
|
+
if (!existsSync52(packageJsonPath)) {
|
|
70084
70457
|
throw new NodeEnvError(`package.json not found: ${packageJsonPath}`);
|
|
70085
70458
|
}
|
|
70086
|
-
const sourceDir =
|
|
70459
|
+
const sourceDir = dirname16(packageJsonPath);
|
|
70087
70460
|
const envDir = join46(cacheRoot, skillName);
|
|
70088
70461
|
const stampPath = join46(envDir, ".package.sha256");
|
|
70089
70462
|
const nodeModulesDir = join46(envDir, "node_modules");
|
|
70090
70463
|
const binDir = join46(nodeModulesDir, ".bin");
|
|
70091
70464
|
const targetHash = hashDepInputs(packageJsonPath);
|
|
70092
|
-
if (!force &&
|
|
70465
|
+
if (!force && existsSync52(stampPath) && existsSync52(nodeModulesDir)) {
|
|
70093
70466
|
const existingHash = readFileSync49(stampPath, "utf8").trim();
|
|
70094
70467
|
if (existingHash === targetHash) {
|
|
70095
70468
|
return {
|
|
@@ -70101,7 +70474,7 @@ function ensureNodeEnv(opts) {
|
|
|
70101
70474
|
};
|
|
70102
70475
|
}
|
|
70103
70476
|
}
|
|
70104
|
-
if (
|
|
70477
|
+
if (existsSync52(envDir)) {
|
|
70105
70478
|
rmSync14(envDir, { recursive: true, force: true });
|
|
70106
70479
|
}
|
|
70107
70480
|
mkdirSync30(envDir, { recursive: true });
|
|
@@ -70109,7 +70482,7 @@ function ensureNodeEnv(opts) {
|
|
|
70109
70482
|
let copiedLockfile = false;
|
|
70110
70483
|
for (const lockName of LOCKFILES_FOR[installer]) {
|
|
70111
70484
|
const lockPath = join46(sourceDir, lockName);
|
|
70112
|
-
if (
|
|
70485
|
+
if (existsSync52(lockPath)) {
|
|
70113
70486
|
copyFileSync9(lockPath, join46(envDir, lockName));
|
|
70114
70487
|
copiedLockfile = true;
|
|
70115
70488
|
}
|
|
@@ -70145,22 +70518,22 @@ function registerDepsCommand(program3) {
|
|
|
70145
70518
|
const deps = program3.command("deps").description("Manage cached per-skill dependency environments");
|
|
70146
70519
|
deps.command("rebuild <skill>").description("Rebuild the Python venv and/or Node node_modules cache for a skill").option("-p, --python", "Rebuild only the Python env").option("-n, --node", "Rebuild only the Node env").action(async (skill, opts) => {
|
|
70147
70520
|
const skillsRoot = builtinSkillsRoot();
|
|
70148
|
-
if (!
|
|
70521
|
+
if (!existsSync53(skillsRoot)) {
|
|
70149
70522
|
console.error(source_default.red(`Bundled skills pool dir not found at ${skillsRoot} \u2014 run \`switchroom update\` to install it.`));
|
|
70150
70523
|
process.exit(1);
|
|
70151
70524
|
}
|
|
70152
70525
|
const skillDir = join47(skillsRoot, skill);
|
|
70153
|
-
if (!
|
|
70526
|
+
if (!existsSync53(skillDir)) {
|
|
70154
70527
|
console.error(source_default.red(`Unknown skill: ${skill} (no dir at ${skillDir})`));
|
|
70155
70528
|
process.exit(1);
|
|
70156
70529
|
}
|
|
70157
70530
|
const requirementsPath = join47(skillDir, "requirements.txt");
|
|
70158
70531
|
const packageJsonPath = join47(skillDir, "package.json");
|
|
70159
|
-
const wantPython = opts.python ?? (!opts.python && !opts.node &&
|
|
70160
|
-
const wantNode = opts.node ?? (!opts.python && !opts.node &&
|
|
70532
|
+
const wantPython = opts.python ?? (!opts.python && !opts.node && existsSync53(requirementsPath));
|
|
70533
|
+
const wantNode = opts.node ?? (!opts.python && !opts.node && existsSync53(packageJsonPath));
|
|
70161
70534
|
let did = 0;
|
|
70162
70535
|
if (wantPython) {
|
|
70163
|
-
if (!
|
|
70536
|
+
if (!existsSync53(requirementsPath)) {
|
|
70164
70537
|
console.error(source_default.red(`Skill "${skill}" has no requirements.txt at ${requirementsPath}`));
|
|
70165
70538
|
process.exit(1);
|
|
70166
70539
|
}
|
|
@@ -70184,7 +70557,7 @@ function registerDepsCommand(program3) {
|
|
|
70184
70557
|
}
|
|
70185
70558
|
}
|
|
70186
70559
|
if (wantNode) {
|
|
70187
|
-
if (!
|
|
70560
|
+
if (!existsSync53(packageJsonPath)) {
|
|
70188
70561
|
console.error(source_default.red(`Skill "${skill}" has no package.json at ${packageJsonPath}`));
|
|
70189
70562
|
process.exit(1);
|
|
70190
70563
|
}
|
|
@@ -70217,9 +70590,9 @@ function registerDepsCommand(program3) {
|
|
|
70217
70590
|
// src/cli/workspace.ts
|
|
70218
70591
|
init_helpers();
|
|
70219
70592
|
init_loader();
|
|
70220
|
-
import { existsSync as
|
|
70593
|
+
import { existsSync as existsSync54 } from "node:fs";
|
|
70221
70594
|
import { resolve as resolve35, sep as sep2 } from "node:path";
|
|
70222
|
-
import { spawnSync as
|
|
70595
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
70223
70596
|
|
|
70224
70597
|
// src/agents/workspace.ts
|
|
70225
70598
|
import { readFile, stat } from "node:fs/promises";
|
|
@@ -70932,7 +71305,7 @@ function registerWorkspaceCommand(program3) {
|
|
|
70932
71305
|
process.exit(1);
|
|
70933
71306
|
}
|
|
70934
71307
|
const editor = process.env["EDITOR"] ?? process.env["VISUAL"] ?? "vi";
|
|
70935
|
-
const child =
|
|
71308
|
+
const child = spawnSync9(editor, [target], { stdio: "inherit" });
|
|
70936
71309
|
if (child.status !== 0 && child.status !== null) {
|
|
70937
71310
|
process.exit(child.status);
|
|
70938
71311
|
}
|
|
@@ -70994,12 +71367,12 @@ function registerWorkspaceCommand(program3) {
|
|
|
70994
71367
|
if (!dir)
|
|
70995
71368
|
return;
|
|
70996
71369
|
const gitDir = resolve35(dir, ".git");
|
|
70997
|
-
if (!
|
|
71370
|
+
if (!existsSync54(gitDir)) {
|
|
70998
71371
|
process.stdout.write(`Workspace is not a git repository. Re-run \`switchroom agent create ${agentName}\` ` + `or manually \`git init\` in ${dir} to enable versioning.
|
|
70999
71372
|
`);
|
|
71000
71373
|
return;
|
|
71001
71374
|
}
|
|
71002
|
-
const statusResult =
|
|
71375
|
+
const statusResult = spawnSync9("git", ["status", "--short"], {
|
|
71003
71376
|
cwd: dir,
|
|
71004
71377
|
encoding: "utf-8"
|
|
71005
71378
|
});
|
|
@@ -71014,7 +71387,7 @@ function registerWorkspaceCommand(program3) {
|
|
|
71014
71387
|
return;
|
|
71015
71388
|
}
|
|
71016
71389
|
const message = opts.message || `checkpoint: ${new Date().toISOString()}`;
|
|
71017
|
-
const addResult =
|
|
71390
|
+
const addResult = spawnSync9("git", ["add", "-A"], {
|
|
71018
71391
|
cwd: dir,
|
|
71019
71392
|
encoding: "utf-8"
|
|
71020
71393
|
});
|
|
@@ -71023,7 +71396,7 @@ function registerWorkspaceCommand(program3) {
|
|
|
71023
71396
|
`);
|
|
71024
71397
|
process.exit(1);
|
|
71025
71398
|
}
|
|
71026
|
-
const commitResult =
|
|
71399
|
+
const commitResult = spawnSync9("git", ["commit", "-m", message], {
|
|
71027
71400
|
cwd: dir,
|
|
71028
71401
|
encoding: "utf-8"
|
|
71029
71402
|
});
|
|
@@ -71032,7 +71405,7 @@ function registerWorkspaceCommand(program3) {
|
|
|
71032
71405
|
`);
|
|
71033
71406
|
process.exit(1);
|
|
71034
71407
|
}
|
|
71035
|
-
const shaResult =
|
|
71408
|
+
const shaResult = spawnSync9("git", ["rev-parse", "--short", "HEAD"], {
|
|
71036
71409
|
cwd: dir,
|
|
71037
71410
|
encoding: "utf-8"
|
|
71038
71411
|
});
|
|
@@ -71048,12 +71421,12 @@ function registerWorkspaceCommand(program3) {
|
|
|
71048
71421
|
if (!dir)
|
|
71049
71422
|
return;
|
|
71050
71423
|
const gitDir = resolve35(dir, ".git");
|
|
71051
|
-
if (!
|
|
71424
|
+
if (!existsSync54(gitDir)) {
|
|
71052
71425
|
process.stdout.write(`Workspace is not a git repository.
|
|
71053
71426
|
`);
|
|
71054
71427
|
return;
|
|
71055
71428
|
}
|
|
71056
|
-
const child =
|
|
71429
|
+
const child = spawnSync9("git", ["status", "--short"], {
|
|
71057
71430
|
cwd: dir,
|
|
71058
71431
|
stdio: "inherit"
|
|
71059
71432
|
});
|
|
@@ -71073,7 +71446,7 @@ function resolveAgentWorkspaceDirOrExit(program3, agentName) {
|
|
|
71073
71446
|
const agentsDir = resolveAgentsDir(config);
|
|
71074
71447
|
const agentDir = resolve35(agentsDir, agentName);
|
|
71075
71448
|
const dir = resolveAgentWorkspaceDir(agentDir);
|
|
71076
|
-
if (!
|
|
71449
|
+
if (!existsSync54(dir)) {
|
|
71077
71450
|
process.stderr.write(`workspace: ${dir} does not exist yet. Run \`switchroom setup\` or \`switchroom agent scaffold ${agentName}\` to seed it.
|
|
71078
71451
|
`);
|
|
71079
71452
|
return;
|
|
@@ -71109,7 +71482,7 @@ function safeParseInt(value, fallback) {
|
|
|
71109
71482
|
init_helpers();
|
|
71110
71483
|
init_loader();
|
|
71111
71484
|
init_merge();
|
|
71112
|
-
import { copyFileSync as copyFileSync10, existsSync as
|
|
71485
|
+
import { copyFileSync as copyFileSync10, existsSync as existsSync55, readFileSync as readFileSync50, writeFileSync as writeFileSync28 } from "node:fs";
|
|
71113
71486
|
import { join as join48, resolve as resolve36 } from "node:path";
|
|
71114
71487
|
init_schema();
|
|
71115
71488
|
function resolveSoulTargetOrExit(program3, agentName) {
|
|
@@ -71125,7 +71498,7 @@ function resolveSoulTargetOrExit(program3, agentName) {
|
|
|
71125
71498
|
const agentsDir = resolveAgentsDir(config);
|
|
71126
71499
|
const agentDir = resolve36(agentsDir, agentName);
|
|
71127
71500
|
const workspaceDir = resolveAgentWorkspaceDir(agentDir);
|
|
71128
|
-
if (!
|
|
71501
|
+
if (!existsSync55(workspaceDir)) {
|
|
71129
71502
|
console.error(`soul: ${workspaceDir} does not exist yet. Run \`switchroom setup\` ` + `or \`switchroom agent scaffold ${agentName}\` to seed it.`);
|
|
71130
71503
|
process.exit(1);
|
|
71131
71504
|
}
|
|
@@ -71151,7 +71524,7 @@ function registerSoulCommand(program3) {
|
|
|
71151
71524
|
const t = resolveSoulTargetOrExit(program3, agentName);
|
|
71152
71525
|
if (!t)
|
|
71153
71526
|
return;
|
|
71154
|
-
if (!
|
|
71527
|
+
if (!existsSync55(t.soulPath)) {
|
|
71155
71528
|
console.error(`soul: ${t.soulPath} does not exist yet \u2014 run ` + `\`switchroom soul reset ${agentName}\` to seed it.`);
|
|
71156
71529
|
process.exit(1);
|
|
71157
71530
|
}
|
|
@@ -71166,7 +71539,7 @@ function registerSoulCommand(program3) {
|
|
|
71166
71539
|
console.error(`soul: profile "${t.profileName}" ships no SOUL.md.hbs \u2014 ` + `nothing to re-seed from.`);
|
|
71167
71540
|
process.exit(1);
|
|
71168
71541
|
}
|
|
71169
|
-
const exists =
|
|
71542
|
+
const exists = existsSync55(t.soulPath);
|
|
71170
71543
|
if (exists && !opts.yes) {
|
|
71171
71544
|
if (!isInteractive()) {
|
|
71172
71545
|
console.error(`soul: ${t.soulPath} already exists. Re-run with --yes to ` + `replace it (the current file is backed up to SOUL.md.bak).`);
|
|
@@ -71181,7 +71554,7 @@ function registerSoulCommand(program3) {
|
|
|
71181
71554
|
let backupPath;
|
|
71182
71555
|
if (exists) {
|
|
71183
71556
|
backupPath = `${t.soulPath}.bak`;
|
|
71184
|
-
if (
|
|
71557
|
+
if (existsSync55(backupPath)) {
|
|
71185
71558
|
backupPath = `${t.soulPath}.bak.${Date.now()}`;
|
|
71186
71559
|
}
|
|
71187
71560
|
copyFileSync10(t.soulPath, backupPath);
|
|
@@ -71200,7 +71573,7 @@ function registerSoulCommand(program3) {
|
|
|
71200
71573
|
// src/cli/debug.ts
|
|
71201
71574
|
init_helpers();
|
|
71202
71575
|
init_loader();
|
|
71203
|
-
import { existsSync as
|
|
71576
|
+
import { existsSync as existsSync56, readFileSync as readFileSync51, readdirSync as readdirSync19, statSync as statSync24 } from "node:fs";
|
|
71204
71577
|
import { resolve as resolve37, join as join49 } from "node:path";
|
|
71205
71578
|
import { createHash as createHash11 } from "node:crypto";
|
|
71206
71579
|
init_merge();
|
|
@@ -71216,7 +71589,7 @@ function sha256(content) {
|
|
|
71216
71589
|
}
|
|
71217
71590
|
function findLatestTranscriptJsonl(claudeConfigDir) {
|
|
71218
71591
|
const projectsDir = join49(claudeConfigDir, "projects");
|
|
71219
|
-
if (!
|
|
71592
|
+
if (!existsSync56(projectsDir))
|
|
71220
71593
|
return;
|
|
71221
71594
|
try {
|
|
71222
71595
|
const entries = readdirSync19(projectsDir, { withFileTypes: true });
|
|
@@ -71226,9 +71599,9 @@ function findLatestTranscriptJsonl(claudeConfigDir) {
|
|
|
71226
71599
|
continue;
|
|
71227
71600
|
const projectPath = join49(projectsDir, entry.name);
|
|
71228
71601
|
const transcriptPath = join49(projectPath, "transcript.jsonl");
|
|
71229
|
-
if (!
|
|
71602
|
+
if (!existsSync56(transcriptPath))
|
|
71230
71603
|
continue;
|
|
71231
|
-
const stat3 =
|
|
71604
|
+
const stat3 = statSync24(transcriptPath);
|
|
71232
71605
|
if (!latest || stat3.mtimeMs > latest.mtime) {
|
|
71233
71606
|
latest = { path: transcriptPath, mtime: stat3.mtimeMs };
|
|
71234
71607
|
}
|
|
@@ -71289,7 +71662,7 @@ function registerDebugCommand(program3) {
|
|
|
71289
71662
|
}
|
|
71290
71663
|
const agentsDir = resolveAgentsDir(config);
|
|
71291
71664
|
const agentDir = resolve37(agentsDir, agentName);
|
|
71292
|
-
if (!
|
|
71665
|
+
if (!existsSync56(agentDir)) {
|
|
71293
71666
|
console.error(`Agent directory not found: ${agentDir}`);
|
|
71294
71667
|
process.exit(1);
|
|
71295
71668
|
}
|
|
@@ -71344,7 +71717,7 @@ function registerDebugCommand(program3) {
|
|
|
71344
71717
|
}
|
|
71345
71718
|
console.log(`=== Append System Prompt (per-session) ===
|
|
71346
71719
|
`);
|
|
71347
|
-
const handoffContent =
|
|
71720
|
+
const handoffContent = existsSync56(handoffPath) ? readFileSync51(handoffPath, "utf-8") : "";
|
|
71348
71721
|
if (handoffContent.trim().length > 0) {
|
|
71349
71722
|
console.log(`-- Handoff Briefing (${formatBytes(handoffContent.length)}) --`);
|
|
71350
71723
|
console.log(handoffContent);
|
|
@@ -71355,7 +71728,7 @@ function registerDebugCommand(program3) {
|
|
|
71355
71728
|
}
|
|
71356
71729
|
console.log(`=== CLAUDE.md (auto-loaded by Claude Code) ===
|
|
71357
71730
|
`);
|
|
71358
|
-
const claudeMdContent =
|
|
71731
|
+
const claudeMdContent = existsSync56(claudeMdPath) ? readFileSync51(claudeMdPath, "utf-8") : "";
|
|
71359
71732
|
if (claudeMdContent.trim().length > 0) {
|
|
71360
71733
|
console.log(`(${formatBytes(claudeMdContent.length)})`);
|
|
71361
71734
|
console.log(claudeMdContent);
|
|
@@ -71366,7 +71739,7 @@ function registerDebugCommand(program3) {
|
|
|
71366
71739
|
}
|
|
71367
71740
|
console.log(`=== Persona (SOUL.md) ===
|
|
71368
71741
|
`);
|
|
71369
|
-
const soulMdContent =
|
|
71742
|
+
const soulMdContent = existsSync56(soulMdPath) ? readFileSync51(soulMdPath, "utf-8") : existsSync56(workspaceSoulMdPath) ? readFileSync51(workspaceSoulMdPath, "utf-8") : "";
|
|
71370
71743
|
if (soulMdContent.trim().length > 0) {
|
|
71371
71744
|
console.log(`(${formatBytes(soulMdContent.length)})`);
|
|
71372
71745
|
console.log(soulMdContent);
|
|
@@ -71447,7 +71820,7 @@ init_source();
|
|
|
71447
71820
|
|
|
71448
71821
|
// src/worktree/claim.ts
|
|
71449
71822
|
import { execFileSync as execFileSync16 } from "node:child_process";
|
|
71450
|
-
import { closeSync as closeSync11, mkdirSync as mkdirSync32, openSync as openSync11, existsSync as
|
|
71823
|
+
import { closeSync as closeSync11, mkdirSync as mkdirSync32, openSync as openSync11, existsSync as existsSync58, unlinkSync as unlinkSync12 } from "node:fs";
|
|
71451
71824
|
import { join as join51, resolve as resolve39 } from "node:path";
|
|
71452
71825
|
import { homedir as homedir27 } from "node:os";
|
|
71453
71826
|
import { randomBytes as randomBytes11 } from "node:crypto";
|
|
@@ -71459,7 +71832,7 @@ import {
|
|
|
71459
71832
|
readFileSync as readFileSync52,
|
|
71460
71833
|
readdirSync as readdirSync20,
|
|
71461
71834
|
unlinkSync as unlinkSync11,
|
|
71462
|
-
existsSync as
|
|
71835
|
+
existsSync as existsSync57,
|
|
71463
71836
|
renameSync as renameSync11
|
|
71464
71837
|
} from "node:fs";
|
|
71465
71838
|
import { join as join50, resolve as resolve38 } from "node:path";
|
|
@@ -71573,7 +71946,7 @@ function expandHome(p) {
|
|
|
71573
71946
|
}
|
|
71574
71947
|
async function claimWorktree(input, codeRepos) {
|
|
71575
71948
|
const repoPath = resolveRepoPath(input.repo, codeRepos);
|
|
71576
|
-
if (!
|
|
71949
|
+
if (!existsSync58(repoPath)) {
|
|
71577
71950
|
throw new Error(`Repository path does not exist: ${repoPath}`);
|
|
71578
71951
|
}
|
|
71579
71952
|
let concurrencyCap = DEFAULT_CONCURRENCY;
|
|
@@ -71627,7 +72000,7 @@ async function claimWorktree(input, codeRepos) {
|
|
|
71627
72000
|
|
|
71628
72001
|
// src/worktree/release.ts
|
|
71629
72002
|
import { execFileSync as execFileSync17 } from "node:child_process";
|
|
71630
|
-
import { existsSync as
|
|
72003
|
+
import { existsSync as existsSync59 } from "node:fs";
|
|
71631
72004
|
function releaseWorktree(input) {
|
|
71632
72005
|
const { id } = input;
|
|
71633
72006
|
const record2 = readRecord(id);
|
|
@@ -71635,7 +72008,7 @@ function releaseWorktree(input) {
|
|
|
71635
72008
|
return { released: true };
|
|
71636
72009
|
}
|
|
71637
72010
|
let gitSuccess = true;
|
|
71638
|
-
if (
|
|
72011
|
+
if (existsSync59(record2.path)) {
|
|
71639
72012
|
try {
|
|
71640
72013
|
execFileSync17("git", ["worktree", "remove", "--force", record2.path], {
|
|
71641
72014
|
cwd: record2.repo,
|
|
@@ -71674,7 +72047,7 @@ function listWorktrees() {
|
|
|
71674
72047
|
|
|
71675
72048
|
// src/worktree/reaper.ts
|
|
71676
72049
|
import { execFileSync as execFileSync18 } from "node:child_process";
|
|
71677
|
-
import { existsSync as
|
|
72050
|
+
import { existsSync as existsSync60 } from "node:fs";
|
|
71678
72051
|
var STALE_THRESHOLD_MS = 10 * 60 * 1000;
|
|
71679
72052
|
function isPathInUse(path7) {
|
|
71680
72053
|
try {
|
|
@@ -71701,7 +72074,7 @@ function hasUncommittedChanges(repoPath, worktreePath) {
|
|
|
71701
72074
|
function reapRecord(record2) {
|
|
71702
72075
|
const { id, path: path7, repo, branch, ownerAgent } = record2;
|
|
71703
72076
|
let warning = null;
|
|
71704
|
-
if (
|
|
72077
|
+
if (existsSync60(path7)) {
|
|
71705
72078
|
if (hasUncommittedChanges(repo, path7)) {
|
|
71706
72079
|
warning = `[worktree-reaper] Reaped worktree with uncommitted changes: ` + `id=${id} branch=${branch} agent=${ownerAgent ?? "unknown"} path=${path7}`;
|
|
71707
72080
|
}
|
|
@@ -71722,7 +72095,7 @@ function runReaper(nowMs) {
|
|
|
71722
72095
|
const warnings = [];
|
|
71723
72096
|
for (const record2 of records) {
|
|
71724
72097
|
const heartbeatAge = now - new Date(record2.heartbeatAt).getTime();
|
|
71725
|
-
const worktreeExists =
|
|
72098
|
+
const worktreeExists = existsSync60(record2.path);
|
|
71726
72099
|
if (!worktreeExists) {
|
|
71727
72100
|
deleteRecord(record2.id);
|
|
71728
72101
|
reaped.push(record2.id);
|
|
@@ -72177,7 +72550,7 @@ function registerDriveMcpLauncherCommand(program3) {
|
|
|
72177
72550
|
|
|
72178
72551
|
// src/cli/apply.ts
|
|
72179
72552
|
init_source();
|
|
72180
|
-
import { accessSync as
|
|
72553
|
+
import { accessSync as accessSync3, constants as fsConstants6, copyFileSync as copyFileSync11, existsSync as existsSync64, mkdirSync as mkdirSync35, readdirSync as readdirSync23, renameSync as renameSync12, writeFileSync as writeFileSync32 } from "node:fs";
|
|
72181
72554
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
72182
72555
|
import { spawnSync as childSpawnSync } from "node:child_process";
|
|
72183
72556
|
import readline from "node:readline";
|
|
@@ -72522,7 +72895,7 @@ agents:
|
|
|
72522
72895
|
|
|
72523
72896
|
// src/cli/apply.ts
|
|
72524
72897
|
init_resolver();
|
|
72525
|
-
import { dirname as
|
|
72898
|
+
import { dirname as dirname19, join as join56, resolve as resolve41 } from "node:path";
|
|
72526
72899
|
import { homedir as homedir29 } from "node:os";
|
|
72527
72900
|
import { execFileSync as execFileSync19 } from "node:child_process";
|
|
72528
72901
|
init_vault();
|
|
@@ -72530,7 +72903,7 @@ init_loader();
|
|
|
72530
72903
|
init_loader();
|
|
72531
72904
|
|
|
72532
72905
|
// src/cli/update-prompt-hook.ts
|
|
72533
|
-
import { existsSync as
|
|
72906
|
+
import { existsSync as existsSync61, readFileSync as readFileSync53, writeFileSync as writeFileSync31, chmodSync as chmodSync10, mkdirSync as mkdirSync34 } from "node:fs";
|
|
72534
72907
|
import { join as join53 } from "node:path";
|
|
72535
72908
|
var HOOK_FILENAME = "update-card-on-prompt.sh";
|
|
72536
72909
|
function updatePromptHookScript() {
|
|
@@ -72602,7 +72975,7 @@ function installUpdatePromptHook(agentDir) {
|
|
|
72602
72975
|
const scriptPath = join53(hooksDir, HOOK_FILENAME);
|
|
72603
72976
|
const desired = updatePromptHookScript();
|
|
72604
72977
|
let installed = false;
|
|
72605
|
-
const existing =
|
|
72978
|
+
const existing = existsSync61(scriptPath) ? readFileSync53(scriptPath, "utf-8") : "";
|
|
72606
72979
|
if (existing !== desired) {
|
|
72607
72980
|
writeFileSync31(scriptPath, desired, { mode: 493 });
|
|
72608
72981
|
chmodSync10(scriptPath, 493);
|
|
@@ -72613,7 +72986,7 @@ function installUpdatePromptHook(agentDir) {
|
|
|
72613
72986
|
} catch {}
|
|
72614
72987
|
}
|
|
72615
72988
|
const settingsPath = join53(agentDir, ".claude", "settings.json");
|
|
72616
|
-
if (!
|
|
72989
|
+
if (!existsSync61(settingsPath)) {
|
|
72617
72990
|
return { scriptPath, settingsPath, installed };
|
|
72618
72991
|
}
|
|
72619
72992
|
const raw = readFileSync53(settingsPath, "utf-8");
|
|
@@ -72729,24 +73102,114 @@ function detectInstallType() {
|
|
|
72729
73102
|
}
|
|
72730
73103
|
}
|
|
72731
73104
|
|
|
73105
|
+
// src/cli/operator-uid.ts
|
|
73106
|
+
import {
|
|
73107
|
+
chownSync as chownSync2,
|
|
73108
|
+
existsSync as existsSync63,
|
|
73109
|
+
lstatSync as lstatSync7,
|
|
73110
|
+
readdirSync as readdirSync22,
|
|
73111
|
+
realpathSync as realpathSync6,
|
|
73112
|
+
statSync as statSync25
|
|
73113
|
+
} from "node:fs";
|
|
73114
|
+
import { join as join55 } from "node:path";
|
|
73115
|
+
function resolveOperatorUid() {
|
|
73116
|
+
const sudoUid = process.env.SUDO_UID;
|
|
73117
|
+
if (sudoUid !== undefined) {
|
|
73118
|
+
const parsed = parseInt(sudoUid, 10);
|
|
73119
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
73120
|
+
return parsed;
|
|
73121
|
+
}
|
|
73122
|
+
if (typeof process.getuid === "function") {
|
|
73123
|
+
const uid = process.getuid();
|
|
73124
|
+
if (uid > 0)
|
|
73125
|
+
return uid;
|
|
73126
|
+
}
|
|
73127
|
+
return;
|
|
73128
|
+
}
|
|
73129
|
+
function operatorOwnedPaths(home2) {
|
|
73130
|
+
const root = join55(home2, ".switchroom");
|
|
73131
|
+
return [
|
|
73132
|
+
join55(root, "vault"),
|
|
73133
|
+
join55(root, "vault-auto-unlock"),
|
|
73134
|
+
join55(root, "vault-audit.log"),
|
|
73135
|
+
join55(root, "host-control-audit.log"),
|
|
73136
|
+
join55(root, "accounts"),
|
|
73137
|
+
join55(root, "compose")
|
|
73138
|
+
];
|
|
73139
|
+
}
|
|
73140
|
+
function restoreOperatorOwnership(home2, operatorUid, deps = {}) {
|
|
73141
|
+
const chown = deps.chown ?? ((p, u, g) => chownSync2(p, u, g));
|
|
73142
|
+
const exists = deps.exists ?? ((p) => existsSync63(p));
|
|
73143
|
+
const isSymlink = deps.isSymlink ?? ((p) => {
|
|
73144
|
+
try {
|
|
73145
|
+
return lstatSync7(p).isSymbolicLink();
|
|
73146
|
+
} catch {
|
|
73147
|
+
return false;
|
|
73148
|
+
}
|
|
73149
|
+
});
|
|
73150
|
+
const isDir = deps.isDir ?? ((p) => {
|
|
73151
|
+
try {
|
|
73152
|
+
return statSync25(p).isDirectory();
|
|
73153
|
+
} catch {
|
|
73154
|
+
return false;
|
|
73155
|
+
}
|
|
73156
|
+
});
|
|
73157
|
+
const realpath2 = deps.realpath ?? ((p) => {
|
|
73158
|
+
try {
|
|
73159
|
+
return realpathSync6(p);
|
|
73160
|
+
} catch {
|
|
73161
|
+
return p;
|
|
73162
|
+
}
|
|
73163
|
+
});
|
|
73164
|
+
const readdir2 = deps.readdir ?? ((p) => {
|
|
73165
|
+
try {
|
|
73166
|
+
return readdirSync22(p);
|
|
73167
|
+
} catch {
|
|
73168
|
+
return [];
|
|
73169
|
+
}
|
|
73170
|
+
});
|
|
73171
|
+
const chowned = [];
|
|
73172
|
+
const seen = new Set;
|
|
73173
|
+
const visit = (path8) => {
|
|
73174
|
+
if (!exists(path8))
|
|
73175
|
+
return;
|
|
73176
|
+
const target = isSymlink(path8) ? realpath2(path8) : path8;
|
|
73177
|
+
if (seen.has(target))
|
|
73178
|
+
return;
|
|
73179
|
+
seen.add(target);
|
|
73180
|
+
try {
|
|
73181
|
+
chown(target, operatorUid, operatorUid);
|
|
73182
|
+
chowned.push(target);
|
|
73183
|
+
} catch {}
|
|
73184
|
+
if (isDir(target)) {
|
|
73185
|
+
for (const entry of readdir2(target)) {
|
|
73186
|
+
visit(join55(target, entry));
|
|
73187
|
+
}
|
|
73188
|
+
}
|
|
73189
|
+
};
|
|
73190
|
+
for (const p of operatorOwnedPaths(home2))
|
|
73191
|
+
visit(p);
|
|
73192
|
+
return chowned;
|
|
73193
|
+
}
|
|
73194
|
+
|
|
72732
73195
|
// src/cli/apply.ts
|
|
72733
73196
|
var EMBEDDED_EXAMPLES = {
|
|
72734
73197
|
switchroom: switchroom_default,
|
|
72735
73198
|
minimal: minimal_default
|
|
72736
73199
|
};
|
|
72737
|
-
var DEFAULT_COMPOSE_PATH2 =
|
|
73200
|
+
var DEFAULT_COMPOSE_PATH2 = join56(homedir29(), ".switchroom", "compose", "docker-compose.yml");
|
|
72738
73201
|
var COMPOSE_PROJECT2 = "switchroom";
|
|
72739
73202
|
function resolveVaultBindMountDir(homeDir, ctx) {
|
|
72740
73203
|
const isCustomPath = ctx.migrationKind === "custom-path-skipped";
|
|
72741
73204
|
if (isCustomPath && ctx.customVaultPath) {
|
|
72742
|
-
return
|
|
73205
|
+
return dirname19(ctx.customVaultPath);
|
|
72743
73206
|
}
|
|
72744
|
-
return
|
|
73207
|
+
return join56(homeDir, ".switchroom", "vault");
|
|
72745
73208
|
}
|
|
72746
73209
|
function inspectVaultBindMountDir(vaultDir) {
|
|
72747
|
-
if (!
|
|
73210
|
+
if (!existsSync64(vaultDir))
|
|
72748
73211
|
return { kind: "missing" };
|
|
72749
|
-
const entries =
|
|
73212
|
+
const entries = readdirSync23(vaultDir);
|
|
72750
73213
|
const unknown = [];
|
|
72751
73214
|
for (const name of entries) {
|
|
72752
73215
|
if (KNOWN_VAULT_ARTIFACT_NAMES.has(name))
|
|
@@ -72772,30 +73235,30 @@ function hasVaultRefs(value) {
|
|
|
72772
73235
|
async function ensureHostMountSources(config) {
|
|
72773
73236
|
const home2 = homedir29();
|
|
72774
73237
|
const dirs = [
|
|
72775
|
-
|
|
72776
|
-
|
|
72777
|
-
|
|
72778
|
-
|
|
72779
|
-
|
|
73238
|
+
join56(home2, ".switchroom", "approvals"),
|
|
73239
|
+
join56(home2, ".switchroom", "scheduler"),
|
|
73240
|
+
join56(home2, ".switchroom", "logs"),
|
|
73241
|
+
join56(home2, ".switchroom", "compose"),
|
|
73242
|
+
join56(home2, ".switchroom", "broker-operator")
|
|
72780
73243
|
];
|
|
72781
73244
|
for (const name of Object.keys(config.agents)) {
|
|
72782
|
-
dirs.push(
|
|
72783
|
-
dirs.push(
|
|
72784
|
-
dirs.push(
|
|
73245
|
+
dirs.push(join56(home2, ".switchroom", "agents", name));
|
|
73246
|
+
dirs.push(join56(home2, ".switchroom", "logs", name));
|
|
73247
|
+
dirs.push(join56(home2, ".claude", "projects", name));
|
|
72785
73248
|
}
|
|
72786
73249
|
for (const dir of dirs) {
|
|
72787
73250
|
await mkdir(dir, { recursive: true });
|
|
72788
73251
|
}
|
|
72789
|
-
const autoUnlockPath =
|
|
72790
|
-
if (!
|
|
73252
|
+
const autoUnlockPath = join56(home2, ".switchroom", "vault-auto-unlock");
|
|
73253
|
+
if (!existsSync64(autoUnlockPath)) {
|
|
72791
73254
|
writeFileSync32(autoUnlockPath, "", { mode: 384 });
|
|
72792
73255
|
}
|
|
72793
|
-
const auditLogPath =
|
|
72794
|
-
if (!
|
|
73256
|
+
const auditLogPath = join56(home2, ".switchroom", "vault-audit.log");
|
|
73257
|
+
if (!existsSync64(auditLogPath)) {
|
|
72795
73258
|
writeFileSync32(auditLogPath, "", { mode: 420 });
|
|
72796
73259
|
}
|
|
72797
|
-
const hostdAuditLogPath =
|
|
72798
|
-
if (!
|
|
73260
|
+
const hostdAuditLogPath = join56(home2, ".switchroom", "host-control-audit.log");
|
|
73261
|
+
if (!existsSync64(hostdAuditLogPath)) {
|
|
72799
73262
|
writeFileSync32(hostdAuditLogPath, "", { mode: 420 });
|
|
72800
73263
|
}
|
|
72801
73264
|
}
|
|
@@ -72816,7 +73279,7 @@ ${out.trim()}`;
|
|
|
72816
73279
|
}
|
|
72817
73280
|
function runApplyPreflight(config, opts = {}) {
|
|
72818
73281
|
const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
|
|
72819
|
-
if (hasVaultRefs(config) && !
|
|
73282
|
+
if (hasVaultRefs(config) && !existsSync64(vaultPath)) {
|
|
72820
73283
|
throw new Error(`Config references vault keys (vault:<name>) but ${vaultPath} is missing. ` + `Run \`switchroom setup\` first to initialise the vault.`);
|
|
72821
73284
|
}
|
|
72822
73285
|
const detect = opts.detectComposeV2 ?? detectComposeV2;
|
|
@@ -72827,7 +73290,7 @@ function runApplyPreflight(config, opts = {}) {
|
|
|
72827
73290
|
detectAndReportLegacyGdriveSlots(vaultPath);
|
|
72828
73291
|
}
|
|
72829
73292
|
function detectAndReportLegacyGdriveSlots(vaultPath) {
|
|
72830
|
-
if (!
|
|
73293
|
+
if (!existsSync64(vaultPath))
|
|
72831
73294
|
return;
|
|
72832
73295
|
const passphrase = process.env.SWITCHROOM_VAULT_PASSPHRASE;
|
|
72833
73296
|
if (!passphrase)
|
|
@@ -72868,8 +73331,8 @@ function detectAndReportLegacyGdriveSlots(vaultPath) {
|
|
|
72868
73331
|
}
|
|
72869
73332
|
function writeInstallTypeCache(homeDir = homedir29()) {
|
|
72870
73333
|
const ctx = detectInstallType();
|
|
72871
|
-
const dir =
|
|
72872
|
-
const out =
|
|
73334
|
+
const dir = join56(homeDir, ".switchroom");
|
|
73335
|
+
const out = join56(dir, "install-type.json");
|
|
72873
73336
|
const tmp = `${out}.tmp`;
|
|
72874
73337
|
mkdirSync35(dir, { recursive: true });
|
|
72875
73338
|
const payload = {
|
|
@@ -72918,14 +73381,14 @@ Applying switchroom config...
|
|
|
72918
73381
|
writeOut(source_default.green(` + ${name}`) + source_default.gray(` (${agentConfig.extends ?? "default"}) \u2014 ${detail}
|
|
72919
73382
|
`));
|
|
72920
73383
|
try {
|
|
72921
|
-
installUpdatePromptHook(
|
|
73384
|
+
installUpdatePromptHook(join56(agentsDir, name));
|
|
72922
73385
|
} catch (hookErr) {
|
|
72923
73386
|
writeOut(source_default.gray(` (update-prompt hook install failed for ${name}: ${hookErr.message})
|
|
72924
73387
|
`));
|
|
72925
73388
|
}
|
|
72926
73389
|
try {
|
|
72927
73390
|
const uid = allocateAgentUid(name);
|
|
72928
|
-
alignAgentUid(name,
|
|
73391
|
+
alignAgentUid(name, join56(agentsDir, name), uid, {
|
|
72929
73392
|
confirm: !options.nonInteractive,
|
|
72930
73393
|
writeOut
|
|
72931
73394
|
});
|
|
@@ -72962,7 +73425,7 @@ Applying switchroom config...
|
|
|
72962
73425
|
for (const name of agentNames) {
|
|
72963
73426
|
try {
|
|
72964
73427
|
const uid = allocateAgentUid(name);
|
|
72965
|
-
alignAgentUid(name,
|
|
73428
|
+
alignAgentUid(name, join56(agentsDir, name), uid, {
|
|
72966
73429
|
confirm: !options.nonInteractive,
|
|
72967
73430
|
writeOut
|
|
72968
73431
|
});
|
|
@@ -73036,20 +73499,7 @@ Applying switchroom config...
|
|
|
73036
73499
|
process.exit(6);
|
|
73037
73500
|
}
|
|
73038
73501
|
const composePath = options.outPath ?? DEFAULT_COMPOSE_PATH2;
|
|
73039
|
-
const operatorUid = (
|
|
73040
|
-
const sudoUid = process.env.SUDO_UID;
|
|
73041
|
-
if (sudoUid !== undefined) {
|
|
73042
|
-
const parsed = parseInt(sudoUid, 10);
|
|
73043
|
-
if (Number.isFinite(parsed) && parsed > 0)
|
|
73044
|
-
return parsed;
|
|
73045
|
-
}
|
|
73046
|
-
if (typeof process.getuid === "function") {
|
|
73047
|
-
const uid = process.getuid();
|
|
73048
|
-
if (uid > 0)
|
|
73049
|
-
return uid;
|
|
73050
|
-
}
|
|
73051
|
-
return;
|
|
73052
|
-
})();
|
|
73502
|
+
const operatorUid = resolveOperatorUid();
|
|
73053
73503
|
const composeRelease = resolveRelease({
|
|
73054
73504
|
override: options.releaseOverride,
|
|
73055
73505
|
root: config.release
|
|
@@ -73064,7 +73514,7 @@ Applying switchroom config...
|
|
|
73064
73514
|
switchroomConfigPath,
|
|
73065
73515
|
operatorUid
|
|
73066
73516
|
});
|
|
73067
|
-
await mkdir(
|
|
73517
|
+
await mkdir(dirname19(composePath), { recursive: true });
|
|
73068
73518
|
await writeFile(composePath, composeContent, {
|
|
73069
73519
|
encoding: "utf8",
|
|
73070
73520
|
mode: 384
|
|
@@ -73079,6 +73529,13 @@ Wrote `) + composePath + source_default.gray(` (${composeBytes} bytes)
|
|
|
73079
73529
|
`);
|
|
73080
73530
|
writeOut(source_default.gray(` (If pull returns 401, login to ghcr.io first: see docs/operators/install.md#ghcr-auth)
|
|
73081
73531
|
`));
|
|
73532
|
+
if (process.geteuid?.() === 0 && operatorUid !== undefined) {
|
|
73533
|
+
const restored = restoreOperatorOwnership(homedir29(), operatorUid);
|
|
73534
|
+
if (restored.length > 0) {
|
|
73535
|
+
writeOut(source_default.gray(` Restored operator ownership of ${restored.length} ~/.switchroom path(s)
|
|
73536
|
+
`));
|
|
73537
|
+
}
|
|
73538
|
+
}
|
|
73082
73539
|
writeOut(source_default.bold(`
|
|
73083
73540
|
Done. Scaffolded ${scaffolded}/${allAgentNames.length} agents.
|
|
73084
73541
|
`));
|
|
@@ -73121,7 +73578,7 @@ function copyExampleConfig2(name) {
|
|
|
73121
73578
|
throw new Error(`Invalid example name: ${name} (must match /^[a-z0-9_-]+$/)`);
|
|
73122
73579
|
}
|
|
73123
73580
|
const dest = resolve41(process.cwd(), "switchroom.yaml");
|
|
73124
|
-
if (
|
|
73581
|
+
if (existsSync64(dest)) {
|
|
73125
73582
|
console.error(source_default.yellow("switchroom.yaml already exists \u2014 skipping example copy"));
|
|
73126
73583
|
return;
|
|
73127
73584
|
}
|
|
@@ -73132,7 +73589,7 @@ function copyExampleConfig2(name) {
|
|
|
73132
73589
|
return;
|
|
73133
73590
|
}
|
|
73134
73591
|
const exampleFile = resolve41(import.meta.dirname, `../../examples/${name}.yaml`);
|
|
73135
|
-
if (!
|
|
73592
|
+
if (!existsSync64(exampleFile)) {
|
|
73136
73593
|
throw new Error(`Example config not found: ${name}.yaml (available: ${Object.keys(EMBEDDED_EXAMPLES).join(", ")})`);
|
|
73137
73594
|
}
|
|
73138
73595
|
copyFileSync11(exampleFile, dest);
|
|
@@ -73143,11 +73600,11 @@ function findUnwritableAgentDirs(config, opts) {
|
|
|
73143
73600
|
const targets = opts.only ? [opts.only] : Object.keys(config.agents ?? {});
|
|
73144
73601
|
const unwritable = [];
|
|
73145
73602
|
for (const name of targets) {
|
|
73146
|
-
const startSh =
|
|
73147
|
-
if (!
|
|
73603
|
+
const startSh = join56(agentsDir, name, "start.sh");
|
|
73604
|
+
if (!existsSync64(startSh))
|
|
73148
73605
|
continue;
|
|
73149
73606
|
try {
|
|
73150
|
-
|
|
73607
|
+
accessSync3(startSh, fsConstants6.W_OK);
|
|
73151
73608
|
} catch {
|
|
73152
73609
|
unwritable.push(name);
|
|
73153
73610
|
}
|
|
@@ -73322,8 +73779,8 @@ function runRedactStdin() {
|
|
|
73322
73779
|
}
|
|
73323
73780
|
|
|
73324
73781
|
// src/cli/status-ask.ts
|
|
73325
|
-
import { readFileSync as readFileSync54, existsSync as
|
|
73326
|
-
import { join as
|
|
73782
|
+
import { readFileSync as readFileSync54, existsSync as existsSync65, readdirSync as readdirSync24 } from "node:fs";
|
|
73783
|
+
import { join as join57 } from "node:path";
|
|
73327
73784
|
import { homedir as homedir30 } from "node:os";
|
|
73328
73785
|
|
|
73329
73786
|
// src/status-ask/report.ts
|
|
@@ -73645,7 +74102,7 @@ function runReport(opts) {
|
|
|
73645
74102
|
function resolveSources(explicitPath) {
|
|
73646
74103
|
if (explicitPath != null && explicitPath.trim() !== "") {
|
|
73647
74104
|
const trimmed = explicitPath.trim();
|
|
73648
|
-
if (!
|
|
74105
|
+
if (!existsSync65(trimmed)) {
|
|
73649
74106
|
process.stderr.write(`status-ask report: ${trimmed}: file not found
|
|
73650
74107
|
`);
|
|
73651
74108
|
process.exit(1);
|
|
@@ -73659,20 +74116,20 @@ function resolveSources(explicitPath) {
|
|
|
73659
74116
|
const config = loadConfig();
|
|
73660
74117
|
agentsDir = resolveAgentsDir(config);
|
|
73661
74118
|
} catch {
|
|
73662
|
-
agentsDir =
|
|
74119
|
+
agentsDir = join57(homedir30(), ".switchroom", "agents");
|
|
73663
74120
|
}
|
|
73664
|
-
if (!
|
|
74121
|
+
if (!existsSync65(agentsDir))
|
|
73665
74122
|
return [];
|
|
73666
74123
|
const sources = [];
|
|
73667
74124
|
let entries;
|
|
73668
74125
|
try {
|
|
73669
|
-
entries =
|
|
74126
|
+
entries = readdirSync24(agentsDir);
|
|
73670
74127
|
} catch {
|
|
73671
74128
|
return [];
|
|
73672
74129
|
}
|
|
73673
74130
|
for (const name of entries) {
|
|
73674
|
-
const path8 =
|
|
73675
|
-
if (
|
|
74131
|
+
const path8 = join57(agentsDir, name, "runtime-metrics.jsonl");
|
|
74132
|
+
if (existsSync65(path8)) {
|
|
73676
74133
|
sources.push({ path: path8, agent: name });
|
|
73677
74134
|
}
|
|
73678
74135
|
}
|
|
@@ -73693,17 +74150,17 @@ function inferAgentFromPath(p) {
|
|
|
73693
74150
|
|
|
73694
74151
|
// src/cli/agent-config.ts
|
|
73695
74152
|
init_helpers();
|
|
73696
|
-
import { join as
|
|
74153
|
+
import { join as join58 } from "node:path";
|
|
73697
74154
|
import { homedir as homedir31 } from "node:os";
|
|
73698
74155
|
import {
|
|
73699
|
-
existsSync as
|
|
74156
|
+
existsSync as existsSync66,
|
|
73700
74157
|
mkdirSync as mkdirSync36,
|
|
73701
74158
|
appendFileSync as appendFileSync3,
|
|
73702
74159
|
readFileSync as readFileSync55
|
|
73703
74160
|
} from "node:fs";
|
|
73704
|
-
var AUDIT_ROOT =
|
|
74161
|
+
var AUDIT_ROOT = join58(homedir31(), ".switchroom", "audit");
|
|
73705
74162
|
function auditPathFor(agent) {
|
|
73706
|
-
return
|
|
74163
|
+
return join58(AUDIT_ROOT, agent, "agent-config.jsonl");
|
|
73707
74164
|
}
|
|
73708
74165
|
function appendAudit(agent, cmd, args, exit, opts = {}) {
|
|
73709
74166
|
const row = {
|
|
@@ -73717,7 +74174,7 @@ function appendAudit(agent, cmd, args, exit, opts = {}) {
|
|
|
73717
74174
|
const path8 = opts.auditPath ?? auditPathFor(agent);
|
|
73718
74175
|
const dir = path8.slice(0, path8.lastIndexOf("/"));
|
|
73719
74176
|
try {
|
|
73720
|
-
if (!
|
|
74177
|
+
if (!existsSync66(dir)) {
|
|
73721
74178
|
mkdirSync36(dir, { recursive: true });
|
|
73722
74179
|
}
|
|
73723
74180
|
appendFileSync3(path8, JSON.stringify(row) + `
|
|
@@ -73729,7 +74186,7 @@ function isContainerContext(env2 = process.env, opts = {}) {
|
|
|
73729
74186
|
return true;
|
|
73730
74187
|
const probe2 = opts.dockerEnvPath ?? "/.dockerenv";
|
|
73731
74188
|
try {
|
|
73732
|
-
if (
|
|
74189
|
+
if (existsSync66(probe2))
|
|
73733
74190
|
return true;
|
|
73734
74191
|
} catch {}
|
|
73735
74192
|
return false;
|
|
@@ -73790,7 +74247,7 @@ function getAgentSlice(config, agent) {
|
|
|
73790
74247
|
}
|
|
73791
74248
|
function readAuditTail(agent, limit, opts = {}) {
|
|
73792
74249
|
const path8 = opts.auditPath ?? auditPathFor(agent);
|
|
73793
|
-
if (!
|
|
74250
|
+
if (!existsSync66(path8))
|
|
73794
74251
|
return [];
|
|
73795
74252
|
let raw;
|
|
73796
74253
|
try {
|
|
@@ -73950,32 +74407,32 @@ var import_yaml14 = __toESM(require_dist(), 1);
|
|
|
73950
74407
|
init_paths();
|
|
73951
74408
|
import {
|
|
73952
74409
|
closeSync as closeSync12,
|
|
73953
|
-
existsSync as
|
|
74410
|
+
existsSync as existsSync67,
|
|
73954
74411
|
fsyncSync as fsyncSync5,
|
|
73955
74412
|
mkdirSync as mkdirSync37,
|
|
73956
74413
|
openSync as openSync12,
|
|
73957
|
-
readdirSync as
|
|
74414
|
+
readdirSync as readdirSync25,
|
|
73958
74415
|
readFileSync as readFileSync56,
|
|
73959
74416
|
renameSync as renameSync13,
|
|
73960
|
-
statSync as
|
|
74417
|
+
statSync as statSync26,
|
|
73961
74418
|
unlinkSync as unlinkSync13,
|
|
73962
74419
|
writeSync as writeSync7
|
|
73963
74420
|
} from "node:fs";
|
|
73964
|
-
import { join as
|
|
74421
|
+
import { join as join59, resolve as resolve42 } from "node:path";
|
|
73965
74422
|
var STAGING_SUBDIR = ".staging";
|
|
73966
74423
|
function overlayPathsFor(agent, opts = {}) {
|
|
73967
74424
|
const base = opts.root ? resolve42(opts.root, agent) : resolve42(resolveDualPath(`~/.switchroom/agents/${agent}`));
|
|
73968
|
-
const scheduleDir =
|
|
73969
|
-
const scheduleStagingDir =
|
|
73970
|
-
const skillsDir =
|
|
73971
|
-
const skillsStagingDir =
|
|
74425
|
+
const scheduleDir = join59(base, "schedule.d");
|
|
74426
|
+
const scheduleStagingDir = join59(scheduleDir, STAGING_SUBDIR);
|
|
74427
|
+
const skillsDir = join59(base, "skills.d");
|
|
74428
|
+
const skillsStagingDir = join59(skillsDir, STAGING_SUBDIR);
|
|
73972
74429
|
return {
|
|
73973
74430
|
agentRoot: base,
|
|
73974
74431
|
scheduleDir,
|
|
73975
74432
|
scheduleStagingDir,
|
|
73976
74433
|
skillsDir,
|
|
73977
74434
|
skillsStagingDir,
|
|
73978
|
-
lockPath:
|
|
74435
|
+
lockPath: join59(base, ".lock"),
|
|
73979
74436
|
stagingDir: scheduleStagingDir
|
|
73980
74437
|
};
|
|
73981
74438
|
}
|
|
@@ -74001,7 +74458,7 @@ function withAgentLock(paths, fn) {
|
|
|
74001
74458
|
if (e.code !== "EEXIST")
|
|
74002
74459
|
throw err;
|
|
74003
74460
|
try {
|
|
74004
|
-
const age = Date.now() -
|
|
74461
|
+
const age = Date.now() - statSync26(paths.lockPath).mtimeMs;
|
|
74005
74462
|
if (age > 30000) {
|
|
74006
74463
|
unlinkSync13(paths.lockPath);
|
|
74007
74464
|
continue;
|
|
@@ -74029,8 +74486,8 @@ function writeOverlayEntry(agent, slug, yamlText, opts = {}) {
|
|
|
74029
74486
|
const paths = overlayPathsFor(agent, opts);
|
|
74030
74487
|
return withAgentLock(paths, () => {
|
|
74031
74488
|
ensureDirs(paths);
|
|
74032
|
-
const stagingPath =
|
|
74033
|
-
const finalPath =
|
|
74489
|
+
const stagingPath = join59(paths.scheduleStagingDir, `${slug}.yaml`);
|
|
74490
|
+
const finalPath = join59(paths.scheduleDir, `${slug}.yaml`);
|
|
74034
74491
|
const fd = openSync12(stagingPath, "w", 384);
|
|
74035
74492
|
try {
|
|
74036
74493
|
writeSync7(fd, yamlText);
|
|
@@ -74046,8 +74503,8 @@ function writeSkillsOverlayEntry(agent, slug, yamlText, opts = {}) {
|
|
|
74046
74503
|
const paths = overlayPathsFor(agent, opts);
|
|
74047
74504
|
return withAgentLock(paths, () => {
|
|
74048
74505
|
ensureSkillsDirs(paths);
|
|
74049
|
-
const stagingPath =
|
|
74050
|
-
const finalPath =
|
|
74506
|
+
const stagingPath = join59(paths.skillsStagingDir, `${slug}.yaml`);
|
|
74507
|
+
const finalPath = join59(paths.skillsDir, `${slug}.yaml`);
|
|
74051
74508
|
const fd = openSync12(stagingPath, "w", 384);
|
|
74052
74509
|
try {
|
|
74053
74510
|
writeSync7(fd, yamlText);
|
|
@@ -74062,8 +74519,8 @@ function writeSkillsOverlayEntry(agent, slug, yamlText, opts = {}) {
|
|
|
74062
74519
|
function deleteSkillsOverlayEntry(agent, slug, opts = {}) {
|
|
74063
74520
|
const paths = overlayPathsFor(agent, opts);
|
|
74064
74521
|
return withAgentLock(paths, () => {
|
|
74065
|
-
const finalPath =
|
|
74066
|
-
if (!
|
|
74522
|
+
const finalPath = join59(paths.skillsDir, `${slug}.yaml`);
|
|
74523
|
+
if (!existsSync67(finalPath))
|
|
74067
74524
|
return false;
|
|
74068
74525
|
unlinkSync13(finalPath);
|
|
74069
74526
|
return true;
|
|
@@ -74071,13 +74528,13 @@ function deleteSkillsOverlayEntry(agent, slug, opts = {}) {
|
|
|
74071
74528
|
}
|
|
74072
74529
|
function listSkillsOverlayEntries(agent, opts = {}) {
|
|
74073
74530
|
const paths = overlayPathsFor(agent, opts);
|
|
74074
|
-
if (!
|
|
74531
|
+
if (!existsSync67(paths.skillsDir))
|
|
74075
74532
|
return [];
|
|
74076
74533
|
const out = [];
|
|
74077
|
-
for (const name of
|
|
74534
|
+
for (const name of readdirSync25(paths.skillsDir)) {
|
|
74078
74535
|
if (!/\.ya?ml$/i.test(name))
|
|
74079
74536
|
continue;
|
|
74080
|
-
const full =
|
|
74537
|
+
const full = join59(paths.skillsDir, name);
|
|
74081
74538
|
try {
|
|
74082
74539
|
const raw = readFileSync56(full, "utf-8");
|
|
74083
74540
|
const slug = name.replace(/\.ya?ml$/i, "");
|
|
@@ -74089,8 +74546,8 @@ function listSkillsOverlayEntries(agent, opts = {}) {
|
|
|
74089
74546
|
function deleteOverlayEntry(agent, slug, opts = {}) {
|
|
74090
74547
|
const paths = overlayPathsFor(agent, opts);
|
|
74091
74548
|
return withAgentLock(paths, () => {
|
|
74092
|
-
const finalPath =
|
|
74093
|
-
if (!
|
|
74549
|
+
const finalPath = join59(paths.scheduleDir, `${slug}.yaml`);
|
|
74550
|
+
if (!existsSync67(finalPath))
|
|
74094
74551
|
return false;
|
|
74095
74552
|
unlinkSync13(finalPath);
|
|
74096
74553
|
return true;
|
|
@@ -74098,13 +74555,13 @@ function deleteOverlayEntry(agent, slug, opts = {}) {
|
|
|
74098
74555
|
}
|
|
74099
74556
|
function listOverlayEntries(agent, opts = {}) {
|
|
74100
74557
|
const paths = overlayPathsFor(agent, opts);
|
|
74101
|
-
if (!
|
|
74558
|
+
if (!existsSync67(paths.scheduleDir))
|
|
74102
74559
|
return [];
|
|
74103
74560
|
const out = [];
|
|
74104
|
-
for (const name of
|
|
74561
|
+
for (const name of readdirSync25(paths.scheduleDir)) {
|
|
74105
74562
|
if (!/\.ya?ml$/i.test(name))
|
|
74106
74563
|
continue;
|
|
74107
|
-
const full =
|
|
74564
|
+
const full = join59(paths.scheduleDir, name);
|
|
74108
74565
|
try {
|
|
74109
74566
|
const raw = readFileSync56(full, "utf-8");
|
|
74110
74567
|
const slug = name.replace(/\.ya?ml$/i, "");
|
|
@@ -74249,23 +74706,23 @@ function reconcileAgentCronOnly(agent) {
|
|
|
74249
74706
|
// src/cli/agent-config-pending.ts
|
|
74250
74707
|
import {
|
|
74251
74708
|
closeSync as closeSync13,
|
|
74252
|
-
existsSync as
|
|
74709
|
+
existsSync as existsSync68,
|
|
74253
74710
|
fsyncSync as fsyncSync6,
|
|
74254
74711
|
mkdirSync as mkdirSync38,
|
|
74255
74712
|
openSync as openSync13,
|
|
74256
|
-
readdirSync as
|
|
74713
|
+
readdirSync as readdirSync26,
|
|
74257
74714
|
readFileSync as readFileSync57,
|
|
74258
74715
|
renameSync as renameSync14,
|
|
74259
74716
|
unlinkSync as unlinkSync14,
|
|
74260
74717
|
writeFileSync as writeFileSync33,
|
|
74261
74718
|
writeSync as writeSync8
|
|
74262
74719
|
} from "node:fs";
|
|
74263
|
-
import { join as
|
|
74720
|
+
import { join as join60 } from "node:path";
|
|
74264
74721
|
import { randomBytes as randomBytes12 } from "node:crypto";
|
|
74265
74722
|
var STAGE_ID_PREFIX = "cap_";
|
|
74266
74723
|
function pendingDir(agent, opts = {}) {
|
|
74267
74724
|
const paths = overlayPathsFor(agent, opts);
|
|
74268
|
-
return
|
|
74725
|
+
return join60(paths.scheduleDir, ".pending");
|
|
74269
74726
|
}
|
|
74270
74727
|
function ensurePendingDir(agent, opts = {}) {
|
|
74271
74728
|
const dir = pendingDir(agent, opts);
|
|
@@ -74278,8 +74735,8 @@ function newStageId() {
|
|
|
74278
74735
|
function stagePendingScheduleEntry(opts) {
|
|
74279
74736
|
const dir = ensurePendingDir(opts.agent, { root: opts.root });
|
|
74280
74737
|
const stageId = opts.stageId ?? newStageId();
|
|
74281
|
-
const yamlPath =
|
|
74282
|
-
const metaPath =
|
|
74738
|
+
const yamlPath = join60(dir, `${stageId}.yaml`);
|
|
74739
|
+
const metaPath = join60(dir, `${stageId}.meta.json`);
|
|
74283
74740
|
const meta = {
|
|
74284
74741
|
v: 1,
|
|
74285
74742
|
stage_id: stageId,
|
|
@@ -74306,16 +74763,16 @@ function stagePendingScheduleEntry(opts) {
|
|
|
74306
74763
|
}
|
|
74307
74764
|
function listPendingScheduleEntries(agent, opts = {}) {
|
|
74308
74765
|
const dir = pendingDir(agent, opts);
|
|
74309
|
-
if (!
|
|
74766
|
+
if (!existsSync68(dir))
|
|
74310
74767
|
return [];
|
|
74311
74768
|
const out = [];
|
|
74312
|
-
for (const name of
|
|
74769
|
+
for (const name of readdirSync26(dir).sort()) {
|
|
74313
74770
|
if (!name.endsWith(".meta.json"))
|
|
74314
74771
|
continue;
|
|
74315
74772
|
const stageId = name.slice(0, -".meta.json".length);
|
|
74316
|
-
const metaPath =
|
|
74317
|
-
const yamlPath =
|
|
74318
|
-
if (!
|
|
74773
|
+
const metaPath = join60(dir, name);
|
|
74774
|
+
const yamlPath = join60(dir, `${stageId}.yaml`);
|
|
74775
|
+
if (!existsSync68(yamlPath))
|
|
74319
74776
|
continue;
|
|
74320
74777
|
try {
|
|
74321
74778
|
const meta = JSON.parse(readFileSync57(metaPath, "utf-8"));
|
|
@@ -74333,8 +74790,8 @@ function commitPendingScheduleEntry(opts) {
|
|
|
74333
74790
|
return { committed: false, reason: "not_found" };
|
|
74334
74791
|
const slug = match.meta.entry.name ?? match.stageId;
|
|
74335
74792
|
const paths = overlayPathsFor(opts.agent, { root: opts.root });
|
|
74336
|
-
const finalPath =
|
|
74337
|
-
if (
|
|
74793
|
+
const finalPath = join60(paths.scheduleDir, `${slug}.yaml`);
|
|
74794
|
+
if (existsSync68(finalPath)) {
|
|
74338
74795
|
return { committed: false, reason: "slug_collision" };
|
|
74339
74796
|
}
|
|
74340
74797
|
renameSync14(match.yamlPath, finalPath);
|
|
@@ -74356,7 +74813,7 @@ function denyPendingScheduleEntry(opts) {
|
|
|
74356
74813
|
}
|
|
74357
74814
|
|
|
74358
74815
|
// src/cli/agent-config-write.ts
|
|
74359
|
-
import { existsSync as
|
|
74816
|
+
import { existsSync as existsSync69, readFileSync as readFileSync58 } from "node:fs";
|
|
74360
74817
|
var MAX_ENTRIES_PER_AGENT = 20;
|
|
74361
74818
|
function checkOperatorContext(verb, env2 = process.env) {
|
|
74362
74819
|
if (env2.SWITCHROOM_OPERATOR === "1")
|
|
@@ -74590,7 +75047,7 @@ function scheduleRemove(opts) {
|
|
|
74590
75047
|
}
|
|
74591
75048
|
let priorContent = null;
|
|
74592
75049
|
try {
|
|
74593
|
-
if (
|
|
75050
|
+
if (existsSync69(match.path))
|
|
74594
75051
|
priorContent = readFileSync58(match.path, "utf-8");
|
|
74595
75052
|
} catch {}
|
|
74596
75053
|
deleteOverlayEntry(agent, match.slug, { root: opts.root });
|
|
@@ -74783,10 +75240,10 @@ function registerAgentConfigWriteCommands(program3) {
|
|
|
74783
75240
|
|
|
74784
75241
|
// src/cli/agent-config-skill-write.ts
|
|
74785
75242
|
var import_yaml15 = __toESM(require_dist(), 1);
|
|
74786
|
-
import { existsSync as
|
|
75243
|
+
import { existsSync as existsSync70 } from "node:fs";
|
|
74787
75244
|
init_reconcile_default_skills();
|
|
74788
75245
|
var import_yaml16 = __toESM(require_dist(), 1);
|
|
74789
|
-
import { join as
|
|
75246
|
+
import { join as join61 } from "node:path";
|
|
74790
75247
|
var MAX_SKILLS_PER_AGENT = 20;
|
|
74791
75248
|
var V1_ALLOWED_SOURCE_PREFIX = "bundled:";
|
|
74792
75249
|
function exitCodeFor2(code) {
|
|
@@ -74861,8 +75318,8 @@ function skillInstall(opts) {
|
|
|
74861
75318
|
return err("E_SKILL_QUOTA_EXCEEDED", `agent ${agent} already has ${used} overlay-installed skills (cap ${MAX_SKILLS_PER_AGENT})`);
|
|
74862
75319
|
}
|
|
74863
75320
|
const poolDir = opts.bundledSkillsPoolDir ?? getBundledSkillsPoolDir();
|
|
74864
|
-
const skillPath =
|
|
74865
|
-
if (!
|
|
75321
|
+
const skillPath = join61(poolDir, skillName);
|
|
75322
|
+
if (!existsSync70(skillPath)) {
|
|
74866
75323
|
return err("E_SKILL_NOT_FOUND", `bundled skill not found at ${skillPath}. The operator needs to ` + `place the skill at this path before the agent can opt in.`);
|
|
74867
75324
|
}
|
|
74868
75325
|
const yamlText = import_yaml15.stringify({ skills: [skillName] });
|
|
@@ -75046,27 +75503,27 @@ init_source();
|
|
|
75046
75503
|
init_helpers();
|
|
75047
75504
|
init_loader();
|
|
75048
75505
|
import {
|
|
75049
|
-
existsSync as
|
|
75050
|
-
readdirSync as
|
|
75506
|
+
existsSync as existsSync72,
|
|
75507
|
+
readdirSync as readdirSync27,
|
|
75051
75508
|
readFileSync as readFileSync60,
|
|
75052
75509
|
renameSync as renameSync15,
|
|
75053
|
-
statSync as
|
|
75510
|
+
statSync as statSync27,
|
|
75054
75511
|
unlinkSync as unlinkSync15
|
|
75055
75512
|
} from "node:fs";
|
|
75056
75513
|
import { createHash as createHash12 } from "node:crypto";
|
|
75057
|
-
import { join as
|
|
75514
|
+
import { join as join62 } from "node:path";
|
|
75058
75515
|
function planCronUnitRenames(agentsDir, agents) {
|
|
75059
75516
|
const plans = [];
|
|
75060
75517
|
for (const [agentName, agentConfig] of Object.entries(agents)) {
|
|
75061
75518
|
const schedule = agentConfig.schedule ?? [];
|
|
75062
75519
|
if (schedule.length === 0)
|
|
75063
75520
|
continue;
|
|
75064
|
-
const telegramDir =
|
|
75065
|
-
if (!
|
|
75521
|
+
const telegramDir = join62(agentsDir, agentName, "telegram");
|
|
75522
|
+
if (!existsSync72(telegramDir))
|
|
75066
75523
|
continue;
|
|
75067
75524
|
let entries;
|
|
75068
75525
|
try {
|
|
75069
|
-
entries =
|
|
75526
|
+
entries = readdirSync27(telegramDir);
|
|
75070
75527
|
} catch {
|
|
75071
75528
|
continue;
|
|
75072
75529
|
}
|
|
@@ -75083,8 +75540,8 @@ function planCronUnitRenames(agentsDir, agents) {
|
|
|
75083
75540
|
continue;
|
|
75084
75541
|
plans.push({
|
|
75085
75542
|
agent: agentName,
|
|
75086
|
-
from:
|
|
75087
|
-
to:
|
|
75543
|
+
from: join62(telegramDir, file),
|
|
75544
|
+
to: join62(telegramDir, canonical),
|
|
75088
75545
|
scheduleIdx: idx,
|
|
75089
75546
|
entry
|
|
75090
75547
|
});
|
|
@@ -75096,7 +75553,7 @@ function sha256File2(path8) {
|
|
|
75096
75553
|
return createHash12("sha256").update(readFileSync60(path8)).digest("hex");
|
|
75097
75554
|
}
|
|
75098
75555
|
function renamePair(from, to, opts = {}) {
|
|
75099
|
-
if (
|
|
75556
|
+
if (existsSync72(to)) {
|
|
75100
75557
|
let identical = false;
|
|
75101
75558
|
try {
|
|
75102
75559
|
identical = sha256File2(from) === sha256File2(to);
|
|
@@ -75189,7 +75646,7 @@ function registerMigrateCommand(program3) {
|
|
|
75189
75646
|
const fromSidecar = p.from.replace(/\.sh$/, ".source");
|
|
75190
75647
|
const toSidecar = p.to.replace(/\.sh$/, ".source");
|
|
75191
75648
|
let sidecarStatus = null;
|
|
75192
|
-
if (
|
|
75649
|
+
if (existsSync72(fromSidecar) && statSync27(fromSidecar).isFile()) {
|
|
75193
75650
|
sidecarStatus = renamePair(fromSidecar, toSidecar);
|
|
75194
75651
|
}
|
|
75195
75652
|
switch (status.kind) {
|
|
@@ -75222,10 +75679,10 @@ function registerMigrateCommand(program3) {
|
|
|
75222
75679
|
init_source();
|
|
75223
75680
|
init_helpers();
|
|
75224
75681
|
init_audit_reader();
|
|
75225
|
-
import { existsSync as
|
|
75682
|
+
import { existsSync as existsSync73, mkdirSync as mkdirSync39, readdirSync as readdirSync28, readFileSync as readFileSync61, writeFileSync as writeFileSync34, statSync as statSync28, copyFileSync as copyFileSync12 } from "node:fs";
|
|
75226
75683
|
import { homedir as homedir32 } from "node:os";
|
|
75227
|
-
import { join as
|
|
75228
|
-
import { spawnSync as
|
|
75684
|
+
import { join as join63 } from "node:path";
|
|
75685
|
+
import { spawnSync as spawnSync11 } from "node:child_process";
|
|
75229
75686
|
var DEFAULT_IMAGE_TAG = "latest";
|
|
75230
75687
|
var HOSTD_COMPOSE_PROJECT = "switchroom-hostd";
|
|
75231
75688
|
function renderHostdComposeFile(opts) {
|
|
@@ -75308,14 +75765,14 @@ networks:
|
|
|
75308
75765
|
`;
|
|
75309
75766
|
}
|
|
75310
75767
|
function hostdDir() {
|
|
75311
|
-
return
|
|
75768
|
+
return join63(homedir32(), ".switchroom", "hostd");
|
|
75312
75769
|
}
|
|
75313
75770
|
function hostdComposePath() {
|
|
75314
|
-
return
|
|
75771
|
+
return join63(hostdDir(), "docker-compose.yml");
|
|
75315
75772
|
}
|
|
75316
75773
|
function backupExistingCompose() {
|
|
75317
75774
|
const p = hostdComposePath();
|
|
75318
|
-
if (!
|
|
75775
|
+
if (!existsSync73(p))
|
|
75319
75776
|
return null;
|
|
75320
75777
|
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
75321
75778
|
const bak = `${p}.bak-${ts}`;
|
|
@@ -75323,7 +75780,7 @@ function backupExistingCompose() {
|
|
|
75323
75780
|
return bak;
|
|
75324
75781
|
}
|
|
75325
75782
|
function runDocker(args) {
|
|
75326
|
-
const r =
|
|
75783
|
+
const r = spawnSync11("docker", args, { encoding: "utf8" });
|
|
75327
75784
|
return {
|
|
75328
75785
|
ok: r.status === 0,
|
|
75329
75786
|
stdout: r.stdout ?? "",
|
|
@@ -75391,7 +75848,7 @@ function doStatus() {
|
|
|
75391
75848
|
const composeYml = hostdComposePath();
|
|
75392
75849
|
console.log(source_default.bold("switchroom-hostd"));
|
|
75393
75850
|
console.log("");
|
|
75394
|
-
if (!
|
|
75851
|
+
if (!existsSync73(composeYml)) {
|
|
75395
75852
|
console.log(source_default.yellow(" compose: not installed"));
|
|
75396
75853
|
console.log(source_default.dim(" run `switchroom hostd install` to set up."));
|
|
75397
75854
|
return;
|
|
@@ -75412,15 +75869,15 @@ function doStatus() {
|
|
|
75412
75869
|
} else {
|
|
75413
75870
|
console.log(source_default.green(` container: ${ps.stdout.trim()}`));
|
|
75414
75871
|
}
|
|
75415
|
-
if (
|
|
75872
|
+
if (existsSync73(dir)) {
|
|
75416
75873
|
const entries = [];
|
|
75417
75874
|
try {
|
|
75418
|
-
for (const name of
|
|
75875
|
+
for (const name of readdirSync28(dir)) {
|
|
75419
75876
|
if (name === "docker-compose.yml" || name.startsWith("docker-compose.yml."))
|
|
75420
75877
|
continue;
|
|
75421
|
-
const sockPath =
|
|
75422
|
-
if (
|
|
75423
|
-
const st =
|
|
75878
|
+
const sockPath = join63(dir, name, "sock");
|
|
75879
|
+
if (existsSync73(sockPath)) {
|
|
75880
|
+
const st = statSync28(sockPath);
|
|
75424
75881
|
if ((st.mode & 61440) === 49152) {
|
|
75425
75882
|
entries.push(`${name} \u2192 ${sockPath}`);
|
|
75426
75883
|
}
|
|
@@ -75438,7 +75895,7 @@ function doStatus() {
|
|
|
75438
75895
|
}
|
|
75439
75896
|
function doUninstall() {
|
|
75440
75897
|
const composeYml = hostdComposePath();
|
|
75441
|
-
if (!
|
|
75898
|
+
if (!existsSync73(composeYml)) {
|
|
75442
75899
|
console.log(source_default.yellow(" No hostd install detected (no compose file at this path)."));
|
|
75443
75900
|
return;
|
|
75444
75901
|
}
|
|
@@ -75462,7 +75919,7 @@ function registerHostdCommand(program3) {
|
|
|
75462
75919
|
hostd.command("uninstall").description("Stop the hostd container. Leaves the compose file in place for re-install.").action(() => doUninstall());
|
|
75463
75920
|
hostd.command("audit").description("Tail and filter the hostd audit log (privileged-verb call history)").option("--tail <n>", "Number of matching entries to show (default: 50)", "50").option("--agent <name>", "Filter to a specific caller agent").option("--op <verb>", "Filter to a specific hostd verb (e.g. update_apply, agent_restart)").option("--error", "Show only failed (error/denied) entries").option("--verbose", "Show the captured stderr / error tail under each failed row").option("--path <file>", "Override audit log path (for debugging)").action((opts) => {
|
|
75464
75921
|
const logPath = opts.path ?? defaultAuditLogPath2();
|
|
75465
|
-
if (!
|
|
75922
|
+
if (!existsSync73(logPath)) {
|
|
75466
75923
|
console.error(source_default.yellow(`Audit log not found at ${logPath}.`) + source_default.gray(`
|
|
75467
75924
|
The log is created when hostd handles its first privileged-verb request.`));
|
|
75468
75925
|
return;
|