@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 +5 -2
- package/dist/agent-service-sandbox.d.ts +15 -0
- package/dist/agent-service-sandbox.js +227 -21
- package/dist/agent-service-ws-protocol.d.ts +1 -1
- package/dist/agent-service-ws-protocol.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/log.d.ts +9 -0
- package/dist/log.js +24 -2
- package/dist/mcp/course-skills-relay.d.ts +2 -0
- package/dist/mcp/course-skills-relay.js +54 -0
- package/dist/mcp/course-skills-server.d.ts +49 -0
- package/dist/mcp/course-skills-server.js +439 -0
- package/dist/run-dispatcher.d.ts +2 -0
- package/dist/run-dispatcher.js +61 -2
- package/dist/runtime-capabilities.d.ts +1 -0
- package/dist/runtime-capabilities.js +4 -0
- package/dist/runtime-skills.d.ts +93 -0
- package/dist/runtime-skills.js +401 -0
- package/dist/runtimes/deepseek-tui.d.ts +2 -1
- package/dist/runtimes/deepseek-tui.js +252 -25
- package/dist/runtimes/engine.d.ts +6 -0
- package/dist/runtimes/engine.js +6 -1
- package/dist/runtimes/progress.d.ts +2 -0
- package/dist/runtimes/progress.js +34 -0
- package/dist/tool-observation.d.ts +21 -0
- package/dist/tool-observation.js +120 -0
- package/dist/types.d.ts +26 -2
- package/package.json +1 -1
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
|
|
118
|
-
|
|
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
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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);
|
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
|
|
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,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 {};
|