@workerdeck/server 3.4.0 → 3.5.0
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/build/index.d.mts +30 -2
- package/build/index.mjs +645 -123
- package/build/index.mjs.map +1 -1
- package/package.json +7 -7
package/build/index.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import { WebSocketServer } from "ws";
|
|
4
|
-
import { BrowserBridgeExecutor, PEER_MESSAGE_MAX_CHARS, PEER_RECENT_DEFAULT, PEER_RECENT_MAX, SHELL_REFUSAL, SHELL_REFUSAL as SHELL_REFUSAL$1, SessionRunner, TTY_REDRAW_CARRY, agentMayWrite, attachmentKind, checkClaudeAuth, clampShellTail, contextResetDirectoryHandle, createEngineSession, encodeShellKeys, getEngineAdapter, installContextResetDirectory, installPeerDirectory, installShellDirectory, isSlashCommand, normalizeMediaType, peerDirectoryHandle, peerSummary, recentLines, shellDirectoryHandle, shellOwnershipRefusal, shellSummary, shellTail, shellToolNeedsCard, ttyRedraws, ttyText, withoutGatewaySecrets } from "@workerdeck/core";
|
|
4
|
+
import { BrowserBridgeExecutor, PEER_MESSAGE_MAX_CHARS, PEER_RECENT_DEFAULT, PEER_RECENT_MAX, SHELL_REFUSAL, SHELL_REFUSAL as SHELL_REFUSAL$1, SessionRunner, TTY_REDRAW_CARRY, agentMayWrite, attachmentKind, checkClaudeAuth, clampShellTail, composeInstructions, contextResetDirectoryHandle, createEngineSession, encodeShellKeys, getEngineAdapter, installContextResetDirectory, installPeerDirectory, installShellDirectory, isSlashCommand, normalizeMediaType, peerDirectoryHandle, peerSummary, recentLines, shellDirectoryHandle, shellOwnershipRefusal, shellSummary, shellTail, shellToolNeedsCard, ttyRedraws, ttyText, withoutGatewaySecrets } from "@workerdeck/core";
|
|
5
5
|
import { JobQueue } from "@workerdeck/queue";
|
|
6
|
-
import { ENGINE_CAPABILITIES, PEER_MENTION_MAX, PROTOCOL_VERSION, SHELL_ARTIFACT_MAX_BYTES, SHELL_ARTIFACT_TTL_MS, SHELL_ATTACH_REPLAY_BYTES, SHELL_COLS, SHELL_COMMAND_MAX, SHELL_INPUT_MAX, SHELL_LABEL_MAX, SHELL_LINGER_MS, SHELL_MAX_COLS, SHELL_MAX_ROWS, SHELL_MAX_RUNNING_PER_SESSION, SHELL_MAX_RUNNING_TOTAL, SHELL_MIN_COLS, SHELL_MIN_ROWS, SHELL_READ_MAX_LINES, SHELL_ROWS, SHELL_SPILL_BYTES, SHELL_TAIL_FLUSH_MS, SHELL_TAIL_RING_BYTES, SHELL_WAIT_DEFAULT_MS, SHELL_WAIT_MAX_MS, costOfByModel, imagePartRef, isJobRun, mergeByModel, mergePricing, peerMentionKey, peerMentionSlug, pickCreateSessionRequest, scanPeerMentions, sessionState, subscriptionComparison, supportsPermissionMode } from "@workerdeck/protocol";
|
|
6
|
+
import { AGENT_SLEEP_AFTER_MS_DEFAULT, ENGINE_CAPABILITIES, PEER_MENTION_MAX, PROTOCOL_VERSION, SHELL_ARTIFACT_MAX_BYTES, SHELL_ARTIFACT_TTL_MS, SHELL_ATTACH_REPLAY_BYTES, SHELL_COLS, SHELL_COMMAND_MAX, SHELL_INPUT_MAX, SHELL_LABEL_MAX, SHELL_LINGER_MS, SHELL_MAX_COLS, SHELL_MAX_ROWS, SHELL_MAX_RUNNING_PER_SESSION, SHELL_MAX_RUNNING_TOTAL, SHELL_MIN_COLS, SHELL_MIN_ROWS, SHELL_READ_MAX_LINES, SHELL_ROWS, SHELL_SPILL_BYTES, SHELL_TAIL_FLUSH_MS, SHELL_TAIL_RING_BYTES, SHELL_WAIT_DEFAULT_MS, SHELL_WAIT_MAX_MS, agentRef, costOfByModel, imagePartRef, isJobRun, mergeByModel, mergePricing, peerMentionKey, peerMentionSlug, pickCreateSessionRequest, scanPeerMentions, sessionState, subscriptionComparison, supportsPermissionMode, teamReaches } from "@workerdeck/protocol";
|
|
7
7
|
import { chmodSync, closeSync, constants, createReadStream, existsSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { arch, homedir, hostname, platform, tmpdir } from "node:os";
|
|
9
9
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -520,7 +520,10 @@ const DEFAULT_PER_MINUTE = 10;
|
|
|
520
520
|
const DEFAULT_MAX_HOPS = 12;
|
|
521
521
|
const WINDOW_MS = 6e4;
|
|
522
522
|
function visible(from, to) {
|
|
523
|
-
return to.id !== from.id && scopeMatches(from.scope, to.scope);
|
|
523
|
+
return to.id !== from.id && scopeMatches(from.scope, to.scope) && teamReaches(from.agent, to.agent);
|
|
524
|
+
}
|
|
525
|
+
function relayable(info, exposed) {
|
|
526
|
+
return info.status !== "closed" && info.agent?.lead === void 0 && scopeMatches(exposed, info.scope);
|
|
524
527
|
}
|
|
525
528
|
function relayEntry(info, live) {
|
|
526
529
|
const items = info.checklist;
|
|
@@ -681,11 +684,11 @@ function createPeerService(deps) {
|
|
|
681
684
|
};
|
|
682
685
|
const exposedInfo = async (sessionId, exposed) => {
|
|
683
686
|
const info = await infoOf(sessionId);
|
|
684
|
-
return info && info
|
|
687
|
+
return info && relayable(info, exposed) ? info : void 0;
|
|
685
688
|
};
|
|
686
689
|
const relayEntries = async (exposed) => {
|
|
687
690
|
const live = new Set(registry().list().map((info) => info.id));
|
|
688
|
-
return (await allSessions()).filter((info) => info
|
|
691
|
+
return (await allSessions()).filter((info) => relayable(info, exposed)).map((info) => relayEntry(info, live.has(info.id)));
|
|
689
692
|
};
|
|
690
693
|
const relayPeek = async (sessionId, recent, exposed) => {
|
|
691
694
|
const info = await exposedInfo(sessionId, exposed);
|
|
@@ -743,8 +746,9 @@ function resolvePeerMentions(rows, text) {
|
|
|
743
746
|
else byKey.set(key, [row]);
|
|
744
747
|
};
|
|
745
748
|
for (const row of rows) {
|
|
746
|
-
|
|
747
|
-
add(peerMentionKey(row.
|
|
749
|
+
const keys = /* @__PURE__ */ new Set([peerMentionKey(peerMentionSlug(row.title, row.id)), peerMentionKey(row.id)]);
|
|
750
|
+
if (row.agent) keys.add(peerMentionKey(peerMentionSlug(row.agent, row.id)));
|
|
751
|
+
for (const key of keys) add(key, row);
|
|
748
752
|
}
|
|
749
753
|
const resolved = [];
|
|
750
754
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -760,7 +764,7 @@ function resolvePeerMentions(rows, text) {
|
|
|
760
764
|
resolved.push({
|
|
761
765
|
typed: token.body,
|
|
762
766
|
id: target.id,
|
|
763
|
-
name: target.title,
|
|
767
|
+
name: target.agent ?? target.title,
|
|
764
768
|
engine: target.engine,
|
|
765
769
|
status: target.status,
|
|
766
770
|
cwd: target.cwd,
|
|
@@ -2372,6 +2376,576 @@ function machineId() {
|
|
|
2372
2376
|
return cached;
|
|
2373
2377
|
}
|
|
2374
2378
|
//#endregion
|
|
2379
|
+
//#region src/services/agents.ts
|
|
2380
|
+
const NAMES = [
|
|
2381
|
+
"Atlas",
|
|
2382
|
+
"Juno",
|
|
2383
|
+
"Pip",
|
|
2384
|
+
"Rook",
|
|
2385
|
+
"Marlow",
|
|
2386
|
+
"Orbit",
|
|
2387
|
+
"Fern",
|
|
2388
|
+
"Quill",
|
|
2389
|
+
"Sable",
|
|
2390
|
+
"Wren",
|
|
2391
|
+
"Ember",
|
|
2392
|
+
"Moss",
|
|
2393
|
+
"Nova",
|
|
2394
|
+
"Otto",
|
|
2395
|
+
"Pike",
|
|
2396
|
+
"Rune",
|
|
2397
|
+
"Sage",
|
|
2398
|
+
"Tansy",
|
|
2399
|
+
"Vale",
|
|
2400
|
+
"Yarrow",
|
|
2401
|
+
"Basil",
|
|
2402
|
+
"Cedar",
|
|
2403
|
+
"Dune",
|
|
2404
|
+
"Flint",
|
|
2405
|
+
"Hazel",
|
|
2406
|
+
"Iris",
|
|
2407
|
+
"Kestrel",
|
|
2408
|
+
"Lark",
|
|
2409
|
+
"Maple",
|
|
2410
|
+
"Nettle",
|
|
2411
|
+
"Onyx",
|
|
2412
|
+
"Piper",
|
|
2413
|
+
"Reed",
|
|
2414
|
+
"Sorrel",
|
|
2415
|
+
"Thistle",
|
|
2416
|
+
"Umber",
|
|
2417
|
+
"Willow",
|
|
2418
|
+
"Zephyr"
|
|
2419
|
+
];
|
|
2420
|
+
const MAX_NAME = 64;
|
|
2421
|
+
const MAX_BRIEF = 16e3;
|
|
2422
|
+
const CONFIG_KEYS = [
|
|
2423
|
+
"cwd",
|
|
2424
|
+
"profile",
|
|
2425
|
+
"model",
|
|
2426
|
+
"reasoningEffort",
|
|
2427
|
+
"permissionMode",
|
|
2428
|
+
"brief",
|
|
2429
|
+
"agentContextReset",
|
|
2430
|
+
"sleepAfterMs"
|
|
2431
|
+
];
|
|
2432
|
+
var AgentService = class {
|
|
2433
|
+
#store;
|
|
2434
|
+
#basePath;
|
|
2435
|
+
#avatars;
|
|
2436
|
+
#sleepAfterMs;
|
|
2437
|
+
#now;
|
|
2438
|
+
#agents = /* @__PURE__ */ new Map();
|
|
2439
|
+
constructor(options) {
|
|
2440
|
+
this.#store = options.store;
|
|
2441
|
+
this.#basePath = options.basePath;
|
|
2442
|
+
this.#avatars = options.avatars ?? false;
|
|
2443
|
+
this.#sleepAfterMs = options.sleepAfterMs ?? AGENT_SLEEP_AFTER_MS_DEFAULT;
|
|
2444
|
+
this.#now = options.now ?? Date.now;
|
|
2445
|
+
}
|
|
2446
|
+
async hydrate() {
|
|
2447
|
+
this.#agents = new Map((await this.#store.list()).map((agent) => [agent.id, agent]));
|
|
2448
|
+
}
|
|
2449
|
+
list() {
|
|
2450
|
+
return [...this.#agents.values()].map((agent) => this.public(agent));
|
|
2451
|
+
}
|
|
2452
|
+
get(id) {
|
|
2453
|
+
return this.#agents.get(id);
|
|
2454
|
+
}
|
|
2455
|
+
bySession(sessionId) {
|
|
2456
|
+
for (const agent of this.#agents.values()) if (agent.sessionId === sessionId) return agent;
|
|
2457
|
+
}
|
|
2458
|
+
decorate(info) {
|
|
2459
|
+
const agent = this.bySession(info.id);
|
|
2460
|
+
if (!agent) return info;
|
|
2461
|
+
const leadName = agent.lead === void 0 ? void 0 : this.#agents.get(agent.lead)?.name;
|
|
2462
|
+
return {
|
|
2463
|
+
...info,
|
|
2464
|
+
agent: agentRef(this.public(agent), {
|
|
2465
|
+
leadName,
|
|
2466
|
+
leads: this.hasMembers(agent.id)
|
|
2467
|
+
})
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
briefFor(sessionId) {
|
|
2471
|
+
return (sessionId === void 0 ? void 0 : this.bySession(sessionId)?.config.brief?.trim()) || void 0;
|
|
2472
|
+
}
|
|
2473
|
+
sleepAfterFor(sessionId) {
|
|
2474
|
+
const agent = this.bySession(sessionId);
|
|
2475
|
+
return agent ? agent.config.sleepAfterMs ?? this.#sleepAfterMs : void 0;
|
|
2476
|
+
}
|
|
2477
|
+
public(agent) {
|
|
2478
|
+
const { avatarSeed: _seed, avatarRecipe: _recipe, avatar: _avatar, ...info } = agent;
|
|
2479
|
+
return this.#avatars ? {
|
|
2480
|
+
...info,
|
|
2481
|
+
avatar: `${this.#basePath}/agents/${agent.id}/avatar.png`
|
|
2482
|
+
} : info;
|
|
2483
|
+
}
|
|
2484
|
+
draft(input) {
|
|
2485
|
+
const config = readConfig(input.config);
|
|
2486
|
+
if ("error" in config) return config;
|
|
2487
|
+
const name = input.name === void 0 ? this.#suggestName() : readName(input.name);
|
|
2488
|
+
if (typeof name !== "string") return name;
|
|
2489
|
+
const id = randomUUID();
|
|
2490
|
+
const at = this.#now();
|
|
2491
|
+
const agent = {
|
|
2492
|
+
id,
|
|
2493
|
+
name,
|
|
2494
|
+
createdAt: at,
|
|
2495
|
+
updatedAt: at,
|
|
2496
|
+
avatarSeed: id,
|
|
2497
|
+
pastSessions: [],
|
|
2498
|
+
config
|
|
2499
|
+
};
|
|
2500
|
+
if (input.lead !== void 0 && input.lead !== null) {
|
|
2501
|
+
const refused = typeof input.lead === "string" ? this.leadRefusal(agent, input.lead) : {
|
|
2502
|
+
status: 400,
|
|
2503
|
+
error: "lead must be an agent id"
|
|
2504
|
+
};
|
|
2505
|
+
if (refused) return refused;
|
|
2506
|
+
agent.lead = input.lead;
|
|
2507
|
+
}
|
|
2508
|
+
return agent;
|
|
2509
|
+
}
|
|
2510
|
+
async save(agent) {
|
|
2511
|
+
const stored = {
|
|
2512
|
+
...agent,
|
|
2513
|
+
updatedAt: this.#now()
|
|
2514
|
+
};
|
|
2515
|
+
await this.#store.save(stored);
|
|
2516
|
+
this.#agents.set(stored.id, stored);
|
|
2517
|
+
return stored;
|
|
2518
|
+
}
|
|
2519
|
+
async bind(agent, sessionId) {
|
|
2520
|
+
const past = agent.sessionId !== void 0 && agent.sessionId !== sessionId ? [...agent.pastSessions, agent.sessionId] : agent.pastSessions;
|
|
2521
|
+
return this.save({
|
|
2522
|
+
...agent,
|
|
2523
|
+
sessionId,
|
|
2524
|
+
pastSessions: past
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2527
|
+
hasMembers(id) {
|
|
2528
|
+
for (const agent of this.#agents.values()) if (agent.lead === id) return true;
|
|
2529
|
+
return false;
|
|
2530
|
+
}
|
|
2531
|
+
members(id) {
|
|
2532
|
+
return [...this.#agents.values()].filter((agent) => agent.lead === id).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
2533
|
+
}
|
|
2534
|
+
leadRefusal(mover, leadId) {
|
|
2535
|
+
if (leadId === mover.id) return {
|
|
2536
|
+
status: 409,
|
|
2537
|
+
error: "an agent cannot lead its own team"
|
|
2538
|
+
};
|
|
2539
|
+
const lead = this.#agents.get(leadId);
|
|
2540
|
+
if (!lead) return {
|
|
2541
|
+
status: 404,
|
|
2542
|
+
error: `no such agent: ${leadId}`
|
|
2543
|
+
};
|
|
2544
|
+
if (lead.lead !== void 0) return {
|
|
2545
|
+
status: 409,
|
|
2546
|
+
error: `${lead.name} is a member of a team; teams are one level deep`
|
|
2547
|
+
};
|
|
2548
|
+
if (this.hasMembers(mover.id)) return {
|
|
2549
|
+
status: 409,
|
|
2550
|
+
error: `${mover.name} leads a team; teams are one level deep`
|
|
2551
|
+
};
|
|
2552
|
+
return null;
|
|
2553
|
+
}
|
|
2554
|
+
async update(id, patch) {
|
|
2555
|
+
const agent = this.#agents.get(id);
|
|
2556
|
+
if (!agent) return {
|
|
2557
|
+
status: 404,
|
|
2558
|
+
error: `no such agent: ${id}`
|
|
2559
|
+
};
|
|
2560
|
+
const next = { ...agent };
|
|
2561
|
+
if (patch.name !== void 0) {
|
|
2562
|
+
const name = readName(patch.name);
|
|
2563
|
+
if (typeof name !== "string") return name;
|
|
2564
|
+
next.name = name;
|
|
2565
|
+
}
|
|
2566
|
+
if (patch.config !== void 0) {
|
|
2567
|
+
if (!isRecord$1(patch.config)) return {
|
|
2568
|
+
status: 400,
|
|
2569
|
+
error: "config must be an object"
|
|
2570
|
+
};
|
|
2571
|
+
const config = readConfig({
|
|
2572
|
+
...agent.config,
|
|
2573
|
+
...patch.config
|
|
2574
|
+
});
|
|
2575
|
+
if ("error" in config) return config;
|
|
2576
|
+
next.config = config;
|
|
2577
|
+
}
|
|
2578
|
+
if (patch.lead === null) delete next.lead;
|
|
2579
|
+
else if (patch.lead !== void 0) {
|
|
2580
|
+
if (typeof patch.lead !== "string") return {
|
|
2581
|
+
status: 400,
|
|
2582
|
+
error: "lead must be an agent id or null"
|
|
2583
|
+
};
|
|
2584
|
+
const refused = this.leadRefusal(agent, patch.lead);
|
|
2585
|
+
if (refused) return refused;
|
|
2586
|
+
next.lead = patch.lead;
|
|
2587
|
+
}
|
|
2588
|
+
if (patch.order !== void 0) {
|
|
2589
|
+
if (typeof patch.order !== "number" || !Number.isFinite(patch.order)) return {
|
|
2590
|
+
status: 400,
|
|
2591
|
+
error: "order must be a number"
|
|
2592
|
+
};
|
|
2593
|
+
next.order = patch.order;
|
|
2594
|
+
}
|
|
2595
|
+
return this.save(next);
|
|
2596
|
+
}
|
|
2597
|
+
async retire(id, members) {
|
|
2598
|
+
const agent = this.#agents.get(id);
|
|
2599
|
+
if (!agent) return {
|
|
2600
|
+
status: 404,
|
|
2601
|
+
error: `no such agent: ${id}`
|
|
2602
|
+
};
|
|
2603
|
+
const crew = this.members(id);
|
|
2604
|
+
const retired = [agent, ...members === "retire" ? crew : []];
|
|
2605
|
+
const released = [];
|
|
2606
|
+
if (members === "release") for (const member of crew) {
|
|
2607
|
+
const { lead: _lead, ...rest } = member;
|
|
2608
|
+
released.push(await this.save(rest));
|
|
2609
|
+
}
|
|
2610
|
+
for (const gone of retired) {
|
|
2611
|
+
await this.#store.delete(gone.id);
|
|
2612
|
+
this.#agents.delete(gone.id);
|
|
2613
|
+
}
|
|
2614
|
+
return {
|
|
2615
|
+
retired,
|
|
2616
|
+
released
|
|
2617
|
+
};
|
|
2618
|
+
}
|
|
2619
|
+
#suggestName() {
|
|
2620
|
+
const taken = new Set([...this.#agents.values()].map((agent) => agent.name.toLowerCase()));
|
|
2621
|
+
const free = NAMES.find((name) => !taken.has(name.toLowerCase()));
|
|
2622
|
+
if (free) return free;
|
|
2623
|
+
for (let n = 2;; n++) {
|
|
2624
|
+
const name = `${NAMES[n % NAMES.length]} ${n}`;
|
|
2625
|
+
if (!taken.has(name.toLowerCase())) return name;
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
};
|
|
2629
|
+
function isAgentRefusal(value) {
|
|
2630
|
+
return isRecord$1(value) && typeof value.status === "number" && typeof value.error === "string";
|
|
2631
|
+
}
|
|
2632
|
+
function readName(value) {
|
|
2633
|
+
if (typeof value !== "string" || value.trim() === "") return {
|
|
2634
|
+
status: 400,
|
|
2635
|
+
error: "name must be a non-empty string"
|
|
2636
|
+
};
|
|
2637
|
+
const name = value.trim();
|
|
2638
|
+
return name.length > MAX_NAME ? {
|
|
2639
|
+
status: 400,
|
|
2640
|
+
error: `name is longer than ${MAX_NAME} characters`
|
|
2641
|
+
} : name;
|
|
2642
|
+
}
|
|
2643
|
+
function readConfig(value) {
|
|
2644
|
+
if (value === void 0) return {};
|
|
2645
|
+
if (!isRecord$1(value)) return {
|
|
2646
|
+
status: 400,
|
|
2647
|
+
error: "config must be an object"
|
|
2648
|
+
};
|
|
2649
|
+
const config = {};
|
|
2650
|
+
for (const key of CONFIG_KEYS) if (value[key] !== void 0) config[key] = value[key];
|
|
2651
|
+
for (const key of [
|
|
2652
|
+
"cwd",
|
|
2653
|
+
"profile",
|
|
2654
|
+
"model",
|
|
2655
|
+
"reasoningEffort",
|
|
2656
|
+
"permissionMode",
|
|
2657
|
+
"brief"
|
|
2658
|
+
]) if (config[key] !== void 0 && typeof config[key] !== "string") return {
|
|
2659
|
+
status: 400,
|
|
2660
|
+
error: `config.${key} must be a string`
|
|
2661
|
+
};
|
|
2662
|
+
if (config.agentContextReset !== void 0 && typeof config.agentContextReset !== "boolean") return {
|
|
2663
|
+
status: 400,
|
|
2664
|
+
error: "config.agentContextReset must be a boolean"
|
|
2665
|
+
};
|
|
2666
|
+
const sleep = config.sleepAfterMs;
|
|
2667
|
+
if (sleep !== void 0 && (typeof sleep !== "number" || !Number.isFinite(sleep) || sleep < 0)) return {
|
|
2668
|
+
status: 400,
|
|
2669
|
+
error: "config.sleepAfterMs must be a non-negative number"
|
|
2670
|
+
};
|
|
2671
|
+
if (typeof config.brief === "string" && config.brief.length > MAX_BRIEF) return {
|
|
2672
|
+
status: 400,
|
|
2673
|
+
error: `config.brief is longer than ${MAX_BRIEF} characters`
|
|
2674
|
+
};
|
|
2675
|
+
return config;
|
|
2676
|
+
}
|
|
2677
|
+
function isRecord$1(value) {
|
|
2678
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2679
|
+
}
|
|
2680
|
+
//#endregion
|
|
2681
|
+
//#region src/lib/host-only-keys.ts
|
|
2682
|
+
const HOST_ONLY_KEY_TABLE = {
|
|
2683
|
+
epoch: "durable",
|
|
2684
|
+
pricing: "transient",
|
|
2685
|
+
queryFn: "transient",
|
|
2686
|
+
env: "transient",
|
|
2687
|
+
pathToClaudeCodeExecutable: "durable",
|
|
2688
|
+
extraOptions: "transient",
|
|
2689
|
+
instructions: "transient",
|
|
2690
|
+
defaultApprovalTimeoutMs: "durable",
|
|
2691
|
+
backfillHistory: "durable",
|
|
2692
|
+
historyFn: "transient",
|
|
2693
|
+
sessionInfoFn: "transient",
|
|
2694
|
+
peers: "transient",
|
|
2695
|
+
contextReset: "transient",
|
|
2696
|
+
shells: "transient",
|
|
2697
|
+
shellAgentWrite: "transient",
|
|
2698
|
+
createdByOperator: "durable",
|
|
2699
|
+
fallbackTitle: "transient",
|
|
2700
|
+
startAsleep: "transient",
|
|
2701
|
+
effortDefaults: "transient",
|
|
2702
|
+
connectFn: "transient",
|
|
2703
|
+
codexHome: "durable",
|
|
2704
|
+
codexPathOverride: "durable",
|
|
2705
|
+
languageModel: "transient",
|
|
2706
|
+
tools: "transient",
|
|
2707
|
+
maxSteps: "durable",
|
|
2708
|
+
executor: "transient",
|
|
2709
|
+
executableTools: "transient",
|
|
2710
|
+
vfs: "transient",
|
|
2711
|
+
executionLimits: "durable",
|
|
2712
|
+
executionBackend: "transient",
|
|
2713
|
+
toolTitles: "transient",
|
|
2714
|
+
shouldApprove: "transient",
|
|
2715
|
+
resolveModel: "transient",
|
|
2716
|
+
contextWindow: "transient",
|
|
2717
|
+
reportMcpServers: "transient",
|
|
2718
|
+
onClose: "transient",
|
|
2719
|
+
restore: "transient"
|
|
2720
|
+
};
|
|
2721
|
+
const HOST_ONLY_KEYS = new Set(Object.keys(HOST_ONLY_KEY_TABLE));
|
|
2722
|
+
const DURABLE_HOST_KEYS = Object.entries(HOST_ONLY_KEY_TABLE).filter(([, lifetime]) => lifetime === "durable").map(([key]) => key);
|
|
2723
|
+
//#endregion
|
|
2724
|
+
//#region src/routes/create-vet.ts
|
|
2725
|
+
function vetCreateRequest(ctx, body, auth) {
|
|
2726
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return {
|
|
2727
|
+
ok: false,
|
|
2728
|
+
status: 400,
|
|
2729
|
+
error: "request body must be a JSON object"
|
|
2730
|
+
};
|
|
2731
|
+
const hostOnly = Object.keys(body).filter((key) => HOST_ONLY_KEYS.has(key));
|
|
2732
|
+
if (hostOnly.length > 0) return {
|
|
2733
|
+
ok: false,
|
|
2734
|
+
status: 400,
|
|
2735
|
+
error: `host-only runner config is not accepted on a session request: ${hostOnly.join(", ")} (set it from the gateway's buildRunnerConfig hook)`
|
|
2736
|
+
};
|
|
2737
|
+
const req = pickCreateSessionRequest(body);
|
|
2738
|
+
const { availability, factory } = ctx;
|
|
2739
|
+
const early = factory.applyScope(req, auth) ?? refusal(403, factory.applyBypassPolicy(req));
|
|
2740
|
+
if (early) return {
|
|
2741
|
+
ok: false,
|
|
2742
|
+
...early
|
|
2743
|
+
};
|
|
2744
|
+
const resolved = factory.resolveProfile(req.profile, auth.allowedProfiles);
|
|
2745
|
+
if (!resolved.ok) return resolved;
|
|
2746
|
+
const refused = refusal(403, refuseHostAuthority(ctx, req, resolved.profile, auth)) ?? availability.checkAvailable(resolved.profile) ?? factory.checkCwd(req, resolved.profile) ?? refusal(400, factory.checkPermissionMode(req.permissionMode, resolved.profile) ?? factory.checkEngineGrants(req, resolved.profile));
|
|
2747
|
+
if (refused) return {
|
|
2748
|
+
ok: false,
|
|
2749
|
+
...refused
|
|
2750
|
+
};
|
|
2751
|
+
factory.stripInertFields(req, resolved.profile);
|
|
2752
|
+
req.profile = resolved.profile?.name;
|
|
2753
|
+
return {
|
|
2754
|
+
ok: true,
|
|
2755
|
+
request: req
|
|
2756
|
+
};
|
|
2757
|
+
}
|
|
2758
|
+
function refusal(status, error) {
|
|
2759
|
+
return error === null ? null : {
|
|
2760
|
+
status,
|
|
2761
|
+
error
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
function refuseHostAuthority(ctx, req, profile, auth) {
|
|
2765
|
+
if (ctx.auth.isOperator(auth)) return null;
|
|
2766
|
+
const engine = engineOf(profile);
|
|
2767
|
+
const name = profile?.name ?? "default";
|
|
2768
|
+
if ((engine === "claude" || engine === "codex") && !auth.allowedProfiles?.includes(name)) return `profile '${name}' runs the ${engine} engine on the host; a non-operator caller needs it named in its allowedProfiles`;
|
|
2769
|
+
const mode = refusePermissionMode(req.permissionMode, { operator: false });
|
|
2770
|
+
if (mode) return mode;
|
|
2771
|
+
if (req.allowDangerouslySkipPermissions) return "allowDangerouslySkipPermissions is reserved to operators";
|
|
2772
|
+
if (req.settingSources !== void 0 && req.settingSources.length > 0) return "settingSources is reserved to operators: it loads hooks and settings from the host";
|
|
2773
|
+
if (!(typeof req.mcpServers === "object" && req.mcpServers !== null ? Object.values(req.mcpServers) : []).every(isRemoteMcpServer)) return "a stdio MCP server is reserved to operators: it runs a command on the host";
|
|
2774
|
+
if (req.resume !== void 0 && !ownsSdkSession(ctx, auth, req.resume)) return "resume is limited to a live session this caller can see";
|
|
2775
|
+
return null;
|
|
2776
|
+
}
|
|
2777
|
+
function ownsSdkSession(ctx, auth, sdkSessionId) {
|
|
2778
|
+
return ctx.registry.list().some((info) => info.sdkSessionId === sdkSessionId && ctx.auth.canSee(auth, info));
|
|
2779
|
+
}
|
|
2780
|
+
function isRemoteMcpServer(server) {
|
|
2781
|
+
const type = server?.type;
|
|
2782
|
+
return type === "http" || type === "sse";
|
|
2783
|
+
}
|
|
2784
|
+
//#endregion
|
|
2785
|
+
//#region src/routes/agents.ts
|
|
2786
|
+
async function handleAgents(ctx, req, res, pathname, auth) {
|
|
2787
|
+
const agents = ctx.agents;
|
|
2788
|
+
const rest = pathname.slice((ctx.basePath + "/agents").length).replace(/^\//, "");
|
|
2789
|
+
if (rest === "") {
|
|
2790
|
+
requireMethod(req, "GET", "POST");
|
|
2791
|
+
if (req.method === "GET") {
|
|
2792
|
+
json(res, 200, { agents: agents.list() });
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
await createAgent(ctx, agents, req, res, auth);
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2798
|
+
const [id, action, ...extra] = rest.split("/").map(decodeURIComponent);
|
|
2799
|
+
const agent = id === void 0 ? void 0 : agents.get(id);
|
|
2800
|
+
if (!agent || extra.length > 0) fail(404, "agent not found");
|
|
2801
|
+
if (action === "restart") {
|
|
2802
|
+
requireMethod(req, "POST");
|
|
2803
|
+
const body = await readJsonBody(req, ctx.maxBodyBytes) ?? {};
|
|
2804
|
+
const previous = agent.sessionId === void 0 ? void 0 : ctx.registry.get(agent.sessionId);
|
|
2805
|
+
const created = await startSession(ctx, agent, typeof body.prompt === "string" ? body.prompt : void 0, auth);
|
|
2806
|
+
const status = previous?.info().status;
|
|
2807
|
+
if (previous && status !== "closed" && status !== "failed") previous.close("server");
|
|
2808
|
+
await respond(ctx, res, 200, await bindWithAvatar(ctx, agent, created), created);
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2811
|
+
if (action === "avatar.png" || action === "avatar-busy.png") {
|
|
2812
|
+
requireMethod(req, "GET");
|
|
2813
|
+
await sendAvatar(ctx, req, res, agent, action === "avatar-busy.png");
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
if (action !== void 0) fail(404, "agent not found");
|
|
2817
|
+
requireMethod(req, "GET", "PATCH", "DELETE");
|
|
2818
|
+
if (req.method === "GET") {
|
|
2819
|
+
await respond(ctx, res, 200, agent);
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
if (req.method === "PATCH") {
|
|
2823
|
+
const patch = await readJsonBody(req, ctx.maxBodyBytes);
|
|
2824
|
+
const updated = await agents.update(agent.id, isRecord(patch) ? patch : {});
|
|
2825
|
+
if (isAgentRefusal(updated)) fail(updated.status, updated.error);
|
|
2826
|
+
await respond(ctx, res, 200, updated);
|
|
2827
|
+
return;
|
|
2828
|
+
}
|
|
2829
|
+
const members = (await readJsonBody(req, ctx.maxBodyBytes) ?? {}).members === "retire" ? "retire" : "release";
|
|
2830
|
+
const outcome = await agents.retire(agent.id, members);
|
|
2831
|
+
if (isAgentRefusal(outcome)) fail(outcome.status, outcome.error);
|
|
2832
|
+
for (const gone of outcome.retired) {
|
|
2833
|
+
const runner = gone.sessionId === void 0 ? void 0 : ctx.registry.get(gone.sessionId);
|
|
2834
|
+
const status = runner?.info().status;
|
|
2835
|
+
if (runner && status !== "closed" && status !== "failed") runner.close("client");
|
|
2836
|
+
}
|
|
2837
|
+
json(res, 200, {
|
|
2838
|
+
retired: outcome.retired.map((a) => a.id),
|
|
2839
|
+
released: outcome.released.map((a) => a.id)
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
2842
|
+
async function createAgent(ctx, agents, req, res, auth) {
|
|
2843
|
+
const body = await readJsonBody(req, ctx.maxBodyBytes);
|
|
2844
|
+
if (!isRecord(body)) fail(400, "request body must be a JSON object");
|
|
2845
|
+
if (body.adopt !== void 0) {
|
|
2846
|
+
await adoptSession(ctx, agents, res, body);
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
const draft = agents.draft(body);
|
|
2850
|
+
if (isAgentRefusal(draft)) fail(draft.status, draft.error);
|
|
2851
|
+
const created = await startSession(ctx, draft, typeof body.prompt === "string" ? body.prompt : void 0, auth);
|
|
2852
|
+
await respond(ctx, res, 201, await bindWithAvatar(ctx, draft, created), created);
|
|
2853
|
+
}
|
|
2854
|
+
async function adoptSession(ctx, agents, res, body) {
|
|
2855
|
+
if (typeof body.adopt !== "string") fail(400, "adopt must be a session id");
|
|
2856
|
+
const info = await sessionInfoOf(ctx, body.adopt);
|
|
2857
|
+
if (!info) fail(404, "session not found");
|
|
2858
|
+
if (agents.bySession(info.id)) fail(409, "that session already belongs to an agent");
|
|
2859
|
+
const draft = agents.draft({
|
|
2860
|
+
name: body.name ?? info.title,
|
|
2861
|
+
config: {
|
|
2862
|
+
...configOf(info),
|
|
2863
|
+
...body.config
|
|
2864
|
+
},
|
|
2865
|
+
lead: body.lead
|
|
2866
|
+
});
|
|
2867
|
+
if (isAgentRefusal(draft)) fail(draft.status, draft.error);
|
|
2868
|
+
await respond(ctx, res, 201, await bindWithAvatar(ctx, draft, info));
|
|
2869
|
+
}
|
|
2870
|
+
async function bindWithAvatar(ctx, agent, session) {
|
|
2871
|
+
const recipe = agent.avatarRecipe ?? await rollAvatar(ctx, agent, session);
|
|
2872
|
+
return ctx.agents.bind(recipe === void 0 ? agent : {
|
|
2873
|
+
...agent,
|
|
2874
|
+
avatarRecipe: recipe
|
|
2875
|
+
}, session.id);
|
|
2876
|
+
}
|
|
2877
|
+
async function rollAvatar(ctx, agent, session) {
|
|
2878
|
+
if (!ctx.avatars) return;
|
|
2879
|
+
const project = session ? ctx.projects.withProject(session).project?.root ?? session.cwd : void 0;
|
|
2880
|
+
try {
|
|
2881
|
+
return await ctx.avatars.roll(agent.avatarSeed, session?.engine, project || void 0);
|
|
2882
|
+
} catch {
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
async function sendAvatar(ctx, req, res, agent, busy) {
|
|
2887
|
+
const avatars = ctx.avatars;
|
|
2888
|
+
if (!avatars) fail(404, "this gateway draws no avatars");
|
|
2889
|
+
let recipe = agent.avatarRecipe;
|
|
2890
|
+
if (recipe === void 0) {
|
|
2891
|
+
recipe = await rollAvatar(ctx, agent, agent.sessionId === void 0 ? void 0 : await sessionInfoOf(ctx, agent.sessionId));
|
|
2892
|
+
if (recipe === void 0) fail(503, "the avatar pack could not be loaded");
|
|
2893
|
+
await ctx.agents.save({
|
|
2894
|
+
...agent,
|
|
2895
|
+
avatarRecipe: recipe
|
|
2896
|
+
});
|
|
2897
|
+
}
|
|
2898
|
+
const image = busy ? await avatars.busy(recipe) : await avatars.still(recipe);
|
|
2899
|
+
if (!image) fail(404, "this avatar pack has no busy animation");
|
|
2900
|
+
const headers = {
|
|
2901
|
+
"content-type": "image/png",
|
|
2902
|
+
etag: image.etag,
|
|
2903
|
+
"cache-control": "private, max-age=86400"
|
|
2904
|
+
};
|
|
2905
|
+
if (image.durations) headers["x-frame-durations"] = image.durations.join(",");
|
|
2906
|
+
if (req.headers["if-none-match"] === image.etag) {
|
|
2907
|
+
res.writeHead(304, headers).end();
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
res.writeHead(200, headers).end(Buffer.from(image.png));
|
|
2911
|
+
}
|
|
2912
|
+
async function startSession(ctx, agent, prompt, auth) {
|
|
2913
|
+
const { config } = agent;
|
|
2914
|
+
const vetted = vetCreateRequest(ctx, stripUndefined({
|
|
2915
|
+
cwd: config.cwd,
|
|
2916
|
+
profile: config.profile,
|
|
2917
|
+
model: config.model,
|
|
2918
|
+
reasoningEffort: config.reasoningEffort,
|
|
2919
|
+
permissionMode: config.permissionMode,
|
|
2920
|
+
allowDangerouslySkipPermissions: config.permissionMode === "bypassPermissions" ? true : void 0,
|
|
2921
|
+
agentContextReset: config.agentContextReset ?? true,
|
|
2922
|
+
prompt
|
|
2923
|
+
}), auth);
|
|
2924
|
+
if (!vetted.ok) fail(vetted.status, vetted.error);
|
|
2925
|
+
return (await ctx.factory.createRunner(ctx.factory.buildRunnerConfig(vetted.request, { operator: ctx.auth.isOperator(auth) }), { brief: config.brief?.trim() || void 0 })).info();
|
|
2926
|
+
}
|
|
2927
|
+
async function respond(ctx, res, status, agent, session) {
|
|
2928
|
+
const info = session ?? (agent.sessionId === void 0 ? void 0 : await sessionInfoOf(ctx, agent.sessionId));
|
|
2929
|
+
const body = { agent: ctx.agents.public(agent) };
|
|
2930
|
+
if (info) body.session = ctx.projects.withProject(info);
|
|
2931
|
+
json(res, status, body);
|
|
2932
|
+
}
|
|
2933
|
+
function configOf(info) {
|
|
2934
|
+
return stripUndefined({
|
|
2935
|
+
cwd: info.cwd || void 0,
|
|
2936
|
+
profile: info.profile,
|
|
2937
|
+
model: info.model,
|
|
2938
|
+
reasoningEffort: info.effort ?? void 0,
|
|
2939
|
+
permissionMode: info.permissionMode
|
|
2940
|
+
});
|
|
2941
|
+
}
|
|
2942
|
+
function stripUndefined(value) {
|
|
2943
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
|
|
2944
|
+
}
|
|
2945
|
+
function isRecord(value) {
|
|
2946
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2947
|
+
}
|
|
2948
|
+
//#endregion
|
|
2375
2949
|
//#region src/routes/executions.ts
|
|
2376
2950
|
async function handleExecutionResult(ctx, req, res, pathname, auth) {
|
|
2377
2951
|
const { auth: authSvc, basePath, parking } = ctx;
|
|
@@ -2819,110 +3393,6 @@ async function handleWrite({ ctx, hostFiles, req, res }) {
|
|
|
2819
3393
|
});
|
|
2820
3394
|
}
|
|
2821
3395
|
//#endregion
|
|
2822
|
-
//#region src/lib/host-only-keys.ts
|
|
2823
|
-
const HOST_ONLY_KEY_TABLE = {
|
|
2824
|
-
epoch: "durable",
|
|
2825
|
-
pricing: "transient",
|
|
2826
|
-
queryFn: "transient",
|
|
2827
|
-
env: "transient",
|
|
2828
|
-
pathToClaudeCodeExecutable: "durable",
|
|
2829
|
-
extraOptions: "transient",
|
|
2830
|
-
instructions: "transient",
|
|
2831
|
-
defaultApprovalTimeoutMs: "durable",
|
|
2832
|
-
backfillHistory: "durable",
|
|
2833
|
-
historyFn: "transient",
|
|
2834
|
-
sessionInfoFn: "transient",
|
|
2835
|
-
peers: "transient",
|
|
2836
|
-
contextReset: "transient",
|
|
2837
|
-
shells: "transient",
|
|
2838
|
-
shellAgentWrite: "transient",
|
|
2839
|
-
createdByOperator: "durable",
|
|
2840
|
-
fallbackTitle: "transient",
|
|
2841
|
-
startAsleep: "transient",
|
|
2842
|
-
effortDefaults: "transient",
|
|
2843
|
-
connectFn: "transient",
|
|
2844
|
-
codexHome: "durable",
|
|
2845
|
-
codexPathOverride: "durable",
|
|
2846
|
-
languageModel: "transient",
|
|
2847
|
-
tools: "transient",
|
|
2848
|
-
maxSteps: "durable",
|
|
2849
|
-
executor: "transient",
|
|
2850
|
-
executableTools: "transient",
|
|
2851
|
-
vfs: "transient",
|
|
2852
|
-
executionLimits: "durable",
|
|
2853
|
-
executionBackend: "transient",
|
|
2854
|
-
toolTitles: "transient",
|
|
2855
|
-
shouldApprove: "transient",
|
|
2856
|
-
resolveModel: "transient",
|
|
2857
|
-
contextWindow: "transient",
|
|
2858
|
-
reportMcpServers: "transient",
|
|
2859
|
-
onClose: "transient",
|
|
2860
|
-
restore: "transient"
|
|
2861
|
-
};
|
|
2862
|
-
const HOST_ONLY_KEYS = new Set(Object.keys(HOST_ONLY_KEY_TABLE));
|
|
2863
|
-
const DURABLE_HOST_KEYS = Object.entries(HOST_ONLY_KEY_TABLE).filter(([, lifetime]) => lifetime === "durable").map(([key]) => key);
|
|
2864
|
-
//#endregion
|
|
2865
|
-
//#region src/routes/create-vet.ts
|
|
2866
|
-
function vetCreateRequest(ctx, body, auth) {
|
|
2867
|
-
if (typeof body !== "object" || body === null || Array.isArray(body)) return {
|
|
2868
|
-
ok: false,
|
|
2869
|
-
status: 400,
|
|
2870
|
-
error: "request body must be a JSON object"
|
|
2871
|
-
};
|
|
2872
|
-
const hostOnly = Object.keys(body).filter((key) => HOST_ONLY_KEYS.has(key));
|
|
2873
|
-
if (hostOnly.length > 0) return {
|
|
2874
|
-
ok: false,
|
|
2875
|
-
status: 400,
|
|
2876
|
-
error: `host-only runner config is not accepted on a session request: ${hostOnly.join(", ")} (set it from the gateway's buildRunnerConfig hook)`
|
|
2877
|
-
};
|
|
2878
|
-
const req = pickCreateSessionRequest(body);
|
|
2879
|
-
const { availability, factory } = ctx;
|
|
2880
|
-
const early = factory.applyScope(req, auth) ?? refusal(403, factory.applyBypassPolicy(req));
|
|
2881
|
-
if (early) return {
|
|
2882
|
-
ok: false,
|
|
2883
|
-
...early
|
|
2884
|
-
};
|
|
2885
|
-
const resolved = factory.resolveProfile(req.profile, auth.allowedProfiles);
|
|
2886
|
-
if (!resolved.ok) return resolved;
|
|
2887
|
-
const refused = refusal(403, refuseHostAuthority(ctx, req, resolved.profile, auth)) ?? availability.checkAvailable(resolved.profile) ?? factory.checkCwd(req, resolved.profile) ?? refusal(400, factory.checkPermissionMode(req.permissionMode, resolved.profile) ?? factory.checkEngineGrants(req, resolved.profile));
|
|
2888
|
-
if (refused) return {
|
|
2889
|
-
ok: false,
|
|
2890
|
-
...refused
|
|
2891
|
-
};
|
|
2892
|
-
factory.stripInertFields(req, resolved.profile);
|
|
2893
|
-
req.profile = resolved.profile?.name;
|
|
2894
|
-
return {
|
|
2895
|
-
ok: true,
|
|
2896
|
-
request: req
|
|
2897
|
-
};
|
|
2898
|
-
}
|
|
2899
|
-
function refusal(status, error) {
|
|
2900
|
-
return error === null ? null : {
|
|
2901
|
-
status,
|
|
2902
|
-
error
|
|
2903
|
-
};
|
|
2904
|
-
}
|
|
2905
|
-
function refuseHostAuthority(ctx, req, profile, auth) {
|
|
2906
|
-
if (ctx.auth.isOperator(auth)) return null;
|
|
2907
|
-
const engine = engineOf(profile);
|
|
2908
|
-
const name = profile?.name ?? "default";
|
|
2909
|
-
if ((engine === "claude" || engine === "codex") && !auth.allowedProfiles?.includes(name)) return `profile '${name}' runs the ${engine} engine on the host; a non-operator caller needs it named in its allowedProfiles`;
|
|
2910
|
-
const mode = refusePermissionMode(req.permissionMode, { operator: false });
|
|
2911
|
-
if (mode) return mode;
|
|
2912
|
-
if (req.allowDangerouslySkipPermissions) return "allowDangerouslySkipPermissions is reserved to operators";
|
|
2913
|
-
if (req.settingSources !== void 0 && req.settingSources.length > 0) return "settingSources is reserved to operators: it loads hooks and settings from the host";
|
|
2914
|
-
if (!(typeof req.mcpServers === "object" && req.mcpServers !== null ? Object.values(req.mcpServers) : []).every(isRemoteMcpServer)) return "a stdio MCP server is reserved to operators: it runs a command on the host";
|
|
2915
|
-
if (req.resume !== void 0 && !ownsSdkSession(ctx, auth, req.resume)) return "resume is limited to a live session this caller can see";
|
|
2916
|
-
return null;
|
|
2917
|
-
}
|
|
2918
|
-
function ownsSdkSession(ctx, auth, sdkSessionId) {
|
|
2919
|
-
return ctx.registry.list().some((info) => info.sdkSessionId === sdkSessionId && ctx.auth.canSee(auth, info));
|
|
2920
|
-
}
|
|
2921
|
-
function isRemoteMcpServer(server) {
|
|
2922
|
-
const type = server?.type;
|
|
2923
|
-
return type === "http" || type === "sse";
|
|
2924
|
-
}
|
|
2925
|
-
//#endregion
|
|
2926
3396
|
//#region src/routes/jobs.ts
|
|
2927
3397
|
async function handleJobs(ctx, req, res, pathname, auth) {
|
|
2928
3398
|
const { auth: authSvc, basePath, queue } = ctx;
|
|
@@ -3757,6 +4227,11 @@ function httpRoutes(ctx) {
|
|
|
3757
4227
|
auth: "any",
|
|
3758
4228
|
handler: (req, res, pathname, auth) => handleProfiles(ctx, req, res, pathname, auth)
|
|
3759
4229
|
}),
|
|
4230
|
+
route({
|
|
4231
|
+
match: pathWhere(under("/agents")),
|
|
4232
|
+
auth: "operator",
|
|
4233
|
+
handler: (req, res, pathname, auth) => handleAgents(ctx, req, res, pathname, auth)
|
|
4234
|
+
}),
|
|
3760
4235
|
route({
|
|
3761
4236
|
match: pathWhere((pathname) => pathname.startsWith(base + "/executions/")),
|
|
3762
4237
|
auth: "any",
|
|
@@ -3824,6 +4299,36 @@ function pathWhere(test) {
|
|
|
3824
4299
|
return (pathname) => test(pathname) ? pathname : void 0;
|
|
3825
4300
|
}
|
|
3826
4301
|
//#endregion
|
|
4302
|
+
//#region src/services/agent-store.ts
|
|
4303
|
+
function createMemoryAgentStore(seed = []) {
|
|
4304
|
+
const agents = new Map(seed.map((a) => [a.id, a]));
|
|
4305
|
+
return {
|
|
4306
|
+
list: () => [...agents.values()],
|
|
4307
|
+
save: (agent) => void agents.set(agent.id, agent),
|
|
4308
|
+
delete: (id) => void agents.delete(id)
|
|
4309
|
+
};
|
|
4310
|
+
}
|
|
4311
|
+
function createFileAgentStore(path = join(process.cwd(), ".workerdeck", "agents.json")) {
|
|
4312
|
+
const read = () => {
|
|
4313
|
+
const parsed = readJsonOrSync(path, []);
|
|
4314
|
+
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Map();
|
|
4315
|
+
return new Map(parsed.filter((a) => a && typeof a.id === "string" && typeof a.name === "string").map((a) => [a.id, a]));
|
|
4316
|
+
};
|
|
4317
|
+
const write = (agents) => writeJsonAtomicSync(path, [...agents.values()], { indent: 2 });
|
|
4318
|
+
return {
|
|
4319
|
+
list: () => [...read().values()],
|
|
4320
|
+
save: (agent) => {
|
|
4321
|
+
const agents = read();
|
|
4322
|
+
agents.set(agent.id, agent);
|
|
4323
|
+
write(agents);
|
|
4324
|
+
},
|
|
4325
|
+
delete: (id) => {
|
|
4326
|
+
const agents = read();
|
|
4327
|
+
if (agents.delete(id)) write(agents);
|
|
4328
|
+
}
|
|
4329
|
+
};
|
|
4330
|
+
}
|
|
4331
|
+
//#endregion
|
|
3827
4332
|
//#region src/services/attachments.ts
|
|
3828
4333
|
const DEFAULT_MAX_FILE_BYTES = 10485760;
|
|
3829
4334
|
const DEFAULT_MAX_SESSION_BYTES = 67108864;
|
|
@@ -4044,7 +4549,7 @@ var EngineSleepTimers = class {
|
|
|
4044
4549
|
return this.#options.afterMs > 0 && !this.#closed;
|
|
4045
4550
|
}
|
|
4046
4551
|
watch(runner) {
|
|
4047
|
-
if (!this.enabled
|
|
4552
|
+
if (this.#closed || !runner.sleep || !this.enabled && !this.#options.afterMsFor) return;
|
|
4048
4553
|
this.#runners.set(runner.id, runner);
|
|
4049
4554
|
const unsubscribe = runner.subscribe((event) => {
|
|
4050
4555
|
if (event.type === "status_changed") this.#rearm(runner);
|
|
@@ -4066,14 +4571,18 @@ var EngineSleepTimers = class {
|
|
|
4066
4571
|
this.#timers.clear();
|
|
4067
4572
|
this.#runners.clear();
|
|
4068
4573
|
}
|
|
4574
|
+
#afterMs(sessionId) {
|
|
4575
|
+
return this.#closed ? 0 : this.#options.afterMsFor?.(sessionId) ?? this.#options.afterMs;
|
|
4576
|
+
}
|
|
4069
4577
|
#rearm(runner) {
|
|
4070
4578
|
this.#disarm(runner.id);
|
|
4071
|
-
|
|
4579
|
+
const afterMs = this.#afterMs(runner.id);
|
|
4580
|
+
if (afterMs <= 0 || !this.#sleepable(runner)) return;
|
|
4072
4581
|
const timer = setTimeout(() => {
|
|
4073
4582
|
this.#timers.delete(runner.id);
|
|
4074
|
-
if (
|
|
4583
|
+
if (this.#afterMs(runner.id) <= 0 || !this.#sleepable(runner)) return;
|
|
4075
4584
|
runner.sleep?.().catch((error) => this.#options.onError?.(error, runner.id));
|
|
4076
|
-
},
|
|
4585
|
+
}, afterMs);
|
|
4077
4586
|
timer.unref?.();
|
|
4078
4587
|
this.#timers.set(runner.id, timer);
|
|
4079
4588
|
}
|
|
@@ -5229,7 +5738,7 @@ function createRelayLink(options, peers, log) {
|
|
|
5229
5738
|
const remoteTarget = parseRelayPeerId;
|
|
5230
5739
|
const reachesRemote = async (from) => {
|
|
5231
5740
|
const me = await peers.relaySender(from);
|
|
5232
|
-
return me.scope === void 0 || Object.keys(me.scope).length === 0;
|
|
5741
|
+
return (me.scope === void 0 || Object.keys(me.scope).length === 0) && me.agent?.lead === void 0;
|
|
5233
5742
|
};
|
|
5234
5743
|
return {
|
|
5235
5744
|
directory: {
|
|
@@ -5507,7 +6016,7 @@ function createSessionFactory(deps) {
|
|
|
5507
6016
|
return adapterFor(profile.engine).sessionEnv?.(profile, process.env) ?? process.env;
|
|
5508
6017
|
}
|
|
5509
6018
|
};
|
|
5510
|
-
const buildRunner = async (built, restore, id) => {
|
|
6019
|
+
const buildRunner = async (built, restore, id, options) => {
|
|
5511
6020
|
const name = built.profile;
|
|
5512
6021
|
const profile = name !== void 0 ? profiles.get(name) : void 0;
|
|
5513
6022
|
if (name !== void 0 && !profile) throw new Error(`unknown profile: ${name}`);
|
|
@@ -5532,6 +6041,8 @@ function createSessionFactory(deps) {
|
|
|
5532
6041
|
const write = deps.shellAgentWrite ?? "read-only";
|
|
5533
6042
|
if (write !== "read-only" && built.createdByOperator === true) config.shellAgentWrite = write;
|
|
5534
6043
|
}
|
|
6044
|
+
const brief = options?.brief ?? deps.agentBrief?.(id ?? restore?.id);
|
|
6045
|
+
if (brief) config.instructions = composeInstructions(config.instructions, brief);
|
|
5535
6046
|
if (config.instructions !== void 0 && capabilities.systemInstructions === false) throw new Error(`the ${engineOf(profile)} engine cannot deliver system instructions`);
|
|
5536
6047
|
const runner = profile && isProviderProfile(profile) ? await deps.createEngineRunner({
|
|
5537
6048
|
config,
|
|
@@ -5549,8 +6060,8 @@ function createSessionFactory(deps) {
|
|
|
5549
6060
|
if (!sameScope(reported, config.scope)) throw new Error(`runner for session ${runner.id} reports scope ${JSON.stringify(reported)}, expected ${JSON.stringify(config.scope)} - echo config.scope from info()`);
|
|
5550
6061
|
return runner;
|
|
5551
6062
|
};
|
|
5552
|
-
const createRunner = async (config) => {
|
|
5553
|
-
const runner = registry.register(await buildRunner(config));
|
|
6063
|
+
const createRunner = async (config, options) => {
|
|
6064
|
+
const runner = registry.register(await buildRunner(config, void 0, void 0, options));
|
|
5554
6065
|
parking.remember(runner.id, config);
|
|
5555
6066
|
parking.watch(runner);
|
|
5556
6067
|
runner.start();
|
|
@@ -5669,7 +6180,13 @@ function createWorkerServer(options = {}) {
|
|
|
5669
6180
|
}
|
|
5670
6181
|
const generation = randomUUID();
|
|
5671
6182
|
const shells = options.shell?.enabled === true ? shellRegistryFor(options.shell, generation) : null;
|
|
5672
|
-
const
|
|
6183
|
+
const agents = new AgentService({
|
|
6184
|
+
store: options.agentStore ?? createMemoryAgentStore(),
|
|
6185
|
+
basePath,
|
|
6186
|
+
avatars: options.avatars !== void 0,
|
|
6187
|
+
sleepAfterMs: options.agentSleepAfterMs
|
|
6188
|
+
});
|
|
6189
|
+
const projects = new ProjectInfoService({ decorate: (info) => agents.decorate(shells ? shells.decorate(info) : info) });
|
|
5673
6190
|
const notifier = new SessionNotifier({
|
|
5674
6191
|
...options.notifications,
|
|
5675
6192
|
decorateInfo: (info) => projects.withProject(info),
|
|
@@ -5678,6 +6195,7 @@ function createWorkerServer(options = {}) {
|
|
|
5678
6195
|
const producedFiles = new ProducedFileStore();
|
|
5679
6196
|
const engineSleep = new EngineSleepTimers({
|
|
5680
6197
|
afterMs: options.engineSleepAfterMs ?? 0,
|
|
6198
|
+
afterMsFor: (sessionId) => agents.sleepAfterFor(sessionId),
|
|
5681
6199
|
attachedCount: (sessionId) => bridge.attachedCount(sessionId),
|
|
5682
6200
|
onError: (error) => diagnose(error, "engine-sleep")
|
|
5683
6201
|
});
|
|
@@ -5764,7 +6282,8 @@ function createWorkerServer(options = {}) {
|
|
|
5764
6282
|
pricing: pricing.pricing,
|
|
5765
6283
|
registry,
|
|
5766
6284
|
parking,
|
|
5767
|
-
bridge
|
|
6285
|
+
bridge,
|
|
6286
|
+
agentBrief: (sessionId) => agents.briefFor(sessionId)
|
|
5768
6287
|
});
|
|
5769
6288
|
const availability = new AvailabilityTracker({
|
|
5770
6289
|
checkCredentials: options.checkCredentials,
|
|
@@ -5812,6 +6331,8 @@ function createWorkerServer(options = {}) {
|
|
|
5812
6331
|
availability,
|
|
5813
6332
|
auth,
|
|
5814
6333
|
factory,
|
|
6334
|
+
agents,
|
|
6335
|
+
avatars: options.avatars,
|
|
5815
6336
|
registry,
|
|
5816
6337
|
parking,
|
|
5817
6338
|
engineSleep,
|
|
@@ -5883,6 +6404,7 @@ function createWorkerServer(options = {}) {
|
|
|
5883
6404
|
listen: async (port, host) => {
|
|
5884
6405
|
await profiles.refreshStored();
|
|
5885
6406
|
await profiles.seedStore();
|
|
6407
|
+
await agents.hydrate();
|
|
5886
6408
|
await parking.hydrate();
|
|
5887
6409
|
await shells?.hydrate();
|
|
5888
6410
|
return new Promise((resolve, reject) => {
|
|
@@ -6078,6 +6600,6 @@ function createFileProfileStore(path = join(process.cwd(), ".workerdeck", "profi
|
|
|
6078
6600
|
};
|
|
6079
6601
|
}
|
|
6080
6602
|
//#endregion
|
|
6081
|
-
export { AttachmentStore, BridgeHub, MemorySessionStore, ProducedFileStore, ProfileUsageTracker, SHELL_REFUSAL, SessionNotifier, SessionParkManager, SessionRegistry, SpendLedger, createFileProfileStore, createFileSessionStore, createFileSpendStore, createMemoryProfileStore, createProviderRunner, createShellDirectory, createShellRegistry, createWorkerServer, dayKey, isDormant, isMissing, readJsonOr, reloadPlan, sandboxedProviderProfile, shellPermitted, toDurableRecord, writeFileAtomic, writeJsonAtomic };
|
|
6603
|
+
export { AttachmentStore, BridgeHub, MemorySessionStore, ProducedFileStore, ProfileUsageTracker, SHELL_REFUSAL, SessionNotifier, SessionParkManager, SessionRegistry, SpendLedger, createFileAgentStore, createFileProfileStore, createFileSessionStore, createFileSpendStore, createMemoryAgentStore, createMemoryProfileStore, createProviderRunner, createShellDirectory, createShellRegistry, createWorkerServer, dayKey, isDormant, isMissing, readJsonOr, reloadPlan, sandboxedProviderProfile, shellPermitted, toDurableRecord, writeFileAtomic, writeJsonAtomic };
|
|
6082
6604
|
|
|
6083
6605
|
//# sourceMappingURL=index.mjs.map
|