switchroom 0.14.91 → 0.14.93
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 +1030 -56
- package/dist/auth-broker/index.js +50 -3
- package/dist/cli/notion-write-pretool.mjs +50 -3
- package/dist/cli/switchroom.js +1239 -906
- package/dist/host-control/main.js +50 -3
- package/dist/vault/approvals/kernel-server.js +51 -4
- package/dist/vault/broker/server.js +51 -4
- package/package.json +1 -1
- package/profiles/_base/cron-session.sh.hbs +77 -0
- package/profiles/_base/start.sh.hbs +13 -0
- package/telegram-plugin/dist/gateway/gateway.js +139 -19
- package/telegram-plugin/gateway/cron-session.ts +45 -0
- package/telegram-plugin/gateway/gateway.ts +133 -10
- package/telegram-plugin/gateway/obligation-ledger.ts +47 -8
- package/telegram-plugin/gateway/turn-active-marker.ts +22 -0
- package/telegram-plugin/tests/cron-session.test.ts +32 -0
- package/telegram-plugin/tests/obligation-determinism.test.ts +63 -3
- package/telegram-plugin/tests/obligation-ledger.test.ts +85 -0
- package/telegram-plugin/tests/turn-active-marker.test.ts +28 -0
|
@@ -6932,7 +6932,7 @@ var require_public_api = __commonJS((exports) => {
|
|
|
6932
6932
|
});
|
|
6933
6933
|
|
|
6934
6934
|
// src/agent-scheduler/index.ts
|
|
6935
|
-
import { resolve as resolve4, join as
|
|
6935
|
+
import { resolve as resolve4, join as join3 } from "node:path";
|
|
6936
6936
|
|
|
6937
6937
|
// src/config/loader.ts
|
|
6938
6938
|
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "node:fs";
|
|
@@ -10969,15 +10969,54 @@ var AgentBindMountSchema = exports_external.object({
|
|
|
10969
10969
|
target: exports_external.string().optional().describe("Container path the source mounts to. Must be absolute. Defaults to " + "the same path as `source` (matches switchroom's existing dual-mount " + "convention so absolute paths in scaffolded scripts Just Work)."),
|
|
10970
10970
|
mode: exports_external.enum(["ro", "rw"]).optional().describe("Read-only (default) or read-write. Use `rw` only when the agent " + "must mutate the host path (e.g. editing switchroom source). " + "Default: 'ro'.")
|
|
10971
10971
|
});
|
|
10972
|
+
var HttpDiffPollSchema = exports_external.object({
|
|
10973
|
+
type: exports_external.literal("http-diff"),
|
|
10974
|
+
url: exports_external.string().url().describe("Poll target. Host MUST match the operator egress allowlist (§6.1) — " + "loopback/private/link-local/non-https are rejected; the IP is " + "resolve-then-pinned against DNS-rebind. Not agent-writable without " + "operator commit."),
|
|
10975
|
+
method: exports_external.enum(["GET", "POST"]).default("GET"),
|
|
10976
|
+
headers: exports_external.record(exports_external.string()).optional(),
|
|
10977
|
+
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault keys this poll may inject into request headers. Each is " + "HOST-PINNED (§6.1): a secret may only be sent to the host it is bound " + "to in operator config, so an approved poll cannot exfil it elsewhere."),
|
|
10978
|
+
diff_jsonpath: exports_external.string().describe("JSONPath into the response; the extracted value is compared to state_key."),
|
|
10979
|
+
state_key: exports_external.string().describe("Key under /state/agent/poll-state.json holding the last-seen value.")
|
|
10980
|
+
});
|
|
10981
|
+
var TelegramReactionsPollSchema = exports_external.object({
|
|
10982
|
+
type: exports_external.literal("telegram-reactions"),
|
|
10983
|
+
chat_id: exports_external.union([exports_external.string().min(1), exports_external.number().int()]).describe("Chat to scan for reactions (model-free internal gateway query)."),
|
|
10984
|
+
emoji: exports_external.string().min(1).describe("Reaction emoji that marks a message for capture (e.g. \uD83D\uDC68\uD83D\uDCBB)."),
|
|
10985
|
+
lookback: exports_external.number().int().positive().max(200).default(40),
|
|
10986
|
+
state_key: exports_external.string().describe("Key under poll-state.json holding the last-processed message id.")
|
|
10987
|
+
});
|
|
10988
|
+
var PollSpecSchema = exports_external.discriminatedUnion("type", [
|
|
10989
|
+
HttpDiffPollSchema,
|
|
10990
|
+
TelegramReactionsPollSchema
|
|
10991
|
+
]);
|
|
10972
10992
|
var ScheduleEntrySchema = exports_external.object({
|
|
10973
10993
|
cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
|
|
10974
|
-
prompt: exports_external.string().describe("Prompt to send at the scheduled time"),
|
|
10975
|
-
|
|
10994
|
+
prompt: exports_external.string().describe("Prompt to send at the scheduled time (the escalation prompt when kind=poll; templated with {{diff}})."),
|
|
10995
|
+
kind: exports_external.enum(["poll", "prompt"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. Honoured only when SWITCHROOM_CHEAP_CRON is on."),
|
|
10996
|
+
poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
|
|
10997
|
+
model: exports_external.string().optional().describe("Cron model hint. Reactivated by SWITCHROOM_CHEAP_CRON (was DEPRECATED/" + "IGNORED in v0.8). A known-cheap id (sonnet/haiku family) routes the " + "fire to a fresh cheap cron session (Tier 1, `context: fresh`); 'opus', " + "a custom id, or unset routes to the agent's live session (Tier 2, " + "`context: agent`) — the conservative default that preserves pre-v0.8 " + "behaviour. Note: a live session's model is fixed at launch, so on Tier " + "2 this is informational. See docs/scheduling.md."),
|
|
10998
|
+
context: exports_external.enum(["fresh", "agent"]).optional().describe("Does this cron need the agent, or just a model? 'fresh' → a minimal-" + "context cheap cron session (Tier 1). 'agent' → the agent's live " + "session with full persona/memory (Tier 2). Unset → inferred from " + "`model` (cheap→fresh, else agent). Honoured only when " + "SWITCHROOM_CHEAP_CRON is on."),
|
|
10976
10999
|
secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault key names this cron task may read via the vault-broker daemon. " + "Empty by default — broker requests for unlisted keys are denied. " + "Note: this is misconfiguration protection (a typo in cron-A doesn't " + "accidentally read cron-B's keys) rather than a security boundary — " + "anyone who can edit cron scripts can also edit switchroom.yaml, and " + "anyone with the vault passphrase can read the vault file directly. " + "See docs/configuration.md for the full framing."),
|
|
10977
11000
|
topic: exports_external.union([
|
|
10978
11001
|
exports_external.string().min(1, "topic alias must be non-empty"),
|
|
10979
11002
|
exports_external.number().int().positive("topic ID must be a positive integer")
|
|
10980
11003
|
]).optional().describe("Forum topic this cron fires into when the owning agent is in " + "supergroup-owned mode (channels.telegram.chat_id set). Either a " + 'string alias resolved against `topic_aliases` (e.g. "planning") ' + "or a numeric topic ID. Falls back to the agent's `default_topic_id` " + "when unset. Ignored for agents in fleet-shared or dm_only mode. " + "Alias-resolution happens at config-load — typos surface immediately. " + "See docs/rfcs/supergroup-mode.md.")
|
|
11004
|
+
}).superRefine((entry, ctx) => {
|
|
11005
|
+
const kind = entry.kind ?? "prompt";
|
|
11006
|
+
if (kind === "poll" && !entry.poll) {
|
|
11007
|
+
ctx.addIssue({
|
|
11008
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11009
|
+
path: ["poll"],
|
|
11010
|
+
message: "kind: poll requires a `poll` spec (http-diff or telegram-reactions)."
|
|
11011
|
+
});
|
|
11012
|
+
}
|
|
11013
|
+
if (kind === "prompt" && entry.poll) {
|
|
11014
|
+
ctx.addIssue({
|
|
11015
|
+
code: exports_external.ZodIssueCode.custom,
|
|
11016
|
+
path: ["poll"],
|
|
11017
|
+
message: "`poll` is only valid when kind: poll."
|
|
11018
|
+
});
|
|
11019
|
+
}
|
|
10981
11020
|
});
|
|
10982
11021
|
var AgentSoulSchema = exports_external.object({
|
|
10983
11022
|
name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
|
|
@@ -11424,6 +11463,13 @@ var HostdConfigSchema = exports_external.object({
|
|
|
11424
11463
|
config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit §3). Default false — the verb returns " + "`E_CONFIG_EDIT_DISABLED` until the operator explicitly flips " + "this to true. When true (and once PR 1c lands the apply path), " + "admin agents can propose unified-diff patches against " + "`/state/config/switchroom.yaml`, gated by an operator approval " + "card in the primary chat. Same trust posture as `update_apply` " + "and `agent_restart`: the human-in-the-loop tap is the security " + "boundary, not the agent's judgement."),
|
|
11425
11464
|
config_edit_rate_per_hour: exports_external.number().int().min(1).max(20).default(3).describe("Per-requesting-agent rate cap for `config_propose_edit` cards " + "(RFC admin-agent-config-edit §5). Default 3 cards/hour; min 1, " + "max 20. Implemented as a sqlite token bucket in PR 1c; the " + "field is wired here in PR 1a so operators can pin it before the " + "limiter is live. Above the cap, the verb returns " + "`E_RATE_LIMITED` without raising a card.")
|
|
11426
11465
|
});
|
|
11466
|
+
var CronEgressSchema = exports_external.object({
|
|
11467
|
+
allowed_hosts: exports_external.array(exports_external.string().min(1)).default([]).describe("Hosts a poll may reach (exact, https-only). loopback/private/IP-literal are always rejected."),
|
|
11468
|
+
secret_bindings: exports_external.record(exports_external.string(), exports_external.string().min(1)).default({}).describe("secretName → the single host it may be sent to. A poll carrying a secret to any other host is rejected.")
|
|
11469
|
+
});
|
|
11470
|
+
var CronConfigSchema = exports_external.object({
|
|
11471
|
+
egress: CronEgressSchema.optional().describe("SSRF/exfil fence for http-diff polls.")
|
|
11472
|
+
});
|
|
11427
11473
|
var SwitchroomConfigSchema = exports_external.object({
|
|
11428
11474
|
switchroom: exports_external.object({
|
|
11429
11475
|
version: exports_external.literal(1).describe("Config schema version"),
|
|
@@ -11472,7 +11518,8 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
11472
11518
|
profiles: exports_external.record(exports_external.string(), ProfileSchema).optional().describe("Named profile definitions. Agents reference via `extends: <name>`. " + "Inline profiles declared here take priority over filesystem " + "profiles/<name>/ directories when both exist."),
|
|
11473
11519
|
agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
|
|
11474
11520
|
message: "Agent name must start with a letter/digit, contain only lowercase letters/digits/hyphens/underscores, and be at most 51 characters (Telegram callback_data byte limit)"
|
|
11475
|
-
}), AgentSchema).describe("Map of agent name to agent configuration")
|
|
11521
|
+
}), AgentSchema).describe("Map of agent name to agent configuration"),
|
|
11522
|
+
cron: CronConfigSchema.optional().describe("Cheap-cron settings (docs/rfcs/cheap-cron-sessions.md). Operator-owned " + "egress allowlist + host-pinned secret bindings for Tier-0 http-diff " + "polls (§6.1). Required to enable any http-diff poll; not agent-writable.")
|
|
11476
11523
|
});
|
|
11477
11524
|
|
|
11478
11525
|
// src/config/paths.ts
|
|
@@ -12162,7 +12209,11 @@ function collectScheduleEntries(config) {
|
|
|
12162
12209
|
cron: entry.cron,
|
|
12163
12210
|
prompt: entry.prompt,
|
|
12164
12211
|
promptKey: createHash("sha256").update(entry.prompt).digest("hex").slice(0, 12),
|
|
12165
|
-
...entry.topic !== undefined ? { topic: entry.topic } : {}
|
|
12212
|
+
...entry.topic !== undefined ? { topic: entry.topic } : {},
|
|
12213
|
+
...entry.kind !== undefined ? { kind: entry.kind } : {},
|
|
12214
|
+
...entry.model !== undefined ? { model: entry.model } : {},
|
|
12215
|
+
...entry.context !== undefined ? { context: entry.context } : {},
|
|
12216
|
+
...entry.poll !== undefined ? { poll: entry.poll } : {}
|
|
12166
12217
|
});
|
|
12167
12218
|
}
|
|
12168
12219
|
}
|
|
@@ -12182,13 +12233,813 @@ function dispatchAsInbound(entry, options, dispatcher) {
|
|
|
12182
12233
|
meta: {
|
|
12183
12234
|
source: "cron",
|
|
12184
12235
|
schedule_index: String(entry.scheduleIndex),
|
|
12185
|
-
prompt_key: entry.promptKey
|
|
12236
|
+
prompt_key: entry.promptKey,
|
|
12237
|
+
...options.session === "cron" ? { session: "cron" } : {},
|
|
12238
|
+
...options.session === "cron" && options.model ? { model: options.model } : {}
|
|
12186
12239
|
}
|
|
12187
12240
|
};
|
|
12188
12241
|
const delivered = dispatcher.sendToAgent(entry.agent, message);
|
|
12189
12242
|
return { delivered, message };
|
|
12190
12243
|
}
|
|
12191
12244
|
|
|
12245
|
+
// src/scheduler/cron-routing.ts
|
|
12246
|
+
var DEFAULT_CRON_MODEL = "claude-sonnet-4-6";
|
|
12247
|
+
var CHEAP_MODEL_RE = /(^|[^a-z])(sonnet|haiku)([^a-z]|$)/i;
|
|
12248
|
+
var OPUS_MODEL_RE = /opus/i;
|
|
12249
|
+
function isKnownCheapModel(model) {
|
|
12250
|
+
return model !== undefined && CHEAP_MODEL_RE.test(model);
|
|
12251
|
+
}
|
|
12252
|
+
function isCheapCronEnabled(env = process.env) {
|
|
12253
|
+
const v = (env.SWITCHROOM_CHEAP_CRON ?? "").toLowerCase();
|
|
12254
|
+
return v === "1" || v === "true" || v === "on";
|
|
12255
|
+
}
|
|
12256
|
+
function resolveCronModel(model) {
|
|
12257
|
+
return isKnownCheapModel(model) ? model : DEFAULT_CRON_MODEL;
|
|
12258
|
+
}
|
|
12259
|
+
function resolveCronRouting(input, opts) {
|
|
12260
|
+
if (!opts.cheapCronEnabled) {
|
|
12261
|
+
return { tier: "main", session: "main", customModelDowngrade: false };
|
|
12262
|
+
}
|
|
12263
|
+
const kind = input.kind ?? "prompt";
|
|
12264
|
+
let context;
|
|
12265
|
+
let customModelDowngrade = false;
|
|
12266
|
+
if (input.context) {
|
|
12267
|
+
context = input.context;
|
|
12268
|
+
} else if (isKnownCheapModel(input.model)) {
|
|
12269
|
+
context = "fresh";
|
|
12270
|
+
} else {
|
|
12271
|
+
context = "agent";
|
|
12272
|
+
customModelDowngrade = input.model !== undefined && !isKnownCheapModel(input.model) && !OPUS_MODEL_RE.test(input.model);
|
|
12273
|
+
}
|
|
12274
|
+
if (kind === "poll") {
|
|
12275
|
+
return { tier: "poll", session: null, customModelDowngrade };
|
|
12276
|
+
}
|
|
12277
|
+
if (context === "fresh") {
|
|
12278
|
+
return {
|
|
12279
|
+
tier: "cheap",
|
|
12280
|
+
session: "cron",
|
|
12281
|
+
cronModel: resolveCronModel(input.model),
|
|
12282
|
+
customModelDowngrade
|
|
12283
|
+
};
|
|
12284
|
+
}
|
|
12285
|
+
return { tier: "main", session: "main", customModelDowngrade };
|
|
12286
|
+
}
|
|
12287
|
+
function resolveEscalationRouting(input, opts) {
|
|
12288
|
+
return resolveCronRouting({ ...input, kind: "prompt" }, opts);
|
|
12289
|
+
}
|
|
12290
|
+
|
|
12291
|
+
// src/agent-scheduler/cheap-cron-wiring.ts
|
|
12292
|
+
import { lookup as dnsLookup } from "node:dns/promises";
|
|
12293
|
+
|
|
12294
|
+
// src/scheduler/poll-egress.ts
|
|
12295
|
+
import { isIP } from "node:net";
|
|
12296
|
+
var ok = { ok: true };
|
|
12297
|
+
var deny = (reason) => ({ ok: false, reason });
|
|
12298
|
+
function normalizeHost(host) {
|
|
12299
|
+
return host.toLowerCase().replace(/\.$/, "").replace(/^\[|\]$/g, "");
|
|
12300
|
+
}
|
|
12301
|
+
function isBlockedAddress(addr) {
|
|
12302
|
+
const v = isIP(addr);
|
|
12303
|
+
if (v === 4)
|
|
12304
|
+
return isBlockedV4(addr);
|
|
12305
|
+
if (v === 6)
|
|
12306
|
+
return isBlockedV6(addr);
|
|
12307
|
+
return false;
|
|
12308
|
+
}
|
|
12309
|
+
function isBlockedV4(addr) {
|
|
12310
|
+
const o = addr.split(".").map((n) => Number(n));
|
|
12311
|
+
if (o.length !== 4 || o.some((n) => !Number.isInteger(n) || n < 0 || n > 255))
|
|
12312
|
+
return true;
|
|
12313
|
+
const [a, b] = o;
|
|
12314
|
+
if (a === 0)
|
|
12315
|
+
return true;
|
|
12316
|
+
if (a === 10)
|
|
12317
|
+
return true;
|
|
12318
|
+
if (a === 127)
|
|
12319
|
+
return true;
|
|
12320
|
+
if (a === 169 && b === 254)
|
|
12321
|
+
return true;
|
|
12322
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
12323
|
+
return true;
|
|
12324
|
+
if (a === 192 && b === 168)
|
|
12325
|
+
return true;
|
|
12326
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
12327
|
+
return true;
|
|
12328
|
+
if (a >= 224)
|
|
12329
|
+
return true;
|
|
12330
|
+
return false;
|
|
12331
|
+
}
|
|
12332
|
+
function isBlockedV6(addr) {
|
|
12333
|
+
const a = addr.toLowerCase();
|
|
12334
|
+
if (a === "::" || a === "::1")
|
|
12335
|
+
return true;
|
|
12336
|
+
if (a.startsWith("fe80"))
|
|
12337
|
+
return true;
|
|
12338
|
+
if (a.startsWith("fc") || a.startsWith("fd"))
|
|
12339
|
+
return true;
|
|
12340
|
+
if (a.startsWith("ff"))
|
|
12341
|
+
return true;
|
|
12342
|
+
const mapped = a.match(/::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
12343
|
+
if (mapped)
|
|
12344
|
+
return isBlockedV4(mapped[1]);
|
|
12345
|
+
const hexMapped = a.match(/::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
12346
|
+
if (hexMapped) {
|
|
12347
|
+
const hi = parseInt(hexMapped[1], 16);
|
|
12348
|
+
const lo = parseInt(hexMapped[2], 16);
|
|
12349
|
+
const v4 = `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
|
|
12350
|
+
return isBlockedV4(v4);
|
|
12351
|
+
}
|
|
12352
|
+
return false;
|
|
12353
|
+
}
|
|
12354
|
+
function checkEgressUrl(rawUrl, allow) {
|
|
12355
|
+
let u;
|
|
12356
|
+
try {
|
|
12357
|
+
u = new URL(rawUrl);
|
|
12358
|
+
} catch {
|
|
12359
|
+
return deny(`unparseable url`);
|
|
12360
|
+
}
|
|
12361
|
+
if (u.protocol !== "https:")
|
|
12362
|
+
return deny(`scheme ${u.protocol} not allowed (https only)`);
|
|
12363
|
+
if (u.username || u.password)
|
|
12364
|
+
return deny(`userinfo in url not allowed`);
|
|
12365
|
+
const host = normalizeHost(u.hostname);
|
|
12366
|
+
if (!host)
|
|
12367
|
+
return deny(`empty host`);
|
|
12368
|
+
if (isIP(host) && isBlockedAddress(host))
|
|
12369
|
+
return deny(`blocked ip literal ${host}`);
|
|
12370
|
+
const allowed = allow.hosts.map(normalizeHost);
|
|
12371
|
+
if (!allowed.includes(host))
|
|
12372
|
+
return deny(`host ${host} not on egress allowlist`);
|
|
12373
|
+
return ok;
|
|
12374
|
+
}
|
|
12375
|
+
function checkSecretBinding(secretName, host, allow) {
|
|
12376
|
+
const bound = allow.secretBindings[secretName];
|
|
12377
|
+
if (!bound)
|
|
12378
|
+
return deny(`secret ${secretName} has no host binding`);
|
|
12379
|
+
if (normalizeHost(bound) !== normalizeHost(host)) {
|
|
12380
|
+
return deny(`secret ${secretName} bound to ${bound}, not ${host}`);
|
|
12381
|
+
}
|
|
12382
|
+
return ok;
|
|
12383
|
+
}
|
|
12384
|
+
function checkHttpDiffEgress(rawUrl, secrets, allow) {
|
|
12385
|
+
const urlCheck = checkEgressUrl(rawUrl, allow);
|
|
12386
|
+
if (!urlCheck.ok)
|
|
12387
|
+
return urlCheck;
|
|
12388
|
+
const host = normalizeHost(new URL(rawUrl).hostname);
|
|
12389
|
+
for (const s of secrets) {
|
|
12390
|
+
const sc = checkSecretBinding(s, host, allow);
|
|
12391
|
+
if (!sc.ok)
|
|
12392
|
+
return sc;
|
|
12393
|
+
}
|
|
12394
|
+
return ok;
|
|
12395
|
+
}
|
|
12396
|
+
|
|
12397
|
+
// src/scheduler/poll-engine.ts
|
|
12398
|
+
var TOKEN_RE = /\.([a-zA-Z0-9_]+)|\[(\*|\d+)\]/g;
|
|
12399
|
+
function extractLeaves(root, jsonpath) {
|
|
12400
|
+
const path = jsonpath.startsWith("$") ? jsonpath.slice(1) : jsonpath;
|
|
12401
|
+
let nodes = [root];
|
|
12402
|
+
let m;
|
|
12403
|
+
TOKEN_RE.lastIndex = 0;
|
|
12404
|
+
while ((m = TOKEN_RE.exec(path)) !== null) {
|
|
12405
|
+
const key = m[1];
|
|
12406
|
+
const idx = m[2];
|
|
12407
|
+
const next = [];
|
|
12408
|
+
for (const node of nodes) {
|
|
12409
|
+
if (node == null)
|
|
12410
|
+
continue;
|
|
12411
|
+
if (key !== undefined) {
|
|
12412
|
+
if (typeof node === "object")
|
|
12413
|
+
next.push(node[key]);
|
|
12414
|
+
} else if (idx === "*") {
|
|
12415
|
+
if (Array.isArray(node))
|
|
12416
|
+
next.push(...node);
|
|
12417
|
+
} else if (idx !== undefined) {
|
|
12418
|
+
if (Array.isArray(node))
|
|
12419
|
+
next.push(node[Number(idx)]);
|
|
12420
|
+
}
|
|
12421
|
+
}
|
|
12422
|
+
nodes = next;
|
|
12423
|
+
}
|
|
12424
|
+
return nodes.filter((n) => n !== undefined);
|
|
12425
|
+
}
|
|
12426
|
+
function cursorOf(leaves) {
|
|
12427
|
+
const nums = leaves.map((l) => Number(l)).filter((n) => Number.isFinite(n));
|
|
12428
|
+
if (nums.length === leaves.length && nums.length > 0) {
|
|
12429
|
+
return { cursor: String(Math.max(...nums)), numeric: true, count: leaves.length };
|
|
12430
|
+
}
|
|
12431
|
+
return { cursor: `n:${leaves.length}`, numeric: false, count: leaves.length };
|
|
12432
|
+
}
|
|
12433
|
+
function cursorAdvanced(prev, next, numeric) {
|
|
12434
|
+
if (prev === undefined)
|
|
12435
|
+
return false;
|
|
12436
|
+
if (numeric)
|
|
12437
|
+
return Number(next) > Number(prev);
|
|
12438
|
+
return next !== prev;
|
|
12439
|
+
}
|
|
12440
|
+
function substituteSecrets(headers, secretValues) {
|
|
12441
|
+
const out = {};
|
|
12442
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
12443
|
+
out[k] = v.replace(/\{\{([a-zA-Z0-9_\-/]+)\}\}/g, (_, name) => secretValues[name] ?? "");
|
|
12444
|
+
}
|
|
12445
|
+
return out;
|
|
12446
|
+
}
|
|
12447
|
+
async function runHttpDiffPoll(spec, prevCursor, deps) {
|
|
12448
|
+
const gate = checkHttpDiffEgress(spec.url, spec.secrets, deps.allow);
|
|
12449
|
+
if (!gate.ok)
|
|
12450
|
+
return { hit: false, baseline: false, error: `egress denied: ${gate.reason}` };
|
|
12451
|
+
const host = normalizeHost(new URL(spec.url).hostname);
|
|
12452
|
+
try {
|
|
12453
|
+
const ip = await deps.lookup(host);
|
|
12454
|
+
if (ip && isBlockedAddress(ip))
|
|
12455
|
+
return { hit: false, baseline: false, error: `resolved ip ${ip} blocked` };
|
|
12456
|
+
} catch (e) {
|
|
12457
|
+
return { hit: false, baseline: false, error: `dns lookup failed: ${e.message}` };
|
|
12458
|
+
}
|
|
12459
|
+
const secretValues = {};
|
|
12460
|
+
for (const name of spec.secrets) {
|
|
12461
|
+
try {
|
|
12462
|
+
secretValues[name] = await deps.resolveSecret(name);
|
|
12463
|
+
} catch (e) {
|
|
12464
|
+
return { hit: false, baseline: false, error: `secret ${name}: ${e.message}` };
|
|
12465
|
+
}
|
|
12466
|
+
}
|
|
12467
|
+
const headers = substituteSecrets(spec.headers ?? {}, secretValues);
|
|
12468
|
+
const controller = new AbortController;
|
|
12469
|
+
const timer = setTimeout(() => controller.abort(), deps.timeoutMs ?? 5000);
|
|
12470
|
+
let json;
|
|
12471
|
+
try {
|
|
12472
|
+
const res = await deps.fetchImpl(spec.url, {
|
|
12473
|
+
method: spec.method,
|
|
12474
|
+
headers,
|
|
12475
|
+
redirect: "error",
|
|
12476
|
+
signal: controller.signal
|
|
12477
|
+
});
|
|
12478
|
+
if (!res.ok)
|
|
12479
|
+
return { hit: false, baseline: false, error: `http ${res.status}` };
|
|
12480
|
+
const text = await readCapped(res, deps.maxBytes ?? 1024 * 1024);
|
|
12481
|
+
json = JSON.parse(text);
|
|
12482
|
+
} catch (e) {
|
|
12483
|
+
return { hit: false, baseline: false, error: `fetch failed: ${e.message}` };
|
|
12484
|
+
} finally {
|
|
12485
|
+
clearTimeout(timer);
|
|
12486
|
+
}
|
|
12487
|
+
const leaves = extractLeaves(json, spec.diff_jsonpath);
|
|
12488
|
+
const { cursor, numeric, count } = cursorOf(leaves);
|
|
12489
|
+
if (prevCursor === undefined) {
|
|
12490
|
+
return { hit: false, baseline: true, cursor };
|
|
12491
|
+
}
|
|
12492
|
+
if (cursorAdvanced(prevCursor, cursor, numeric)) {
|
|
12493
|
+
const diff = numeric ? `cursor ${cursor} > ${prevCursor} (${count} item(s))` : `${count} item(s), cursor ${prevCursor} → ${cursor}`;
|
|
12494
|
+
return { hit: true, baseline: false, cursor, diff };
|
|
12495
|
+
}
|
|
12496
|
+
return { hit: false, baseline: false, cursor };
|
|
12497
|
+
}
|
|
12498
|
+
async function readCapped(res, maxBytes) {
|
|
12499
|
+
const buf = await res.arrayBuffer();
|
|
12500
|
+
if (buf.byteLength > maxBytes)
|
|
12501
|
+
throw new Error(`response ${buf.byteLength}B exceeds cap ${maxBytes}B`);
|
|
12502
|
+
return new TextDecoder().decode(buf);
|
|
12503
|
+
}
|
|
12504
|
+
|
|
12505
|
+
// src/scheduler/poll-state.ts
|
|
12506
|
+
import { mkdirSync, readFileSync as readFileSync3, renameSync, writeFileSync } from "node:fs";
|
|
12507
|
+
import { dirname } from "node:path";
|
|
12508
|
+
function createFilePollStateStore(path) {
|
|
12509
|
+
function read() {
|
|
12510
|
+
try {
|
|
12511
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
12512
|
+
} catch {
|
|
12513
|
+
return {};
|
|
12514
|
+
}
|
|
12515
|
+
}
|
|
12516
|
+
function write(state) {
|
|
12517
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
12518
|
+
const tmp = `${path}.tmp`;
|
|
12519
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2), "utf8");
|
|
12520
|
+
renameSync(tmp, path);
|
|
12521
|
+
}
|
|
12522
|
+
return {
|
|
12523
|
+
get(key) {
|
|
12524
|
+
return read()[key];
|
|
12525
|
+
},
|
|
12526
|
+
writeAhead(key, value, fireTs, diff) {
|
|
12527
|
+
const state = read();
|
|
12528
|
+
state[key] = { value, escalatedAt: fireTs, pendingEscalation: diff };
|
|
12529
|
+
write(state);
|
|
12530
|
+
},
|
|
12531
|
+
setBaseline(key, value) {
|
|
12532
|
+
const state = read();
|
|
12533
|
+
const prev = state[key];
|
|
12534
|
+
state[key] = { value, escalatedAt: prev?.escalatedAt };
|
|
12535
|
+
write(state);
|
|
12536
|
+
},
|
|
12537
|
+
clearPending(key) {
|
|
12538
|
+
const state = read();
|
|
12539
|
+
const e = state[key];
|
|
12540
|
+
if (e?.pendingEscalation !== undefined) {
|
|
12541
|
+
delete e.pendingEscalation;
|
|
12542
|
+
write(state);
|
|
12543
|
+
}
|
|
12544
|
+
}
|
|
12545
|
+
};
|
|
12546
|
+
}
|
|
12547
|
+
|
|
12548
|
+
// src/vault/broker/client.ts
|
|
12549
|
+
import * as net from "node:net";
|
|
12550
|
+
import * as fs from "node:fs";
|
|
12551
|
+
import { homedir as homedir2 } from "node:os";
|
|
12552
|
+
import { join } from "node:path";
|
|
12553
|
+
|
|
12554
|
+
// src/vault/broker/peercred.ts
|
|
12555
|
+
var RESERVED_AGENT_NAMES = new Set(["operator", "hostd"]);
|
|
12556
|
+
function isReservedAgentName(name) {
|
|
12557
|
+
return RESERVED_AGENT_NAMES.has(name);
|
|
12558
|
+
}
|
|
12559
|
+
|
|
12560
|
+
// src/vault/broker/protocol.ts
|
|
12561
|
+
var MAX_FRAME_BYTES = 64 * 1024;
|
|
12562
|
+
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), {
|
|
12563
|
+
message: "agent name is reserved"
|
|
12564
|
+
});
|
|
12565
|
+
var GetRequestSchema = exports_external.object({
|
|
12566
|
+
v: exports_external.literal(1),
|
|
12567
|
+
op: exports_external.literal("get"),
|
|
12568
|
+
key: exports_external.string().min(1),
|
|
12569
|
+
filename: exports_external.string().optional(),
|
|
12570
|
+
token: exports_external.string().optional()
|
|
12571
|
+
});
|
|
12572
|
+
var PutRequestSchema = exports_external.object({
|
|
12573
|
+
v: exports_external.literal(1),
|
|
12574
|
+
op: exports_external.literal("put"),
|
|
12575
|
+
key: exports_external.string().min(1),
|
|
12576
|
+
entry: exports_external.union([
|
|
12577
|
+
exports_external.object({ kind: exports_external.literal("string"), value: exports_external.string() }),
|
|
12578
|
+
exports_external.object({ kind: exports_external.literal("binary"), value: exports_external.string() })
|
|
12579
|
+
]),
|
|
12580
|
+
token: exports_external.string().optional(),
|
|
12581
|
+
passphrase: exports_external.string().optional(),
|
|
12582
|
+
attest_via_posture: exports_external.boolean().optional()
|
|
12583
|
+
});
|
|
12584
|
+
var ListRequestSchema = exports_external.object({
|
|
12585
|
+
v: exports_external.literal(1),
|
|
12586
|
+
op: exports_external.literal("list"),
|
|
12587
|
+
token: exports_external.string().optional()
|
|
12588
|
+
});
|
|
12589
|
+
var MintGrantRequestSchema = exports_external.object({
|
|
12590
|
+
v: exports_external.literal(1),
|
|
12591
|
+
op: exports_external.literal("mint_grant"),
|
|
12592
|
+
agent: AgentNameSchema,
|
|
12593
|
+
keys: exports_external.array(exports_external.string().min(1)),
|
|
12594
|
+
ttl_seconds: exports_external.number().int().positive().nullable(),
|
|
12595
|
+
description: exports_external.string().optional(),
|
|
12596
|
+
write_keys: exports_external.array(exports_external.string().min(1)).optional(),
|
|
12597
|
+
passphrase: exports_external.string().optional(),
|
|
12598
|
+
attest_via_posture: exports_external.boolean().optional(),
|
|
12599
|
+
decision_id: exports_external.string().optional()
|
|
12600
|
+
});
|
|
12601
|
+
var ListGrantsRequestSchema = exports_external.object({
|
|
12602
|
+
v: exports_external.literal(1),
|
|
12603
|
+
op: exports_external.literal("list_grants"),
|
|
12604
|
+
agent: AgentNameSchema.optional(),
|
|
12605
|
+
passphrase: exports_external.string().optional(),
|
|
12606
|
+
attest_via_posture: exports_external.boolean().optional()
|
|
12607
|
+
});
|
|
12608
|
+
var RevokeGrantRequestSchema = exports_external.object({
|
|
12609
|
+
v: exports_external.literal(1),
|
|
12610
|
+
op: exports_external.literal("revoke_grant"),
|
|
12611
|
+
id: exports_external.string().min(1)
|
|
12612
|
+
});
|
|
12613
|
+
var StatusRequestSchema = exports_external.object({
|
|
12614
|
+
v: exports_external.literal(1),
|
|
12615
|
+
op: exports_external.literal("status")
|
|
12616
|
+
});
|
|
12617
|
+
var LockRequestSchema = exports_external.object({
|
|
12618
|
+
v: exports_external.literal(1),
|
|
12619
|
+
op: exports_external.literal("lock")
|
|
12620
|
+
});
|
|
12621
|
+
var PreflightAccessRequestSchema = exports_external.object({
|
|
12622
|
+
v: exports_external.literal(1),
|
|
12623
|
+
op: exports_external.literal("preflight_access"),
|
|
12624
|
+
agent: AgentNameSchema,
|
|
12625
|
+
keys: exports_external.array(exports_external.string().min(1)).min(1).max(128)
|
|
12626
|
+
});
|
|
12627
|
+
var OkPreflightAccessResponseSchema = exports_external.object({
|
|
12628
|
+
ok: exports_external.literal(true),
|
|
12629
|
+
op: exports_external.literal("preflight_access"),
|
|
12630
|
+
results: exports_external.array(exports_external.object({
|
|
12631
|
+
key: exports_external.string(),
|
|
12632
|
+
exists: exports_external.boolean(),
|
|
12633
|
+
acl_ok: exports_external.boolean(),
|
|
12634
|
+
acl_reason: exports_external.string().optional(),
|
|
12635
|
+
scope_ok: exports_external.boolean(),
|
|
12636
|
+
scope_reason: exports_external.string().optional()
|
|
12637
|
+
}))
|
|
12638
|
+
});
|
|
12639
|
+
var ApprovalRequestRequestSchema = exports_external.object({
|
|
12640
|
+
v: exports_external.literal(1),
|
|
12641
|
+
op: exports_external.literal("approval_request"),
|
|
12642
|
+
agent_unit: exports_external.string().min(1),
|
|
12643
|
+
scope: exports_external.string().min(1),
|
|
12644
|
+
action: exports_external.string().min(1),
|
|
12645
|
+
approver_set: exports_external.array(exports_external.string()),
|
|
12646
|
+
why: exports_external.string().optional(),
|
|
12647
|
+
ttl_ms: exports_external.number().int().positive().optional()
|
|
12648
|
+
});
|
|
12649
|
+
var ApprovalLookupRequestSchema = exports_external.object({
|
|
12650
|
+
v: exports_external.literal(1),
|
|
12651
|
+
op: exports_external.literal("approval_lookup"),
|
|
12652
|
+
agent_unit: exports_external.string().min(1),
|
|
12653
|
+
scope: exports_external.string().min(1),
|
|
12654
|
+
action: exports_external.string().min(1),
|
|
12655
|
+
current_approver_set: exports_external.array(exports_external.string())
|
|
12656
|
+
});
|
|
12657
|
+
var ApprovalConsumeRequestSchema = exports_external.object({
|
|
12658
|
+
v: exports_external.literal(1),
|
|
12659
|
+
op: exports_external.literal("approval_consume"),
|
|
12660
|
+
request_id: exports_external.string().regex(/^[0-9a-f]{32}$/)
|
|
12661
|
+
});
|
|
12662
|
+
var ApprovalRevokeRequestSchema = exports_external.object({
|
|
12663
|
+
v: exports_external.literal(1),
|
|
12664
|
+
op: exports_external.literal("approval_revoke"),
|
|
12665
|
+
decision_id: exports_external.string().min(1),
|
|
12666
|
+
actor: exports_external.string().min(1),
|
|
12667
|
+
reason: exports_external.string().optional()
|
|
12668
|
+
});
|
|
12669
|
+
var ApprovalListRequestSchema = exports_external.object({
|
|
12670
|
+
v: exports_external.literal(1),
|
|
12671
|
+
op: exports_external.literal("approval_list"),
|
|
12672
|
+
agent_unit: exports_external.string().optional()
|
|
12673
|
+
});
|
|
12674
|
+
var ApprovalDecisionModeSchema = exports_external.enum([
|
|
12675
|
+
"allow_once",
|
|
12676
|
+
"allow_always",
|
|
12677
|
+
"allow_ttl",
|
|
12678
|
+
"deny",
|
|
12679
|
+
"deny_perm"
|
|
12680
|
+
]);
|
|
12681
|
+
var ApprovalRecordRequestSchema = exports_external.object({
|
|
12682
|
+
v: exports_external.literal(1),
|
|
12683
|
+
op: exports_external.literal("approval_record"),
|
|
12684
|
+
request_id: exports_external.string().regex(/^[0-9a-f]{32}$/),
|
|
12685
|
+
decision: ApprovalDecisionModeSchema,
|
|
12686
|
+
approver_set: exports_external.array(exports_external.string()),
|
|
12687
|
+
granted_by_user_id: exports_external.number().int(),
|
|
12688
|
+
ttl_ms: exports_external.number().int().positive().nullable().optional()
|
|
12689
|
+
});
|
|
12690
|
+
var ApprovalConsumeRecordRequestSchema = exports_external.object({
|
|
12691
|
+
v: exports_external.literal(1),
|
|
12692
|
+
op: exports_external.literal("approval_consume_record"),
|
|
12693
|
+
request_id: exports_external.string().regex(/^[0-9a-f]{32}$/),
|
|
12694
|
+
decision: ApprovalDecisionModeSchema,
|
|
12695
|
+
approver_set: exports_external.array(exports_external.string()),
|
|
12696
|
+
granted_by_user_id: exports_external.number().int(),
|
|
12697
|
+
ttl_ms: exports_external.number().int().positive().nullable().optional()
|
|
12698
|
+
});
|
|
12699
|
+
var RequestSchema = exports_external.discriminatedUnion("op", [
|
|
12700
|
+
GetRequestSchema,
|
|
12701
|
+
PutRequestSchema,
|
|
12702
|
+
ListRequestSchema,
|
|
12703
|
+
StatusRequestSchema,
|
|
12704
|
+
LockRequestSchema,
|
|
12705
|
+
PreflightAccessRequestSchema,
|
|
12706
|
+
MintGrantRequestSchema,
|
|
12707
|
+
ListGrantsRequestSchema,
|
|
12708
|
+
RevokeGrantRequestSchema,
|
|
12709
|
+
ApprovalRequestRequestSchema,
|
|
12710
|
+
ApprovalLookupRequestSchema,
|
|
12711
|
+
ApprovalConsumeRequestSchema,
|
|
12712
|
+
ApprovalRevokeRequestSchema,
|
|
12713
|
+
ApprovalListRequestSchema,
|
|
12714
|
+
ApprovalRecordRequestSchema,
|
|
12715
|
+
ApprovalConsumeRecordRequestSchema
|
|
12716
|
+
]);
|
|
12717
|
+
var VaultEntrySchema = exports_external.union([
|
|
12718
|
+
exports_external.object({ kind: exports_external.literal("string"), value: exports_external.string() }),
|
|
12719
|
+
exports_external.object({ kind: exports_external.literal("binary"), value: exports_external.string() }),
|
|
12720
|
+
exports_external.object({
|
|
12721
|
+
kind: exports_external.literal("files"),
|
|
12722
|
+
files: exports_external.record(exports_external.string(), exports_external.object({
|
|
12723
|
+
encoding: exports_external.enum(["utf8", "base64"]),
|
|
12724
|
+
value: exports_external.string()
|
|
12725
|
+
}))
|
|
12726
|
+
})
|
|
12727
|
+
]);
|
|
12728
|
+
var ErrorCode = exports_external.enum([
|
|
12729
|
+
"LOCKED",
|
|
12730
|
+
"DENIED",
|
|
12731
|
+
"UNKNOWN_KEY",
|
|
12732
|
+
"BAD_REQUEST",
|
|
12733
|
+
"INTERNAL"
|
|
12734
|
+
]);
|
|
12735
|
+
var OkEntryResponseSchema = exports_external.object({
|
|
12736
|
+
ok: exports_external.literal(true),
|
|
12737
|
+
entry: VaultEntrySchema
|
|
12738
|
+
});
|
|
12739
|
+
var OkKeysResponseSchema = exports_external.object({
|
|
12740
|
+
ok: exports_external.literal(true),
|
|
12741
|
+
keys: exports_external.array(exports_external.string())
|
|
12742
|
+
});
|
|
12743
|
+
var BrokerStatus = exports_external.object({
|
|
12744
|
+
unlocked: exports_external.boolean(),
|
|
12745
|
+
keyCount: exports_external.number().int().nonnegative(),
|
|
12746
|
+
uptimeSec: exports_external.number().nonnegative()
|
|
12747
|
+
});
|
|
12748
|
+
var OkStatusResponseSchema = exports_external.object({
|
|
12749
|
+
ok: exports_external.literal(true),
|
|
12750
|
+
status: BrokerStatus
|
|
12751
|
+
});
|
|
12752
|
+
var OkLockResponseSchema = exports_external.object({
|
|
12753
|
+
ok: exports_external.literal(true),
|
|
12754
|
+
locked: exports_external.literal(true)
|
|
12755
|
+
});
|
|
12756
|
+
var OkPutResponseSchema = exports_external.object({
|
|
12757
|
+
ok: exports_external.literal(true),
|
|
12758
|
+
put: exports_external.literal(true),
|
|
12759
|
+
key: exports_external.string()
|
|
12760
|
+
});
|
|
12761
|
+
var OkMintGrantResponseSchema = exports_external.object({
|
|
12762
|
+
ok: exports_external.literal(true),
|
|
12763
|
+
token: exports_external.string(),
|
|
12764
|
+
id: exports_external.string(),
|
|
12765
|
+
expires_at: exports_external.number().nullable()
|
|
12766
|
+
});
|
|
12767
|
+
var GrantMetaSchema = exports_external.object({
|
|
12768
|
+
id: exports_external.string(),
|
|
12769
|
+
agent_slug: exports_external.string(),
|
|
12770
|
+
key_allow: exports_external.array(exports_external.string()),
|
|
12771
|
+
write_allow: exports_external.array(exports_external.string()).default([]),
|
|
12772
|
+
expires_at: exports_external.number().nullable(),
|
|
12773
|
+
created_at: exports_external.number(),
|
|
12774
|
+
description: exports_external.string().nullable()
|
|
12775
|
+
});
|
|
12776
|
+
var OkListGrantsResponseSchema = exports_external.object({
|
|
12777
|
+
ok: exports_external.literal(true),
|
|
12778
|
+
grants: exports_external.array(GrantMetaSchema)
|
|
12779
|
+
});
|
|
12780
|
+
var OkRevokeGrantResponseSchema = exports_external.object({
|
|
12781
|
+
ok: exports_external.literal(true),
|
|
12782
|
+
revoked: exports_external.boolean()
|
|
12783
|
+
});
|
|
12784
|
+
var OkApprovalRequestResponseSchema = exports_external.discriminatedUnion("state", [
|
|
12785
|
+
exports_external.object({
|
|
12786
|
+
ok: exports_external.literal(true),
|
|
12787
|
+
kind: exports_external.literal("approval_request"),
|
|
12788
|
+
state: exports_external.literal("pending"),
|
|
12789
|
+
request_id: exports_external.string(),
|
|
12790
|
+
expires_at: exports_external.number()
|
|
12791
|
+
}),
|
|
12792
|
+
exports_external.object({
|
|
12793
|
+
ok: exports_external.literal(true),
|
|
12794
|
+
kind: exports_external.literal("approval_request"),
|
|
12795
|
+
state: exports_external.literal("rate_limited"),
|
|
12796
|
+
retry_after_ms: exports_external.number()
|
|
12797
|
+
})
|
|
12798
|
+
]);
|
|
12799
|
+
var ApprovalDecisionMetaSchema = exports_external.object({
|
|
12800
|
+
id: exports_external.string(),
|
|
12801
|
+
agent_unit: exports_external.string(),
|
|
12802
|
+
scope: exports_external.string(),
|
|
12803
|
+
action: exports_external.string(),
|
|
12804
|
+
decision: ApprovalDecisionModeSchema,
|
|
12805
|
+
granted_at: exports_external.number(),
|
|
12806
|
+
granted_by_user_id: exports_external.number(),
|
|
12807
|
+
ttl_expires_at: exports_external.number().nullable(),
|
|
12808
|
+
last_used_at: exports_external.number().nullable(),
|
|
12809
|
+
revoked_at: exports_external.number().nullable(),
|
|
12810
|
+
revoke_reason: exports_external.string().nullable()
|
|
12811
|
+
});
|
|
12812
|
+
var OkApprovalLookupResponseSchema = exports_external.object({
|
|
12813
|
+
ok: exports_external.literal(true),
|
|
12814
|
+
state: exports_external.enum(["granted", "denied", "pending", "expired", "drift_revoked", "no_decision"]),
|
|
12815
|
+
decision: ApprovalDecisionMetaSchema.nullable().optional()
|
|
12816
|
+
});
|
|
12817
|
+
var OkApprovalConsumeResponseSchema = exports_external.object({
|
|
12818
|
+
ok: exports_external.literal(true),
|
|
12819
|
+
consumed: exports_external.boolean(),
|
|
12820
|
+
agent_unit: exports_external.string().optional(),
|
|
12821
|
+
scope: exports_external.string().optional(),
|
|
12822
|
+
action: exports_external.string().optional(),
|
|
12823
|
+
why: exports_external.string().nullable().optional()
|
|
12824
|
+
});
|
|
12825
|
+
var OkApprovalRevokeResponseSchema = exports_external.object({
|
|
12826
|
+
ok: exports_external.literal(true),
|
|
12827
|
+
revoked: exports_external.boolean()
|
|
12828
|
+
});
|
|
12829
|
+
var OkApprovalListResponseSchema = exports_external.object({
|
|
12830
|
+
ok: exports_external.literal(true),
|
|
12831
|
+
decisions: exports_external.array(ApprovalDecisionMetaSchema)
|
|
12832
|
+
});
|
|
12833
|
+
var OkApprovalRecordResponseSchema = exports_external.object({
|
|
12834
|
+
ok: exports_external.literal(true),
|
|
12835
|
+
decision_id: exports_external.string()
|
|
12836
|
+
});
|
|
12837
|
+
var OkApprovalConsumeRecordResponseSchema = exports_external.object({
|
|
12838
|
+
ok: exports_external.literal(true),
|
|
12839
|
+
consumed: exports_external.boolean(),
|
|
12840
|
+
decision_id: exports_external.string().optional(),
|
|
12841
|
+
agent_unit: exports_external.string().optional(),
|
|
12842
|
+
scope: exports_external.string().optional(),
|
|
12843
|
+
action: exports_external.string().optional(),
|
|
12844
|
+
why: exports_external.string().nullable().optional()
|
|
12845
|
+
});
|
|
12846
|
+
var ErrorResponseSchema = exports_external.object({
|
|
12847
|
+
ok: exports_external.literal(false),
|
|
12848
|
+
code: ErrorCode,
|
|
12849
|
+
msg: exports_external.string()
|
|
12850
|
+
});
|
|
12851
|
+
var ResponseSchema = exports_external.union([
|
|
12852
|
+
OkEntryResponseSchema,
|
|
12853
|
+
OkKeysResponseSchema,
|
|
12854
|
+
OkStatusResponseSchema,
|
|
12855
|
+
OkLockResponseSchema,
|
|
12856
|
+
OkPreflightAccessResponseSchema,
|
|
12857
|
+
OkPutResponseSchema,
|
|
12858
|
+
OkMintGrantResponseSchema,
|
|
12859
|
+
OkListGrantsResponseSchema,
|
|
12860
|
+
OkRevokeGrantResponseSchema,
|
|
12861
|
+
OkApprovalRequestResponseSchema,
|
|
12862
|
+
OkApprovalLookupResponseSchema,
|
|
12863
|
+
OkApprovalConsumeResponseSchema,
|
|
12864
|
+
OkApprovalRevokeResponseSchema,
|
|
12865
|
+
OkApprovalListResponseSchema,
|
|
12866
|
+
OkApprovalRecordResponseSchema,
|
|
12867
|
+
OkApprovalConsumeRecordResponseSchema,
|
|
12868
|
+
ErrorResponseSchema
|
|
12869
|
+
]);
|
|
12870
|
+
function encodeRequest(req) {
|
|
12871
|
+
const json = JSON.stringify(req);
|
|
12872
|
+
if (Buffer.byteLength(json, "utf8") > MAX_FRAME_BYTES) {
|
|
12873
|
+
throw new Error(`Request frame too large (${Buffer.byteLength(json, "utf8")} bytes; max ${MAX_FRAME_BYTES})`);
|
|
12874
|
+
}
|
|
12875
|
+
return json + `
|
|
12876
|
+
`;
|
|
12877
|
+
}
|
|
12878
|
+
function decodeResponse(line) {
|
|
12879
|
+
if (Buffer.byteLength(line, "utf8") > MAX_FRAME_BYTES) {
|
|
12880
|
+
throw new RangeError(`Response frame too large (${Buffer.byteLength(line, "utf8")} bytes; max ${MAX_FRAME_BYTES})`);
|
|
12881
|
+
}
|
|
12882
|
+
const obj = JSON.parse(line);
|
|
12883
|
+
return ResponseSchema.parse(obj);
|
|
12884
|
+
}
|
|
12885
|
+
|
|
12886
|
+
// src/runtime-mode.ts
|
|
12887
|
+
function isDockerRuntime() {
|
|
12888
|
+
return process.env.SWITCHROOM_RUNTIME === "docker";
|
|
12889
|
+
}
|
|
12890
|
+
|
|
12891
|
+
// src/vault/broker/client.ts
|
|
12892
|
+
var DEFAULT_TIMEOUT_MS = 2000;
|
|
12893
|
+
var LEGACY_SOCKET_PATH = join(homedir2(), ".switchroom", "vault-broker.sock");
|
|
12894
|
+
var OPERATOR_SOCKET_PATH = join(homedir2(), ".switchroom", "broker-operator", "sock");
|
|
12895
|
+
function defaultBrokerSocketPath() {
|
|
12896
|
+
if (fs.existsSync(OPERATOR_SOCKET_PATH))
|
|
12897
|
+
return OPERATOR_SOCKET_PATH;
|
|
12898
|
+
if (isDockerRuntime())
|
|
12899
|
+
return OPERATOR_SOCKET_PATH;
|
|
12900
|
+
return LEGACY_SOCKET_PATH;
|
|
12901
|
+
}
|
|
12902
|
+
function resolveBrokerSocketPath(opts) {
|
|
12903
|
+
if (opts?.socket)
|
|
12904
|
+
return opts.socket;
|
|
12905
|
+
const env = process.env.SWITCHROOM_VAULT_BROKER_SOCK;
|
|
12906
|
+
if (env)
|
|
12907
|
+
return env;
|
|
12908
|
+
if (opts?.vaultBrokerSocket)
|
|
12909
|
+
return opts.vaultBrokerSocket;
|
|
12910
|
+
return defaultBrokerSocketPath();
|
|
12911
|
+
}
|
|
12912
|
+
async function rpc(req, opts) {
|
|
12913
|
+
const socketPath = resolveBrokerSocketPath(opts);
|
|
12914
|
+
const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
12915
|
+
return new Promise((resolve4) => {
|
|
12916
|
+
let settled = false;
|
|
12917
|
+
const settle = (val) => {
|
|
12918
|
+
if (settled)
|
|
12919
|
+
return;
|
|
12920
|
+
settled = true;
|
|
12921
|
+
resolve4(val);
|
|
12922
|
+
};
|
|
12923
|
+
const client = new net.Socket;
|
|
12924
|
+
const timer = setTimeout(() => {
|
|
12925
|
+
client.destroy();
|
|
12926
|
+
settle({ kind: "unreachable", msg: `broker did not respond within ${timeoutMs}ms` });
|
|
12927
|
+
}, timeoutMs);
|
|
12928
|
+
client.on("error", (err) => {
|
|
12929
|
+
clearTimeout(timer);
|
|
12930
|
+
const code = err.code ?? "ERR";
|
|
12931
|
+
let msg;
|
|
12932
|
+
if (code === "ENOENT")
|
|
12933
|
+
msg = "broker socket not found (is the daemon running?)";
|
|
12934
|
+
else if (code === "ECONNREFUSED")
|
|
12935
|
+
msg = "broker socket exists but refused connection";
|
|
12936
|
+
else if (code === "EACCES")
|
|
12937
|
+
msg = "broker socket access denied (wrong UID?)";
|
|
12938
|
+
else
|
|
12939
|
+
msg = `broker connection failed: ${err.message}`;
|
|
12940
|
+
settle({ kind: "unreachable", msg });
|
|
12941
|
+
});
|
|
12942
|
+
let buffer = "";
|
|
12943
|
+
client.on("data", (chunk) => {
|
|
12944
|
+
buffer += chunk.toString("utf8");
|
|
12945
|
+
const newlineIdx = buffer.indexOf(`
|
|
12946
|
+
`);
|
|
12947
|
+
if (newlineIdx !== -1) {
|
|
12948
|
+
const line = buffer.slice(0, newlineIdx).trimEnd();
|
|
12949
|
+
clearTimeout(timer);
|
|
12950
|
+
client.destroy();
|
|
12951
|
+
try {
|
|
12952
|
+
const resp = decodeResponse(line);
|
|
12953
|
+
settle({ kind: "response", resp });
|
|
12954
|
+
} catch (err) {
|
|
12955
|
+
settle({
|
|
12956
|
+
kind: "unreachable",
|
|
12957
|
+
msg: `unparseable broker response: ${err instanceof Error ? err.message : String(err)}`
|
|
12958
|
+
});
|
|
12959
|
+
}
|
|
12960
|
+
}
|
|
12961
|
+
});
|
|
12962
|
+
client.on("connect", () => {
|
|
12963
|
+
try {
|
|
12964
|
+
client.write(encodeRequest(req));
|
|
12965
|
+
} catch (err) {
|
|
12966
|
+
clearTimeout(timer);
|
|
12967
|
+
client.destroy();
|
|
12968
|
+
settle({
|
|
12969
|
+
kind: "unreachable",
|
|
12970
|
+
msg: `failed to send request: ${err instanceof Error ? err.message : String(err)}`
|
|
12971
|
+
});
|
|
12972
|
+
}
|
|
12973
|
+
});
|
|
12974
|
+
client.connect({ path: socketPath });
|
|
12975
|
+
});
|
|
12976
|
+
}
|
|
12977
|
+
async function getViaBrokerStructured(key, opts) {
|
|
12978
|
+
const token = opts?.token;
|
|
12979
|
+
const result = await rpc({ v: 1, op: "get", key, ...token ? { token } : {} }, opts);
|
|
12980
|
+
if (result.kind === "unreachable") {
|
|
12981
|
+
return { kind: "unreachable", msg: result.msg };
|
|
12982
|
+
}
|
|
12983
|
+
const resp = result.resp;
|
|
12984
|
+
if (resp.ok && "entry" in resp) {
|
|
12985
|
+
return { kind: "ok", entry: resp.entry };
|
|
12986
|
+
}
|
|
12987
|
+
if (!resp.ok) {
|
|
12988
|
+
if (resp.code === "UNKNOWN_KEY") {
|
|
12989
|
+
return { kind: "not_found", code: resp.code, msg: resp.msg };
|
|
12990
|
+
}
|
|
12991
|
+
return { kind: "denied", code: resp.code, msg: resp.msg };
|
|
12992
|
+
}
|
|
12993
|
+
return { kind: "unreachable", msg: "unexpected broker response shape" };
|
|
12994
|
+
}
|
|
12995
|
+
async function getViaBroker(key, opts) {
|
|
12996
|
+
const result = await getViaBrokerStructured(key, opts);
|
|
12997
|
+
return result.kind === "ok" ? result.entry : null;
|
|
12998
|
+
}
|
|
12999
|
+
|
|
13000
|
+
// src/agent-scheduler/cheap-cron-wiring.ts
|
|
13001
|
+
var defaultResolveSecret = async (name) => {
|
|
13002
|
+
const entry = await getViaBroker(name);
|
|
13003
|
+
if (!entry)
|
|
13004
|
+
throw new Error(`secret '${name}' unavailable from vault broker`);
|
|
13005
|
+
if (entry.kind !== "string")
|
|
13006
|
+
throw new Error(`secret '${name}' is not a string secret`);
|
|
13007
|
+
return entry.value;
|
|
13008
|
+
};
|
|
13009
|
+
var defaultLookup = async (host) => {
|
|
13010
|
+
try {
|
|
13011
|
+
return (await dnsLookup(host)).address;
|
|
13012
|
+
} catch {
|
|
13013
|
+
return null;
|
|
13014
|
+
}
|
|
13015
|
+
};
|
|
13016
|
+
function buildCheapCronHooks(config, env, deps = {}) {
|
|
13017
|
+
if (!isCheapCronEnabled(env))
|
|
13018
|
+
return;
|
|
13019
|
+
const egress = {
|
|
13020
|
+
hosts: config.cron?.egress?.allowed_hosts ?? [],
|
|
13021
|
+
secretBindings: config.cron?.egress?.secret_bindings ?? {}
|
|
13022
|
+
};
|
|
13023
|
+
const pollState = deps.pollState ?? createFilePollStateStore(env.SWITCHROOM_AGENT_POLL_STATE ?? "/state/agent/poll-state.json");
|
|
13024
|
+
const resolveSecret = deps.resolveSecret ?? defaultResolveSecret;
|
|
13025
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
13026
|
+
const lookup = deps.lookup ?? defaultLookup;
|
|
13027
|
+
const now = deps.now ?? Date.now;
|
|
13028
|
+
const runPoll = async (spec, prevCursor) => {
|
|
13029
|
+
if (spec.type === "http-diff") {
|
|
13030
|
+
return runHttpDiffPoll(spec, prevCursor, {
|
|
13031
|
+
fetchImpl,
|
|
13032
|
+
resolveSecret,
|
|
13033
|
+
lookup,
|
|
13034
|
+
allow: egress,
|
|
13035
|
+
now
|
|
13036
|
+
});
|
|
13037
|
+
}
|
|
13038
|
+
return { hit: false, baseline: false, error: `poll type '${spec.type}' not yet wired (staged)` };
|
|
13039
|
+
};
|
|
13040
|
+
return { enabled: true, pollState, runPoll };
|
|
13041
|
+
}
|
|
13042
|
+
|
|
12192
13043
|
// src/telegram/topic-router.ts
|
|
12193
13044
|
var ALERTS_ALIAS = "alerts";
|
|
12194
13045
|
var ADMIN_ALIAS = "admin";
|
|
@@ -12280,13 +13131,13 @@ function resolveEntryThreadId(entry, channel) {
|
|
|
12280
13131
|
}
|
|
12281
13132
|
|
|
12282
13133
|
// src/scheduler/audit.ts
|
|
12283
|
-
import { appendFileSync, mkdirSync } from "node:fs";
|
|
12284
|
-
import { dirname } from "node:path";
|
|
13134
|
+
import { appendFileSync, mkdirSync as mkdirSync2 } from "node:fs";
|
|
13135
|
+
import { dirname as dirname2 } from "node:path";
|
|
12285
13136
|
class JsonlAuditSink {
|
|
12286
13137
|
path;
|
|
12287
13138
|
constructor(path) {
|
|
12288
13139
|
this.path = path;
|
|
12289
|
-
|
|
13140
|
+
mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
|
|
12290
13141
|
}
|
|
12291
13142
|
recordFire(r) {
|
|
12292
13143
|
appendFileSync(this.path, JSON.stringify(r) + `
|
|
@@ -12312,13 +13163,13 @@ function decideQuotaPreflight(state) {
|
|
|
12312
13163
|
}
|
|
12313
13164
|
|
|
12314
13165
|
// src/auth/broker/client.ts
|
|
12315
|
-
import * as
|
|
12316
|
-
import { homedir as
|
|
13166
|
+
import * as net2 from "node:net";
|
|
13167
|
+
import { homedir as homedir3 } from "node:os";
|
|
12317
13168
|
import { randomUUID } from "node:crypto";
|
|
12318
|
-
import { join } from "node:path";
|
|
13169
|
+
import { join as join2 } from "node:path";
|
|
12319
13170
|
|
|
12320
13171
|
// src/auth/broker/protocol.ts
|
|
12321
|
-
var
|
|
13172
|
+
var MAX_FRAME_BYTES2 = 64 * 1024;
|
|
12322
13173
|
var PROTOCOL_VERSION = 1;
|
|
12323
13174
|
var ProviderNameSchema = exports_external.enum(["anthropic", "google", "microsoft"]);
|
|
12324
13175
|
var GetCredentialsRequestSchema = exports_external.object({
|
|
@@ -12432,7 +13283,7 @@ var ProbeQuotaRequestSchema = exports_external.object({
|
|
|
12432
13283
|
accounts: exports_external.array(exports_external.string().min(1)).min(1).max(32),
|
|
12433
13284
|
timeoutMs: exports_external.number().int().positive().max(60000).optional()
|
|
12434
13285
|
});
|
|
12435
|
-
var
|
|
13286
|
+
var RequestSchema2 = exports_external.discriminatedUnion("op", [
|
|
12436
13287
|
GetCredentialsRequestSchema,
|
|
12437
13288
|
ListStateRequestSchema,
|
|
12438
13289
|
SetActiveRequestSchema,
|
|
@@ -12539,25 +13390,25 @@ var SuccessResponseSchema = exports_external.object({
|
|
|
12539
13390
|
ok: exports_external.literal(true),
|
|
12540
13391
|
data: exports_external.unknown()
|
|
12541
13392
|
});
|
|
12542
|
-
var
|
|
13393
|
+
var ErrorResponseSchema2 = exports_external.object({
|
|
12543
13394
|
v: exports_external.literal(PROTOCOL_VERSION),
|
|
12544
13395
|
id: exports_external.string(),
|
|
12545
13396
|
ok: exports_external.literal(false),
|
|
12546
13397
|
error: ErrorBodySchema
|
|
12547
13398
|
});
|
|
12548
|
-
var
|
|
13399
|
+
var ResponseSchema2 = exports_external.discriminatedUnion("ok", [
|
|
12549
13400
|
SuccessResponseSchema,
|
|
12550
|
-
|
|
13401
|
+
ErrorResponseSchema2
|
|
12551
13402
|
]);
|
|
12552
|
-
function
|
|
12553
|
-
const line = JSON.stringify(
|
|
13403
|
+
function encodeRequest2(req) {
|
|
13404
|
+
const line = JSON.stringify(RequestSchema2.parse(req)) + `
|
|
12554
13405
|
`;
|
|
12555
|
-
if (Buffer.byteLength(line, "utf-8") >
|
|
12556
|
-
throw new Error(`auth-broker request exceeds MAX_FRAME_BYTES (${
|
|
13406
|
+
if (Buffer.byteLength(line, "utf-8") > MAX_FRAME_BYTES2) {
|
|
13407
|
+
throw new Error(`auth-broker request exceeds MAX_FRAME_BYTES (${MAX_FRAME_BYTES2})`);
|
|
12557
13408
|
}
|
|
12558
13409
|
return line;
|
|
12559
13410
|
}
|
|
12560
|
-
function
|
|
13411
|
+
function decodeResponse2(line) {
|
|
12561
13412
|
const trimmed = line.endsWith(`
|
|
12562
13413
|
`) ? line.slice(0, -1) : line;
|
|
12563
13414
|
let parsed;
|
|
@@ -12566,11 +13417,11 @@ function decodeResponse(line) {
|
|
|
12566
13417
|
} catch {
|
|
12567
13418
|
throw new Error("auth-broker response is not valid JSON");
|
|
12568
13419
|
}
|
|
12569
|
-
return
|
|
13420
|
+
return ResponseSchema2.parse(parsed);
|
|
12570
13421
|
}
|
|
12571
13422
|
|
|
12572
13423
|
// src/auth/broker/client.ts
|
|
12573
|
-
var
|
|
13424
|
+
var DEFAULT_TIMEOUT_MS2 = 5000;
|
|
12574
13425
|
function reviveDate(v) {
|
|
12575
13426
|
if (v == null)
|
|
12576
13427
|
return null;
|
|
@@ -12579,8 +13430,8 @@ function reviveDate(v) {
|
|
|
12579
13430
|
const d = new Date(v);
|
|
12580
13431
|
return Number.isNaN(d.getTime()) ? null : d;
|
|
12581
13432
|
}
|
|
12582
|
-
function operatorSocketPath(home2 =
|
|
12583
|
-
return
|
|
13433
|
+
function operatorSocketPath(home2 = homedir3()) {
|
|
13434
|
+
return join2(home2, ".switchroom", "state", "auth-broker-operator", "sock");
|
|
12584
13435
|
}
|
|
12585
13436
|
function resolveAuthBrokerSocketPath(opts) {
|
|
12586
13437
|
if (opts?.socket)
|
|
@@ -12621,7 +13472,7 @@ class AuthBrokerClient {
|
|
|
12621
13472
|
closed = false;
|
|
12622
13473
|
constructor(opts = {}) {
|
|
12623
13474
|
this.socketPath = resolveAuthBrokerSocketPath(opts);
|
|
12624
|
-
this.timeoutMs = opts.timeoutMs ??
|
|
13475
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
12625
13476
|
}
|
|
12626
13477
|
getSocketPath() {
|
|
12627
13478
|
return this.socketPath;
|
|
@@ -12757,7 +13608,7 @@ class AuthBrokerClient {
|
|
|
12757
13608
|
if (this.connecting)
|
|
12758
13609
|
return this.connecting;
|
|
12759
13610
|
this.connecting = new Promise((resolve4, reject) => {
|
|
12760
|
-
const sock = new
|
|
13611
|
+
const sock = new net2.Socket;
|
|
12761
13612
|
const onError = (err) => {
|
|
12762
13613
|
sock.removeAllListeners();
|
|
12763
13614
|
sock.destroy();
|
|
@@ -12801,7 +13652,7 @@ class AuthBrokerClient {
|
|
|
12801
13652
|
continue;
|
|
12802
13653
|
let resp;
|
|
12803
13654
|
try {
|
|
12804
|
-
resp =
|
|
13655
|
+
resp = decodeResponse2(line);
|
|
12805
13656
|
} catch (err) {
|
|
12806
13657
|
const msg = `unparseable auth-broker response: ${err instanceof Error ? err.message : String(err)}`;
|
|
12807
13658
|
this.failAll(new AuthBrokerUnreachableError(msg, this.socketPath));
|
|
@@ -12839,7 +13690,7 @@ class AuthBrokerClient {
|
|
|
12839
13690
|
async send(req) {
|
|
12840
13691
|
const sock = await this.ensureConnected();
|
|
12841
13692
|
const id = req.id;
|
|
12842
|
-
const frame =
|
|
13693
|
+
const frame = encodeRequest2(req);
|
|
12843
13694
|
return new Promise((resolve4, reject) => {
|
|
12844
13695
|
const timer = setTimeout(() => {
|
|
12845
13696
|
this.pending.delete(id);
|
|
@@ -12871,7 +13722,7 @@ class AuthBrokerClient {
|
|
|
12871
13722
|
}
|
|
12872
13723
|
|
|
12873
13724
|
// src/agent-scheduler/ipc-client.ts
|
|
12874
|
-
import { createConnection } from "node:net";
|
|
13725
|
+
import { createConnection as createConnection2 } from "node:net";
|
|
12875
13726
|
function createInjectIpcClient(options) {
|
|
12876
13727
|
const {
|
|
12877
13728
|
socketPath,
|
|
@@ -12879,7 +13730,7 @@ function createInjectIpcClient(options) {
|
|
|
12879
13730
|
maxReconnectDelayMs = 30000,
|
|
12880
13731
|
connectTimeoutMs = 5000,
|
|
12881
13732
|
log = () => {},
|
|
12882
|
-
_connect = (path) =>
|
|
13733
|
+
_connect = (path) => createConnection2(path)
|
|
12883
13734
|
} = options;
|
|
12884
13735
|
let socket = null;
|
|
12885
13736
|
let connected = false;
|
|
@@ -13008,16 +13859,16 @@ function createInjectIpcClient(options) {
|
|
|
13008
13859
|
import {
|
|
13009
13860
|
closeSync,
|
|
13010
13861
|
openSync,
|
|
13011
|
-
readFileSync as
|
|
13012
|
-
statSync as
|
|
13862
|
+
readFileSync as readFileSync5,
|
|
13863
|
+
statSync as statSync3,
|
|
13013
13864
|
unlinkSync,
|
|
13014
13865
|
writeSync,
|
|
13015
|
-
mkdirSync as
|
|
13866
|
+
mkdirSync as mkdirSync3
|
|
13016
13867
|
} from "node:fs";
|
|
13017
|
-
import { dirname as
|
|
13868
|
+
import { dirname as dirname3 } from "node:path";
|
|
13018
13869
|
function readContainerBootTimeMs() {
|
|
13019
13870
|
try {
|
|
13020
|
-
const stat1 =
|
|
13871
|
+
const stat1 = readFileSync5("/proc/1/stat", "utf8");
|
|
13021
13872
|
const lastParen = stat1.lastIndexOf(")");
|
|
13022
13873
|
if (lastParen < 0)
|
|
13023
13874
|
return null;
|
|
@@ -13025,7 +13876,7 @@ function readContainerBootTimeMs() {
|
|
|
13025
13876
|
const starttimeTicks = Number(after[19]);
|
|
13026
13877
|
if (!Number.isFinite(starttimeTicks))
|
|
13027
13878
|
return null;
|
|
13028
|
-
const procStat =
|
|
13879
|
+
const procStat = readFileSync5("/proc/stat", "utf8");
|
|
13029
13880
|
const btimeLine = procStat.split(`
|
|
13030
13881
|
`).find((l) => l.startsWith("btime "));
|
|
13031
13882
|
if (!btimeLine)
|
|
@@ -13043,7 +13894,7 @@ var FRESHNESS_MARGIN_MS = 2000;
|
|
|
13043
13894
|
function acquireLock(path, pid = process.pid, options = {}) {
|
|
13044
13895
|
const bootTimeMs = "containerBootTimeMs" in options ? options.containerBootTimeMs : readContainerBootTimeMs();
|
|
13045
13896
|
try {
|
|
13046
|
-
|
|
13897
|
+
mkdirSync3(dirname3(path), { recursive: true, mode: 448 });
|
|
13047
13898
|
} catch {}
|
|
13048
13899
|
for (let attempt = 0;attempt < 2; attempt++) {
|
|
13049
13900
|
try {
|
|
@@ -13060,7 +13911,7 @@ function acquireLock(path, pid = process.pid, options = {}) {
|
|
|
13060
13911
|
}
|
|
13061
13912
|
if (bootTimeMs != null) {
|
|
13062
13913
|
try {
|
|
13063
|
-
const lockMtime =
|
|
13914
|
+
const lockMtime = statSync3(path).mtimeMs;
|
|
13064
13915
|
if (lockMtime < bootTimeMs - FRESHNESS_MARGIN_MS) {
|
|
13065
13916
|
try {
|
|
13066
13917
|
unlinkSync(path);
|
|
@@ -13071,7 +13922,7 @@ function acquireLock(path, pid = process.pid, options = {}) {
|
|
|
13071
13922
|
}
|
|
13072
13923
|
let holderPid;
|
|
13073
13924
|
try {
|
|
13074
|
-
const raw =
|
|
13925
|
+
const raw = readFileSync5(path, "utf8").trim();
|
|
13075
13926
|
const parsed = Number.parseInt(raw, 10);
|
|
13076
13927
|
if (Number.isInteger(parsed) && parsed > 0)
|
|
13077
13928
|
holderPid = parsed;
|
|
@@ -13286,12 +14137,12 @@ function findStaleSkippedFires(opts) {
|
|
|
13286
14137
|
return out;
|
|
13287
14138
|
}
|
|
13288
14139
|
function readRecentFires(jsonlPath) {
|
|
13289
|
-
const
|
|
13290
|
-
if (!
|
|
14140
|
+
const fs2 = __require("node:fs");
|
|
14141
|
+
if (!fs2.existsSync(jsonlPath))
|
|
13291
14142
|
return [];
|
|
13292
14143
|
let raw;
|
|
13293
14144
|
try {
|
|
13294
|
-
raw =
|
|
14145
|
+
raw = fs2.readFileSync(jsonlPath, "utf8");
|
|
13295
14146
|
} catch {
|
|
13296
14147
|
return [];
|
|
13297
14148
|
}
|
|
@@ -13309,6 +14160,46 @@ function readRecentFires(jsonlPath) {
|
|
|
13309
14160
|
}
|
|
13310
14161
|
|
|
13311
14162
|
// src/agent-scheduler/index.ts
|
|
14163
|
+
function templateEscalationPrompt(prompt, diff) {
|
|
14164
|
+
return prompt.replace(/\{\{\s*diff\s*\}\}/g, diff);
|
|
14165
|
+
}
|
|
14166
|
+
function recoverPendingEscalations(opts) {
|
|
14167
|
+
let recovered = 0;
|
|
14168
|
+
for (const entry of opts.entries) {
|
|
14169
|
+
if (entry.kind !== "poll" || !entry.poll)
|
|
14170
|
+
continue;
|
|
14171
|
+
const st = opts.pollState.get(entry.poll.state_key);
|
|
14172
|
+
if (!st?.pendingEscalation)
|
|
14173
|
+
continue;
|
|
14174
|
+
const esc = resolveEscalationRouting(entry, { cheapCronEnabled: opts.cheapCronEnabled });
|
|
14175
|
+
const threadId = resolveEntryThreadId(entry, opts.channel);
|
|
14176
|
+
const startedAt = opts.now();
|
|
14177
|
+
try {
|
|
14178
|
+
const r = dispatchAsInbound({ ...entry, prompt: templateEscalationPrompt(entry.prompt, st.pendingEscalation) }, { chatId: opts.channel.chatId, threadId, now: opts.now, session: esc.session ?? "main", model: esc.cronModel }, opts.dispatcher);
|
|
14179
|
+
if (r.delivered) {
|
|
14180
|
+
opts.pollState.clearPending(entry.poll.state_key);
|
|
14181
|
+
recovered += 1;
|
|
14182
|
+
opts.log(`recovered pending escalation for poll '${entry.poll.state_key}'`);
|
|
14183
|
+
} else {
|
|
14184
|
+
opts.log(`pending escalation for '${entry.poll.state_key}' not delivered — will retry next boot`);
|
|
14185
|
+
}
|
|
14186
|
+
opts.sink?.recordFire({
|
|
14187
|
+
agent: entry.agent,
|
|
14188
|
+
scheduleIndex: entry.scheduleIndex,
|
|
14189
|
+
promptKey: entry.promptKey,
|
|
14190
|
+
exitCode: r.delivered ? 0 : -1,
|
|
14191
|
+
outputSummary: r.delivered ? "recovered pending escalation (boot)" : "pending escalation not delivered (boot)",
|
|
14192
|
+
startedAt,
|
|
14193
|
+
finishedAt: opts.now(),
|
|
14194
|
+
tier: esc.tier === "cheap" ? "cheap" : "main",
|
|
14195
|
+
...esc.cronModel ? { modelUsed: esc.cronModel } : {}
|
|
14196
|
+
});
|
|
14197
|
+
} catch (e) {
|
|
14198
|
+
opts.log(`pending-escalation recovery failed for '${entry.poll.state_key}': ${e.message}`);
|
|
14199
|
+
}
|
|
14200
|
+
}
|
|
14201
|
+
return recovered;
|
|
14202
|
+
}
|
|
13312
14203
|
var DEFAULT_MAX_QUOTA_DEFER_ATTEMPTS = 3;
|
|
13313
14204
|
function defaultQuotaDeferBackoffMs(attempt) {
|
|
13314
14205
|
return [60000, 180000, 300000][attempt] ?? 300000;
|
|
@@ -13358,25 +14249,76 @@ function registerAgentSchedule(opts) {
|
|
|
13358
14249
|
return;
|
|
13359
14250
|
}
|
|
13360
14251
|
}
|
|
14252
|
+
const cheapEnabled = opts.cheapCron?.enabled ?? false;
|
|
14253
|
+
const routing = resolveCronRouting(entry, { cheapCronEnabled: cheapEnabled });
|
|
14254
|
+
const threadId = resolveEntryThreadId(entry, opts.channel);
|
|
14255
|
+
const record = (fields) => opts.sink.recordFire({
|
|
14256
|
+
agent: entry.agent,
|
|
14257
|
+
scheduleIndex: entry.scheduleIndex,
|
|
14258
|
+
promptKey: entry.promptKey,
|
|
14259
|
+
exitCode: fields.exitCode,
|
|
14260
|
+
outputSummary: fields.summary.slice(0, 200),
|
|
14261
|
+
startedAt,
|
|
14262
|
+
finishedAt: now(),
|
|
14263
|
+
tier: fields.tier,
|
|
14264
|
+
...fields.modelUsed ? { modelUsed: fields.modelUsed } : {}
|
|
14265
|
+
});
|
|
14266
|
+
if (routing.tier === "poll" && opts.cheapCron && entry.poll) {
|
|
14267
|
+
const stateKey = entry.poll.state_key;
|
|
14268
|
+
const prev = opts.cheapCron.pollState.get(stateKey)?.value;
|
|
14269
|
+
let outcome;
|
|
14270
|
+
try {
|
|
14271
|
+
outcome = await opts.cheapCron.runPoll(entry.poll, prev);
|
|
14272
|
+
} catch (err) {
|
|
14273
|
+
outcome = { hit: false, baseline: false, error: err.message };
|
|
14274
|
+
}
|
|
14275
|
+
if (outcome.error) {
|
|
14276
|
+
record({ exitCode: -3, summary: `poll error: ${outcome.error}`, tier: "poll" });
|
|
14277
|
+
return;
|
|
14278
|
+
}
|
|
14279
|
+
if (outcome.baseline) {
|
|
14280
|
+
opts.cheapCron.pollState.setBaseline(stateKey, outcome.cursor);
|
|
14281
|
+
record({ exitCode: 0, summary: "poll baseline recorded — first run, no escalate", tier: "poll" });
|
|
14282
|
+
return;
|
|
14283
|
+
}
|
|
14284
|
+
if (!outcome.hit) {
|
|
14285
|
+
record({ exitCode: 0, summary: "HEARTBEAT_OK — no change (model-free)", tier: "poll" });
|
|
14286
|
+
return;
|
|
14287
|
+
}
|
|
14288
|
+
opts.cheapCron.pollState.writeAhead(stateKey, outcome.cursor, startedAt, outcome.diff ?? "");
|
|
14289
|
+
const esc = resolveEscalationRouting(entry, { cheapCronEnabled: cheapEnabled });
|
|
14290
|
+
let delivered2 = false;
|
|
14291
|
+
try {
|
|
14292
|
+
const r = dispatchAsInbound({ ...entry, prompt: templateEscalationPrompt(entry.prompt, outcome.diff ?? "") }, { chatId: opts.channel.chatId, threadId, now, session: esc.session ?? "main", model: esc.cronModel }, opts.dispatcher);
|
|
14293
|
+
delivered2 = r.delivered;
|
|
14294
|
+
} catch (err) {
|
|
14295
|
+
record({ exitCode: -1, summary: `escalation dispatch error: ${err.message}`, tier: esc.tier === "cheap" ? "cheap" : "main", modelUsed: esc.cronModel });
|
|
14296
|
+
return;
|
|
14297
|
+
}
|
|
14298
|
+
if (delivered2)
|
|
14299
|
+
opts.cheapCron.pollState.clearPending(stateKey);
|
|
14300
|
+
record({
|
|
14301
|
+
exitCode: delivered2 ? 0 : -1,
|
|
14302
|
+
summary: delivered2 ? `poll hit → escalated (${outcome.diff})` : "poll hit → escalation not delivered",
|
|
14303
|
+
tier: esc.tier === "cheap" ? "cheap" : "main",
|
|
14304
|
+
modelUsed: esc.cronModel
|
|
14305
|
+
});
|
|
14306
|
+
return;
|
|
14307
|
+
}
|
|
13361
14308
|
let delivered = false;
|
|
13362
14309
|
let summary = "";
|
|
13363
14310
|
try {
|
|
13364
|
-
const
|
|
13365
|
-
const result = dispatchAsInbound(entry, { chatId: opts.channel.chatId, threadId, now }, opts.dispatcher);
|
|
14311
|
+
const result = dispatchAsInbound(entry, { chatId: opts.channel.chatId, threadId, now, session: routing.session ?? "main", model: routing.cronModel }, opts.dispatcher);
|
|
13366
14312
|
delivered = result.delivered;
|
|
13367
14313
|
summary = delivered ? "delivered to bridge via gateway" : "no agent client connected — fire dropped";
|
|
13368
14314
|
} catch (err) {
|
|
13369
14315
|
summary = `dispatch error: ${err.message}`.slice(0, 200);
|
|
13370
14316
|
}
|
|
13371
|
-
|
|
13372
|
-
opts.sink.recordFire({
|
|
13373
|
-
agent: entry.agent,
|
|
13374
|
-
scheduleIndex: entry.scheduleIndex,
|
|
13375
|
-
promptKey: entry.promptKey,
|
|
14317
|
+
record({
|
|
13376
14318
|
exitCode: delivered ? 0 : -1,
|
|
13377
|
-
|
|
13378
|
-
|
|
13379
|
-
|
|
14319
|
+
summary,
|
|
14320
|
+
tier: routing.tier === "cheap" ? "cheap" : "main",
|
|
14321
|
+
modelUsed: routing.cronModel
|
|
13380
14322
|
});
|
|
13381
14323
|
};
|
|
13382
14324
|
const task = opts.cronLib.schedule(entry.cron, () => attemptFire(0));
|
|
@@ -13414,7 +14356,7 @@ async function main() {
|
|
|
13414
14356
|
}
|
|
13415
14357
|
const configPath = process.env.SWITCHROOM_CONFIG ?? "/state/config/switchroom.yaml";
|
|
13416
14358
|
const stateDir = process.env.TELEGRAM_STATE_DIR ?? "/state/agent/telegram";
|
|
13417
|
-
const socketPath = process.env.SWITCHROOM_GATEWAY_SOCKET ??
|
|
14359
|
+
const socketPath = process.env.SWITCHROOM_GATEWAY_SOCKET ?? join3(stateDir, "gateway.sock");
|
|
13418
14360
|
const jsonlPath = process.env.SWITCHROOM_AGENT_SCHEDULER_JSONL ?? "/state/agent/scheduler.jsonl";
|
|
13419
14361
|
const lockPath = process.env.SWITCHROOM_AGENT_SCHEDULER_LOCK ?? "/state/agent/scheduler.lock";
|
|
13420
14362
|
const lock = acquireLock(lockPath);
|
|
@@ -13476,8 +14418,22 @@ async function main() {
|
|
|
13476
14418
|
if (missed.length > 0) {
|
|
13477
14419
|
process.stdout.write(`agent-scheduler: replaying ${missed.length} missed fire(s) ` + `from past ${windowMinutes}min — ` + missed.map((m) => `[idx=${m.entry.scheduleIndex} key=${m.entry.promptKey}]`).join(" ") + `
|
|
13478
14420
|
`);
|
|
14421
|
+
const cheapEnabledForReplay = isCheapCronEnabled(process.env);
|
|
13479
14422
|
for (const m of missed) {
|
|
13480
14423
|
const startedAt = Date.now();
|
|
14424
|
+
if (cheapEnabledForReplay && m.entry.kind === "poll") {
|
|
14425
|
+
sink.recordFire({
|
|
14426
|
+
agent: m.entry.agent,
|
|
14427
|
+
scheduleIndex: m.entry.scheduleIndex,
|
|
14428
|
+
promptKey: m.entry.promptKey,
|
|
14429
|
+
exitCode: 0,
|
|
14430
|
+
outputSummary: "poll replay skipped — re-polls on next tick (stateful cursor)",
|
|
14431
|
+
startedAt,
|
|
14432
|
+
finishedAt: Date.now(),
|
|
14433
|
+
tier: "poll"
|
|
14434
|
+
});
|
|
14435
|
+
continue;
|
|
14436
|
+
}
|
|
13481
14437
|
const threadId = resolveEntryThreadId(m.entry, channel);
|
|
13482
14438
|
const result = dispatchAsInbound(m.entry, { chatId: channel.chatId, threadId }, dispatcher);
|
|
13483
14439
|
sink.recordFire({
|
|
@@ -13551,13 +14507,30 @@ Briefly and plainly tell the user these scheduled runs did not ` + "happen so th
|
|
|
13551
14507
|
await client.close().catch(() => {});
|
|
13552
14508
|
}
|
|
13553
14509
|
} : undefined;
|
|
14510
|
+
const cheapCron = buildCheapCronHooks(config, process.env);
|
|
14511
|
+
if (cheapCron) {
|
|
14512
|
+
const recovered = recoverPendingEscalations({
|
|
14513
|
+
entries,
|
|
14514
|
+
pollState: cheapCron.pollState,
|
|
14515
|
+
dispatcher,
|
|
14516
|
+
channel,
|
|
14517
|
+
cheapCronEnabled: true,
|
|
14518
|
+
now: Date.now,
|
|
14519
|
+
log: (m) => process.stderr.write(`agent-scheduler: ${m}
|
|
14520
|
+
`),
|
|
14521
|
+
sink
|
|
14522
|
+
});
|
|
14523
|
+
process.stdout.write(`agent-scheduler: ${agentName} cheap-cron ENABLED` + (recovered > 0 ? ` (recovered ${recovered} pending escalation(s))` : "") + `
|
|
14524
|
+
`);
|
|
14525
|
+
}
|
|
13554
14526
|
const tasks = registerAgentSchedule({
|
|
13555
14527
|
entries,
|
|
13556
14528
|
channel,
|
|
13557
14529
|
sink,
|
|
13558
14530
|
cronLib,
|
|
13559
14531
|
dispatcher,
|
|
13560
|
-
...quotaGate ? { quotaGate } : {}
|
|
14532
|
+
...quotaGate ? { quotaGate } : {},
|
|
14533
|
+
...cheapCron ? { cheapCron } : {}
|
|
13561
14534
|
});
|
|
13562
14535
|
process.stdout.write(`agent-scheduler: ${agentName} registered ${tasks.length} task(s); ` + `chat=${channel.chatId} thread=${channel.threadId ?? "(none)"} ` + `socket=${socketPath} jsonl=${jsonlPath}
|
|
13563
14536
|
`);
|
|
@@ -13583,6 +14556,7 @@ export {
|
|
|
13583
14556
|
resolveEntryThreadId,
|
|
13584
14557
|
resolveChannelTarget,
|
|
13585
14558
|
registerAgentSchedule,
|
|
14559
|
+
recoverPendingEscalations,
|
|
13586
14560
|
main,
|
|
13587
14561
|
ipcDispatcher
|
|
13588
14562
|
};
|