@botlearn-course/daemon 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/agent-service-session.d.ts +67 -0
- package/dist/agent-service-session.js +796 -0
- package/dist/agent-service-ws-protocol.d.ts +28 -0
- package/dist/agent-service-ws-protocol.js +128 -0
- package/dist/cli.js +112 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/run-dispatcher.d.ts +15 -0
- package/dist/run-dispatcher.js +42 -6
- package/dist/runtime-env.d.ts +5 -0
- package/dist/runtime-env.js +23 -0
- package/dist/runtime-profile.js +25 -13
- package/dist/runtimes/acp-stream.js +2 -0
- package/dist/runtimes/codex.d.ts +1 -1
- package/dist/runtimes/codex.js +2 -2
- package/dist/runtimes/deepseek-tui.js +10 -9
- package/dist/runtimes/engine.d.ts +1 -0
- package/dist/runtimes/engine.js +3 -1
- package/dist/runtimes/hermes-agent.d.ts +1 -1
- package/dist/runtimes/hermes-agent.js +3 -2
- package/dist/runtimes/ndjson-stream.d.ts +1 -1
- package/dist/runtimes/ndjson-stream.js +4 -2
- package/dist/runtimes/openclaw-acp.js +3 -1
- package/dist/sandbox-supervisor.d.ts +4 -0
- package/dist/sandbox-supervisor.js +187 -0
- package/dist/types.d.ts +9 -1
- package/dist/websocket-client.d.ts +43 -0
- package/dist/websocket-client.js +320 -0
- package/dist/workspace.d.ts +9 -0
- package/dist/workspace.js +43 -2
- package/package.json +3 -2
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare const AGENT_SERVICE_WS_SCHEMA: "botlearn-agent-sandbox-ws/0.1";
|
|
2
|
+
export declare const AGENT_SERVICE_WS_SUBPROTOCOL: "botlearn-agent-sandbox.v1";
|
|
3
|
+
export type SandboxFrameType = "session.hello" | "session.sync" | "turn.start" | "turn.cancel" | "session.drain" | "session.shutdown" | "event.ack" | "turn.file.upload_grant" | "auth.rotate" | "ping" | "session.ready" | "session.heartbeat" | "command.ack" | "turn.event" | "turn.file.prepare" | "turn.file.committed" | "session.drained" | "pong" | "protocol.error";
|
|
4
|
+
export interface SandboxFrame {
|
|
5
|
+
schema_version: typeof AGENT_SERVICE_WS_SCHEMA;
|
|
6
|
+
type: SandboxFrameType;
|
|
7
|
+
frame_id: string;
|
|
8
|
+
session_id: string;
|
|
9
|
+
session_generation: number;
|
|
10
|
+
connection_epoch: number;
|
|
11
|
+
seq: number;
|
|
12
|
+
sent_at: string;
|
|
13
|
+
agent_run_id: string | null;
|
|
14
|
+
worker_attempt: number | null;
|
|
15
|
+
payload: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export declare function parseSandboxFrame(raw: string, maxBytes?: number): SandboxFrame;
|
|
18
|
+
export declare function createSandboxFrame(input: {
|
|
19
|
+
type: SandboxFrameType;
|
|
20
|
+
sessionId: string;
|
|
21
|
+
sessionGeneration: number;
|
|
22
|
+
connectionEpoch: number;
|
|
23
|
+
seq: number;
|
|
24
|
+
payload?: Record<string, unknown>;
|
|
25
|
+
agentRunId?: string;
|
|
26
|
+
workerAttempt?: number;
|
|
27
|
+
frameId?: string;
|
|
28
|
+
}): SandboxFrame;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
export const AGENT_SERVICE_WS_SCHEMA = "botlearn-agent-sandbox-ws/0.1";
|
|
3
|
+
export const AGENT_SERVICE_WS_SUBPROTOCOL = "botlearn-agent-sandbox.v1";
|
|
4
|
+
const FRAME_TYPES = new Set([
|
|
5
|
+
"session.hello",
|
|
6
|
+
"session.sync",
|
|
7
|
+
"turn.start",
|
|
8
|
+
"turn.cancel",
|
|
9
|
+
"session.drain",
|
|
10
|
+
"session.shutdown",
|
|
11
|
+
"event.ack",
|
|
12
|
+
"turn.file.upload_grant",
|
|
13
|
+
"auth.rotate",
|
|
14
|
+
"ping",
|
|
15
|
+
"session.ready",
|
|
16
|
+
"session.heartbeat",
|
|
17
|
+
"command.ack",
|
|
18
|
+
"turn.event",
|
|
19
|
+
"turn.file.prepare",
|
|
20
|
+
"turn.file.committed",
|
|
21
|
+
"session.drained",
|
|
22
|
+
"pong",
|
|
23
|
+
"protocol.error",
|
|
24
|
+
]);
|
|
25
|
+
const TURN_TYPES = new Set([
|
|
26
|
+
"turn.start",
|
|
27
|
+
"turn.cancel",
|
|
28
|
+
"event.ack",
|
|
29
|
+
"turn.file.upload_grant",
|
|
30
|
+
"turn.event",
|
|
31
|
+
"turn.file.prepare",
|
|
32
|
+
"turn.file.committed",
|
|
33
|
+
]);
|
|
34
|
+
const FRAME_KEYS = new Set([
|
|
35
|
+
"schema_version",
|
|
36
|
+
"type",
|
|
37
|
+
"frame_id",
|
|
38
|
+
"session_id",
|
|
39
|
+
"session_generation",
|
|
40
|
+
"connection_epoch",
|
|
41
|
+
"seq",
|
|
42
|
+
"sent_at",
|
|
43
|
+
"agent_run_id",
|
|
44
|
+
"worker_attempt",
|
|
45
|
+
"payload",
|
|
46
|
+
]);
|
|
47
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
48
|
+
function positiveInteger(value, label) {
|
|
49
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
50
|
+
throw new Error(`${label} must be a positive integer`);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
export function parseSandboxFrame(raw, maxBytes = 262_144) {
|
|
55
|
+
if (Buffer.byteLength(raw, "utf8") > maxBytes)
|
|
56
|
+
throw new Error("frame exceeds size limit");
|
|
57
|
+
const decoded = JSON.parse(raw);
|
|
58
|
+
if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) {
|
|
59
|
+
throw new Error("frame must be an object");
|
|
60
|
+
}
|
|
61
|
+
const value = decoded;
|
|
62
|
+
for (const key of Object.keys(value)) {
|
|
63
|
+
if (!FRAME_KEYS.has(key))
|
|
64
|
+
throw new Error(`unknown sandbox WebSocket frame field: ${key}`);
|
|
65
|
+
}
|
|
66
|
+
if (value.schema_version !== AGENT_SERVICE_WS_SCHEMA) {
|
|
67
|
+
throw new Error("unsupported sandbox WebSocket schema");
|
|
68
|
+
}
|
|
69
|
+
if (typeof value.type !== "string" || !FRAME_TYPES.has(value.type)) {
|
|
70
|
+
throw new Error("unknown sandbox WebSocket frame type");
|
|
71
|
+
}
|
|
72
|
+
const type = value.type;
|
|
73
|
+
const agentRunId = typeof value.agent_run_id === "string" ? value.agent_run_id : null;
|
|
74
|
+
const workerAttempt = value.worker_attempt == null
|
|
75
|
+
? null
|
|
76
|
+
: positiveInteger(value.worker_attempt, "worker_attempt");
|
|
77
|
+
if (TURN_TYPES.has(type) && (!agentRunId || workerAttempt === null)) {
|
|
78
|
+
throw new Error(`${type} requires turn fencing fields`);
|
|
79
|
+
}
|
|
80
|
+
if (!TURN_TYPES.has(type) && (agentRunId !== null || workerAttempt !== null)) {
|
|
81
|
+
throw new Error(`${type} cannot carry turn fencing fields`);
|
|
82
|
+
}
|
|
83
|
+
if (!value.payload || typeof value.payload !== "object" || Array.isArray(value.payload)) {
|
|
84
|
+
throw new Error("frame payload must be an object");
|
|
85
|
+
}
|
|
86
|
+
for (const key of ["frame_id", "session_id", "sent_at"]) {
|
|
87
|
+
if (typeof value[key] !== "string" || !value[key])
|
|
88
|
+
throw new Error(`${key} is required`);
|
|
89
|
+
}
|
|
90
|
+
if (value.frame_id.length > 120)
|
|
91
|
+
throw new Error("frame_id is too long");
|
|
92
|
+
if (!UUID_PATTERN.test(value.session_id))
|
|
93
|
+
throw new Error("session_id must be a UUID");
|
|
94
|
+
if (agentRunId !== null && !UUID_PATTERN.test(agentRunId)) {
|
|
95
|
+
throw new Error("agent_run_id must be a UUID");
|
|
96
|
+
}
|
|
97
|
+
if (Number.isNaN(Date.parse(value.sent_at)))
|
|
98
|
+
throw new Error("sent_at is invalid");
|
|
99
|
+
return {
|
|
100
|
+
schema_version: AGENT_SERVICE_WS_SCHEMA,
|
|
101
|
+
type,
|
|
102
|
+
frame_id: value.frame_id,
|
|
103
|
+
session_id: value.session_id,
|
|
104
|
+
session_generation: positiveInteger(value.session_generation, "session_generation"),
|
|
105
|
+
connection_epoch: positiveInteger(value.connection_epoch, "connection_epoch"),
|
|
106
|
+
seq: positiveInteger(value.seq, "seq"),
|
|
107
|
+
sent_at: value.sent_at,
|
|
108
|
+
agent_run_id: agentRunId,
|
|
109
|
+
worker_attempt: workerAttempt,
|
|
110
|
+
payload: value.payload,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function createSandboxFrame(input) {
|
|
114
|
+
const frame = {
|
|
115
|
+
schema_version: AGENT_SERVICE_WS_SCHEMA,
|
|
116
|
+
type: input.type,
|
|
117
|
+
frame_id: input.frameId ?? `frm_${randomUUID().replaceAll("-", "")}`,
|
|
118
|
+
session_id: input.sessionId,
|
|
119
|
+
session_generation: input.sessionGeneration,
|
|
120
|
+
connection_epoch: input.connectionEpoch,
|
|
121
|
+
seq: input.seq,
|
|
122
|
+
sent_at: new Date().toISOString(),
|
|
123
|
+
agent_run_id: input.agentRunId ?? null,
|
|
124
|
+
worker_attempt: input.workerAttempt ?? null,
|
|
125
|
+
payload: input.payload ?? {},
|
|
126
|
+
};
|
|
127
|
+
return parseSandboxFrame(JSON.stringify(frame));
|
|
128
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { hostname } from "node:os";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { authFilePath, clearAuth, machineFingerprint, readAuth, writeAuth } from "./auth-store.js";
|
|
7
7
|
import { AgentServiceRunClient } from "./agent-service-client.js";
|
|
8
|
+
import { AgentServiceSessionClient } from "./agent-service-session.js";
|
|
8
9
|
import { CourseClient, isAuthFailure, loginCourseDaemon } from "./course-client.js";
|
|
9
10
|
import { runCourseDoctor } from "./doctor.js";
|
|
10
11
|
import { log } from "./log.js";
|
|
@@ -14,6 +15,17 @@ import { RunDispatcher } from "./run-dispatcher.js";
|
|
|
14
15
|
import { clearAgentServiceControlEnv } from "./runtime-env.js";
|
|
15
16
|
import { RUNTIME_MODULES, detectAvailableRuntimeIds, fakeRuntimeEnabled, } from "./runtimes/index.js";
|
|
16
17
|
const pkg = createRequire(import.meta.url)("../package.json");
|
|
18
|
+
const SESSION_BOOTSTRAP_MAX_BYTES = 64 * 1024;
|
|
19
|
+
const SESSION_RUNTIME_ENV_KEYS = new Set([
|
|
20
|
+
"OPENAI_API_KEY",
|
|
21
|
+
"OPENAI_BASE_URL",
|
|
22
|
+
"ANTHROPIC_API_KEY",
|
|
23
|
+
"ANTHROPIC_BASE_URL",
|
|
24
|
+
"DEEPSEEK_API_KEY",
|
|
25
|
+
"DEEPSEEK_BASE_URL",
|
|
26
|
+
"GEMINI_API_KEY",
|
|
27
|
+
"GEMINI_BASE_URL",
|
|
28
|
+
]);
|
|
17
29
|
const HELP = `botlearn-course-daemon ${pkg.version} — run BotLearn Course tasks on your own machine (BYOA)
|
|
18
30
|
|
|
19
31
|
Usage:
|
|
@@ -22,6 +34,7 @@ Usage:
|
|
|
22
34
|
botlearn-course-daemon course logout
|
|
23
35
|
botlearn-course-daemon course doctor
|
|
24
36
|
botlearn-course-daemon agent-service run
|
|
37
|
+
botlearn-course-daemon agent-service session
|
|
25
38
|
botlearn-course-daemon --help | --version
|
|
26
39
|
|
|
27
40
|
Commands:
|
|
@@ -42,11 +55,14 @@ Agent Service sandbox environment:
|
|
|
42
55
|
BOTLEARN_COURSE_API_URL
|
|
43
56
|
BOTLEARN_AGENT_SERVICE_RUN_ID
|
|
44
57
|
BOTLEARN_AGENT_SERVICE_RUN_TOKEN
|
|
45
|
-
BOTLEARN_AGENT_SERVICE_WORKER_ID
|
|
58
|
+
BOTLEARN_AGENT_SERVICE_WORKER_ID
|
|
59
|
+
BOTLEARN_AGENT_SERVICE_WS_URL
|
|
60
|
+
BOTLEARN_AGENT_SERVICE_RUNTIME_SESSION_ID
|
|
61
|
+
BOTLEARN_AGENT_SERVICE_SESSION_TOKEN`;
|
|
46
62
|
// ---------------------------------------------------------------
|
|
47
63
|
// flag parser(极简:--k v / --k=v / 布尔开关)
|
|
48
64
|
// ---------------------------------------------------------------
|
|
49
|
-
const BOOLEAN_FLAGS = new Set(["once", "help", "version"]);
|
|
65
|
+
const BOOLEAN_FLAGS = new Set(["once", "help", "version", "bootstrap-stdin"]);
|
|
50
66
|
export { clearAgentServiceControlEnv } from "./runtime-env.js";
|
|
51
67
|
export function parseCliArgs(argv) {
|
|
52
68
|
const positional = [];
|
|
@@ -288,6 +304,97 @@ async function cmdAgentServiceRun() {
|
|
|
288
304
|
}
|
|
289
305
|
return 0;
|
|
290
306
|
}
|
|
307
|
+
async function readSessionBootstrapFromStdin() {
|
|
308
|
+
const chunks = [];
|
|
309
|
+
let size = 0;
|
|
310
|
+
for await (const chunk of process.stdin) {
|
|
311
|
+
const bytes = Buffer.from(chunk);
|
|
312
|
+
size += bytes.length;
|
|
313
|
+
if (size > SESSION_BOOTSTRAP_MAX_BYTES) {
|
|
314
|
+
throw new Error("Agent Service session bootstrap exceeds size limit");
|
|
315
|
+
}
|
|
316
|
+
chunks.push(bytes);
|
|
317
|
+
}
|
|
318
|
+
const raw = Buffer.concat(chunks);
|
|
319
|
+
try {
|
|
320
|
+
const parsed = JSON.parse(raw.toString("utf8"));
|
|
321
|
+
if (parsed.schemaVersion !== "agent-service-session-bootstrap/0.1") {
|
|
322
|
+
throw new Error("Agent Service session bootstrap schema is unsupported");
|
|
323
|
+
}
|
|
324
|
+
const runtimeEnv = {};
|
|
325
|
+
if (parsed.runtimeEnv !== undefined) {
|
|
326
|
+
if (!parsed.runtimeEnv ||
|
|
327
|
+
typeof parsed.runtimeEnv !== "object" ||
|
|
328
|
+
Array.isArray(parsed.runtimeEnv)) {
|
|
329
|
+
throw new Error("Agent Service session bootstrap runtimeEnv is invalid");
|
|
330
|
+
}
|
|
331
|
+
for (const [key, value] of Object.entries(parsed.runtimeEnv)) {
|
|
332
|
+
if (!SESSION_RUNTIME_ENV_KEYS.has(key) ||
|
|
333
|
+
typeof value !== "string" ||
|
|
334
|
+
value.length < 1 ||
|
|
335
|
+
value.length > 8192) {
|
|
336
|
+
throw new Error(`Agent Service session bootstrap runtime env is forbidden: ${key}`);
|
|
337
|
+
}
|
|
338
|
+
runtimeEnv[key] = value;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const wsUrl = parsed.wsUrl;
|
|
342
|
+
const runtimeSessionId = parsed.runtimeSessionId;
|
|
343
|
+
const sessionToken = parsed.sessionToken;
|
|
344
|
+
if (typeof wsUrl !== "string" ||
|
|
345
|
+
!/^wss?:\/\//.test(wsUrl) ||
|
|
346
|
+
typeof runtimeSessionId !== "string" ||
|
|
347
|
+
!runtimeSessionId ||
|
|
348
|
+
typeof sessionToken !== "string" ||
|
|
349
|
+
!sessionToken) {
|
|
350
|
+
throw new Error("Agent Service session bootstrap is incomplete");
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
wsUrl,
|
|
354
|
+
runtimeSessionId,
|
|
355
|
+
sessionToken,
|
|
356
|
+
...(Object.keys(runtimeEnv).length > 0 ? { runtimeEnv } : {}),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
raw.fill(0);
|
|
361
|
+
for (const chunk of chunks)
|
|
362
|
+
chunk.fill(0);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function cmdAgentServiceSession(args) {
|
|
366
|
+
const bootstrap = args.flags["bootstrap-stdin"] === true
|
|
367
|
+
? await readSessionBootstrapFromStdin()
|
|
368
|
+
: {
|
|
369
|
+
wsUrl: process.env.BOTLEARN_AGENT_SERVICE_WS_URL,
|
|
370
|
+
runtimeSessionId: process.env.BOTLEARN_AGENT_SERVICE_RUNTIME_SESSION_ID,
|
|
371
|
+
sessionToken: process.env.BOTLEARN_AGENT_SERVICE_SESSION_TOKEN,
|
|
372
|
+
runtimeEnv: undefined,
|
|
373
|
+
};
|
|
374
|
+
const { wsUrl, runtimeSessionId, sessionToken } = bootstrap;
|
|
375
|
+
if (!wsUrl || !runtimeSessionId || !sessionToken) {
|
|
376
|
+
console.error("Agent Service sandbox session environment is incomplete");
|
|
377
|
+
return 1;
|
|
378
|
+
}
|
|
379
|
+
augmentProcessPath();
|
|
380
|
+
const runtimes = new Map();
|
|
381
|
+
for (const mod of RUNTIME_MODULES) {
|
|
382
|
+
if (mod.hidden && !fakeRuntimeEnabled())
|
|
383
|
+
continue;
|
|
384
|
+
runtimes.set(mod.id, mod.create());
|
|
385
|
+
}
|
|
386
|
+
const client = new AgentServiceSessionClient({
|
|
387
|
+
wsUrl,
|
|
388
|
+
sessionId: runtimeSessionId,
|
|
389
|
+
sessionToken,
|
|
390
|
+
...(bootstrap.runtimeEnv ? { runtimeEnv: bootstrap.runtimeEnv } : {}),
|
|
391
|
+
runtimes,
|
|
392
|
+
daemonVersion: pkg.version,
|
|
393
|
+
});
|
|
394
|
+
clearAgentServiceControlEnv();
|
|
395
|
+
await client.run();
|
|
396
|
+
return 0;
|
|
397
|
+
}
|
|
291
398
|
// ---------------------------------------------------------------
|
|
292
399
|
// 入口
|
|
293
400
|
// ---------------------------------------------------------------
|
|
@@ -320,6 +427,9 @@ export async function runCli(argv) {
|
|
|
320
427
|
if (command === "agent-service" && sub === "run") {
|
|
321
428
|
return cmdAgentServiceRun();
|
|
322
429
|
}
|
|
430
|
+
if (command === "agent-service" && sub === "session") {
|
|
431
|
+
return cmdAgentServiceSession(args);
|
|
432
|
+
}
|
|
323
433
|
console.error(HELP);
|
|
324
434
|
return 1;
|
|
325
435
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export * from "./types.js";
|
|
|
6
6
|
export * from "./auth-store.js";
|
|
7
7
|
export * from "./course-client.js";
|
|
8
8
|
export * from "./agent-service-client.js";
|
|
9
|
+
export * from "./agent-service-session.js";
|
|
10
|
+
export * from "./agent-service-ws-protocol.js";
|
|
9
11
|
export * from "./run-dispatcher.js";
|
|
10
12
|
export * from "./run-queue.js";
|
|
11
13
|
export * from "./workspace.js";
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,8 @@ export * from "./types.js";
|
|
|
6
6
|
export * from "./auth-store.js";
|
|
7
7
|
export * from "./course-client.js";
|
|
8
8
|
export * from "./agent-service-client.js";
|
|
9
|
+
export * from "./agent-service-session.js";
|
|
10
|
+
export * from "./agent-service-ws-protocol.js";
|
|
9
11
|
export * from "./run-dispatcher.js";
|
|
10
12
|
export * from "./run-queue.js";
|
|
11
13
|
export * from "./workspace.js";
|
package/dist/run-dispatcher.d.ts
CHANGED
|
@@ -6,6 +6,18 @@ export interface RunDispatcherOptions {
|
|
|
6
6
|
log?: Logger;
|
|
7
7
|
scanLimits?: ScanLimits;
|
|
8
8
|
now?: () => number;
|
|
9
|
+
persistentSession?: PersistentSessionExecution;
|
|
10
|
+
}
|
|
11
|
+
export interface PreparedPersistentTurn {
|
|
12
|
+
workspaceDir: string;
|
|
13
|
+
transcriptFile: string;
|
|
14
|
+
nativeSessionId: string | null;
|
|
15
|
+
contextRevision: number;
|
|
16
|
+
runtimeEnv?: NodeJS.ProcessEnv;
|
|
17
|
+
}
|
|
18
|
+
export interface PersistentSessionExecution {
|
|
19
|
+
prepareTurn(payload: RunStartPayload): PreparedPersistentTurn;
|
|
20
|
+
persistNativeSession(sessionId: string): void;
|
|
9
21
|
}
|
|
10
22
|
export interface RunReportingClient {
|
|
11
23
|
postEvent(agentRunId: string, event: RunEvent): Promise<void>;
|
|
@@ -27,10 +39,13 @@ export declare class RunDispatcher {
|
|
|
27
39
|
private readonly runtimes;
|
|
28
40
|
private readonly queue;
|
|
29
41
|
private readonly inflight;
|
|
42
|
+
private readonly scheduledRunIds;
|
|
43
|
+
private readonly pendingCancellations;
|
|
30
44
|
private readonly defaultRuntimeId;
|
|
31
45
|
private readonly log;
|
|
32
46
|
private readonly scanLimits?;
|
|
33
47
|
private readonly now;
|
|
48
|
+
private readonly persistentSession?;
|
|
34
49
|
constructor(client: RunReportingClient, runtimes: Map<string, CourseRuntime>, opts?: RunDispatcherOptions);
|
|
35
50
|
/** 排队执行一个 run。返回的 Promise 不 reject(失败已归一化回报为 run.failed)。 */
|
|
36
51
|
dispatch(payload: RunStartPayload): Promise<void>;
|
package/dist/run-dispatcher.js
CHANGED
|
@@ -36,10 +36,13 @@ export class RunDispatcher {
|
|
|
36
36
|
runtimes;
|
|
37
37
|
queue = new RunQueue();
|
|
38
38
|
inflight = new Map();
|
|
39
|
+
scheduledRunIds = new Set();
|
|
40
|
+
pendingCancellations = new Set();
|
|
39
41
|
defaultRuntimeId;
|
|
40
42
|
log;
|
|
41
43
|
scanLimits;
|
|
42
44
|
now;
|
|
45
|
+
persistentSession;
|
|
43
46
|
constructor(client, runtimes, opts = {}) {
|
|
44
47
|
this.client = client;
|
|
45
48
|
this.runtimes = runtimes;
|
|
@@ -47,22 +50,37 @@ export class RunDispatcher {
|
|
|
47
50
|
this.log = opts.log ?? defaultLog;
|
|
48
51
|
this.scanLimits = opts.scanLimits;
|
|
49
52
|
this.now = opts.now ?? Date.now;
|
|
53
|
+
this.persistentSession = opts.persistentSession;
|
|
50
54
|
}
|
|
51
55
|
/** 排队执行一个 run。返回的 Promise 不 reject(失败已归一化回报为 run.failed)。 */
|
|
52
56
|
dispatch(payload) {
|
|
53
57
|
const key = payload.agent_instance_id ?? payload.agent_run_id;
|
|
54
|
-
|
|
58
|
+
this.scheduledRunIds.add(payload.agent_run_id);
|
|
59
|
+
return this.queue
|
|
60
|
+
.enqueue(key, () => this.execute(payload))
|
|
61
|
+
.finally(() => {
|
|
62
|
+
this.scheduledRunIds.delete(payload.agent_run_id);
|
|
63
|
+
this.pendingCancellations.delete(payload.agent_run_id);
|
|
64
|
+
});
|
|
55
65
|
}
|
|
56
66
|
cancel(agentRunId) {
|
|
57
67
|
const controller = this.inflight.get(agentRunId);
|
|
58
|
-
if (
|
|
68
|
+
if (controller) {
|
|
69
|
+
controller.abort();
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (!this.scheduledRunIds.has(agentRunId))
|
|
59
73
|
return false;
|
|
60
|
-
|
|
74
|
+
// dispatch() queues execution on a microtask. A durable cancel may arrive after the
|
|
75
|
+
// command ACK but before execute() installs its AbortController.
|
|
76
|
+
this.pendingCancellations.add(agentRunId);
|
|
61
77
|
return true;
|
|
62
78
|
}
|
|
63
79
|
cancelAll() {
|
|
64
80
|
for (const controller of this.inflight.values())
|
|
65
81
|
controller.abort();
|
|
82
|
+
for (const runId of this.scheduledRunIds)
|
|
83
|
+
this.pendingCancellations.add(runId);
|
|
66
84
|
}
|
|
67
85
|
get activeCount() {
|
|
68
86
|
return this.inflight.size;
|
|
@@ -81,6 +99,8 @@ export class RunDispatcher {
|
|
|
81
99
|
const runId = payload.agent_run_id;
|
|
82
100
|
const controller = new AbortController();
|
|
83
101
|
this.inflight.set(runId, controller);
|
|
102
|
+
if (this.pendingCancellations.delete(runId))
|
|
103
|
+
controller.abort();
|
|
84
104
|
let seq = 0;
|
|
85
105
|
// 服务端已把本 run 判为终态(409):停止一切后续上报。
|
|
86
106
|
let serverTerminal = false;
|
|
@@ -173,12 +193,13 @@ export class RunDispatcher {
|
|
|
173
193
|
...runtimeProfileInstructions(payload, applied),
|
|
174
194
|
];
|
|
175
195
|
}
|
|
176
|
-
const
|
|
196
|
+
const persistentTurn = this.persistentSession?.prepareTurn(payload);
|
|
197
|
+
const { workspaceDir } = persistentTurn ?? ensureRunWorkspace(runId);
|
|
177
198
|
const missingCapabilities = missingRunCapabilities(payload, workspaceDir);
|
|
178
199
|
if (missingCapabilities.length > 0) {
|
|
179
200
|
throw new RuntimeExecutionError(`runtime is missing required capabilities: ${missingCapabilities.join(", ")}`, "runtime_unavailable");
|
|
180
201
|
}
|
|
181
|
-
const transcript = new TranscriptWriter(transcriptPath(runId));
|
|
202
|
+
const transcript = new TranscriptWriter(persistentTurn?.transcriptFile ?? transcriptPath(runId));
|
|
182
203
|
const timeoutSeconds = clampTimeoutSeconds(payload.limits.timeout_seconds);
|
|
183
204
|
timer = setTimeout(() => {
|
|
184
205
|
timedOut = true;
|
|
@@ -289,8 +310,23 @@ export class RunDispatcher {
|
|
|
289
310
|
file: async (file) => {
|
|
290
311
|
await this.client.postFile(runId, file);
|
|
291
312
|
},
|
|
313
|
+
runtimeSession: async (sessionId) => {
|
|
314
|
+
this.persistentSession?.persistNativeSession(sessionId);
|
|
315
|
+
},
|
|
292
316
|
};
|
|
293
|
-
await runtime.run({
|
|
317
|
+
await runtime.run({
|
|
318
|
+
payload,
|
|
319
|
+
workspaceDir,
|
|
320
|
+
...(persistentTurn
|
|
321
|
+
? {
|
|
322
|
+
nativeSessionId: persistentTurn.nativeSessionId,
|
|
323
|
+
contextRevision: persistentTurn.contextRevision,
|
|
324
|
+
...(persistentTurn.runtimeEnv
|
|
325
|
+
? { runtimeEnv: persistentTurn.runtimeEnv }
|
|
326
|
+
: {}),
|
|
327
|
+
}
|
|
328
|
+
: {}),
|
|
329
|
+
}, sink, controller.signal);
|
|
294
330
|
clearTimeout(timer);
|
|
295
331
|
if (serverTerminal)
|
|
296
332
|
return;
|
package/dist/runtime-env.d.ts
CHANGED
|
@@ -2,3 +2,8 @@
|
|
|
2
2
|
export declare function clearAgentServiceControlEnv(env?: NodeJS.ProcessEnv): void;
|
|
3
3
|
/** Copy an environment for a runtime child without leaking Agent Service control values. */
|
|
4
4
|
export declare function runtimeChildEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
5
|
+
/** Run model processes as the Template's untrusted runtime UID when configured. */
|
|
6
|
+
export declare function runtimeChildIdentity(env?: NodeJS.ProcessEnv): {
|
|
7
|
+
uid?: number;
|
|
8
|
+
gid?: number;
|
|
9
|
+
};
|
package/dist/runtime-env.js
CHANGED
|
@@ -3,6 +3,15 @@ const AGENT_SERVICE_CONTROL_ENV_KEYS = [
|
|
|
3
3
|
"BOTLEARN_AGENT_SERVICE_RUN_ID",
|
|
4
4
|
"BOTLEARN_AGENT_SERVICE_RUN_TOKEN",
|
|
5
5
|
"BOTLEARN_AGENT_SERVICE_WORKER_ID",
|
|
6
|
+
"BOTLEARN_AGENT_SERVICE_WS_URL",
|
|
7
|
+
"BOTLEARN_AGENT_SERVICE_RUNTIME_SESSION_ID",
|
|
8
|
+
"BOTLEARN_AGENT_SERVICE_SESSION_TOKEN",
|
|
9
|
+
];
|
|
10
|
+
const AGENT_SERVICE_SUPERVISOR_ENV_KEYS = [
|
|
11
|
+
"BOTLEARN_RUNTIME_UID",
|
|
12
|
+
"BOTLEARN_RUNTIME_GID",
|
|
13
|
+
"BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT",
|
|
14
|
+
"BOTLEARN_AGENT_SERVICE_PROFILE_ROOT",
|
|
6
15
|
];
|
|
7
16
|
/** Remove Course control-plane coordinates before any model/runtime child is created. */
|
|
8
17
|
export function clearAgentServiceControlEnv(env = process.env) {
|
|
@@ -13,5 +22,19 @@ export function clearAgentServiceControlEnv(env = process.env) {
|
|
|
13
22
|
export function runtimeChildEnv(env = process.env) {
|
|
14
23
|
const childEnv = { ...env };
|
|
15
24
|
clearAgentServiceControlEnv(childEnv);
|
|
25
|
+
for (const key of AGENT_SERVICE_SUPERVISOR_ENV_KEYS)
|
|
26
|
+
delete childEnv[key];
|
|
16
27
|
return childEnv;
|
|
17
28
|
}
|
|
29
|
+
/** Run model processes as the Template's untrusted runtime UID when configured. */
|
|
30
|
+
export function runtimeChildIdentity(env = process.env) {
|
|
31
|
+
const uid = Number(env.BOTLEARN_RUNTIME_UID);
|
|
32
|
+
const gid = Number(env.BOTLEARN_RUNTIME_GID);
|
|
33
|
+
if (!Number.isInteger(uid) ||
|
|
34
|
+
uid < 1 ||
|
|
35
|
+
!Number.isInteger(gid) ||
|
|
36
|
+
gid < 1) {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
return { uid, gid };
|
|
40
|
+
}
|
package/dist/runtime-profile.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { chmodSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { runtimeProfileDir,
|
|
4
|
+
import { runtimeProfileDir, runtimeProfileRunRootDir } from "./workspace.js";
|
|
5
5
|
const SAFE_ASSET_ID = /^[a-z0-9][a-z0-9._-]{0,159}$/;
|
|
6
6
|
const SHA256 = /^sha256:[0-9a-f]{64}$/;
|
|
7
7
|
const MAX_SKILL_PACKAGES = 32;
|
|
@@ -20,13 +20,19 @@ export function applyRunRuntimeProfile(payload, raw) {
|
|
|
20
20
|
if (payload.context.profileHash !== profile.profileHash) {
|
|
21
21
|
throw new RuntimeProfileApplyError("run.start profileHash does not match runtime profile");
|
|
22
22
|
}
|
|
23
|
-
const runDir =
|
|
23
|
+
const runDir = runtimeProfileRunRootDir(payload.agent_run_id);
|
|
24
24
|
const target = runtimeProfileDir(payload.agent_run_id);
|
|
25
|
-
|
|
25
|
+
const managedReadView = Boolean(process.env.BOTLEARN_AGENT_SERVICE_PROFILE_ROOT?.trim());
|
|
26
|
+
const directoryMode = managedReadView ? 0o750 : 0o700;
|
|
27
|
+
const fileMode = managedReadView ? 0o640 : 0o600;
|
|
28
|
+
mkdirSync(runDir, { recursive: true, mode: directoryMode });
|
|
29
|
+
chmodSync(runDir, directoryMode);
|
|
26
30
|
const staging = mkdtempSync(path.join(runDir, ".runtime-profile-"));
|
|
31
|
+
chmodSync(staging, directoryMode);
|
|
27
32
|
try {
|
|
28
33
|
const skillsRoot = path.join(staging, "skills");
|
|
29
|
-
mkdirSync(skillsRoot, { recursive: true, mode:
|
|
34
|
+
mkdirSync(skillsRoot, { recursive: true, mode: directoryMode });
|
|
35
|
+
chmodSync(skillsRoot, directoryMode);
|
|
30
36
|
const skillRefs = [];
|
|
31
37
|
const skillIds = new Set();
|
|
32
38
|
for (const skill of profile.skillPackages) {
|
|
@@ -34,12 +40,14 @@ export function applyRunRuntimeProfile(payload, raw) {
|
|
|
34
40
|
throw new RuntimeProfileApplyError(`duplicate runtime Skill id: ${skill.id}`);
|
|
35
41
|
}
|
|
36
42
|
skillIds.add(skill.id);
|
|
37
|
-
installSkillPackage(skillsRoot, skill);
|
|
43
|
+
installSkillPackage(skillsRoot, skill, { directoryMode, fileMode });
|
|
38
44
|
skillRefs.push(`${skill.id}@${skill.version}`);
|
|
39
45
|
}
|
|
40
46
|
const promptPackPath = path.join(staging, "prompt-pack.md");
|
|
41
|
-
writeFileSync(promptPackPath, profile.promptPack.systemInstructions, { mode:
|
|
42
|
-
|
|
47
|
+
writeFileSync(promptPackPath, profile.promptPack.systemInstructions, { mode: fileMode });
|
|
48
|
+
chmodSync(promptPackPath, fileMode);
|
|
49
|
+
const manifestPath = path.join(staging, "profile.json");
|
|
50
|
+
writeFileSync(manifestPath, `${JSON.stringify({
|
|
43
51
|
schemaVersion: profile.schemaVersion,
|
|
44
52
|
profileId: profile.profileId,
|
|
45
53
|
profileHash: profile.profileHash,
|
|
@@ -51,7 +59,8 @@ export function applyRunRuntimeProfile(payload, raw) {
|
|
|
51
59
|
},
|
|
52
60
|
skillRefs,
|
|
53
61
|
requiredCapabilities: profile.requiredCapabilities,
|
|
54
|
-
}, null, 2)}\n`, { mode:
|
|
62
|
+
}, null, 2)}\n`, { mode: fileMode });
|
|
63
|
+
chmodSync(manifestPath, fileMode);
|
|
55
64
|
rmSync(target, { recursive: true, force: true });
|
|
56
65
|
renameSync(staging, target);
|
|
57
66
|
return {
|
|
@@ -94,7 +103,7 @@ export function runtimeProfileInstructions(payload, applied) {
|
|
|
94
103
|
: []),
|
|
95
104
|
];
|
|
96
105
|
}
|
|
97
|
-
function installSkillPackage(skillsRoot, skill) {
|
|
106
|
+
function installSkillPackage(skillsRoot, skill, modes) {
|
|
98
107
|
assertSafeAssetId(skill.id, "Skill id");
|
|
99
108
|
if (!SHA256.test(skill.digest)) {
|
|
100
109
|
throw new RuntimeProfileApplyError(`Skill ${skill.id} has an invalid digest`);
|
|
@@ -104,11 +113,14 @@ function installSkillPackage(skillsRoot, skill) {
|
|
|
104
113
|
throw new RuntimeProfileApplyError(`Skill ${skill.id}@${skill.version} digest mismatch`);
|
|
105
114
|
}
|
|
106
115
|
const skillDir = path.join(skillsRoot, skill.id);
|
|
107
|
-
mkdirSync(skillDir, { recursive: true, mode:
|
|
116
|
+
mkdirSync(skillDir, { recursive: true, mode: modes.directoryMode });
|
|
117
|
+
chmodSync(skillDir, modes.directoryMode);
|
|
108
118
|
for (const entry of entries) {
|
|
109
119
|
const destination = path.join(skillDir, entry.path);
|
|
110
|
-
mkdirSync(path.dirname(destination), { recursive: true, mode:
|
|
111
|
-
|
|
120
|
+
mkdirSync(path.dirname(destination), { recursive: true, mode: modes.directoryMode });
|
|
121
|
+
chmodSync(path.dirname(destination), modes.directoryMode);
|
|
122
|
+
writeFileSync(destination, entry.content, { mode: modes.fileMode });
|
|
123
|
+
chmodSync(destination, modes.fileMode);
|
|
112
124
|
}
|
|
113
125
|
}
|
|
114
126
|
function archiveEntries(skill) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { runtimeChildIdentity } from "../runtime-env.js";
|
|
2
3
|
import { sanitizeRuntimeFailureText } from "../redaction.js";
|
|
3
4
|
import { sliceUtf8Bytes, utf8ByteLength } from "./text-cap.js";
|
|
4
5
|
import { consoleLogger, } from "./engine.js";
|
|
@@ -206,6 +207,7 @@ export class AcpRuntimeAdapter {
|
|
|
206
207
|
const child = spawn(binary, args, {
|
|
207
208
|
cwd: opts.cwd,
|
|
208
209
|
env: this.spawnEnv(opts),
|
|
210
|
+
...runtimeChildIdentity(),
|
|
209
211
|
stdio: ["pipe", "pipe", "pipe"],
|
|
210
212
|
});
|
|
211
213
|
let killTimer = null;
|
package/dist/runtimes/codex.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export declare class CodexAdapter extends NdjsonStreamAdapter {
|
|
|
38
38
|
* `--` 隔开 flags 与 positionals,防止以 `-` 开头的 prompt 被解析成选项。
|
|
39
39
|
*/
|
|
40
40
|
protected buildArgs(opts: EngineRunOptions): string[];
|
|
41
|
-
protected spawnEnv(
|
|
41
|
+
protected spawnEnv(opts: EngineRunOptions): NodeJS.ProcessEnv;
|
|
42
42
|
protected handleEvent(raw: unknown, ctx: NdjsonEventCtx): void;
|
|
43
43
|
}
|
|
44
44
|
export declare const codexModule: RuntimeModule;
|
package/dist/runtimes/codex.js
CHANGED
|
@@ -181,9 +181,9 @@ export class CodexAdapter extends NdjsonStreamAdapter {
|
|
|
181
181
|
}
|
|
182
182
|
return ["exec", ...tail, "--", prompt];
|
|
183
183
|
}
|
|
184
|
-
spawnEnv(
|
|
184
|
+
spawnEnv(opts) {
|
|
185
185
|
return {
|
|
186
|
-
...
|
|
186
|
+
...super.spawnEnv(opts),
|
|
187
187
|
// 保证 JSONL 输出不混入 ANSI 转义。
|
|
188
188
|
FORCE_COLOR: "0",
|
|
189
189
|
NO_COLOR: "1",
|