@springbrand/agent-runtime 0.2.0-alpha.41 → 0.2.0-alpha.43

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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/src/adapter/cloudflare/resources/r2-skill-source.ts +215 -0
  3. package/src/adapter/cloudflare/resources/runtime-resources.ts +1 -18
  4. package/src/adapter/cloudflare/subagent/tools.ts +2 -5
  5. package/src/adapter/cloudflare/universal-agent/preparation.ts +21 -9
  6. package/src/adapter/cloudflare/universal-agent/tools.ts +3 -2
  7. package/src/adapter/cloudflare/workspace/scoped-workspace.ts +15 -0
  8. package/src/index.ts +3 -0
  9. package/src/kernel/bindings.ts +38 -6
  10. package/src/layers/context/budget/gate.ts +3 -3
  11. package/src/layers/orchestration/temporary-agent/workspace.ts +2 -0
  12. package/src/lib/prompt.ts +30 -14
  13. package/src/pi/assembly/snapshot.ts +7 -1
  14. package/src/pi/runtime-adapter/assembly.ts +8 -3
  15. package/src/pi/runtime-adapter/execution.ts +108 -13
  16. package/src/pi/runtime-adapter/models.ts +9 -6
  17. package/src/pi/runtime-adapter/openrouter-messages.ts +10 -3
  18. package/src/pi/tool/base.ts +42 -14
  19. package/src/pi/tool/compiler.ts +15 -2
  20. package/src/pi/tool/core-host.ts +228 -1
  21. package/src/pi/tool/core.ts +37 -5
  22. package/src/pi/tool/declared.ts +3 -0
  23. package/src/pi/tool/nested-tools.ts +5 -1
  24. package/src/pi/tool/schedule.ts +12 -10
  25. package/src/pi/tool/skill.ts +240 -86
  26. package/src/pi/tool/subagent.ts +2 -0
  27. package/src/pi/tool/time.ts +1 -1
  28. package/src/pi/tool/web-fetch.ts +1 -1
  29. package/src/pi/tool/web-search/web-search.ts +2 -1
  30. package/src/pi/tool/workspace-revision.ts +2 -1
  31. package/src/pi/tool/workspace-sandbox.ts +10 -21
  32. package/src/runtime-agent.ts +3 -0
  33. package/src/runtime-assembler.ts +105 -7
  34. package/src/runtime-definition.ts +1 -0
  35. package/src/runtime.ts +50 -0
@@ -6,12 +6,15 @@ import {
6
6
  type SkillScriptRunner,
7
7
  type SkillSource,
8
8
  } from "agents/skills";
9
+ import { truncateResponse, truncateResult } from "@cloudflare/codemode";
9
10
  import { tool } from "ai";
10
11
  import { z } from "zod";
11
12
  import type {
12
13
  RuntimeSkillScriptPolicy,
13
14
  WorkspacePort,
14
15
  } from "../../kernel/bindings";
16
+ import { STORAGE_LEAF_MAX_CHARS } from "../../layers/context/budget/gate";
17
+ import { serializeOutput } from "../../lib/artifacts";
15
18
  import { aiToolToPi } from "./ai-adapter";
16
19
  import type { PiToolCandidate } from "./compiler";
17
20
  import { piCandidatesToAiTools } from "./nested-tools";
@@ -46,6 +49,13 @@ const SKILL_RESOURCE_BUDGET_BYTES = 8 * 1024 * 1024;
46
49
 
47
50
  type LoadedSkill = NonNullable<Awaited<ReturnType<SkillSource["load"]>>>;
48
51
 
52
+ const SKILL_RESOURCE_READ_GUIDANCE =
53
+ "Bundled Skill resources are not Workspace files. " +
54
+ "Inside execute, use tools.read_skill_resource for text instead of state.* on /skills.";
55
+ const SKILL_RESOURCE_MATERIALIZE_GUIDANCE =
56
+ "Inside execute, use tools.materialize_skill_resource for Workspace assets when available.";
57
+ const SKILL_SCRIPT_OUTPUT_ROOT = "/scratch/skill-output";
58
+
49
59
  /**
50
60
  * 按体积裁掉超预算的 Skill 资源,并把裁掉的事实写回 Skill 正文。
51
61
  *
@@ -93,7 +103,7 @@ function budgetSkillResources(skill: LoadedSkill): LoadedSkill {
93
103
  if (portable.length > 0) {
94
104
  lines.push(
95
105
  `> If any of these resources are needed, copy all required ones into the Workspace ` +
96
- `in one materialize_skill_resource call using its resources array: ` +
106
+ `in one execute with tools.materialize_skill_resource: ` +
97
107
  `${portable.map((entry) => entry.path).join(", ")}.`,
98
108
  );
99
109
  }
@@ -121,7 +131,21 @@ function catalogSkillSource(binding: PiSkillBinding): SkillSource {
121
131
  }],
122
132
  load: async (name) => {
123
133
  const skill = await source.load(name);
124
- return skill && budgetSkillResources(skill);
134
+ if (!skill) return null;
135
+ const loaded = budgetSkillResources(skill);
136
+ const hasOversizedResource = skill.resources?.some((resource) =>
137
+ typeof resource.size === "number" &&
138
+ resource.size > SKILL_RESOURCE_BUDGET_BYTES
139
+ );
140
+ const guidance = hasOversizedResource
141
+ ? SKILL_RESOURCE_READ_GUIDANCE
142
+ : `${SKILL_RESOURCE_READ_GUIDANCE} ${SKILL_RESOURCE_MATERIALIZE_GUIDANCE}`;
143
+ return loaded.resources?.length
144
+ ? {
145
+ ...loaded,
146
+ body: `${loaded.body}\n\n${guidance}`,
147
+ }
148
+ : loaded;
125
149
  },
126
150
  ...(source.readResource
127
151
  ? { readResource: (name: string, path: string) =>
@@ -193,6 +217,141 @@ const SKILL_TOOL_PARAMETER_DESCRIPTIONS: Readonly<Record<string, Readonly<Record
193
217
  const SKILL_ENTRY_READ_GUIDANCE =
194
218
  "SKILL.md contains the Skill instructions; use activate_skill instead.";
195
219
 
220
+ function normalizeRunSkillScriptArguments(input: unknown): unknown {
221
+ if (input === null || typeof input !== "object") return input;
222
+ const value = input as Record<string, unknown>;
223
+ if (typeof value.input !== "string") return input;
224
+ try {
225
+ const decoded = JSON.parse(value.input);
226
+ return decoded !== null && typeof decoded === "object"
227
+ ? { ...value, input: decoded }
228
+ : input;
229
+ } catch {
230
+ return input;
231
+ }
232
+ }
233
+
234
+ function result(details: unknown) {
235
+ return {
236
+ content: [{ type: "text" as const, text: serializeOutput(details).text }],
237
+ details,
238
+ };
239
+ }
240
+
241
+ function resourceTarget(
242
+ bindings: readonly PiSkillBinding[],
243
+ input: unknown,
244
+ ): { binding: PiSkillBinding; path: string } | null {
245
+ if (input === null || typeof input !== "object") return null;
246
+ const target = input as { name?: unknown; path?: unknown };
247
+ if (typeof target.path !== "string") return null;
248
+ if (typeof target.name === "string") {
249
+ const binding = bindings.find(({ name }) => name === target.name);
250
+ return binding ? { binding, path: target.path } : null;
251
+ }
252
+ const [name, ...rest] = target.path.split("/");
253
+ const binding = bindings.find((candidate) => candidate.name === name);
254
+ return binding && rest.length > 0
255
+ ? { binding, path: rest.join("/") }
256
+ : null;
257
+ }
258
+
259
+ function materializationRequired(name: string, path: string, bytes?: number) {
260
+ return result({
261
+ status: "materialization_required",
262
+ name,
263
+ path,
264
+ ...(bytes === undefined ? {} : { bytes }),
265
+ next: "tools.materialize_skill_resource",
266
+ });
267
+ }
268
+
269
+ type ScriptOutputFile = {
270
+ path?: unknown;
271
+ content?: unknown;
272
+ encoding?: unknown;
273
+ mimeType?: unknown;
274
+ };
275
+
276
+ function scratchSegment(value: string): string {
277
+ const segment = value.replace(/[^a-zA-Z0-9._-]+/g, "_");
278
+ return !segment || segment === "." || segment === ".." ? "_" : segment;
279
+ }
280
+
281
+ function outputRelativePath(value: unknown): string {
282
+ const path = String(value ?? "output.txt").replace(/^\/?output\//, "");
283
+ const parts = path.split("/");
284
+ if (parts.some((part) => !part || part === "." || part === "..")) {
285
+ throw new Error(`Invalid Skill output path: ${path}`);
286
+ }
287
+ return parts.join("/");
288
+ }
289
+
290
+ async function projectSkillScriptResult(
291
+ details: unknown,
292
+ input: unknown,
293
+ toolCallId: string,
294
+ workspace: WorkspacePort | undefined,
295
+ signal: AbortSignal | undefined,
296
+ ): Promise<unknown> {
297
+ const record = details !== null && typeof details === "object"
298
+ ? details as Record<string, unknown>
299
+ : null;
300
+ const outputFiles = Array.isArray(record?.outputFiles)
301
+ ? record.outputFiles as ScriptOutputFile[]
302
+ : [];
303
+ const target = input as { name?: unknown };
304
+ const outputs: Array<{ path: string; bytes: number; mediaType?: string }> = [];
305
+ if (outputFiles.length > 0 && !workspace) {
306
+ throw new Error("Skill output files require Workspace access");
307
+ }
308
+ for (const file of outputFiles) {
309
+ signal?.throwIfAborted();
310
+ const relative = outputRelativePath(file.path);
311
+ const destination = `${SKILL_SCRIPT_OUTPUT_ROOT}/${scratchSegment(String(target.name ?? "skill"))}/${scratchSegment(toolCallId)}/${relative}`;
312
+ const content = String(file.content ?? "");
313
+ const binary = file.encoding === "base64";
314
+ const mediaType = typeof file.mimeType === "string"
315
+ ? file.mimeType
316
+ : binary ? "application/octet-stream" : "text/plain";
317
+ let bytes = new Blob([content]).size;
318
+ await serializeWorkspaceMutation(workspace!, destination, async () => {
319
+ const parent = destination.replace(/\/[^/]+$/, "");
320
+ await workspace!.mkdir(parent, { recursive: true });
321
+ if (!binary) {
322
+ await workspace!.writeFile(destination, content, mediaType);
323
+ return;
324
+ }
325
+ const decoded = resourceBytes({
326
+ path: relative,
327
+ kind: "file",
328
+ encoding: "base64",
329
+ content,
330
+ });
331
+ bytes = decoded.byteLength;
332
+ await workspace!.writeFileBytes(destination, decoded, mediaType);
333
+ });
334
+ outputs.push({ path: destination, bytes, mediaType });
335
+ }
336
+ const scriptResult = record && "result" in record
337
+ ? record.result
338
+ : record && ("outputFiles" in record || "logs" in record)
339
+ ? Object.fromEntries(Object.entries(record).filter(([key]) =>
340
+ key !== "outputFiles" && key !== "logs"
341
+ ))
342
+ : details;
343
+ const logs = Array.isArray(record?.logs)
344
+ ? record.logs.map(String)
345
+ : [];
346
+ return {
347
+ result: truncateResult(scriptResult),
348
+ ...(logs.length > 0
349
+ ? { logs: [truncateResponse(logs.join("\n"))] }
350
+ : {}),
351
+ ...(outputs.length > 0 ? { outputs } : {}),
352
+ };
353
+ }
354
+
196
355
  function resourceBytes(resource: SkillResource): Uint8Array {
197
356
  if ((resource.encoding ?? "text") === "text") {
198
357
  return new TextEncoder().encode(resource.content);
@@ -211,95 +370,44 @@ function materializeSkillResourceTool(
211
370
  ) {
212
371
  const names = bindings.map(({ name }) => name) as [string, ...string[]];
213
372
  const byName = new Map(bindings.map((binding) => [binding.name, binding]));
214
- const resourceInput = z.object({
215
- name: z.enum(names).describe("Activated Skill name"),
216
- path: z.string().min(1).describe("Bundled resource path listed by activate_skill"),
217
- destination: z.string().min(1).startsWith("/").describe(
218
- "Absolute destination path in the Workspace",
219
- ),
220
- });
221
373
  return tool({
222
374
  description:
223
- "Copy complete bundled Skill resources directly into the Workspace without returning their contents to the model. " +
375
+ "Copy a complete bundled Skill resource directly into the Workspace without returning its contents to the model. " +
224
376
  "Use this instead of read_skill_resource when a template, script, image, font, or other asset must become a Workspace file. " +
225
- "Pass every required item in the resources array; items are copied in order and failures are reported per item. " +
377
+ "When multiple resources are needed, copy them sequentially in one execute; never start one execute per resource. " +
226
378
  "The destination is created or overwritten, including parent directories.",
227
379
  inputSchema: z.object({
228
- resources: z.array(resourceInput).min(1).max(100),
380
+ name: z.enum(names).describe("Activated Skill name"),
381
+ path: z.string().min(1).describe("Bundled resource path listed by activate_skill"),
382
+ destination: z.string().min(1).describe("Absolute destination path in the Workspace"),
229
383
  }),
230
- execute: async ({ resources }, { abortSignal }) => {
384
+ execute: async ({ name, path, destination }, { abortSignal }) => {
231
385
  abortSignal?.throwIfAborted();
232
- const destinations = new Set<string>();
233
- for (const { destination } of resources) {
234
- if (destinations.has(destination)) {
235
- throw new Error(`Duplicate materialize destination: ${destination}`);
236
- }
237
- destinations.add(destination);
386
+ const source = byName.get(name)?.source;
387
+ if (!source?.readResource) {
388
+ throw new Error(`Skill \"${name}\" has no readable resources.`);
238
389
  }
390
+ const resource = await source.readResource(name, path);
391
+ if (!resource) throw new Error(`Resource not found: ${name}/${path}`);
392
+ abortSignal?.throwIfAborted();
393
+ const bytes = resourceBytes(resource);
239
394
 
240
- const results: Array<Record<string, unknown>> = [];
241
- for (const { name, path, destination } of resources) {
395
+ await serializeWorkspaceMutation(workspace, destination, async () => {
242
396
  abortSignal?.throwIfAborted();
243
- const source = byName.get(name)?.source;
244
- if (!source?.readResource) {
245
- results.push({
246
- status: "error",
247
- name,
248
- path,
249
- destination,
250
- error: `Skill \"${name}\" has no readable resources.`,
251
- });
252
- continue;
397
+ const parent = destination.replace(/\/[^/]+$/, "");
398
+ if (parent && parent !== "/") {
399
+ await workspace.mkdir(parent, { recursive: true });
253
400
  }
254
- try {
255
- const resource = await source.readResource(name, path);
256
- if (!resource) {
257
- results.push({
258
- status: "error",
259
- name,
260
- path,
261
- destination,
262
- error: `Resource not found: ${name}/${path}`,
263
- });
264
- continue;
265
- }
266
- abortSignal?.throwIfAborted();
267
- const bytes = resourceBytes(resource);
268
-
269
- await serializeWorkspaceMutation(workspace, destination, async () => {
270
- abortSignal?.throwIfAborted();
271
- const parent = destination.replace(/\/[^/]+$/, "");
272
- if (parent && parent !== "/") {
273
- await workspace.mkdir(parent, { recursive: true });
274
- }
275
- await workspace.writeFileBytes(destination, bytes, resource.mimeType);
276
- });
277
-
278
- results.push({
279
- status: "written",
280
- name,
281
- path: resource.path,
282
- destination,
283
- bytesWritten: bytes.byteLength,
284
- encoding: resource.encoding ?? "text",
285
- ...(resource.mimeType ? { mimeType: resource.mimeType } : {}),
286
- });
287
- } catch (error) {
288
- abortSignal?.throwIfAborted();
289
- results.push({
290
- status: "error",
291
- name,
292
- path,
293
- destination,
294
- error: "Resource materialization failed",
295
- });
296
- }
297
- }
401
+ await workspace.writeFileBytes(destination, bytes, resource.mimeType);
402
+ });
298
403
 
299
404
  return {
300
- written: results.filter(({ status }) => status === "written").length,
301
- failed: results.filter(({ status }) => status === "error").length,
302
- results,
405
+ name,
406
+ path: resource.path,
407
+ destination,
408
+ bytesWritten: bytes.byteLength,
409
+ encoding: resource.encoding ?? "text",
410
+ ...(resource.mimeType ? { mimeType: resource.mimeType } : {}),
303
411
  };
304
412
  },
305
413
  });
@@ -349,7 +457,7 @@ export async function skillPiToolCandidates(
349
457
  }
350
458
  if (name === "read_skill_resource") {
351
459
  const execute = adapted.execute;
352
- adapted.execute = (toolCallId, input, signal) => {
460
+ adapted.execute = async (toolCallId, input, signal) => {
353
461
  const target = input as { name?: unknown; path?: unknown };
354
462
  const parts = typeof target.path === "string"
355
463
  ? target.path.split("/")
@@ -360,26 +468,72 @@ export async function skillPiToolCandidates(
360
468
  parts.length === 2 &&
361
469
  parts[1] === "SKILL.md")
362
470
  ) {
363
- return Promise.resolve({
471
+ return {
364
472
  content: [{ type: "text", text: SKILL_ENTRY_READ_GUIDANCE }],
365
473
  details: SKILL_ENTRY_READ_GUIDANCE,
366
- });
474
+ };
475
+ }
476
+ const resolved = resourceTarget(bindings, input);
477
+ if (resolved) {
478
+ const skill = await resolved.binding.source
479
+ .load(resolved.binding.name)
480
+ .catch(() => null);
481
+ const descriptor = skill?.resources?.find(({ path }) =>
482
+ path === resolved.path
483
+ );
484
+ if (
485
+ typeof descriptor?.size === "number" &&
486
+ descriptor.size > STORAGE_LEAF_MAX_CHARS
487
+ ) {
488
+ return materializationRequired(
489
+ resolved.binding.name,
490
+ resolved.path,
491
+ descriptor.size,
492
+ );
493
+ }
494
+ }
495
+ const output = await execute(toolCallId, input, signal);
496
+ if (
497
+ resolved &&
498
+ typeof output.details === "string" &&
499
+ output.details.length > STORAGE_LEAF_MAX_CHARS
500
+ ) {
501
+ return materializationRequired(
502
+ resolved.binding.name,
503
+ resolved.path,
504
+ );
367
505
  }
368
- return execute(toolCallId, input, signal);
506
+ return output;
507
+ };
508
+ }
509
+ if (name === "run_skill_script") {
510
+ adapted.description +=
511
+ " Large artifacts must be written directly to Workspace, not returned or logged; scratch outputs are materialized to Workspace and returned only as file references.";
512
+ adapted.prepareArguments = normalizeRunSkillScriptArguments;
513
+ const execute = adapted.execute;
514
+ adapted.execute = async (toolCallId, input, signal) => {
515
+ const output = await execute(toolCallId, input, signal);
516
+ return result(await projectSkillScriptResult(
517
+ output.details,
518
+ input,
519
+ toolCallId,
520
+ options.workspace,
521
+ signal,
522
+ ));
369
523
  };
370
524
  }
371
525
  return {
372
526
  owner: "runtime-skill",
373
- requiredExecutionLevel:
374
- name === "run_skill_script" || name === "materialize_skill_resource"
375
- ? "high"
376
- : "safe",
527
+ requiredExecutionLevel: "safe",
377
528
  // activate_skill 返回的是 Skill 指令本身,是模型接下来所有动作的依据。把它外置
378
529
  // 成文件,模型手里就只剩一个路径,必须再取一次才能知道该做什么 —— 而典型
379
530
  // SKILL.md 正好落在会触发外置的区间。指令必须当场到手。
380
531
  ...(name === "activate_skill"
381
532
  ? { outputBudget: { kind: "structure" as const } }
382
533
  : {}),
534
+ ...(name === "activate_skill"
535
+ ? { exposureMode: "direct" as const }
536
+ : { exposureMode: "codemode" as const }),
383
537
  tool: adapted,
384
538
  };
385
539
  });
@@ -180,6 +180,7 @@ export function subagentPiToolCandidates(
180
180
  };
181
181
  return {
182
182
  owner: `subagent:${type.name}`,
183
+ exposureMode: "direct",
183
184
  requiredExecutionLevel: "safe",
184
185
  tool,
185
186
  };
@@ -262,6 +263,7 @@ export function subagentPiToolCandidates(
262
263
  };
263
264
  candidates.push({
264
265
  owner: "subagent:background",
266
+ exposureMode: "direct",
265
267
  requiredExecutionLevel: "low",
266
268
  summary: "Dispatch a background sub-agent",
267
269
  source: "action",
@@ -4,7 +4,7 @@ import type { PiToolCandidate } from "./compiler";
4
4
 
5
5
  export const GET_TIME_TOOL_NAME = "get_time";
6
6
 
7
- const parameters = Type.Object({});
7
+ const parameters = Type.Object({}, { additionalProperties: false });
8
8
 
9
9
  /** Product-neutral current-time Tool for Host registries. */
10
10
  export function getTimePiToolCandidate(owner = "utilities"): PiToolCandidate {
@@ -51,7 +51,7 @@ export const webFetchParameters = Type.Object({
51
51
  "默认 false(保留位,当前实现总是返回原始内容)。",
52
52
  }),
53
53
  ),
54
- });
54
+ }, { additionalProperties: false });
55
55
 
56
56
  /** webFetch 工具调用的结构化 details,随 AgentToolResult 一起持久化。 */
57
57
  export interface WebFetchDetails {
@@ -18,7 +18,7 @@ export const webSearchParameters = Type.Object({
18
18
  description: "Additional URLs to analyze along with search (up to 20)",
19
19
  maxItems: 20,
20
20
  })),
21
- });
21
+ }, { additionalProperties: false });
22
22
 
23
23
  export interface WebSearchDetails extends Omit<WebSearchResult, "text"> {
24
24
  resultCount: number;
@@ -119,6 +119,7 @@ export function webSearchPiToolCandidate(
119
119
  };
120
120
  return {
121
121
  owner: "runtime-base",
122
+ exposureMode: "direct",
122
123
  requiredExecutionLevel: "safe",
123
124
  source: "action",
124
125
  tool,
@@ -14,7 +14,7 @@ const parameters = Type.Object({
14
14
  ], {
15
15
  description: "Use previous for the revision immediately before the current one.",
16
16
  }),
17
- });
17
+ }, { additionalProperties: false });
18
18
 
19
19
  function result<T>(details: T): AgentToolResult<T> {
20
20
  return {
@@ -43,6 +43,7 @@ export function workspaceRevisionPiToolCandidate(
43
43
  };
44
44
  return {
45
45
  owner: "workspace",
46
+ exposureMode: "direct",
46
47
  tool,
47
48
  requiredExecutionLevel: "safe",
48
49
  alwaysRequiresApproval: true,
@@ -1,5 +1,4 @@
1
1
  import {
2
- createBashTool,
3
2
  createDeleteTool,
4
3
  createEditTool,
5
4
  createFindTool,
@@ -68,7 +67,7 @@ const workspaceReadParameters = Type.Object({
68
67
  minimum: 1,
69
68
  description: "Maximum lines to return; a page is capped by size regardless",
70
69
  })),
71
- });
70
+ }, { additionalProperties: false });
72
71
  const workspaceEditParameters = Type.Object({
73
72
  path: Type.String({
74
73
  minLength: 1,
@@ -80,7 +79,7 @@ const workspaceEditParameters = Type.Object({
80
79
  "Exact existing text to replace. Include enough surrounding context to match one location.",
81
80
  }),
82
81
  new_string: Type.String({ description: "Replacement text." }),
83
- });
82
+ }, { additionalProperties: false });
84
83
 
85
84
  /**
86
85
  * 单页 read 返回的字符上限。
@@ -176,7 +175,7 @@ function pageReadResult(
176
175
  const span = `lines ${fromLine}-${toLine} of ${totalLines ?? toLine}`;
177
176
  const lossyNote = lossy
178
177
  ? " Some lines were longer than 2000 chars and are cut short; " +
179
- "paging cannot recover them — use grep or bash on this path instead."
178
+ "paging cannot recover them — use grep on this path instead."
180
179
  : "";
181
180
  const marker = fromLine === 1 && eof && !lossy
182
181
  ? ""
@@ -399,12 +398,6 @@ export function workspacePiToolCandidates(
399
398
  },
400
399
  };
401
400
 
402
- const bash = aiToolToPi(
403
- "bash",
404
- createBashTool({ ops: workspace }),
405
- { label: "Workspace Bash" },
406
- );
407
-
408
401
  return [
409
402
  {
410
403
  owner: "workspace",
@@ -420,13 +413,6 @@ export function workspacePiToolCandidates(
420
413
  requiredExecutionLevel: "safe" as const,
421
414
  tool,
422
415
  })),
423
- {
424
- owner: "workspace",
425
- requiredExecutionLevel: "high" as const,
426
- summary: "Run a Bash script over Workspace files",
427
- source: "action" as const,
428
- tool: bash,
429
- },
430
416
  ];
431
417
  }
432
418
 
@@ -460,15 +446,18 @@ const sandboxExecParameters = Type.Object({
460
446
  description: "Maximum command runtime in milliseconds, from 1 to 60000.",
461
447
  }),
462
448
  ),
463
- });
464
- const sandboxStartParameters = Type.Object(sandboxCommandParameters);
449
+ }, { additionalProperties: false });
450
+ const sandboxStartParameters = Type.Object(
451
+ sandboxCommandParameters,
452
+ { additionalProperties: false },
453
+ );
465
454
  const sandboxProcessParameters = Type.Object({
466
455
  id: Type.String({
467
456
  minLength: 1,
468
457
  maxLength: 128,
469
458
  description: "Process ID returned by sandbox_start_process.",
470
459
  }),
471
- });
460
+ }, { additionalProperties: false });
472
461
  const sandboxPublishParameters = Type.Object({
473
462
  paths: Type.Array(Type.String({
474
463
  minLength: 1,
@@ -479,7 +468,7 @@ const sandboxPublishParameters = Type.Object({
479
468
  maxItems: 100,
480
469
  description: "Files to copy from the temporary Sandbox into the persistent Workspace.",
481
470
  }),
482
- });
471
+ }, { additionalProperties: false });
483
472
 
484
473
  /**
485
474
  * 为现有 RuntimeSandboxPort 创建 Pi 命令、进程和文件发布工具。
@@ -672,6 +672,9 @@ export function defineRuntimeAgent<
672
672
  ...(assembly.bindings?.workspace
673
673
  ? { workspace: assembly.bindings.workspace }
674
674
  : {}),
675
+ ...(assembly.bindings?.codeExecution
676
+ ? { codeExecution: assembly.bindings.codeExecution }
677
+ : {}),
675
678
  },
676
679
  memoryProfile: {
677
680
  enabled: false,