switchroom 0.12.0 → 0.12.1
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 +869 -430
- 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.1";
|
|
46898
|
+
var COMMIT_SHA = "0de1edd1";
|
|
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,17 +68765,17 @@ 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;
|
|
@@ -68458,7 +68813,7 @@ function planUpdate(opts) {
|
|
|
68458
68813
|
steps.push({
|
|
68459
68814
|
name: "pull-images",
|
|
68460
68815
|
description: "Pull broker / kernel / agent images from GHCR",
|
|
68461
|
-
skipReason: opts.skipImages ? "--skip-images flag set" : !
|
|
68816
|
+
skipReason: opts.skipImages ? "--skip-images flag set" : !existsSync48(composePath) ? `compose file not found at ${composePath} (run \`switchroom apply --compose-only\` first)` : undefined,
|
|
68462
68817
|
run: () => {
|
|
68463
68818
|
const r = runner("docker", [
|
|
68464
68819
|
"compose",
|
|
@@ -68537,16 +68892,16 @@ function planUpdate(opts) {
|
|
|
68537
68892
|
}
|
|
68538
68893
|
const source = resolve30(import.meta.dirname, "../../skills");
|
|
68539
68894
|
const dest = join42(homedir22(), ".switchroom", "skills", "_bundled");
|
|
68540
|
-
if (!
|
|
68895
|
+
if (!existsSync48(source)) {
|
|
68541
68896
|
process.stderr.write(`switchroom update: sync-bundled-skills \u2014 CLI bundle has no adjacent skills/ at ${source}; skipping.
|
|
68542
68897
|
`);
|
|
68543
68898
|
return;
|
|
68544
68899
|
}
|
|
68545
68900
|
try {
|
|
68546
|
-
if (
|
|
68901
|
+
if (existsSync48(dest)) {
|
|
68547
68902
|
rmSync12(dest, { recursive: true, force: true });
|
|
68548
68903
|
}
|
|
68549
|
-
mkdirSync27(
|
|
68904
|
+
mkdirSync27(dirname13(dest), { recursive: true });
|
|
68550
68905
|
cpSync2(source, dest, { recursive: true, dereference: false });
|
|
68551
68906
|
} catch (err) {
|
|
68552
68907
|
throw new Error(`sync-bundled-skills failed: ${err.message}`);
|
|
@@ -68605,7 +68960,7 @@ function planUpdate(opts) {
|
|
|
68605
68960
|
return steps;
|
|
68606
68961
|
}
|
|
68607
68962
|
function defaultRunner(cmd, args) {
|
|
68608
|
-
const r =
|
|
68963
|
+
const r = spawnSync8(cmd, args, { stdio: "inherit" });
|
|
68609
68964
|
return { status: r.status ?? 1 };
|
|
68610
68965
|
}
|
|
68611
68966
|
function writeMarkerInPreferredLocation(agent, reason, runner) {
|
|
@@ -68642,16 +68997,16 @@ function defaultStatusProbe(composePath) {
|
|
|
68642
68997
|
let scriptPath = rawScriptPath;
|
|
68643
68998
|
try {
|
|
68644
68999
|
if (rawScriptPath)
|
|
68645
|
-
scriptPath =
|
|
69000
|
+
scriptPath = realpathSync5(rawScriptPath);
|
|
68646
69001
|
} catch {}
|
|
68647
69002
|
if (scriptPath) {
|
|
68648
69003
|
try {
|
|
68649
|
-
cliBuiltAt = new Date(
|
|
69004
|
+
cliBuiltAt = new Date(statSync22(scriptPath).mtimeMs).toISOString();
|
|
68650
69005
|
} catch {}
|
|
68651
|
-
let dir =
|
|
69006
|
+
let dir = dirname13(scriptPath);
|
|
68652
69007
|
for (let i = 0;i < 8; i++) {
|
|
68653
69008
|
const pkgPath = join42(dir, "package.json");
|
|
68654
|
-
if (
|
|
69009
|
+
if (existsSync48(pkgPath)) {
|
|
68655
69010
|
try {
|
|
68656
69011
|
const pkg = JSON.parse(readFileSync45(pkgPath, "utf-8"));
|
|
68657
69012
|
if (typeof pkg.version === "string")
|
|
@@ -68661,7 +69016,7 @@ function defaultStatusProbe(composePath) {
|
|
|
68661
69016
|
}
|
|
68662
69017
|
break;
|
|
68663
69018
|
}
|
|
68664
|
-
const parent =
|
|
69019
|
+
const parent = dirname13(dir);
|
|
68665
69020
|
if (parent === dir)
|
|
68666
69021
|
break;
|
|
68667
69022
|
dir = parent;
|
|
@@ -68674,13 +69029,13 @@ function defaultStatusProbe(composePath) {
|
|
|
68674
69029
|
warnings.push("could not resolve CLI version (no package.json found above the resolved script path)");
|
|
68675
69030
|
}
|
|
68676
69031
|
const services = [];
|
|
68677
|
-
if (!
|
|
69032
|
+
if (!existsSync48(composePath)) {
|
|
68678
69033
|
warnings.push(`compose file not found at ${composePath}; service status unknown`);
|
|
68679
69034
|
return { cliVersion, cliBuiltAt, services, warnings };
|
|
68680
69035
|
}
|
|
68681
69036
|
let serviceList = [];
|
|
68682
69037
|
try {
|
|
68683
|
-
const r =
|
|
69038
|
+
const r = spawnSync8("docker", ["compose", "-p", "switchroom", "-f", composePath, "config", "--services"], { encoding: "utf-8", timeout: 1e4 });
|
|
68684
69039
|
if (r.status !== 0) {
|
|
68685
69040
|
warnings.push(`docker compose config --services failed: ${r.stderr?.trim() ?? r.error?.message ?? "unknown"}`);
|
|
68686
69041
|
return { cliVersion, cliBuiltAt, services, warnings };
|
|
@@ -68697,7 +69052,7 @@ function defaultStatusProbe(composePath) {
|
|
|
68697
69052
|
let containerCreatedAt = null;
|
|
68698
69053
|
let status = "<unknown>";
|
|
68699
69054
|
try {
|
|
68700
|
-
const r =
|
|
69055
|
+
const r = spawnSync8("docker", ["inspect", "-f", "{{.Config.Image}}|{{.Created}}|{{.State.Status}}", containerName2], { encoding: "utf-8", timeout: 5000 });
|
|
68701
69056
|
if (r.status === 0) {
|
|
68702
69057
|
const [img, created, st] = r.stdout.trim().split("|");
|
|
68703
69058
|
image = img ?? null;
|
|
@@ -68713,7 +69068,7 @@ function defaultStatusProbe(composePath) {
|
|
|
68713
69068
|
let imagePulledAt = null;
|
|
68714
69069
|
if (image) {
|
|
68715
69070
|
try {
|
|
68716
|
-
const r =
|
|
69071
|
+
const r = spawnSync8("docker", ["image", "inspect", "-f", "{{.Id}}|{{.Created}}|{{.Metadata.LastTagTime}}", image], { encoding: "utf-8", timeout: 5000 });
|
|
68717
69072
|
if (r.status === 0) {
|
|
68718
69073
|
const [id, created, lastTag] = r.stdout.trim().split("|");
|
|
68719
69074
|
imageDigestShort = id?.replace(/^sha256:/, "").slice(0, 12) ?? null;
|
|
@@ -68861,8 +69216,8 @@ init_source();
|
|
|
68861
69216
|
init_helpers();
|
|
68862
69217
|
init_lifecycle();
|
|
68863
69218
|
import { execSync as execSync4 } from "node:child_process";
|
|
68864
|
-
import { existsSync as
|
|
68865
|
-
import { dirname as
|
|
69219
|
+
import { existsSync as existsSync49, readFileSync as readFileSync46 } from "node:fs";
|
|
69220
|
+
import { dirname as dirname14, join as join43 } from "node:path";
|
|
68866
69221
|
function getClaudeCodeVersion() {
|
|
68867
69222
|
try {
|
|
68868
69223
|
const out = execSync4("claude --version 2>/dev/null", {
|
|
@@ -68913,15 +69268,15 @@ function locateSwitchroomInstallDir() {
|
|
|
68913
69268
|
let dir = import.meta.dirname;
|
|
68914
69269
|
for (let i = 0;i < 10 && dir && dir !== "/"; i++) {
|
|
68915
69270
|
const pkgPath = join43(dir, "package.json");
|
|
68916
|
-
if (
|
|
69271
|
+
if (existsSync49(pkgPath)) {
|
|
68917
69272
|
try {
|
|
68918
69273
|
const pkg = JSON.parse(readFileSync46(pkgPath, "utf-8"));
|
|
68919
|
-
if (pkg.name === "switchroom" &&
|
|
69274
|
+
if (pkg.name === "switchroom" && existsSync49(join43(dir, ".git"))) {
|
|
68920
69275
|
return dir;
|
|
68921
69276
|
}
|
|
68922
69277
|
} catch {}
|
|
68923
69278
|
}
|
|
68924
|
-
dir =
|
|
69279
|
+
dir = dirname14(dir);
|
|
68925
69280
|
}
|
|
68926
69281
|
return null;
|
|
68927
69282
|
}
|
|
@@ -69135,13 +69490,13 @@ function registerHandoffCommand(program3) {
|
|
|
69135
69490
|
// src/issues/store.ts
|
|
69136
69491
|
import {
|
|
69137
69492
|
closeSync as closeSync10,
|
|
69138
|
-
existsSync as
|
|
69493
|
+
existsSync as existsSync50,
|
|
69139
69494
|
mkdirSync as mkdirSync28,
|
|
69140
69495
|
openSync as openSync10,
|
|
69141
69496
|
readdirSync as readdirSync18,
|
|
69142
69497
|
readFileSync as readFileSync47,
|
|
69143
69498
|
renameSync as renameSync10,
|
|
69144
|
-
statSync as
|
|
69499
|
+
statSync as statSync23,
|
|
69145
69500
|
unlinkSync as unlinkSync10,
|
|
69146
69501
|
writeFileSync as writeFileSync25,
|
|
69147
69502
|
writeSync as writeSync6
|
|
@@ -69467,7 +69822,7 @@ var ISSUES_FILE = "issues.jsonl";
|
|
|
69467
69822
|
var ISSUES_LOCK = "issues.lock";
|
|
69468
69823
|
function readAll(stateDir) {
|
|
69469
69824
|
const path4 = join44(stateDir, ISSUES_FILE);
|
|
69470
|
-
if (!
|
|
69825
|
+
if (!existsSync50(path4))
|
|
69471
69826
|
return [];
|
|
69472
69827
|
let raw;
|
|
69473
69828
|
try {
|
|
@@ -69544,7 +69899,7 @@ function record(stateDir, input, nowFn = Date.now) {
|
|
|
69544
69899
|
});
|
|
69545
69900
|
}
|
|
69546
69901
|
function resolve33(stateDir, fingerprint, nowFn = Date.now) {
|
|
69547
|
-
if (!
|
|
69902
|
+
if (!existsSync50(join44(stateDir, ISSUES_FILE)))
|
|
69548
69903
|
return 0;
|
|
69549
69904
|
return withLock(stateDir, () => {
|
|
69550
69905
|
const all = readAll(stateDir);
|
|
@@ -69562,7 +69917,7 @@ function resolve33(stateDir, fingerprint, nowFn = Date.now) {
|
|
|
69562
69917
|
});
|
|
69563
69918
|
}
|
|
69564
69919
|
function resolveAllBySource(stateDir, source, nowFn = Date.now) {
|
|
69565
|
-
if (!
|
|
69920
|
+
if (!existsSync50(join44(stateDir, ISSUES_FILE)))
|
|
69566
69921
|
return 0;
|
|
69567
69922
|
return withLock(stateDir, () => {
|
|
69568
69923
|
const all = readAll(stateDir);
|
|
@@ -69580,7 +69935,7 @@ function resolveAllBySource(stateDir, source, nowFn = Date.now) {
|
|
|
69580
69935
|
});
|
|
69581
69936
|
}
|
|
69582
69937
|
function prune(stateDir, opts = {}) {
|
|
69583
|
-
if (!
|
|
69938
|
+
if (!existsSync50(join44(stateDir, ISSUES_FILE)))
|
|
69584
69939
|
return 0;
|
|
69585
69940
|
return withLock(stateDir, () => {
|
|
69586
69941
|
const all = readAll(stateDir);
|
|
@@ -69637,7 +69992,7 @@ function sweepOrphanTmpFiles(stateDir) {
|
|
|
69637
69992
|
continue;
|
|
69638
69993
|
const tmpPath = join44(stateDir, entry);
|
|
69639
69994
|
try {
|
|
69640
|
-
const stat =
|
|
69995
|
+
const stat = statSync23(tmpPath);
|
|
69641
69996
|
if (stat.mtimeMs < cutoff) {
|
|
69642
69997
|
unlinkSync10(tmpPath);
|
|
69643
69998
|
}
|
|
@@ -69930,20 +70285,20 @@ function relTime(deltaMs) {
|
|
|
69930
70285
|
|
|
69931
70286
|
// src/cli/deps.ts
|
|
69932
70287
|
init_source();
|
|
69933
|
-
import { existsSync as
|
|
70288
|
+
import { existsSync as existsSync53 } from "node:fs";
|
|
69934
70289
|
import { homedir as homedir25 } from "node:os";
|
|
69935
70290
|
import { join as join47, resolve as resolve34 } from "node:path";
|
|
69936
70291
|
|
|
69937
70292
|
// src/deps/python.ts
|
|
69938
70293
|
import { createHash as createHash9 } from "node:crypto";
|
|
69939
70294
|
import {
|
|
69940
|
-
existsSync as
|
|
70295
|
+
existsSync as existsSync51,
|
|
69941
70296
|
mkdirSync as mkdirSync29,
|
|
69942
70297
|
readFileSync as readFileSync48,
|
|
69943
70298
|
rmSync as rmSync13,
|
|
69944
70299
|
writeFileSync as writeFileSync26
|
|
69945
70300
|
} from "node:fs";
|
|
69946
|
-
import { dirname as
|
|
70301
|
+
import { dirname as dirname15, join as join45 } from "node:path";
|
|
69947
70302
|
import { homedir as homedir23 } from "node:os";
|
|
69948
70303
|
import { execFileSync as execFileSync14 } from "node:child_process";
|
|
69949
70304
|
|
|
@@ -69965,7 +70320,7 @@ function ensurePythonEnv(opts) {
|
|
|
69965
70320
|
const { skillName, requirementsPath, force = false } = opts;
|
|
69966
70321
|
const cacheRoot = opts.cacheRoot ?? defaultPythonCacheRoot();
|
|
69967
70322
|
const hostPython = opts.pythonBin ?? "python3";
|
|
69968
|
-
if (!
|
|
70323
|
+
if (!existsSync51(requirementsPath)) {
|
|
69969
70324
|
throw new PythonEnvError(`requirements file not found: ${requirementsPath}`);
|
|
69970
70325
|
}
|
|
69971
70326
|
const venvDir = join45(cacheRoot, skillName);
|
|
@@ -69974,7 +70329,7 @@ function ensurePythonEnv(opts) {
|
|
|
69974
70329
|
const pythonBin = join45(binDir, "python");
|
|
69975
70330
|
const pipBin = join45(binDir, "pip");
|
|
69976
70331
|
const targetHash = hashFile(requirementsPath);
|
|
69977
|
-
if (!force &&
|
|
70332
|
+
if (!force && existsSync51(stampPath) && existsSync51(pythonBin)) {
|
|
69978
70333
|
const existingHash = readFileSync48(stampPath, "utf8").trim();
|
|
69979
70334
|
if (existingHash === targetHash) {
|
|
69980
70335
|
return {
|
|
@@ -69987,10 +70342,10 @@ function ensurePythonEnv(opts) {
|
|
|
69987
70342
|
};
|
|
69988
70343
|
}
|
|
69989
70344
|
}
|
|
69990
|
-
if (
|
|
70345
|
+
if (existsSync51(venvDir)) {
|
|
69991
70346
|
rmSync13(venvDir, { recursive: true, force: true });
|
|
69992
70347
|
}
|
|
69993
|
-
mkdirSync29(
|
|
70348
|
+
mkdirSync29(dirname15(venvDir), { recursive: true });
|
|
69994
70349
|
try {
|
|
69995
70350
|
execFileSync14(hostPython, ["-m", "venv", venvDir], { stdio: "pipe" });
|
|
69996
70351
|
} catch (err) {
|
|
@@ -70025,13 +70380,13 @@ function ensurePythonEnv(opts) {
|
|
|
70025
70380
|
import { createHash as createHash10 } from "node:crypto";
|
|
70026
70381
|
import {
|
|
70027
70382
|
copyFileSync as copyFileSync9,
|
|
70028
|
-
existsSync as
|
|
70383
|
+
existsSync as existsSync52,
|
|
70029
70384
|
mkdirSync as mkdirSync30,
|
|
70030
70385
|
readFileSync as readFileSync49,
|
|
70031
70386
|
rmSync as rmSync14,
|
|
70032
70387
|
writeFileSync as writeFileSync27
|
|
70033
70388
|
} from "node:fs";
|
|
70034
|
-
import { dirname as
|
|
70389
|
+
import { dirname as dirname16, join as join46 } from "node:path";
|
|
70035
70390
|
import { homedir as homedir24 } from "node:os";
|
|
70036
70391
|
import { execFileSync as execFileSync15 } from "node:child_process";
|
|
70037
70392
|
|
|
@@ -70058,14 +70413,14 @@ function defaultNodeCacheRoot() {
|
|
|
70058
70413
|
return join46(homedir24(), ".switchroom", "deps", "node");
|
|
70059
70414
|
}
|
|
70060
70415
|
function hashDepInputs(packageJsonPath) {
|
|
70061
|
-
const sourceDir =
|
|
70416
|
+
const sourceDir = dirname16(packageJsonPath);
|
|
70062
70417
|
const hasher = createHash10("sha256");
|
|
70063
70418
|
hasher.update(`package.json
|
|
70064
70419
|
`);
|
|
70065
70420
|
hasher.update(readFileSync49(packageJsonPath));
|
|
70066
70421
|
for (const lockName of ALL_LOCKFILES) {
|
|
70067
70422
|
const lockPath = join46(sourceDir, lockName);
|
|
70068
|
-
if (
|
|
70423
|
+
if (existsSync52(lockPath)) {
|
|
70069
70424
|
hasher.update(`
|
|
70070
70425
|
`);
|
|
70071
70426
|
hasher.update(lockName);
|
|
@@ -70080,16 +70435,16 @@ function ensureNodeEnv(opts) {
|
|
|
70080
70435
|
const { skillName, packageJsonPath, force = false } = opts;
|
|
70081
70436
|
const cacheRoot = opts.cacheRoot ?? defaultNodeCacheRoot();
|
|
70082
70437
|
const installer = opts.installer ?? "bun";
|
|
70083
|
-
if (!
|
|
70438
|
+
if (!existsSync52(packageJsonPath)) {
|
|
70084
70439
|
throw new NodeEnvError(`package.json not found: ${packageJsonPath}`);
|
|
70085
70440
|
}
|
|
70086
|
-
const sourceDir =
|
|
70441
|
+
const sourceDir = dirname16(packageJsonPath);
|
|
70087
70442
|
const envDir = join46(cacheRoot, skillName);
|
|
70088
70443
|
const stampPath = join46(envDir, ".package.sha256");
|
|
70089
70444
|
const nodeModulesDir = join46(envDir, "node_modules");
|
|
70090
70445
|
const binDir = join46(nodeModulesDir, ".bin");
|
|
70091
70446
|
const targetHash = hashDepInputs(packageJsonPath);
|
|
70092
|
-
if (!force &&
|
|
70447
|
+
if (!force && existsSync52(stampPath) && existsSync52(nodeModulesDir)) {
|
|
70093
70448
|
const existingHash = readFileSync49(stampPath, "utf8").trim();
|
|
70094
70449
|
if (existingHash === targetHash) {
|
|
70095
70450
|
return {
|
|
@@ -70101,7 +70456,7 @@ function ensureNodeEnv(opts) {
|
|
|
70101
70456
|
};
|
|
70102
70457
|
}
|
|
70103
70458
|
}
|
|
70104
|
-
if (
|
|
70459
|
+
if (existsSync52(envDir)) {
|
|
70105
70460
|
rmSync14(envDir, { recursive: true, force: true });
|
|
70106
70461
|
}
|
|
70107
70462
|
mkdirSync30(envDir, { recursive: true });
|
|
@@ -70109,7 +70464,7 @@ function ensureNodeEnv(opts) {
|
|
|
70109
70464
|
let copiedLockfile = false;
|
|
70110
70465
|
for (const lockName of LOCKFILES_FOR[installer]) {
|
|
70111
70466
|
const lockPath = join46(sourceDir, lockName);
|
|
70112
|
-
if (
|
|
70467
|
+
if (existsSync52(lockPath)) {
|
|
70113
70468
|
copyFileSync9(lockPath, join46(envDir, lockName));
|
|
70114
70469
|
copiedLockfile = true;
|
|
70115
70470
|
}
|
|
@@ -70145,22 +70500,22 @@ function registerDepsCommand(program3) {
|
|
|
70145
70500
|
const deps = program3.command("deps").description("Manage cached per-skill dependency environments");
|
|
70146
70501
|
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
70502
|
const skillsRoot = builtinSkillsRoot();
|
|
70148
|
-
if (!
|
|
70503
|
+
if (!existsSync53(skillsRoot)) {
|
|
70149
70504
|
console.error(source_default.red(`Bundled skills pool dir not found at ${skillsRoot} \u2014 run \`switchroom update\` to install it.`));
|
|
70150
70505
|
process.exit(1);
|
|
70151
70506
|
}
|
|
70152
70507
|
const skillDir = join47(skillsRoot, skill);
|
|
70153
|
-
if (!
|
|
70508
|
+
if (!existsSync53(skillDir)) {
|
|
70154
70509
|
console.error(source_default.red(`Unknown skill: ${skill} (no dir at ${skillDir})`));
|
|
70155
70510
|
process.exit(1);
|
|
70156
70511
|
}
|
|
70157
70512
|
const requirementsPath = join47(skillDir, "requirements.txt");
|
|
70158
70513
|
const packageJsonPath = join47(skillDir, "package.json");
|
|
70159
|
-
const wantPython = opts.python ?? (!opts.python && !opts.node &&
|
|
70160
|
-
const wantNode = opts.node ?? (!opts.python && !opts.node &&
|
|
70514
|
+
const wantPython = opts.python ?? (!opts.python && !opts.node && existsSync53(requirementsPath));
|
|
70515
|
+
const wantNode = opts.node ?? (!opts.python && !opts.node && existsSync53(packageJsonPath));
|
|
70161
70516
|
let did = 0;
|
|
70162
70517
|
if (wantPython) {
|
|
70163
|
-
if (!
|
|
70518
|
+
if (!existsSync53(requirementsPath)) {
|
|
70164
70519
|
console.error(source_default.red(`Skill "${skill}" has no requirements.txt at ${requirementsPath}`));
|
|
70165
70520
|
process.exit(1);
|
|
70166
70521
|
}
|
|
@@ -70184,7 +70539,7 @@ function registerDepsCommand(program3) {
|
|
|
70184
70539
|
}
|
|
70185
70540
|
}
|
|
70186
70541
|
if (wantNode) {
|
|
70187
|
-
if (!
|
|
70542
|
+
if (!existsSync53(packageJsonPath)) {
|
|
70188
70543
|
console.error(source_default.red(`Skill "${skill}" has no package.json at ${packageJsonPath}`));
|
|
70189
70544
|
process.exit(1);
|
|
70190
70545
|
}
|
|
@@ -70217,9 +70572,9 @@ function registerDepsCommand(program3) {
|
|
|
70217
70572
|
// src/cli/workspace.ts
|
|
70218
70573
|
init_helpers();
|
|
70219
70574
|
init_loader();
|
|
70220
|
-
import { existsSync as
|
|
70575
|
+
import { existsSync as existsSync54 } from "node:fs";
|
|
70221
70576
|
import { resolve as resolve35, sep as sep2 } from "node:path";
|
|
70222
|
-
import { spawnSync as
|
|
70577
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
70223
70578
|
|
|
70224
70579
|
// src/agents/workspace.ts
|
|
70225
70580
|
import { readFile, stat } from "node:fs/promises";
|
|
@@ -70932,7 +71287,7 @@ function registerWorkspaceCommand(program3) {
|
|
|
70932
71287
|
process.exit(1);
|
|
70933
71288
|
}
|
|
70934
71289
|
const editor = process.env["EDITOR"] ?? process.env["VISUAL"] ?? "vi";
|
|
70935
|
-
const child =
|
|
71290
|
+
const child = spawnSync9(editor, [target], { stdio: "inherit" });
|
|
70936
71291
|
if (child.status !== 0 && child.status !== null) {
|
|
70937
71292
|
process.exit(child.status);
|
|
70938
71293
|
}
|
|
@@ -70994,12 +71349,12 @@ function registerWorkspaceCommand(program3) {
|
|
|
70994
71349
|
if (!dir)
|
|
70995
71350
|
return;
|
|
70996
71351
|
const gitDir = resolve35(dir, ".git");
|
|
70997
|
-
if (!
|
|
71352
|
+
if (!existsSync54(gitDir)) {
|
|
70998
71353
|
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
71354
|
`);
|
|
71000
71355
|
return;
|
|
71001
71356
|
}
|
|
71002
|
-
const statusResult =
|
|
71357
|
+
const statusResult = spawnSync9("git", ["status", "--short"], {
|
|
71003
71358
|
cwd: dir,
|
|
71004
71359
|
encoding: "utf-8"
|
|
71005
71360
|
});
|
|
@@ -71014,7 +71369,7 @@ function registerWorkspaceCommand(program3) {
|
|
|
71014
71369
|
return;
|
|
71015
71370
|
}
|
|
71016
71371
|
const message = opts.message || `checkpoint: ${new Date().toISOString()}`;
|
|
71017
|
-
const addResult =
|
|
71372
|
+
const addResult = spawnSync9("git", ["add", "-A"], {
|
|
71018
71373
|
cwd: dir,
|
|
71019
71374
|
encoding: "utf-8"
|
|
71020
71375
|
});
|
|
@@ -71023,7 +71378,7 @@ function registerWorkspaceCommand(program3) {
|
|
|
71023
71378
|
`);
|
|
71024
71379
|
process.exit(1);
|
|
71025
71380
|
}
|
|
71026
|
-
const commitResult =
|
|
71381
|
+
const commitResult = spawnSync9("git", ["commit", "-m", message], {
|
|
71027
71382
|
cwd: dir,
|
|
71028
71383
|
encoding: "utf-8"
|
|
71029
71384
|
});
|
|
@@ -71032,7 +71387,7 @@ function registerWorkspaceCommand(program3) {
|
|
|
71032
71387
|
`);
|
|
71033
71388
|
process.exit(1);
|
|
71034
71389
|
}
|
|
71035
|
-
const shaResult =
|
|
71390
|
+
const shaResult = spawnSync9("git", ["rev-parse", "--short", "HEAD"], {
|
|
71036
71391
|
cwd: dir,
|
|
71037
71392
|
encoding: "utf-8"
|
|
71038
71393
|
});
|
|
@@ -71048,12 +71403,12 @@ function registerWorkspaceCommand(program3) {
|
|
|
71048
71403
|
if (!dir)
|
|
71049
71404
|
return;
|
|
71050
71405
|
const gitDir = resolve35(dir, ".git");
|
|
71051
|
-
if (!
|
|
71406
|
+
if (!existsSync54(gitDir)) {
|
|
71052
71407
|
process.stdout.write(`Workspace is not a git repository.
|
|
71053
71408
|
`);
|
|
71054
71409
|
return;
|
|
71055
71410
|
}
|
|
71056
|
-
const child =
|
|
71411
|
+
const child = spawnSync9("git", ["status", "--short"], {
|
|
71057
71412
|
cwd: dir,
|
|
71058
71413
|
stdio: "inherit"
|
|
71059
71414
|
});
|
|
@@ -71073,7 +71428,7 @@ function resolveAgentWorkspaceDirOrExit(program3, agentName) {
|
|
|
71073
71428
|
const agentsDir = resolveAgentsDir(config);
|
|
71074
71429
|
const agentDir = resolve35(agentsDir, agentName);
|
|
71075
71430
|
const dir = resolveAgentWorkspaceDir(agentDir);
|
|
71076
|
-
if (!
|
|
71431
|
+
if (!existsSync54(dir)) {
|
|
71077
71432
|
process.stderr.write(`workspace: ${dir} does not exist yet. Run \`switchroom setup\` or \`switchroom agent scaffold ${agentName}\` to seed it.
|
|
71078
71433
|
`);
|
|
71079
71434
|
return;
|
|
@@ -71109,7 +71464,7 @@ function safeParseInt(value, fallback) {
|
|
|
71109
71464
|
init_helpers();
|
|
71110
71465
|
init_loader();
|
|
71111
71466
|
init_merge();
|
|
71112
|
-
import { copyFileSync as copyFileSync10, existsSync as
|
|
71467
|
+
import { copyFileSync as copyFileSync10, existsSync as existsSync55, readFileSync as readFileSync50, writeFileSync as writeFileSync28 } from "node:fs";
|
|
71113
71468
|
import { join as join48, resolve as resolve36 } from "node:path";
|
|
71114
71469
|
init_schema();
|
|
71115
71470
|
function resolveSoulTargetOrExit(program3, agentName) {
|
|
@@ -71125,7 +71480,7 @@ function resolveSoulTargetOrExit(program3, agentName) {
|
|
|
71125
71480
|
const agentsDir = resolveAgentsDir(config);
|
|
71126
71481
|
const agentDir = resolve36(agentsDir, agentName);
|
|
71127
71482
|
const workspaceDir = resolveAgentWorkspaceDir(agentDir);
|
|
71128
|
-
if (!
|
|
71483
|
+
if (!existsSync55(workspaceDir)) {
|
|
71129
71484
|
console.error(`soul: ${workspaceDir} does not exist yet. Run \`switchroom setup\` ` + `or \`switchroom agent scaffold ${agentName}\` to seed it.`);
|
|
71130
71485
|
process.exit(1);
|
|
71131
71486
|
}
|
|
@@ -71151,7 +71506,7 @@ function registerSoulCommand(program3) {
|
|
|
71151
71506
|
const t = resolveSoulTargetOrExit(program3, agentName);
|
|
71152
71507
|
if (!t)
|
|
71153
71508
|
return;
|
|
71154
|
-
if (!
|
|
71509
|
+
if (!existsSync55(t.soulPath)) {
|
|
71155
71510
|
console.error(`soul: ${t.soulPath} does not exist yet \u2014 run ` + `\`switchroom soul reset ${agentName}\` to seed it.`);
|
|
71156
71511
|
process.exit(1);
|
|
71157
71512
|
}
|
|
@@ -71166,7 +71521,7 @@ function registerSoulCommand(program3) {
|
|
|
71166
71521
|
console.error(`soul: profile "${t.profileName}" ships no SOUL.md.hbs \u2014 ` + `nothing to re-seed from.`);
|
|
71167
71522
|
process.exit(1);
|
|
71168
71523
|
}
|
|
71169
|
-
const exists =
|
|
71524
|
+
const exists = existsSync55(t.soulPath);
|
|
71170
71525
|
if (exists && !opts.yes) {
|
|
71171
71526
|
if (!isInteractive()) {
|
|
71172
71527
|
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 +71536,7 @@ function registerSoulCommand(program3) {
|
|
|
71181
71536
|
let backupPath;
|
|
71182
71537
|
if (exists) {
|
|
71183
71538
|
backupPath = `${t.soulPath}.bak`;
|
|
71184
|
-
if (
|
|
71539
|
+
if (existsSync55(backupPath)) {
|
|
71185
71540
|
backupPath = `${t.soulPath}.bak.${Date.now()}`;
|
|
71186
71541
|
}
|
|
71187
71542
|
copyFileSync10(t.soulPath, backupPath);
|
|
@@ -71200,7 +71555,7 @@ function registerSoulCommand(program3) {
|
|
|
71200
71555
|
// src/cli/debug.ts
|
|
71201
71556
|
init_helpers();
|
|
71202
71557
|
init_loader();
|
|
71203
|
-
import { existsSync as
|
|
71558
|
+
import { existsSync as existsSync56, readFileSync as readFileSync51, readdirSync as readdirSync19, statSync as statSync24 } from "node:fs";
|
|
71204
71559
|
import { resolve as resolve37, join as join49 } from "node:path";
|
|
71205
71560
|
import { createHash as createHash11 } from "node:crypto";
|
|
71206
71561
|
init_merge();
|
|
@@ -71216,7 +71571,7 @@ function sha256(content) {
|
|
|
71216
71571
|
}
|
|
71217
71572
|
function findLatestTranscriptJsonl(claudeConfigDir) {
|
|
71218
71573
|
const projectsDir = join49(claudeConfigDir, "projects");
|
|
71219
|
-
if (!
|
|
71574
|
+
if (!existsSync56(projectsDir))
|
|
71220
71575
|
return;
|
|
71221
71576
|
try {
|
|
71222
71577
|
const entries = readdirSync19(projectsDir, { withFileTypes: true });
|
|
@@ -71226,9 +71581,9 @@ function findLatestTranscriptJsonl(claudeConfigDir) {
|
|
|
71226
71581
|
continue;
|
|
71227
71582
|
const projectPath = join49(projectsDir, entry.name);
|
|
71228
71583
|
const transcriptPath = join49(projectPath, "transcript.jsonl");
|
|
71229
|
-
if (!
|
|
71584
|
+
if (!existsSync56(transcriptPath))
|
|
71230
71585
|
continue;
|
|
71231
|
-
const stat3 =
|
|
71586
|
+
const stat3 = statSync24(transcriptPath);
|
|
71232
71587
|
if (!latest || stat3.mtimeMs > latest.mtime) {
|
|
71233
71588
|
latest = { path: transcriptPath, mtime: stat3.mtimeMs };
|
|
71234
71589
|
}
|
|
@@ -71289,7 +71644,7 @@ function registerDebugCommand(program3) {
|
|
|
71289
71644
|
}
|
|
71290
71645
|
const agentsDir = resolveAgentsDir(config);
|
|
71291
71646
|
const agentDir = resolve37(agentsDir, agentName);
|
|
71292
|
-
if (!
|
|
71647
|
+
if (!existsSync56(agentDir)) {
|
|
71293
71648
|
console.error(`Agent directory not found: ${agentDir}`);
|
|
71294
71649
|
process.exit(1);
|
|
71295
71650
|
}
|
|
@@ -71344,7 +71699,7 @@ function registerDebugCommand(program3) {
|
|
|
71344
71699
|
}
|
|
71345
71700
|
console.log(`=== Append System Prompt (per-session) ===
|
|
71346
71701
|
`);
|
|
71347
|
-
const handoffContent =
|
|
71702
|
+
const handoffContent = existsSync56(handoffPath) ? readFileSync51(handoffPath, "utf-8") : "";
|
|
71348
71703
|
if (handoffContent.trim().length > 0) {
|
|
71349
71704
|
console.log(`-- Handoff Briefing (${formatBytes(handoffContent.length)}) --`);
|
|
71350
71705
|
console.log(handoffContent);
|
|
@@ -71355,7 +71710,7 @@ function registerDebugCommand(program3) {
|
|
|
71355
71710
|
}
|
|
71356
71711
|
console.log(`=== CLAUDE.md (auto-loaded by Claude Code) ===
|
|
71357
71712
|
`);
|
|
71358
|
-
const claudeMdContent =
|
|
71713
|
+
const claudeMdContent = existsSync56(claudeMdPath) ? readFileSync51(claudeMdPath, "utf-8") : "";
|
|
71359
71714
|
if (claudeMdContent.trim().length > 0) {
|
|
71360
71715
|
console.log(`(${formatBytes(claudeMdContent.length)})`);
|
|
71361
71716
|
console.log(claudeMdContent);
|
|
@@ -71366,7 +71721,7 @@ function registerDebugCommand(program3) {
|
|
|
71366
71721
|
}
|
|
71367
71722
|
console.log(`=== Persona (SOUL.md) ===
|
|
71368
71723
|
`);
|
|
71369
|
-
const soulMdContent =
|
|
71724
|
+
const soulMdContent = existsSync56(soulMdPath) ? readFileSync51(soulMdPath, "utf-8") : existsSync56(workspaceSoulMdPath) ? readFileSync51(workspaceSoulMdPath, "utf-8") : "";
|
|
71370
71725
|
if (soulMdContent.trim().length > 0) {
|
|
71371
71726
|
console.log(`(${formatBytes(soulMdContent.length)})`);
|
|
71372
71727
|
console.log(soulMdContent);
|
|
@@ -71447,7 +71802,7 @@ init_source();
|
|
|
71447
71802
|
|
|
71448
71803
|
// src/worktree/claim.ts
|
|
71449
71804
|
import { execFileSync as execFileSync16 } from "node:child_process";
|
|
71450
|
-
import { closeSync as closeSync11, mkdirSync as mkdirSync32, openSync as openSync11, existsSync as
|
|
71805
|
+
import { closeSync as closeSync11, mkdirSync as mkdirSync32, openSync as openSync11, existsSync as existsSync58, unlinkSync as unlinkSync12 } from "node:fs";
|
|
71451
71806
|
import { join as join51, resolve as resolve39 } from "node:path";
|
|
71452
71807
|
import { homedir as homedir27 } from "node:os";
|
|
71453
71808
|
import { randomBytes as randomBytes11 } from "node:crypto";
|
|
@@ -71459,7 +71814,7 @@ import {
|
|
|
71459
71814
|
readFileSync as readFileSync52,
|
|
71460
71815
|
readdirSync as readdirSync20,
|
|
71461
71816
|
unlinkSync as unlinkSync11,
|
|
71462
|
-
existsSync as
|
|
71817
|
+
existsSync as existsSync57,
|
|
71463
71818
|
renameSync as renameSync11
|
|
71464
71819
|
} from "node:fs";
|
|
71465
71820
|
import { join as join50, resolve as resolve38 } from "node:path";
|
|
@@ -71573,7 +71928,7 @@ function expandHome(p) {
|
|
|
71573
71928
|
}
|
|
71574
71929
|
async function claimWorktree(input, codeRepos) {
|
|
71575
71930
|
const repoPath = resolveRepoPath(input.repo, codeRepos);
|
|
71576
|
-
if (!
|
|
71931
|
+
if (!existsSync58(repoPath)) {
|
|
71577
71932
|
throw new Error(`Repository path does not exist: ${repoPath}`);
|
|
71578
71933
|
}
|
|
71579
71934
|
let concurrencyCap = DEFAULT_CONCURRENCY;
|
|
@@ -71627,7 +71982,7 @@ async function claimWorktree(input, codeRepos) {
|
|
|
71627
71982
|
|
|
71628
71983
|
// src/worktree/release.ts
|
|
71629
71984
|
import { execFileSync as execFileSync17 } from "node:child_process";
|
|
71630
|
-
import { existsSync as
|
|
71985
|
+
import { existsSync as existsSync59 } from "node:fs";
|
|
71631
71986
|
function releaseWorktree(input) {
|
|
71632
71987
|
const { id } = input;
|
|
71633
71988
|
const record2 = readRecord(id);
|
|
@@ -71635,7 +71990,7 @@ function releaseWorktree(input) {
|
|
|
71635
71990
|
return { released: true };
|
|
71636
71991
|
}
|
|
71637
71992
|
let gitSuccess = true;
|
|
71638
|
-
if (
|
|
71993
|
+
if (existsSync59(record2.path)) {
|
|
71639
71994
|
try {
|
|
71640
71995
|
execFileSync17("git", ["worktree", "remove", "--force", record2.path], {
|
|
71641
71996
|
cwd: record2.repo,
|
|
@@ -71674,7 +72029,7 @@ function listWorktrees() {
|
|
|
71674
72029
|
|
|
71675
72030
|
// src/worktree/reaper.ts
|
|
71676
72031
|
import { execFileSync as execFileSync18 } from "node:child_process";
|
|
71677
|
-
import { existsSync as
|
|
72032
|
+
import { existsSync as existsSync60 } from "node:fs";
|
|
71678
72033
|
var STALE_THRESHOLD_MS = 10 * 60 * 1000;
|
|
71679
72034
|
function isPathInUse(path7) {
|
|
71680
72035
|
try {
|
|
@@ -71701,7 +72056,7 @@ function hasUncommittedChanges(repoPath, worktreePath) {
|
|
|
71701
72056
|
function reapRecord(record2) {
|
|
71702
72057
|
const { id, path: path7, repo, branch, ownerAgent } = record2;
|
|
71703
72058
|
let warning = null;
|
|
71704
|
-
if (
|
|
72059
|
+
if (existsSync60(path7)) {
|
|
71705
72060
|
if (hasUncommittedChanges(repo, path7)) {
|
|
71706
72061
|
warning = `[worktree-reaper] Reaped worktree with uncommitted changes: ` + `id=${id} branch=${branch} agent=${ownerAgent ?? "unknown"} path=${path7}`;
|
|
71707
72062
|
}
|
|
@@ -71722,7 +72077,7 @@ function runReaper(nowMs) {
|
|
|
71722
72077
|
const warnings = [];
|
|
71723
72078
|
for (const record2 of records) {
|
|
71724
72079
|
const heartbeatAge = now - new Date(record2.heartbeatAt).getTime();
|
|
71725
|
-
const worktreeExists =
|
|
72080
|
+
const worktreeExists = existsSync60(record2.path);
|
|
71726
72081
|
if (!worktreeExists) {
|
|
71727
72082
|
deleteRecord(record2.id);
|
|
71728
72083
|
reaped.push(record2.id);
|
|
@@ -72177,7 +72532,7 @@ function registerDriveMcpLauncherCommand(program3) {
|
|
|
72177
72532
|
|
|
72178
72533
|
// src/cli/apply.ts
|
|
72179
72534
|
init_source();
|
|
72180
|
-
import { accessSync as
|
|
72535
|
+
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
72536
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
72182
72537
|
import { spawnSync as childSpawnSync } from "node:child_process";
|
|
72183
72538
|
import readline from "node:readline";
|
|
@@ -72522,7 +72877,7 @@ agents:
|
|
|
72522
72877
|
|
|
72523
72878
|
// src/cli/apply.ts
|
|
72524
72879
|
init_resolver();
|
|
72525
|
-
import { dirname as
|
|
72880
|
+
import { dirname as dirname19, join as join56, resolve as resolve41 } from "node:path";
|
|
72526
72881
|
import { homedir as homedir29 } from "node:os";
|
|
72527
72882
|
import { execFileSync as execFileSync19 } from "node:child_process";
|
|
72528
72883
|
init_vault();
|
|
@@ -72530,7 +72885,7 @@ init_loader();
|
|
|
72530
72885
|
init_loader();
|
|
72531
72886
|
|
|
72532
72887
|
// src/cli/update-prompt-hook.ts
|
|
72533
|
-
import { existsSync as
|
|
72888
|
+
import { existsSync as existsSync61, readFileSync as readFileSync53, writeFileSync as writeFileSync31, chmodSync as chmodSync10, mkdirSync as mkdirSync34 } from "node:fs";
|
|
72534
72889
|
import { join as join53 } from "node:path";
|
|
72535
72890
|
var HOOK_FILENAME = "update-card-on-prompt.sh";
|
|
72536
72891
|
function updatePromptHookScript() {
|
|
@@ -72602,7 +72957,7 @@ function installUpdatePromptHook(agentDir) {
|
|
|
72602
72957
|
const scriptPath = join53(hooksDir, HOOK_FILENAME);
|
|
72603
72958
|
const desired = updatePromptHookScript();
|
|
72604
72959
|
let installed = false;
|
|
72605
|
-
const existing =
|
|
72960
|
+
const existing = existsSync61(scriptPath) ? readFileSync53(scriptPath, "utf-8") : "";
|
|
72606
72961
|
if (existing !== desired) {
|
|
72607
72962
|
writeFileSync31(scriptPath, desired, { mode: 493 });
|
|
72608
72963
|
chmodSync10(scriptPath, 493);
|
|
@@ -72613,7 +72968,7 @@ function installUpdatePromptHook(agentDir) {
|
|
|
72613
72968
|
} catch {}
|
|
72614
72969
|
}
|
|
72615
72970
|
const settingsPath = join53(agentDir, ".claude", "settings.json");
|
|
72616
|
-
if (!
|
|
72971
|
+
if (!existsSync61(settingsPath)) {
|
|
72617
72972
|
return { scriptPath, settingsPath, installed };
|
|
72618
72973
|
}
|
|
72619
72974
|
const raw = readFileSync53(settingsPath, "utf-8");
|
|
@@ -72729,24 +73084,114 @@ function detectInstallType() {
|
|
|
72729
73084
|
}
|
|
72730
73085
|
}
|
|
72731
73086
|
|
|
73087
|
+
// src/cli/operator-uid.ts
|
|
73088
|
+
import {
|
|
73089
|
+
chownSync as chownSync2,
|
|
73090
|
+
existsSync as existsSync63,
|
|
73091
|
+
lstatSync as lstatSync7,
|
|
73092
|
+
readdirSync as readdirSync22,
|
|
73093
|
+
realpathSync as realpathSync6,
|
|
73094
|
+
statSync as statSync25
|
|
73095
|
+
} from "node:fs";
|
|
73096
|
+
import { join as join55 } from "node:path";
|
|
73097
|
+
function resolveOperatorUid() {
|
|
73098
|
+
const sudoUid = process.env.SUDO_UID;
|
|
73099
|
+
if (sudoUid !== undefined) {
|
|
73100
|
+
const parsed = parseInt(sudoUid, 10);
|
|
73101
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
73102
|
+
return parsed;
|
|
73103
|
+
}
|
|
73104
|
+
if (typeof process.getuid === "function") {
|
|
73105
|
+
const uid = process.getuid();
|
|
73106
|
+
if (uid > 0)
|
|
73107
|
+
return uid;
|
|
73108
|
+
}
|
|
73109
|
+
return;
|
|
73110
|
+
}
|
|
73111
|
+
function operatorOwnedPaths(home2) {
|
|
73112
|
+
const root = join55(home2, ".switchroom");
|
|
73113
|
+
return [
|
|
73114
|
+
join55(root, "vault"),
|
|
73115
|
+
join55(root, "vault-auto-unlock"),
|
|
73116
|
+
join55(root, "vault-audit.log"),
|
|
73117
|
+
join55(root, "host-control-audit.log"),
|
|
73118
|
+
join55(root, "accounts"),
|
|
73119
|
+
join55(root, "compose")
|
|
73120
|
+
];
|
|
73121
|
+
}
|
|
73122
|
+
function restoreOperatorOwnership(home2, operatorUid, deps = {}) {
|
|
73123
|
+
const chown = deps.chown ?? ((p, u, g) => chownSync2(p, u, g));
|
|
73124
|
+
const exists = deps.exists ?? ((p) => existsSync63(p));
|
|
73125
|
+
const isSymlink = deps.isSymlink ?? ((p) => {
|
|
73126
|
+
try {
|
|
73127
|
+
return lstatSync7(p).isSymbolicLink();
|
|
73128
|
+
} catch {
|
|
73129
|
+
return false;
|
|
73130
|
+
}
|
|
73131
|
+
});
|
|
73132
|
+
const isDir = deps.isDir ?? ((p) => {
|
|
73133
|
+
try {
|
|
73134
|
+
return statSync25(p).isDirectory();
|
|
73135
|
+
} catch {
|
|
73136
|
+
return false;
|
|
73137
|
+
}
|
|
73138
|
+
});
|
|
73139
|
+
const realpath2 = deps.realpath ?? ((p) => {
|
|
73140
|
+
try {
|
|
73141
|
+
return realpathSync6(p);
|
|
73142
|
+
} catch {
|
|
73143
|
+
return p;
|
|
73144
|
+
}
|
|
73145
|
+
});
|
|
73146
|
+
const readdir2 = deps.readdir ?? ((p) => {
|
|
73147
|
+
try {
|
|
73148
|
+
return readdirSync22(p);
|
|
73149
|
+
} catch {
|
|
73150
|
+
return [];
|
|
73151
|
+
}
|
|
73152
|
+
});
|
|
73153
|
+
const chowned = [];
|
|
73154
|
+
const seen = new Set;
|
|
73155
|
+
const visit = (path8) => {
|
|
73156
|
+
if (!exists(path8))
|
|
73157
|
+
return;
|
|
73158
|
+
const target = isSymlink(path8) ? realpath2(path8) : path8;
|
|
73159
|
+
if (seen.has(target))
|
|
73160
|
+
return;
|
|
73161
|
+
seen.add(target);
|
|
73162
|
+
try {
|
|
73163
|
+
chown(target, operatorUid, operatorUid);
|
|
73164
|
+
chowned.push(target);
|
|
73165
|
+
} catch {}
|
|
73166
|
+
if (isDir(target)) {
|
|
73167
|
+
for (const entry of readdir2(target)) {
|
|
73168
|
+
visit(join55(target, entry));
|
|
73169
|
+
}
|
|
73170
|
+
}
|
|
73171
|
+
};
|
|
73172
|
+
for (const p of operatorOwnedPaths(home2))
|
|
73173
|
+
visit(p);
|
|
73174
|
+
return chowned;
|
|
73175
|
+
}
|
|
73176
|
+
|
|
72732
73177
|
// src/cli/apply.ts
|
|
72733
73178
|
var EMBEDDED_EXAMPLES = {
|
|
72734
73179
|
switchroom: switchroom_default,
|
|
72735
73180
|
minimal: minimal_default
|
|
72736
73181
|
};
|
|
72737
|
-
var DEFAULT_COMPOSE_PATH2 =
|
|
73182
|
+
var DEFAULT_COMPOSE_PATH2 = join56(homedir29(), ".switchroom", "compose", "docker-compose.yml");
|
|
72738
73183
|
var COMPOSE_PROJECT2 = "switchroom";
|
|
72739
73184
|
function resolveVaultBindMountDir(homeDir, ctx) {
|
|
72740
73185
|
const isCustomPath = ctx.migrationKind === "custom-path-skipped";
|
|
72741
73186
|
if (isCustomPath && ctx.customVaultPath) {
|
|
72742
|
-
return
|
|
73187
|
+
return dirname19(ctx.customVaultPath);
|
|
72743
73188
|
}
|
|
72744
|
-
return
|
|
73189
|
+
return join56(homeDir, ".switchroom", "vault");
|
|
72745
73190
|
}
|
|
72746
73191
|
function inspectVaultBindMountDir(vaultDir) {
|
|
72747
|
-
if (!
|
|
73192
|
+
if (!existsSync64(vaultDir))
|
|
72748
73193
|
return { kind: "missing" };
|
|
72749
|
-
const entries =
|
|
73194
|
+
const entries = readdirSync23(vaultDir);
|
|
72750
73195
|
const unknown = [];
|
|
72751
73196
|
for (const name of entries) {
|
|
72752
73197
|
if (KNOWN_VAULT_ARTIFACT_NAMES.has(name))
|
|
@@ -72772,30 +73217,30 @@ function hasVaultRefs(value) {
|
|
|
72772
73217
|
async function ensureHostMountSources(config) {
|
|
72773
73218
|
const home2 = homedir29();
|
|
72774
73219
|
const dirs = [
|
|
72775
|
-
|
|
72776
|
-
|
|
72777
|
-
|
|
72778
|
-
|
|
72779
|
-
|
|
73220
|
+
join56(home2, ".switchroom", "approvals"),
|
|
73221
|
+
join56(home2, ".switchroom", "scheduler"),
|
|
73222
|
+
join56(home2, ".switchroom", "logs"),
|
|
73223
|
+
join56(home2, ".switchroom", "compose"),
|
|
73224
|
+
join56(home2, ".switchroom", "broker-operator")
|
|
72780
73225
|
];
|
|
72781
73226
|
for (const name of Object.keys(config.agents)) {
|
|
72782
|
-
dirs.push(
|
|
72783
|
-
dirs.push(
|
|
72784
|
-
dirs.push(
|
|
73227
|
+
dirs.push(join56(home2, ".switchroom", "agents", name));
|
|
73228
|
+
dirs.push(join56(home2, ".switchroom", "logs", name));
|
|
73229
|
+
dirs.push(join56(home2, ".claude", "projects", name));
|
|
72785
73230
|
}
|
|
72786
73231
|
for (const dir of dirs) {
|
|
72787
73232
|
await mkdir(dir, { recursive: true });
|
|
72788
73233
|
}
|
|
72789
|
-
const autoUnlockPath =
|
|
72790
|
-
if (!
|
|
73234
|
+
const autoUnlockPath = join56(home2, ".switchroom", "vault-auto-unlock");
|
|
73235
|
+
if (!existsSync64(autoUnlockPath)) {
|
|
72791
73236
|
writeFileSync32(autoUnlockPath, "", { mode: 384 });
|
|
72792
73237
|
}
|
|
72793
|
-
const auditLogPath =
|
|
72794
|
-
if (!
|
|
73238
|
+
const auditLogPath = join56(home2, ".switchroom", "vault-audit.log");
|
|
73239
|
+
if (!existsSync64(auditLogPath)) {
|
|
72795
73240
|
writeFileSync32(auditLogPath, "", { mode: 420 });
|
|
72796
73241
|
}
|
|
72797
|
-
const hostdAuditLogPath =
|
|
72798
|
-
if (!
|
|
73242
|
+
const hostdAuditLogPath = join56(home2, ".switchroom", "host-control-audit.log");
|
|
73243
|
+
if (!existsSync64(hostdAuditLogPath)) {
|
|
72799
73244
|
writeFileSync32(hostdAuditLogPath, "", { mode: 420 });
|
|
72800
73245
|
}
|
|
72801
73246
|
}
|
|
@@ -72816,7 +73261,7 @@ ${out.trim()}`;
|
|
|
72816
73261
|
}
|
|
72817
73262
|
function runApplyPreflight(config, opts = {}) {
|
|
72818
73263
|
const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
|
|
72819
|
-
if (hasVaultRefs(config) && !
|
|
73264
|
+
if (hasVaultRefs(config) && !existsSync64(vaultPath)) {
|
|
72820
73265
|
throw new Error(`Config references vault keys (vault:<name>) but ${vaultPath} is missing. ` + `Run \`switchroom setup\` first to initialise the vault.`);
|
|
72821
73266
|
}
|
|
72822
73267
|
const detect = opts.detectComposeV2 ?? detectComposeV2;
|
|
@@ -72827,7 +73272,7 @@ function runApplyPreflight(config, opts = {}) {
|
|
|
72827
73272
|
detectAndReportLegacyGdriveSlots(vaultPath);
|
|
72828
73273
|
}
|
|
72829
73274
|
function detectAndReportLegacyGdriveSlots(vaultPath) {
|
|
72830
|
-
if (!
|
|
73275
|
+
if (!existsSync64(vaultPath))
|
|
72831
73276
|
return;
|
|
72832
73277
|
const passphrase = process.env.SWITCHROOM_VAULT_PASSPHRASE;
|
|
72833
73278
|
if (!passphrase)
|
|
@@ -72868,8 +73313,8 @@ function detectAndReportLegacyGdriveSlots(vaultPath) {
|
|
|
72868
73313
|
}
|
|
72869
73314
|
function writeInstallTypeCache(homeDir = homedir29()) {
|
|
72870
73315
|
const ctx = detectInstallType();
|
|
72871
|
-
const dir =
|
|
72872
|
-
const out =
|
|
73316
|
+
const dir = join56(homeDir, ".switchroom");
|
|
73317
|
+
const out = join56(dir, "install-type.json");
|
|
72873
73318
|
const tmp = `${out}.tmp`;
|
|
72874
73319
|
mkdirSync35(dir, { recursive: true });
|
|
72875
73320
|
const payload = {
|
|
@@ -72918,14 +73363,14 @@ Applying switchroom config...
|
|
|
72918
73363
|
writeOut(source_default.green(` + ${name}`) + source_default.gray(` (${agentConfig.extends ?? "default"}) \u2014 ${detail}
|
|
72919
73364
|
`));
|
|
72920
73365
|
try {
|
|
72921
|
-
installUpdatePromptHook(
|
|
73366
|
+
installUpdatePromptHook(join56(agentsDir, name));
|
|
72922
73367
|
} catch (hookErr) {
|
|
72923
73368
|
writeOut(source_default.gray(` (update-prompt hook install failed for ${name}: ${hookErr.message})
|
|
72924
73369
|
`));
|
|
72925
73370
|
}
|
|
72926
73371
|
try {
|
|
72927
73372
|
const uid = allocateAgentUid(name);
|
|
72928
|
-
alignAgentUid(name,
|
|
73373
|
+
alignAgentUid(name, join56(agentsDir, name), uid, {
|
|
72929
73374
|
confirm: !options.nonInteractive,
|
|
72930
73375
|
writeOut
|
|
72931
73376
|
});
|
|
@@ -72962,7 +73407,7 @@ Applying switchroom config...
|
|
|
72962
73407
|
for (const name of agentNames) {
|
|
72963
73408
|
try {
|
|
72964
73409
|
const uid = allocateAgentUid(name);
|
|
72965
|
-
alignAgentUid(name,
|
|
73410
|
+
alignAgentUid(name, join56(agentsDir, name), uid, {
|
|
72966
73411
|
confirm: !options.nonInteractive,
|
|
72967
73412
|
writeOut
|
|
72968
73413
|
});
|
|
@@ -73036,20 +73481,7 @@ Applying switchroom config...
|
|
|
73036
73481
|
process.exit(6);
|
|
73037
73482
|
}
|
|
73038
73483
|
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
|
-
})();
|
|
73484
|
+
const operatorUid = resolveOperatorUid();
|
|
73053
73485
|
const composeRelease = resolveRelease({
|
|
73054
73486
|
override: options.releaseOverride,
|
|
73055
73487
|
root: config.release
|
|
@@ -73064,7 +73496,7 @@ Applying switchroom config...
|
|
|
73064
73496
|
switchroomConfigPath,
|
|
73065
73497
|
operatorUid
|
|
73066
73498
|
});
|
|
73067
|
-
await mkdir(
|
|
73499
|
+
await mkdir(dirname19(composePath), { recursive: true });
|
|
73068
73500
|
await writeFile(composePath, composeContent, {
|
|
73069
73501
|
encoding: "utf8",
|
|
73070
73502
|
mode: 384
|
|
@@ -73079,6 +73511,13 @@ Wrote `) + composePath + source_default.gray(` (${composeBytes} bytes)
|
|
|
73079
73511
|
`);
|
|
73080
73512
|
writeOut(source_default.gray(` (If pull returns 401, login to ghcr.io first: see docs/operators/install.md#ghcr-auth)
|
|
73081
73513
|
`));
|
|
73514
|
+
if (process.geteuid?.() === 0 && operatorUid !== undefined) {
|
|
73515
|
+
const restored = restoreOperatorOwnership(homedir29(), operatorUid);
|
|
73516
|
+
if (restored.length > 0) {
|
|
73517
|
+
writeOut(source_default.gray(` Restored operator ownership of ${restored.length} ~/.switchroom path(s)
|
|
73518
|
+
`));
|
|
73519
|
+
}
|
|
73520
|
+
}
|
|
73082
73521
|
writeOut(source_default.bold(`
|
|
73083
73522
|
Done. Scaffolded ${scaffolded}/${allAgentNames.length} agents.
|
|
73084
73523
|
`));
|
|
@@ -73121,7 +73560,7 @@ function copyExampleConfig2(name) {
|
|
|
73121
73560
|
throw new Error(`Invalid example name: ${name} (must match /^[a-z0-9_-]+$/)`);
|
|
73122
73561
|
}
|
|
73123
73562
|
const dest = resolve41(process.cwd(), "switchroom.yaml");
|
|
73124
|
-
if (
|
|
73563
|
+
if (existsSync64(dest)) {
|
|
73125
73564
|
console.error(source_default.yellow("switchroom.yaml already exists \u2014 skipping example copy"));
|
|
73126
73565
|
return;
|
|
73127
73566
|
}
|
|
@@ -73132,7 +73571,7 @@ function copyExampleConfig2(name) {
|
|
|
73132
73571
|
return;
|
|
73133
73572
|
}
|
|
73134
73573
|
const exampleFile = resolve41(import.meta.dirname, `../../examples/${name}.yaml`);
|
|
73135
|
-
if (!
|
|
73574
|
+
if (!existsSync64(exampleFile)) {
|
|
73136
73575
|
throw new Error(`Example config not found: ${name}.yaml (available: ${Object.keys(EMBEDDED_EXAMPLES).join(", ")})`);
|
|
73137
73576
|
}
|
|
73138
73577
|
copyFileSync11(exampleFile, dest);
|
|
@@ -73143,11 +73582,11 @@ function findUnwritableAgentDirs(config, opts) {
|
|
|
73143
73582
|
const targets = opts.only ? [opts.only] : Object.keys(config.agents ?? {});
|
|
73144
73583
|
const unwritable = [];
|
|
73145
73584
|
for (const name of targets) {
|
|
73146
|
-
const startSh =
|
|
73147
|
-
if (!
|
|
73585
|
+
const startSh = join56(agentsDir, name, "start.sh");
|
|
73586
|
+
if (!existsSync64(startSh))
|
|
73148
73587
|
continue;
|
|
73149
73588
|
try {
|
|
73150
|
-
|
|
73589
|
+
accessSync3(startSh, fsConstants6.W_OK);
|
|
73151
73590
|
} catch {
|
|
73152
73591
|
unwritable.push(name);
|
|
73153
73592
|
}
|
|
@@ -73322,8 +73761,8 @@ function runRedactStdin() {
|
|
|
73322
73761
|
}
|
|
73323
73762
|
|
|
73324
73763
|
// src/cli/status-ask.ts
|
|
73325
|
-
import { readFileSync as readFileSync54, existsSync as
|
|
73326
|
-
import { join as
|
|
73764
|
+
import { readFileSync as readFileSync54, existsSync as existsSync65, readdirSync as readdirSync24 } from "node:fs";
|
|
73765
|
+
import { join as join57 } from "node:path";
|
|
73327
73766
|
import { homedir as homedir30 } from "node:os";
|
|
73328
73767
|
|
|
73329
73768
|
// src/status-ask/report.ts
|
|
@@ -73645,7 +74084,7 @@ function runReport(opts) {
|
|
|
73645
74084
|
function resolveSources(explicitPath) {
|
|
73646
74085
|
if (explicitPath != null && explicitPath.trim() !== "") {
|
|
73647
74086
|
const trimmed = explicitPath.trim();
|
|
73648
|
-
if (!
|
|
74087
|
+
if (!existsSync65(trimmed)) {
|
|
73649
74088
|
process.stderr.write(`status-ask report: ${trimmed}: file not found
|
|
73650
74089
|
`);
|
|
73651
74090
|
process.exit(1);
|
|
@@ -73659,20 +74098,20 @@ function resolveSources(explicitPath) {
|
|
|
73659
74098
|
const config = loadConfig();
|
|
73660
74099
|
agentsDir = resolveAgentsDir(config);
|
|
73661
74100
|
} catch {
|
|
73662
|
-
agentsDir =
|
|
74101
|
+
agentsDir = join57(homedir30(), ".switchroom", "agents");
|
|
73663
74102
|
}
|
|
73664
|
-
if (!
|
|
74103
|
+
if (!existsSync65(agentsDir))
|
|
73665
74104
|
return [];
|
|
73666
74105
|
const sources = [];
|
|
73667
74106
|
let entries;
|
|
73668
74107
|
try {
|
|
73669
|
-
entries =
|
|
74108
|
+
entries = readdirSync24(agentsDir);
|
|
73670
74109
|
} catch {
|
|
73671
74110
|
return [];
|
|
73672
74111
|
}
|
|
73673
74112
|
for (const name of entries) {
|
|
73674
|
-
const path8 =
|
|
73675
|
-
if (
|
|
74113
|
+
const path8 = join57(agentsDir, name, "runtime-metrics.jsonl");
|
|
74114
|
+
if (existsSync65(path8)) {
|
|
73676
74115
|
sources.push({ path: path8, agent: name });
|
|
73677
74116
|
}
|
|
73678
74117
|
}
|
|
@@ -73693,17 +74132,17 @@ function inferAgentFromPath(p) {
|
|
|
73693
74132
|
|
|
73694
74133
|
// src/cli/agent-config.ts
|
|
73695
74134
|
init_helpers();
|
|
73696
|
-
import { join as
|
|
74135
|
+
import { join as join58 } from "node:path";
|
|
73697
74136
|
import { homedir as homedir31 } from "node:os";
|
|
73698
74137
|
import {
|
|
73699
|
-
existsSync as
|
|
74138
|
+
existsSync as existsSync66,
|
|
73700
74139
|
mkdirSync as mkdirSync36,
|
|
73701
74140
|
appendFileSync as appendFileSync3,
|
|
73702
74141
|
readFileSync as readFileSync55
|
|
73703
74142
|
} from "node:fs";
|
|
73704
|
-
var AUDIT_ROOT =
|
|
74143
|
+
var AUDIT_ROOT = join58(homedir31(), ".switchroom", "audit");
|
|
73705
74144
|
function auditPathFor(agent) {
|
|
73706
|
-
return
|
|
74145
|
+
return join58(AUDIT_ROOT, agent, "agent-config.jsonl");
|
|
73707
74146
|
}
|
|
73708
74147
|
function appendAudit(agent, cmd, args, exit, opts = {}) {
|
|
73709
74148
|
const row = {
|
|
@@ -73717,7 +74156,7 @@ function appendAudit(agent, cmd, args, exit, opts = {}) {
|
|
|
73717
74156
|
const path8 = opts.auditPath ?? auditPathFor(agent);
|
|
73718
74157
|
const dir = path8.slice(0, path8.lastIndexOf("/"));
|
|
73719
74158
|
try {
|
|
73720
|
-
if (!
|
|
74159
|
+
if (!existsSync66(dir)) {
|
|
73721
74160
|
mkdirSync36(dir, { recursive: true });
|
|
73722
74161
|
}
|
|
73723
74162
|
appendFileSync3(path8, JSON.stringify(row) + `
|
|
@@ -73729,7 +74168,7 @@ function isContainerContext(env2 = process.env, opts = {}) {
|
|
|
73729
74168
|
return true;
|
|
73730
74169
|
const probe2 = opts.dockerEnvPath ?? "/.dockerenv";
|
|
73731
74170
|
try {
|
|
73732
|
-
if (
|
|
74171
|
+
if (existsSync66(probe2))
|
|
73733
74172
|
return true;
|
|
73734
74173
|
} catch {}
|
|
73735
74174
|
return false;
|
|
@@ -73790,7 +74229,7 @@ function getAgentSlice(config, agent) {
|
|
|
73790
74229
|
}
|
|
73791
74230
|
function readAuditTail(agent, limit, opts = {}) {
|
|
73792
74231
|
const path8 = opts.auditPath ?? auditPathFor(agent);
|
|
73793
|
-
if (!
|
|
74232
|
+
if (!existsSync66(path8))
|
|
73794
74233
|
return [];
|
|
73795
74234
|
let raw;
|
|
73796
74235
|
try {
|
|
@@ -73950,32 +74389,32 @@ var import_yaml14 = __toESM(require_dist(), 1);
|
|
|
73950
74389
|
init_paths();
|
|
73951
74390
|
import {
|
|
73952
74391
|
closeSync as closeSync12,
|
|
73953
|
-
existsSync as
|
|
74392
|
+
existsSync as existsSync67,
|
|
73954
74393
|
fsyncSync as fsyncSync5,
|
|
73955
74394
|
mkdirSync as mkdirSync37,
|
|
73956
74395
|
openSync as openSync12,
|
|
73957
|
-
readdirSync as
|
|
74396
|
+
readdirSync as readdirSync25,
|
|
73958
74397
|
readFileSync as readFileSync56,
|
|
73959
74398
|
renameSync as renameSync13,
|
|
73960
|
-
statSync as
|
|
74399
|
+
statSync as statSync26,
|
|
73961
74400
|
unlinkSync as unlinkSync13,
|
|
73962
74401
|
writeSync as writeSync7
|
|
73963
74402
|
} from "node:fs";
|
|
73964
|
-
import { join as
|
|
74403
|
+
import { join as join59, resolve as resolve42 } from "node:path";
|
|
73965
74404
|
var STAGING_SUBDIR = ".staging";
|
|
73966
74405
|
function overlayPathsFor(agent, opts = {}) {
|
|
73967
74406
|
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 =
|
|
74407
|
+
const scheduleDir = join59(base, "schedule.d");
|
|
74408
|
+
const scheduleStagingDir = join59(scheduleDir, STAGING_SUBDIR);
|
|
74409
|
+
const skillsDir = join59(base, "skills.d");
|
|
74410
|
+
const skillsStagingDir = join59(skillsDir, STAGING_SUBDIR);
|
|
73972
74411
|
return {
|
|
73973
74412
|
agentRoot: base,
|
|
73974
74413
|
scheduleDir,
|
|
73975
74414
|
scheduleStagingDir,
|
|
73976
74415
|
skillsDir,
|
|
73977
74416
|
skillsStagingDir,
|
|
73978
|
-
lockPath:
|
|
74417
|
+
lockPath: join59(base, ".lock"),
|
|
73979
74418
|
stagingDir: scheduleStagingDir
|
|
73980
74419
|
};
|
|
73981
74420
|
}
|
|
@@ -74001,7 +74440,7 @@ function withAgentLock(paths, fn) {
|
|
|
74001
74440
|
if (e.code !== "EEXIST")
|
|
74002
74441
|
throw err;
|
|
74003
74442
|
try {
|
|
74004
|
-
const age = Date.now() -
|
|
74443
|
+
const age = Date.now() - statSync26(paths.lockPath).mtimeMs;
|
|
74005
74444
|
if (age > 30000) {
|
|
74006
74445
|
unlinkSync13(paths.lockPath);
|
|
74007
74446
|
continue;
|
|
@@ -74029,8 +74468,8 @@ function writeOverlayEntry(agent, slug, yamlText, opts = {}) {
|
|
|
74029
74468
|
const paths = overlayPathsFor(agent, opts);
|
|
74030
74469
|
return withAgentLock(paths, () => {
|
|
74031
74470
|
ensureDirs(paths);
|
|
74032
|
-
const stagingPath =
|
|
74033
|
-
const finalPath =
|
|
74471
|
+
const stagingPath = join59(paths.scheduleStagingDir, `${slug}.yaml`);
|
|
74472
|
+
const finalPath = join59(paths.scheduleDir, `${slug}.yaml`);
|
|
74034
74473
|
const fd = openSync12(stagingPath, "w", 384);
|
|
74035
74474
|
try {
|
|
74036
74475
|
writeSync7(fd, yamlText);
|
|
@@ -74046,8 +74485,8 @@ function writeSkillsOverlayEntry(agent, slug, yamlText, opts = {}) {
|
|
|
74046
74485
|
const paths = overlayPathsFor(agent, opts);
|
|
74047
74486
|
return withAgentLock(paths, () => {
|
|
74048
74487
|
ensureSkillsDirs(paths);
|
|
74049
|
-
const stagingPath =
|
|
74050
|
-
const finalPath =
|
|
74488
|
+
const stagingPath = join59(paths.skillsStagingDir, `${slug}.yaml`);
|
|
74489
|
+
const finalPath = join59(paths.skillsDir, `${slug}.yaml`);
|
|
74051
74490
|
const fd = openSync12(stagingPath, "w", 384);
|
|
74052
74491
|
try {
|
|
74053
74492
|
writeSync7(fd, yamlText);
|
|
@@ -74062,8 +74501,8 @@ function writeSkillsOverlayEntry(agent, slug, yamlText, opts = {}) {
|
|
|
74062
74501
|
function deleteSkillsOverlayEntry(agent, slug, opts = {}) {
|
|
74063
74502
|
const paths = overlayPathsFor(agent, opts);
|
|
74064
74503
|
return withAgentLock(paths, () => {
|
|
74065
|
-
const finalPath =
|
|
74066
|
-
if (!
|
|
74504
|
+
const finalPath = join59(paths.skillsDir, `${slug}.yaml`);
|
|
74505
|
+
if (!existsSync67(finalPath))
|
|
74067
74506
|
return false;
|
|
74068
74507
|
unlinkSync13(finalPath);
|
|
74069
74508
|
return true;
|
|
@@ -74071,13 +74510,13 @@ function deleteSkillsOverlayEntry(agent, slug, opts = {}) {
|
|
|
74071
74510
|
}
|
|
74072
74511
|
function listSkillsOverlayEntries(agent, opts = {}) {
|
|
74073
74512
|
const paths = overlayPathsFor(agent, opts);
|
|
74074
|
-
if (!
|
|
74513
|
+
if (!existsSync67(paths.skillsDir))
|
|
74075
74514
|
return [];
|
|
74076
74515
|
const out = [];
|
|
74077
|
-
for (const name of
|
|
74516
|
+
for (const name of readdirSync25(paths.skillsDir)) {
|
|
74078
74517
|
if (!/\.ya?ml$/i.test(name))
|
|
74079
74518
|
continue;
|
|
74080
|
-
const full =
|
|
74519
|
+
const full = join59(paths.skillsDir, name);
|
|
74081
74520
|
try {
|
|
74082
74521
|
const raw = readFileSync56(full, "utf-8");
|
|
74083
74522
|
const slug = name.replace(/\.ya?ml$/i, "");
|
|
@@ -74089,8 +74528,8 @@ function listSkillsOverlayEntries(agent, opts = {}) {
|
|
|
74089
74528
|
function deleteOverlayEntry(agent, slug, opts = {}) {
|
|
74090
74529
|
const paths = overlayPathsFor(agent, opts);
|
|
74091
74530
|
return withAgentLock(paths, () => {
|
|
74092
|
-
const finalPath =
|
|
74093
|
-
if (!
|
|
74531
|
+
const finalPath = join59(paths.scheduleDir, `${slug}.yaml`);
|
|
74532
|
+
if (!existsSync67(finalPath))
|
|
74094
74533
|
return false;
|
|
74095
74534
|
unlinkSync13(finalPath);
|
|
74096
74535
|
return true;
|
|
@@ -74098,13 +74537,13 @@ function deleteOverlayEntry(agent, slug, opts = {}) {
|
|
|
74098
74537
|
}
|
|
74099
74538
|
function listOverlayEntries(agent, opts = {}) {
|
|
74100
74539
|
const paths = overlayPathsFor(agent, opts);
|
|
74101
|
-
if (!
|
|
74540
|
+
if (!existsSync67(paths.scheduleDir))
|
|
74102
74541
|
return [];
|
|
74103
74542
|
const out = [];
|
|
74104
|
-
for (const name of
|
|
74543
|
+
for (const name of readdirSync25(paths.scheduleDir)) {
|
|
74105
74544
|
if (!/\.ya?ml$/i.test(name))
|
|
74106
74545
|
continue;
|
|
74107
|
-
const full =
|
|
74546
|
+
const full = join59(paths.scheduleDir, name);
|
|
74108
74547
|
try {
|
|
74109
74548
|
const raw = readFileSync56(full, "utf-8");
|
|
74110
74549
|
const slug = name.replace(/\.ya?ml$/i, "");
|
|
@@ -74249,23 +74688,23 @@ function reconcileAgentCronOnly(agent) {
|
|
|
74249
74688
|
// src/cli/agent-config-pending.ts
|
|
74250
74689
|
import {
|
|
74251
74690
|
closeSync as closeSync13,
|
|
74252
|
-
existsSync as
|
|
74691
|
+
existsSync as existsSync68,
|
|
74253
74692
|
fsyncSync as fsyncSync6,
|
|
74254
74693
|
mkdirSync as mkdirSync38,
|
|
74255
74694
|
openSync as openSync13,
|
|
74256
|
-
readdirSync as
|
|
74695
|
+
readdirSync as readdirSync26,
|
|
74257
74696
|
readFileSync as readFileSync57,
|
|
74258
74697
|
renameSync as renameSync14,
|
|
74259
74698
|
unlinkSync as unlinkSync14,
|
|
74260
74699
|
writeFileSync as writeFileSync33,
|
|
74261
74700
|
writeSync as writeSync8
|
|
74262
74701
|
} from "node:fs";
|
|
74263
|
-
import { join as
|
|
74702
|
+
import { join as join60 } from "node:path";
|
|
74264
74703
|
import { randomBytes as randomBytes12 } from "node:crypto";
|
|
74265
74704
|
var STAGE_ID_PREFIX = "cap_";
|
|
74266
74705
|
function pendingDir(agent, opts = {}) {
|
|
74267
74706
|
const paths = overlayPathsFor(agent, opts);
|
|
74268
|
-
return
|
|
74707
|
+
return join60(paths.scheduleDir, ".pending");
|
|
74269
74708
|
}
|
|
74270
74709
|
function ensurePendingDir(agent, opts = {}) {
|
|
74271
74710
|
const dir = pendingDir(agent, opts);
|
|
@@ -74278,8 +74717,8 @@ function newStageId() {
|
|
|
74278
74717
|
function stagePendingScheduleEntry(opts) {
|
|
74279
74718
|
const dir = ensurePendingDir(opts.agent, { root: opts.root });
|
|
74280
74719
|
const stageId = opts.stageId ?? newStageId();
|
|
74281
|
-
const yamlPath =
|
|
74282
|
-
const metaPath =
|
|
74720
|
+
const yamlPath = join60(dir, `${stageId}.yaml`);
|
|
74721
|
+
const metaPath = join60(dir, `${stageId}.meta.json`);
|
|
74283
74722
|
const meta = {
|
|
74284
74723
|
v: 1,
|
|
74285
74724
|
stage_id: stageId,
|
|
@@ -74306,16 +74745,16 @@ function stagePendingScheduleEntry(opts) {
|
|
|
74306
74745
|
}
|
|
74307
74746
|
function listPendingScheduleEntries(agent, opts = {}) {
|
|
74308
74747
|
const dir = pendingDir(agent, opts);
|
|
74309
|
-
if (!
|
|
74748
|
+
if (!existsSync68(dir))
|
|
74310
74749
|
return [];
|
|
74311
74750
|
const out = [];
|
|
74312
|
-
for (const name of
|
|
74751
|
+
for (const name of readdirSync26(dir).sort()) {
|
|
74313
74752
|
if (!name.endsWith(".meta.json"))
|
|
74314
74753
|
continue;
|
|
74315
74754
|
const stageId = name.slice(0, -".meta.json".length);
|
|
74316
|
-
const metaPath =
|
|
74317
|
-
const yamlPath =
|
|
74318
|
-
if (!
|
|
74755
|
+
const metaPath = join60(dir, name);
|
|
74756
|
+
const yamlPath = join60(dir, `${stageId}.yaml`);
|
|
74757
|
+
if (!existsSync68(yamlPath))
|
|
74319
74758
|
continue;
|
|
74320
74759
|
try {
|
|
74321
74760
|
const meta = JSON.parse(readFileSync57(metaPath, "utf-8"));
|
|
@@ -74333,8 +74772,8 @@ function commitPendingScheduleEntry(opts) {
|
|
|
74333
74772
|
return { committed: false, reason: "not_found" };
|
|
74334
74773
|
const slug = match.meta.entry.name ?? match.stageId;
|
|
74335
74774
|
const paths = overlayPathsFor(opts.agent, { root: opts.root });
|
|
74336
|
-
const finalPath =
|
|
74337
|
-
if (
|
|
74775
|
+
const finalPath = join60(paths.scheduleDir, `${slug}.yaml`);
|
|
74776
|
+
if (existsSync68(finalPath)) {
|
|
74338
74777
|
return { committed: false, reason: "slug_collision" };
|
|
74339
74778
|
}
|
|
74340
74779
|
renameSync14(match.yamlPath, finalPath);
|
|
@@ -74356,7 +74795,7 @@ function denyPendingScheduleEntry(opts) {
|
|
|
74356
74795
|
}
|
|
74357
74796
|
|
|
74358
74797
|
// src/cli/agent-config-write.ts
|
|
74359
|
-
import { existsSync as
|
|
74798
|
+
import { existsSync as existsSync69, readFileSync as readFileSync58 } from "node:fs";
|
|
74360
74799
|
var MAX_ENTRIES_PER_AGENT = 20;
|
|
74361
74800
|
function checkOperatorContext(verb, env2 = process.env) {
|
|
74362
74801
|
if (env2.SWITCHROOM_OPERATOR === "1")
|
|
@@ -74590,7 +75029,7 @@ function scheduleRemove(opts) {
|
|
|
74590
75029
|
}
|
|
74591
75030
|
let priorContent = null;
|
|
74592
75031
|
try {
|
|
74593
|
-
if (
|
|
75032
|
+
if (existsSync69(match.path))
|
|
74594
75033
|
priorContent = readFileSync58(match.path, "utf-8");
|
|
74595
75034
|
} catch {}
|
|
74596
75035
|
deleteOverlayEntry(agent, match.slug, { root: opts.root });
|
|
@@ -74783,10 +75222,10 @@ function registerAgentConfigWriteCommands(program3) {
|
|
|
74783
75222
|
|
|
74784
75223
|
// src/cli/agent-config-skill-write.ts
|
|
74785
75224
|
var import_yaml15 = __toESM(require_dist(), 1);
|
|
74786
|
-
import { existsSync as
|
|
75225
|
+
import { existsSync as existsSync70 } from "node:fs";
|
|
74787
75226
|
init_reconcile_default_skills();
|
|
74788
75227
|
var import_yaml16 = __toESM(require_dist(), 1);
|
|
74789
|
-
import { join as
|
|
75228
|
+
import { join as join61 } from "node:path";
|
|
74790
75229
|
var MAX_SKILLS_PER_AGENT = 20;
|
|
74791
75230
|
var V1_ALLOWED_SOURCE_PREFIX = "bundled:";
|
|
74792
75231
|
function exitCodeFor2(code) {
|
|
@@ -74861,8 +75300,8 @@ function skillInstall(opts) {
|
|
|
74861
75300
|
return err("E_SKILL_QUOTA_EXCEEDED", `agent ${agent} already has ${used} overlay-installed skills (cap ${MAX_SKILLS_PER_AGENT})`);
|
|
74862
75301
|
}
|
|
74863
75302
|
const poolDir = opts.bundledSkillsPoolDir ?? getBundledSkillsPoolDir();
|
|
74864
|
-
const skillPath =
|
|
74865
|
-
if (!
|
|
75303
|
+
const skillPath = join61(poolDir, skillName);
|
|
75304
|
+
if (!existsSync70(skillPath)) {
|
|
74866
75305
|
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
75306
|
}
|
|
74868
75307
|
const yamlText = import_yaml15.stringify({ skills: [skillName] });
|
|
@@ -75046,27 +75485,27 @@ init_source();
|
|
|
75046
75485
|
init_helpers();
|
|
75047
75486
|
init_loader();
|
|
75048
75487
|
import {
|
|
75049
|
-
existsSync as
|
|
75050
|
-
readdirSync as
|
|
75488
|
+
existsSync as existsSync72,
|
|
75489
|
+
readdirSync as readdirSync27,
|
|
75051
75490
|
readFileSync as readFileSync60,
|
|
75052
75491
|
renameSync as renameSync15,
|
|
75053
|
-
statSync as
|
|
75492
|
+
statSync as statSync27,
|
|
75054
75493
|
unlinkSync as unlinkSync15
|
|
75055
75494
|
} from "node:fs";
|
|
75056
75495
|
import { createHash as createHash12 } from "node:crypto";
|
|
75057
|
-
import { join as
|
|
75496
|
+
import { join as join62 } from "node:path";
|
|
75058
75497
|
function planCronUnitRenames(agentsDir, agents) {
|
|
75059
75498
|
const plans = [];
|
|
75060
75499
|
for (const [agentName, agentConfig] of Object.entries(agents)) {
|
|
75061
75500
|
const schedule = agentConfig.schedule ?? [];
|
|
75062
75501
|
if (schedule.length === 0)
|
|
75063
75502
|
continue;
|
|
75064
|
-
const telegramDir =
|
|
75065
|
-
if (!
|
|
75503
|
+
const telegramDir = join62(agentsDir, agentName, "telegram");
|
|
75504
|
+
if (!existsSync72(telegramDir))
|
|
75066
75505
|
continue;
|
|
75067
75506
|
let entries;
|
|
75068
75507
|
try {
|
|
75069
|
-
entries =
|
|
75508
|
+
entries = readdirSync27(telegramDir);
|
|
75070
75509
|
} catch {
|
|
75071
75510
|
continue;
|
|
75072
75511
|
}
|
|
@@ -75083,8 +75522,8 @@ function planCronUnitRenames(agentsDir, agents) {
|
|
|
75083
75522
|
continue;
|
|
75084
75523
|
plans.push({
|
|
75085
75524
|
agent: agentName,
|
|
75086
|
-
from:
|
|
75087
|
-
to:
|
|
75525
|
+
from: join62(telegramDir, file),
|
|
75526
|
+
to: join62(telegramDir, canonical),
|
|
75088
75527
|
scheduleIdx: idx,
|
|
75089
75528
|
entry
|
|
75090
75529
|
});
|
|
@@ -75096,7 +75535,7 @@ function sha256File2(path8) {
|
|
|
75096
75535
|
return createHash12("sha256").update(readFileSync60(path8)).digest("hex");
|
|
75097
75536
|
}
|
|
75098
75537
|
function renamePair(from, to, opts = {}) {
|
|
75099
|
-
if (
|
|
75538
|
+
if (existsSync72(to)) {
|
|
75100
75539
|
let identical = false;
|
|
75101
75540
|
try {
|
|
75102
75541
|
identical = sha256File2(from) === sha256File2(to);
|
|
@@ -75189,7 +75628,7 @@ function registerMigrateCommand(program3) {
|
|
|
75189
75628
|
const fromSidecar = p.from.replace(/\.sh$/, ".source");
|
|
75190
75629
|
const toSidecar = p.to.replace(/\.sh$/, ".source");
|
|
75191
75630
|
let sidecarStatus = null;
|
|
75192
|
-
if (
|
|
75631
|
+
if (existsSync72(fromSidecar) && statSync27(fromSidecar).isFile()) {
|
|
75193
75632
|
sidecarStatus = renamePair(fromSidecar, toSidecar);
|
|
75194
75633
|
}
|
|
75195
75634
|
switch (status.kind) {
|
|
@@ -75222,10 +75661,10 @@ function registerMigrateCommand(program3) {
|
|
|
75222
75661
|
init_source();
|
|
75223
75662
|
init_helpers();
|
|
75224
75663
|
init_audit_reader();
|
|
75225
|
-
import { existsSync as
|
|
75664
|
+
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
75665
|
import { homedir as homedir32 } from "node:os";
|
|
75227
|
-
import { join as
|
|
75228
|
-
import { spawnSync as
|
|
75666
|
+
import { join as join63 } from "node:path";
|
|
75667
|
+
import { spawnSync as spawnSync11 } from "node:child_process";
|
|
75229
75668
|
var DEFAULT_IMAGE_TAG = "latest";
|
|
75230
75669
|
var HOSTD_COMPOSE_PROJECT = "switchroom-hostd";
|
|
75231
75670
|
function renderHostdComposeFile(opts) {
|
|
@@ -75308,14 +75747,14 @@ networks:
|
|
|
75308
75747
|
`;
|
|
75309
75748
|
}
|
|
75310
75749
|
function hostdDir() {
|
|
75311
|
-
return
|
|
75750
|
+
return join63(homedir32(), ".switchroom", "hostd");
|
|
75312
75751
|
}
|
|
75313
75752
|
function hostdComposePath() {
|
|
75314
|
-
return
|
|
75753
|
+
return join63(hostdDir(), "docker-compose.yml");
|
|
75315
75754
|
}
|
|
75316
75755
|
function backupExistingCompose() {
|
|
75317
75756
|
const p = hostdComposePath();
|
|
75318
|
-
if (!
|
|
75757
|
+
if (!existsSync73(p))
|
|
75319
75758
|
return null;
|
|
75320
75759
|
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
75321
75760
|
const bak = `${p}.bak-${ts}`;
|
|
@@ -75323,7 +75762,7 @@ function backupExistingCompose() {
|
|
|
75323
75762
|
return bak;
|
|
75324
75763
|
}
|
|
75325
75764
|
function runDocker(args) {
|
|
75326
|
-
const r =
|
|
75765
|
+
const r = spawnSync11("docker", args, { encoding: "utf8" });
|
|
75327
75766
|
return {
|
|
75328
75767
|
ok: r.status === 0,
|
|
75329
75768
|
stdout: r.stdout ?? "",
|
|
@@ -75391,7 +75830,7 @@ function doStatus() {
|
|
|
75391
75830
|
const composeYml = hostdComposePath();
|
|
75392
75831
|
console.log(source_default.bold("switchroom-hostd"));
|
|
75393
75832
|
console.log("");
|
|
75394
|
-
if (!
|
|
75833
|
+
if (!existsSync73(composeYml)) {
|
|
75395
75834
|
console.log(source_default.yellow(" compose: not installed"));
|
|
75396
75835
|
console.log(source_default.dim(" run `switchroom hostd install` to set up."));
|
|
75397
75836
|
return;
|
|
@@ -75412,15 +75851,15 @@ function doStatus() {
|
|
|
75412
75851
|
} else {
|
|
75413
75852
|
console.log(source_default.green(` container: ${ps.stdout.trim()}`));
|
|
75414
75853
|
}
|
|
75415
|
-
if (
|
|
75854
|
+
if (existsSync73(dir)) {
|
|
75416
75855
|
const entries = [];
|
|
75417
75856
|
try {
|
|
75418
|
-
for (const name of
|
|
75857
|
+
for (const name of readdirSync28(dir)) {
|
|
75419
75858
|
if (name === "docker-compose.yml" || name.startsWith("docker-compose.yml."))
|
|
75420
75859
|
continue;
|
|
75421
|
-
const sockPath =
|
|
75422
|
-
if (
|
|
75423
|
-
const st =
|
|
75860
|
+
const sockPath = join63(dir, name, "sock");
|
|
75861
|
+
if (existsSync73(sockPath)) {
|
|
75862
|
+
const st = statSync28(sockPath);
|
|
75424
75863
|
if ((st.mode & 61440) === 49152) {
|
|
75425
75864
|
entries.push(`${name} \u2192 ${sockPath}`);
|
|
75426
75865
|
}
|
|
@@ -75438,7 +75877,7 @@ function doStatus() {
|
|
|
75438
75877
|
}
|
|
75439
75878
|
function doUninstall() {
|
|
75440
75879
|
const composeYml = hostdComposePath();
|
|
75441
|
-
if (!
|
|
75880
|
+
if (!existsSync73(composeYml)) {
|
|
75442
75881
|
console.log(source_default.yellow(" No hostd install detected (no compose file at this path)."));
|
|
75443
75882
|
return;
|
|
75444
75883
|
}
|
|
@@ -75462,7 +75901,7 @@ function registerHostdCommand(program3) {
|
|
|
75462
75901
|
hostd.command("uninstall").description("Stop the hostd container. Leaves the compose file in place for re-install.").action(() => doUninstall());
|
|
75463
75902
|
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
75903
|
const logPath = opts.path ?? defaultAuditLogPath2();
|
|
75465
|
-
if (!
|
|
75904
|
+
if (!existsSync73(logPath)) {
|
|
75466
75905
|
console.error(source_default.yellow(`Audit log not found at ${logPath}.`) + source_default.gray(`
|
|
75467
75906
|
The log is created when hostd handles its first privileged-verb request.`));
|
|
75468
75907
|
return;
|