@botlearn-course/daemon 0.0.7 → 0.0.9

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 CHANGED
@@ -14,16 +14,16 @@ Claude Code、Gemini 等)完成任务,并把过程事件与产出文件候
14
14
 
15
15
  ```bash
16
16
  # 1. 环境自检:探测各 runtime 的安装 / 版本 / 登录状态
17
- npx @botlearn-course/daemon@latest course doctor
17
+ npx --yes --package @botlearn-course/daemon@latest botlearn-course-daemon course doctor
18
18
 
19
19
  # 2. 登录:用前端「我的 Daemon」页面生成的 install code 绑定本机
20
- npx @botlearn-course/daemon@latest course login --api-url <course-api-url> --code <blic_xxx> [--label <名称>]
20
+ npx --yes --package @botlearn-course/daemon@latest botlearn-course-daemon course login --api-url <course-api-url> --code <blic_xxx> [--label <名称>]
21
21
 
22
22
  # 3. 启动:前台轮询并执行分配给本机的 run(Ctrl+C 优雅退出)
23
- npx @botlearn-course/daemon@latest course start [--once] [--poll-interval-ms <ms>]
23
+ npx --yes --package @botlearn-course/daemon@latest botlearn-course-daemon course start [--once] [--poll-interval-ms <ms>]
24
24
 
25
25
  # 4. 登出:删除本机凭据文件
26
- npx @botlearn-course/daemon@latest course logout
26
+ npx --yes --package @botlearn-course/daemon@latest botlearn-course-daemon course logout
27
27
  ```
28
28
 
29
29
  命令说明:
@@ -38,9 +38,10 @@ npx @botlearn-course/daemon@latest course logout
38
38
 
39
39
  包内还包含供 BotLearn 托管 E2B Template 使用的内部命令
40
40
  `botlearn-sandbox-supervisor agent-service session`。它不是 BYOA 用户入口:生产环境只允许由
41
- Agent Service 以固定 argv 启动,通过 stdin 接收一次性 bootstrap,并把 daemon/runtime 分别降权
42
- `botlearn-control`/`user` UID。E2B Template 通过 root-owned 固定 launcher 将 DeepSeek
43
- 单向降权为 `user` 并设置 `no_new_privs`;runtime 用户本身没有 sudo 权限。托管启动链只执行
41
+ Agent Service 以固定 argv 启动,通过 stdin 接收一次性 bootstrap。E2B daemon/runtime 分别
42
+ 降权到 `botlearn-control`/`user` UID;继承 `NoNewPrivs=1`、无法使用 sudo 的平台保留 root control
43
+ daemon,由 root-owned 固定 launcher 清空附加组后直接降权为 `user`。两条路径都为 DeepSeek
44
+ 设置 `no_new_privs`,runtime 用户本身没有 sudo 权限。托管启动链只执行
44
45
  `/opt` 下的固定 Node、supervisor、daemon、launcher 与 DeepSeek 文件,不信任 E2B 会开放给 runtime
45
46
  写入的 `/usr/local/bin`。
46
47
  `agent-service session --bootstrap-stdin` 同样属于受 supervisor 保护的内部协议,不应直接暴露给
@@ -17,8 +17,8 @@ export class AgentServiceRunClient {
17
17
  }
18
18
  url(path) {
19
19
  const base = this.baseUrl.replace(/\/+$/, "");
20
- if (base.endsWith("/api/v1") && path.startsWith("/api/v1/")) {
21
- return `${base}${path.slice("/api/v1".length)}`;
20
+ if (base.endsWith("/course/v1") && path.startsWith("/course/v1/")) {
21
+ return `${base}${path.slice("/course/v1".length)}`;
22
22
  }
23
23
  return `${base}${path}`;
24
24
  }
@@ -40,7 +40,7 @@ export class AgentServiceRunClient {
40
40
  return (raw ? JSON.parse(raw) : null);
41
41
  }
42
42
  async getRun() {
43
- const payload = await this.request("GET", `/api/v1/agent-service/runs/${this.agentRunId}`);
43
+ const payload = await this.request("GET", `/course/v1/agent-service/runs/${this.agentRunId}`);
44
44
  this.traceId = payload.trace_id;
45
45
  return payload;
46
46
  }
@@ -48,14 +48,14 @@ export class AgentServiceRunClient {
48
48
  this.assertRunId(agentRunId);
49
49
  this.traceId = event.trace_id ?? this.traceId;
50
50
  const sanitized = redactSecretsDeep(event, 8, [this.runToken]);
51
- await this.request("POST", `/api/v1/agent-service/runs/${agentRunId}/events`, sanitized);
51
+ await this.request("POST", `/course/v1/agent-service/runs/${agentRunId}/events`, sanitized);
52
52
  }
53
53
  async postFile(agentRunId, file) {
54
54
  this.assertRunId(agentRunId);
55
55
  const sanitized = redactSecretsDeep(file, 8, [this.runToken]);
56
56
  for (let attempt = 0; attempt < 3; attempt += 1) {
57
57
  try {
58
- return await this.request("POST", `/api/v1/agent-service/runs/${agentRunId}/files`, sanitized);
58
+ return await this.request("POST", `/course/v1/agent-service/runs/${agentRunId}/files`, sanitized);
59
59
  }
60
60
  catch (err) {
61
61
  const retryable = !(err instanceof CourseClientError) || err.status === 429 || err.status >= 500;
@@ -70,7 +70,7 @@ export class AgentServiceRunClient {
70
70
  this.assertRunId(agentRunId);
71
71
  const data = await readFile(absPath);
72
72
  assertNoInjectedCredentials(data, [this.runToken]);
73
- const path = `/api/v1/agent-service/runs/${agentRunId}/files/${fileId}/content`;
73
+ const path = `/course/v1/agent-service/runs/${agentRunId}/files/${fileId}/content`;
74
74
  for (let attempt = 0; attempt < 3; attempt += 1) {
75
75
  let response;
76
76
  try {
@@ -102,10 +102,10 @@ export class AgentServiceRunClient {
102
102
  }
103
103
  async getRunRuntimeProfile(agentRunId) {
104
104
  this.assertRunId(agentRunId);
105
- return this.request("GET", `/api/v1/agent-service/runs/${agentRunId}/runtime-profile`);
105
+ return this.request("GET", `/course/v1/agent-service/runs/${agentRunId}/runtime-profile`);
106
106
  }
107
107
  async heartbeat() {
108
- await this.request("POST", `/api/v1/agent-service/runs/${this.agentRunId}/heartbeat`, { worker_id: this.workerId });
108
+ await this.request("POST", `/course/v1/agent-service/runs/${this.agentRunId}/heartbeat`, { worker_id: this.workerId });
109
109
  }
110
110
  assertRunId(value) {
111
111
  if (value !== this.agentRunId)
@@ -0,0 +1,108 @@
1
+ import { type Logger } from "./log.js";
2
+ import { type PersistentSessionExecution, type PreparedPersistentTurn, type RunReportingClient } from "./run-dispatcher.js";
3
+ import type { CourseRuntime, CourseRuntimeProfile, RunEvent, RunFileCandidate, RunFileRecord, RunStartPayload } from "./types.js";
4
+ export interface AgentServiceSandboxOptions {
5
+ wsUrl: string;
6
+ sandboxId: string;
7
+ sandboxToken: string;
8
+ runtimes: Map<string, CourseRuntime>;
9
+ daemonVersion: string;
10
+ log?: Logger;
11
+ random?: () => number;
12
+ sleep?: (ms: number) => Promise<void>;
13
+ }
14
+ /**
15
+ * Long-running daemon client for one user-scoped managed sandbox (ADR-015).
16
+ *
17
+ * 一个 sandbox 内可承载多个 runtime session(CourseRun Session)。server 通过显式命令帧
18
+ * (session.open/activate/close、turn.start/cancel、sandbox.drain/shutdown/sync)驱动;
19
+ * daemon 同时只允许一个 active session,且整个 sandbox 同时只有一个 active turn。
20
+ */
21
+ export declare class AgentServiceSandboxClient implements RunReportingClient, PersistentSessionExecution {
22
+ private readonly options;
23
+ private readonly log;
24
+ private readonly random;
25
+ private readonly sleep;
26
+ private readonly state;
27
+ private readonly dispatcher;
28
+ /** agent_run_id → TURN scope(session/attempt/activation),事件与文件帧路由用。 */
29
+ private readonly turnScopes;
30
+ /** 每 run 内嵌 profile(turn payload 自带时优先)。 */
31
+ private readonly runProfiles;
32
+ /** activate 时装配的 per-session profile,turn.start 复用。 */
33
+ private readonly sessionProfiles;
34
+ /** activation-scoped 模型短凭据;只保存在内存中,绝不进入 state.json。 */
35
+ private readonly activationContexts;
36
+ private readonly pendingAcks;
37
+ private readonly pendingFilePrepares;
38
+ private readonly pendingFileCommits;
39
+ private readonly fileGrants;
40
+ private readonly inflightCommands;
41
+ private socket;
42
+ private sandboxGeneration;
43
+ private connectionEpoch;
44
+ private outboundSeq;
45
+ private inboundSeq;
46
+ private activeSessionId;
47
+ /** 全局串行下当前正在执行 turn 的 session(persistNativeSession 路由用)。 */
48
+ private currentTurnSessionId;
49
+ private heartbeatMs;
50
+ private staleMs;
51
+ private stopped;
52
+ private permanentFailure;
53
+ private lifecycleChain;
54
+ constructor(options: AgentServiceSandboxOptions);
55
+ prepareTurn(payload: RunStartPayload): PreparedPersistentTurn;
56
+ persistNativeSession(nativeSessionId: string): void;
57
+ finishTurn(payload: RunStartPayload): void;
58
+ private completePendingActivationCleanup;
59
+ private recoverPendingActivationCleanups;
60
+ run(): Promise<void>;
61
+ stop(): void;
62
+ postEvent(agentRunId: string, event: RunEvent): Promise<void>;
63
+ postFile(agentRunId: string, file: RunFileCandidate): Promise<RunFileRecord>;
64
+ uploadFileContent(agentRunId: string, fileId: string, absPath: string, mimeType?: string): Promise<RunFileRecord>;
65
+ getRunRuntimeProfile(agentRunId: string): Promise<CourseRuntimeProfile>;
66
+ private connectOnce;
67
+ private handleHello;
68
+ private assertServerFrame;
69
+ private handleServerFrame;
70
+ /** sandbox.sync 只做对账:重放 spool、关闭待关 session、应用 drain/shutdown。 */
71
+ private applySync;
72
+ private handleSessionOpen;
73
+ private handleSessionActivate;
74
+ private handleTurnStart;
75
+ private handleTurnCancel;
76
+ /**
77
+ * 幂等关闭一个 runtime session:终止其 runtime 子进程、删除 workspace/native state、
78
+ * 清空本地状态分片,并回 session.closed。未知 session 直接回 closed。
79
+ */
80
+ private closeSession;
81
+ private reconcileInterruptedCommand;
82
+ private turnContextRevision;
83
+ private activationInstructions;
84
+ private validTurnPayload;
85
+ private scheduleLifecycle;
86
+ private currentLifecycleFence;
87
+ private isCurrentLifecycleFence;
88
+ private runIdsForSession;
89
+ private deactivateSession;
90
+ private sameTurnScope;
91
+ private matchesTurnScope;
92
+ private assertTurnScope;
93
+ private ackEventOrFile;
94
+ private acceptFileGrant;
95
+ private replaySpool;
96
+ private sendHeartbeat;
97
+ private sendCommandAck;
98
+ private sendControlFrame;
99
+ private sendSessionFrame;
100
+ private sendFrame;
101
+ private nextOutboundSeq;
102
+ private spoolFrameCount;
103
+ private spoolBytes;
104
+ private enforceSpoolLimit;
105
+ private rejectPending;
106
+ private rejectPendingFiles;
107
+ private persist;
108
+ }