@capybara-hq/kernel 0.1.0-rc.8 → 0.1.0-rc.9

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 (110) hide show
  1. package/dist/background/finish.js +2 -1
  2. package/dist/background/launch.d.ts +13 -7
  3. package/dist/background/launch.js +15 -8
  4. package/dist/background/poll.job.d.ts +1 -1
  5. package/dist/background/poll.job.js +8 -3
  6. package/dist/background/session.d.ts +2 -2
  7. package/dist/background/session.js +3 -2
  8. package/dist/background/status.d.ts +1 -1
  9. package/dist/background/status.js +1 -1
  10. package/dist/background/stop.js +4 -3
  11. package/dist/background/store.d.ts +6 -1
  12. package/dist/background/store.js +5 -1
  13. package/dist/background/tasks.service.d.ts +1 -1
  14. package/dist/background/tasks.service.js +4 -4
  15. package/dist/catalog/marketplaces.service.d.ts +1 -1
  16. package/dist/config/docs.js +2 -0
  17. package/dist/config/schema.d.ts +7 -0
  18. package/dist/config/schema.js +45 -0
  19. package/dist/conversations/event-log.d.ts +4 -3
  20. package/dist/conversations/event-log.js +4 -3
  21. package/dist/conversations/events.d.ts +9 -0
  22. package/dist/conversations/events.js +14 -0
  23. package/dist/db/kernel-schema.d.ts +26 -1
  24. package/dist/devices/device-auth.controller.d.ts +42 -0
  25. package/dist/devices/device-auth.controller.js +116 -0
  26. package/dist/devices/device-auth.d.ts +119 -0
  27. package/dist/devices/device-auth.js +403 -0
  28. package/dist/devices/device-auth.module.d.ts +7 -0
  29. package/dist/devices/device-auth.module.js +25 -0
  30. package/dist/devices/device-gateway.d.ts +128 -0
  31. package/dist/devices/device-gateway.js +530 -0
  32. package/dist/devices/device-provider.d.ts +89 -0
  33. package/dist/devices/device-provider.js +151 -0
  34. package/dist/devices/device-routes.d.ts +36 -0
  35. package/dist/devices/device-routes.js +90 -0
  36. package/dist/devices/devices.module.d.ts +16 -0
  37. package/dist/devices/devices.module.js +44 -0
  38. package/dist/devices/dto.d.ts +66 -0
  39. package/dist/devices/dto.js +44 -0
  40. package/dist/devices/keys.d.ts +25 -0
  41. package/dist/devices/keys.js +25 -0
  42. package/dist/devices/machines.d.ts +106 -0
  43. package/dist/devices/machines.js +301 -0
  44. package/dist/devices/relay.d.ts +100 -0
  45. package/dist/devices/relay.js +373 -0
  46. package/dist/devices/targets.d.ts +28 -0
  47. package/dist/devices/targets.js +73 -0
  48. package/dist/environments/access.d.ts +11 -6
  49. package/dist/environments/access.js +23 -8
  50. package/dist/environments/conversation-environments.d.ts +20 -6
  51. package/dist/environments/conversation-environments.js +70 -9
  52. package/dist/environments/environments.controller.d.ts +4 -1
  53. package/dist/environments/environments.controller.js +15 -1
  54. package/dist/environments/environments.module.js +3 -1
  55. package/dist/environments/environments.service.d.ts +54 -7
  56. package/dist/environments/environments.service.js +104 -20
  57. package/dist/environments/legacy-cleanup.job.d.ts +1 -1
  58. package/dist/environments/legacy-cleanup.job.js +4 -4
  59. package/dist/errors.d.ts +24 -0
  60. package/dist/errors.js +24 -0
  61. package/dist/evals/recorded-tools.d.ts +9 -0
  62. package/dist/evals/recorded-tools.js +27 -0
  63. package/dist/http/api.module.js +4 -0
  64. package/dist/http/gateway-routes.d.ts +7 -3
  65. package/dist/http/gateway-routes.js +6 -3
  66. package/dist/infra/config/kernel-config.service.js +2 -0
  67. package/dist/infra/throttler/named-throttlers.d.ts +5 -0
  68. package/dist/infra/throttler/named-throttlers.js +38 -0
  69. package/dist/kernel.module.js +3 -0
  70. package/dist/lifecycle/kernel-lifecycle.js +5 -0
  71. package/dist/platform/exclusions.js +33 -0
  72. package/dist/platform/ops/environments.ops.js +21 -3
  73. package/dist/roles/gateway-role.d.ts +14 -3
  74. package/dist/roles/gateway-role.js +25 -5
  75. package/dist/roles/gateway.module.d.ts +2 -1
  76. package/dist/roles/gateway.module.js +6 -3
  77. package/dist/runtime/context/build.d.ts +2 -0
  78. package/dist/runtime/context/build.js +14 -0
  79. package/dist/runtime/context/compaction.js +1 -0
  80. package/dist/runtime/context/environment-paths.d.ts +40 -0
  81. package/dist/runtime/context/environment-paths.js +48 -0
  82. package/dist/runtime/model-call.js +2 -0
  83. package/dist/sandbox/providers.d.ts +29 -7
  84. package/dist/sandbox/providers.js +43 -8
  85. package/dist/sandbox/sandbox.service.d.ts +70 -18
  86. package/dist/sandbox/sandbox.service.js +176 -59
  87. package/dist/tools/builtins/sandbox/bash.d.ts +2 -2
  88. package/dist/tools/builtins/sandbox/bash.js +39 -27
  89. package/dist/tools/builtins/sandbox/common.d.ts +60 -25
  90. package/dist/tools/builtins/sandbox/common.js +121 -30
  91. package/dist/tools/builtins/sandbox/deliver.js +9 -6
  92. package/dist/tools/builtins/sandbox/edit.js +40 -37
  93. package/dist/tools/builtins/sandbox/glob.js +13 -8
  94. package/dist/tools/builtins/sandbox/grep.js +10 -6
  95. package/dist/tools/builtins/sandbox/index.d.ts +6 -4
  96. package/dist/tools/builtins/sandbox/index.js +6 -4
  97. package/dist/tools/builtins/sandbox/read.js +9 -6
  98. package/dist/tools/builtins/sandbox/task-output.d.ts +7 -3
  99. package/dist/tools/builtins/sandbox/task-output.js +14 -10
  100. package/dist/tools/builtins/sandbox/task-stop.js +3 -3
  101. package/dist/tools/builtins/sandbox/write.js +33 -30
  102. package/dist/tools/builtins/skill-load.d.ts +8 -5
  103. package/dist/tools/builtins/skill-load.js +21 -15
  104. package/dist/tools/builtins/skill-write.js +2 -2
  105. package/dist/tools/pipeline.js +2 -0
  106. package/dist/tools/registry.d.ts +21 -4
  107. package/dist/tools/registry.js +10 -2
  108. package/package.json +5 -2
  109. package/src/migrations/0093_connected_environments.sql +37 -0
  110. package/src/migrations/0094_environment_paths_idx.sql +7 -0
@@ -1,5 +1,5 @@
1
1
  import { ToolError } from "@capybara-hq/plugin-sdk";
2
- import { checkPath, clip, display, inSandbox, isSyncMarkerEntry, sandboxBaseTimeoutMs, searchTimeoutMs, unavailableWhenNoSandbox, withNotices, } from "./common.js";
2
+ import { checkPath, checkPathShape, clip, display, inSandbox, sandboxBaseTimeoutMs, searchTimeoutMs, syncMarkerMatcher, unavailableWhenNoSandbox, withNotices, } from "./common.js";
3
3
  import { compileGlob, GlobPatternError } from "./glob-match.js";
4
4
  /** 最多列出的文件数。 */
5
5
  const MAX_RESULTS = 1000;
@@ -11,7 +11,7 @@ const SPEC = {
11
11
  keyArgs: ["pattern"],
12
12
  description: "Find files by name pattern in this conversation's sandbox. Use it to list files and to " +
13
13
  'locate files by name, e.g. pattern "**/*.py" for all Python files. The pattern is matched ' +
14
- "against paths relative to path (default /workspace): * matches within one directory, ** " +
14
+ "against paths relative to path (default: the workspace directory): * matches within one directory, ** " +
15
15
  "matches across directories, ? matches one character, {a,b} matches either. Returns absolute " +
16
16
  "paths sorted by name, up to 1000. To search file contents, use Grep.",
17
17
  inputSchema: {
@@ -25,7 +25,7 @@ const SPEC = {
25
25
  path: {
26
26
  type: "string",
27
27
  minLength: 1,
28
- description: "Absolute directory to search in. Default /workspace.",
28
+ description: "Absolute directory to search in. Default: the workspace directory.",
29
29
  },
30
30
  },
31
31
  required: ["pattern"],
@@ -34,7 +34,7 @@ const SPEC = {
34
34
  annotations: { readOnlyHint: true },
35
35
  };
36
36
  const ABSOLUTE_PATTERN = 'The pattern must be relative to path; pass the directory in path and a relative pattern such as "**/*.py".';
37
- const notDirectory = (path) => `${clip(path, 200)} is not a directory; pass an existing directory in path (default /workspace).`;
37
+ const notDirectory = (path, workspace) => `${clip(path, 200)} is not a directory; pass an existing directory in path (default ${workspace}).`;
38
38
  const FIND_FAILED = "Listing files did not finish in time; pass a narrower path or pattern and retry.";
39
39
  /** 内置工具 `Glob`:按文件名模式找文件(只读)。 */
40
40
  export function sandboxGlobTool(deps) {
@@ -48,7 +48,6 @@ export function sandboxGlobTool(deps) {
48
48
  const { pattern } = input;
49
49
  if (pattern.startsWith("/"))
50
50
  throw new ToolError(ABSOLUTE_PATTERN);
51
- const dir = checkPath(input.path ?? "/workspace");
52
51
  let matches;
53
52
  try {
54
53
  matches = compileGlob(pattern);
@@ -58,10 +57,15 @@ export function sandboxGlobTool(deps) {
58
57
  throw e;
59
58
  throw new ToolError(`Invalid glob pattern: ${e.message}; pass a simpler pattern (fewer wildcards and braces) and retry.`);
60
59
  }
61
- const { value: files, notices } = await inSandbox(deps, ctx, async ({ h, p, signal }) => {
60
+ const given = input.path;
61
+ if (given !== undefined)
62
+ checkPathShape(given);
63
+ let dir = "";
64
+ const { value: files, notices } = await inSandbox(deps, ctx, async ({ h, roots, p, signal }) => {
65
+ dir = checkPath(input.path ?? roots.workspace, roots);
62
66
  const st = await p.stat(h, dir, signal);
63
67
  if (!st?.isDir)
64
- throw new ToolError(notDirectory(dir));
68
+ throw new ToolError(notDirectory(dir, roots.workspace));
65
69
  const r = await p.exec(h, {
66
70
  command: "find . -type f -print0",
67
71
  cwd: dir,
@@ -77,12 +81,13 @@ export function sandboxGlobTool(deps) {
77
81
  if (r.truncated)
78
82
  names.pop();
79
83
  const base = dir === "/" ? "" : dir;
84
+ const isMarker = syncMarkerMatcher(roots);
80
85
  return (names
81
86
  .filter((l) => l.startsWith("./"))
82
87
  .map((l) => l.slice(2))
83
88
  .filter((rel) => matches(rel))
84
89
  .map((rel) => `${base}/${rel}`)
85
- .filter((abs) => !isSyncMarkerEntry(abs))
90
+ .filter((abs) => !isMarker(abs))
86
91
  .sort()
87
92
  // 文件名里的换行显示为 `\n`(与 Grep 一致),一行一个文件
88
93
  .map((abs) => abs.replace(/\n/g, "\\n")));
@@ -1,5 +1,5 @@
1
1
  import { ToolError } from "@capybara-hq/plugin-sdk";
2
- import { checkPath, clip, display, inSandbox, isSyncMarkerEntry, sandboxBaseTimeoutMs, searchTimeoutMs, shellQuote, unavailableWhenNoSandbox, withNotices, } from "./common.js";
2
+ import { checkPath, checkPathShape, clip, display, inSandbox, sandboxBaseTimeoutMs, searchTimeoutMs, shellQuote, syncMarkerMatcher, unavailableWhenNoSandbox, withNotices, } from "./common.js";
3
3
  /** The most output entries (lines, files or counts) returned in one call, whatever `head_limit` says. */
4
4
  const MAX_MATCHES = 500;
5
5
  /** `head_limit` when the model gives none. */
@@ -15,7 +15,7 @@ const SPEC = {
15
15
  keyArgs: ["pattern", "path"],
16
16
  description: "Search file contents in this conversation's sandbox with a regular expression (POSIX " +
17
17
  "extended, as grep -E). Use it to find where something is defined or mentioned. Searches path " +
18
- "(a file, or a directory recursively; default /workspace), skipping binary files; glob limits " +
18
+ "(a file, or a directory recursively; default: the workspace directory), skipping binary files; glob limits " +
19
19
  'the files searched by name, e.g. "*.py". output_mode picks what comes back: ' +
20
20
  '"files_with_matches" (default) lists the paths of files that match, "content" returns the ' +
21
21
  "matching lines as <path>:<line>:<text> (with -A, -B or -C lines of context around them), " +
@@ -34,7 +34,7 @@ const SPEC = {
34
34
  path: {
35
35
  type: "string",
36
36
  minLength: 1,
37
- description: "Absolute file or directory to search. Default /workspace.",
37
+ description: "Absolute file or directory to search. Default: the workspace directory.",
38
38
  },
39
39
  glob: {
40
40
  type: "string",
@@ -160,8 +160,11 @@ export function sandboxGrepTool(deps) {
160
160
  const { pattern, glob } = g;
161
161
  const mode = g.output_mode ?? "files_with_matches";
162
162
  const numbered = g["-n"] ?? true;
163
- const path = checkPath(g.path ?? "/workspace");
164
- const { value, notices } = await inSandbox(deps, ctx, async ({ h, p, signal }) => {
163
+ if (g.path !== undefined)
164
+ checkPathShape(g.path);
165
+ let path = "";
166
+ const { value, notices } = await inSandbox(deps, ctx, async ({ h, roots, p, signal }) => {
167
+ path = checkPath(g.path ?? roots.workspace, roots);
165
168
  const st = await p.stat(h, path, signal);
166
169
  if (st === null)
167
170
  throw new ToolError(notFound(path));
@@ -207,7 +210,8 @@ export function sandboxGrepTool(deps) {
207
210
  : mode === "count"
208
211
  ? parseCounts(r.stdout, base)
209
212
  : parseGrepNull(r.stdout, base, numbered);
210
- const entries = all.filter((l) => !isSyncMarkerEntry(l));
213
+ const isMarker = syncMarkerMatcher(roots);
214
+ const entries = all.filter((l) => !isMarker(l));
211
215
  // 退出码 2 = 有错误(如个别文件不可读);有输出时照常返回(可能为空:只匹配到隐藏的标记文件)并附上说明
212
216
  if (r.exitCode !== 0 && all.length === 0)
213
217
  throw new ToolError(failed(r.stderr));
@@ -2,12 +2,14 @@ import type { BuiltinTool } from "../../registry.js";
2
2
  import type { SandboxToolDeps } from "./common.js";
3
3
  export { SANDBOX_ASK_TOOL_NAMES, SANDBOX_TOOL_NAMES } from "../../builtin-names.js";
4
4
  export { BASH_SPILL_CHARS, formatExec } from "./bash.js";
5
- export { assignAttachmentNames, RUN_DEADLINE_MARGIN_MS, runTimeLeftMs, SANDBOX_RESTARTED_NOTICE, SANDBOX_SETUP_MARGIN_MS, SANDBOX_STARTED_NOTICE, SANDBOX_UNAVAILABLE_REASON, type SandboxToolDeps, sandboxBaseTimeoutMs, searchTimeoutMs, shellQuote, } from "./common.js";
5
+ export { assignAttachmentNames, RUN_DEADLINE_MARGIN_MS, runTimeLeftMs, SANDBOX_RESTARTED_NOTICE, SANDBOX_SETUP_MARGIN_MS, SANDBOX_STARTED_NOTICE, SANDBOX_UNAVAILABLE_REASON, type SandboxToolDeps, sandboxBaseTimeoutMs, sandboxStartedNotice, sandboxUsable, searchTimeoutMs, shellQuote, } from "./common.js";
6
6
  export { compileGlob, GlobPatternError, MAX_GLOB_STARS } from "./glob-match.js";
7
7
  /**
8
- * 沙箱内置工具(M5 T8):bash / read / write / edit / glob / grep / deliver,都在本对话的沙箱上执行
9
- * (`SandboxService.withSandbox`),执行前同步附件到 `/workspace/attachments`。没有沙箱提供方时报不可用。
10
- * 默认 allow;组织设置 `sandboxToolPolicy` 为 ask 时除 read / glob / grep 外需要审批(见 policy.ts)。
8
+ * The sandbox builtins (M5 T8): Bash / Read / Write / Edit / Glob / Grep / Deliver, all run in the
9
+ * conversation's environment (`SandboxService.withSandbox`) after syncing attachments to
10
+ * `<workspace>/attachments` (the instance's roots, environments spec §13.1); unavailable without
11
+ * a provider for it. Allowed by default; with the org setting `sandboxToolPolicy` set to ask,
12
+ * every one but Read / Glob / Grep needs approval (see policy.ts).
11
13
  * `TaskOutput` and `TaskStop` read and stop background `Bash` tasks through
12
14
  * `SandboxService.ifRunning` (never creating or resuming a sandbox); always allowed.
13
15
  */
@@ -9,12 +9,14 @@ import { taskStopTool } from "./task-stop.js";
9
9
  import { sandboxWriteTool } from "./write.js";
10
10
  export { SANDBOX_ASK_TOOL_NAMES, SANDBOX_TOOL_NAMES } from "../../builtin-names.js";
11
11
  export { BASH_SPILL_CHARS, formatExec } from "./bash.js";
12
- export { assignAttachmentNames, RUN_DEADLINE_MARGIN_MS, runTimeLeftMs, SANDBOX_RESTARTED_NOTICE, SANDBOX_SETUP_MARGIN_MS, SANDBOX_STARTED_NOTICE, SANDBOX_UNAVAILABLE_REASON, sandboxBaseTimeoutMs, searchTimeoutMs, shellQuote, } from "./common.js";
12
+ export { assignAttachmentNames, RUN_DEADLINE_MARGIN_MS, runTimeLeftMs, SANDBOX_RESTARTED_NOTICE, SANDBOX_SETUP_MARGIN_MS, SANDBOX_STARTED_NOTICE, SANDBOX_UNAVAILABLE_REASON, sandboxBaseTimeoutMs, sandboxStartedNotice, sandboxUsable, searchTimeoutMs, shellQuote, } from "./common.js";
13
13
  export { compileGlob, GlobPatternError, MAX_GLOB_STARS } from "./glob-match.js";
14
14
  /**
15
- * 沙箱内置工具(M5 T8):bash / read / write / edit / glob / grep / deliver,都在本对话的沙箱上执行
16
- * (`SandboxService.withSandbox`),执行前同步附件到 `/workspace/attachments`。没有沙箱提供方时报不可用。
17
- * 默认 allow;组织设置 `sandboxToolPolicy` 为 ask 时除 read / glob / grep 外需要审批(见 policy.ts)。
15
+ * The sandbox builtins (M5 T8): Bash / Read / Write / Edit / Glob / Grep / Deliver, all run in the
16
+ * conversation's environment (`SandboxService.withSandbox`) after syncing attachments to
17
+ * `<workspace>/attachments` (the instance's roots, environments spec §13.1); unavailable without
18
+ * a provider for it. Allowed by default; with the org setting `sandboxToolPolicy` set to ask,
19
+ * every one but Read / Glob / Grep needs approval (see policy.ts).
18
20
  * `TaskOutput` and `TaskStop` read and stop background `Bash` tasks through
19
21
  * `SandboxService.ifRunning` (never creating or resuming a sandbox); always allowed.
20
22
  */
@@ -1,6 +1,6 @@
1
1
  import { ToolError } from "@capybara-hq/plugin-sdk";
2
2
  import { viewPath } from "../../view.js";
3
- import { checkPath, clip, display, inSandbox, looksBinary, sandboxBaseTimeoutMs, unavailableWhenNoSandbox, withNotices, } from "./common.js";
3
+ import { checkPath, checkPathShape, clip, display, inSandbox, looksBinary, sandboxBaseTimeoutMs, unavailableWhenNoSandbox, withNotices, } from "./common.js";
4
4
  import { normalizeSandboxPath, recordedFile } from "./file-state.js";
5
5
  /** 不给 `limit` 时一次读的行数。 */
6
6
  const DEFAULT_LIMIT = 2000;
@@ -13,8 +13,9 @@ const SPEC = {
13
13
  // Display only (the tool row; not in the fingerprint, never sent to the model).
14
14
  keyArgs: ["file_path"],
15
15
  description: "Read a text file from this conversation's sandbox, with line numbers. Use it to look at " +
16
- "source code, data files, logs and files the user attached (under /workspace/attachments/<conversation>/). " +
17
- "Paths are absolute, under /workspace, /skills or /tmp. Lines are numbered from 1; pass " +
16
+ "source code, data files, logs and files the user attached (under attachments/<conversation>/ in " +
17
+ "the workspace directory). Paths are absolute, under the workspace directory, the skills " +
18
+ "directory or the temporary files directory. Lines are numbered from 1; pass " +
18
19
  "offset and limit to read part of a long file. Binary files are not shown; use Bash " +
19
20
  "to inspect them. Do not use it to list directories; use Glob instead. Read a file before you " +
20
21
  "write it: Write and Edit fail on a file you have not read, or one changed since you read it.",
@@ -24,7 +25,7 @@ const SPEC = {
24
25
  file_path: {
25
26
  type: "string",
26
27
  minLength: 1,
27
- description: 'Absolute path of the file, e.g. "/workspace/src/main.py".',
28
+ description: "Absolute path of the file, e.g. src/main.py in the workspace directory.",
28
29
  },
29
30
  offset: {
30
31
  type: "integer",
@@ -81,8 +82,10 @@ export function sandboxReadTool(deps) {
81
82
  timeoutMs: () => sandboxBaseTimeoutMs(deps.config.get()),
82
83
  async run(input, ctx) {
83
84
  const { offset = 1, limit = DEFAULT_LIMIT } = input;
84
- const path = checkPath(input.file_path, "file_path");
85
- const { value, notices } = await inSandbox(deps, ctx, async ({ h, p, signal }) => {
85
+ checkPathShape(input.file_path, "file_path");
86
+ let path = "";
87
+ const { value, notices } = await inSandbox(deps, ctx, async ({ h, roots, p, signal }) => {
88
+ path = checkPath(input.file_path, roots, "file_path");
86
89
  const st = await p.stat(h, path, signal);
87
90
  if (st === null)
88
91
  throw new ToolError(notFound(path));
@@ -16,8 +16,12 @@ export interface TaskEnv {
16
16
  ifRunning: SandboxService["ifRunning"];
17
17
  environments: SandboxToolDeps["environments"];
18
18
  }
19
- /** The task tools' dependencies; throws the tool's error when they are missing. */
20
- export declare function taskEnv(deps: SandboxToolDeps): TaskEnv;
19
+ /**
20
+ * The task tools' dependencies; throws the tool's error when they are missing, or when the
21
+ * conversation's environment cannot run (no cloud provider, and not a registered `@device` one:
22
+ * ruling R8).
23
+ */
24
+ export declare function taskEnv(deps: SandboxToolDeps, ctx: BuiltinCallContext): Promise<TaskEnv>;
21
25
  /**
22
26
  * The task `id` when it belongs to the calling conversation; a task of another conversation (a
23
27
  * sub-agent's, when called from its parent) is not found, the same as an unknown id.
@@ -45,7 +49,7 @@ export declare const outputFile: (task: BackgroundTaskRow) => string;
45
49
  /** Why the output cannot be read: the task's sandbox instance is not running. */
46
50
  export declare function unavailableText(task: BackgroundTaskRow, r: IfRunningResult<unknown>): string;
47
51
  /**
48
- * The exec that checks and reads task `t` (relative to `/workspace`): for a running task first
52
+ * The exec that checks and reads task `t` (relative to the workspace root): for a running task first
49
53
  * its status line (the poller's, {@link pollScript}: liveness, then the exit file), then
50
54
  * `size <bytes>` and `out <base64>`, at most `cap` bytes of the output from byte `offset` on.
51
55
  * The file is never read whole: `tail -c +<offset+1> | head -c <cap>`. Without `read` only the
@@ -1,11 +1,11 @@
1
1
  import { setTimeout as sleep } from "node:timers/promises";
2
- import { isSandboxGoneError, ToolError, } from "@capybara-hq/plugin-sdk";
3
- import { TASK_ID_RE, TASKS_DIR } from "../../../background/launch.js";
2
+ import { isSandboxGoneError, rootsOf, ToolError, } from "@capybara-hq/plugin-sdk";
3
+ import { TASK_ID_RE, TASKS_DIR, taskOutputFile } from "../../../background/launch.js";
4
4
  import { formatElapsed, lostReasonText } from "../../../background/render.js";
5
5
  import { actingAs, SID_RE, taskActor } from "../../../background/session.js";
6
6
  import { parsePollOutput, polledAs, pollScript, } from "../../../background/status.js";
7
7
  import { notRunningAs, RECREATED } from "../../../background/stop.js";
8
- import { clip, display, mapSandboxError, noSandboxError, runTimeLeftMs, sandboxBaseTimeoutMs, sandboxConversationOf, unavailableWhenNoSandbox, } from "./common.js";
8
+ import { clip, display, mapSandboxError, noSandboxError, runTimeLeftMs, sandboxBaseTimeoutMs, sandboxConversationOf, sandboxUsable, unavailableWhenNoSandbox, } from "./common.js";
9
9
  /** How long `TaskOutput` waits with `block` when `timeout` is not given (ms). */
10
10
  export const TASK_OUTPUT_DEFAULT_TIMEOUT_MS = 30_000;
11
11
  /** The longest `TaskOutput` wait (ms). */
@@ -49,9 +49,13 @@ const SPEC = {
49
49
  additionalProperties: false,
50
50
  },
51
51
  };
52
- /** The task tools' dependencies; throws the tool's error when they are missing. */
53
- export function taskEnv(deps) {
54
- if (!deps.sandbox.available)
52
+ /**
53
+ * The task tools' dependencies; throws the tool's error when they are missing, or when the
54
+ * conversation's environment cannot run (no cloud provider, and not a registered `@device` one:
55
+ * ruling R8).
56
+ */
57
+ export async function taskEnv(deps, ctx) {
58
+ if (!(await sandboxUsable(deps, ctx.orgId, sandboxConversationOf(ctx))))
55
59
  throw noSandboxError();
56
60
  const { background, tasks } = deps;
57
61
  if (!background || !tasks || !deps.sandbox.ifRunning) {
@@ -122,7 +126,7 @@ export function statusLine(task, now = new Date()) {
122
126
  return `${s} after ${elapsed}`;
123
127
  }
124
128
  /** The absolute path of the task's output file, as the model sees it. */
125
- export const outputFile = (task) => `/workspace/${task.outputPath}`;
129
+ export const outputFile = (task) => taskOutputFile(task.outputPath);
126
130
  /** Why the output cannot be read: the task's sandbox instance is not running. */
127
131
  export function unavailableText(task, r) {
128
132
  if (r.kind === "not_running" && r.status === "suspended") {
@@ -132,7 +136,7 @@ export function unavailableText(task, r) {
132
136
  }
133
137
  // ---- reading a task ----
134
138
  /**
135
- * The exec that checks and reads task `t` (relative to `/workspace`): for a running task first
139
+ * The exec that checks and reads task `t` (relative to the workspace root): for a running task first
136
140
  * its status line (the poller's, {@link pollScript}: liveness, then the exit file), then
137
141
  * `size <bytes>` and `out <base64>`, at most `cap` bytes of the output from byte `offset` on.
138
142
  * The file is never read whole: `tail -c +<offset+1> | head -c <cap>`. Without `read` only the
@@ -189,7 +193,7 @@ async function look(env, task, read, signal) {
189
193
  return onTaskInstance(env, task, async (p, h, sig) => {
190
194
  const r = await p.exec(h, {
191
195
  command: script,
192
- cwd: "/workspace",
196
+ cwd: rootsOf(h).workspace,
193
197
  timeoutMs: READ_EXEC_TIMEOUT_MS,
194
198
  maxOutputBytes: maxBytes,
195
199
  signal: sig,
@@ -262,7 +266,7 @@ export function taskOutputTool(deps) {
262
266
  },
263
267
  async run(input, ctx) {
264
268
  const { task_id, block = true, timeout } = input;
265
- const env = taskEnv(deps);
269
+ const env = await taskEnv(deps, ctx);
266
270
  let task = await ownedTask(env, ctx, task_id);
267
271
  await recheckEnvironment(env, ctx);
268
272
  if (block && task.status === "running") {
@@ -1,4 +1,4 @@
1
- import { ToolError } from "@capybara-hq/plugin-sdk";
1
+ import { rootsOf, ToolError } from "@capybara-hq/plugin-sdk";
2
2
  import { decodeTail, tailCommand } from "../../../background/session.js";
3
3
  import { clip, display, mapSandboxError, sandboxBaseTimeoutMs, unavailableWhenNoSandbox, } from "./common.js";
4
4
  import { onTaskInstance, ownedTask, recheckEnvironment, statusLine, taskEnv, } from "./task-output.js";
@@ -33,7 +33,7 @@ async function lastLinesOf(env, task, signal) {
33
33
  const r = await onTaskInstance(env, task, async (p, h, sig) => {
34
34
  const res = await p.exec(h, {
35
35
  command: tailCommand(task.id),
36
- cwd: "/workspace",
36
+ cwd: rootsOf(h).workspace,
37
37
  timeoutMs: TAIL_EXEC_TIMEOUT_MS,
38
38
  maxOutputBytes: 64 * 1024,
39
39
  signal: sig,
@@ -82,7 +82,7 @@ export function taskStopTool(deps) {
82
82
  timeoutMs: () => sandboxBaseTimeoutMs(deps.config.get()),
83
83
  async run(input, ctx) {
84
84
  const { task_id } = input;
85
- const env = taskEnv(deps);
85
+ const env = await taskEnv(deps, ctx);
86
86
  const task = await ownedTask(env, ctx, task_id);
87
87
  await recheckEnvironment(env, ctx);
88
88
  if (task.status !== "running") {
@@ -1,6 +1,6 @@
1
1
  import { ToolError } from "@capybara-hq/plugin-sdk";
2
2
  import { lineDiff, TOOL_VIEW_DIFF_MAX_BYTES, viewPath } from "../../view.js";
3
- import { checkWritable, clip, display, inSandbox, looksBinary, sandboxBaseTimeoutMs, unavailableWhenNoSandbox, withNotices, withPathLease, } from "./common.js";
3
+ import { checkPathShape, checkWritable, clip, display, inSandbox, looksBinary, sandboxBaseTimeoutMs, unavailableWhenNoSandbox, withNotices, withPathLease, } from "./common.js";
4
4
  import { assertReadAndUnchanged, fileStateOf, normalizeSandboxPath, recordedFile, } from "./file-state.js";
5
5
  import { READ_MAX_BYTES } from "./read.js";
6
6
  const SPEC = {
@@ -9,8 +9,8 @@ const SPEC = {
9
9
  keyArgs: ["file_path"],
10
10
  description: "Write a text file in this conversation's sandbox, creating parent directories as needed. " +
11
11
  "Each call replaces the whole file (it is created if missing). Use it to create scripts, " +
12
- "source files and data files. The path is absolute, under /workspace or /tmp (/skills is " +
13
- "read-only). Read an existing file first; overwriting a file you have not read, or one changed " +
12
+ "source files and data files. The path is absolute, under the workspace directory or the " +
13
+ "temporary files directory (the skills directory is read-only). Read an existing file first; overwriting a file you have not read, or one changed " +
14
14
  "since you read it, fails. To change part of an existing file, use Edit instead; to give a file to " +
15
15
  "the user, use Deliver.",
16
16
  inputSchema: {
@@ -19,7 +19,7 @@ const SPEC = {
19
19
  file_path: {
20
20
  type: "string",
21
21
  minLength: 1,
22
- description: 'Absolute path of the file, e.g. "/workspace/report.md".',
22
+ description: "Absolute path of the file, e.g. report.md in the workspace directory.",
23
23
  },
24
24
  content: { type: "string", description: "The complete new content of the file." },
25
25
  },
@@ -43,33 +43,36 @@ export function sandboxWriteTool(deps) {
43
43
  unavailable: unavailableWhenNoSandbox(deps),
44
44
  timeoutMs: () => sandboxBaseTimeoutMs(deps.config.get()),
45
45
  async run(input, ctx) {
46
- const path = checkWritable(input.file_path);
47
46
  const data = new TextEncoder().encode(input.content);
48
- const { value, notices } = await inSandbox(deps, ctx, ({ environmentId, h, p, signal }) =>
49
- // stat, read the previous content, write: one holder at a time per file
50
- withPathLease(deps, { environmentId }, path, signal, async (signal) => {
51
- const st = await p.stat(h, path, signal);
52
- if (st?.isDir)
53
- throw new ToolError(isDirectory(path));
54
- // the previous content: for the read-before-write check, and the diff on the row (display
55
- // only: a file too large or binary to diff just leaves the view without hunks)
56
- let previous = "";
57
- if (st !== null) {
58
- const got = await p.readFile(h, path, { maxBytes: READ_MAX_BYTES, signal });
59
- if (got === null)
60
- throw new ToolError(vanished(path));
61
- await assertReadAndUnchanged(deps, ctx.conversationId, path, fileStateOf(got.data, st.size));
62
- previous =
63
- st.size > PREVIOUS_MAX_BYTES ||
64
- got.truncated ||
65
- looksBinary(got.data) ||
66
- got.data.includes(0)
67
- ? null
68
- : new TextDecoder().decode(got.data);
69
- }
70
- await p.writeFile(h, path, data, signal);
71
- return { existed: st !== null, previous };
72
- }));
47
+ checkPathShape(input.file_path, "file_path");
48
+ let path = "";
49
+ const { value, notices } = await inSandbox(deps, ctx, ({ environmentId, h, roots, p, signal }) => {
50
+ path = checkWritable(input.file_path, roots);
51
+ // stat, read the previous content, write: one holder at a time per file
52
+ return withPathLease(deps, { environmentId }, path, signal, async (signal) => {
53
+ const st = await p.stat(h, path, signal);
54
+ if (st?.isDir)
55
+ throw new ToolError(isDirectory(path));
56
+ // the previous content: for the read-before-write check, and the diff on the row (display
57
+ // only: a file too large or binary to diff just leaves the view without hunks)
58
+ let previous = "";
59
+ if (st !== null) {
60
+ const got = await p.readFile(h, path, { maxBytes: READ_MAX_BYTES, signal });
61
+ if (got === null)
62
+ throw new ToolError(vanished(path));
63
+ await assertReadAndUnchanged(deps, ctx.conversationId, path, fileStateOf(got.data, st.size));
64
+ previous =
65
+ st.size > PREVIOUS_MAX_BYTES ||
66
+ got.truncated ||
67
+ looksBinary(got.data) ||
68
+ got.data.includes(0)
69
+ ? null
70
+ : new TextDecoder().decode(got.data);
71
+ }
72
+ await p.writeFile(h, path, data, signal);
73
+ return { existed: st !== null, previous };
74
+ });
75
+ });
73
76
  const { existed, previous } = value;
74
77
  const text = `${existed ? "Overwrote" : "Created"} ${path} (${data.byteLength} bytes).`;
75
78
  return {
@@ -8,14 +8,14 @@ import { type SandboxToolDeps } from "./sandbox/common.js";
8
8
  * Builtin tool `Skill` (M5 T14, ruling M17): resolves the name against the latest skills catalog
9
9
  * snapshot in the log to an id and version, and returns that version's SKILL.md body and file list
10
10
  * (the result names the version). A skill with other files is copied whole into the conversation's
11
- * environment at `/skills/<name>@<digest>/` (the provider writes as root, commands read only; the
11
+ * environment at `<skills root>/<name>@<digest>/` (the instance's skills root, `/skills` by default; the provider writes as root, commands read only; the
12
12
  * digest is {@link skillDigest} of the version's file manifest, so versions of one skill coexist in
13
13
  * the shared file system). Without a sandbox provider only the body is given, with a note that
14
14
  * scripts are unavailable.
15
15
  *
16
16
  * - Every file read from storage is checked against the manifest's sha256; a mismatch is refused.
17
17
  * - The copy is recorded in a kernel-controlled marker, one per skill version
18
- * ({@link skillMarkerPath}: `/skills/.capybara/<name>@<digest>.json`, commands cannot change it):
18
+ * ({@link skillMarkerPath}: `<skills root>/.capybara/<name>@<digest>.json`, commands cannot change it):
19
19
  * `{ digest, files }`. A present marker with the same digest skips the copy.
20
20
  * - An agent without any sandbox tool creates no sandbox and copies nothing (the model could not
21
21
  * run the scripts), and is told so.
@@ -23,11 +23,14 @@ import { type SandboxToolDeps } from "./sandbox/common.js";
23
23
  * to run scripts with an interpreter (`sh` / `python3` / `node`).
24
24
  */
25
25
  /**
26
- * 已复制进沙箱的技能的标记目录(内核控制:`/skills` 只有提供方以 root 能写;技能名不以 `.` 开头,不会撞名)。
26
+ * 已复制进沙箱的技能的标记目录(内核控制:技能根目录只有提供方以 root 能写;技能名不以 `.` 开头,不会撞名)。
27
+ * `skillsRoot`: the instance's skills root.
27
28
  */
28
- export declare const SKILLS_MARKER_DIR = "/skills/.capybara";
29
+ export declare const skillsMarkerDir: (skillsRoot?: string) => string;
30
+ /** {@link skillsMarkerDir} of an instance with the default roots. */
31
+ export declare const SKILLS_MARKER_DIR: string;
29
32
  /** The marker file of one skill version. */
30
- export declare const skillMarkerPath: (name: string, digest: string) => string;
33
+ export declare const skillMarkerPath: (name: string, digest: string, skillsRoot?: string) => string;
31
34
  /** 依赖:沙箱工具的依赖,加上读技能行用的库与读快照用的日志。 */
32
35
  export interface SkillLoadDeps extends SandboxToolDeps {
33
36
  db: KernelSchemaDb;
@@ -9,19 +9,19 @@ import { resolveSkillForkType } from "../../runtime/subagents/roster.js";
9
9
  import { invocationOf, resolveSkills } from "../../skills/catalog.js";
10
10
  import { PLATFORM_SOURCE, skillFileStore } from "../../skills/usable.js";
11
11
  import { AGENT_TOOL, SANDBOX_TOOL_NAMES } from "../builtin-names.js";
12
- import { clip, display, inSandbox, sandboxBaseTimeoutMs, withNotices, withPathLease, } from "./sandbox/common.js";
12
+ import { clip, display, inSandbox, sandboxBaseTimeoutMs, sandboxConversationOf, sandboxUsable, withNotices, withPathLease, } from "./sandbox/common.js";
13
13
  /**
14
14
  * Builtin tool `Skill` (M5 T14, ruling M17): resolves the name against the latest skills catalog
15
15
  * snapshot in the log to an id and version, and returns that version's SKILL.md body and file list
16
16
  * (the result names the version). A skill with other files is copied whole into the conversation's
17
- * environment at `/skills/<name>@<digest>/` (the provider writes as root, commands read only; the
17
+ * environment at `<skills root>/<name>@<digest>/` (the instance's skills root, `/skills` by default; the provider writes as root, commands read only; the
18
18
  * digest is {@link skillDigest} of the version's file manifest, so versions of one skill coexist in
19
19
  * the shared file system). Without a sandbox provider only the body is given, with a note that
20
20
  * scripts are unavailable.
21
21
  *
22
22
  * - Every file read from storage is checked against the manifest's sha256; a mismatch is refused.
23
23
  * - The copy is recorded in a kernel-controlled marker, one per skill version
24
- * ({@link skillMarkerPath}: `/skills/.capybara/<name>@<digest>.json`, commands cannot change it):
24
+ * ({@link skillMarkerPath}: `<skills root>/.capybara/<name>@<digest>.json`, commands cannot change it):
25
25
  * `{ digest, files }`. A present marker with the same digest skips the copy.
26
26
  * - An agent without any sandbox tool creates no sandbox and copies nothing (the model could not
27
27
  * run the scripts), and is told so.
@@ -29,11 +29,14 @@ import { clip, display, inSandbox, sandboxBaseTimeoutMs, withNotices, withPathLe
29
29
  * to run scripts with an interpreter (`sh` / `python3` / `node`).
30
30
  */
31
31
  /**
32
- * 已复制进沙箱的技能的标记目录(内核控制:`/skills` 只有提供方以 root 能写;技能名不以 `.` 开头,不会撞名)。
32
+ * 已复制进沙箱的技能的标记目录(内核控制:技能根目录只有提供方以 root 能写;技能名不以 `.` 开头,不会撞名)。
33
+ * `skillsRoot`: the instance's skills root.
33
34
  */
34
- export const SKILLS_MARKER_DIR = `${SKILLS_SANDBOX_ROOT}/.capybara`;
35
+ export const skillsMarkerDir = (skillsRoot = SKILLS_SANDBOX_ROOT) => `${skillsRoot}/.capybara`;
36
+ /** {@link skillsMarkerDir} of an instance with the default roots. */
37
+ export const SKILLS_MARKER_DIR = skillsMarkerDir();
35
38
  /** The marker file of one skill version. */
36
- export const skillMarkerPath = (name, digest) => `${SKILLS_MARKER_DIR}/${name}@${digest}.json`;
39
+ export const skillMarkerPath = (name, digest, skillsRoot = SKILLS_SANDBOX_ROOT) => `${skillsMarkerDir(skillsRoot)}/${name}@${digest}.json`;
37
40
  const MARKER_MAX_BYTES = 256 * 1024;
38
41
  /** 结果里最多列出的文件数(其余只给数量)。 */
39
42
  const MAX_LISTED_FILES = 200;
@@ -46,7 +49,7 @@ const SPEC = {
46
49
  description: "Load a skill from the skills list: returns the skill's instructions " +
47
50
  "(its SKILL.md) and the list of its files. Call it before starting a task that matches a " +
48
51
  "skill's description, then follow the returned instructions. If the skill has scripts or other " +
49
- "files, they are copied read-only to /skills/<name>@<version-id>/ in this environment; run scripts " +
52
+ "files, they are copied read-only to <name>@<version-id>/ in the skills directory of this environment; run scripts " +
50
53
  "there with an interpreter (sh, python3, node). Call it again when the skills list changes. Pass args when the task gives the skill arguments (they replace " +
51
54
  "$ARGUMENTS and $1, $2… in the instructions). Do not use it for skills that are not in the list.",
52
55
  inputSchema: {
@@ -157,13 +160,13 @@ async function readVerified(store, key, file, skill, signal) {
157
160
  }
158
161
  }
159
162
  /**
160
- * Copies the skill version whole to `/skills/<name>@<digest>/` (skipped when its marker matches).
163
+ * Copies the skill version whole to `<skills root>/<name>@<digest>/` (skipped when its marker matches).
161
164
  * Every file is checked against the manifest before it is written; the marker is written last, so
162
165
  * a failure midway means the next call starts over.
163
166
  */
164
167
  async function copySkill(s, v) {
165
168
  const { name } = v.entry;
166
- const markerPath = skillMarkerPath(name, v.digest);
169
+ const markerPath = skillMarkerPath(name, v.digest, s.roots.skills);
167
170
  const m = await s.p.readFile(s.h, markerPath, { maxBytes: MARKER_MAX_BYTES, signal: s.signal });
168
171
  const cur = m && !m.truncated ? parseMarker(m.data) : undefined;
169
172
  if (cur?.digest === v.digest)
@@ -172,7 +175,7 @@ async function copySkill(s, v) {
172
175
  const data = f.path === SKILL_ENTRY_FILE
173
176
  ? v.entryBytes
174
177
  : await readVerified(v.store, `${v.keyPrefix}${f.path}`, f, name, s.signal);
175
- await s.p.writeFile(s.h, skillSandboxPath(name, v.digest, f.path), data, s.signal);
178
+ await s.p.writeFile(s.h, skillSandboxPath(name, v.digest, f.path, s.roots.skills), data, s.signal);
176
179
  }
177
180
  const entry = { digest: v.digest, files: v.manifest.files.map((f) => f.path) };
178
181
  await s.p.writeFile(s.h, markerPath, new TextEncoder().encode(JSON.stringify(entry)), s.signal);
@@ -435,14 +438,15 @@ export function skillLoadTool(deps) {
435
438
  }
436
439
  if ("errors" in parsed)
437
440
  throw new ToolError(integrityText(name));
438
- const dir = `${skillSandboxDir(name, digest)}/`;
441
+ // the skills root of the instance the files were copied to (the default without a copy)
442
+ let skillsRoot = SKILLS_SANDBOX_ROOT;
439
443
  const others = manifest.files.filter((f) => f.path !== SKILL_ENTRY_FILE);
440
444
  let notices = [];
441
445
  let filesText;
442
446
  if (others.length === 0) {
443
447
  filesText = "This skill has no files besides its instructions.";
444
448
  }
445
- else if (!deps.sandbox.available) {
449
+ else if (!(await sandboxUsable(deps, ctx.orgId, sandboxConversationOf(ctx)))) {
446
450
  filesText =
447
451
  `Skill files (for reference):\n${fileList(manifest.files)}\n` +
448
452
  "This deployment has no sandbox, so the skill's scripts and other files are not available; " +
@@ -457,19 +461,21 @@ export function skillLoadTool(deps) {
457
461
  }
458
462
  else {
459
463
  // the skill's marker is read, compared and written: one holder at a time per skill
460
- const r = await inSandbox(deps, ctx, (s) => withPathLease(deps, s, skillMarkerPath(name, digest), s.signal, (signal) => copySkill({ ...s, signal }, v)));
464
+ const r = await inSandbox(deps, ctx, (s) => withPathLease(deps, s, skillMarkerPath(name, digest, s.roots.skills), s.signal, (signal) => copySkill({ ...s, signal }, v).then(() => s.roots)));
461
465
  notices = r.notices;
466
+ skillsRoot = r.value.skills;
467
+ const dir = `${skillSandboxDir(name, digest, skillsRoot)}/`;
462
468
  filesText =
463
469
  `Skill files (copied to ${dir} in the sandbox, read-only):\n${fileList(manifest.files)}\n` +
464
470
  `Run scripts with an interpreter, e.g. \`sh ${dir}<script>.sh\`, \`python3 ${dir}<script>.py\` ` +
465
471
  "or `node`; the files are not executable, so `./script` does not work. Write outputs under " +
466
- `/workspace, not ${SKILLS_SANDBOX_ROOT}.`;
472
+ `${r.value.workspace}, not ${skillsRoot}.`;
467
473
  }
468
474
  const invocation = invocationOf(manifest);
469
475
  const fork = invocation.fork;
470
476
  let instructions;
471
477
  try {
472
- instructions = renderSkillBody(parsed.body.trim(), invocation, args ?? "", name, digest);
478
+ instructions = renderSkillBody(parsed.body.trim(), invocation, args ?? "", name, digest, skillsRoot);
473
479
  }
474
480
  catch (e) {
475
481
  if (!(e instanceof RangeError))
@@ -29,7 +29,7 @@ const SPEC = {
29
29
  "user-invocable, context, agent, argument-hint and arguments, which are kept; with " +
30
30
  "disable-model-invocation: true only a person can invoke the skill and you cannot load it). " +
31
31
  "Every other entry in `files` is stored at " +
32
- "/skills/<name>@<version-id>/<path> once loaded. Files of the previous version that you leave out are not part of the new " +
32
+ "<name>@<version-id>/<path> in the skills directory once loaded. Files of the previous version that you leave out are not part of the new " +
33
33
  "version. Text files only, at most 1 MB per skill in total. Use an existing skill's name to update " +
34
34
  "it; an organization skill with the same name is shadowed by this agent's own skill. On a private " +
35
35
  "agent the new version takes effect immediately. On a shared agent the change is sent to the " +
@@ -41,7 +41,7 @@ const SPEC = {
41
41
  type: "string",
42
42
  minLength: 1,
43
43
  maxLength: SKILL_LIMITS.nameMaxChars,
44
- description: 'Skill name: lowercase letters, digits and single hyphens, e.g. "weekly-report". Also the folder name under /skills/.',
44
+ description: 'Skill name: lowercase letters, digits and single hyphens, e.g. "weekly-report". Also the folder name in the skills directory.',
45
45
  },
46
46
  description: {
47
47
  type: "string",
@@ -249,6 +249,8 @@ let ToolPipeline = class ToolPipeline {
249
249
  subagents: ctx.agent.subagents,
250
250
  child: ctx.parent !== undefined,
251
251
  forkChild: ctx.parent?.forSkill === true,
252
+ sandboxConversationId: ctx.parent?.rootConversationId ?? ctx.conversationId,
253
+ replay: ctx.conversationKind === "eval",
252
254
  });
253
255
  }
254
256
  return resolved;