@botlearn-course/daemon 0.0.13-beta.1 → 0.0.14

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
@@ -114,8 +114,11 @@ runtime 凭据。
114
114
  - 本包 production 依赖为零,发布 tarball 只包含 `dist/`、`README.md`、`package.json`、
115
115
  `LICENSE`(CI 发布前强制校验)。
116
116
  - 托管 session 的 control token/reconnect state 位于 runtime UID 不可访问的 `0700` control home;
117
- runtime workspace 独立可写,Prompt Pack/Skill 则通过 control-owned、group-readable 的只读视图
118
- 提供。长期模型 provider key 留在 Agent Service,runtime 只拿 generation-scoped proxy grant。
117
+ runtime workspace 独立可写。activation-scoped Skill Provider binding 只留在 daemon control
118
+ process 内存;DeepSeek 只通过 clean-env `course_skills` Unix Socket relay 按需读取当前授权
119
+ `SKILL.md`/reference,endpoint/token 不进入 runtime env、MCP config、state、workspace 或日志。
120
+ Prompt Pack 仍通过受信 system context 应用。长期模型 provider key 留在 Agent Service,runtime
121
+ 只拿 generation-scoped proxy grant。
119
122
 
120
123
  ## 发布
121
124
 
@@ -1,4 +1,5 @@
1
1
  import { type Logger } from "./log.js";
2
+ import { type RuntimeSkillProviderFactory } from "./runtime-skills.js";
2
3
  import { type PersistentSessionExecution, type PreparedPersistentTurn, type RunReportingClient } from "./run-dispatcher.js";
3
4
  import type { CourseRuntime, CourseRuntimeProfile, RunEvent, RunFileCandidate, RunFileRecord, RunStartPayload } from "./types.js";
4
5
  export interface AgentServiceSandboxOptions {
@@ -7,9 +8,12 @@ export interface AgentServiceSandboxOptions {
7
8
  sandboxToken: string;
8
9
  runtimes: Map<string, CourseRuntime>;
9
10
  daemonVersion: string;
11
+ deepseekTuiVersion?: string;
10
12
  log?: Logger;
11
13
  random?: () => number;
12
14
  sleep?: (ms: number) => Promise<void>;
15
+ /** Test/provider injection; production uses the bounded HTTP Runtime Skill Provider. */
16
+ prepareSkillProvider?: RuntimeSkillProviderFactory;
13
17
  }
14
18
  /**
15
19
  * Long-running daemon client for one user-scoped managed sandbox (ADR-015).
@@ -23,6 +27,7 @@ export declare class AgentServiceSandboxClient implements RunReportingClient, Pe
23
27
  private readonly log;
24
28
  private readonly random;
25
29
  private readonly sleep;
30
+ private readonly prepareSkillProvider;
26
31
  private readonly state;
27
32
  private readonly dispatcher;
28
33
  /** agent_run_id → TURN scope(session/attempt/activation),事件与文件帧路由用。 */
@@ -3,9 +3,10 @@ import path from "node:path";
3
3
  import { ensureDaemonHome } from "./auth-store.js";
4
4
  import { AGENT_SERVICE_WS_SCHEMA, AGENT_SERVICE_WS_SUBPROTOCOL, createSandboxFrame, parseSandboxFrame, UnsupportedSandboxProtocolError, } from "./agent-service-ws-protocol.js";
5
5
  import { log as defaultLog } from "./log.js";
6
- import { availableRunCapabilities } from "./runtime-capabilities.js";
6
+ import { availableRunCapabilities, runtimeSupportsCourseSkills, } from "./runtime-capabilities.js";
7
7
  import { activationRuntimeEnv, runtimeChildEnv } from "./runtime-env.js";
8
8
  import { redactSecretString } from "./redaction.js";
9
+ import { parseRuntimeSkillProviderGrantSet, prepareRuntimeSkillProvider, RuntimeSkillProviderError, } from "./runtime-skills.js";
9
10
  import { RunDispatcher, } from "./run-dispatcher.js";
10
11
  import { ensureRuntimeSessionDirectories, ensureRuntimeSessionWorkspace, exposeRuntimeSessionWorkspace, removeRuntimeSessionWorkspace, revokeRuntimeSessionWorkspace, } from "./workspace.js";
11
12
  import { WebSocketClient, } from "./websocket-client.js";
@@ -44,6 +45,9 @@ function inputAttachmentGrant(value) {
44
45
  }
45
46
  return { baseUrl: parsed.toString().replace(/\/$/, ""), token };
46
47
  }
48
+ function runtimeSkillGrantKey(grants) {
49
+ return JSON.stringify(grants);
50
+ }
47
51
  class SandboxClosedError extends Error {
48
52
  code;
49
53
  reason;
@@ -244,6 +248,7 @@ export class AgentServiceSandboxClient {
244
248
  log;
245
249
  random;
246
250
  sleep;
251
+ prepareSkillProvider;
247
252
  state;
248
253
  dispatcher;
249
254
  /** agent_run_id → TURN scope(session/attempt/activation),事件与文件帧路由用。 */
@@ -277,6 +282,8 @@ export class AgentServiceSandboxClient {
277
282
  this.log = options.log ?? defaultLog;
278
283
  this.random = options.random ?? Math.random;
279
284
  this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
285
+ this.prepareSkillProvider =
286
+ options.prepareSkillProvider ?? ((raw) => prepareRuntimeSkillProvider(raw));
280
287
  this.state = loadState(options.sandboxId, options.sandboxToken);
281
288
  this.dispatcher = new RunDispatcher(this, options.runtimes, {
282
289
  persistentSession: this,
@@ -313,10 +320,12 @@ export class AgentServiceSandboxClient {
313
320
  return {
314
321
  workspaceDir: prepared.workspaceDir,
315
322
  transcriptFile: prepared.transcriptFile,
323
+ runtimeStateDir: prepared.rootDir,
316
324
  nativeSessionId: session.nativeSessionId,
317
325
  contextRevision,
318
326
  runtimeEnv,
319
327
  inputAttachmentGrant: activation.inputAttachmentGrant,
328
+ ...(activation.skillProvider ? { skillProvider: activation.skillProvider } : {}),
320
329
  };
321
330
  }
322
331
  persistNativeSession(nativeSessionId) {
@@ -678,6 +687,12 @@ export class AgentServiceSandboxClient {
678
687
  await this.sendControlFrame("sandbox.ready", {
679
688
  protocol_versions: [AGENT_SERVICE_WS_SCHEMA],
680
689
  daemon_version: this.options.daemonVersion,
690
+ runtime_versions: {
691
+ course_daemon: this.options.daemonVersion,
692
+ ...(this.options.deepseekTuiVersion
693
+ ? { deepseek_tui: this.options.deepseekTuiVersion }
694
+ : {}),
695
+ },
681
696
  resumed_sessions: Object.keys(this.state.sessions),
682
697
  spool_frames: this.spoolFrameCount(),
683
698
  });
@@ -842,33 +857,59 @@ export class AgentServiceSandboxClient {
842
857
  await this.sendCommandAck(frame, "rejected", error instanceof Error ? error.message : "invalid_instructions");
843
858
  return;
844
859
  }
860
+ const rawSkillProvider = frame.payload.skill_provider;
861
+ const hasSkillProvider = rawSkillProvider !== undefined && rawSkillProvider !== null;
862
+ if (hasSkillProvider && !runtimeSupportsCourseSkills(runtimeId)) {
863
+ await this.sendCommandAck(frame, "rejected", "skill_provider_runtime_unsupported");
864
+ return;
865
+ }
866
+ let skillGrantKey;
867
+ if (hasSkillProvider) {
868
+ try {
869
+ skillGrantKey = runtimeSkillGrantKey(parseRuntimeSkillProviderGrantSet(rawSkillProvider));
870
+ }
871
+ catch (error) {
872
+ const code = error instanceof RuntimeSkillProviderError
873
+ ? error.code
874
+ : "skill_provider_binding_invalid";
875
+ await this.sendCommandAck(frame, "rejected", code);
876
+ return;
877
+ }
878
+ }
845
879
  const capabilities = Array.isArray(frame.payload.capabilities)
846
880
  ? frame.payload.capabilities.filter((item) => typeof item === "string" && item.length > 0)
847
881
  : [];
848
- if (capabilities.length > 0) {
849
- const workspace = ensureRuntimeSessionDirectories(sessionId, this.sandboxGeneration);
850
- const available = new Set(availableRunCapabilities({
851
- agent_run_id: "activation-probe",
852
- course_run_id: session.courseRunId ?? "",
853
- lesson_id: null,
854
- task_id: null,
855
- agent_instance_id: null,
856
- runtime: { id: runtimeId },
857
- input: {},
858
- context: {},
859
- limits: {},
860
- }, workspace.workspaceDir));
861
- const missing = capabilities.filter((item) => !available.has(item)).sort();
862
- if (missing.length > 0) {
863
- await this.sendCommandAck(frame, "rejected", `missing_capabilities:${missing.join(",")}`);
864
- return;
882
+ let availableCapabilities;
883
+ const missingCapabilities = (required) => {
884
+ if (required.length === 0)
885
+ return [];
886
+ if (!availableCapabilities) {
887
+ const workspace = ensureRuntimeSessionDirectories(sessionId, this.sandboxGeneration);
888
+ availableCapabilities = new Set(availableRunCapabilities({
889
+ agent_run_id: "activation-probe",
890
+ course_run_id: session.courseRunId ?? "",
891
+ lesson_id: null,
892
+ task_id: null,
893
+ agent_instance_id: null,
894
+ runtime: { id: runtimeId },
895
+ input: {},
896
+ context: {},
897
+ limits: {},
898
+ }, workspace.workspaceDir));
865
899
  }
900
+ return Array.from(new Set(required.filter((item) => !availableCapabilities.has(item)))).sort();
901
+ };
902
+ const missingDeclaredCapabilities = missingCapabilities(capabilities);
903
+ if (missingDeclaredCapabilities.length > 0) {
904
+ await this.sendCommandAck(frame, "rejected", `missing_capabilities:${missingDeclaredCapabilities.join(",")}`);
905
+ return;
866
906
  }
867
907
  const existingActivation = this.activationContexts.get(sessionId);
868
908
  if (existingActivation?.activationId === activationId) {
869
909
  if (this.activeSessionId !== sessionId ||
870
910
  session.runtimeId !== runtimeId ||
871
- session.contextRevision !== contextRevision) {
911
+ session.contextRevision !== contextRevision ||
912
+ existingActivation.skillGrantKey !== skillGrantKey) {
872
913
  await this.sendCommandAck(frame, "rejected", "activation_redefinition");
873
914
  return;
874
915
  }
@@ -884,6 +925,25 @@ export class AgentServiceSandboxClient {
884
925
  await this.sendCommandAck(frame, "ok");
885
926
  return;
886
927
  }
928
+ let skillProvider;
929
+ if (hasSkillProvider) {
930
+ try {
931
+ skillProvider = await this.prepareSkillProvider(rawSkillProvider);
932
+ }
933
+ catch (error) {
934
+ const code = error instanceof RuntimeSkillProviderError
935
+ ? error.code
936
+ : "skill_provider_prepare_failed";
937
+ await this.sendCommandAck(frame, "rejected", code);
938
+ return;
939
+ }
940
+ const requiredBySkills = skillProvider.catalog.flatMap((entry) => entry.requiredCapabilities);
941
+ const missingSkillCapabilities = missingCapabilities(requiredBySkills);
942
+ if (missingSkillCapabilities.length > 0) {
943
+ await this.sendCommandAck(frame, "rejected", `missing_capabilities:${missingSkillCapabilities.join(",")}`);
944
+ return;
945
+ }
946
+ }
887
947
  // 同一时刻只允许一个 active session/activation。先撤销上一 workspace 的
888
948
  // runtime 访问并等待进程退出,再暴露新 workspace,避免同 UID 跨 Session 读取。
889
949
  const previousSessionId = this.activeSessionId;
@@ -913,6 +973,8 @@ export class AgentServiceSandboxClient {
913
973
  runtimeEnv,
914
974
  instructions,
915
975
  inputAttachmentGrant: attachmentGrant,
976
+ ...(skillProvider ? { skillProvider } : {}),
977
+ ...(skillGrantKey ? { skillGrantKey } : {}),
916
978
  });
917
979
  this.activeSessionId = sessionId;
918
980
  this.persist();
package/dist/cli.d.ts CHANGED
@@ -20,4 +20,5 @@ export interface PollLoopDeps {
20
20
  log?: Logger;
21
21
  }
22
22
  export declare function runPollLoop(deps: PollLoopDeps): Promise<PollLoopExit>;
23
+ export declare function assertManagedCourseDaemonVersion(actualVersion: string, selectedVersion: string | undefined): void;
23
24
  export declare function runCli(argv: string[]): Promise<number>;
package/dist/cli.js CHANGED
@@ -337,6 +337,11 @@ async function readSandboxBootstrapFromStdin() {
337
337
  chunk.fill(0);
338
338
  }
339
339
  }
340
+ export function assertManagedCourseDaemonVersion(actualVersion, selectedVersion) {
341
+ if (selectedVersion && selectedVersion !== actualVersion) {
342
+ throw new Error("Managed Course Daemon version does not match the selected release");
343
+ }
344
+ }
340
345
  async function cmdAgentServiceSession(args) {
341
346
  const bootstrap = args.flags["bootstrap-stdin"] === true
342
347
  ? await readSandboxBootstrapFromStdin()
@@ -350,6 +355,7 @@ async function cmdAgentServiceSession(args) {
350
355
  console.error("Agent Service sandbox environment is incomplete");
351
356
  return 1;
352
357
  }
358
+ assertManagedCourseDaemonVersion(pkg.version, process.env.BOTLEARN_MANAGED_COURSE_DAEMON_VERSION?.trim() || undefined);
353
359
  augmentProcessPath();
354
360
  const runtimes = new Map();
355
361
  for (const mod of RUNTIME_MODULES) {
@@ -363,6 +369,7 @@ async function cmdAgentServiceSession(args) {
363
369
  sandboxToken,
364
370
  runtimes,
365
371
  daemonVersion: pkg.version,
372
+ deepseekTuiVersion: process.env.BOTLEARN_MANAGED_DEEPSEEK_TUI_VERSION?.trim() || undefined,
366
373
  });
367
374
  clearAgentServiceControlEnv();
368
375
  await client.run();
package/dist/index.d.ts CHANGED
@@ -18,6 +18,7 @@ export * from "./log.js";
18
18
  export * from "./redaction.js";
19
19
  export * from "./runtime-profile.js";
20
20
  export * from "./runtime-env.js";
21
+ export * from "./runtime-skills.js";
21
22
  export * from "./mcp/report-progress.js";
22
23
  export * from "./runtimes/index.js";
23
24
  export * from "./runtimes/engine.js";
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ export * from "./log.js";
18
18
  export * from "./redaction.js";
19
19
  export * from "./runtime-profile.js";
20
20
  export * from "./runtime-env.js";
21
+ export * from "./runtime-skills.js";
21
22
  export * from "./mcp/report-progress.js";
22
23
  export * from "./runtimes/index.js";
23
24
  export * from "./runtimes/engine.js";
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function runCourseSkillsRelay(argv?: string[]): void;
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from "node:fs";
3
+ import net from "node:net";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ export function runCourseSkillsRelay(argv = process.argv.slice(2)) {
7
+ const socketPath = relaySocketPath(argv);
8
+ const socket = net.createConnection({ path: socketPath });
9
+ const fail = () => {
10
+ process.stderr.write("course_skills relay unavailable\n");
11
+ process.exitCode = 1;
12
+ };
13
+ socket.once("error", fail);
14
+ socket.once("connect", () => {
15
+ process.stdin.pipe(socket);
16
+ socket.pipe(process.stdout);
17
+ });
18
+ socket.once("close", () => {
19
+ if (!process.stdin.readableEnded)
20
+ process.stdin.destroy();
21
+ });
22
+ }
23
+ function relaySocketPath(argv) {
24
+ if (argv.length !== 2 || argv[0] !== "--socket") {
25
+ throw new Error("course_skills relay requires --socket");
26
+ }
27
+ const value = argv[1] ?? "";
28
+ if (!path.isAbsolute(value)
29
+ || value.includes("\0")
30
+ || Buffer.byteLength(value, "utf8") > 240) {
31
+ throw new Error("course_skills relay socket is invalid");
32
+ }
33
+ return value;
34
+ }
35
+ function isMainModule() {
36
+ const entry = process.argv[1];
37
+ if (!entry)
38
+ return false;
39
+ try {
40
+ return realpathSync(entry) === fileURLToPath(import.meta.url);
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ if (isMainModule()) {
47
+ try {
48
+ runCourseSkillsRelay();
49
+ }
50
+ catch {
51
+ process.stderr.write("course_skills relay configuration invalid\n");
52
+ process.exitCode = 1;
53
+ }
54
+ }
@@ -0,0 +1,49 @@
1
+ import { type PreparedRuntimeSkillProvider, type RuntimeSkillCatalogEntry, type RuntimeSkillEvent } from "../runtime-skills.js";
2
+ type JsonRpcId = string | number | null;
3
+ interface JsonRpcRequest {
4
+ jsonrpc?: unknown;
5
+ id?: unknown;
6
+ method?: unknown;
7
+ params?: unknown;
8
+ }
9
+ interface JsonRpcResponse {
10
+ jsonrpc: "2.0";
11
+ id: JsonRpcId;
12
+ result?: unknown;
13
+ error?: {
14
+ code: number;
15
+ message: string;
16
+ };
17
+ }
18
+ export interface CourseSkillsMcpServer {
19
+ socketPath: string;
20
+ catalog: RuntimeSkillCatalogEntry[];
21
+ markApplied(): Promise<void>;
22
+ close(): Promise<void>;
23
+ }
24
+ export interface CourseSkillsMcpServerOptions {
25
+ prepared: PreparedRuntimeSkillProvider;
26
+ onEvent(event: RuntimeSkillEvent): Promise<void>;
27
+ /**
28
+ * Managed Agent Service root. `undefined` auto-detects it; `null` is only for
29
+ * private same-UID tests/BYOA experiments.
30
+ */
31
+ managedRoot?: string | null;
32
+ }
33
+ export declare class CourseSkillsMcpRuntime {
34
+ private readonly options;
35
+ private readonly grantsByRef;
36
+ private readonly loadedSkills;
37
+ private readonly loadedReferences;
38
+ private referenceBytes;
39
+ constructor(options: CourseSkillsMcpServerOptions);
40
+ emitAppliedEvents(): Promise<void>;
41
+ handle(request: JsonRpcRequest): Promise<JsonRpcResponse | null>;
42
+ private loadSkill;
43
+ private loadReference;
44
+ private requireGrant;
45
+ private emit;
46
+ private emitLoadFailed;
47
+ }
48
+ export declare function startCourseSkillsMcpServer(options: CourseSkillsMcpServerOptions): Promise<CourseSkillsMcpServer>;
49
+ export {};