switchroom 0.18.14 → 0.18.17
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 +3 -0
- package/dist/auth-broker/index.js +473 -49
- package/dist/cli/notion-write-pretool.mjs +3 -0
- package/dist/cli/switchroom.js +1200 -1067
- package/dist/host-control/main.js +56 -51
- package/dist/vault/approvals/kernel-server.js +19 -12
- package/dist/vault/broker/server.js +675 -668
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +81 -139
- package/telegram-plugin/dist/bridge/bridge.js +21 -0
- package/telegram-plugin/dist/gateway/gateway.js +531 -259
- package/telegram-plugin/dist/server.js +22 -1
- package/telegram-plugin/draft-stream.ts +78 -3
- package/telegram-plugin/gateway/bridge-dead-watchdog.ts +3 -4
- package/telegram-plugin/gateway/effort-command.ts +9 -7
- package/telegram-plugin/gateway/gateway.ts +310 -219
- package/telegram-plugin/gateway/litellm-local-notice-wiring.ts +200 -0
- package/telegram-plugin/gateway/model-command.ts +96 -18
- package/telegram-plugin/gateway/pending-session-command.ts +10 -8
- package/telegram-plugin/gateway/session-model-file.ts +38 -172
- package/telegram-plugin/litellm-local-notice.ts +189 -0
- package/telegram-plugin/model-unavailable.ts +214 -0
- package/telegram-plugin/quota-watch.ts +16 -4
- package/telegram-plugin/runtime-metrics.ts +47 -0
- package/telegram-plugin/send-gate-degraded.test.ts +9 -7
- package/telegram-plugin/send-gate.ts +34 -4
- package/telegram-plugin/session-tail.ts +14 -2
- package/telegram-plugin/stream-controller.ts +143 -20
- package/telegram-plugin/stream-reply-handler.ts +12 -2
- package/telegram-plugin/tests/bot-api.harness.ts +7 -2
- package/telegram-plugin/tests/draft-stream.test.ts +110 -1
- package/telegram-plugin/tests/effort-command.test.ts +4 -4
- package/telegram-plugin/tests/flood-windows-persistence.test.ts +2 -2
- package/telegram-plugin/tests/gateway-pending-command-wiring.test.ts +33 -19
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +47 -127
- package/telegram-plugin/tests/litellm-local-notice.test.ts +417 -0
- package/telegram-plugin/tests/model-command.test.ts +84 -1
- 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/quota-watch.test.ts +21 -0
- package/telegram-plugin/tests/reaction-gate-routing.test.ts +2 -2
- package/telegram-plugin/tests/runtime-metrics.test.ts +24 -0
- package/telegram-plugin/tests/session-model-file.test.ts +7 -155
- package/telegram-plugin/tests/stream-controller-send-gate.test.ts +521 -0
- package/telegram-plugin/tests/stream-reply-handler.test.ts +44 -0
- package/telegram-plugin/tests/throttle-tier.test.ts +176 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +207 -0
- package/telegram-plugin/throttle-tier.ts +98 -1
- package/telegram-plugin/worker-activity-feed.ts +83 -8
|
@@ -4281,6 +4281,9 @@ var init_schema = __esm(() => {
|
|
|
4281
4281
|
window_ms: exports_external.number().int().nonnegative().optional().describe("Sliding-window (ms) for merging consecutive inbound messages from " + "the same sender+topic into ONE Claude turn. Each new message resets " + "the timer; the turn starts once the sender pauses for this long. " + "Catches forwarded bursts, pasted text the Telegram client split " + "into several messages, and mixed text+media forwards. Default 500. " + "Set 0 to disable (every message becomes its own turn). Raise for " + "users who think in multiple short messages; the trade-off is the " + "single-message turn start is delayed by this much (the \uD83D\uDC40 ack still " + "fires immediately, so perceived latency is unchanged)."),
|
|
4282
4282
|
max_attachments: exports_external.number().int().positive().optional().describe("Maximum number of media attachments carried into ONE coalesced " + "Claude turn. Default 10 — a full Telegram album (media_group caps " + "at 10) or a text+multi-image forwarded burst arrives as a single " + "turn; the agent sees numbered attachment fields (image_path, " + "image_path_2, …). Set 1 to restore the historical " + "single-attachment-per-turn behaviour. Excess attachments beyond " + "the cap spill into the next turn. Each attachment is downloaded, " + "so a high cap on a slow link delays turn start.")
|
|
4283
4283
|
}).optional().describe("Inbound coalescing — how the gateway groups rapid consecutive messages " + "into a single turn so a forwarded album or split paste doesn't fan out " + "into N separate turns. Cascades from defaults.channels.telegram.coalesce."),
|
|
4284
|
+
litellm_notice: exports_external.object({
|
|
4285
|
+
window_ms: exports_external.number().int().positive().optional().describe("Per-agent cooldown window (ms) for the litellm-local 429 notice. " + "When the agent trips the LiteLLM proxy's OWN tpm_limit/rpm_limit " + "cap (a `litellm-local` classified 429 — see docs/auth.md § " + "LiteLLM-proxy-local 429s), the gateway posts ONE calm notice " + "naming the fleet token limiter, then counts further hits silently " + "for this long; the first notice after the window expires says " + "how many were absorbed. Default 900000 (15 min). Invalid values " + "fall back to the default.")
|
|
4286
|
+
}).optional().describe("Debounce tuning for the litellm-local throttle notice — the calm " + "'fleet token limiter engaged' message posted when the LiteLLM " + "proxy's own rate cap trips (never an Anthropic account limit). " + "Cascades from defaults.channels.telegram.litellm_notice."),
|
|
4284
4287
|
interrupt: exports_external.object({
|
|
4285
4288
|
safe_boundary: exports_external.boolean().optional().describe("When true (the default), a `!`-prefix interrupt that arrives while " + "the agent is mid-tool-call is DEFERRED: the SIGINT and the " + "replacement turn wait until the in-flight tool call finishes (a " + "clean boundary) instead of C-c'ing the agent mid-write/mid-bash. If " + "no tool is in flight the interrupt still fires immediately. Bounded " + "by max_wait_ms so a long tool never strands the user. Set false to " + "fire synchronously the moment `!` is received (historical " + "behaviour). Rapid repeated `!` while one is pending coalesce into a " + "single deferred interrupt carrying the latest body."),
|
|
4286
4289
|
max_wait_ms: exports_external.number().int().positive().optional().describe("Upper bound (ms) the gateway waits for a safe boundary before firing " + "a deferred `!` interrupt anyway. Only consulted when safe_boundary is " + "true. Default 8000. Keep it short — the user explicitly asked to " + "interrupt, so a long in-flight tool shouldn't ghost them; the cap " + "trades a tiny risk of a mid-tool C-c for a guaranteed response.")
|
|
@@ -12179,7 +12182,7 @@ var init_overlay_schema = __esm(() => {
|
|
|
12179
12182
|
});
|
|
12180
12183
|
|
|
12181
12184
|
// src/config/overlay-loader.ts
|
|
12182
|
-
import { existsSync as existsSync2, readFileSync, readdirSync, statSync } from "node:fs";
|
|
12185
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "node:fs";
|
|
12183
12186
|
import { basename, resolve as resolve2 } from "node:path";
|
|
12184
12187
|
function deriveOverlayTitle(raw, fileName) {
|
|
12185
12188
|
const titleFromComment = raw.match(/^#[^\S\n]*name:[^\S\n]*(\S.*?)[^\S\n]*$/m)?.[1];
|
|
@@ -12243,7 +12246,7 @@ function applyAgentOverlays(config) {
|
|
|
12243
12246
|
const merged = [...agentCfg.schedule ?? []];
|
|
12244
12247
|
for (const file of files) {
|
|
12245
12248
|
try {
|
|
12246
|
-
const raw =
|
|
12249
|
+
const raw = readFileSync2(file, "utf-8");
|
|
12247
12250
|
const parsed = import_yaml.parse(raw);
|
|
12248
12251
|
const doc = OverlayDocSchema.parse(parsed);
|
|
12249
12252
|
const title = deriveOverlayTitle(raw, basename(file));
|
|
@@ -12284,7 +12287,7 @@ function applyAgentOverlays(config) {
|
|
|
12284
12287
|
const seen = new Set(merged);
|
|
12285
12288
|
for (const file of skillFiles) {
|
|
12286
12289
|
try {
|
|
12287
|
-
const raw =
|
|
12290
|
+
const raw = readFileSync2(file, "utf-8");
|
|
12288
12291
|
const parsed = import_yaml.parse(raw);
|
|
12289
12292
|
const doc = OverlayDocSchema.parse(parsed);
|
|
12290
12293
|
for (const skillName of doc.skills ?? []) {
|
|
@@ -12359,7 +12362,7 @@ __export(exports_loader, {
|
|
|
12359
12362
|
findConfigFile: () => findConfigFile,
|
|
12360
12363
|
ConfigError: () => ConfigError
|
|
12361
12364
|
});
|
|
12362
|
-
import { readFileSync as
|
|
12365
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "node:fs";
|
|
12363
12366
|
import { homedir } from "node:os";
|
|
12364
12367
|
import { resolve as resolve3 } from "node:path";
|
|
12365
12368
|
function formatZodErrors(error) {
|
|
@@ -12439,7 +12442,7 @@ function loadConfig(configPath) {
|
|
|
12439
12442
|
}
|
|
12440
12443
|
let raw;
|
|
12441
12444
|
try {
|
|
12442
|
-
raw =
|
|
12445
|
+
raw = readFileSync3(filePath, "utf-8");
|
|
12443
12446
|
} catch (err) {
|
|
12444
12447
|
throw new ConfigError(`Failed to read config file: ${filePath}`, [
|
|
12445
12448
|
` ${err.message}`
|
|
@@ -18096,249 +18099,597 @@ import * as net from "node:net";
|
|
|
18096
18099
|
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";
|
|
18097
18100
|
|
|
18098
18101
|
// src/agents/compose.ts
|
|
18099
|
-
import { createHash } from "node:crypto";
|
|
18100
18102
|
init_schema();
|
|
18101
18103
|
|
|
18102
18104
|
// src/agents/scaffold.ts
|
|
18103
18105
|
import { join as join4, resolve as resolve6 } from "node:path";
|
|
18104
18106
|
init_atomic();
|
|
18105
|
-
init_schema();
|
|
18106
|
-
|
|
18107
|
-
// src/config/users.ts
|
|
18108
|
-
init_merge();
|
|
18109
|
-
|
|
18110
|
-
// src/agents/scaffold.ts
|
|
18111
|
-
init_merge();
|
|
18112
|
-
|
|
18113
|
-
// src/config/timezone.ts
|
|
18114
|
-
var CONTAINER_DEFAULT_UTC_ZONES = new Set([
|
|
18115
|
-
"UTC",
|
|
18116
|
-
"Etc/UTC",
|
|
18117
|
-
"Etc/Universal",
|
|
18118
|
-
"Universal"
|
|
18119
|
-
]);
|
|
18120
18107
|
|
|
18121
|
-
// src/
|
|
18122
|
-
import {
|
|
18123
|
-
import { homedir as homedir2 } from "node:os";
|
|
18124
|
-
|
|
18125
|
-
// src/cli/helpers.ts
|
|
18126
|
-
init_loader();
|
|
18108
|
+
// src/agents/agent-uid.ts
|
|
18109
|
+
import { createHash } from "node:crypto";
|
|
18127
18110
|
|
|
18128
|
-
// src/
|
|
18129
|
-
|
|
18130
|
-
|
|
18111
|
+
// src/vault/broker/peercred.ts
|
|
18112
|
+
import { execFileSync } from "node:child_process";
|
|
18113
|
+
import { readFileSync, readlinkSync, fstatSync } from "node:fs";
|
|
18131
18114
|
|
|
18132
|
-
// src/
|
|
18133
|
-
function
|
|
18134
|
-
if (
|
|
18135
|
-
return
|
|
18136
|
-
|
|
18137
|
-
|
|
18138
|
-
|
|
18139
|
-
|
|
18140
|
-
|
|
18141
|
-
|
|
18142
|
-
|
|
18143
|
-
|
|
18144
|
-
|
|
18145
|
-
|
|
18146
|
-
|
|
18115
|
+
// src/vault/broker/peercred-ffi.ts
|
|
18116
|
+
function getPeerCred(fd) {
|
|
18117
|
+
if (process.platform !== "linux")
|
|
18118
|
+
return null;
|
|
18119
|
+
try {
|
|
18120
|
+
const ffi = __require("bun:ffi");
|
|
18121
|
+
const { dlopen, FFIType, ptr } = ffi;
|
|
18122
|
+
const SOL_SOCKET = 1;
|
|
18123
|
+
const SO_PEERCRED = 17;
|
|
18124
|
+
const UCRED_SIZE = 12;
|
|
18125
|
+
const cache = getPeerCred;
|
|
18126
|
+
const lib = cache._lib ?? (() => {
|
|
18127
|
+
const candidates = ["libc.so.6", "libc.so"];
|
|
18128
|
+
const symbolSpec = {
|
|
18129
|
+
getsockopt: {
|
|
18130
|
+
args: [FFIType.i32, FFIType.i32, FFIType.i32, FFIType.ptr, FFIType.ptr],
|
|
18131
|
+
returns: FFIType.i32
|
|
18132
|
+
}
|
|
18133
|
+
};
|
|
18134
|
+
const errors2 = [];
|
|
18135
|
+
for (const name of candidates) {
|
|
18136
|
+
try {
|
|
18137
|
+
const opened = dlopen(name, symbolSpec);
|
|
18138
|
+
cache._lib = opened;
|
|
18139
|
+
return opened;
|
|
18140
|
+
} catch (e) {
|
|
18141
|
+
errors2.push(`${name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
18142
|
+
}
|
|
18143
|
+
}
|
|
18144
|
+
process.stderr.write(`[vault-broker] peercred-ffi: dlopen failed for all libc candidates ` + `(${errors2.join("; ")}); falling back to ss-parsing.
|
|
18145
|
+
`);
|
|
18146
|
+
throw new Error("no libc candidate could be opened");
|
|
18147
|
+
})();
|
|
18148
|
+
const credBuf = new ArrayBuffer(UCRED_SIZE);
|
|
18149
|
+
const lenBuf = new Uint32Array(1);
|
|
18150
|
+
lenBuf[0] = UCRED_SIZE;
|
|
18151
|
+
const rc = lib.symbols.getsockopt(fd, SOL_SOCKET, SO_PEERCRED, ptr(credBuf), ptr(lenBuf.buffer));
|
|
18152
|
+
if (rc !== 0)
|
|
18153
|
+
return null;
|
|
18154
|
+
if (lenBuf[0] !== UCRED_SIZE)
|
|
18155
|
+
return null;
|
|
18156
|
+
const view = new DataView(credBuf);
|
|
18157
|
+
return {
|
|
18158
|
+
pid: view.getInt32(0, true),
|
|
18159
|
+
uid: view.getInt32(4, true),
|
|
18160
|
+
gid: view.getInt32(8, true)
|
|
18161
|
+
};
|
|
18162
|
+
} catch {
|
|
18147
18163
|
return null;
|
|
18148
|
-
const key = value.slice("vault:".length).split("#")[0];
|
|
18149
|
-
return key.length > 0 ? key : null;
|
|
18150
|
-
}
|
|
18151
|
-
function isGoogleClientCredentialKeyForAgent(config, agentName, key) {
|
|
18152
|
-
if (!agentName || !key)
|
|
18153
|
-
return false;
|
|
18154
|
-
const agentConfig = config.agents?.[agentName];
|
|
18155
|
-
if (!agentConfig)
|
|
18156
|
-
return false;
|
|
18157
|
-
if (agentConfig.mcp_servers?.["gdrive"] === false) {
|
|
18158
|
-
return false;
|
|
18159
|
-
}
|
|
18160
|
-
const account = agentConfig.google_workspace?.account;
|
|
18161
|
-
if (!shouldEmitGdriveMcp(agentName, account, config.google_accounts)) {
|
|
18162
|
-
return false;
|
|
18163
18164
|
}
|
|
18164
|
-
|
|
18165
|
-
|
|
18166
|
-
|
|
18167
|
-
|
|
18168
|
-
|
|
18169
|
-
|
|
18165
|
+
}
|
|
18166
|
+
|
|
18167
|
+
// src/broker-common/peercred-path.ts
|
|
18168
|
+
function matchSocketName(socketPath, patterns, maxNameLen) {
|
|
18169
|
+
if (typeof socketPath !== "string" || socketPath.length === 0)
|
|
18170
|
+
return null;
|
|
18171
|
+
let name = null;
|
|
18172
|
+
for (const re of patterns) {
|
|
18173
|
+
const m = socketPath.match(re);
|
|
18174
|
+
if (m && typeof m[1] === "string") {
|
|
18175
|
+
name = m[1];
|
|
18176
|
+
break;
|
|
18177
|
+
}
|
|
18170
18178
|
}
|
|
18171
|
-
|
|
18179
|
+
if (name === null || name.length === 0)
|
|
18180
|
+
return null;
|
|
18181
|
+
if (maxNameLen !== undefined && name.length > maxNameLen)
|
|
18182
|
+
return null;
|
|
18183
|
+
return name;
|
|
18184
|
+
}
|
|
18185
|
+
function parseSocketIdentity(socketPath, spec) {
|
|
18186
|
+
const name = matchSocketName(socketPath, spec.patterns, spec.maxNameLen);
|
|
18187
|
+
if (name === null)
|
|
18188
|
+
return null;
|
|
18189
|
+
const operatorName = spec.operatorName ?? "operator";
|
|
18190
|
+
if (name === operatorName)
|
|
18191
|
+
return { kind: "operator" };
|
|
18192
|
+
if (spec.reservedNames.has(name))
|
|
18193
|
+
return null;
|
|
18194
|
+
return { kind: "agent", name };
|
|
18172
18195
|
}
|
|
18173
18196
|
|
|
18174
|
-
// src/vault/broker/
|
|
18175
|
-
var
|
|
18176
|
-
|
|
18177
|
-
|
|
18178
|
-
|
|
18179
|
-
|
|
18180
|
-
|
|
18181
|
-
|
|
18182
|
-
|
|
18183
|
-
if ((agentConfig?.mcp_servers ?? {})["webkite"] === false)
|
|
18184
|
-
return false;
|
|
18185
|
-
return true;
|
|
18197
|
+
// src/vault/broker/peercred.ts
|
|
18198
|
+
var SOCKET_PATH_AGENT_RE = /^\/run\/switchroom\/broker\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\.sock$/;
|
|
18199
|
+
var SOCKET_PATH_AGENT_SUBDIR_RE = /^\/run\/switchroom\/broker\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\/sock$/;
|
|
18200
|
+
var RESERVED_AGENT_NAMES = new Set(["operator", "hostd"]);
|
|
18201
|
+
function socketPathToIdentity(socketPath) {
|
|
18202
|
+
return parseSocketIdentity(socketPath, {
|
|
18203
|
+
patterns: [SOCKET_PATH_AGENT_RE, SOCKET_PATH_AGENT_SUBDIR_RE],
|
|
18204
|
+
reservedNames: RESERVED_AGENT_NAMES
|
|
18205
|
+
});
|
|
18186
18206
|
}
|
|
18187
|
-
function
|
|
18188
|
-
|
|
18189
|
-
|
|
18207
|
+
function socketPathToAgent(socketPath) {
|
|
18208
|
+
const identity = socketPathToIdentity(socketPath);
|
|
18209
|
+
return identity?.kind === "agent" ? identity.name : null;
|
|
18210
|
+
}
|
|
18211
|
+
function isReservedAgentName(name) {
|
|
18212
|
+
return RESERVED_AGENT_NAMES.has(name);
|
|
18213
|
+
}
|
|
18214
|
+
function unlockSocketFor(dataSocketPath) {
|
|
18215
|
+
if (dataSocketPath.endsWith("/sock")) {
|
|
18216
|
+
return dataSocketPath.slice(0, -"/sock".length) + "/unlock";
|
|
18190
18217
|
}
|
|
18191
|
-
|
|
18192
|
-
|
|
18193
|
-
|
|
18194
|
-
|
|
18195
|
-
|
|
18196
|
-
|
|
18197
|
-
|
|
18218
|
+
return dataSocketPath.replace(/\.sock$/, ".unlock.sock");
|
|
18219
|
+
}
|
|
18220
|
+
function parseSsRows(output) {
|
|
18221
|
+
const rows = [];
|
|
18222
|
+
const lines = output.split(`
|
|
18223
|
+
`);
|
|
18224
|
+
for (const line of lines) {
|
|
18225
|
+
if (!line.trim() || line.startsWith("Netid"))
|
|
18226
|
+
continue;
|
|
18227
|
+
const tokens = line.split(/\s+/).filter((t) => t.length > 0);
|
|
18228
|
+
if (tokens.length < 8)
|
|
18229
|
+
continue;
|
|
18230
|
+
const localAddr = tokens[4];
|
|
18231
|
+
const localInode = tokens[5];
|
|
18232
|
+
const peerAddr = tokens[6];
|
|
18233
|
+
const peerInode = tokens[7];
|
|
18234
|
+
const usersToken = tokens.slice(8).join(" ");
|
|
18235
|
+
const m = usersToken.match(/users:\(\(".*?",pid=(\d+),fd=\d+\)\)/);
|
|
18236
|
+
const pid = m ? parseInt(m[1], 10) : null;
|
|
18237
|
+
rows.push({ localAddr, localInode, peerAddr, peerInode, pid });
|
|
18198
18238
|
}
|
|
18199
|
-
|
|
18200
|
-
|
|
18201
|
-
|
|
18202
|
-
|
|
18203
|
-
|
|
18204
|
-
|
|
18239
|
+
return rows;
|
|
18240
|
+
}
|
|
18241
|
+
function findClientPids(rows, socketPath) {
|
|
18242
|
+
const pids = [];
|
|
18243
|
+
for (const serverRow of rows) {
|
|
18244
|
+
if (serverRow.localAddr !== socketPath)
|
|
18245
|
+
continue;
|
|
18246
|
+
for (const clientRow of rows) {
|
|
18247
|
+
if (clientRow.localAddr !== "*")
|
|
18248
|
+
continue;
|
|
18249
|
+
if (clientRow.localInode !== serverRow.peerInode)
|
|
18250
|
+
continue;
|
|
18251
|
+
if (clientRow.pid === null)
|
|
18252
|
+
continue;
|
|
18253
|
+
pids.push(clientRow.pid);
|
|
18254
|
+
break;
|
|
18205
18255
|
}
|
|
18206
18256
|
}
|
|
18207
|
-
return
|
|
18257
|
+
return pids;
|
|
18208
18258
|
}
|
|
18209
|
-
function
|
|
18210
|
-
|
|
18211
|
-
|
|
18212
|
-
|
|
18213
|
-
|
|
18214
|
-
|
|
18215
|
-
|
|
18216
|
-
|
|
18217
|
-
|
|
18218
|
-
|
|
18219
|
-
|
|
18259
|
+
function findClientPidByServerInode(rows, socketPath, serverInode) {
|
|
18260
|
+
const serverInodeStr = String(serverInode);
|
|
18261
|
+
for (const serverRow of rows) {
|
|
18262
|
+
if (serverRow.localAddr !== socketPath)
|
|
18263
|
+
continue;
|
|
18264
|
+
if (serverRow.localInode !== serverInodeStr)
|
|
18265
|
+
continue;
|
|
18266
|
+
for (const clientRow of rows) {
|
|
18267
|
+
if (clientRow.localAddr !== "*")
|
|
18268
|
+
continue;
|
|
18269
|
+
if (clientRow.localInode !== serverRow.peerInode)
|
|
18270
|
+
continue;
|
|
18271
|
+
if (clientRow.pid === null)
|
|
18272
|
+
continue;
|
|
18273
|
+
return clientRow.pid;
|
|
18274
|
+
}
|
|
18275
|
+
return null;
|
|
18220
18276
|
}
|
|
18221
|
-
|
|
18222
|
-
|
|
18277
|
+
return null;
|
|
18278
|
+
}
|
|
18279
|
+
function readUid(pid) {
|
|
18280
|
+
try {
|
|
18281
|
+
const status = readFileSync(`/proc/${pid}/status`, "utf8");
|
|
18282
|
+
const m = status.match(/^Uid:\s+(\d+)/m);
|
|
18283
|
+
if (!m)
|
|
18284
|
+
return null;
|
|
18285
|
+
return parseInt(m[1], 10);
|
|
18286
|
+
} catch {
|
|
18287
|
+
return null;
|
|
18223
18288
|
}
|
|
18224
|
-
|
|
18225
|
-
|
|
18289
|
+
}
|
|
18290
|
+
function readExe(pid) {
|
|
18291
|
+
try {
|
|
18292
|
+
return readlinkSync(`/proc/${pid}/exe`);
|
|
18293
|
+
} catch {
|
|
18294
|
+
return null;
|
|
18226
18295
|
}
|
|
18227
|
-
|
|
18228
|
-
|
|
18229
|
-
|
|
18230
|
-
const
|
|
18231
|
-
|
|
18232
|
-
|
|
18296
|
+
}
|
|
18297
|
+
function readSystemdUnit(pid) {
|
|
18298
|
+
try {
|
|
18299
|
+
const content = readFileSync(`/proc/${pid}/cgroup`, "utf8");
|
|
18300
|
+
const lines = content.split(`
|
|
18301
|
+
`);
|
|
18302
|
+
for (const line of lines) {
|
|
18303
|
+
if (!line.trim())
|
|
18304
|
+
continue;
|
|
18305
|
+
const parts = line.split(":");
|
|
18306
|
+
if (parts.length < 3)
|
|
18307
|
+
continue;
|
|
18308
|
+
const controller = parts[1];
|
|
18309
|
+
const isV2 = parts[0] === "0" && controller === "";
|
|
18310
|
+
const isV1Systemd = controller === "name=systemd";
|
|
18311
|
+
if (!isV2 && !isV1Systemd)
|
|
18312
|
+
continue;
|
|
18313
|
+
const cgroupPath = parts.slice(2).join(":");
|
|
18314
|
+
const segments = cgroupPath.split("/");
|
|
18315
|
+
const lastSegment = segments[segments.length - 1];
|
|
18316
|
+
if (!lastSegment)
|
|
18317
|
+
continue;
|
|
18318
|
+
if (/^switchroom-[a-zA-Z0-9_-]+(-cron-\d+)?\.service$/.test(lastSegment)) {
|
|
18319
|
+
return lastSegment;
|
|
18320
|
+
}
|
|
18233
18321
|
}
|
|
18322
|
+
return null;
|
|
18323
|
+
} catch {
|
|
18324
|
+
return null;
|
|
18234
18325
|
}
|
|
18235
|
-
|
|
18236
|
-
|
|
18237
|
-
|
|
18238
|
-
|
|
18239
|
-
|
|
18240
|
-
|
|
18241
|
-
|
|
18242
|
-
|
|
18243
|
-
|
|
18244
|
-
|
|
18245
|
-
|
|
18246
|
-
|
|
18247
|
-
|
|
18248
|
-
return { allow: true };
|
|
18249
|
-
}
|
|
18326
|
+
}
|
|
18327
|
+
function verifySystemdUnit(unitName, runner) {
|
|
18328
|
+
let raw;
|
|
18329
|
+
try {
|
|
18330
|
+
const out = runner("systemctl", [
|
|
18331
|
+
"--user",
|
|
18332
|
+
"show",
|
|
18333
|
+
unitName,
|
|
18334
|
+
"--property=LoadState,ActiveState"
|
|
18335
|
+
], { timeout: 500, encoding: "utf8" });
|
|
18336
|
+
raw = typeof out === "string" ? out : out.toString("utf8");
|
|
18337
|
+
} catch {
|
|
18338
|
+
return false;
|
|
18250
18339
|
}
|
|
18251
|
-
const
|
|
18252
|
-
const
|
|
18253
|
-
|
|
18254
|
-
|
|
18255
|
-
|
|
18256
|
-
|
|
18257
|
-
];
|
|
18258
|
-
if (standingSecrets.includes(key)) {
|
|
18259
|
-
return { allow: true };
|
|
18340
|
+
const props = {};
|
|
18341
|
+
for (const line of raw.split(`
|
|
18342
|
+
`)) {
|
|
18343
|
+
const m = line.match(/^([A-Za-z]+)=(.*)$/);
|
|
18344
|
+
if (m)
|
|
18345
|
+
props[m[1]] = m[2];
|
|
18260
18346
|
}
|
|
18261
|
-
|
|
18262
|
-
|
|
18263
|
-
|
|
18264
|
-
|
|
18265
|
-
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`
|
|
18266
|
-
};
|
|
18347
|
+
if (props.LoadState !== "loaded")
|
|
18348
|
+
return false;
|
|
18349
|
+
if (props.ActiveState !== "active" && props.ActiveState !== "activating") {
|
|
18350
|
+
return false;
|
|
18267
18351
|
}
|
|
18268
|
-
|
|
18269
|
-
|
|
18270
|
-
|
|
18271
|
-
|
|
18272
|
-
|
|
18352
|
+
return true;
|
|
18353
|
+
}
|
|
18354
|
+
function readFdInode(fd) {
|
|
18355
|
+
try {
|
|
18356
|
+
const stat = fstatSync(fd);
|
|
18357
|
+
return stat.ino;
|
|
18358
|
+
} catch {
|
|
18359
|
+
return null;
|
|
18273
18360
|
}
|
|
18274
|
-
return {
|
|
18275
|
-
allow: false,
|
|
18276
|
-
reason: `key '${key}' not in ACL for agent '${agentName}'`
|
|
18277
|
-
};
|
|
18278
18361
|
}
|
|
18279
|
-
function
|
|
18280
|
-
const
|
|
18281
|
-
if (!
|
|
18362
|
+
function fdFromSocket(socket) {
|
|
18363
|
+
const handle = socket._handle;
|
|
18364
|
+
if (!handle || typeof handle.fd !== "number" || handle.fd < 0)
|
|
18282
18365
|
return null;
|
|
18283
|
-
return
|
|
18366
|
+
return handle.fd;
|
|
18284
18367
|
}
|
|
18285
|
-
function
|
|
18286
|
-
|
|
18287
|
-
|
|
18288
|
-
const accountEntry = accounts[accountKey] ?? accounts[account];
|
|
18289
|
-
if (!accountEntry) {
|
|
18290
|
-
return {
|
|
18291
|
-
allow: false,
|
|
18292
|
-
reason: `google_accounts['${account}'] not configured (key '${key}')`
|
|
18293
|
-
};
|
|
18368
|
+
function identify(socketPath, socket, execFileSyncOverride) {
|
|
18369
|
+
if (process.platform !== "linux") {
|
|
18370
|
+
return null;
|
|
18294
18371
|
}
|
|
18295
|
-
const
|
|
18296
|
-
|
|
18297
|
-
|
|
18298
|
-
|
|
18299
|
-
|
|
18300
|
-
|
|
18372
|
+
const runner = execFileSyncOverride ?? execFileSync;
|
|
18373
|
+
let pid = null;
|
|
18374
|
+
if (socket !== undefined) {
|
|
18375
|
+
const fd = fdFromSocket(socket);
|
|
18376
|
+
if (fd !== null) {
|
|
18377
|
+
const cred = getPeerCred(fd);
|
|
18378
|
+
if (cred !== null)
|
|
18379
|
+
pid = cred.pid;
|
|
18380
|
+
}
|
|
18301
18381
|
}
|
|
18302
|
-
if (
|
|
18303
|
-
|
|
18304
|
-
|
|
18305
|
-
|
|
18306
|
-
|
|
18382
|
+
if (pid === null) {
|
|
18383
|
+
let ssOutput;
|
|
18384
|
+
try {
|
|
18385
|
+
const raw = runner("ss", ["-xpn"], {
|
|
18386
|
+
timeout: 200,
|
|
18387
|
+
encoding: "utf8"
|
|
18388
|
+
});
|
|
18389
|
+
ssOutput = typeof raw === "string" ? raw : raw.toString("utf8");
|
|
18390
|
+
} catch {
|
|
18391
|
+
return null;
|
|
18392
|
+
}
|
|
18393
|
+
const rows = parseSsRows(ssOutput);
|
|
18394
|
+
let serverInode = null;
|
|
18395
|
+
if (socket !== undefined) {
|
|
18396
|
+
const fd = fdFromSocket(socket);
|
|
18397
|
+
if (fd !== null)
|
|
18398
|
+
serverInode = readFdInode(fd);
|
|
18399
|
+
}
|
|
18400
|
+
if (serverInode !== null) {
|
|
18401
|
+
pid = findClientPidByServerInode(rows, socketPath, serverInode);
|
|
18402
|
+
} else {
|
|
18403
|
+
const clientPids = findClientPids(rows, socketPath);
|
|
18404
|
+
if (clientPids.length === 0)
|
|
18405
|
+
return null;
|
|
18406
|
+
if (clientPids.length > 1) {
|
|
18407
|
+
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.)
|
|
18408
|
+
`);
|
|
18409
|
+
}
|
|
18410
|
+
pid = clientPids[0];
|
|
18411
|
+
}
|
|
18307
18412
|
}
|
|
18308
|
-
|
|
18413
|
+
if (pid === null)
|
|
18414
|
+
return null;
|
|
18415
|
+
const uid = readUid(pid);
|
|
18416
|
+
if (uid === null) {
|
|
18417
|
+
return null;
|
|
18418
|
+
}
|
|
18419
|
+
const brokerUid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
18420
|
+
if (brokerUid !== null && uid !== brokerUid) {
|
|
18421
|
+
process.stderr.write(`[vault-broker] peercred: UID mismatch — caller uid=${uid}, broker uid=${brokerUid}; denying
|
|
18422
|
+
`);
|
|
18423
|
+
return null;
|
|
18424
|
+
}
|
|
18425
|
+
const exe = readExe(pid);
|
|
18426
|
+
if (exe === null) {
|
|
18427
|
+
return null;
|
|
18428
|
+
}
|
|
18429
|
+
const cgroupClaim = readSystemdUnit(pid);
|
|
18430
|
+
let systemdUnit = null;
|
|
18431
|
+
if (cgroupClaim !== null) {
|
|
18432
|
+
if (verifySystemdUnit(cgroupClaim, runner)) {
|
|
18433
|
+
systemdUnit = cgroupClaim;
|
|
18434
|
+
} else {
|
|
18435
|
+
process.stderr.write(`[vault-broker] peercred: cgroup claims unit=${cgroupClaim} but systemd-user does not report it as loaded+running; treating caller as unidentified
|
|
18436
|
+
`);
|
|
18437
|
+
}
|
|
18438
|
+
}
|
|
18439
|
+
return { uid, pid, exe, systemdUnit };
|
|
18309
18440
|
}
|
|
18310
18441
|
|
|
18311
|
-
// src/
|
|
18312
|
-
|
|
18313
|
-
|
|
18314
|
-
|
|
18315
|
-
|
|
18316
|
-
|
|
18317
|
-
// src/agents/profiles.ts
|
|
18318
|
-
var import_handlebars = __toESM(require_lib(), 1);
|
|
18319
|
-
import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2, copyFileSync, mkdirSync, realpathSync } from "node:fs";
|
|
18320
|
-
import { resolve as resolve4, join as join2, sep as pathSep } from "node:path";
|
|
18321
|
-
var PROFILES_ROOT = resolve4(import.meta.dirname, "../../profiles");
|
|
18322
|
-
import_handlebars.default.registerHelper("json", (value) => {
|
|
18323
|
-
return new import_handlebars.default.SafeString(JSON.stringify(value, null, 2));
|
|
18324
|
-
});
|
|
18325
|
-
import_handlebars.default.registerHelper("isNumber", (value) => {
|
|
18326
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
18327
|
-
});
|
|
18328
|
-
var SHARED_FRAGMENTS_DIR = resolve4(PROFILES_ROOT, "_shared");
|
|
18329
|
-
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol"];
|
|
18330
|
-
for (const name of SHARED_FRAGMENTS) {
|
|
18331
|
-
const fragPath = join2(SHARED_FRAGMENTS_DIR, `${name}.md.hbs`);
|
|
18332
|
-
if (existsSync4(fragPath)) {
|
|
18333
|
-
import_handlebars.default.registerPartial(name, readFileSync3(fragPath, "utf-8"));
|
|
18442
|
+
// src/agents/agent-uid.ts
|
|
18443
|
+
var AGENT_UID_MIN = 10001;
|
|
18444
|
+
var AGENT_UID_MAX = 10999;
|
|
18445
|
+
function allocateAgentUid(name) {
|
|
18446
|
+
if (isReservedAgentName(name)) {
|
|
18447
|
+
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.`);
|
|
18334
18448
|
}
|
|
18449
|
+
const hash = createHash("sha256").update(name).digest();
|
|
18450
|
+
const u32 = hash.readUInt32BE(0);
|
|
18451
|
+
const range = AGENT_UID_MAX - AGENT_UID_MIN + 1;
|
|
18452
|
+
return AGENT_UID_MIN + u32 % range;
|
|
18335
18453
|
}
|
|
18336
18454
|
|
|
18337
|
-
// src/agents/
|
|
18338
|
-
|
|
18455
|
+
// src/agents/scaffold.ts
|
|
18456
|
+
init_schema();
|
|
18339
18457
|
|
|
18340
|
-
// src/
|
|
18341
|
-
|
|
18458
|
+
// src/config/users.ts
|
|
18459
|
+
init_merge();
|
|
18460
|
+
|
|
18461
|
+
// src/agents/scaffold.ts
|
|
18462
|
+
init_merge();
|
|
18463
|
+
|
|
18464
|
+
// src/config/timezone.ts
|
|
18465
|
+
var CONTAINER_DEFAULT_UTC_ZONES = new Set([
|
|
18466
|
+
"UTC",
|
|
18467
|
+
"Etc/UTC",
|
|
18468
|
+
"Etc/Universal",
|
|
18469
|
+
"Universal"
|
|
18470
|
+
]);
|
|
18471
|
+
|
|
18472
|
+
// src/cli/agent-config.ts
|
|
18473
|
+
import { join } from "node:path";
|
|
18474
|
+
import { homedir as homedir2 } from "node:os";
|
|
18475
|
+
|
|
18476
|
+
// src/cli/helpers.ts
|
|
18477
|
+
init_loader();
|
|
18478
|
+
|
|
18479
|
+
// src/cli/agent-config.ts
|
|
18480
|
+
init_loader();
|
|
18481
|
+
init_merge();
|
|
18482
|
+
|
|
18483
|
+
// src/config/google-workspace-acl.ts
|
|
18484
|
+
function shouldEmitGdriveMcp(agentName, agentGoogleAccount, googleAccounts) {
|
|
18485
|
+
if (!agentGoogleAccount)
|
|
18486
|
+
return false;
|
|
18487
|
+
const account = agentGoogleAccount.trim().toLowerCase();
|
|
18488
|
+
if (account.length === 0)
|
|
18489
|
+
return false;
|
|
18490
|
+
const acctEntry = googleAccounts?.[account];
|
|
18491
|
+
if (!acctEntry)
|
|
18492
|
+
return false;
|
|
18493
|
+
const enabledFor = acctEntry.enabled_for ?? [];
|
|
18494
|
+
return enabledFor.includes(agentName);
|
|
18495
|
+
}
|
|
18496
|
+
function vaultRefKey(value) {
|
|
18497
|
+
if (typeof value !== "string" || !value.startsWith("vault:"))
|
|
18498
|
+
return null;
|
|
18499
|
+
const key = value.slice("vault:".length).split("#")[0];
|
|
18500
|
+
return key.length > 0 ? key : null;
|
|
18501
|
+
}
|
|
18502
|
+
function isGoogleClientCredentialKeyForAgent(config, agentName, key) {
|
|
18503
|
+
if (!agentName || !key)
|
|
18504
|
+
return false;
|
|
18505
|
+
const agentConfig = config.agents?.[agentName];
|
|
18506
|
+
if (!agentConfig)
|
|
18507
|
+
return false;
|
|
18508
|
+
if (agentConfig.mcp_servers?.["gdrive"] === false) {
|
|
18509
|
+
return false;
|
|
18510
|
+
}
|
|
18511
|
+
const account = agentConfig.google_workspace?.account;
|
|
18512
|
+
if (!shouldEmitGdriveMcp(agentName, account, config.google_accounts)) {
|
|
18513
|
+
return false;
|
|
18514
|
+
}
|
|
18515
|
+
const gw = config.google_workspace;
|
|
18516
|
+
if (!gw)
|
|
18517
|
+
return false;
|
|
18518
|
+
for (const ref of [gw.google_client_id, gw.google_client_secret]) {
|
|
18519
|
+
if (vaultRefKey(ref) === key)
|
|
18520
|
+
return true;
|
|
18521
|
+
}
|
|
18522
|
+
return false;
|
|
18523
|
+
}
|
|
18524
|
+
|
|
18525
|
+
// src/vault/broker/acl.ts
|
|
18526
|
+
var WEBKITE_VAULT_KEYS = new Set([
|
|
18527
|
+
"webkite/cloudflare-account-id",
|
|
18528
|
+
"webkite/cloudflare-api-token",
|
|
18529
|
+
"webkite/firecrawl-api-key"
|
|
18530
|
+
]);
|
|
18531
|
+
function isWebkiteCredentialKeyForAgent(agentConfig, key) {
|
|
18532
|
+
if (!WEBKITE_VAULT_KEYS.has(key))
|
|
18533
|
+
return false;
|
|
18534
|
+
if ((agentConfig?.mcp_servers ?? {})["webkite"] === false)
|
|
18535
|
+
return false;
|
|
18536
|
+
return true;
|
|
18537
|
+
}
|
|
18538
|
+
function checkEntryScope(scope, agentSlug) {
|
|
18539
|
+
if (scope === undefined || scope === null) {
|
|
18540
|
+
return { allow: true };
|
|
18541
|
+
}
|
|
18542
|
+
const deny = scope.deny ?? [];
|
|
18543
|
+
const allow = scope.allow ?? [];
|
|
18544
|
+
if (agentSlug !== null && deny.includes(agentSlug)) {
|
|
18545
|
+
return {
|
|
18546
|
+
allow: false,
|
|
18547
|
+
reason: `agent '${agentSlug}' is in the entry's deny list (scope-deny)`
|
|
18548
|
+
};
|
|
18549
|
+
}
|
|
18550
|
+
if (allow.length > 0) {
|
|
18551
|
+
if (agentSlug === null || !allow.includes(agentSlug)) {
|
|
18552
|
+
return {
|
|
18553
|
+
allow: false,
|
|
18554
|
+
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)`
|
|
18555
|
+
};
|
|
18556
|
+
}
|
|
18557
|
+
}
|
|
18558
|
+
return { allow: true };
|
|
18559
|
+
}
|
|
18560
|
+
function checkAclByAgent(config, agentName, key) {
|
|
18561
|
+
if (!agentName) {
|
|
18562
|
+
return { allow: false, reason: "agent name unresolved" };
|
|
18563
|
+
}
|
|
18564
|
+
const agentConfig = config.agents?.[agentName];
|
|
18565
|
+
if (!agentConfig) {
|
|
18566
|
+
return { allow: false, reason: `agent '${agentName}' not found in config` };
|
|
18567
|
+
}
|
|
18568
|
+
const googleSlot = parseGoogleAccountSlotKey(key);
|
|
18569
|
+
if (googleSlot !== null) {
|
|
18570
|
+
return checkGoogleAccountAcl(config, agentName, googleSlot.account, key);
|
|
18571
|
+
}
|
|
18572
|
+
if (isGoogleClientCredentialKeyForAgent(config, agentName, key)) {
|
|
18573
|
+
return { allow: true };
|
|
18574
|
+
}
|
|
18575
|
+
if (isWebkiteCredentialKeyForAgent(agentConfig, key)) {
|
|
18576
|
+
return { allow: true };
|
|
18577
|
+
}
|
|
18578
|
+
const agentBot = agentConfig.bot_token;
|
|
18579
|
+
const botRef = agentBot && agentBot.length > 0 ? agentBot : config.telegram?.bot_token;
|
|
18580
|
+
if (typeof botRef === "string" && botRef.startsWith("vault:")) {
|
|
18581
|
+
const botKey = botRef.slice("vault:".length).split("#")[0];
|
|
18582
|
+
if (botKey.length > 0 && botKey === key) {
|
|
18583
|
+
return { allow: true };
|
|
18584
|
+
}
|
|
18585
|
+
}
|
|
18586
|
+
const cfgWithProfiles = config;
|
|
18587
|
+
const profileName = agentConfig.extends;
|
|
18588
|
+
const profileMcp = profileName != null && profileName.length > 0 ? cfgWithProfiles.profiles?.[profileName]?.mcp_servers ?? {} : {};
|
|
18589
|
+
const effectiveMcp = {
|
|
18590
|
+
...cfgWithProfiles.defaults?.mcp_servers ?? {},
|
|
18591
|
+
...profileMcp,
|
|
18592
|
+
...agentConfig.mcp_servers ?? {}
|
|
18593
|
+
};
|
|
18594
|
+
for (const mcpEntry of Object.values(effectiveMcp)) {
|
|
18595
|
+
if (!mcpEntry || typeof mcpEntry !== "object")
|
|
18596
|
+
continue;
|
|
18597
|
+
const declared = mcpEntry.secrets;
|
|
18598
|
+
if (Array.isArray(declared) && declared.includes(key)) {
|
|
18599
|
+
return { allow: true };
|
|
18600
|
+
}
|
|
18601
|
+
}
|
|
18602
|
+
const cfgSecrets = config;
|
|
18603
|
+
const profileSecrets = profileName != null && profileName.length > 0 ? cfgSecrets.profiles?.[profileName]?.secrets : undefined;
|
|
18604
|
+
const standingSecrets = [
|
|
18605
|
+
...Array.isArray(cfgSecrets.defaults?.secrets) ? cfgSecrets.defaults.secrets : [],
|
|
18606
|
+
...Array.isArray(profileSecrets) ? profileSecrets : [],
|
|
18607
|
+
...Array.isArray(agentConfig.secrets) ? agentConfig.secrets : []
|
|
18608
|
+
];
|
|
18609
|
+
if (standingSecrets.includes(key)) {
|
|
18610
|
+
return { allow: true };
|
|
18611
|
+
}
|
|
18612
|
+
const schedule = agentConfig.schedule ?? [];
|
|
18613
|
+
if (schedule.length === 0) {
|
|
18614
|
+
return {
|
|
18615
|
+
allow: false,
|
|
18616
|
+
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`
|
|
18617
|
+
};
|
|
18618
|
+
}
|
|
18619
|
+
for (const entry of schedule) {
|
|
18620
|
+
const allowed = entry?.secrets ?? [];
|
|
18621
|
+
if (allowed.includes(key)) {
|
|
18622
|
+
return { allow: true };
|
|
18623
|
+
}
|
|
18624
|
+
}
|
|
18625
|
+
return {
|
|
18626
|
+
allow: false,
|
|
18627
|
+
reason: `key '${key}' not in ACL for agent '${agentName}'`
|
|
18628
|
+
};
|
|
18629
|
+
}
|
|
18630
|
+
function parseGoogleAccountSlotKey(key) {
|
|
18631
|
+
const match = key.match(/^google:([^:]+):([a-z_]+)$/);
|
|
18632
|
+
if (!match)
|
|
18633
|
+
return null;
|
|
18634
|
+
return { account: match[1], field: match[2] };
|
|
18635
|
+
}
|
|
18636
|
+
function checkGoogleAccountAcl(config, agentName, account, key) {
|
|
18637
|
+
const accounts = config.google_accounts ?? {};
|
|
18638
|
+
const accountKey = account.toLowerCase();
|
|
18639
|
+
const accountEntry = accounts[accountKey] ?? accounts[account];
|
|
18640
|
+
if (!accountEntry) {
|
|
18641
|
+
return {
|
|
18642
|
+
allow: false,
|
|
18643
|
+
reason: `google_accounts['${account}'] not configured (key '${key}')`
|
|
18644
|
+
};
|
|
18645
|
+
}
|
|
18646
|
+
const enabled = accountEntry.enabled_for ?? [];
|
|
18647
|
+
if (enabled.length === 0) {
|
|
18648
|
+
return {
|
|
18649
|
+
allow: false,
|
|
18650
|
+
reason: `google_accounts['${account}'].enabled_for is empty (key '${key}')`
|
|
18651
|
+
};
|
|
18652
|
+
}
|
|
18653
|
+
if (!enabled.includes(agentName)) {
|
|
18654
|
+
return {
|
|
18655
|
+
allow: false,
|
|
18656
|
+
reason: `agent '${agentName}' not in google_accounts['${account}'].enabled_for (key '${key}')`
|
|
18657
|
+
};
|
|
18658
|
+
}
|
|
18659
|
+
return { allow: true };
|
|
18660
|
+
}
|
|
18661
|
+
|
|
18662
|
+
// src/scheduler/cron-introspect.ts
|
|
18663
|
+
init_overlay_loader();
|
|
18664
|
+
|
|
18665
|
+
// src/cli/agent-config.ts
|
|
18666
|
+
var AUDIT_ROOT = join(homedir2(), ".switchroom", "audit");
|
|
18667
|
+
|
|
18668
|
+
// src/agents/profiles.ts
|
|
18669
|
+
var import_handlebars = __toESM(require_lib(), 1);
|
|
18670
|
+
import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2, copyFileSync, mkdirSync, realpathSync } from "node:fs";
|
|
18671
|
+
import { resolve as resolve4, join as join2, sep as pathSep } from "node:path";
|
|
18672
|
+
var PROFILES_ROOT = resolve4(import.meta.dirname, "../../profiles");
|
|
18673
|
+
import_handlebars.default.registerHelper("json", (value) => {
|
|
18674
|
+
return new import_handlebars.default.SafeString(JSON.stringify(value, null, 2));
|
|
18675
|
+
});
|
|
18676
|
+
import_handlebars.default.registerHelper("isNumber", (value) => {
|
|
18677
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
18678
|
+
});
|
|
18679
|
+
var SHARED_FRAGMENTS_DIR = resolve4(PROFILES_ROOT, "_shared");
|
|
18680
|
+
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol"];
|
|
18681
|
+
for (const name of SHARED_FRAGMENTS) {
|
|
18682
|
+
const fragPath = join2(SHARED_FRAGMENTS_DIR, `${name}.md.hbs`);
|
|
18683
|
+
if (existsSync4(fragPath)) {
|
|
18684
|
+
import_handlebars.default.registerPartial(name, readFileSync4(fragPath, "utf-8"));
|
|
18685
|
+
}
|
|
18686
|
+
}
|
|
18687
|
+
|
|
18688
|
+
// src/agents/pane-lock.ts
|
|
18689
|
+
var tails = new Map;
|
|
18690
|
+
|
|
18691
|
+
// src/agents/inject.ts
|
|
18692
|
+
var INJECT_COMMANDS = new Map([
|
|
18342
18693
|
["/cost", { description: "Show session cost", expectsOutput: true, dialog: true, argsAllowed: false }],
|
|
18343
18694
|
["/status", { description: "Show session status", expectsOutput: true, dialog: true, argsAllowed: false }],
|
|
18344
18695
|
["/usage", { description: "Show plan quota", expectsOutput: true, dialog: true, argsAllowed: false }],
|
|
@@ -18430,7 +18781,7 @@ init_loader();
|
|
|
18430
18781
|
// src/vault/vault.ts
|
|
18431
18782
|
import { randomBytes, scryptSync, createCipheriv, createDecipheriv } from "node:crypto";
|
|
18432
18783
|
import {
|
|
18433
|
-
readFileSync as
|
|
18784
|
+
readFileSync as readFileSync6,
|
|
18434
18785
|
writeSync as writeSync3,
|
|
18435
18786
|
existsSync as existsSync6,
|
|
18436
18787
|
renameSync as renameSync2,
|
|
@@ -18452,7 +18803,7 @@ import {
|
|
|
18452
18803
|
writeSync as writeSync2,
|
|
18453
18804
|
fsyncSync as fsyncSync2,
|
|
18454
18805
|
unlinkSync,
|
|
18455
|
-
readFileSync as
|
|
18806
|
+
readFileSync as readFileSync5,
|
|
18456
18807
|
readdirSync as readdirSync3,
|
|
18457
18808
|
rmdirSync,
|
|
18458
18809
|
statSync as statSync3,
|
|
@@ -18464,7 +18815,7 @@ function lockPathFor(vaultPath) {
|
|
|
18464
18815
|
}
|
|
18465
18816
|
function readLockHolder(lockPath) {
|
|
18466
18817
|
try {
|
|
18467
|
-
const raw =
|
|
18818
|
+
const raw = readFileSync5(lockPath, "utf8");
|
|
18468
18819
|
const lines = raw.split(`
|
|
18469
18820
|
`);
|
|
18470
18821
|
const pid = Number.parseInt(lines[0] ?? "", 10);
|
|
@@ -18516,8 +18867,8 @@ function pidStartTimeMs(pid) {
|
|
|
18516
18867
|
if (process.platform !== "linux")
|
|
18517
18868
|
return null;
|
|
18518
18869
|
try {
|
|
18519
|
-
const statLine =
|
|
18520
|
-
const procStat =
|
|
18870
|
+
const statLine = readFileSync5(`/proc/${pid}/stat`, "utf8");
|
|
18871
|
+
const procStat = readFileSync5("/proc/stat", "utf8");
|
|
18521
18872
|
return parseProcStartTimeMs(statLine, procStat, Date.now());
|
|
18522
18873
|
} catch {
|
|
18523
18874
|
return null;
|
|
@@ -18702,479 +19053,146 @@ function atomicWriteFileSync(path, data, mode) {
|
|
|
18702
19053
|
try {
|
|
18703
19054
|
if (existsSync6(tmp))
|
|
18704
19055
|
unlinkSync2(tmp);
|
|
18705
|
-
} catch {}
|
|
18706
|
-
throw err;
|
|
18707
|
-
}
|
|
18708
|
-
}
|
|
18709
|
-
|
|
18710
|
-
class VaultError extends Error {
|
|
18711
|
-
cause;
|
|
18712
|
-
constructor(message, options) {
|
|
18713
|
-
super(message);
|
|
18714
|
-
this.name = "VaultError";
|
|
18715
|
-
if (options?.cause !== undefined)
|
|
18716
|
-
this.cause = options.cause;
|
|
18717
|
-
}
|
|
18718
|
-
}
|
|
18719
|
-
var LEGACY_SCRYPT_N = 16384;
|
|
18720
|
-
function deriveKey(passphrase, salt, params = { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }) {
|
|
18721
|
-
return scryptSync(passphrase, salt, 32, {
|
|
18722
|
-
N: params.N,
|
|
18723
|
-
r: params.r,
|
|
18724
|
-
p: params.p,
|
|
18725
|
-
maxmem: SCRYPT_MAXMEM
|
|
18726
|
-
});
|
|
18727
|
-
}
|
|
18728
|
-
function encrypt(key, plaintext) {
|
|
18729
|
-
const iv = randomBytes(12);
|
|
18730
|
-
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
18731
|
-
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
18732
|
-
const tag = cipher.getAuthTag();
|
|
18733
|
-
return {
|
|
18734
|
-
iv: iv.toString("hex"),
|
|
18735
|
-
data: encrypted.toString("hex"),
|
|
18736
|
-
tag: tag.toString("hex")
|
|
18737
|
-
};
|
|
18738
|
-
}
|
|
18739
|
-
function decrypt(key, iv, data, tag) {
|
|
18740
|
-
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "hex"));
|
|
18741
|
-
decipher.setAuthTag(Buffer.from(tag, "hex"));
|
|
18742
|
-
const decrypted = Buffer.concat([
|
|
18743
|
-
decipher.update(Buffer.from(data, "hex")),
|
|
18744
|
-
decipher.final()
|
|
18745
|
-
]);
|
|
18746
|
-
return decrypted.toString("utf8");
|
|
18747
|
-
}
|
|
18748
|
-
function normalizeEntry(raw) {
|
|
18749
|
-
if (typeof raw === "string") {
|
|
18750
|
-
return { kind: "string", value: raw };
|
|
18751
|
-
}
|
|
18752
|
-
return raw;
|
|
18753
|
-
}
|
|
18754
|
-
function normalizeSecrets(raw) {
|
|
18755
|
-
const out = {};
|
|
18756
|
-
for (const [k, v] of Object.entries(raw)) {
|
|
18757
|
-
out[k] = normalizeEntry(v);
|
|
18758
|
-
}
|
|
18759
|
-
return out;
|
|
18760
|
-
}
|
|
18761
|
-
function openVault(passphrase, vaultPath) {
|
|
18762
|
-
if (!existsSync6(vaultPath)) {
|
|
18763
|
-
throw new VaultError(`Vault file not found: ${vaultPath}`);
|
|
18764
|
-
}
|
|
18765
|
-
let vaultFile;
|
|
18766
|
-
try {
|
|
18767
|
-
vaultFile = JSON.parse(readFileSync5(vaultPath, "utf8"));
|
|
18768
|
-
} catch {
|
|
18769
|
-
throw new VaultError(`Failed to read vault file: ${vaultPath}`);
|
|
18770
|
-
}
|
|
18771
|
-
const salt = Buffer.from(vaultFile.salt, "hex");
|
|
18772
|
-
const kdfParams = vaultFile.kdf ?? { N: LEGACY_SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P };
|
|
18773
|
-
const key = deriveKey(passphrase, salt, kdfParams);
|
|
18774
|
-
let plaintext;
|
|
18775
|
-
try {
|
|
18776
|
-
plaintext = decrypt(key, vaultFile.iv, vaultFile.data, vaultFile.tag);
|
|
18777
|
-
} catch {
|
|
18778
|
-
throw new VaultError("Failed to decrypt vault. Wrong passphrase?");
|
|
18779
|
-
}
|
|
18780
|
-
let vaultData;
|
|
18781
|
-
try {
|
|
18782
|
-
vaultData = JSON.parse(plaintext);
|
|
18783
|
-
} catch {
|
|
18784
|
-
throw new VaultError("Vault data is corrupted");
|
|
18785
|
-
}
|
|
18786
|
-
return normalizeSecrets(vaultData.secrets ?? {});
|
|
18787
|
-
}
|
|
18788
|
-
function saveVault(passphrase, vaultPath, secrets) {
|
|
18789
|
-
if (!existsSync6(vaultPath)) {
|
|
18790
|
-
throw new VaultError(`Vault file not found: ${vaultPath}`);
|
|
18791
|
-
}
|
|
18792
|
-
let releaseLock = null;
|
|
18793
|
-
try {
|
|
18794
|
-
releaseLock = acquireLock(vaultPath, { budgetMs: SAVE_VAULT_LOCK_RETRY_MS }).release;
|
|
18795
|
-
} catch (err) {
|
|
18796
|
-
if (err instanceof VaultBusyError) {
|
|
18797
|
-
throw new VaultError(err.message, { cause: err });
|
|
18798
|
-
}
|
|
18799
|
-
throw err;
|
|
18800
|
-
}
|
|
18801
|
-
try {
|
|
18802
|
-
let vaultFile;
|
|
18803
|
-
try {
|
|
18804
|
-
vaultFile = JSON.parse(readFileSync5(vaultPath, "utf8"));
|
|
18805
|
-
} catch {
|
|
18806
|
-
throw new VaultError(`Failed to read vault file: ${vaultPath}`);
|
|
18807
|
-
}
|
|
18808
|
-
const salt = Buffer.from(vaultFile.salt, "hex");
|
|
18809
|
-
const key = deriveKey(passphrase, salt, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P });
|
|
18810
|
-
vaultFile.kdf = { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P };
|
|
18811
|
-
const vaultData = { secrets };
|
|
18812
|
-
const { iv, data, tag } = encrypt(key, JSON.stringify(vaultData));
|
|
18813
|
-
vaultFile.iv = iv;
|
|
18814
|
-
vaultFile.data = data;
|
|
18815
|
-
vaultFile.tag = tag;
|
|
18816
|
-
atomicWriteFileSync(vaultPath, JSON.stringify(vaultFile, null, 2), 384);
|
|
18817
|
-
} finally {
|
|
18818
|
-
if (releaseLock !== null) {
|
|
18819
|
-
try {
|
|
18820
|
-
releaseLock();
|
|
18821
|
-
} catch {}
|
|
18822
|
-
}
|
|
18823
|
-
}
|
|
18824
|
-
}
|
|
18825
|
-
function acquireVaultLock(vaultPath) {
|
|
18826
|
-
try {
|
|
18827
|
-
return acquireLock(vaultPath, { budgetMs: SAVE_VAULT_LOCK_RETRY_MS }).release;
|
|
18828
|
-
} catch (err) {
|
|
18829
|
-
if (err instanceof VaultBusyError) {
|
|
18830
|
-
throw new VaultError(err.message, { cause: err });
|
|
18831
|
-
}
|
|
18832
|
-
throw err;
|
|
18833
|
-
}
|
|
18834
|
-
}
|
|
18835
|
-
|
|
18836
|
-
// src/vault/resolver.ts
|
|
18837
|
-
init_loader();
|
|
18838
|
-
|
|
18839
|
-
// src/vault/broker/client.ts
|
|
18840
|
-
import { homedir as homedir3 } from "node:os";
|
|
18841
|
-
import { join as join3 } from "node:path";
|
|
18842
|
-
|
|
18843
|
-
// src/vault/broker/protocol.ts
|
|
18844
|
-
init_zod();
|
|
18845
|
-
|
|
18846
|
-
// src/vault/broker/peercred.ts
|
|
18847
|
-
import { execFileSync } from "node:child_process";
|
|
18848
|
-
import { readFileSync as readFileSync6, readlinkSync, fstatSync } from "node:fs";
|
|
18849
|
-
|
|
18850
|
-
// src/vault/broker/peercred-ffi.ts
|
|
18851
|
-
function getPeerCred(fd) {
|
|
18852
|
-
if (process.platform !== "linux")
|
|
18853
|
-
return null;
|
|
18854
|
-
try {
|
|
18855
|
-
const ffi = __require("bun:ffi");
|
|
18856
|
-
const { dlopen, FFIType, ptr } = ffi;
|
|
18857
|
-
const SOL_SOCKET = 1;
|
|
18858
|
-
const SO_PEERCRED = 17;
|
|
18859
|
-
const UCRED_SIZE = 12;
|
|
18860
|
-
const cache = getPeerCred;
|
|
18861
|
-
const lib = cache._lib ?? (() => {
|
|
18862
|
-
const candidates = ["libc.so.6", "libc.so"];
|
|
18863
|
-
const symbolSpec = {
|
|
18864
|
-
getsockopt: {
|
|
18865
|
-
args: [FFIType.i32, FFIType.i32, FFIType.i32, FFIType.ptr, FFIType.ptr],
|
|
18866
|
-
returns: FFIType.i32
|
|
18867
|
-
}
|
|
18868
|
-
};
|
|
18869
|
-
const errors2 = [];
|
|
18870
|
-
for (const name of candidates) {
|
|
18871
|
-
try {
|
|
18872
|
-
const opened = dlopen(name, symbolSpec);
|
|
18873
|
-
cache._lib = opened;
|
|
18874
|
-
return opened;
|
|
18875
|
-
} catch (e) {
|
|
18876
|
-
errors2.push(`${name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
18877
|
-
}
|
|
18878
|
-
}
|
|
18879
|
-
process.stderr.write(`[vault-broker] peercred-ffi: dlopen failed for all libc candidates ` + `(${errors2.join("; ")}); falling back to ss-parsing.
|
|
18880
|
-
`);
|
|
18881
|
-
throw new Error("no libc candidate could be opened");
|
|
18882
|
-
})();
|
|
18883
|
-
const credBuf = new ArrayBuffer(UCRED_SIZE);
|
|
18884
|
-
const lenBuf = new Uint32Array(1);
|
|
18885
|
-
lenBuf[0] = UCRED_SIZE;
|
|
18886
|
-
const rc = lib.symbols.getsockopt(fd, SOL_SOCKET, SO_PEERCRED, ptr(credBuf), ptr(lenBuf.buffer));
|
|
18887
|
-
if (rc !== 0)
|
|
18888
|
-
return null;
|
|
18889
|
-
if (lenBuf[0] !== UCRED_SIZE)
|
|
18890
|
-
return null;
|
|
18891
|
-
const view = new DataView(credBuf);
|
|
18892
|
-
return {
|
|
18893
|
-
pid: view.getInt32(0, true),
|
|
18894
|
-
uid: view.getInt32(4, true),
|
|
18895
|
-
gid: view.getInt32(8, true)
|
|
18896
|
-
};
|
|
18897
|
-
} catch {
|
|
18898
|
-
return null;
|
|
18899
|
-
}
|
|
18900
|
-
}
|
|
18901
|
-
|
|
18902
|
-
// src/broker-common/peercred-path.ts
|
|
18903
|
-
function matchSocketName(socketPath, patterns, maxNameLen) {
|
|
18904
|
-
if (typeof socketPath !== "string" || socketPath.length === 0)
|
|
18905
|
-
return null;
|
|
18906
|
-
let name = null;
|
|
18907
|
-
for (const re of patterns) {
|
|
18908
|
-
const m = socketPath.match(re);
|
|
18909
|
-
if (m && typeof m[1] === "string") {
|
|
18910
|
-
name = m[1];
|
|
18911
|
-
break;
|
|
18912
|
-
}
|
|
18913
|
-
}
|
|
18914
|
-
if (name === null || name.length === 0)
|
|
18915
|
-
return null;
|
|
18916
|
-
if (maxNameLen !== undefined && name.length > maxNameLen)
|
|
18917
|
-
return null;
|
|
18918
|
-
return name;
|
|
18919
|
-
}
|
|
18920
|
-
function parseSocketIdentity(socketPath, spec) {
|
|
18921
|
-
const name = matchSocketName(socketPath, spec.patterns, spec.maxNameLen);
|
|
18922
|
-
if (name === null)
|
|
18923
|
-
return null;
|
|
18924
|
-
const operatorName = spec.operatorName ?? "operator";
|
|
18925
|
-
if (name === operatorName)
|
|
18926
|
-
return { kind: "operator" };
|
|
18927
|
-
if (spec.reservedNames.has(name))
|
|
18928
|
-
return null;
|
|
18929
|
-
return { kind: "agent", name };
|
|
18930
|
-
}
|
|
18931
|
-
|
|
18932
|
-
// src/vault/broker/peercred.ts
|
|
18933
|
-
var SOCKET_PATH_AGENT_RE = /^\/run\/switchroom\/broker\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\.sock$/;
|
|
18934
|
-
var SOCKET_PATH_AGENT_SUBDIR_RE = /^\/run\/switchroom\/broker\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\/sock$/;
|
|
18935
|
-
var RESERVED_AGENT_NAMES = new Set(["operator", "hostd"]);
|
|
18936
|
-
function socketPathToIdentity(socketPath) {
|
|
18937
|
-
return parseSocketIdentity(socketPath, {
|
|
18938
|
-
patterns: [SOCKET_PATH_AGENT_RE, SOCKET_PATH_AGENT_SUBDIR_RE],
|
|
18939
|
-
reservedNames: RESERVED_AGENT_NAMES
|
|
18940
|
-
});
|
|
18941
|
-
}
|
|
18942
|
-
function socketPathToAgent(socketPath) {
|
|
18943
|
-
const identity = socketPathToIdentity(socketPath);
|
|
18944
|
-
return identity?.kind === "agent" ? identity.name : null;
|
|
18945
|
-
}
|
|
18946
|
-
function isReservedAgentName(name) {
|
|
18947
|
-
return RESERVED_AGENT_NAMES.has(name);
|
|
18948
|
-
}
|
|
18949
|
-
function unlockSocketFor(dataSocketPath) {
|
|
18950
|
-
if (dataSocketPath.endsWith("/sock")) {
|
|
18951
|
-
return dataSocketPath.slice(0, -"/sock".length) + "/unlock";
|
|
18952
|
-
}
|
|
18953
|
-
return dataSocketPath.replace(/\.sock$/, ".unlock.sock");
|
|
18954
|
-
}
|
|
18955
|
-
function parseSsRows(output) {
|
|
18956
|
-
const rows = [];
|
|
18957
|
-
const lines = output.split(`
|
|
18958
|
-
`);
|
|
18959
|
-
for (const line of lines) {
|
|
18960
|
-
if (!line.trim() || line.startsWith("Netid"))
|
|
18961
|
-
continue;
|
|
18962
|
-
const tokens = line.split(/\s+/).filter((t) => t.length > 0);
|
|
18963
|
-
if (tokens.length < 8)
|
|
18964
|
-
continue;
|
|
18965
|
-
const localAddr = tokens[4];
|
|
18966
|
-
const localInode = tokens[5];
|
|
18967
|
-
const peerAddr = tokens[6];
|
|
18968
|
-
const peerInode = tokens[7];
|
|
18969
|
-
const usersToken = tokens.slice(8).join(" ");
|
|
18970
|
-
const m = usersToken.match(/users:\(\(".*?",pid=(\d+),fd=\d+\)\)/);
|
|
18971
|
-
const pid = m ? parseInt(m[1], 10) : null;
|
|
18972
|
-
rows.push({ localAddr, localInode, peerAddr, peerInode, pid });
|
|
18973
|
-
}
|
|
18974
|
-
return rows;
|
|
18975
|
-
}
|
|
18976
|
-
function findClientPids(rows, socketPath) {
|
|
18977
|
-
const pids = [];
|
|
18978
|
-
for (const serverRow of rows) {
|
|
18979
|
-
if (serverRow.localAddr !== socketPath)
|
|
18980
|
-
continue;
|
|
18981
|
-
for (const clientRow of rows) {
|
|
18982
|
-
if (clientRow.localAddr !== "*")
|
|
18983
|
-
continue;
|
|
18984
|
-
if (clientRow.localInode !== serverRow.peerInode)
|
|
18985
|
-
continue;
|
|
18986
|
-
if (clientRow.pid === null)
|
|
18987
|
-
continue;
|
|
18988
|
-
pids.push(clientRow.pid);
|
|
18989
|
-
break;
|
|
18990
|
-
}
|
|
18991
|
-
}
|
|
18992
|
-
return pids;
|
|
18993
|
-
}
|
|
18994
|
-
function findClientPidByServerInode(rows, socketPath, serverInode) {
|
|
18995
|
-
const serverInodeStr = String(serverInode);
|
|
18996
|
-
for (const serverRow of rows) {
|
|
18997
|
-
if (serverRow.localAddr !== socketPath)
|
|
18998
|
-
continue;
|
|
18999
|
-
if (serverRow.localInode !== serverInodeStr)
|
|
19000
|
-
continue;
|
|
19001
|
-
for (const clientRow of rows) {
|
|
19002
|
-
if (clientRow.localAddr !== "*")
|
|
19003
|
-
continue;
|
|
19004
|
-
if (clientRow.localInode !== serverRow.peerInode)
|
|
19005
|
-
continue;
|
|
19006
|
-
if (clientRow.pid === null)
|
|
19007
|
-
continue;
|
|
19008
|
-
return clientRow.pid;
|
|
19009
|
-
}
|
|
19010
|
-
return null;
|
|
19056
|
+
} catch {}
|
|
19057
|
+
throw err;
|
|
19011
19058
|
}
|
|
19012
|
-
return null;
|
|
19013
19059
|
}
|
|
19014
|
-
|
|
19015
|
-
|
|
19016
|
-
|
|
19017
|
-
|
|
19018
|
-
|
|
19019
|
-
|
|
19020
|
-
|
|
19021
|
-
|
|
19022
|
-
return null;
|
|
19060
|
+
|
|
19061
|
+
class VaultError extends Error {
|
|
19062
|
+
cause;
|
|
19063
|
+
constructor(message, options) {
|
|
19064
|
+
super(message);
|
|
19065
|
+
this.name = "VaultError";
|
|
19066
|
+
if (options?.cause !== undefined)
|
|
19067
|
+
this.cause = options.cause;
|
|
19023
19068
|
}
|
|
19024
19069
|
}
|
|
19025
|
-
|
|
19026
|
-
|
|
19027
|
-
|
|
19028
|
-
|
|
19029
|
-
|
|
19070
|
+
var LEGACY_SCRYPT_N = 16384;
|
|
19071
|
+
function deriveKey(passphrase, salt, params = { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }) {
|
|
19072
|
+
return scryptSync(passphrase, salt, 32, {
|
|
19073
|
+
N: params.N,
|
|
19074
|
+
r: params.r,
|
|
19075
|
+
p: params.p,
|
|
19076
|
+
maxmem: SCRYPT_MAXMEM
|
|
19077
|
+
});
|
|
19078
|
+
}
|
|
19079
|
+
function encrypt(key, plaintext) {
|
|
19080
|
+
const iv = randomBytes(12);
|
|
19081
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
19082
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
19083
|
+
const tag = cipher.getAuthTag();
|
|
19084
|
+
return {
|
|
19085
|
+
iv: iv.toString("hex"),
|
|
19086
|
+
data: encrypted.toString("hex"),
|
|
19087
|
+
tag: tag.toString("hex")
|
|
19088
|
+
};
|
|
19089
|
+
}
|
|
19090
|
+
function decrypt(key, iv, data, tag) {
|
|
19091
|
+
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "hex"));
|
|
19092
|
+
decipher.setAuthTag(Buffer.from(tag, "hex"));
|
|
19093
|
+
const decrypted = Buffer.concat([
|
|
19094
|
+
decipher.update(Buffer.from(data, "hex")),
|
|
19095
|
+
decipher.final()
|
|
19096
|
+
]);
|
|
19097
|
+
return decrypted.toString("utf8");
|
|
19098
|
+
}
|
|
19099
|
+
function normalizeEntry(raw) {
|
|
19100
|
+
if (typeof raw === "string") {
|
|
19101
|
+
return { kind: "string", value: raw };
|
|
19030
19102
|
}
|
|
19103
|
+
return raw;
|
|
19031
19104
|
}
|
|
19032
|
-
function
|
|
19033
|
-
|
|
19034
|
-
|
|
19035
|
-
|
|
19036
|
-
`);
|
|
19037
|
-
for (const line of lines) {
|
|
19038
|
-
if (!line.trim())
|
|
19039
|
-
continue;
|
|
19040
|
-
const parts = line.split(":");
|
|
19041
|
-
if (parts.length < 3)
|
|
19042
|
-
continue;
|
|
19043
|
-
const controller = parts[1];
|
|
19044
|
-
const isV2 = parts[0] === "0" && controller === "";
|
|
19045
|
-
const isV1Systemd = controller === "name=systemd";
|
|
19046
|
-
if (!isV2 && !isV1Systemd)
|
|
19047
|
-
continue;
|
|
19048
|
-
const cgroupPath = parts.slice(2).join(":");
|
|
19049
|
-
const segments = cgroupPath.split("/");
|
|
19050
|
-
const lastSegment = segments[segments.length - 1];
|
|
19051
|
-
if (!lastSegment)
|
|
19052
|
-
continue;
|
|
19053
|
-
if (/^switchroom-[a-zA-Z0-9_-]+(-cron-\d+)?\.service$/.test(lastSegment)) {
|
|
19054
|
-
return lastSegment;
|
|
19055
|
-
}
|
|
19056
|
-
}
|
|
19057
|
-
return null;
|
|
19058
|
-
} catch {
|
|
19059
|
-
return null;
|
|
19105
|
+
function normalizeSecrets(raw) {
|
|
19106
|
+
const out = {};
|
|
19107
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
19108
|
+
out[k] = normalizeEntry(v);
|
|
19060
19109
|
}
|
|
19110
|
+
return out;
|
|
19061
19111
|
}
|
|
19062
|
-
function
|
|
19063
|
-
|
|
19112
|
+
function openVault(passphrase, vaultPath) {
|
|
19113
|
+
if (!existsSync6(vaultPath)) {
|
|
19114
|
+
throw new VaultError(`Vault file not found: ${vaultPath}`);
|
|
19115
|
+
}
|
|
19116
|
+
let vaultFile;
|
|
19064
19117
|
try {
|
|
19065
|
-
|
|
19066
|
-
"--user",
|
|
19067
|
-
"show",
|
|
19068
|
-
unitName,
|
|
19069
|
-
"--property=LoadState,ActiveState"
|
|
19070
|
-
], { timeout: 500, encoding: "utf8" });
|
|
19071
|
-
raw = typeof out === "string" ? out : out.toString("utf8");
|
|
19118
|
+
vaultFile = JSON.parse(readFileSync6(vaultPath, "utf8"));
|
|
19072
19119
|
} catch {
|
|
19073
|
-
|
|
19074
|
-
}
|
|
19075
|
-
const props = {};
|
|
19076
|
-
for (const line of raw.split(`
|
|
19077
|
-
`)) {
|
|
19078
|
-
const m = line.match(/^([A-Za-z]+)=(.*)$/);
|
|
19079
|
-
if (m)
|
|
19080
|
-
props[m[1]] = m[2];
|
|
19120
|
+
throw new VaultError(`Failed to read vault file: ${vaultPath}`);
|
|
19081
19121
|
}
|
|
19082
|
-
|
|
19083
|
-
|
|
19084
|
-
|
|
19085
|
-
|
|
19122
|
+
const salt = Buffer.from(vaultFile.salt, "hex");
|
|
19123
|
+
const kdfParams = vaultFile.kdf ?? { N: LEGACY_SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P };
|
|
19124
|
+
const key = deriveKey(passphrase, salt, kdfParams);
|
|
19125
|
+
let plaintext;
|
|
19126
|
+
try {
|
|
19127
|
+
plaintext = decrypt(key, vaultFile.iv, vaultFile.data, vaultFile.tag);
|
|
19128
|
+
} catch {
|
|
19129
|
+
throw new VaultError("Failed to decrypt vault. Wrong passphrase?");
|
|
19086
19130
|
}
|
|
19087
|
-
|
|
19088
|
-
}
|
|
19089
|
-
function readFdInode(fd) {
|
|
19131
|
+
let vaultData;
|
|
19090
19132
|
try {
|
|
19091
|
-
|
|
19092
|
-
return stat.ino;
|
|
19133
|
+
vaultData = JSON.parse(plaintext);
|
|
19093
19134
|
} catch {
|
|
19094
|
-
|
|
19135
|
+
throw new VaultError("Vault data is corrupted");
|
|
19095
19136
|
}
|
|
19137
|
+
return normalizeSecrets(vaultData.secrets ?? {});
|
|
19096
19138
|
}
|
|
19097
|
-
function
|
|
19098
|
-
|
|
19099
|
-
|
|
19100
|
-
return null;
|
|
19101
|
-
return handle.fd;
|
|
19102
|
-
}
|
|
19103
|
-
function identify(socketPath, socket, execFileSyncOverride) {
|
|
19104
|
-
if (process.platform !== "linux") {
|
|
19105
|
-
return null;
|
|
19139
|
+
function saveVault(passphrase, vaultPath, secrets) {
|
|
19140
|
+
if (!existsSync6(vaultPath)) {
|
|
19141
|
+
throw new VaultError(`Vault file not found: ${vaultPath}`);
|
|
19106
19142
|
}
|
|
19107
|
-
|
|
19108
|
-
|
|
19109
|
-
|
|
19110
|
-
|
|
19111
|
-
if (
|
|
19112
|
-
|
|
19113
|
-
if (cred !== null)
|
|
19114
|
-
pid = cred.pid;
|
|
19143
|
+
let releaseLock = null;
|
|
19144
|
+
try {
|
|
19145
|
+
releaseLock = acquireLock(vaultPath, { budgetMs: SAVE_VAULT_LOCK_RETRY_MS }).release;
|
|
19146
|
+
} catch (err) {
|
|
19147
|
+
if (err instanceof VaultBusyError) {
|
|
19148
|
+
throw new VaultError(err.message, { cause: err });
|
|
19115
19149
|
}
|
|
19150
|
+
throw err;
|
|
19116
19151
|
}
|
|
19117
|
-
|
|
19118
|
-
let
|
|
19152
|
+
try {
|
|
19153
|
+
let vaultFile;
|
|
19119
19154
|
try {
|
|
19120
|
-
|
|
19121
|
-
timeout: 200,
|
|
19122
|
-
encoding: "utf8"
|
|
19123
|
-
});
|
|
19124
|
-
ssOutput = typeof raw === "string" ? raw : raw.toString("utf8");
|
|
19155
|
+
vaultFile = JSON.parse(readFileSync6(vaultPath, "utf8"));
|
|
19125
19156
|
} catch {
|
|
19126
|
-
|
|
19127
|
-
}
|
|
19128
|
-
const rows = parseSsRows(ssOutput);
|
|
19129
|
-
let serverInode = null;
|
|
19130
|
-
if (socket !== undefined) {
|
|
19131
|
-
const fd = fdFromSocket(socket);
|
|
19132
|
-
if (fd !== null)
|
|
19133
|
-
serverInode = readFdInode(fd);
|
|
19157
|
+
throw new VaultError(`Failed to read vault file: ${vaultPath}`);
|
|
19134
19158
|
}
|
|
19135
|
-
|
|
19136
|
-
|
|
19137
|
-
|
|
19138
|
-
|
|
19139
|
-
|
|
19140
|
-
|
|
19141
|
-
|
|
19142
|
-
|
|
19143
|
-
|
|
19144
|
-
|
|
19145
|
-
|
|
19159
|
+
const salt = Buffer.from(vaultFile.salt, "hex");
|
|
19160
|
+
const key = deriveKey(passphrase, salt, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P });
|
|
19161
|
+
vaultFile.kdf = { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P };
|
|
19162
|
+
const vaultData = { secrets };
|
|
19163
|
+
const { iv, data, tag } = encrypt(key, JSON.stringify(vaultData));
|
|
19164
|
+
vaultFile.iv = iv;
|
|
19165
|
+
vaultFile.data = data;
|
|
19166
|
+
vaultFile.tag = tag;
|
|
19167
|
+
atomicWriteFileSync(vaultPath, JSON.stringify(vaultFile, null, 2), 384);
|
|
19168
|
+
} finally {
|
|
19169
|
+
if (releaseLock !== null) {
|
|
19170
|
+
try {
|
|
19171
|
+
releaseLock();
|
|
19172
|
+
} catch {}
|
|
19146
19173
|
}
|
|
19147
19174
|
}
|
|
19148
|
-
|
|
19149
|
-
|
|
19150
|
-
|
|
19151
|
-
|
|
19152
|
-
|
|
19153
|
-
|
|
19154
|
-
|
|
19155
|
-
if (brokerUid !== null && uid !== brokerUid) {
|
|
19156
|
-
process.stderr.write(`[vault-broker] peercred: UID mismatch — caller uid=${uid}, broker uid=${brokerUid}; denying
|
|
19157
|
-
`);
|
|
19158
|
-
return null;
|
|
19159
|
-
}
|
|
19160
|
-
const exe = readExe(pid);
|
|
19161
|
-
if (exe === null) {
|
|
19162
|
-
return null;
|
|
19163
|
-
}
|
|
19164
|
-
const cgroupClaim = readSystemdUnit(pid);
|
|
19165
|
-
let systemdUnit = null;
|
|
19166
|
-
if (cgroupClaim !== null) {
|
|
19167
|
-
if (verifySystemdUnit(cgroupClaim, runner)) {
|
|
19168
|
-
systemdUnit = cgroupClaim;
|
|
19169
|
-
} else {
|
|
19170
|
-
process.stderr.write(`[vault-broker] peercred: cgroup claims unit=${cgroupClaim} but systemd-user does not report it as loaded+running; treating caller as unidentified
|
|
19171
|
-
`);
|
|
19175
|
+
}
|
|
19176
|
+
function acquireVaultLock(vaultPath) {
|
|
19177
|
+
try {
|
|
19178
|
+
return acquireLock(vaultPath, { budgetMs: SAVE_VAULT_LOCK_RETRY_MS }).release;
|
|
19179
|
+
} catch (err) {
|
|
19180
|
+
if (err instanceof VaultBusyError) {
|
|
19181
|
+
throw new VaultError(err.message, { cause: err });
|
|
19172
19182
|
}
|
|
19183
|
+
throw err;
|
|
19173
19184
|
}
|
|
19174
|
-
return { uid, pid, exe, systemdUnit };
|
|
19175
19185
|
}
|
|
19176
19186
|
|
|
19187
|
+
// src/vault/resolver.ts
|
|
19188
|
+
init_loader();
|
|
19189
|
+
|
|
19190
|
+
// src/vault/broker/client.ts
|
|
19191
|
+
import { homedir as homedir3 } from "node:os";
|
|
19192
|
+
import { join as join3 } from "node:path";
|
|
19193
|
+
|
|
19177
19194
|
// src/vault/broker/protocol.ts
|
|
19195
|
+
init_zod();
|
|
19178
19196
|
var MAX_FRAME_BYTES = 64 * 1024;
|
|
19179
19197
|
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), {
|
|
19180
19198
|
message: "agent name is reserved"
|
|
@@ -19562,17 +19580,6 @@ init_merge();
|
|
|
19562
19580
|
init_paths();
|
|
19563
19581
|
|
|
19564
19582
|
// src/agents/compose.ts
|
|
19565
|
-
var AGENT_UID_MIN = 10001;
|
|
19566
|
-
var AGENT_UID_MAX = 10999;
|
|
19567
|
-
function allocateAgentUid(name) {
|
|
19568
|
-
if (isReservedAgentName(name)) {
|
|
19569
|
-
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.`);
|
|
19570
|
-
}
|
|
19571
|
-
const hash = createHash("sha256").update(name).digest();
|
|
19572
|
-
const u32 = hash.readUInt32BE(0);
|
|
19573
|
-
const range = AGENT_UID_MAX - AGENT_UID_MIN + 1;
|
|
19574
|
-
return AGENT_UID_MIN + u32 % range;
|
|
19575
|
-
}
|
|
19576
19583
|
var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
|
|
19577
19584
|
|
|
19578
19585
|
// src/vault/broker/server.ts
|