@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.
@@ -0,0 +1,439 @@
1
+ import { chmodSync, lstatSync, mkdtempSync, rmSync, } from "node:fs";
2
+ import net from "node:net";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { RuntimeSkillProviderError, safeReferencePath, } from "../runtime-skills.js";
6
+ const DEFAULT_PROTOCOL_VERSION = "2024-11-05";
7
+ const MAX_REQUEST_LINE_BYTES = 32 * 1024;
8
+ const MAX_REFERENCE_BYTES_PER_ACTIVATION = 1024 * 1024;
9
+ const TOOLS = [
10
+ {
11
+ name: "list",
12
+ description: "List the immutable BotLearn Course Skills authorized for this activation.",
13
+ inputSchema: {
14
+ type: "object",
15
+ properties: {},
16
+ additionalProperties: false,
17
+ },
18
+ },
19
+ {
20
+ name: "load",
21
+ description: "Load the locked SKILL.md for one authorized Course Skill before following it.",
22
+ inputSchema: {
23
+ type: "object",
24
+ properties: {
25
+ ref: { type: "string", minLength: 5, maxLength: 240 },
26
+ },
27
+ required: ["ref"],
28
+ additionalProperties: false,
29
+ },
30
+ },
31
+ {
32
+ name: "load_reference",
33
+ description: "Load one relative reference path from a Course Skill already loaded in this activation.",
34
+ inputSchema: {
35
+ type: "object",
36
+ properties: {
37
+ ref: { type: "string", minLength: 5, maxLength: 240 },
38
+ path: { type: "string", minLength: 1, maxLength: 240 },
39
+ },
40
+ required: ["ref", "path"],
41
+ additionalProperties: false,
42
+ },
43
+ },
44
+ ];
45
+ export class CourseSkillsMcpRuntime {
46
+ options;
47
+ grantsByRef;
48
+ loadedSkills = new Map();
49
+ loadedReferences = new Map();
50
+ referenceBytes = 0;
51
+ constructor(options) {
52
+ this.options = options;
53
+ this.grantsByRef = new Map(options.prepared.catalog.map((entry) => [entry.ref, entry]));
54
+ }
55
+ async emitAppliedEvents() {
56
+ for (const entry of this.options.prepared.catalog) {
57
+ await this.emit(entry, "applied");
58
+ }
59
+ }
60
+ async handle(request) {
61
+ if (request.id === undefined)
62
+ return null;
63
+ const id = responseId(request.id);
64
+ if (request.jsonrpc !== "2.0" || typeof request.method !== "string") {
65
+ return { jsonrpc: "2.0", id, error: { code: -32600, message: "Invalid Request" } };
66
+ }
67
+ if (request.method === "initialize") {
68
+ const requested = request.params && typeof request.params === "object"
69
+ ? request.params.protocolVersion
70
+ : undefined;
71
+ return {
72
+ jsonrpc: "2.0",
73
+ id,
74
+ result: {
75
+ protocolVersion: typeof requested === "string" && requested.length > 0
76
+ ? requested
77
+ : DEFAULT_PROTOCOL_VERSION,
78
+ capabilities: { tools: {} },
79
+ serverInfo: { name: "botlearn-course-skills", version: "0.1.0" },
80
+ },
81
+ };
82
+ }
83
+ if (request.method === "tools/list") {
84
+ return { jsonrpc: "2.0", id, result: { tools: TOOLS } };
85
+ }
86
+ if (request.method !== "tools/call") {
87
+ return { jsonrpc: "2.0", id, error: { code: -32601, message: "Method not found" } };
88
+ }
89
+ const params = objectValue(request.params);
90
+ const name = params && typeof params.name === "string" ? params.name : "";
91
+ const args = params ? objectValue(params.arguments) : null;
92
+ try {
93
+ if (name === "list") {
94
+ requireExactArguments(args, []);
95
+ return toolSuccess(id, "skill_catalog_listed", {
96
+ skills: this.options.prepared.catalog.map((entry) => ({
97
+ ref: entry.ref,
98
+ digest: entry.digest,
99
+ name: entry.name,
100
+ description: entry.description,
101
+ version: entry.version,
102
+ requiredCapabilities: entry.requiredCapabilities,
103
+ })),
104
+ });
105
+ }
106
+ if (name === "load") {
107
+ requireExactArguments(args, ["ref"]);
108
+ const ref = requiredArgument(args, "ref");
109
+ return toolSuccess(id, "skill_loaded", await this.loadSkill(ref));
110
+ }
111
+ if (name === "load_reference") {
112
+ requireExactArguments(args, ["ref", "path"]);
113
+ const ref = requiredArgument(args, "ref");
114
+ const referencePath = requiredArgument(args, "path");
115
+ return toolSuccess(id, "skill_reference_loaded", await this.loadReference(ref, referencePath));
116
+ }
117
+ return toolFailure(id, "unknown_tool");
118
+ }
119
+ catch (error) {
120
+ return toolFailure(id, safeRuntimeErrorCode(error));
121
+ }
122
+ }
123
+ async loadSkill(ref) {
124
+ const entry = this.requireGrant(ref);
125
+ const cached = this.loadedSkills.get(ref);
126
+ if (cached)
127
+ return cached;
128
+ const pending = (async () => {
129
+ await this.emit(entry, "selected");
130
+ try {
131
+ const loaded = await this.options.prepared.provider.loadSkill(ref);
132
+ await this.emit(entry, "loaded", {
133
+ bytes: loaded.bytes,
134
+ truncated: loaded.truncated,
135
+ });
136
+ return loaded;
137
+ }
138
+ catch (error) {
139
+ await this.emitLoadFailed(entry, error);
140
+ throw error;
141
+ }
142
+ })();
143
+ this.loadedSkills.set(ref, pending);
144
+ try {
145
+ return await pending;
146
+ }
147
+ catch (error) {
148
+ this.loadedSkills.delete(ref);
149
+ throw error;
150
+ }
151
+ }
152
+ async loadReference(ref, referencePath) {
153
+ const entry = this.requireGrant(ref);
154
+ let safePath;
155
+ try {
156
+ safePath = safeReferencePath(referencePath);
157
+ }
158
+ catch (error) {
159
+ await this.emitLoadFailed(entry, error);
160
+ throw error;
161
+ }
162
+ const selected = this.loadedSkills.get(ref);
163
+ if (!selected) {
164
+ const error = new RuntimeSkillProviderError("skill_not_selected", "Load the Skill before loading one of its references");
165
+ await this.emitLoadFailed(entry, error);
166
+ throw error;
167
+ }
168
+ // Presence in the cache only means load(ref) was selected. A parallel MCP
169
+ // request must not read references until the locked main content has
170
+ // actually passed Provider identity validation.
171
+ await selected;
172
+ const key = `${ref}\0${safePath}`;
173
+ const cached = this.loadedReferences.get(key);
174
+ if (cached)
175
+ return cached;
176
+ const pending = (async () => {
177
+ try {
178
+ const loaded = await this.options.prepared.provider.loadReference(ref, safePath);
179
+ if (loaded.bytes > MAX_REFERENCE_BYTES_PER_ACTIVATION - this.referenceBytes) {
180
+ throw new RuntimeSkillProviderError("skill_load_budget_exceeded", "Course Skill reference read budget exceeded");
181
+ }
182
+ this.referenceBytes += loaded.bytes;
183
+ await this.emit(entry, "reference_loaded", {
184
+ bytes: loaded.bytes,
185
+ truncated: loaded.truncated,
186
+ });
187
+ return loaded;
188
+ }
189
+ catch (error) {
190
+ await this.emitLoadFailed(entry, error);
191
+ throw error;
192
+ }
193
+ })();
194
+ this.loadedReferences.set(key, pending);
195
+ try {
196
+ return await pending;
197
+ }
198
+ catch (error) {
199
+ this.loadedReferences.delete(key);
200
+ throw error;
201
+ }
202
+ }
203
+ requireGrant(ref) {
204
+ const entry = this.grantsByRef.get(ref);
205
+ if (!entry) {
206
+ throw new RuntimeSkillProviderError("skill_not_eligible", "Skill is not eligible for the current activation");
207
+ }
208
+ return entry;
209
+ }
210
+ async emit(entry, state, fields = {}) {
211
+ await this.options.onEvent({
212
+ schema_version: "agent-skill-event/0.1",
213
+ kind: "skill_event",
214
+ state,
215
+ ref: entry.ref,
216
+ digest: entry.digest,
217
+ load_mode: "mcp",
218
+ ...fields,
219
+ });
220
+ }
221
+ async emitLoadFailed(entry, error) {
222
+ await this.options.onEvent({
223
+ schema_version: "agent-skill-event/0.1",
224
+ kind: "skill_event",
225
+ state: "load_failed",
226
+ ref: entry.ref,
227
+ digest: entry.digest,
228
+ load_mode: "mcp",
229
+ code: safeRuntimeErrorCode(error),
230
+ });
231
+ }
232
+ }
233
+ export async function startCourseSkillsMcpServer(options) {
234
+ const managedRoot = resolveManagedSkillsRoot(options.managedRoot);
235
+ const dir = mkdtempSync(path.join(managedRoot ?? tmpdir(), "botlearn-skills-mcp-"));
236
+ const socketPath = path.join(dir, "server.sock");
237
+ const runtime = new CourseSkillsMcpRuntime(options);
238
+ const sockets = new Set();
239
+ const server = net.createServer((socket) => {
240
+ sockets.add(socket);
241
+ socket.on("close", () => sockets.delete(socket));
242
+ socket.on("error", () => socket.destroy());
243
+ serveSocket(socket, runtime);
244
+ });
245
+ try {
246
+ if (managedRoot)
247
+ chmodSync(dir, 0o750);
248
+ await listen(server, socketPath);
249
+ chmodSync(socketPath, managedRoot ? 0o660 : 0o600);
250
+ }
251
+ catch (error) {
252
+ await closeServer(server, sockets);
253
+ rmSync(dir, { recursive: true, force: true });
254
+ throw error;
255
+ }
256
+ let closed = false;
257
+ let applied;
258
+ return {
259
+ socketPath,
260
+ catalog: options.prepared.catalog,
261
+ markApplied() {
262
+ applied ??= runtime.emitAppliedEvents();
263
+ return applied;
264
+ },
265
+ async close() {
266
+ if (closed)
267
+ return;
268
+ closed = true;
269
+ await closeServer(server, sockets);
270
+ rmSync(dir, { recursive: true, force: true });
271
+ },
272
+ };
273
+ }
274
+ function serveSocket(socket, runtime) {
275
+ socket.setEncoding("utf8");
276
+ let buffer = "";
277
+ let chain = Promise.resolve();
278
+ socket.on("data", (chunk) => {
279
+ buffer += chunk;
280
+ if (Buffer.byteLength(buffer, "utf8") > MAX_REQUEST_LINE_BYTES) {
281
+ socket.destroy();
282
+ return;
283
+ }
284
+ let newline;
285
+ while ((newline = buffer.indexOf("\n")) >= 0) {
286
+ const line = buffer.slice(0, newline);
287
+ buffer = buffer.slice(newline + 1);
288
+ if (!line.trim())
289
+ continue;
290
+ chain = chain.then(async () => {
291
+ let response;
292
+ try {
293
+ response = await runtime.handle(JSON.parse(line));
294
+ }
295
+ catch {
296
+ response = {
297
+ jsonrpc: "2.0",
298
+ id: null,
299
+ error: { code: -32700, message: "Parse error" },
300
+ };
301
+ }
302
+ if (response && !socket.destroyed) {
303
+ socket.write(`${JSON.stringify(response)}\n`);
304
+ }
305
+ }).catch(() => {
306
+ socket.destroy();
307
+ });
308
+ }
309
+ });
310
+ }
311
+ function responseId(value) {
312
+ return typeof value === "string" || typeof value === "number" || value === null
313
+ ? value
314
+ : null;
315
+ }
316
+ function toolSuccess(id, code, data) {
317
+ return {
318
+ jsonrpc: "2.0",
319
+ id,
320
+ result: {
321
+ content: [{
322
+ type: "text",
323
+ text: JSON.stringify({
324
+ schemaVersion: "tool-result/0.1",
325
+ ok: true,
326
+ code,
327
+ data,
328
+ evidence: [],
329
+ }),
330
+ }],
331
+ isError: false,
332
+ },
333
+ };
334
+ }
335
+ function toolFailure(id, code) {
336
+ return {
337
+ jsonrpc: "2.0",
338
+ id,
339
+ result: {
340
+ content: [{
341
+ type: "text",
342
+ text: JSON.stringify({
343
+ schemaVersion: "tool-result/0.1",
344
+ ok: false,
345
+ code,
346
+ error: { message: "Course Skill request failed" },
347
+ evidence: [],
348
+ }),
349
+ }],
350
+ isError: true,
351
+ },
352
+ };
353
+ }
354
+ function objectValue(value) {
355
+ return value && typeof value === "object" && !Array.isArray(value)
356
+ ? value
357
+ : null;
358
+ }
359
+ function requireExactArguments(args, keys) {
360
+ if (keys.length === 0 && args === null)
361
+ return;
362
+ if (!args || Object.keys(args).length !== keys.length) {
363
+ throw new RuntimeSkillProviderError("skill_request_invalid", "Course Skill tool arguments are invalid");
364
+ }
365
+ const allowed = new Set(keys);
366
+ if (Object.keys(args).some((key) => !allowed.has(key))) {
367
+ throw new RuntimeSkillProviderError("skill_request_invalid", "Course Skill tool arguments are invalid");
368
+ }
369
+ }
370
+ function requiredArgument(args, key) {
371
+ const value = args?.[key];
372
+ if (typeof value !== "string"
373
+ || value.length < 1
374
+ || Buffer.byteLength(value, "utf8") > 240) {
375
+ throw new RuntimeSkillProviderError("skill_request_invalid", "Course Skill tool arguments are invalid");
376
+ }
377
+ return value;
378
+ }
379
+ function safeRuntimeErrorCode(error) {
380
+ if (error instanceof RuntimeSkillProviderError
381
+ && /^[a-z][a-z0-9_]{2,79}$/.test(error.code)) {
382
+ return error.code;
383
+ }
384
+ return "skill_load_failed";
385
+ }
386
+ function resolveManagedSkillsRoot(explicit) {
387
+ if (explicit === null)
388
+ return null;
389
+ let root = explicit?.trim();
390
+ if (root === undefined) {
391
+ const managedRuntime = process.env.BOTLEARN_RUNTIME_USER?.trim()
392
+ || process.env.BOTLEARN_RUNTIME_LAUNCH_MODE?.trim();
393
+ if (!managedRuntime)
394
+ return null;
395
+ root = process.env.BOTLEARN_AGENT_SERVICE_PROFILE_ROOT?.trim();
396
+ if (!root) {
397
+ throw new RuntimeSkillProviderError("skill_provider_runtime_boundary_invalid", "Managed runtime is missing its profile root");
398
+ }
399
+ }
400
+ if (!root || !path.isAbsolute(root)) {
401
+ throw new RuntimeSkillProviderError("skill_provider_runtime_boundary_invalid", "Managed Course Skill root must be absolute");
402
+ }
403
+ try {
404
+ const stat = lstatSync(root);
405
+ const currentUid = process.getuid?.();
406
+ if (!stat.isDirectory()
407
+ || stat.isSymbolicLink()
408
+ || (currentUid !== undefined && stat.uid !== currentUid)
409
+ || (stat.mode & 0o022) !== 0) {
410
+ throw new Error("unsafe root");
411
+ }
412
+ }
413
+ catch {
414
+ throw new RuntimeSkillProviderError("skill_provider_runtime_boundary_invalid", "Managed Course Skill root is unavailable");
415
+ }
416
+ return root;
417
+ }
418
+ function listen(server, socketPath) {
419
+ return new Promise((resolve, reject) => {
420
+ const onError = (error) => {
421
+ server.off("listening", onListening);
422
+ reject(error);
423
+ };
424
+ const onListening = () => {
425
+ server.off("error", onError);
426
+ resolve();
427
+ };
428
+ server.once("error", onError);
429
+ server.once("listening", onListening);
430
+ server.listen(socketPath);
431
+ });
432
+ }
433
+ function closeServer(server, sockets) {
434
+ for (const socket of sockets)
435
+ socket.destroy();
436
+ if (!server.listening)
437
+ return Promise.resolve();
438
+ return new Promise((resolve) => server.close(() => resolve()));
439
+ }
@@ -1,6 +1,7 @@
1
1
  import { type ScanLimits } from "./file-candidates.js";
2
2
  import { type InputAttachmentGrant } from "./input-attachments.js";
3
3
  import { type Logger } from "./log.js";
4
+ import type { PreparedRuntimeSkillProvider } from "./runtime-skills.js";
4
5
  import { type CourseRuntimeProfile, type CourseRuntime, type RunEvent, type RunStartPayload } from "./types.js";
5
6
  export interface RunDispatcherOptions {
6
7
  defaultRuntimeId?: string;
@@ -17,6 +18,7 @@ export interface PreparedPersistentTurn {
17
18
  contextRevision: number;
18
19
  runtimeEnv?: NodeJS.ProcessEnv;
19
20
  inputAttachmentGrant?: InputAttachmentGrant;
21
+ skillProvider?: PreparedRuntimeSkillProvider;
20
22
  }
21
23
  export interface PersistentSessionExecution {
22
24
  prepareTurn(payload: RunStartPayload): PreparedPersistentTurn;
@@ -9,6 +9,7 @@ import { missingRunCapabilities } from "./runtime-capabilities.js";
9
9
  import { RunQueue } from "./run-queue.js";
10
10
  import { applyRunRuntimeProfile, cleanupRunRuntimeProfile, RuntimeProfileApplyError, runtimeProfileInstructions, } from "./runtime-profile.js";
11
11
  import { TranscriptWriter } from "./transcript.js";
12
+ import { buildToolObservation } from "./tool-observation.js";
12
13
  import { RuntimeExecutionError, } from "./types.js";
13
14
  import { ensureRunWorkspace, transcriptPath } from "./workspace.js";
14
15
  // 与 runtimes/index.ts 的 DEFAULT_RUNTIME_ID 保持一致(此处不 import registry,避免拉入全部 adapter)。
@@ -24,7 +25,8 @@ const CONTENT_FLUSH_INTERVAL_MS = 100;
24
25
  const AGENT_STREAM_SCHEMA_VERSION = "agent-stream/0.1";
25
26
  const SAFE_TOOL_NAME = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,79}$/;
26
27
  const SAFE_FAILURE_CODE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$/;
27
- const FAILURE_DIAGNOSTIC_SCHEMA_VERSION = "botlearn-agent-run-failure/1";
28
+ const SAFE_FAILURE_MODEL = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$/;
29
+ const FAILURE_DIAGNOSTIC_SCHEMA_VERSION = "botlearn-agent-run-failure/2";
28
30
  function wireFailureDiagnostic(runtime, error, message) {
29
31
  const failure = error instanceof RuntimeExecutionError ? error.failure : undefined;
30
32
  const info = errorInfo(error);
@@ -52,6 +54,29 @@ function wireFailureDiagnostic(runtime, error, message) {
52
54
  if (typeof errorName === "string" && SAFE_FAILURE_CODE.test(errorName)) {
53
55
  diagnostic.error_name = errorName;
54
56
  }
57
+ if (typeof failure?.error_code === "string" && SAFE_FAILURE_CODE.test(failure.error_code)) {
58
+ diagnostic.error_code = failure.error_code;
59
+ }
60
+ if (typeof failure?.model === "string" && SAFE_FAILURE_MODEL.test(failure.model)) {
61
+ diagnostic.model = failure.model;
62
+ }
63
+ if (failure?.completion) {
64
+ diagnostic.completion = {
65
+ assistant_message_count: Math.max(0, Math.min(10_000, Math.floor(failure.completion.assistant_message_count))),
66
+ reasoning_message_count: Math.max(0, Math.min(10_000, Math.floor(failure.completion.reasoning_message_count))),
67
+ assistant_content_present: Boolean(failure.completion.assistant_content_present),
68
+ reasoning_content_present: Boolean(failure.completion.reasoning_content_present),
69
+ tool_call_count: Math.max(0, Math.min(10_000, Math.floor(failure.completion.tool_call_count))),
70
+ ...(typeof failure.completion.turn_status === "string"
71
+ && SAFE_FAILURE_CODE.test(failure.completion.turn_status)
72
+ ? { turn_status: failure.completion.turn_status }
73
+ : {}),
74
+ ...(typeof failure.completion.finish_reason === "string"
75
+ && SAFE_FAILURE_CODE.test(failure.completion.finish_reason)
76
+ ? { finish_reason: failure.completion.finish_reason }
77
+ : {}),
78
+ };
79
+ }
55
80
  if (typeof failure?.stderr_tail === "string" && failure.stderr_tail) {
56
81
  diagnostic.stderr_tail = sanitizeRuntimeFailureText(failure.stderr_tail, 8192);
57
82
  }
@@ -207,6 +232,7 @@ export class RunDispatcher {
207
232
  const providerReported = inputTokens !== undefined
208
233
  || runtimeUsage.cached_input_tokens !== undefined
209
234
  || outputTokens !== undefined
235
+ || runtimeUsage.reasoning_tokens !== undefined
210
236
  || runtimeUsage.cost_usd !== undefined;
211
237
  return {
212
238
  schema_version: "agent-run-usage/0.1",
@@ -278,7 +304,6 @@ export class RunDispatcher {
278
304
  agentRunId: runId,
279
305
  traceId,
280
306
  runtime: runtimeId,
281
- model: payload.runtime.model,
282
307
  status: event.type,
283
308
  errorType: event.payload?.error_type,
284
309
  });
@@ -491,6 +516,23 @@ export class RunDispatcher {
491
516
  });
492
517
  };
493
518
  const sink = {
519
+ skillEvent: async (event) => {
520
+ await flushContent();
521
+ await queueStreamEvent({
522
+ type: "run.block",
523
+ payload: {
524
+ schema_version: event.schema_version,
525
+ kind: event.kind,
526
+ state: event.state,
527
+ ref: event.ref,
528
+ digest: event.digest,
529
+ load_mode: event.load_mode,
530
+ ...(event.bytes !== undefined ? { bytes: event.bytes } : {}),
531
+ ...(event.truncated !== undefined ? { truncated: event.truncated } : {}),
532
+ ...(event.code !== undefined ? { code: event.code } : {}),
533
+ },
534
+ });
535
+ },
494
536
  progressDispositions: async (dispositions) => {
495
537
  progressDisposition.invalid += dispositions.invalid;
496
538
  progressDisposition.deduplicated += dispositions.deduplicated;
@@ -622,6 +664,13 @@ export class RunDispatcher {
622
664
  if (block.kind === "tool_call") {
623
665
  lastReportedKind = block.kind;
624
666
  lastReasoningPhase = null;
667
+ const observation = buildToolObservation(block, runtime.id, workspaceDir);
668
+ if (observation) {
669
+ await send({
670
+ type: "run.observation",
671
+ payload: observation,
672
+ });
673
+ }
625
674
  await queueStreamEvent({
626
675
  type: "run.block",
627
676
  payload: {
@@ -639,6 +688,13 @@ export class RunDispatcher {
639
688
  if (block.kind === "tool_result") {
640
689
  lastReportedKind = block.kind;
641
690
  lastReasoningPhase = null;
691
+ const observation = buildToolObservation(block, runtime.id, workspaceDir);
692
+ if (observation) {
693
+ await send({
694
+ type: "run.observation",
695
+ payload: observation,
696
+ });
697
+ }
642
698
  await queueStreamEvent({
643
699
  type: "run.block",
644
700
  payload: {
@@ -693,6 +749,9 @@ export class RunDispatcher {
693
749
  ...(persistentTurn.runtimeEnv
694
750
  ? { runtimeEnv: persistentTurn.runtimeEnv }
695
751
  : {}),
752
+ ...(persistentTurn.skillProvider
753
+ ? { skillProvider: persistentTurn.skillProvider }
754
+ : {}),
696
755
  }
697
756
  : {}),
698
757
  }, sink, controller.signal);
@@ -1,3 +1,4 @@
1
1
  import type { RunStartPayload } from "./types.js";
2
+ export declare function runtimeSupportsCourseSkills(runtimeId: string): boolean;
2
3
  export declare function availableRunCapabilities(payload: RunStartPayload, workspaceDir: string): string[];
3
4
  export declare function missingRunCapabilities(payload: RunStartPayload, workspaceDir: string): string[];
@@ -9,6 +9,10 @@ const WEB_SEARCH_RUNTIMES = new Set([
9
9
  "hermes-agent",
10
10
  "fake",
11
11
  ]);
12
+ const COURSE_SKILL_RUNTIMES = new Set(["deepseek-tui"]);
13
+ export function runtimeSupportsCourseSkills(runtimeId) {
14
+ return COURSE_SKILL_RUNTIMES.has(runtimeId);
15
+ }
12
16
  export function availableRunCapabilities(payload, workspaceDir) {
13
17
  const capabilities = new Set();
14
18
  const probePath = path.join(workspaceDir, `.botlearn-write-probe-${process.pid}-${randomUUID()}`);
@@ -0,0 +1,93 @@
1
+ export declare const COURSE_SKILLS_BINDING_SCHEMA: "course-skills-binding/0.1";
2
+ export declare const COURSE_SKILLS_PROTOCOL: "course-skills/0.1";
3
+ export declare const COURSE_SKILLS_CALL_SCHEMA: "course-skills-call/0.1";
4
+ export interface RuntimeSkillGrant {
5
+ assetVersionId: string;
6
+ ref: string;
7
+ digest: string;
8
+ }
9
+ export interface RuntimeSkillProviderBinding {
10
+ schemaVersion: typeof COURSE_SKILLS_BINDING_SCHEMA;
11
+ protocol: typeof COURSE_SKILLS_PROTOCOL;
12
+ endpoint: string;
13
+ bearerToken: string;
14
+ expiresAt: string;
15
+ effectiveSkillGrants: RuntimeSkillGrant[];
16
+ }
17
+ export interface RuntimeSkillCatalogEntry extends RuntimeSkillGrant {
18
+ name: string;
19
+ description: string;
20
+ version: string;
21
+ requiredCapabilities: string[];
22
+ }
23
+ export interface RuntimeLoadedSkill {
24
+ ref: string;
25
+ digest: string;
26
+ content: string;
27
+ bytes: number;
28
+ truncated: boolean;
29
+ }
30
+ export interface RuntimeSkillReference extends RuntimeLoadedSkill {
31
+ path: string;
32
+ }
33
+ export interface RuntimeSkillProvider {
34
+ listEligibleSkills(): Promise<RuntimeSkillCatalogEntry[]>;
35
+ loadSkill(ref: string): Promise<RuntimeLoadedSkill>;
36
+ loadReference(ref: string, referencePath: string): Promise<RuntimeSkillReference>;
37
+ }
38
+ export interface PreparedRuntimeSkillProvider {
39
+ binding: RuntimeSkillProviderBinding;
40
+ provider: RuntimeSkillProvider;
41
+ catalog: RuntimeSkillCatalogEntry[];
42
+ }
43
+ export type RuntimeSkillEventState = "applied" | "selected" | "loaded" | "reference_loaded" | "load_failed";
44
+ export interface RuntimeSkillEvent {
45
+ schema_version: "agent-skill-event/0.1";
46
+ kind: "skill_event";
47
+ state: RuntimeSkillEventState;
48
+ ref: string;
49
+ digest: string;
50
+ load_mode: "mcp";
51
+ bytes?: number;
52
+ truncated?: boolean;
53
+ code?: string;
54
+ }
55
+ export declare class RuntimeSkillProviderError extends Error {
56
+ readonly code: string;
57
+ constructor(code: string, message: string);
58
+ }
59
+ export type RuntimeSkillProviderFactory = (rawBinding: unknown) => Promise<PreparedRuntimeSkillProvider>;
60
+ interface RuntimeSkillBindingParseOptions {
61
+ allowInsecureLoopbackForTests?: boolean;
62
+ }
63
+ export declare function parseRuntimeSkillProviderBinding(raw: unknown, now?: number, options?: RuntimeSkillBindingParseOptions): RuntimeSkillProviderBinding;
64
+ /**
65
+ * Parse only the stable grant identity of a replayed binding.
66
+ *
67
+ * Credential values remain the first accepted activation snapshot, so replay validation
68
+ * deliberately checks their shape but not their endpoint/expiry semantics.
69
+ */
70
+ export declare function parseRuntimeSkillProviderGrantSet(raw: unknown): RuntimeSkillGrant[];
71
+ export declare function prepareRuntimeSkillProvider(rawBinding: unknown, options?: {
72
+ fetchFn?: typeof fetch;
73
+ now?: () => number;
74
+ /** Tests only: production bindings must use HTTPS. */
75
+ allowInsecureLoopbackForTests?: boolean;
76
+ }): Promise<PreparedRuntimeSkillProvider>;
77
+ export declare class HttpRuntimeSkillProvider implements RuntimeSkillProvider {
78
+ private readonly binding;
79
+ private readonly fetchFn;
80
+ private readonly now;
81
+ private readonly grantsByRef;
82
+ constructor(binding: RuntimeSkillProviderBinding, options?: {
83
+ fetchFn?: typeof fetch;
84
+ now?: () => number;
85
+ });
86
+ listEligibleSkills(): Promise<RuntimeSkillCatalogEntry[]>;
87
+ loadSkill(ref: string): Promise<RuntimeLoadedSkill>;
88
+ loadReference(ref: string, referencePath: string): Promise<RuntimeSkillReference>;
89
+ private requireEligible;
90
+ private call;
91
+ }
92
+ export declare function safeReferencePath(value: unknown): string;
93
+ export {};