@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.
@@ -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
- const text = renderConversationInput(payload);
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({ kind, raw: block.raw });
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.
@@ -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 finalText = `已根据任务完成草稿。\n\n任务:${brief.slice(0, BRIEF_PREVIEW_CHARS)}`;
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
- { kind: "status", text: "fake runtime started", raw: { event: "started" } },
21
- { kind: "thinking", text: "正在起草……", raw: { event: "thinking" } },
22
- { kind: "text", text: finalText, raw: { event: "text" } },
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(path.join(run.workspaceDir, "draft.md"), finalText, "utf8");
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
  }
@@ -238,7 +238,20 @@ function normalizeBlock(obj, seq) {
238
238
  else if (type === "init" || type === "result") {
239
239
  kind = "system";
240
240
  }
241
- return { raw: obj, kind, seq };
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
- ctx.emitBlock({ raw: params, kind: blockKind, seq: ctx.seq });
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_` 开头的选项,没有再退回第一个
@@ -323,7 +323,15 @@ function normalizeBlock(obj, seq) {
323
323
  else if (obj.category || obj.severity) {
324
324
  kind = "system";
325
325
  }
326
- return { raw: obj, kind, seq };
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({ raw: sanitizeAssistantChunk(note, text), kind: "assistant_text", seq });
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
- emitBlock({ raw: note, kind, seq });
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
- /** Create an ephemeral config that preserves a user's existing MCP servers and settings. */
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
- /** Create an ephemeral config that preserves a user's existing MCP servers and settings. */
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 = options.baseConfigPath === undefined
131
- ? resolveExistingDeepseekMcpConfig()
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 acquireSessionSupervisorLock(runtimeSessionId: string, lockRoot?: string): (() => void) | null;
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 acquireSessionSupervisorLock(runtimeSessionId, lockRoot = SUPERVISOR_LOCK_ROOT) {
53
- const lockDir = path.join(lockRoot, runtimeSessionId);
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, runtimeUid, runtimeSessionId, sessionGeneration) {
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: 0o750 });
92
+ mkdirSync(RUNTIME_PROFILE_ROOT, { recursive: true, mode: 0o710 });
93
93
  chownSync(RUNTIME_PROFILE_ROOT, controlUid, controlGid);
94
- chmodSync(RUNTIME_PROFILE_ROOT, 0o750);
95
- mkdirSync(WORKSPACE, { recursive: true, mode: 0o770 });
96
- chownSync(WORKSPACE, runtimeUid, controlGid);
97
- chmodSync(WORKSPACE, 0o770);
98
- const sessionWorkspace = path.join(WORKSPACE, runtimeSessionId, `generation-${sessionGeneration}`);
99
- mkdirSync(sessionWorkspace, { recursive: true, mode: 0o770 });
100
- chownSync(path.join(WORKSPACE, runtimeSessionId), runtimeUid, controlGid);
101
- chownSync(sessionWorkspace, runtimeUid, controlGid);
102
- chmodSync(path.join(WORKSPACE, runtimeSessionId), 0o770);
103
- chmodSync(sessionWorkspace, 0o770);
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 runtimeSessionId = decoded.runtimeSessionId;
121
- const sessionGeneration = Number(decoded.sessionGeneration ?? 0);
122
- if (typeof runtimeSessionId !== "string" ||
123
- !/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(runtimeSessionId) ||
124
- !Number.isInteger(sessionGeneration) ||
125
- sessionGeneration < 1) {
126
- throw new Error("sandbox supervisor bootstrap session scope is invalid");
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 = acquireSessionSupervisorLock(runtimeSessionId);
128
+ releaseLock = acquireSandboxSupervisorLock(sandboxId);
129
129
  if (releaseLock === null)
130
130
  return 0;
131
- prepareDirectories(controlUid, controlGid, runtimeUid, runtimeSessionId, sessionGeneration);
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 的进度遥测。 */
@@ -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, sessionGeneration: number): string;
22
- export declare function runtimeSessionWorkspaceDir(runtimeSessionId: string, sessionGeneration: number): string;
23
- export declare function runtimeSessionTranscriptPath(runtimeSessionId: string, sessionGeneration: number, agentRunId: string): string;
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
- export declare function ensureRuntimeSessionWorkspace(runtimeSessionId: string, sessionGeneration: number, agentRunId: string): {
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;