switchroom 0.18.13 → 0.18.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-scheduler/index.js +49 -9
- package/dist/auth-broker/index.js +152 -46
- package/dist/cli/autoaccept-poll.js +23 -0
- package/dist/cli/drive-write-pretool.mjs +24 -1
- package/dist/cli/foreground-hog-pretool.mjs +264 -0
- package/dist/cli/notion-write-pretool.mjs +0 -1
- package/dist/cli/switchroom.js +1185 -1072
- package/dist/host-control/main.js +53 -52
- package/dist/vault/approvals/kernel-server.js +16 -13
- package/dist/vault/broker/server.js +672 -669
- package/package.json +1 -1
- package/profiles/coding/CLAUDE.md.hbs +2 -0
- package/profiles/default/CLAUDE.md.hbs +2 -0
- package/skills/switchroom-architecture/telegram.md +0 -1
- package/telegram-plugin/auth-snapshot-format.ts +37 -5
- package/telegram-plugin/auto-fallback-fleet.ts +29 -1
- package/telegram-plugin/bridge/bridge.ts +2 -0
- package/telegram-plugin/dist/bridge/bridge.js +23 -0
- package/telegram-plugin/dist/gateway/gateway.js +765 -67
- package/telegram-plugin/dist/server.js +24 -1
- package/telegram-plugin/gateway/auth-broker-client.ts +1 -0
- package/telegram-plugin/gateway/auth-command.ts +14 -0
- package/telegram-plugin/gateway/forward-origin.ts +235 -0
- package/telegram-plugin/gateway/gateway.ts +270 -10
- package/telegram-plugin/gateway/throttle-tier-wiring.ts +268 -0
- package/telegram-plugin/history.ts +55 -6
- package/telegram-plugin/model-unavailable.ts +234 -2
- package/telegram-plugin/render/rich-render.ts +40 -32
- package/telegram-plugin/runtime-metrics.ts +31 -0
- package/telegram-plugin/session-tail.ts +14 -2
- package/telegram-plugin/stream-controller.ts +3 -2
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +72 -0
- package/telegram-plugin/tests/forward-origin.test.ts +309 -0
- package/telegram-plugin/tests/history.test.ts +157 -0
- package/telegram-plugin/tests/model-unavailable.test.ts +187 -0
- package/telegram-plugin/tests/operator-events-session-tail.test.ts +55 -0
- package/telegram-plugin/tests/render/render-outbound-chunks.test.ts +6 -4
- package/telegram-plugin/tests/render/rich-render.test.ts +41 -22
- package/telegram-plugin/tests/runtime-metrics.test.ts +24 -0
- package/telegram-plugin/tests/single-mode-stream-reply.test.ts +5 -3
- package/telegram-plugin/tests/status-accent.test.ts +5 -3
- package/telegram-plugin/tests/stream-controller-chunk-cap.test.ts +20 -20
- package/telegram-plugin/tests/stream-reply-handler.test.ts +5 -2
- package/telegram-plugin/tests/throttle-tier-wiring.test.ts +290 -0
- package/telegram-plugin/tests/throttle-tier.test.ts +454 -0
- package/telegram-plugin/throttle-tier.ts +323 -0
- package/telegram-plugin/uat/scenarios/jtbd-rich-formatting-render-dm.test.ts +8 -7
|
@@ -4244,7 +4244,6 @@ var init_schema = __esm(() => {
|
|
|
4244
4244
|
enabled: exports_external.boolean().default(true).describe("Master switch for the per-agent Telegram gateway sidecar. " + "When false, start.sh skips the gateway supervise loop and the " + "agent boots without bot-token requirements (smoke-test + " + "offline-dev use case)."),
|
|
4245
4245
|
plugin: exports_external.enum(["switchroom", "official"]).optional().describe("Which Telegram MCP plugin to load. Default is 'switchroom' — the " + "enhanced fork with streaming edits, reactions, history, and " + "access control. Set to 'official' for the upstream marketplace " + "plugin (basic send/receive only)."),
|
|
4246
4246
|
format: exports_external.enum(["html", "markdownv2", "text"]).optional().describe("Default reply format passed to the plugin"),
|
|
4247
|
-
rate_limit_ms: exports_external.number().optional().describe("Minimum delay between outgoing messages in ms"),
|
|
4248
4247
|
stream_mode: exports_external.enum(["pty", "checklist"]).optional().describe("How live progress is streamed to Telegram during a turn. " + "'pty' (default) surfaces text snapshots of Claude Code's TUI — " + "compatible but can flicker as Ink re-renders. 'checklist' drives " + "a structured progress card from session-tail events — stable " + "order, per-tool status emojis, fires only on semantic transitions."),
|
|
4249
4248
|
stream_throttle_ms: exports_external.number().int().nonnegative().optional().describe("Throttle window in ms between successive in-place stream edits " + "during a turn. Lower = more responsive stream, higher = fewer API " + "calls. Floored at 250 by draft-stream itself. Default 400 ms for DMs " + "and 1000 ms for groups/forums (respects Telegram's ~1 edit/sec/message " + "practical ceiling). Override per-agent if a particular agent needs " + "snappier or quieter streaming."),
|
|
4250
4249
|
clear_status_on_completion: exports_external.boolean().optional().describe("When true, the live activity/status feed (the in-place 'what it's " + "doing' message — Reading X, Searching the web for Y, …) is DELETED " + "when the turn's final answer lands, so only the reply remains. " + "Default false: the status message is left in the chat as a record " + "(its last step marked done) — no post-then-delete. Per-agent " + "override; cascades defaults → profile → agent (per-key)."),
|
|
@@ -12180,7 +12179,7 @@ var init_overlay_schema = __esm(() => {
|
|
|
12180
12179
|
});
|
|
12181
12180
|
|
|
12182
12181
|
// src/config/overlay-loader.ts
|
|
12183
|
-
import { existsSync as existsSync2, readFileSync, readdirSync, statSync } from "node:fs";
|
|
12182
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "node:fs";
|
|
12184
12183
|
import { basename, resolve as resolve2 } from "node:path";
|
|
12185
12184
|
function deriveOverlayTitle(raw, fileName) {
|
|
12186
12185
|
const titleFromComment = raw.match(/^#[^\S\n]*name:[^\S\n]*(\S.*?)[^\S\n]*$/m)?.[1];
|
|
@@ -12244,7 +12243,7 @@ function applyAgentOverlays(config) {
|
|
|
12244
12243
|
const merged = [...agentCfg.schedule ?? []];
|
|
12245
12244
|
for (const file of files) {
|
|
12246
12245
|
try {
|
|
12247
|
-
const raw =
|
|
12246
|
+
const raw = readFileSync2(file, "utf-8");
|
|
12248
12247
|
const parsed = import_yaml.parse(raw);
|
|
12249
12248
|
const doc = OverlayDocSchema.parse(parsed);
|
|
12250
12249
|
const title = deriveOverlayTitle(raw, basename(file));
|
|
@@ -12285,7 +12284,7 @@ function applyAgentOverlays(config) {
|
|
|
12285
12284
|
const seen = new Set(merged);
|
|
12286
12285
|
for (const file of skillFiles) {
|
|
12287
12286
|
try {
|
|
12288
|
-
const raw =
|
|
12287
|
+
const raw = readFileSync2(file, "utf-8");
|
|
12289
12288
|
const parsed = import_yaml.parse(raw);
|
|
12290
12289
|
const doc = OverlayDocSchema.parse(parsed);
|
|
12291
12290
|
for (const skillName of doc.skills ?? []) {
|
|
@@ -12360,7 +12359,7 @@ __export(exports_loader, {
|
|
|
12360
12359
|
findConfigFile: () => findConfigFile,
|
|
12361
12360
|
ConfigError: () => ConfigError
|
|
12362
12361
|
});
|
|
12363
|
-
import { readFileSync as
|
|
12362
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "node:fs";
|
|
12364
12363
|
import { homedir } from "node:os";
|
|
12365
12364
|
import { resolve as resolve3 } from "node:path";
|
|
12366
12365
|
function formatZodErrors(error) {
|
|
@@ -12440,7 +12439,7 @@ function loadConfig(configPath) {
|
|
|
12440
12439
|
}
|
|
12441
12440
|
let raw;
|
|
12442
12441
|
try {
|
|
12443
|
-
raw =
|
|
12442
|
+
raw = readFileSync3(filePath, "utf-8");
|
|
12444
12443
|
} catch (err) {
|
|
12445
12444
|
throw new ConfigError(`Failed to read config file: ${filePath}`, [
|
|
12446
12445
|
` ${err.message}`
|
|
@@ -18097,249 +18096,597 @@ import * as net from "node:net";
|
|
|
18097
18096
|
import { mkdirSync as mkdirSync6, chmodSync as chmodSync4, chownSync, existsSync as existsSync11, readFileSync as readFileSync10, readdirSync as readdirSync4, statSync as statSync6, unlinkSync as unlinkSync6, writeFileSync as writeFileSync3, renameSync as renameSync7 } from "node:fs";
|
|
18098
18097
|
|
|
18099
18098
|
// src/agents/compose.ts
|
|
18100
|
-
import { createHash } from "node:crypto";
|
|
18101
18099
|
init_schema();
|
|
18102
18100
|
|
|
18103
18101
|
// src/agents/scaffold.ts
|
|
18104
18102
|
import { join as join4, resolve as resolve6 } from "node:path";
|
|
18105
18103
|
init_atomic();
|
|
18106
|
-
init_schema();
|
|
18107
|
-
|
|
18108
|
-
// src/config/users.ts
|
|
18109
|
-
init_merge();
|
|
18110
|
-
|
|
18111
|
-
// src/agents/scaffold.ts
|
|
18112
|
-
init_merge();
|
|
18113
|
-
|
|
18114
|
-
// src/config/timezone.ts
|
|
18115
|
-
var CONTAINER_DEFAULT_UTC_ZONES = new Set([
|
|
18116
|
-
"UTC",
|
|
18117
|
-
"Etc/UTC",
|
|
18118
|
-
"Etc/Universal",
|
|
18119
|
-
"Universal"
|
|
18120
|
-
]);
|
|
18121
18104
|
|
|
18122
|
-
// src/
|
|
18123
|
-
import {
|
|
18124
|
-
import { homedir as homedir2 } from "node:os";
|
|
18125
|
-
|
|
18126
|
-
// src/cli/helpers.ts
|
|
18127
|
-
init_loader();
|
|
18105
|
+
// src/agents/agent-uid.ts
|
|
18106
|
+
import { createHash } from "node:crypto";
|
|
18128
18107
|
|
|
18129
|
-
// src/
|
|
18130
|
-
|
|
18131
|
-
|
|
18108
|
+
// src/vault/broker/peercred.ts
|
|
18109
|
+
import { execFileSync } from "node:child_process";
|
|
18110
|
+
import { readFileSync, readlinkSync, fstatSync } from "node:fs";
|
|
18132
18111
|
|
|
18133
|
-
// src/
|
|
18134
|
-
function
|
|
18135
|
-
if (
|
|
18136
|
-
return
|
|
18137
|
-
|
|
18138
|
-
|
|
18139
|
-
|
|
18140
|
-
|
|
18141
|
-
|
|
18142
|
-
|
|
18143
|
-
|
|
18144
|
-
|
|
18145
|
-
|
|
18146
|
-
|
|
18147
|
-
|
|
18112
|
+
// src/vault/broker/peercred-ffi.ts
|
|
18113
|
+
function getPeerCred(fd) {
|
|
18114
|
+
if (process.platform !== "linux")
|
|
18115
|
+
return null;
|
|
18116
|
+
try {
|
|
18117
|
+
const ffi = __require("bun:ffi");
|
|
18118
|
+
const { dlopen, FFIType, ptr } = ffi;
|
|
18119
|
+
const SOL_SOCKET = 1;
|
|
18120
|
+
const SO_PEERCRED = 17;
|
|
18121
|
+
const UCRED_SIZE = 12;
|
|
18122
|
+
const cache = getPeerCred;
|
|
18123
|
+
const lib = cache._lib ?? (() => {
|
|
18124
|
+
const candidates = ["libc.so.6", "libc.so"];
|
|
18125
|
+
const symbolSpec = {
|
|
18126
|
+
getsockopt: {
|
|
18127
|
+
args: [FFIType.i32, FFIType.i32, FFIType.i32, FFIType.ptr, FFIType.ptr],
|
|
18128
|
+
returns: FFIType.i32
|
|
18129
|
+
}
|
|
18130
|
+
};
|
|
18131
|
+
const errors2 = [];
|
|
18132
|
+
for (const name of candidates) {
|
|
18133
|
+
try {
|
|
18134
|
+
const opened = dlopen(name, symbolSpec);
|
|
18135
|
+
cache._lib = opened;
|
|
18136
|
+
return opened;
|
|
18137
|
+
} catch (e) {
|
|
18138
|
+
errors2.push(`${name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
18139
|
+
}
|
|
18140
|
+
}
|
|
18141
|
+
process.stderr.write(`[vault-broker] peercred-ffi: dlopen failed for all libc candidates ` + `(${errors2.join("; ")}); falling back to ss-parsing.
|
|
18142
|
+
`);
|
|
18143
|
+
throw new Error("no libc candidate could be opened");
|
|
18144
|
+
})();
|
|
18145
|
+
const credBuf = new ArrayBuffer(UCRED_SIZE);
|
|
18146
|
+
const lenBuf = new Uint32Array(1);
|
|
18147
|
+
lenBuf[0] = UCRED_SIZE;
|
|
18148
|
+
const rc = lib.symbols.getsockopt(fd, SOL_SOCKET, SO_PEERCRED, ptr(credBuf), ptr(lenBuf.buffer));
|
|
18149
|
+
if (rc !== 0)
|
|
18150
|
+
return null;
|
|
18151
|
+
if (lenBuf[0] !== UCRED_SIZE)
|
|
18152
|
+
return null;
|
|
18153
|
+
const view = new DataView(credBuf);
|
|
18154
|
+
return {
|
|
18155
|
+
pid: view.getInt32(0, true),
|
|
18156
|
+
uid: view.getInt32(4, true),
|
|
18157
|
+
gid: view.getInt32(8, true)
|
|
18158
|
+
};
|
|
18159
|
+
} catch {
|
|
18148
18160
|
return null;
|
|
18149
|
-
const key = value.slice("vault:".length).split("#")[0];
|
|
18150
|
-
return key.length > 0 ? key : null;
|
|
18151
|
-
}
|
|
18152
|
-
function isGoogleClientCredentialKeyForAgent(config, agentName, key) {
|
|
18153
|
-
if (!agentName || !key)
|
|
18154
|
-
return false;
|
|
18155
|
-
const agentConfig = config.agents?.[agentName];
|
|
18156
|
-
if (!agentConfig)
|
|
18157
|
-
return false;
|
|
18158
|
-
if (agentConfig.mcp_servers?.["gdrive"] === false) {
|
|
18159
|
-
return false;
|
|
18160
|
-
}
|
|
18161
|
-
const account = agentConfig.google_workspace?.account;
|
|
18162
|
-
if (!shouldEmitGdriveMcp(agentName, account, config.google_accounts)) {
|
|
18163
|
-
return false;
|
|
18164
18161
|
}
|
|
18165
|
-
|
|
18166
|
-
|
|
18167
|
-
|
|
18168
|
-
|
|
18169
|
-
|
|
18170
|
-
|
|
18162
|
+
}
|
|
18163
|
+
|
|
18164
|
+
// src/broker-common/peercred-path.ts
|
|
18165
|
+
function matchSocketName(socketPath, patterns, maxNameLen) {
|
|
18166
|
+
if (typeof socketPath !== "string" || socketPath.length === 0)
|
|
18167
|
+
return null;
|
|
18168
|
+
let name = null;
|
|
18169
|
+
for (const re of patterns) {
|
|
18170
|
+
const m = socketPath.match(re);
|
|
18171
|
+
if (m && typeof m[1] === "string") {
|
|
18172
|
+
name = m[1];
|
|
18173
|
+
break;
|
|
18174
|
+
}
|
|
18171
18175
|
}
|
|
18172
|
-
|
|
18176
|
+
if (name === null || name.length === 0)
|
|
18177
|
+
return null;
|
|
18178
|
+
if (maxNameLen !== undefined && name.length > maxNameLen)
|
|
18179
|
+
return null;
|
|
18180
|
+
return name;
|
|
18181
|
+
}
|
|
18182
|
+
function parseSocketIdentity(socketPath, spec) {
|
|
18183
|
+
const name = matchSocketName(socketPath, spec.patterns, spec.maxNameLen);
|
|
18184
|
+
if (name === null)
|
|
18185
|
+
return null;
|
|
18186
|
+
const operatorName = spec.operatorName ?? "operator";
|
|
18187
|
+
if (name === operatorName)
|
|
18188
|
+
return { kind: "operator" };
|
|
18189
|
+
if (spec.reservedNames.has(name))
|
|
18190
|
+
return null;
|
|
18191
|
+
return { kind: "agent", name };
|
|
18173
18192
|
}
|
|
18174
18193
|
|
|
18175
|
-
// src/vault/broker/
|
|
18176
|
-
var
|
|
18177
|
-
|
|
18178
|
-
|
|
18179
|
-
|
|
18180
|
-
|
|
18181
|
-
|
|
18182
|
-
|
|
18183
|
-
|
|
18184
|
-
if ((agentConfig?.mcp_servers ?? {})["webkite"] === false)
|
|
18185
|
-
return false;
|
|
18186
|
-
return true;
|
|
18194
|
+
// src/vault/broker/peercred.ts
|
|
18195
|
+
var SOCKET_PATH_AGENT_RE = /^\/run\/switchroom\/broker\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\.sock$/;
|
|
18196
|
+
var SOCKET_PATH_AGENT_SUBDIR_RE = /^\/run\/switchroom\/broker\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\/sock$/;
|
|
18197
|
+
var RESERVED_AGENT_NAMES = new Set(["operator", "hostd"]);
|
|
18198
|
+
function socketPathToIdentity(socketPath) {
|
|
18199
|
+
return parseSocketIdentity(socketPath, {
|
|
18200
|
+
patterns: [SOCKET_PATH_AGENT_RE, SOCKET_PATH_AGENT_SUBDIR_RE],
|
|
18201
|
+
reservedNames: RESERVED_AGENT_NAMES
|
|
18202
|
+
});
|
|
18187
18203
|
}
|
|
18188
|
-
function
|
|
18189
|
-
|
|
18190
|
-
|
|
18204
|
+
function socketPathToAgent(socketPath) {
|
|
18205
|
+
const identity = socketPathToIdentity(socketPath);
|
|
18206
|
+
return identity?.kind === "agent" ? identity.name : null;
|
|
18207
|
+
}
|
|
18208
|
+
function isReservedAgentName(name) {
|
|
18209
|
+
return RESERVED_AGENT_NAMES.has(name);
|
|
18210
|
+
}
|
|
18211
|
+
function unlockSocketFor(dataSocketPath) {
|
|
18212
|
+
if (dataSocketPath.endsWith("/sock")) {
|
|
18213
|
+
return dataSocketPath.slice(0, -"/sock".length) + "/unlock";
|
|
18191
18214
|
}
|
|
18192
|
-
|
|
18193
|
-
|
|
18194
|
-
|
|
18195
|
-
|
|
18196
|
-
|
|
18197
|
-
|
|
18198
|
-
|
|
18215
|
+
return dataSocketPath.replace(/\.sock$/, ".unlock.sock");
|
|
18216
|
+
}
|
|
18217
|
+
function parseSsRows(output) {
|
|
18218
|
+
const rows = [];
|
|
18219
|
+
const lines = output.split(`
|
|
18220
|
+
`);
|
|
18221
|
+
for (const line of lines) {
|
|
18222
|
+
if (!line.trim() || line.startsWith("Netid"))
|
|
18223
|
+
continue;
|
|
18224
|
+
const tokens = line.split(/\s+/).filter((t) => t.length > 0);
|
|
18225
|
+
if (tokens.length < 8)
|
|
18226
|
+
continue;
|
|
18227
|
+
const localAddr = tokens[4];
|
|
18228
|
+
const localInode = tokens[5];
|
|
18229
|
+
const peerAddr = tokens[6];
|
|
18230
|
+
const peerInode = tokens[7];
|
|
18231
|
+
const usersToken = tokens.slice(8).join(" ");
|
|
18232
|
+
const m = usersToken.match(/users:\(\(".*?",pid=(\d+),fd=\d+\)\)/);
|
|
18233
|
+
const pid = m ? parseInt(m[1], 10) : null;
|
|
18234
|
+
rows.push({ localAddr, localInode, peerAddr, peerInode, pid });
|
|
18199
18235
|
}
|
|
18200
|
-
|
|
18201
|
-
|
|
18202
|
-
|
|
18203
|
-
|
|
18204
|
-
|
|
18205
|
-
|
|
18236
|
+
return rows;
|
|
18237
|
+
}
|
|
18238
|
+
function findClientPids(rows, socketPath) {
|
|
18239
|
+
const pids = [];
|
|
18240
|
+
for (const serverRow of rows) {
|
|
18241
|
+
if (serverRow.localAddr !== socketPath)
|
|
18242
|
+
continue;
|
|
18243
|
+
for (const clientRow of rows) {
|
|
18244
|
+
if (clientRow.localAddr !== "*")
|
|
18245
|
+
continue;
|
|
18246
|
+
if (clientRow.localInode !== serverRow.peerInode)
|
|
18247
|
+
continue;
|
|
18248
|
+
if (clientRow.pid === null)
|
|
18249
|
+
continue;
|
|
18250
|
+
pids.push(clientRow.pid);
|
|
18251
|
+
break;
|
|
18206
18252
|
}
|
|
18207
18253
|
}
|
|
18208
|
-
return
|
|
18254
|
+
return pids;
|
|
18209
18255
|
}
|
|
18210
|
-
function
|
|
18211
|
-
|
|
18212
|
-
|
|
18213
|
-
|
|
18214
|
-
|
|
18215
|
-
|
|
18216
|
-
|
|
18217
|
-
|
|
18218
|
-
|
|
18219
|
-
|
|
18220
|
-
|
|
18256
|
+
function findClientPidByServerInode(rows, socketPath, serverInode) {
|
|
18257
|
+
const serverInodeStr = String(serverInode);
|
|
18258
|
+
for (const serverRow of rows) {
|
|
18259
|
+
if (serverRow.localAddr !== socketPath)
|
|
18260
|
+
continue;
|
|
18261
|
+
if (serverRow.localInode !== serverInodeStr)
|
|
18262
|
+
continue;
|
|
18263
|
+
for (const clientRow of rows) {
|
|
18264
|
+
if (clientRow.localAddr !== "*")
|
|
18265
|
+
continue;
|
|
18266
|
+
if (clientRow.localInode !== serverRow.peerInode)
|
|
18267
|
+
continue;
|
|
18268
|
+
if (clientRow.pid === null)
|
|
18269
|
+
continue;
|
|
18270
|
+
return clientRow.pid;
|
|
18271
|
+
}
|
|
18272
|
+
return null;
|
|
18221
18273
|
}
|
|
18222
|
-
|
|
18223
|
-
|
|
18274
|
+
return null;
|
|
18275
|
+
}
|
|
18276
|
+
function readUid(pid) {
|
|
18277
|
+
try {
|
|
18278
|
+
const status = readFileSync(`/proc/${pid}/status`, "utf8");
|
|
18279
|
+
const m = status.match(/^Uid:\s+(\d+)/m);
|
|
18280
|
+
if (!m)
|
|
18281
|
+
return null;
|
|
18282
|
+
return parseInt(m[1], 10);
|
|
18283
|
+
} catch {
|
|
18284
|
+
return null;
|
|
18224
18285
|
}
|
|
18225
|
-
|
|
18226
|
-
|
|
18286
|
+
}
|
|
18287
|
+
function readExe(pid) {
|
|
18288
|
+
try {
|
|
18289
|
+
return readlinkSync(`/proc/${pid}/exe`);
|
|
18290
|
+
} catch {
|
|
18291
|
+
return null;
|
|
18227
18292
|
}
|
|
18228
|
-
|
|
18229
|
-
|
|
18230
|
-
|
|
18231
|
-
const
|
|
18232
|
-
|
|
18233
|
-
|
|
18293
|
+
}
|
|
18294
|
+
function readSystemdUnit(pid) {
|
|
18295
|
+
try {
|
|
18296
|
+
const content = readFileSync(`/proc/${pid}/cgroup`, "utf8");
|
|
18297
|
+
const lines = content.split(`
|
|
18298
|
+
`);
|
|
18299
|
+
for (const line of lines) {
|
|
18300
|
+
if (!line.trim())
|
|
18301
|
+
continue;
|
|
18302
|
+
const parts = line.split(":");
|
|
18303
|
+
if (parts.length < 3)
|
|
18304
|
+
continue;
|
|
18305
|
+
const controller = parts[1];
|
|
18306
|
+
const isV2 = parts[0] === "0" && controller === "";
|
|
18307
|
+
const isV1Systemd = controller === "name=systemd";
|
|
18308
|
+
if (!isV2 && !isV1Systemd)
|
|
18309
|
+
continue;
|
|
18310
|
+
const cgroupPath = parts.slice(2).join(":");
|
|
18311
|
+
const segments = cgroupPath.split("/");
|
|
18312
|
+
const lastSegment = segments[segments.length - 1];
|
|
18313
|
+
if (!lastSegment)
|
|
18314
|
+
continue;
|
|
18315
|
+
if (/^switchroom-[a-zA-Z0-9_-]+(-cron-\d+)?\.service$/.test(lastSegment)) {
|
|
18316
|
+
return lastSegment;
|
|
18317
|
+
}
|
|
18234
18318
|
}
|
|
18319
|
+
return null;
|
|
18320
|
+
} catch {
|
|
18321
|
+
return null;
|
|
18235
18322
|
}
|
|
18236
|
-
|
|
18237
|
-
|
|
18238
|
-
|
|
18239
|
-
|
|
18240
|
-
|
|
18241
|
-
|
|
18242
|
-
|
|
18243
|
-
|
|
18244
|
-
|
|
18245
|
-
|
|
18246
|
-
|
|
18247
|
-
|
|
18248
|
-
|
|
18249
|
-
return { allow: true };
|
|
18250
|
-
}
|
|
18323
|
+
}
|
|
18324
|
+
function verifySystemdUnit(unitName, runner) {
|
|
18325
|
+
let raw;
|
|
18326
|
+
try {
|
|
18327
|
+
const out = runner("systemctl", [
|
|
18328
|
+
"--user",
|
|
18329
|
+
"show",
|
|
18330
|
+
unitName,
|
|
18331
|
+
"--property=LoadState,ActiveState"
|
|
18332
|
+
], { timeout: 500, encoding: "utf8" });
|
|
18333
|
+
raw = typeof out === "string" ? out : out.toString("utf8");
|
|
18334
|
+
} catch {
|
|
18335
|
+
return false;
|
|
18251
18336
|
}
|
|
18252
|
-
const
|
|
18253
|
-
const
|
|
18254
|
-
|
|
18255
|
-
|
|
18256
|
-
|
|
18257
|
-
|
|
18258
|
-
];
|
|
18259
|
-
if (standingSecrets.includes(key)) {
|
|
18260
|
-
return { allow: true };
|
|
18337
|
+
const props = {};
|
|
18338
|
+
for (const line of raw.split(`
|
|
18339
|
+
`)) {
|
|
18340
|
+
const m = line.match(/^([A-Za-z]+)=(.*)$/);
|
|
18341
|
+
if (m)
|
|
18342
|
+
props[m[1]] = m[2];
|
|
18261
18343
|
}
|
|
18262
|
-
|
|
18263
|
-
|
|
18264
|
-
|
|
18265
|
-
|
|
18266
|
-
reason: `agent '${agentName}' has no schedule entries declaring 'secrets', no mcp_servers.*.secrets[], and no agents.${agentName}.secrets[] standing grant declaring '${key}'; nothing is broker-accessible`
|
|
18267
|
-
};
|
|
18344
|
+
if (props.LoadState !== "loaded")
|
|
18345
|
+
return false;
|
|
18346
|
+
if (props.ActiveState !== "active" && props.ActiveState !== "activating") {
|
|
18347
|
+
return false;
|
|
18268
18348
|
}
|
|
18269
|
-
|
|
18270
|
-
|
|
18271
|
-
|
|
18272
|
-
|
|
18273
|
-
|
|
18349
|
+
return true;
|
|
18350
|
+
}
|
|
18351
|
+
function readFdInode(fd) {
|
|
18352
|
+
try {
|
|
18353
|
+
const stat = fstatSync(fd);
|
|
18354
|
+
return stat.ino;
|
|
18355
|
+
} catch {
|
|
18356
|
+
return null;
|
|
18274
18357
|
}
|
|
18275
|
-
return {
|
|
18276
|
-
allow: false,
|
|
18277
|
-
reason: `key '${key}' not in ACL for agent '${agentName}'`
|
|
18278
|
-
};
|
|
18279
18358
|
}
|
|
18280
|
-
function
|
|
18281
|
-
const
|
|
18282
|
-
if (!
|
|
18359
|
+
function fdFromSocket(socket) {
|
|
18360
|
+
const handle = socket._handle;
|
|
18361
|
+
if (!handle || typeof handle.fd !== "number" || handle.fd < 0)
|
|
18283
18362
|
return null;
|
|
18284
|
-
return
|
|
18363
|
+
return handle.fd;
|
|
18285
18364
|
}
|
|
18286
|
-
function
|
|
18287
|
-
|
|
18288
|
-
|
|
18289
|
-
const accountEntry = accounts[accountKey] ?? accounts[account];
|
|
18290
|
-
if (!accountEntry) {
|
|
18291
|
-
return {
|
|
18292
|
-
allow: false,
|
|
18293
|
-
reason: `google_accounts['${account}'] not configured (key '${key}')`
|
|
18294
|
-
};
|
|
18365
|
+
function identify(socketPath, socket, execFileSyncOverride) {
|
|
18366
|
+
if (process.platform !== "linux") {
|
|
18367
|
+
return null;
|
|
18295
18368
|
}
|
|
18296
|
-
const
|
|
18297
|
-
|
|
18298
|
-
|
|
18299
|
-
|
|
18300
|
-
|
|
18301
|
-
|
|
18369
|
+
const runner = execFileSyncOverride ?? execFileSync;
|
|
18370
|
+
let pid = null;
|
|
18371
|
+
if (socket !== undefined) {
|
|
18372
|
+
const fd = fdFromSocket(socket);
|
|
18373
|
+
if (fd !== null) {
|
|
18374
|
+
const cred = getPeerCred(fd);
|
|
18375
|
+
if (cred !== null)
|
|
18376
|
+
pid = cred.pid;
|
|
18377
|
+
}
|
|
18302
18378
|
}
|
|
18303
|
-
if (
|
|
18304
|
-
|
|
18305
|
-
|
|
18306
|
-
|
|
18307
|
-
|
|
18379
|
+
if (pid === null) {
|
|
18380
|
+
let ssOutput;
|
|
18381
|
+
try {
|
|
18382
|
+
const raw = runner("ss", ["-xpn"], {
|
|
18383
|
+
timeout: 200,
|
|
18384
|
+
encoding: "utf8"
|
|
18385
|
+
});
|
|
18386
|
+
ssOutput = typeof raw === "string" ? raw : raw.toString("utf8");
|
|
18387
|
+
} catch {
|
|
18388
|
+
return null;
|
|
18389
|
+
}
|
|
18390
|
+
const rows = parseSsRows(ssOutput);
|
|
18391
|
+
let serverInode = null;
|
|
18392
|
+
if (socket !== undefined) {
|
|
18393
|
+
const fd = fdFromSocket(socket);
|
|
18394
|
+
if (fd !== null)
|
|
18395
|
+
serverInode = readFdInode(fd);
|
|
18396
|
+
}
|
|
18397
|
+
if (serverInode !== null) {
|
|
18398
|
+
pid = findClientPidByServerInode(rows, socketPath, serverInode);
|
|
18399
|
+
} else {
|
|
18400
|
+
const clientPids = findClientPids(rows, socketPath);
|
|
18401
|
+
if (clientPids.length === 0)
|
|
18402
|
+
return null;
|
|
18403
|
+
if (clientPids.length > 1) {
|
|
18404
|
+
process.stderr.write(`[vault-broker] peercred: ${clientPids.length} connected peers found for ${socketPath}; ` + `using pid=${clientPids[0]}. ` + `Multiple simultaneous connections reduce identification accuracy. ` + `(This warning means identify() was called without a socket arg — likely a stale call site.)
|
|
18405
|
+
`);
|
|
18406
|
+
}
|
|
18407
|
+
pid = clientPids[0];
|
|
18408
|
+
}
|
|
18308
18409
|
}
|
|
18309
|
-
|
|
18410
|
+
if (pid === null)
|
|
18411
|
+
return null;
|
|
18412
|
+
const uid = readUid(pid);
|
|
18413
|
+
if (uid === null) {
|
|
18414
|
+
return null;
|
|
18415
|
+
}
|
|
18416
|
+
const brokerUid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
18417
|
+
if (brokerUid !== null && uid !== brokerUid) {
|
|
18418
|
+
process.stderr.write(`[vault-broker] peercred: UID mismatch — caller uid=${uid}, broker uid=${brokerUid}; denying
|
|
18419
|
+
`);
|
|
18420
|
+
return null;
|
|
18421
|
+
}
|
|
18422
|
+
const exe = readExe(pid);
|
|
18423
|
+
if (exe === null) {
|
|
18424
|
+
return null;
|
|
18425
|
+
}
|
|
18426
|
+
const cgroupClaim = readSystemdUnit(pid);
|
|
18427
|
+
let systemdUnit = null;
|
|
18428
|
+
if (cgroupClaim !== null) {
|
|
18429
|
+
if (verifySystemdUnit(cgroupClaim, runner)) {
|
|
18430
|
+
systemdUnit = cgroupClaim;
|
|
18431
|
+
} else {
|
|
18432
|
+
process.stderr.write(`[vault-broker] peercred: cgroup claims unit=${cgroupClaim} but systemd-user does not report it as loaded+running; treating caller as unidentified
|
|
18433
|
+
`);
|
|
18434
|
+
}
|
|
18435
|
+
}
|
|
18436
|
+
return { uid, pid, exe, systemdUnit };
|
|
18310
18437
|
}
|
|
18311
18438
|
|
|
18312
|
-
// src/
|
|
18313
|
-
|
|
18314
|
-
|
|
18315
|
-
|
|
18316
|
-
|
|
18317
|
-
|
|
18318
|
-
// src/agents/profiles.ts
|
|
18319
|
-
var import_handlebars = __toESM(require_lib(), 1);
|
|
18320
|
-
import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2, copyFileSync, mkdirSync, realpathSync } from "node:fs";
|
|
18321
|
-
import { resolve as resolve4, join as join2, sep as pathSep } from "node:path";
|
|
18322
|
-
var PROFILES_ROOT = resolve4(import.meta.dirname, "../../profiles");
|
|
18323
|
-
import_handlebars.default.registerHelper("json", (value) => {
|
|
18324
|
-
return new import_handlebars.default.SafeString(JSON.stringify(value, null, 2));
|
|
18325
|
-
});
|
|
18326
|
-
import_handlebars.default.registerHelper("isNumber", (value) => {
|
|
18327
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
18328
|
-
});
|
|
18329
|
-
var SHARED_FRAGMENTS_DIR = resolve4(PROFILES_ROOT, "_shared");
|
|
18330
|
-
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol"];
|
|
18331
|
-
for (const name of SHARED_FRAGMENTS) {
|
|
18332
|
-
const fragPath = join2(SHARED_FRAGMENTS_DIR, `${name}.md.hbs`);
|
|
18333
|
-
if (existsSync4(fragPath)) {
|
|
18334
|
-
import_handlebars.default.registerPartial(name, readFileSync3(fragPath, "utf-8"));
|
|
18439
|
+
// src/agents/agent-uid.ts
|
|
18440
|
+
var AGENT_UID_MIN = 10001;
|
|
18441
|
+
var AGENT_UID_MAX = 10999;
|
|
18442
|
+
function allocateAgentUid(name) {
|
|
18443
|
+
if (isReservedAgentName(name)) {
|
|
18444
|
+
throw new Error(`agent name '${name}' is reserved by switchroom for another identity kind ` + `(see vault/broker/peercred.ts:RESERVED_AGENT_NAMES). Pick a different name.`);
|
|
18335
18445
|
}
|
|
18446
|
+
const hash = createHash("sha256").update(name).digest();
|
|
18447
|
+
const u32 = hash.readUInt32BE(0);
|
|
18448
|
+
const range = AGENT_UID_MAX - AGENT_UID_MIN + 1;
|
|
18449
|
+
return AGENT_UID_MIN + u32 % range;
|
|
18336
18450
|
}
|
|
18337
18451
|
|
|
18338
|
-
// src/agents/
|
|
18339
|
-
|
|
18452
|
+
// src/agents/scaffold.ts
|
|
18453
|
+
init_schema();
|
|
18340
18454
|
|
|
18341
|
-
// src/
|
|
18342
|
-
|
|
18455
|
+
// src/config/users.ts
|
|
18456
|
+
init_merge();
|
|
18457
|
+
|
|
18458
|
+
// src/agents/scaffold.ts
|
|
18459
|
+
init_merge();
|
|
18460
|
+
|
|
18461
|
+
// src/config/timezone.ts
|
|
18462
|
+
var CONTAINER_DEFAULT_UTC_ZONES = new Set([
|
|
18463
|
+
"UTC",
|
|
18464
|
+
"Etc/UTC",
|
|
18465
|
+
"Etc/Universal",
|
|
18466
|
+
"Universal"
|
|
18467
|
+
]);
|
|
18468
|
+
|
|
18469
|
+
// src/cli/agent-config.ts
|
|
18470
|
+
import { join } from "node:path";
|
|
18471
|
+
import { homedir as homedir2 } from "node:os";
|
|
18472
|
+
|
|
18473
|
+
// src/cli/helpers.ts
|
|
18474
|
+
init_loader();
|
|
18475
|
+
|
|
18476
|
+
// src/cli/agent-config.ts
|
|
18477
|
+
init_loader();
|
|
18478
|
+
init_merge();
|
|
18479
|
+
|
|
18480
|
+
// src/config/google-workspace-acl.ts
|
|
18481
|
+
function shouldEmitGdriveMcp(agentName, agentGoogleAccount, googleAccounts) {
|
|
18482
|
+
if (!agentGoogleAccount)
|
|
18483
|
+
return false;
|
|
18484
|
+
const account = agentGoogleAccount.trim().toLowerCase();
|
|
18485
|
+
if (account.length === 0)
|
|
18486
|
+
return false;
|
|
18487
|
+
const acctEntry = googleAccounts?.[account];
|
|
18488
|
+
if (!acctEntry)
|
|
18489
|
+
return false;
|
|
18490
|
+
const enabledFor = acctEntry.enabled_for ?? [];
|
|
18491
|
+
return enabledFor.includes(agentName);
|
|
18492
|
+
}
|
|
18493
|
+
function vaultRefKey(value) {
|
|
18494
|
+
if (typeof value !== "string" || !value.startsWith("vault:"))
|
|
18495
|
+
return null;
|
|
18496
|
+
const key = value.slice("vault:".length).split("#")[0];
|
|
18497
|
+
return key.length > 0 ? key : null;
|
|
18498
|
+
}
|
|
18499
|
+
function isGoogleClientCredentialKeyForAgent(config, agentName, key) {
|
|
18500
|
+
if (!agentName || !key)
|
|
18501
|
+
return false;
|
|
18502
|
+
const agentConfig = config.agents?.[agentName];
|
|
18503
|
+
if (!agentConfig)
|
|
18504
|
+
return false;
|
|
18505
|
+
if (agentConfig.mcp_servers?.["gdrive"] === false) {
|
|
18506
|
+
return false;
|
|
18507
|
+
}
|
|
18508
|
+
const account = agentConfig.google_workspace?.account;
|
|
18509
|
+
if (!shouldEmitGdriveMcp(agentName, account, config.google_accounts)) {
|
|
18510
|
+
return false;
|
|
18511
|
+
}
|
|
18512
|
+
const gw = config.google_workspace;
|
|
18513
|
+
if (!gw)
|
|
18514
|
+
return false;
|
|
18515
|
+
for (const ref of [gw.google_client_id, gw.google_client_secret]) {
|
|
18516
|
+
if (vaultRefKey(ref) === key)
|
|
18517
|
+
return true;
|
|
18518
|
+
}
|
|
18519
|
+
return false;
|
|
18520
|
+
}
|
|
18521
|
+
|
|
18522
|
+
// src/vault/broker/acl.ts
|
|
18523
|
+
var WEBKITE_VAULT_KEYS = new Set([
|
|
18524
|
+
"webkite/cloudflare-account-id",
|
|
18525
|
+
"webkite/cloudflare-api-token",
|
|
18526
|
+
"webkite/firecrawl-api-key"
|
|
18527
|
+
]);
|
|
18528
|
+
function isWebkiteCredentialKeyForAgent(agentConfig, key) {
|
|
18529
|
+
if (!WEBKITE_VAULT_KEYS.has(key))
|
|
18530
|
+
return false;
|
|
18531
|
+
if ((agentConfig?.mcp_servers ?? {})["webkite"] === false)
|
|
18532
|
+
return false;
|
|
18533
|
+
return true;
|
|
18534
|
+
}
|
|
18535
|
+
function checkEntryScope(scope, agentSlug) {
|
|
18536
|
+
if (scope === undefined || scope === null) {
|
|
18537
|
+
return { allow: true };
|
|
18538
|
+
}
|
|
18539
|
+
const deny = scope.deny ?? [];
|
|
18540
|
+
const allow = scope.allow ?? [];
|
|
18541
|
+
if (agentSlug !== null && deny.includes(agentSlug)) {
|
|
18542
|
+
return {
|
|
18543
|
+
allow: false,
|
|
18544
|
+
reason: `agent '${agentSlug}' is in the entry's deny list (scope-deny)`
|
|
18545
|
+
};
|
|
18546
|
+
}
|
|
18547
|
+
if (allow.length > 0) {
|
|
18548
|
+
if (agentSlug === null || !allow.includes(agentSlug)) {
|
|
18549
|
+
return {
|
|
18550
|
+
allow: false,
|
|
18551
|
+
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)`
|
|
18552
|
+
};
|
|
18553
|
+
}
|
|
18554
|
+
}
|
|
18555
|
+
return { allow: true };
|
|
18556
|
+
}
|
|
18557
|
+
function checkAclByAgent(config, agentName, key) {
|
|
18558
|
+
if (!agentName) {
|
|
18559
|
+
return { allow: false, reason: "agent name unresolved" };
|
|
18560
|
+
}
|
|
18561
|
+
const agentConfig = config.agents?.[agentName];
|
|
18562
|
+
if (!agentConfig) {
|
|
18563
|
+
return { allow: false, reason: `agent '${agentName}' not found in config` };
|
|
18564
|
+
}
|
|
18565
|
+
const googleSlot = parseGoogleAccountSlotKey(key);
|
|
18566
|
+
if (googleSlot !== null) {
|
|
18567
|
+
return checkGoogleAccountAcl(config, agentName, googleSlot.account, key);
|
|
18568
|
+
}
|
|
18569
|
+
if (isGoogleClientCredentialKeyForAgent(config, agentName, key)) {
|
|
18570
|
+
return { allow: true };
|
|
18571
|
+
}
|
|
18572
|
+
if (isWebkiteCredentialKeyForAgent(agentConfig, key)) {
|
|
18573
|
+
return { allow: true };
|
|
18574
|
+
}
|
|
18575
|
+
const agentBot = agentConfig.bot_token;
|
|
18576
|
+
const botRef = agentBot && agentBot.length > 0 ? agentBot : config.telegram?.bot_token;
|
|
18577
|
+
if (typeof botRef === "string" && botRef.startsWith("vault:")) {
|
|
18578
|
+
const botKey = botRef.slice("vault:".length).split("#")[0];
|
|
18579
|
+
if (botKey.length > 0 && botKey === key) {
|
|
18580
|
+
return { allow: true };
|
|
18581
|
+
}
|
|
18582
|
+
}
|
|
18583
|
+
const cfgWithProfiles = config;
|
|
18584
|
+
const profileName = agentConfig.extends;
|
|
18585
|
+
const profileMcp = profileName != null && profileName.length > 0 ? cfgWithProfiles.profiles?.[profileName]?.mcp_servers ?? {} : {};
|
|
18586
|
+
const effectiveMcp = {
|
|
18587
|
+
...cfgWithProfiles.defaults?.mcp_servers ?? {},
|
|
18588
|
+
...profileMcp,
|
|
18589
|
+
...agentConfig.mcp_servers ?? {}
|
|
18590
|
+
};
|
|
18591
|
+
for (const mcpEntry of Object.values(effectiveMcp)) {
|
|
18592
|
+
if (!mcpEntry || typeof mcpEntry !== "object")
|
|
18593
|
+
continue;
|
|
18594
|
+
const declared = mcpEntry.secrets;
|
|
18595
|
+
if (Array.isArray(declared) && declared.includes(key)) {
|
|
18596
|
+
return { allow: true };
|
|
18597
|
+
}
|
|
18598
|
+
}
|
|
18599
|
+
const cfgSecrets = config;
|
|
18600
|
+
const profileSecrets = profileName != null && profileName.length > 0 ? cfgSecrets.profiles?.[profileName]?.secrets : undefined;
|
|
18601
|
+
const standingSecrets = [
|
|
18602
|
+
...Array.isArray(cfgSecrets.defaults?.secrets) ? cfgSecrets.defaults.secrets : [],
|
|
18603
|
+
...Array.isArray(profileSecrets) ? profileSecrets : [],
|
|
18604
|
+
...Array.isArray(agentConfig.secrets) ? agentConfig.secrets : []
|
|
18605
|
+
];
|
|
18606
|
+
if (standingSecrets.includes(key)) {
|
|
18607
|
+
return { allow: true };
|
|
18608
|
+
}
|
|
18609
|
+
const schedule = agentConfig.schedule ?? [];
|
|
18610
|
+
if (schedule.length === 0) {
|
|
18611
|
+
return {
|
|
18612
|
+
allow: false,
|
|
18613
|
+
reason: `agent '${agentName}' has no schedule entries declaring 'secrets', no mcp_servers.*.secrets[], and no agents.${agentName}.secrets[] standing grant declaring '${key}'; nothing is broker-accessible`
|
|
18614
|
+
};
|
|
18615
|
+
}
|
|
18616
|
+
for (const entry of schedule) {
|
|
18617
|
+
const allowed = entry?.secrets ?? [];
|
|
18618
|
+
if (allowed.includes(key)) {
|
|
18619
|
+
return { allow: true };
|
|
18620
|
+
}
|
|
18621
|
+
}
|
|
18622
|
+
return {
|
|
18623
|
+
allow: false,
|
|
18624
|
+
reason: `key '${key}' not in ACL for agent '${agentName}'`
|
|
18625
|
+
};
|
|
18626
|
+
}
|
|
18627
|
+
function parseGoogleAccountSlotKey(key) {
|
|
18628
|
+
const match = key.match(/^google:([^:]+):([a-z_]+)$/);
|
|
18629
|
+
if (!match)
|
|
18630
|
+
return null;
|
|
18631
|
+
return { account: match[1], field: match[2] };
|
|
18632
|
+
}
|
|
18633
|
+
function checkGoogleAccountAcl(config, agentName, account, key) {
|
|
18634
|
+
const accounts = config.google_accounts ?? {};
|
|
18635
|
+
const accountKey = account.toLowerCase();
|
|
18636
|
+
const accountEntry = accounts[accountKey] ?? accounts[account];
|
|
18637
|
+
if (!accountEntry) {
|
|
18638
|
+
return {
|
|
18639
|
+
allow: false,
|
|
18640
|
+
reason: `google_accounts['${account}'] not configured (key '${key}')`
|
|
18641
|
+
};
|
|
18642
|
+
}
|
|
18643
|
+
const enabled = accountEntry.enabled_for ?? [];
|
|
18644
|
+
if (enabled.length === 0) {
|
|
18645
|
+
return {
|
|
18646
|
+
allow: false,
|
|
18647
|
+
reason: `google_accounts['${account}'].enabled_for is empty (key '${key}')`
|
|
18648
|
+
};
|
|
18649
|
+
}
|
|
18650
|
+
if (!enabled.includes(agentName)) {
|
|
18651
|
+
return {
|
|
18652
|
+
allow: false,
|
|
18653
|
+
reason: `agent '${agentName}' not in google_accounts['${account}'].enabled_for (key '${key}')`
|
|
18654
|
+
};
|
|
18655
|
+
}
|
|
18656
|
+
return { allow: true };
|
|
18657
|
+
}
|
|
18658
|
+
|
|
18659
|
+
// src/scheduler/cron-introspect.ts
|
|
18660
|
+
init_overlay_loader();
|
|
18661
|
+
|
|
18662
|
+
// src/cli/agent-config.ts
|
|
18663
|
+
var AUDIT_ROOT = join(homedir2(), ".switchroom", "audit");
|
|
18664
|
+
|
|
18665
|
+
// src/agents/profiles.ts
|
|
18666
|
+
var import_handlebars = __toESM(require_lib(), 1);
|
|
18667
|
+
import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2, copyFileSync, mkdirSync, realpathSync } from "node:fs";
|
|
18668
|
+
import { resolve as resolve4, join as join2, sep as pathSep } from "node:path";
|
|
18669
|
+
var PROFILES_ROOT = resolve4(import.meta.dirname, "../../profiles");
|
|
18670
|
+
import_handlebars.default.registerHelper("json", (value) => {
|
|
18671
|
+
return new import_handlebars.default.SafeString(JSON.stringify(value, null, 2));
|
|
18672
|
+
});
|
|
18673
|
+
import_handlebars.default.registerHelper("isNumber", (value) => {
|
|
18674
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
18675
|
+
});
|
|
18676
|
+
var SHARED_FRAGMENTS_DIR = resolve4(PROFILES_ROOT, "_shared");
|
|
18677
|
+
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol"];
|
|
18678
|
+
for (const name of SHARED_FRAGMENTS) {
|
|
18679
|
+
const fragPath = join2(SHARED_FRAGMENTS_DIR, `${name}.md.hbs`);
|
|
18680
|
+
if (existsSync4(fragPath)) {
|
|
18681
|
+
import_handlebars.default.registerPartial(name, readFileSync4(fragPath, "utf-8"));
|
|
18682
|
+
}
|
|
18683
|
+
}
|
|
18684
|
+
|
|
18685
|
+
// src/agents/pane-lock.ts
|
|
18686
|
+
var tails = new Map;
|
|
18687
|
+
|
|
18688
|
+
// src/agents/inject.ts
|
|
18689
|
+
var INJECT_COMMANDS = new Map([
|
|
18343
18690
|
["/cost", { description: "Show session cost", expectsOutput: true, dialog: true, argsAllowed: false }],
|
|
18344
18691
|
["/status", { description: "Show session status", expectsOutput: true, dialog: true, argsAllowed: false }],
|
|
18345
18692
|
["/usage", { description: "Show plan quota", expectsOutput: true, dialog: true, argsAllowed: false }],
|
|
@@ -18431,7 +18778,7 @@ init_loader();
|
|
|
18431
18778
|
// src/vault/vault.ts
|
|
18432
18779
|
import { randomBytes, scryptSync, createCipheriv, createDecipheriv } from "node:crypto";
|
|
18433
18780
|
import {
|
|
18434
|
-
readFileSync as
|
|
18781
|
+
readFileSync as readFileSync6,
|
|
18435
18782
|
writeSync as writeSync3,
|
|
18436
18783
|
existsSync as existsSync6,
|
|
18437
18784
|
renameSync as renameSync2,
|
|
@@ -18453,7 +18800,7 @@ import {
|
|
|
18453
18800
|
writeSync as writeSync2,
|
|
18454
18801
|
fsyncSync as fsyncSync2,
|
|
18455
18802
|
unlinkSync,
|
|
18456
|
-
readFileSync as
|
|
18803
|
+
readFileSync as readFileSync5,
|
|
18457
18804
|
readdirSync as readdirSync3,
|
|
18458
18805
|
rmdirSync,
|
|
18459
18806
|
statSync as statSync3,
|
|
@@ -18465,7 +18812,7 @@ function lockPathFor(vaultPath) {
|
|
|
18465
18812
|
}
|
|
18466
18813
|
function readLockHolder(lockPath) {
|
|
18467
18814
|
try {
|
|
18468
|
-
const raw =
|
|
18815
|
+
const raw = readFileSync5(lockPath, "utf8");
|
|
18469
18816
|
const lines = raw.split(`
|
|
18470
18817
|
`);
|
|
18471
18818
|
const pid = Number.parseInt(lines[0] ?? "", 10);
|
|
@@ -18517,8 +18864,8 @@ function pidStartTimeMs(pid) {
|
|
|
18517
18864
|
if (process.platform !== "linux")
|
|
18518
18865
|
return null;
|
|
18519
18866
|
try {
|
|
18520
|
-
const statLine =
|
|
18521
|
-
const procStat =
|
|
18867
|
+
const statLine = readFileSync5(`/proc/${pid}/stat`, "utf8");
|
|
18868
|
+
const procStat = readFileSync5("/proc/stat", "utf8");
|
|
18522
18869
|
return parseProcStartTimeMs(statLine, procStat, Date.now());
|
|
18523
18870
|
} catch {
|
|
18524
18871
|
return null;
|
|
@@ -18703,479 +19050,146 @@ function atomicWriteFileSync(path, data, mode) {
|
|
|
18703
19050
|
try {
|
|
18704
19051
|
if (existsSync6(tmp))
|
|
18705
19052
|
unlinkSync2(tmp);
|
|
18706
|
-
} catch {}
|
|
18707
|
-
throw err;
|
|
18708
|
-
}
|
|
18709
|
-
}
|
|
18710
|
-
|
|
18711
|
-
class VaultError extends Error {
|
|
18712
|
-
cause;
|
|
18713
|
-
constructor(message, options) {
|
|
18714
|
-
super(message);
|
|
18715
|
-
this.name = "VaultError";
|
|
18716
|
-
if (options?.cause !== undefined)
|
|
18717
|
-
this.cause = options.cause;
|
|
18718
|
-
}
|
|
18719
|
-
}
|
|
18720
|
-
var LEGACY_SCRYPT_N = 16384;
|
|
18721
|
-
function deriveKey(passphrase, salt, params = { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }) {
|
|
18722
|
-
return scryptSync(passphrase, salt, 32, {
|
|
18723
|
-
N: params.N,
|
|
18724
|
-
r: params.r,
|
|
18725
|
-
p: params.p,
|
|
18726
|
-
maxmem: SCRYPT_MAXMEM
|
|
18727
|
-
});
|
|
18728
|
-
}
|
|
18729
|
-
function encrypt(key, plaintext) {
|
|
18730
|
-
const iv = randomBytes(12);
|
|
18731
|
-
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
18732
|
-
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
18733
|
-
const tag = cipher.getAuthTag();
|
|
18734
|
-
return {
|
|
18735
|
-
iv: iv.toString("hex"),
|
|
18736
|
-
data: encrypted.toString("hex"),
|
|
18737
|
-
tag: tag.toString("hex")
|
|
18738
|
-
};
|
|
18739
|
-
}
|
|
18740
|
-
function decrypt(key, iv, data, tag) {
|
|
18741
|
-
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "hex"));
|
|
18742
|
-
decipher.setAuthTag(Buffer.from(tag, "hex"));
|
|
18743
|
-
const decrypted = Buffer.concat([
|
|
18744
|
-
decipher.update(Buffer.from(data, "hex")),
|
|
18745
|
-
decipher.final()
|
|
18746
|
-
]);
|
|
18747
|
-
return decrypted.toString("utf8");
|
|
18748
|
-
}
|
|
18749
|
-
function normalizeEntry(raw) {
|
|
18750
|
-
if (typeof raw === "string") {
|
|
18751
|
-
return { kind: "string", value: raw };
|
|
18752
|
-
}
|
|
18753
|
-
return raw;
|
|
18754
|
-
}
|
|
18755
|
-
function normalizeSecrets(raw) {
|
|
18756
|
-
const out = {};
|
|
18757
|
-
for (const [k, v] of Object.entries(raw)) {
|
|
18758
|
-
out[k] = normalizeEntry(v);
|
|
18759
|
-
}
|
|
18760
|
-
return out;
|
|
18761
|
-
}
|
|
18762
|
-
function openVault(passphrase, vaultPath) {
|
|
18763
|
-
if (!existsSync6(vaultPath)) {
|
|
18764
|
-
throw new VaultError(`Vault file not found: ${vaultPath}`);
|
|
18765
|
-
}
|
|
18766
|
-
let vaultFile;
|
|
18767
|
-
try {
|
|
18768
|
-
vaultFile = JSON.parse(readFileSync5(vaultPath, "utf8"));
|
|
18769
|
-
} catch {
|
|
18770
|
-
throw new VaultError(`Failed to read vault file: ${vaultPath}`);
|
|
18771
|
-
}
|
|
18772
|
-
const salt = Buffer.from(vaultFile.salt, "hex");
|
|
18773
|
-
const kdfParams = vaultFile.kdf ?? { N: LEGACY_SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P };
|
|
18774
|
-
const key = deriveKey(passphrase, salt, kdfParams);
|
|
18775
|
-
let plaintext;
|
|
18776
|
-
try {
|
|
18777
|
-
plaintext = decrypt(key, vaultFile.iv, vaultFile.data, vaultFile.tag);
|
|
18778
|
-
} catch {
|
|
18779
|
-
throw new VaultError("Failed to decrypt vault. Wrong passphrase?");
|
|
18780
|
-
}
|
|
18781
|
-
let vaultData;
|
|
18782
|
-
try {
|
|
18783
|
-
vaultData = JSON.parse(plaintext);
|
|
18784
|
-
} catch {
|
|
18785
|
-
throw new VaultError("Vault data is corrupted");
|
|
18786
|
-
}
|
|
18787
|
-
return normalizeSecrets(vaultData.secrets ?? {});
|
|
18788
|
-
}
|
|
18789
|
-
function saveVault(passphrase, vaultPath, secrets) {
|
|
18790
|
-
if (!existsSync6(vaultPath)) {
|
|
18791
|
-
throw new VaultError(`Vault file not found: ${vaultPath}`);
|
|
18792
|
-
}
|
|
18793
|
-
let releaseLock = null;
|
|
18794
|
-
try {
|
|
18795
|
-
releaseLock = acquireLock(vaultPath, { budgetMs: SAVE_VAULT_LOCK_RETRY_MS }).release;
|
|
18796
|
-
} catch (err) {
|
|
18797
|
-
if (err instanceof VaultBusyError) {
|
|
18798
|
-
throw new VaultError(err.message, { cause: err });
|
|
18799
|
-
}
|
|
18800
|
-
throw err;
|
|
18801
|
-
}
|
|
18802
|
-
try {
|
|
18803
|
-
let vaultFile;
|
|
18804
|
-
try {
|
|
18805
|
-
vaultFile = JSON.parse(readFileSync5(vaultPath, "utf8"));
|
|
18806
|
-
} catch {
|
|
18807
|
-
throw new VaultError(`Failed to read vault file: ${vaultPath}`);
|
|
18808
|
-
}
|
|
18809
|
-
const salt = Buffer.from(vaultFile.salt, "hex");
|
|
18810
|
-
const key = deriveKey(passphrase, salt, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P });
|
|
18811
|
-
vaultFile.kdf = { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P };
|
|
18812
|
-
const vaultData = { secrets };
|
|
18813
|
-
const { iv, data, tag } = encrypt(key, JSON.stringify(vaultData));
|
|
18814
|
-
vaultFile.iv = iv;
|
|
18815
|
-
vaultFile.data = data;
|
|
18816
|
-
vaultFile.tag = tag;
|
|
18817
|
-
atomicWriteFileSync(vaultPath, JSON.stringify(vaultFile, null, 2), 384);
|
|
18818
|
-
} finally {
|
|
18819
|
-
if (releaseLock !== null) {
|
|
18820
|
-
try {
|
|
18821
|
-
releaseLock();
|
|
18822
|
-
} catch {}
|
|
18823
|
-
}
|
|
18824
|
-
}
|
|
18825
|
-
}
|
|
18826
|
-
function acquireVaultLock(vaultPath) {
|
|
18827
|
-
try {
|
|
18828
|
-
return acquireLock(vaultPath, { budgetMs: SAVE_VAULT_LOCK_RETRY_MS }).release;
|
|
18829
|
-
} catch (err) {
|
|
18830
|
-
if (err instanceof VaultBusyError) {
|
|
18831
|
-
throw new VaultError(err.message, { cause: err });
|
|
18832
|
-
}
|
|
18833
|
-
throw err;
|
|
18834
|
-
}
|
|
18835
|
-
}
|
|
18836
|
-
|
|
18837
|
-
// src/vault/resolver.ts
|
|
18838
|
-
init_loader();
|
|
18839
|
-
|
|
18840
|
-
// src/vault/broker/client.ts
|
|
18841
|
-
import { homedir as homedir3 } from "node:os";
|
|
18842
|
-
import { join as join3 } from "node:path";
|
|
18843
|
-
|
|
18844
|
-
// src/vault/broker/protocol.ts
|
|
18845
|
-
init_zod();
|
|
18846
|
-
|
|
18847
|
-
// src/vault/broker/peercred.ts
|
|
18848
|
-
import { execFileSync } from "node:child_process";
|
|
18849
|
-
import { readFileSync as readFileSync6, readlinkSync, fstatSync } from "node:fs";
|
|
18850
|
-
|
|
18851
|
-
// src/vault/broker/peercred-ffi.ts
|
|
18852
|
-
function getPeerCred(fd) {
|
|
18853
|
-
if (process.platform !== "linux")
|
|
18854
|
-
return null;
|
|
18855
|
-
try {
|
|
18856
|
-
const ffi = __require("bun:ffi");
|
|
18857
|
-
const { dlopen, FFIType, ptr } = ffi;
|
|
18858
|
-
const SOL_SOCKET = 1;
|
|
18859
|
-
const SO_PEERCRED = 17;
|
|
18860
|
-
const UCRED_SIZE = 12;
|
|
18861
|
-
const cache = getPeerCred;
|
|
18862
|
-
const lib = cache._lib ?? (() => {
|
|
18863
|
-
const candidates = ["libc.so.6", "libc.so"];
|
|
18864
|
-
const symbolSpec = {
|
|
18865
|
-
getsockopt: {
|
|
18866
|
-
args: [FFIType.i32, FFIType.i32, FFIType.i32, FFIType.ptr, FFIType.ptr],
|
|
18867
|
-
returns: FFIType.i32
|
|
18868
|
-
}
|
|
18869
|
-
};
|
|
18870
|
-
const errors2 = [];
|
|
18871
|
-
for (const name of candidates) {
|
|
18872
|
-
try {
|
|
18873
|
-
const opened = dlopen(name, symbolSpec);
|
|
18874
|
-
cache._lib = opened;
|
|
18875
|
-
return opened;
|
|
18876
|
-
} catch (e) {
|
|
18877
|
-
errors2.push(`${name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
18878
|
-
}
|
|
18879
|
-
}
|
|
18880
|
-
process.stderr.write(`[vault-broker] peercred-ffi: dlopen failed for all libc candidates ` + `(${errors2.join("; ")}); falling back to ss-parsing.
|
|
18881
|
-
`);
|
|
18882
|
-
throw new Error("no libc candidate could be opened");
|
|
18883
|
-
})();
|
|
18884
|
-
const credBuf = new ArrayBuffer(UCRED_SIZE);
|
|
18885
|
-
const lenBuf = new Uint32Array(1);
|
|
18886
|
-
lenBuf[0] = UCRED_SIZE;
|
|
18887
|
-
const rc = lib.symbols.getsockopt(fd, SOL_SOCKET, SO_PEERCRED, ptr(credBuf), ptr(lenBuf.buffer));
|
|
18888
|
-
if (rc !== 0)
|
|
18889
|
-
return null;
|
|
18890
|
-
if (lenBuf[0] !== UCRED_SIZE)
|
|
18891
|
-
return null;
|
|
18892
|
-
const view = new DataView(credBuf);
|
|
18893
|
-
return {
|
|
18894
|
-
pid: view.getInt32(0, true),
|
|
18895
|
-
uid: view.getInt32(4, true),
|
|
18896
|
-
gid: view.getInt32(8, true)
|
|
18897
|
-
};
|
|
18898
|
-
} catch {
|
|
18899
|
-
return null;
|
|
18900
|
-
}
|
|
18901
|
-
}
|
|
18902
|
-
|
|
18903
|
-
// src/broker-common/peercred-path.ts
|
|
18904
|
-
function matchSocketName(socketPath, patterns, maxNameLen) {
|
|
18905
|
-
if (typeof socketPath !== "string" || socketPath.length === 0)
|
|
18906
|
-
return null;
|
|
18907
|
-
let name = null;
|
|
18908
|
-
for (const re of patterns) {
|
|
18909
|
-
const m = socketPath.match(re);
|
|
18910
|
-
if (m && typeof m[1] === "string") {
|
|
18911
|
-
name = m[1];
|
|
18912
|
-
break;
|
|
18913
|
-
}
|
|
18914
|
-
}
|
|
18915
|
-
if (name === null || name.length === 0)
|
|
18916
|
-
return null;
|
|
18917
|
-
if (maxNameLen !== undefined && name.length > maxNameLen)
|
|
18918
|
-
return null;
|
|
18919
|
-
return name;
|
|
18920
|
-
}
|
|
18921
|
-
function parseSocketIdentity(socketPath, spec) {
|
|
18922
|
-
const name = matchSocketName(socketPath, spec.patterns, spec.maxNameLen);
|
|
18923
|
-
if (name === null)
|
|
18924
|
-
return null;
|
|
18925
|
-
const operatorName = spec.operatorName ?? "operator";
|
|
18926
|
-
if (name === operatorName)
|
|
18927
|
-
return { kind: "operator" };
|
|
18928
|
-
if (spec.reservedNames.has(name))
|
|
18929
|
-
return null;
|
|
18930
|
-
return { kind: "agent", name };
|
|
18931
|
-
}
|
|
18932
|
-
|
|
18933
|
-
// src/vault/broker/peercred.ts
|
|
18934
|
-
var SOCKET_PATH_AGENT_RE = /^\/run\/switchroom\/broker\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\.sock$/;
|
|
18935
|
-
var SOCKET_PATH_AGENT_SUBDIR_RE = /^\/run\/switchroom\/broker\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\/sock$/;
|
|
18936
|
-
var RESERVED_AGENT_NAMES = new Set(["operator", "hostd"]);
|
|
18937
|
-
function socketPathToIdentity(socketPath) {
|
|
18938
|
-
return parseSocketIdentity(socketPath, {
|
|
18939
|
-
patterns: [SOCKET_PATH_AGENT_RE, SOCKET_PATH_AGENT_SUBDIR_RE],
|
|
18940
|
-
reservedNames: RESERVED_AGENT_NAMES
|
|
18941
|
-
});
|
|
18942
|
-
}
|
|
18943
|
-
function socketPathToAgent(socketPath) {
|
|
18944
|
-
const identity = socketPathToIdentity(socketPath);
|
|
18945
|
-
return identity?.kind === "agent" ? identity.name : null;
|
|
18946
|
-
}
|
|
18947
|
-
function isReservedAgentName(name) {
|
|
18948
|
-
return RESERVED_AGENT_NAMES.has(name);
|
|
18949
|
-
}
|
|
18950
|
-
function unlockSocketFor(dataSocketPath) {
|
|
18951
|
-
if (dataSocketPath.endsWith("/sock")) {
|
|
18952
|
-
return dataSocketPath.slice(0, -"/sock".length) + "/unlock";
|
|
18953
|
-
}
|
|
18954
|
-
return dataSocketPath.replace(/\.sock$/, ".unlock.sock");
|
|
18955
|
-
}
|
|
18956
|
-
function parseSsRows(output) {
|
|
18957
|
-
const rows = [];
|
|
18958
|
-
const lines = output.split(`
|
|
18959
|
-
`);
|
|
18960
|
-
for (const line of lines) {
|
|
18961
|
-
if (!line.trim() || line.startsWith("Netid"))
|
|
18962
|
-
continue;
|
|
18963
|
-
const tokens = line.split(/\s+/).filter((t) => t.length > 0);
|
|
18964
|
-
if (tokens.length < 8)
|
|
18965
|
-
continue;
|
|
18966
|
-
const localAddr = tokens[4];
|
|
18967
|
-
const localInode = tokens[5];
|
|
18968
|
-
const peerAddr = tokens[6];
|
|
18969
|
-
const peerInode = tokens[7];
|
|
18970
|
-
const usersToken = tokens.slice(8).join(" ");
|
|
18971
|
-
const m = usersToken.match(/users:\(\(".*?",pid=(\d+),fd=\d+\)\)/);
|
|
18972
|
-
const pid = m ? parseInt(m[1], 10) : null;
|
|
18973
|
-
rows.push({ localAddr, localInode, peerAddr, peerInode, pid });
|
|
18974
|
-
}
|
|
18975
|
-
return rows;
|
|
18976
|
-
}
|
|
18977
|
-
function findClientPids(rows, socketPath) {
|
|
18978
|
-
const pids = [];
|
|
18979
|
-
for (const serverRow of rows) {
|
|
18980
|
-
if (serverRow.localAddr !== socketPath)
|
|
18981
|
-
continue;
|
|
18982
|
-
for (const clientRow of rows) {
|
|
18983
|
-
if (clientRow.localAddr !== "*")
|
|
18984
|
-
continue;
|
|
18985
|
-
if (clientRow.localInode !== serverRow.peerInode)
|
|
18986
|
-
continue;
|
|
18987
|
-
if (clientRow.pid === null)
|
|
18988
|
-
continue;
|
|
18989
|
-
pids.push(clientRow.pid);
|
|
18990
|
-
break;
|
|
18991
|
-
}
|
|
18992
|
-
}
|
|
18993
|
-
return pids;
|
|
18994
|
-
}
|
|
18995
|
-
function findClientPidByServerInode(rows, socketPath, serverInode) {
|
|
18996
|
-
const serverInodeStr = String(serverInode);
|
|
18997
|
-
for (const serverRow of rows) {
|
|
18998
|
-
if (serverRow.localAddr !== socketPath)
|
|
18999
|
-
continue;
|
|
19000
|
-
if (serverRow.localInode !== serverInodeStr)
|
|
19001
|
-
continue;
|
|
19002
|
-
for (const clientRow of rows) {
|
|
19003
|
-
if (clientRow.localAddr !== "*")
|
|
19004
|
-
continue;
|
|
19005
|
-
if (clientRow.localInode !== serverRow.peerInode)
|
|
19006
|
-
continue;
|
|
19007
|
-
if (clientRow.pid === null)
|
|
19008
|
-
continue;
|
|
19009
|
-
return clientRow.pid;
|
|
19010
|
-
}
|
|
19011
|
-
return null;
|
|
19053
|
+
} catch {}
|
|
19054
|
+
throw err;
|
|
19012
19055
|
}
|
|
19013
|
-
return null;
|
|
19014
19056
|
}
|
|
19015
|
-
|
|
19016
|
-
|
|
19017
|
-
|
|
19018
|
-
|
|
19019
|
-
|
|
19020
|
-
|
|
19021
|
-
|
|
19022
|
-
|
|
19023
|
-
return null;
|
|
19057
|
+
|
|
19058
|
+
class VaultError extends Error {
|
|
19059
|
+
cause;
|
|
19060
|
+
constructor(message, options) {
|
|
19061
|
+
super(message);
|
|
19062
|
+
this.name = "VaultError";
|
|
19063
|
+
if (options?.cause !== undefined)
|
|
19064
|
+
this.cause = options.cause;
|
|
19024
19065
|
}
|
|
19025
19066
|
}
|
|
19026
|
-
|
|
19027
|
-
|
|
19028
|
-
|
|
19029
|
-
|
|
19030
|
-
|
|
19067
|
+
var LEGACY_SCRYPT_N = 16384;
|
|
19068
|
+
function deriveKey(passphrase, salt, params = { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }) {
|
|
19069
|
+
return scryptSync(passphrase, salt, 32, {
|
|
19070
|
+
N: params.N,
|
|
19071
|
+
r: params.r,
|
|
19072
|
+
p: params.p,
|
|
19073
|
+
maxmem: SCRYPT_MAXMEM
|
|
19074
|
+
});
|
|
19075
|
+
}
|
|
19076
|
+
function encrypt(key, plaintext) {
|
|
19077
|
+
const iv = randomBytes(12);
|
|
19078
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
19079
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
19080
|
+
const tag = cipher.getAuthTag();
|
|
19081
|
+
return {
|
|
19082
|
+
iv: iv.toString("hex"),
|
|
19083
|
+
data: encrypted.toString("hex"),
|
|
19084
|
+
tag: tag.toString("hex")
|
|
19085
|
+
};
|
|
19086
|
+
}
|
|
19087
|
+
function decrypt(key, iv, data, tag) {
|
|
19088
|
+
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "hex"));
|
|
19089
|
+
decipher.setAuthTag(Buffer.from(tag, "hex"));
|
|
19090
|
+
const decrypted = Buffer.concat([
|
|
19091
|
+
decipher.update(Buffer.from(data, "hex")),
|
|
19092
|
+
decipher.final()
|
|
19093
|
+
]);
|
|
19094
|
+
return decrypted.toString("utf8");
|
|
19095
|
+
}
|
|
19096
|
+
function normalizeEntry(raw) {
|
|
19097
|
+
if (typeof raw === "string") {
|
|
19098
|
+
return { kind: "string", value: raw };
|
|
19031
19099
|
}
|
|
19100
|
+
return raw;
|
|
19032
19101
|
}
|
|
19033
|
-
function
|
|
19034
|
-
|
|
19035
|
-
|
|
19036
|
-
|
|
19037
|
-
`);
|
|
19038
|
-
for (const line of lines) {
|
|
19039
|
-
if (!line.trim())
|
|
19040
|
-
continue;
|
|
19041
|
-
const parts = line.split(":");
|
|
19042
|
-
if (parts.length < 3)
|
|
19043
|
-
continue;
|
|
19044
|
-
const controller = parts[1];
|
|
19045
|
-
const isV2 = parts[0] === "0" && controller === "";
|
|
19046
|
-
const isV1Systemd = controller === "name=systemd";
|
|
19047
|
-
if (!isV2 && !isV1Systemd)
|
|
19048
|
-
continue;
|
|
19049
|
-
const cgroupPath = parts.slice(2).join(":");
|
|
19050
|
-
const segments = cgroupPath.split("/");
|
|
19051
|
-
const lastSegment = segments[segments.length - 1];
|
|
19052
|
-
if (!lastSegment)
|
|
19053
|
-
continue;
|
|
19054
|
-
if (/^switchroom-[a-zA-Z0-9_-]+(-cron-\d+)?\.service$/.test(lastSegment)) {
|
|
19055
|
-
return lastSegment;
|
|
19056
|
-
}
|
|
19057
|
-
}
|
|
19058
|
-
return null;
|
|
19059
|
-
} catch {
|
|
19060
|
-
return null;
|
|
19102
|
+
function normalizeSecrets(raw) {
|
|
19103
|
+
const out = {};
|
|
19104
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
19105
|
+
out[k] = normalizeEntry(v);
|
|
19061
19106
|
}
|
|
19107
|
+
return out;
|
|
19062
19108
|
}
|
|
19063
|
-
function
|
|
19064
|
-
|
|
19109
|
+
function openVault(passphrase, vaultPath) {
|
|
19110
|
+
if (!existsSync6(vaultPath)) {
|
|
19111
|
+
throw new VaultError(`Vault file not found: ${vaultPath}`);
|
|
19112
|
+
}
|
|
19113
|
+
let vaultFile;
|
|
19065
19114
|
try {
|
|
19066
|
-
|
|
19067
|
-
"--user",
|
|
19068
|
-
"show",
|
|
19069
|
-
unitName,
|
|
19070
|
-
"--property=LoadState,ActiveState"
|
|
19071
|
-
], { timeout: 500, encoding: "utf8" });
|
|
19072
|
-
raw = typeof out === "string" ? out : out.toString("utf8");
|
|
19115
|
+
vaultFile = JSON.parse(readFileSync6(vaultPath, "utf8"));
|
|
19073
19116
|
} catch {
|
|
19074
|
-
|
|
19075
|
-
}
|
|
19076
|
-
const props = {};
|
|
19077
|
-
for (const line of raw.split(`
|
|
19078
|
-
`)) {
|
|
19079
|
-
const m = line.match(/^([A-Za-z]+)=(.*)$/);
|
|
19080
|
-
if (m)
|
|
19081
|
-
props[m[1]] = m[2];
|
|
19117
|
+
throw new VaultError(`Failed to read vault file: ${vaultPath}`);
|
|
19082
19118
|
}
|
|
19083
|
-
|
|
19084
|
-
|
|
19085
|
-
|
|
19086
|
-
|
|
19119
|
+
const salt = Buffer.from(vaultFile.salt, "hex");
|
|
19120
|
+
const kdfParams = vaultFile.kdf ?? { N: LEGACY_SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P };
|
|
19121
|
+
const key = deriveKey(passphrase, salt, kdfParams);
|
|
19122
|
+
let plaintext;
|
|
19123
|
+
try {
|
|
19124
|
+
plaintext = decrypt(key, vaultFile.iv, vaultFile.data, vaultFile.tag);
|
|
19125
|
+
} catch {
|
|
19126
|
+
throw new VaultError("Failed to decrypt vault. Wrong passphrase?");
|
|
19087
19127
|
}
|
|
19088
|
-
|
|
19089
|
-
}
|
|
19090
|
-
function readFdInode(fd) {
|
|
19128
|
+
let vaultData;
|
|
19091
19129
|
try {
|
|
19092
|
-
|
|
19093
|
-
return stat.ino;
|
|
19130
|
+
vaultData = JSON.parse(plaintext);
|
|
19094
19131
|
} catch {
|
|
19095
|
-
|
|
19132
|
+
throw new VaultError("Vault data is corrupted");
|
|
19096
19133
|
}
|
|
19134
|
+
return normalizeSecrets(vaultData.secrets ?? {});
|
|
19097
19135
|
}
|
|
19098
|
-
function
|
|
19099
|
-
|
|
19100
|
-
|
|
19101
|
-
return null;
|
|
19102
|
-
return handle.fd;
|
|
19103
|
-
}
|
|
19104
|
-
function identify(socketPath, socket, execFileSyncOverride) {
|
|
19105
|
-
if (process.platform !== "linux") {
|
|
19106
|
-
return null;
|
|
19136
|
+
function saveVault(passphrase, vaultPath, secrets) {
|
|
19137
|
+
if (!existsSync6(vaultPath)) {
|
|
19138
|
+
throw new VaultError(`Vault file not found: ${vaultPath}`);
|
|
19107
19139
|
}
|
|
19108
|
-
|
|
19109
|
-
|
|
19110
|
-
|
|
19111
|
-
|
|
19112
|
-
if (
|
|
19113
|
-
|
|
19114
|
-
if (cred !== null)
|
|
19115
|
-
pid = cred.pid;
|
|
19140
|
+
let releaseLock = null;
|
|
19141
|
+
try {
|
|
19142
|
+
releaseLock = acquireLock(vaultPath, { budgetMs: SAVE_VAULT_LOCK_RETRY_MS }).release;
|
|
19143
|
+
} catch (err) {
|
|
19144
|
+
if (err instanceof VaultBusyError) {
|
|
19145
|
+
throw new VaultError(err.message, { cause: err });
|
|
19116
19146
|
}
|
|
19147
|
+
throw err;
|
|
19117
19148
|
}
|
|
19118
|
-
|
|
19119
|
-
let
|
|
19149
|
+
try {
|
|
19150
|
+
let vaultFile;
|
|
19120
19151
|
try {
|
|
19121
|
-
|
|
19122
|
-
timeout: 200,
|
|
19123
|
-
encoding: "utf8"
|
|
19124
|
-
});
|
|
19125
|
-
ssOutput = typeof raw === "string" ? raw : raw.toString("utf8");
|
|
19152
|
+
vaultFile = JSON.parse(readFileSync6(vaultPath, "utf8"));
|
|
19126
19153
|
} catch {
|
|
19127
|
-
|
|
19128
|
-
}
|
|
19129
|
-
const rows = parseSsRows(ssOutput);
|
|
19130
|
-
let serverInode = null;
|
|
19131
|
-
if (socket !== undefined) {
|
|
19132
|
-
const fd = fdFromSocket(socket);
|
|
19133
|
-
if (fd !== null)
|
|
19134
|
-
serverInode = readFdInode(fd);
|
|
19154
|
+
throw new VaultError(`Failed to read vault file: ${vaultPath}`);
|
|
19135
19155
|
}
|
|
19136
|
-
|
|
19137
|
-
|
|
19138
|
-
|
|
19139
|
-
|
|
19140
|
-
|
|
19141
|
-
|
|
19142
|
-
|
|
19143
|
-
|
|
19144
|
-
|
|
19145
|
-
|
|
19146
|
-
|
|
19156
|
+
const salt = Buffer.from(vaultFile.salt, "hex");
|
|
19157
|
+
const key = deriveKey(passphrase, salt, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P });
|
|
19158
|
+
vaultFile.kdf = { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P };
|
|
19159
|
+
const vaultData = { secrets };
|
|
19160
|
+
const { iv, data, tag } = encrypt(key, JSON.stringify(vaultData));
|
|
19161
|
+
vaultFile.iv = iv;
|
|
19162
|
+
vaultFile.data = data;
|
|
19163
|
+
vaultFile.tag = tag;
|
|
19164
|
+
atomicWriteFileSync(vaultPath, JSON.stringify(vaultFile, null, 2), 384);
|
|
19165
|
+
} finally {
|
|
19166
|
+
if (releaseLock !== null) {
|
|
19167
|
+
try {
|
|
19168
|
+
releaseLock();
|
|
19169
|
+
} catch {}
|
|
19147
19170
|
}
|
|
19148
19171
|
}
|
|
19149
|
-
|
|
19150
|
-
|
|
19151
|
-
|
|
19152
|
-
|
|
19153
|
-
|
|
19154
|
-
|
|
19155
|
-
|
|
19156
|
-
if (brokerUid !== null && uid !== brokerUid) {
|
|
19157
|
-
process.stderr.write(`[vault-broker] peercred: UID mismatch — caller uid=${uid}, broker uid=${brokerUid}; denying
|
|
19158
|
-
`);
|
|
19159
|
-
return null;
|
|
19160
|
-
}
|
|
19161
|
-
const exe = readExe(pid);
|
|
19162
|
-
if (exe === null) {
|
|
19163
|
-
return null;
|
|
19164
|
-
}
|
|
19165
|
-
const cgroupClaim = readSystemdUnit(pid);
|
|
19166
|
-
let systemdUnit = null;
|
|
19167
|
-
if (cgroupClaim !== null) {
|
|
19168
|
-
if (verifySystemdUnit(cgroupClaim, runner)) {
|
|
19169
|
-
systemdUnit = cgroupClaim;
|
|
19170
|
-
} else {
|
|
19171
|
-
process.stderr.write(`[vault-broker] peercred: cgroup claims unit=${cgroupClaim} but systemd-user does not report it as loaded+running; treating caller as unidentified
|
|
19172
|
-
`);
|
|
19172
|
+
}
|
|
19173
|
+
function acquireVaultLock(vaultPath) {
|
|
19174
|
+
try {
|
|
19175
|
+
return acquireLock(vaultPath, { budgetMs: SAVE_VAULT_LOCK_RETRY_MS }).release;
|
|
19176
|
+
} catch (err) {
|
|
19177
|
+
if (err instanceof VaultBusyError) {
|
|
19178
|
+
throw new VaultError(err.message, { cause: err });
|
|
19173
19179
|
}
|
|
19180
|
+
throw err;
|
|
19174
19181
|
}
|
|
19175
|
-
return { uid, pid, exe, systemdUnit };
|
|
19176
19182
|
}
|
|
19177
19183
|
|
|
19184
|
+
// src/vault/resolver.ts
|
|
19185
|
+
init_loader();
|
|
19186
|
+
|
|
19187
|
+
// src/vault/broker/client.ts
|
|
19188
|
+
import { homedir as homedir3 } from "node:os";
|
|
19189
|
+
import { join as join3 } from "node:path";
|
|
19190
|
+
|
|
19178
19191
|
// src/vault/broker/protocol.ts
|
|
19192
|
+
init_zod();
|
|
19179
19193
|
var MAX_FRAME_BYTES = 64 * 1024;
|
|
19180
19194
|
var AgentNameSchema = exports_external.string().min(1).max(64, "agent name max 64 chars").regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/, "agent name must be kebab-case ASCII (alnum + _- only, first char alnum)").refine((s) => !isReservedAgentName(s), {
|
|
19181
19195
|
message: "agent name is reserved"
|
|
@@ -19563,17 +19577,6 @@ init_merge();
|
|
|
19563
19577
|
init_paths();
|
|
19564
19578
|
|
|
19565
19579
|
// src/agents/compose.ts
|
|
19566
|
-
var AGENT_UID_MIN = 10001;
|
|
19567
|
-
var AGENT_UID_MAX = 10999;
|
|
19568
|
-
function allocateAgentUid(name) {
|
|
19569
|
-
if (isReservedAgentName(name)) {
|
|
19570
|
-
throw new Error(`agent name '${name}' is reserved by switchroom for another identity kind ` + `(see vault/broker/peercred.ts:RESERVED_AGENT_NAMES). Pick a different name.`);
|
|
19571
|
-
}
|
|
19572
|
-
const hash = createHash("sha256").update(name).digest();
|
|
19573
|
-
const u32 = hash.readUInt32BE(0);
|
|
19574
|
-
const range = AGENT_UID_MAX - AGENT_UID_MIN + 1;
|
|
19575
|
-
return AGENT_UID_MIN + u32 % range;
|
|
19576
|
-
}
|
|
19577
19580
|
var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
|
|
19578
19581
|
|
|
19579
19582
|
// src/vault/broker/server.ts
|