@botlearn-course/daemon 0.0.13 → 0.0.15

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 {
@@ -11,6 +12,8 @@ export interface AgentServiceSandboxOptions {
11
12
  log?: Logger;
12
13
  random?: () => number;
13
14
  sleep?: (ms: number) => Promise<void>;
15
+ /** Test/provider injection; production uses the bounded HTTP Runtime Skill Provider. */
16
+ prepareSkillProvider?: RuntimeSkillProviderFactory;
14
17
  }
15
18
  /**
16
19
  * Long-running daemon client for one user-scoped managed sandbox (ADR-015).
@@ -24,6 +27,7 @@ export declare class AgentServiceSandboxClient implements RunReportingClient, Pe
24
27
  private readonly log;
25
28
  private readonly random;
26
29
  private readonly sleep;
30
+ private readonly prepareSkillProvider;
27
31
  private readonly state;
28
32
  private readonly dispatcher;
29
33
  /** agent_run_id → TURN scope(session/attempt/activation),事件与文件帧路由用。 */
@@ -52,6 +56,15 @@ export declare class AgentServiceSandboxClient implements RunReportingClient, Pe
52
56
  private stopped;
53
57
  private permanentFailure;
54
58
  private lifecycleChain;
59
+ private runtimeLogPolicy;
60
+ private runtimeLogHandshakeReady;
61
+ private runtimeLogWindowStartedAt;
62
+ private runtimeLogWindowEvents;
63
+ private runtimeLogWindowBytes;
64
+ private runtimeLogSuppressedEvents;
65
+ private runtimeLogSuppressedBytes;
66
+ private runtimeLogSendChain;
67
+ private readonly removeOperationalLogSink;
55
68
  constructor(options: AgentServiceSandboxOptions);
56
69
  prepareTurn(payload: RunStartPayload): PreparedPersistentTurn;
57
70
  persistNativeSession(nativeSessionId: string): void;
@@ -65,6 +78,8 @@ export declare class AgentServiceSandboxClient implements RunReportingClient, Pe
65
78
  getRunRuntimeProfile(agentRunId: string): Promise<CourseRuntimeProfile>;
66
79
  private connectOnce;
67
80
  private handleHello;
81
+ private forwardOperationalLog;
82
+ private queueRuntimeLog;
68
83
  private assertServerFrame;
69
84
  private handleServerFrame;
70
85
  /** sandbox.sync 只做对账:重放 spool、关闭待关 session、应用 drain/shutdown。 */
@@ -2,10 +2,11 @@ import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readF
2
2
  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
- import { log as defaultLog } from "./log.js";
6
- import { availableRunCapabilities } from "./runtime-capabilities.js";
5
+ import { log as defaultLog, setOperationalLogSink, } from "./log.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";
@@ -16,6 +17,70 @@ const MAX_EVENT_ACK_WINDOW = 64;
16
17
  const LEGACY_EVENT_ACK_WINDOW = 1;
17
18
  /** 出站 seq 基址:seq = connection_epoch * SEQ_EPOCH_BASE + n,跨重连单调(合同 §1.1)。 */
18
19
  const SEQ_EPOCH_BASE = 1_000_000_000;
20
+ const RUNTIME_LOG_WINDOW_MS = 60_000;
21
+ const DISABLED_RUNTIME_LOG_POLICY = {
22
+ enabled: false,
23
+ maxEventBytes: 4096,
24
+ maxEventsPerMinute: 1,
25
+ maxBytesPerMinute: 4096,
26
+ };
27
+ function boundedUtf8(value, maxBytes) {
28
+ const encoded = Buffer.from(value, "utf8");
29
+ if (encoded.length <= maxBytes)
30
+ return value;
31
+ const suffix = "…[truncated]";
32
+ const budget = Math.max(0, maxBytes - Buffer.byteLength(suffix, "utf8"));
33
+ return encoded.subarray(0, budget).toString("utf8").replace(/�$/u, "") + suffix;
34
+ }
35
+ function boundedLogFields(fields, maxBytes) {
36
+ if (!fields || maxBytes < 2)
37
+ return { bytes: 0 };
38
+ const bounded = {};
39
+ let truncated = false;
40
+ for (const [key, value] of Object.entries(fields)) {
41
+ bounded[key] = value;
42
+ let bytes;
43
+ try {
44
+ bytes = Buffer.byteLength(JSON.stringify(bounded), "utf8");
45
+ }
46
+ catch {
47
+ bytes = maxBytes + 1;
48
+ }
49
+ if (bytes > maxBytes) {
50
+ delete bounded[key];
51
+ truncated = true;
52
+ break;
53
+ }
54
+ }
55
+ if (truncated) {
56
+ const marked = { ...bounded, runtime_log_fields_truncated: true };
57
+ if (Buffer.byteLength(JSON.stringify(marked), "utf8") <= maxBytes) {
58
+ Object.assign(bounded, { runtime_log_fields_truncated: true });
59
+ }
60
+ }
61
+ if (!Object.keys(bounded).length)
62
+ return { bytes: 0 };
63
+ const bytes = Buffer.byteLength(JSON.stringify(bounded), "utf8");
64
+ return { fields: bounded, bytes };
65
+ }
66
+ function runtimeLogPolicy(value) {
67
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
68
+ return DISABLED_RUNTIME_LOG_POLICY;
69
+ }
70
+ const raw = value;
71
+ const integer = (key, fallback, min, max) => {
72
+ const candidate = Number(raw[key]);
73
+ return Number.isSafeInteger(candidate) && candidate >= min && candidate <= max
74
+ ? candidate
75
+ : fallback;
76
+ };
77
+ return {
78
+ enabled: raw.enabled === true,
79
+ maxEventBytes: integer("max_event_bytes", 4096, 256, 16_384),
80
+ maxEventsPerMinute: integer("max_events_per_minute", 60, 1, 600),
81
+ maxBytesPerMinute: integer("max_bytes_per_minute", 131_072, 4096, 1_048_576),
82
+ };
83
+ }
19
84
  function inputAttachmentGrant(value) {
20
85
  if (value === undefined || value === null)
21
86
  return undefined;
@@ -44,6 +109,9 @@ function inputAttachmentGrant(value) {
44
109
  }
45
110
  return { baseUrl: parsed.toString().replace(/\/$/, ""), token };
46
111
  }
112
+ function runtimeSkillGrantKey(grants) {
113
+ return JSON.stringify(grants);
114
+ }
47
115
  class SandboxClosedError extends Error {
48
116
  code;
49
117
  reason;
@@ -244,6 +312,7 @@ export class AgentServiceSandboxClient {
244
312
  log;
245
313
  random;
246
314
  sleep;
315
+ prepareSkillProvider;
247
316
  state;
248
317
  dispatcher;
249
318
  /** agent_run_id → TURN scope(session/attempt/activation),事件与文件帧路由用。 */
@@ -272,15 +341,29 @@ export class AgentServiceSandboxClient {
272
341
  stopped = false;
273
342
  permanentFailure = false;
274
343
  lifecycleChain = Promise.resolve();
344
+ runtimeLogPolicy = DISABLED_RUNTIME_LOG_POLICY;
345
+ runtimeLogHandshakeReady = false;
346
+ runtimeLogWindowStartedAt = 0;
347
+ runtimeLogWindowEvents = 0;
348
+ runtimeLogWindowBytes = 0;
349
+ runtimeLogSuppressedEvents = 0;
350
+ runtimeLogSuppressedBytes = 0;
351
+ runtimeLogSendChain = Promise.resolve();
352
+ removeOperationalLogSink;
275
353
  constructor(options) {
276
354
  this.options = options;
277
355
  this.log = options.log ?? defaultLog;
278
356
  this.random = options.random ?? Math.random;
279
357
  this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
358
+ this.prepareSkillProvider =
359
+ options.prepareSkillProvider ?? ((raw) => prepareRuntimeSkillProvider(raw));
280
360
  this.state = loadState(options.sandboxId, options.sandboxToken);
281
361
  this.dispatcher = new RunDispatcher(this, options.runtimes, {
282
362
  persistentSession: this,
283
363
  });
364
+ this.removeOperationalLogSink = setOperationalLogSink((entry) => {
365
+ this.forwardOperationalLog(entry);
366
+ });
284
367
  }
285
368
  prepareTurn(payload) {
286
369
  const scope = this.turnScopes.get(payload.agent_run_id);
@@ -318,6 +401,7 @@ export class AgentServiceSandboxClient {
318
401
  contextRevision,
319
402
  runtimeEnv,
320
403
  inputAttachmentGrant: activation.inputAttachmentGrant,
404
+ ...(activation.skillProvider ? { skillProvider: activation.skillProvider } : {}),
321
405
  };
322
406
  }
323
407
  persistNativeSession(nativeSessionId) {
@@ -440,6 +524,7 @@ export class AgentServiceSandboxClient {
440
524
  await this.sleep(delay);
441
525
  }
442
526
  }
527
+ this.removeOperationalLogSink();
443
528
  if (this.permanentFailure) {
444
529
  throw new Error("Agent Service sandbox stopped after a permanent protocol failure");
445
530
  }
@@ -456,6 +541,8 @@ export class AgentServiceSandboxClient {
456
541
  this.activeSessionId = null;
457
542
  this.currentTurnSessionId = null;
458
543
  this.persist();
544
+ this.runtimeLogHandshakeReady = false;
545
+ this.removeOperationalLogSink();
459
546
  this.socket?.close(1000, "daemon_stopping");
460
547
  }
461
548
  async postEvent(agentRunId, event) {
@@ -631,6 +718,7 @@ export class AgentServiceSandboxClient {
631
718
  this.rejectPendingFiles(new Error("Agent Service file transport disconnected"));
632
719
  if (this.socket === socket)
633
720
  this.socket = null;
721
+ this.runtimeLogHandshakeReady = false;
634
722
  if (socket.readyState === WebSocketClient.OPEN)
635
723
  socket.close(1000, "reconnecting");
636
724
  }
@@ -675,6 +763,7 @@ export class AgentServiceSandboxClient {
675
763
  ? advertisedAckWindow
676
764
  : LEGACY_EVENT_ACK_WINDOW;
677
765
  this.maxUnackedEvents = Math.min(MAX_EVENT_ACK_WINDOW, ackWindow);
766
+ this.runtimeLogPolicy = runtimeLogPolicy(frame.payload.runtime_log_policy);
678
767
  this.persist();
679
768
  await this.sendControlFrame("sandbox.ready", {
680
769
  protocol_versions: [AGENT_SERVICE_WS_SCHEMA],
@@ -688,6 +777,73 @@ export class AgentServiceSandboxClient {
688
777
  resumed_sessions: Object.keys(this.state.sessions),
689
778
  spool_frames: this.spoolFrameCount(),
690
779
  });
780
+ this.runtimeLogHandshakeReady = true;
781
+ }
782
+ forwardOperationalLog(entry) {
783
+ const policy = this.runtimeLogPolicy;
784
+ if (!policy.enabled || !this.runtimeLogHandshakeReady)
785
+ return;
786
+ const message = boundedUtf8(entry.message, policy.maxEventBytes);
787
+ const messageBytes = Buffer.byteLength(message, "utf8");
788
+ const boundedFields = boundedLogFields(entry.fields, Math.max(0, policy.maxEventBytes - messageBytes));
789
+ const contentBytes = messageBytes + boundedFields.bytes;
790
+ const now = Date.now();
791
+ if (now - this.runtimeLogWindowStartedAt >= RUNTIME_LOG_WINDOW_MS) {
792
+ if (this.runtimeLogSuppressedEvents > 0) {
793
+ this.queueRuntimeLog({
794
+ level: "warn",
795
+ message: "sandbox runtime logs suppressed at daemon source",
796
+ fields: {
797
+ suppressed_count: this.runtimeLogSuppressedEvents,
798
+ suppressed_bytes: this.runtimeLogSuppressedBytes,
799
+ },
800
+ timestamp: new Date(now).toISOString(),
801
+ });
802
+ }
803
+ this.runtimeLogWindowStartedAt = now;
804
+ this.runtimeLogWindowEvents = 0;
805
+ this.runtimeLogWindowBytes = 0;
806
+ this.runtimeLogSuppressedEvents = 0;
807
+ this.runtimeLogSuppressedBytes = 0;
808
+ }
809
+ if (this.runtimeLogWindowEvents >= policy.maxEventsPerMinute ||
810
+ this.runtimeLogWindowBytes + contentBytes > policy.maxBytesPerMinute) {
811
+ this.runtimeLogSuppressedEvents += 1;
812
+ this.runtimeLogSuppressedBytes += contentBytes;
813
+ return;
814
+ }
815
+ this.runtimeLogWindowEvents += 1;
816
+ this.runtimeLogWindowBytes += contentBytes;
817
+ this.queueRuntimeLog({
818
+ ...entry,
819
+ message,
820
+ fields: boundedFields.fields,
821
+ });
822
+ }
823
+ queueRuntimeLog(entry) {
824
+ this.runtimeLogSendChain = this.runtimeLogSendChain
825
+ .then(async () => {
826
+ if (!this.runtimeLogHandshakeReady || this.socket?.readyState !== WebSocketClient.OPEN) {
827
+ return;
828
+ }
829
+ await this.sendFrame(createSandboxFrame({
830
+ type: "sandbox.log",
831
+ sandboxId: this.options.sandboxId,
832
+ sandboxGeneration: this.sandboxGeneration,
833
+ connectionEpoch: this.connectionEpoch,
834
+ seq: this.nextOutboundSeq(),
835
+ payload: {
836
+ level: entry.level,
837
+ stream: "stderr",
838
+ message: entry.message,
839
+ emitted_at: entry.timestamp,
840
+ ...(entry.fields ? { fields: entry.fields } : {}),
841
+ },
842
+ }));
843
+ })
844
+ .catch(() => {
845
+ // Sentry forwarding is best-effort and must not enter the daemon logger.
846
+ });
691
847
  }
692
848
  assertServerFrame(frame) {
693
849
  if (frame.sandbox_id !== this.options.sandboxId ||
@@ -849,33 +1005,59 @@ export class AgentServiceSandboxClient {
849
1005
  await this.sendCommandAck(frame, "rejected", error instanceof Error ? error.message : "invalid_instructions");
850
1006
  return;
851
1007
  }
1008
+ const rawSkillProvider = frame.payload.skill_provider;
1009
+ const hasSkillProvider = rawSkillProvider !== undefined && rawSkillProvider !== null;
1010
+ if (hasSkillProvider && !runtimeSupportsCourseSkills(runtimeId)) {
1011
+ await this.sendCommandAck(frame, "rejected", "skill_provider_runtime_unsupported");
1012
+ return;
1013
+ }
1014
+ let skillGrantKey;
1015
+ if (hasSkillProvider) {
1016
+ try {
1017
+ skillGrantKey = runtimeSkillGrantKey(parseRuntimeSkillProviderGrantSet(rawSkillProvider));
1018
+ }
1019
+ catch (error) {
1020
+ const code = error instanceof RuntimeSkillProviderError
1021
+ ? error.code
1022
+ : "skill_provider_binding_invalid";
1023
+ await this.sendCommandAck(frame, "rejected", code);
1024
+ return;
1025
+ }
1026
+ }
852
1027
  const capabilities = Array.isArray(frame.payload.capabilities)
853
1028
  ? frame.payload.capabilities.filter((item) => typeof item === "string" && item.length > 0)
854
1029
  : [];
855
- if (capabilities.length > 0) {
856
- const workspace = ensureRuntimeSessionDirectories(sessionId, this.sandboxGeneration);
857
- const available = new Set(availableRunCapabilities({
858
- agent_run_id: "activation-probe",
859
- course_run_id: session.courseRunId ?? "",
860
- lesson_id: null,
861
- task_id: null,
862
- agent_instance_id: null,
863
- runtime: { id: runtimeId },
864
- input: {},
865
- context: {},
866
- limits: {},
867
- }, workspace.workspaceDir));
868
- const missing = capabilities.filter((item) => !available.has(item)).sort();
869
- if (missing.length > 0) {
870
- await this.sendCommandAck(frame, "rejected", `missing_capabilities:${missing.join(",")}`);
871
- return;
1030
+ let availableCapabilities;
1031
+ const missingCapabilities = (required) => {
1032
+ if (required.length === 0)
1033
+ return [];
1034
+ if (!availableCapabilities) {
1035
+ const workspace = ensureRuntimeSessionDirectories(sessionId, this.sandboxGeneration);
1036
+ availableCapabilities = new Set(availableRunCapabilities({
1037
+ agent_run_id: "activation-probe",
1038
+ course_run_id: session.courseRunId ?? "",
1039
+ lesson_id: null,
1040
+ task_id: null,
1041
+ agent_instance_id: null,
1042
+ runtime: { id: runtimeId },
1043
+ input: {},
1044
+ context: {},
1045
+ limits: {},
1046
+ }, workspace.workspaceDir));
872
1047
  }
1048
+ return Array.from(new Set(required.filter((item) => !availableCapabilities.has(item)))).sort();
1049
+ };
1050
+ const missingDeclaredCapabilities = missingCapabilities(capabilities);
1051
+ if (missingDeclaredCapabilities.length > 0) {
1052
+ await this.sendCommandAck(frame, "rejected", `missing_capabilities:${missingDeclaredCapabilities.join(",")}`);
1053
+ return;
873
1054
  }
874
1055
  const existingActivation = this.activationContexts.get(sessionId);
875
1056
  if (existingActivation?.activationId === activationId) {
876
1057
  if (this.activeSessionId !== sessionId ||
877
1058
  session.runtimeId !== runtimeId ||
878
- session.contextRevision !== contextRevision) {
1059
+ session.contextRevision !== contextRevision ||
1060
+ existingActivation.skillGrantKey !== skillGrantKey) {
879
1061
  await this.sendCommandAck(frame, "rejected", "activation_redefinition");
880
1062
  return;
881
1063
  }
@@ -891,6 +1073,25 @@ export class AgentServiceSandboxClient {
891
1073
  await this.sendCommandAck(frame, "ok");
892
1074
  return;
893
1075
  }
1076
+ let skillProvider;
1077
+ if (hasSkillProvider) {
1078
+ try {
1079
+ skillProvider = await this.prepareSkillProvider(rawSkillProvider);
1080
+ }
1081
+ catch (error) {
1082
+ const code = error instanceof RuntimeSkillProviderError
1083
+ ? error.code
1084
+ : "skill_provider_prepare_failed";
1085
+ await this.sendCommandAck(frame, "rejected", code);
1086
+ return;
1087
+ }
1088
+ const requiredBySkills = skillProvider.catalog.flatMap((entry) => entry.requiredCapabilities);
1089
+ const missingSkillCapabilities = missingCapabilities(requiredBySkills);
1090
+ if (missingSkillCapabilities.length > 0) {
1091
+ await this.sendCommandAck(frame, "rejected", `missing_capabilities:${missingSkillCapabilities.join(",")}`);
1092
+ return;
1093
+ }
1094
+ }
894
1095
  // 同一时刻只允许一个 active session/activation。先撤销上一 workspace 的
895
1096
  // runtime 访问并等待进程退出,再暴露新 workspace,避免同 UID 跨 Session 读取。
896
1097
  const previousSessionId = this.activeSessionId;
@@ -920,6 +1121,8 @@ export class AgentServiceSandboxClient {
920
1121
  runtimeEnv,
921
1122
  instructions,
922
1123
  inputAttachmentGrant: attachmentGrant,
1124
+ ...(skillProvider ? { skillProvider } : {}),
1125
+ ...(skillGrantKey ? { skillGrantKey } : {}),
923
1126
  });
924
1127
  this.activeSessionId = sessionId;
925
1128
  this.persist();
@@ -1037,7 +1240,10 @@ export class AgentServiceSandboxClient {
1037
1240
  await this.sendCommandAck(frame, "rejected", "turn_scope_mismatch");
1038
1241
  return;
1039
1242
  }
1040
- this.dispatcher.cancel(frame.agent_run_id);
1243
+ if (!this.dispatcher.cancel(frame.agent_run_id)) {
1244
+ await this.sendCommandAck(frame, "rejected", "turn_not_running");
1245
+ return;
1246
+ }
1041
1247
  await this.sendCommandAck(frame, "ok");
1042
1248
  }
1043
1249
  /**
@@ -1,6 +1,6 @@
1
1
  export declare const AGENT_SERVICE_WS_SCHEMA: "botlearn-agent-sandbox-ws/0.2";
2
2
  export declare const AGENT_SERVICE_WS_SUBPROTOCOL: "botlearn-agent-sandbox.v2";
3
- export type SandboxFrameType = "sandbox.hello" | "sandbox.sync" | "session.open" | "session.activate" | "turn.start" | "turn.cancel" | "session.close" | "sandbox.drain" | "sandbox.shutdown" | "event.ack" | "auth.rotate" | "ping" | "sandbox.ready" | "sandbox.heartbeat" | "command.ack" | "session.opened" | "session.closed" | "turn.event" | "turn.file.report" | "sandbox.drained" | "pong" | "protocol.error";
3
+ export type SandboxFrameType = "sandbox.hello" | "sandbox.sync" | "session.open" | "session.activate" | "turn.start" | "turn.cancel" | "session.close" | "sandbox.drain" | "sandbox.shutdown" | "event.ack" | "auth.rotate" | "ping" | "sandbox.ready" | "sandbox.heartbeat" | "command.ack" | "session.opened" | "session.closed" | "turn.event" | "turn.file.report" | "sandbox.drained" | "sandbox.log" | "pong" | "protocol.error";
4
4
  export declare class UnsupportedSandboxProtocolError extends Error {
5
5
  readonly schemaVersion: unknown;
6
6
  constructor(schemaVersion: unknown);
@@ -22,6 +22,7 @@ const FRAME_TYPES = new Set([
22
22
  "turn.event",
23
23
  "turn.file.report",
24
24
  "sandbox.drained",
25
+ "sandbox.log",
25
26
  "pong",
26
27
  "protocol.error",
27
28
  ]);
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";
package/dist/log.d.ts CHANGED
@@ -1,4 +1,13 @@
1
1
  type Level = "info" | "warn" | "error" | "debug";
2
+ export interface OperationalLogEntry {
3
+ level: Level;
4
+ message: string;
5
+ fields?: Record<string, unknown>;
6
+ timestamp: string;
7
+ }
8
+ export type OperationalLogSink = (entry: OperationalLogEntry) => void;
9
+ /** One managed sandbox daemon runs per process, so it owns one ephemeral remote sink. */
10
+ export declare function setOperationalLogSink(sink: OperationalLogSink): () => void;
2
11
  export interface Logger {
3
12
  info(msg: string, fields?: Record<string, unknown>): void;
4
13
  warn(msg: string, fields?: Record<string, unknown>): void;
package/dist/log.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { appendFileSync, mkdirSync, readdirSync, renameSync, statSync, unlinkSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { daemonHome } from "./auth-store.js";
4
- import { redactSecretsDeep } from "./redaction.js";
4
+ import { redactSecretString, redactSecretsDeep } from "./redaction.js";
5
5
  const LOG_DIR = path.join(daemonHome(), "logs");
6
6
  const LOG_FILE = path.join(LOG_DIR, "daemon.log");
7
7
  const LOG_ROTATE_MAX_BYTES = 10 * 1024 * 1024;
@@ -18,6 +18,15 @@ function ensureDir() {
18
18
  }
19
19
  inited = true;
20
20
  }
21
+ let operationalLogSink = null;
22
+ /** One managed sandbox daemon runs per process, so it owns one ephemeral remote sink. */
23
+ export function setOperationalLogSink(sink) {
24
+ operationalLogSink = sink;
25
+ return () => {
26
+ if (operationalLogSink === sink)
27
+ operationalLogSink = null;
28
+ };
29
+ }
21
30
  function formatValue(value) {
22
31
  if (value instanceof Error)
23
32
  return JSON.stringify(value.stack ?? value.message);
@@ -131,7 +140,9 @@ function write(level, msg, fields) {
131
140
  ensureDir();
132
141
  // 所有 fields 值序列化前先深度脱敏,token 类值绝不落盘/上屏。
133
142
  const safeFields = fields === undefined ? undefined : redactSecretsDeep(fields);
134
- const line = formatLogLine(level, msg, safeFields);
143
+ const safeMessage = redactSecretString(msg);
144
+ const timestamp = new Date();
145
+ const line = formatLogLine(level, safeMessage, safeFields, timestamp);
135
146
  try {
136
147
  rotateLogIfNeeded(LOG_FILE, Buffer.byteLength(line) + 1);
137
148
  appendFileSync(LOG_FILE, line + "\n", { mode: 0o600 });
@@ -141,6 +152,17 @@ function write(level, msg, fields) {
141
152
  }
142
153
  // 同步镜像到 stderr,前台运行时可直接观察。
143
154
  process.stderr.write(line + "\n");
155
+ try {
156
+ operationalLogSink?.({
157
+ level,
158
+ message: safeMessage,
159
+ fields: safeFields,
160
+ timestamp: timestamp.toISOString(),
161
+ });
162
+ }
163
+ catch {
164
+ // Observability must never affect daemon execution or recurse into the logger.
165
+ }
144
166
  }
145
167
  export const log = {
146
168
  info: (msg, fields) => write("info", msg, fields),
@@ -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 {};