@botlearn-course/daemon 0.0.6 → 0.0.8
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 +3 -0
- package/dist/agent-service-client.js +8 -8
- package/dist/agent-service-sandbox.d.ts +108 -0
- package/dist/agent-service-sandbox.js +1400 -0
- package/dist/agent-service-ws-protocol.d.ts +15 -7
- package/dist/agent-service-ws-protocol.js +86 -24
- package/dist/cli.js +26 -53
- package/dist/course-client.js +9 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/run-dispatcher.d.ts +3 -0
- package/dist/run-dispatcher.js +194 -13
- package/dist/runtime-env.d.ts +6 -0
- package/dist/runtime-env.js +35 -2
- package/dist/runtime-profile.js +12 -1
- package/dist/runtimes/claude-code.js +14 -3
- package/dist/runtimes/codex.js +17 -1
- package/dist/runtimes/deepseek-tui.d.ts +3 -2
- package/dist/runtimes/deepseek-tui.js +87 -15
- package/dist/runtimes/engine.d.ts +5 -0
- package/dist/runtimes/engine.js +22 -2
- package/dist/runtimes/fake.js +102 -6
- package/dist/runtimes/gemini.js +14 -1
- package/dist/runtimes/hermes-agent.js +19 -2
- package/dist/runtimes/kimi.js +9 -1
- package/dist/runtimes/openclaw-acp.js +21 -2
- package/dist/runtimes/progress.d.ts +13 -1
- package/dist/runtimes/progress.js +61 -6
- package/dist/sandbox-supervisor.d.ts +1 -1
- package/dist/sandbox-supervisor.js +23 -23
- package/dist/types.d.ts +5 -0
- package/dist/workspace.d.ts +27 -4
- package/dist/workspace.js +96 -23
- package/package.json +1 -1
- package/dist/agent-service-session.d.ts +0 -67
- package/dist/agent-service-session.js +0 -796
package/dist/runtimes/engine.js
CHANGED
|
@@ -88,7 +88,15 @@ export function wrapEngineAdapter(id, engine, opts) {
|
|
|
88
88
|
id,
|
|
89
89
|
async run(run, sink, signal) {
|
|
90
90
|
const payload = run.payload;
|
|
91
|
-
|
|
91
|
+
// DeepSeek persists and replays the native thread history. The durable Course Service
|
|
92
|
+
// conversation is recovery/bootstrap data, not a second history to inject on every turn.
|
|
93
|
+
// Agent Service activations intentionally start a fresh local server because their model
|
|
94
|
+
// credential expires with the turn, so a cached thread id cannot suppress durable context.
|
|
95
|
+
const managedActivation = Boolean(run.runtimeEnv?.BOTLEARN_AGENT_SERVICE_ACTIVATION_ID?.trim());
|
|
96
|
+
const resumesDeepseekThread = id === "deepseek-tui" && !managedActivation && Boolean(run.nativeSessionId?.trim());
|
|
97
|
+
const text = resumesDeepseekThread
|
|
98
|
+
? (payload.input.text ?? "")
|
|
99
|
+
: renderConversationInput(payload);
|
|
92
100
|
if (!text.trim()) {
|
|
93
101
|
throw new RuntimeExecutionError("empty task brief");
|
|
94
102
|
}
|
|
@@ -131,10 +139,22 @@ export function wrapEngineAdapter(id, engine, opts) {
|
|
|
131
139
|
return;
|
|
132
140
|
}
|
|
133
141
|
const kind = BLOCK_KIND_MAP[block.kind] ?? "status";
|
|
134
|
-
queueBlock({
|
|
142
|
+
queueBlock({
|
|
143
|
+
kind,
|
|
144
|
+
raw: block.raw,
|
|
145
|
+
...(block.text !== undefined ? { text: block.text } : {}),
|
|
146
|
+
...(block.name !== undefined ? { name: block.name } : {}),
|
|
147
|
+
...(block.status !== undefined ? { status: block.status } : {}),
|
|
148
|
+
});
|
|
135
149
|
},
|
|
136
150
|
onStatus: (event) => {
|
|
137
151
|
consoleLogger.debug(`${id} status`, { kind: event.kind, phase: event.phase });
|
|
152
|
+
if (event.kind === "thinking") {
|
|
153
|
+
queueBlock({
|
|
154
|
+
kind: "thinking",
|
|
155
|
+
phase: event.phase === "stopped" ? "completed" : "in_progress",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
138
158
|
},
|
|
139
159
|
});
|
|
140
160
|
// Preserve provider event order through durable run.block before the final run.message.
|
package/dist/runtimes/fake.js
CHANGED
|
@@ -1,7 +1,35 @@
|
|
|
1
|
-
import { writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { RuntimeExecutionError } from "../types.js";
|
|
3
4
|
/** message 里回显的任务简报截断长度。 */
|
|
4
5
|
const BRIEF_PREVIEW_CHARS = 80;
|
|
6
|
+
const RUNTIME_ERROR_SCENARIO = "[[e2e:runtime-error]]";
|
|
7
|
+
const SLOW_RUNTIME_SCENARIO = "[[e2e:slow]]";
|
|
8
|
+
function configuredStepDelayMs(brief) {
|
|
9
|
+
if (brief.includes(SLOW_RUNTIME_SCENARIO))
|
|
10
|
+
return 1_000;
|
|
11
|
+
const configured = Number(process.env.BOTLEARN_FAKE_RUNTIME_STEP_DELAY_MS ?? 0);
|
|
12
|
+
if (!Number.isFinite(configured))
|
|
13
|
+
return 0;
|
|
14
|
+
return Math.max(0, Math.min(5_000, Math.floor(configured)));
|
|
15
|
+
}
|
|
16
|
+
async function pauseBetweenSteps(signal, runtimeId, brief) {
|
|
17
|
+
const delayMs = configuredStepDelayMs(brief);
|
|
18
|
+
throwIfAborted(signal, runtimeId);
|
|
19
|
+
if (delayMs <= 0)
|
|
20
|
+
return;
|
|
21
|
+
await new Promise((resolve, reject) => {
|
|
22
|
+
const timer = setTimeout(() => {
|
|
23
|
+
signal.removeEventListener("abort", onAbort);
|
|
24
|
+
resolve();
|
|
25
|
+
}, delayMs);
|
|
26
|
+
const onAbort = () => {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
reject(new Error(`${runtimeId} runtime aborted`));
|
|
29
|
+
};
|
|
30
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
31
|
+
});
|
|
32
|
+
}
|
|
5
33
|
/**
|
|
6
34
|
* 确定性假 runtime:不 spawn 任何进程,直接实现 CourseRuntime。
|
|
7
35
|
* 仅供端到端联调/测试(registry 里 hidden,capabilities 需
|
|
@@ -15,18 +43,86 @@ export class FakeRuntime {
|
|
|
15
43
|
}
|
|
16
44
|
async run(run, sink, signal) {
|
|
17
45
|
const brief = run.payload.input.text ?? "";
|
|
18
|
-
const
|
|
46
|
+
const draftPath = path.join(run.workspaceDir, "draft.md");
|
|
47
|
+
const priorDraft = existsSync(draftPath) ? readFileSync(draftPath, "utf8") : "";
|
|
48
|
+
const isContinuation = priorDraft.length > 0;
|
|
49
|
+
if (isContinuation && run.contextRevision !== undefined && !run.nativeSessionId) {
|
|
50
|
+
throw new RuntimeExecutionError("E2E FakeRuntime lost its native session between turns", "runtime_unavailable");
|
|
51
|
+
}
|
|
52
|
+
if (!isContinuation && run.contextRevision !== undefined) {
|
|
53
|
+
await sink.runtimeSession?.(`fake-thread-${run.payload.course_run_id}`);
|
|
54
|
+
}
|
|
55
|
+
const draft = isContinuation
|
|
56
|
+
? [
|
|
57
|
+
"# E2E 草稿",
|
|
58
|
+
"",
|
|
59
|
+
"第一段:这是由真实 Course Daemon 协议生成的初稿内容。",
|
|
60
|
+
"",
|
|
61
|
+
"第二段:本轮读取并延续了上一轮工作区中的 draft.md。",
|
|
62
|
+
].join("\n")
|
|
63
|
+
: [
|
|
64
|
+
"# E2E 草稿",
|
|
65
|
+
"",
|
|
66
|
+
"第一段:这是由真实 Course Daemon 协议生成的初稿内容。",
|
|
67
|
+
].join("\n");
|
|
68
|
+
const finalText = isContinuation
|
|
69
|
+
? `已读取同一工作区的上一轮 draft.md,并补全为标题和两段正文。\n\n${draft}`
|
|
70
|
+
: `已生成包含标题和第一段正文的初稿,请检查后继续完善。\n\n${draft}\n\n任务:${brief.slice(0, BRIEF_PREVIEW_CHARS)}`;
|
|
19
71
|
const blocks = [
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
72
|
+
{
|
|
73
|
+
kind: "status",
|
|
74
|
+
text: "deterministic runtime started",
|
|
75
|
+
raw: { event: "started" },
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
kind: "thinking",
|
|
79
|
+
phase: "in_progress",
|
|
80
|
+
text: "正在检查任务和工作区。",
|
|
81
|
+
raw: { event: "reasoning" },
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
kind: "tool_call",
|
|
85
|
+
name: "write_file",
|
|
86
|
+
text: "draft.md",
|
|
87
|
+
raw: { event: "tool_call" },
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
kind: "progress",
|
|
91
|
+
runtime: this.id,
|
|
92
|
+
summary: isContinuation ? "正在补全草稿" : "正在生成初稿",
|
|
93
|
+
status: "in_progress",
|
|
94
|
+
},
|
|
23
95
|
];
|
|
24
96
|
for (const block of blocks) {
|
|
25
97
|
throwIfAborted(signal, this.id);
|
|
26
98
|
await sink.block(block);
|
|
99
|
+
await pauseBetweenSteps(signal, this.id, brief);
|
|
100
|
+
}
|
|
101
|
+
if (brief.includes(RUNTIME_ERROR_SCENARIO)) {
|
|
102
|
+
throw new RuntimeExecutionError("E2E deterministic runtime failure");
|
|
27
103
|
}
|
|
28
104
|
throwIfAborted(signal, this.id);
|
|
29
|
-
writeFileSync(
|
|
105
|
+
writeFileSync(draftPath, draft, "utf8");
|
|
106
|
+
await sink.block({
|
|
107
|
+
kind: "tool_result",
|
|
108
|
+
name: "write_file",
|
|
109
|
+
status: "completed",
|
|
110
|
+
text: "draft.md",
|
|
111
|
+
raw: { event: "tool_result" },
|
|
112
|
+
});
|
|
113
|
+
await pauseBetweenSteps(signal, this.id, brief);
|
|
114
|
+
await sink.block({
|
|
115
|
+
kind: "thinking",
|
|
116
|
+
phase: "completed",
|
|
117
|
+
raw: { event: "reasoning" },
|
|
118
|
+
});
|
|
119
|
+
await sink.block({
|
|
120
|
+
kind: "progress",
|
|
121
|
+
runtime: this.id,
|
|
122
|
+
summary: isContinuation ? "草稿已补全" : "初稿已生成",
|
|
123
|
+
status: "completed",
|
|
124
|
+
});
|
|
125
|
+
await sink.block({ kind: "text_delta", text: finalText, raw: { event: "text" } });
|
|
30
126
|
await sink.message(finalText);
|
|
31
127
|
}
|
|
32
128
|
}
|
package/dist/runtimes/gemini.js
CHANGED
|
@@ -238,7 +238,20 @@ function normalizeBlock(obj, seq) {
|
|
|
238
238
|
else if (type === "init" || type === "result") {
|
|
239
239
|
kind = "system";
|
|
240
240
|
}
|
|
241
|
-
return {
|
|
241
|
+
return {
|
|
242
|
+
raw: obj,
|
|
243
|
+
kind,
|
|
244
|
+
seq,
|
|
245
|
+
...(kind === "assistant_text" && typeof obj.content === "string"
|
|
246
|
+
? { text: obj.content }
|
|
247
|
+
: {}),
|
|
248
|
+
...(kind === "tool_use" && typeof obj.tool_name === "string"
|
|
249
|
+
? { name: obj.tool_name }
|
|
250
|
+
: {}),
|
|
251
|
+
...(kind === "tool_result"
|
|
252
|
+
? { status: obj.status === "error" ? "error" : "completed" }
|
|
253
|
+
: {}),
|
|
254
|
+
};
|
|
242
255
|
}
|
|
243
256
|
export const geminiModule = {
|
|
244
257
|
id: "gemini",
|
|
@@ -123,7 +123,7 @@ export class HermesAgentAdapter extends AcpRuntimeAdapter {
|
|
|
123
123
|
blockKind = "assistant_text";
|
|
124
124
|
}
|
|
125
125
|
else if (kind === "tool_call" || kind === "tool_call_update") {
|
|
126
|
-
blockKind = "tool_use";
|
|
126
|
+
blockKind = kind === "tool_call_update" ? "tool_result" : "tool_use";
|
|
127
127
|
}
|
|
128
128
|
else if (kind === "user_message_chunk") {
|
|
129
129
|
blockKind = "other";
|
|
@@ -132,7 +132,24 @@ export class HermesAgentAdapter extends AcpRuntimeAdapter {
|
|
|
132
132
|
const status = hermesStatusEvent(kind, update, assistantTextSeen);
|
|
133
133
|
if (status)
|
|
134
134
|
ctx.emitStatus(status);
|
|
135
|
-
|
|
135
|
+
const tool = update.toolCall;
|
|
136
|
+
ctx.emitBlock({
|
|
137
|
+
raw: params,
|
|
138
|
+
kind: blockKind,
|
|
139
|
+
seq: ctx.seq,
|
|
140
|
+
...(blockKind === "assistant_text" && kind === "agent_message_chunk"
|
|
141
|
+
? {
|
|
142
|
+
text: update.content?.text ?? "",
|
|
143
|
+
}
|
|
144
|
+
: {}),
|
|
145
|
+
...((blockKind === "tool_use" || blockKind === "tool_result")
|
|
146
|
+
&& typeof tool?.name === "string"
|
|
147
|
+
? { name: tool.name }
|
|
148
|
+
: {}),
|
|
149
|
+
...(blockKind === "tool_result"
|
|
150
|
+
? { status: tool?.status === "failed" ? "error" : "completed" }
|
|
151
|
+
: {}),
|
|
152
|
+
});
|
|
136
153
|
}
|
|
137
154
|
/**
|
|
138
155
|
* owner 信任:选第一个 `kind` 以 `allow_` 开头的选项,没有再退回第一个
|
package/dist/runtimes/kimi.js
CHANGED
|
@@ -323,7 +323,15 @@ function normalizeBlock(obj, seq) {
|
|
|
323
323
|
else if (obj.category || obj.severity) {
|
|
324
324
|
kind = "system";
|
|
325
325
|
}
|
|
326
|
-
|
|
326
|
+
const text = kind === "assistant_text" ? extractText(obj.content) : "";
|
|
327
|
+
return {
|
|
328
|
+
raw: obj,
|
|
329
|
+
kind,
|
|
330
|
+
seq,
|
|
331
|
+
...(text ? { text } : {}),
|
|
332
|
+
...(kind === "tool_use" ? { name: firstToolName(obj.tool_calls) } : {}),
|
|
333
|
+
...(kind === "tool_result" ? { status: "completed" } : {}),
|
|
334
|
+
};
|
|
327
335
|
}
|
|
328
336
|
export const kimiCliModule = {
|
|
329
337
|
id: "kimi-cli",
|
|
@@ -177,12 +177,30 @@ export class OpenclawAcpAdapter {
|
|
|
177
177
|
if (!text)
|
|
178
178
|
return;
|
|
179
179
|
seq += 1;
|
|
180
|
-
emitBlock({
|
|
180
|
+
emitBlock({
|
|
181
|
+
raw: sanitizeAssistantChunk(note, text),
|
|
182
|
+
kind: "assistant_text",
|
|
183
|
+
seq,
|
|
184
|
+
text,
|
|
185
|
+
});
|
|
181
186
|
return;
|
|
182
187
|
}
|
|
183
188
|
seq += 1;
|
|
184
189
|
const kind = classifyAcpUpdate(note);
|
|
185
|
-
|
|
190
|
+
const toolCall = update?.toolCall;
|
|
191
|
+
const toolName = toolCall && typeof toolCall.name === "string" ? toolCall.name : undefined;
|
|
192
|
+
const toolStatus = toolCall && typeof toolCall.status === "string" ? toolCall.status.toLowerCase() : "";
|
|
193
|
+
emitBlock({
|
|
194
|
+
raw: note,
|
|
195
|
+
kind,
|
|
196
|
+
seq,
|
|
197
|
+
...((kind === "tool_use" || kind === "tool_result") && toolName
|
|
198
|
+
? { name: toolName }
|
|
199
|
+
: {}),
|
|
200
|
+
...(kind === "tool_result"
|
|
201
|
+
? { status: /fail|error/.test(toolStatus) ? "error" : "completed" }
|
|
202
|
+
: {}),
|
|
203
|
+
});
|
|
186
204
|
};
|
|
187
205
|
let abortListener;
|
|
188
206
|
try {
|
|
@@ -293,6 +311,7 @@ export class OpenclawAcpAdapter {
|
|
|
293
311
|
},
|
|
294
312
|
kind: "assistant_text",
|
|
295
313
|
seq,
|
|
314
|
+
text: textForBlock,
|
|
296
315
|
});
|
|
297
316
|
}
|
|
298
317
|
}
|
|
@@ -23,6 +23,11 @@ export interface ProgressMcpConfig {
|
|
|
23
23
|
export interface ProgressMcpConfigOptions {
|
|
24
24
|
/** undefined auto-discovers explicit/default config; null creates a progress-only config. */
|
|
25
25
|
baseConfigPath?: string | null;
|
|
26
|
+
/**
|
|
27
|
+
* undefined auto-detects the managed runtime boundary, null forces a private BYOA
|
|
28
|
+
* config, and a path writes a sanitized runtime-readable config below that root.
|
|
29
|
+
*/
|
|
30
|
+
managedRoot?: string | null;
|
|
26
31
|
platform?: NodeJS.Platform;
|
|
27
32
|
}
|
|
28
33
|
export declare class ProgressMcpConfigError extends Error {
|
|
@@ -45,6 +50,13 @@ export declare function isDeepseekProgressCompletion(payload: unknown, state: De
|
|
|
45
50
|
*/
|
|
46
51
|
export declare function progressMcpAutoInjectionSupported(platform?: NodeJS.Platform): boolean;
|
|
47
52
|
export declare function resolveExistingDeepseekMcpConfig(env?: NodeJS.ProcessEnv, home?: string): string | null;
|
|
48
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Create an ephemeral DeepSeek MCP config.
|
|
55
|
+
*
|
|
56
|
+
* BYOA keeps the user's existing MCP settings in a private 0700/0600 directory. Managed
|
|
57
|
+
* Agent Service sessions instead write a progress-only 0750/0640 handoff below the
|
|
58
|
+
* supervisor-owned profile root so the separate runtime UID can read it without exposing
|
|
59
|
+
* control-plane configuration.
|
|
60
|
+
*/
|
|
49
61
|
export declare function createProgressMcpConfig(options?: ProgressMcpConfigOptions): ProgressMcpConfig;
|
|
50
62
|
export declare function cleanupProgressMcpConfig(config: ProgressMcpConfig | undefined): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
1
|
+
import { chmodSync, existsSync, lstatSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { homedir, tmpdir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -10,6 +10,7 @@ export const DEEPSEEK_PROGRESS_TOOL_ALIASES = new Set([
|
|
|
10
10
|
]);
|
|
11
11
|
export const DEEPSEEK_PROGRESS_SYSTEM_INSTRUCTION = [
|
|
12
12
|
"BotLearn execution progress reporting:",
|
|
13
|
+
"- DeepSeek may defer this MCP tool. Before the first meaningful progress phase, if report_progress is not exposed, call tool_search_tool_regex once with query report_progress, then call the discovered progress tool.",
|
|
13
14
|
"- Use report_progress only when a meaningful user-visible execution phase starts or completes.",
|
|
14
15
|
"- Use status in_progress at phase start and completed only when that execution phase actually ends.",
|
|
15
16
|
"- Short tasks need no progress report; do not report every command, file read, or retry.",
|
|
@@ -120,22 +121,37 @@ export function resolveExistingDeepseekMcpConfig(env = process.env, home = homed
|
|
|
120
121
|
const defaultPath = path.join(home, ".deepseek", "mcp.json");
|
|
121
122
|
return existsSync(defaultPath) ? defaultPath : null;
|
|
122
123
|
}
|
|
123
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* Create an ephemeral DeepSeek MCP config.
|
|
126
|
+
*
|
|
127
|
+
* BYOA keeps the user's existing MCP settings in a private 0700/0600 directory. Managed
|
|
128
|
+
* Agent Service sessions instead write a progress-only 0750/0640 handoff below the
|
|
129
|
+
* supervisor-owned profile root so the separate runtime UID can read it without exposing
|
|
130
|
+
* control-plane configuration.
|
|
131
|
+
*/
|
|
124
132
|
export function createProgressMcpConfig(options = {}) {
|
|
125
133
|
const platform = options.platform ?? process.platform;
|
|
126
134
|
if (!progressMcpAutoInjectionSupported(platform)) {
|
|
127
135
|
throw new ProgressMcpConfigError("report_progress MCP auto-injection is unavailable on Windows because clean child env isolation cannot be guaranteed");
|
|
128
136
|
}
|
|
137
|
+
const managedRoot = resolveManagedProgressRoot(options.managedRoot);
|
|
138
|
+
if (managedRoot
|
|
139
|
+
&& options.baseConfigPath !== undefined
|
|
140
|
+
&& options.baseConfigPath !== null) {
|
|
141
|
+
throw new ProgressMcpConfigError("managed progress MCP config cannot preserve an existing DeepSeek MCP config");
|
|
142
|
+
}
|
|
129
143
|
const serverPath = fileURLToPath(new URL("../mcp/report-progress-server.js", import.meta.url));
|
|
130
|
-
const baseConfigPath =
|
|
131
|
-
?
|
|
132
|
-
: options.baseConfigPath
|
|
144
|
+
const baseConfigPath = managedRoot
|
|
145
|
+
? null
|
|
146
|
+
: options.baseConfigPath === undefined
|
|
147
|
+
? resolveExistingDeepseekMcpConfig()
|
|
148
|
+
: options.baseConfigPath;
|
|
133
149
|
const baseConfig = loadBaseMcpConfig(baseConfigPath);
|
|
134
150
|
const baseServers = mergeMcpServerFields(baseConfig);
|
|
135
151
|
if (Object.hasOwn(baseServers, "botlearn")) {
|
|
136
152
|
throw new ProgressMcpConfigError("DeepSeek MCP server key 'botlearn' is reserved for BotLearn progress reporting");
|
|
137
153
|
}
|
|
138
|
-
const dir = mkdtempSync(path.join(tmpdir(), "botlearn-progress-mcp-"));
|
|
154
|
+
const dir = mkdtempSync(path.join(managedRoot ?? tmpdir(), "botlearn-progress-mcp-"));
|
|
139
155
|
const configPath = path.join(dir, "mcp.json");
|
|
140
156
|
const stagingPath = path.join(dir, ".mcp.json.tmp");
|
|
141
157
|
const minimalPath = "/usr/bin:/bin";
|
|
@@ -157,7 +173,11 @@ export function createProgressMcpConfig(options = {}) {
|
|
|
157
173
|
},
|
|
158
174
|
};
|
|
159
175
|
try {
|
|
176
|
+
if (managedRoot)
|
|
177
|
+
chmodSync(dir, 0o750);
|
|
160
178
|
writeFileSync(stagingPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
179
|
+
if (managedRoot)
|
|
180
|
+
chmodSync(stagingPath, 0o640);
|
|
161
181
|
renameSync(stagingPath, configPath);
|
|
162
182
|
return { dir, path: configPath };
|
|
163
183
|
}
|
|
@@ -166,6 +186,41 @@ export function createProgressMcpConfig(options = {}) {
|
|
|
166
186
|
throw error;
|
|
167
187
|
}
|
|
168
188
|
}
|
|
189
|
+
function resolveManagedProgressRoot(explicit) {
|
|
190
|
+
if (explicit === null)
|
|
191
|
+
return null;
|
|
192
|
+
let root = explicit?.trim();
|
|
193
|
+
if (root === undefined) {
|
|
194
|
+
const managedRuntime = process.env.BOTLEARN_RUNTIME_USER?.trim();
|
|
195
|
+
if (!managedRuntime)
|
|
196
|
+
return null;
|
|
197
|
+
root = process.env.BOTLEARN_AGENT_SERVICE_PROFILE_ROOT?.trim();
|
|
198
|
+
if (!root) {
|
|
199
|
+
throw new ProgressMcpConfigError("managed runtime is missing BOTLEARN_AGENT_SERVICE_PROFILE_ROOT");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (!root || !path.isAbsolute(root)) {
|
|
203
|
+
throw new ProgressMcpConfigError("managed progress MCP root must be an absolute path");
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const stat = lstatSync(root);
|
|
207
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
208
|
+
throw new Error("root is not a regular directory");
|
|
209
|
+
}
|
|
210
|
+
const currentUid = process.getuid?.();
|
|
211
|
+
if (currentUid !== undefined && stat.uid !== currentUid) {
|
|
212
|
+
throw new Error("root is not owned by the control process uid");
|
|
213
|
+
}
|
|
214
|
+
if ((stat.mode & 0o022) !== 0) {
|
|
215
|
+
throw new Error("root must not be group or world writable");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
220
|
+
throw new ProgressMcpConfigError(`managed progress MCP root is unavailable: ${message}`);
|
|
221
|
+
}
|
|
222
|
+
return root;
|
|
223
|
+
}
|
|
169
224
|
function loadBaseMcpConfig(configPath) {
|
|
170
225
|
if (!configPath)
|
|
171
226
|
return {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export declare function
|
|
2
|
+
export declare function acquireSandboxSupervisorLock(sandboxId: string, lockRoot?: string): (() => void) | null;
|
|
3
3
|
export declare function runSandboxSupervisor(argv: string[]): Promise<number>;
|
|
4
4
|
export declare function isMainModule(entry?: string): boolean;
|
|
@@ -49,8 +49,8 @@ async function readOneShotBootstrap() {
|
|
|
49
49
|
throw new Error("sandbox supervisor bootstrap is empty");
|
|
50
50
|
return Buffer.concat(chunks);
|
|
51
51
|
}
|
|
52
|
-
export function
|
|
53
|
-
const lockDir = path.join(lockRoot,
|
|
52
|
+
export function acquireSandboxSupervisorLock(sandboxId, lockRoot = SUPERVISOR_LOCK_ROOT) {
|
|
53
|
+
const lockDir = path.join(lockRoot, sandboxId);
|
|
54
54
|
const pidFile = path.join(lockDir, "pid");
|
|
55
55
|
mkdirSync(lockRoot, { recursive: true, mode: 0o700 });
|
|
56
56
|
try {
|
|
@@ -84,23 +84,23 @@ export function acquireSessionSupervisorLock(runtimeSessionId, lockRoot = SUPERV
|
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
-
function prepareDirectories(controlUid, controlGid
|
|
87
|
+
function prepareDirectories(controlUid, controlGid) {
|
|
88
88
|
mkdirSync(CONTROL_HOME, { recursive: true, mode: 0o700 });
|
|
89
89
|
chownSync("/home/botlearn-control/.botlearn-course", controlUid, controlGid);
|
|
90
90
|
chownSync(CONTROL_HOME, controlUid, controlGid);
|
|
91
91
|
chmodSync(CONTROL_HOME, 0o700);
|
|
92
|
-
mkdirSync(RUNTIME_PROFILE_ROOT, { recursive: true, mode:
|
|
92
|
+
mkdirSync(RUNTIME_PROFILE_ROOT, { recursive: true, mode: 0o710 });
|
|
93
93
|
chownSync(RUNTIME_PROFILE_ROOT, controlUid, controlGid);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
chmodSync(
|
|
94
|
+
// Runtime can traverse to the active run profile path named in its instructions,
|
|
95
|
+
// but cannot enumerate profiles belonging to other activations.
|
|
96
|
+
chmodSync(RUNTIME_PROFILE_ROOT, 0o710);
|
|
97
|
+
// /workspace 只准备根目录;per-session 子目录
|
|
98
|
+
// `<root>/<runtime_session_id>/generation-<sandbox_generation>` 由 daemon
|
|
99
|
+
// (botlearn-control)在 session.open 时创建。根目录只允许 runtime 组穿过,
|
|
100
|
+
// 不允许列举 session id;daemon 仅在 activation 期间开放目标子目录。
|
|
101
|
+
mkdirSync(WORKSPACE, { recursive: true, mode: 0o710 });
|
|
102
|
+
chownSync(WORKSPACE, controlUid, controlGid);
|
|
103
|
+
chmodSync(WORKSPACE, 0o710);
|
|
104
104
|
}
|
|
105
105
|
export async function runSandboxSupervisor(argv) {
|
|
106
106
|
if (argv.length !== 2 || argv[0] !== "agent-service" || argv[1] !== "session") {
|
|
@@ -117,18 +117,18 @@ export async function runSandboxSupervisor(argv) {
|
|
|
117
117
|
let releaseLock = null;
|
|
118
118
|
try {
|
|
119
119
|
const decoded = JSON.parse(bootstrap.toString("utf8"));
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
if (typeof
|
|
123
|
-
!/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(
|
|
124
|
-
!Number.isInteger(
|
|
125
|
-
|
|
126
|
-
throw new Error("sandbox supervisor bootstrap
|
|
120
|
+
const sandboxId = decoded.sandboxId;
|
|
121
|
+
const sandboxGeneration = Number(decoded.sandboxGeneration ?? 0);
|
|
122
|
+
if (typeof sandboxId !== "string" ||
|
|
123
|
+
!/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(sandboxId) ||
|
|
124
|
+
!Number.isInteger(sandboxGeneration) ||
|
|
125
|
+
sandboxGeneration < 1) {
|
|
126
|
+
throw new Error("sandbox supervisor bootstrap sandbox scope is invalid");
|
|
127
127
|
}
|
|
128
|
-
releaseLock =
|
|
128
|
+
releaseLock = acquireSandboxSupervisorLock(sandboxId);
|
|
129
129
|
if (releaseLock === null)
|
|
130
130
|
return 0;
|
|
131
|
-
prepareDirectories(controlUid, controlGid
|
|
131
|
+
prepareDirectories(controlUid, controlGid);
|
|
132
132
|
child = spawn(NODE_BINARY, [
|
|
133
133
|
DAEMON_ENTRY,
|
|
134
134
|
"agent-service",
|
package/dist/types.d.ts
CHANGED
|
@@ -114,6 +114,11 @@ export interface AppliedRunRuntimeProfile {
|
|
|
114
114
|
export interface RuntimeContentBlock {
|
|
115
115
|
kind: "text_delta" | "text" | "thinking" | "tool_call" | "tool_result" | "status" | "error";
|
|
116
116
|
text?: string;
|
|
117
|
+
/** Safe provider-normalized tool identifier. Raw arguments/results stay in the transcript only. */
|
|
118
|
+
name?: string;
|
|
119
|
+
/** Public lifecycle metadata; never carries provider reasoning or tool output. */
|
|
120
|
+
phase?: "in_progress" | "completed";
|
|
121
|
+
status?: "completed" | "error";
|
|
117
122
|
raw?: unknown;
|
|
118
123
|
}
|
|
119
124
|
/** 已由 provider adapter 严格归一化、无 provider raw envelope 的进度遥测。 */
|
package/dist/workspace.d.ts
CHANGED
|
@@ -18,14 +18,37 @@ export declare function runWorkspaceDir(agentRunId: string): string;
|
|
|
18
18
|
export declare function transcriptPath(agentRunId: string): string;
|
|
19
19
|
export declare function runtimeProfileDir(agentRunId: string): string;
|
|
20
20
|
export declare function runtimeProfileRunRootDir(agentRunId: string): string;
|
|
21
|
-
export declare function runtimeSessionRootDir(runtimeSessionId: string,
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
export declare function runtimeSessionRootDir(runtimeSessionId: string, sandboxGeneration: number): string;
|
|
22
|
+
/**
|
|
23
|
+
* per-session workspace(ADR-015 §7):managed sandbox 下为
|
|
24
|
+
* `<BOTLEARN_AGENT_SERVICE_WORKSPACE_ROOT>/<runtime_session_id>/generation-<sandbox_generation>`,
|
|
25
|
+
* 由 daemon 在 session.open 时创建。
|
|
26
|
+
*/
|
|
27
|
+
export declare function runtimeSessionWorkspaceDir(runtimeSessionId: string, sandboxGeneration: number): string;
|
|
28
|
+
export declare function runtimeSessionTranscriptPath(runtimeSessionId: string, sandboxGeneration: number, agentRunId: string): string;
|
|
24
29
|
export declare function ensureRunWorkspace(agentRunId: string): {
|
|
25
30
|
rootDir: string;
|
|
26
31
|
workspaceDir: string;
|
|
27
32
|
};
|
|
28
|
-
|
|
33
|
+
/**
|
|
34
|
+
* session.open 时创建 per-session 目录。managed workspace 默认保持 0700,只有当前
|
|
35
|
+
* activation 会通过 exposeRuntimeSessionWorkspace() 临时开放给 runtime 组。
|
|
36
|
+
*/
|
|
37
|
+
export declare function ensureRuntimeSessionDirectories(runtimeSessionId: string, sandboxGeneration: number): {
|
|
38
|
+
rootDir: string;
|
|
39
|
+
workspaceDir: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* 暴露当前 activation 的 managed workspace。sandbox 根目录由 supervisor 设为
|
|
43
|
+
* 0710,因此 runtime 只能穿过根目录,不能列举其他 session id;未激活 session
|
|
44
|
+
* 的父目录与 workspace 始终为 0700。
|
|
45
|
+
*/
|
|
46
|
+
export declare function exposeRuntimeSessionWorkspace(runtimeSessionId: string, sandboxGeneration: number): void;
|
|
47
|
+
/** Revoke a managed workspace without deleting the session's durable local materialization. */
|
|
48
|
+
export declare function revokeRuntimeSessionWorkspace(runtimeSessionId: string, sandboxGeneration: number): void;
|
|
49
|
+
/** session.close 时删除 workspace 与本地 session 物化状态(transcripts 等)。幂等。 */
|
|
50
|
+
export declare function removeRuntimeSessionWorkspace(runtimeSessionId: string, sandboxGeneration: number): void;
|
|
51
|
+
export declare function ensureRuntimeSessionWorkspace(runtimeSessionId: string, sandboxGeneration: number, agentRunId: string): {
|
|
29
52
|
rootDir: string;
|
|
30
53
|
workspaceDir: string;
|
|
31
54
|
transcriptFile: string;
|