@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
@@ -13,6 +13,7 @@ import { afterCommit } from "../infra/database/after-commit.js";
13
13
  import { TransactionHost } from "../infra/database/transaction-host.js";
14
14
  import { Transactional } from "../infra/database/transactional.decorator.js";
15
15
  import { KernelEvents } from "../infra/events/kernel-events.js";
16
+ import { taskOutputFile } from "./launch.js";
16
17
  import { LAST_LINES_MAX_CHARS } from "./session.js";
17
18
  import { BackgroundTaskStore } from "./store.js";
18
19
  /** The idempotency key of a task's `background_task_finished` event. */
@@ -63,7 +64,7 @@ let BackgroundTaskFinisher = class BackgroundTaskFinisher {
63
64
  lastLines: lastLines.length > LAST_LINES_MAX_CHARS
64
65
  ? lastLines.slice(-LAST_LINES_MAX_CHARS)
65
66
  : lastLines,
66
- outputPath: `/workspace/${task.outputPath}`,
67
+ outputPath: taskOutputFile(task.outputPath),
67
68
  reported,
68
69
  ...(to.lostReason !== undefined && { lostReason: to.lostReason }),
69
70
  ...(opts.stoppedBy !== undefined && { stoppedBy: opts.stoppedBy }),
@@ -2,8 +2,8 @@ import type { KernelConfig } from "../config/schema.js";
2
2
  /**
3
3
  * Launching a background Bash task (spec 2026-11-05-background-bash-design §4.2): the task id, the
4
4
  * one short exec that starts the command detached from the exec, and the model-visible result.
5
- * Every path is relative to `/workspace` (the launch exec's `cwd`), so a provider that maps
6
- * `/workspace` needs no rewriting inside the script.
5
+ * Every path is relative to the instance's workspace root (the launch exec's `cwd`), so the
6
+ * script is the same whatever the instance's roots are.
7
7
  */
8
8
  /** The launch exec's own time limit: it only writes a file and forks. */
9
9
  export declare const LAUNCH_EXEC_TIMEOUT_MS = 30000;
@@ -12,11 +12,17 @@ export declare const LAUNCH_EXEC_TIMEOUT_MS = 30000;
12
12
  * `sandbox.limits.maxCommandSeconds`. It is also a task's default `timeout`.
13
13
  */
14
14
  export declare const backgroundMaxSeconds: (cfg: Pick<KernelConfig, "sandbox">) => number;
15
- /** The directory of the task files, relative to `/workspace`. */
15
+ /** The directory of the task files, relative to the workspace root. */
16
16
  export declare const TASKS_DIR = ".capybara/tasks";
17
- /** A task's output file, relative to `/workspace` (`output_path`). */
17
+ /** A task's output file, relative to the workspace root (`output_path` while reserved). */
18
18
  export declare const taskOutputPath: (id: string) => string;
19
- /** The file the launch writes the session id to, relative to `/workspace`. */
19
+ /**
20
+ * The absolute output file of a task from its `output_path`: absolute once the task started (the
21
+ * launch records `<workspace>/.capybara/tasks/<id>.out` of its instance); a reservation's relative
22
+ * path is read under the default workspace.
23
+ */
24
+ export declare const taskOutputFile: (outputPath: string) => string;
25
+ /** The file the launch writes the session id to, relative to the workspace root. */
20
26
  export declare const taskSidPath: (id: string) => string;
21
27
  /** A new task id: `bt_` plus 6 lowercase base32 characters. */
22
28
  export declare function newTaskId(): string;
@@ -32,5 +38,5 @@ export declare const TASK_ID_RE: RegExp;
32
38
  * positive integer (both go into the script unquoted).
33
39
  */
34
40
  export declare function launchScript(id: string, command: string, seconds: number): string;
35
- /** The model-visible result of a launch (spec §2.1). */
36
- export declare function startedText(id: string, description: string): string;
41
+ /** The model-visible result of a launch (spec §2.1); `workspace`: the instance's workspace root. */
42
+ export declare function startedText(id: string, description: string, workspace?: string): string;
@@ -1,9 +1,10 @@
1
1
  import { randomBytes } from "node:crypto";
2
+ import { DEFAULT_SANDBOX_ROOTS } from "@capybara-hq/plugin-sdk";
2
3
  /**
3
4
  * Launching a background Bash task (spec 2026-11-05-background-bash-design §4.2): the task id, the
4
5
  * one short exec that starts the command detached from the exec, and the model-visible result.
5
- * Every path is relative to `/workspace` (the launch exec's `cwd`), so a provider that maps
6
- * `/workspace` needs no rewriting inside the script.
6
+ * Every path is relative to the instance's workspace root (the launch exec's `cwd`), so the
7
+ * script is the same whatever the instance's roots are.
7
8
  */
8
9
  /** The launch exec's own time limit: it only writes a file and forks. */
9
10
  export const LAUNCH_EXEC_TIMEOUT_MS = 30_000;
@@ -12,11 +13,17 @@ export const LAUNCH_EXEC_TIMEOUT_MS = 30_000;
12
13
  * `sandbox.limits.maxCommandSeconds`. It is also a task's default `timeout`.
13
14
  */
14
15
  export const backgroundMaxSeconds = (cfg) => Math.min(cfg.sandbox.background.maxSeconds, cfg.sandbox.limits.maxCommandSeconds);
15
- /** The directory of the task files, relative to `/workspace`. */
16
+ /** The directory of the task files, relative to the workspace root. */
16
17
  export const TASKS_DIR = ".capybara/tasks";
17
- /** A task's output file, relative to `/workspace` (`output_path`). */
18
+ /** A task's output file, relative to the workspace root (`output_path` while reserved). */
18
19
  export const taskOutputPath = (id) => `${TASKS_DIR}/${id}.out`;
19
- /** The file the launch writes the session id to, relative to `/workspace`. */
20
+ /**
21
+ * The absolute output file of a task from its `output_path`: absolute once the task started (the
22
+ * launch records `<workspace>/.capybara/tasks/<id>.out` of its instance); a reservation's relative
23
+ * path is read under the default workspace.
24
+ */
25
+ export const taskOutputFile = (outputPath) => outputPath.startsWith("/") ? outputPath : `${DEFAULT_SANDBOX_ROOTS.workspace}/${outputPath}`;
26
+ /** The file the launch writes the session id to, relative to the workspace root. */
20
27
  export const taskSidPath = (id) => `${TASKS_DIR}/${id}.sid`;
21
28
  const BASE32 = "abcdefghijklmnopqrstuvwxyz234567";
22
29
  /** A new task id: `bt_` plus 6 lowercase base32 characters. */
@@ -60,11 +67,11 @@ export function launchScript(id, command, seconds) {
60
67
  `cat ${t}.sid`,
61
68
  ].join("\n");
62
69
  }
63
- /** The model-visible result of a launch (spec §2.1). */
64
- export function startedText(id, description) {
70
+ /** The model-visible result of a launch (spec §2.1); `workspace`: the instance's workspace root. */
71
+ export function startedText(id, description, workspace = DEFAULT_SANDBOX_ROOTS.workspace) {
65
72
  return [
66
73
  `Started background task ${id} ("${description}").`,
67
- `Output: /workspace/${taskOutputPath(id)}`,
74
+ `Output: ${workspace}/${taskOutputPath(id)}`,
68
75
  "Read it with TaskOutput (or Read), stop it with TaskStop. You will be told when it finishes.",
69
76
  ].join("\n");
70
77
  }
@@ -37,7 +37,7 @@ export declare class BackgroundTaskPollJob {
37
37
  readonly config: KernelConfigService;
38
38
  readonly lockService: LockService;
39
39
  readonly log: KernelLogger;
40
- constructor(store: Pick<BackgroundTaskStore, "runningStarted" | "pruneFinished" | "failAbandonedLaunches">, finisher: BackgroundTaskFinisher, eventLog: Pick<EventLog, "publishChanged">, sandbox: Pick<SandboxService, "available" | "ifRunning">, config: KernelConfigService, lockService: LockService, log: KernelLogger);
40
+ constructor(store: Pick<BackgroundTaskStore, "runningStarted" | "pruneFinished" | "failAbandonedLaunches">, finisher: BackgroundTaskFinisher, eventLog: Pick<EventLog, "publishChanged">, sandbox: Pick<SandboxService, "cloudAvailable" | "availableFor" | "ifRunning">, config: KernelConfigService, lockService: LockService, log: KernelLogger);
41
41
  poll(signal?: AbortSignal): Promise<void>;
42
42
  /**
43
43
  * One poll of every running task (the body of {@link poll}); returns how many tasks finished.
@@ -7,6 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
+ import { rootsOf } from "@capybara-hq/plugin-sdk";
10
11
  import { Inject, Injectable } from "@nestjs/common";
11
12
  import { EventLog } from "../conversations/event-log.js";
12
13
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
@@ -88,8 +89,6 @@ let BackgroundTaskPollJob = class BackgroundTaskPollJob {
88
89
  for (const c of conversations)
89
90
  await this.eventLog.publishChanged(c, "background");
90
91
  }
91
- if (!this.sandbox.available)
92
- return 0;
93
92
  const groups = new Map();
94
93
  for (const t of await this.store.runningStarted()) {
95
94
  if (t.sid === null || t.sandboxCreatedAt === null)
@@ -103,6 +102,12 @@ let BackgroundTaskPollJob = class BackgroundTaskPollJob {
103
102
  if (signal.aborted)
104
103
  break;
105
104
  try {
105
+ // the environment's provider is not there (no cloud provider, `@device` not served):
106
+ // its tasks stay as they are until it is
107
+ const env = tasks[0]?.environmentId ?? null;
108
+ if (env === null ? !this.sandbox.cloudAvailable : !(await this.sandbox.availableFor(env))) {
109
+ continue;
110
+ }
106
111
  finished += await this.pollSandbox(tasks, signal);
107
112
  }
108
113
  catch (err) {
@@ -153,7 +158,7 @@ let BackgroundTaskPollJob = class BackgroundTaskPollJob {
153
158
  async pollExec(p, h, tasks, signal) {
154
159
  const res = await p.exec(h, {
155
160
  command: pollScript(tasks.map((t) => ({ id: t.id, sid: t.sid }))),
156
- cwd: "/workspace",
161
+ cwd: rootsOf(h).workspace,
157
162
  timeoutMs: POLL_EXEC_TIMEOUT_MS,
158
163
  maxOutputBytes: POLL_MAX_OUTPUT_BYTES,
159
164
  signal,
@@ -1,4 +1,4 @@
1
- import type { SandboxActor, SandboxHandle } from "@capybara-hq/plugin-sdk";
1
+ import { type SandboxActor, type SandboxHandle } from "@capybara-hq/plugin-sdk";
2
2
  import type { HeldProvider, TaggedProvider } from "../sandbox/sandbox.service.js";
3
3
  /**
4
4
  * A background task's session inside the sandbox (spec 2026-11-05-background-bash-design §4.2):
@@ -41,7 +41,7 @@ export declare function killSession(p: Pick<HeldProvider, "exec">, h: SandboxHan
41
41
  export declare const LAST_LINES_MAX_CHARS = 4000;
42
42
  /**
43
43
  * A shell pipeline printing the last 20 lines of task `id`'s output (at most {@link TAIL_BYTES}),
44
- * base64 on one line (empty when there is no output file). Relative to `/workspace`.
44
+ * base64 on one line (empty when there is no output file). Relative to the workspace root.
45
45
  */
46
46
  export declare function tailCommand(id: string): string;
47
47
  /** Decodes a {@link tailCommand} result into `lastLines`: its end, at most 4 000 characters. */
@@ -1,3 +1,4 @@
1
+ import { rootsOf } from "@capybara-hq/plugin-sdk";
1
2
  import { TASK_ID_RE, TASKS_DIR } from "./launch.js";
2
3
  /**
3
4
  * A background task's session inside the sandbox (spec 2026-11-05-background-bash-design §4.2):
@@ -53,7 +54,7 @@ export async function killSession(p, h, sid) {
53
54
  const r = await p
54
55
  .exec(h, {
55
56
  command: `${killSessionScript(sid)}; exit "$ks"`,
56
- cwd: "/workspace",
57
+ cwd: rootsOf(h).workspace,
57
58
  timeoutMs: KILL_SESSION_TIMEOUT_MS,
58
59
  maxOutputBytes: 4096,
59
60
  signal: AbortSignal.timeout(KILL_SESSION_TIMEOUT_MS),
@@ -67,7 +68,7 @@ const TAIL_BYTES = 12_000;
67
68
  export const LAST_LINES_MAX_CHARS = 4000;
68
69
  /**
69
70
  * A shell pipeline printing the last 20 lines of task `id`'s output (at most {@link TAIL_BYTES}),
70
- * base64 on one line (empty when there is no output file). Relative to `/workspace`.
71
+ * base64 on one line (empty when there is no output file). Relative to the workspace root.
71
72
  */
72
73
  export function tailCommand(id) {
73
74
  return `{ tail -n 20 ${TASKS_DIR}/${id}.out 2>/dev/null | tail -c ${TAIL_BYTES} | base64 | tr -d '\\n'; }`;
@@ -19,7 +19,7 @@ export interface TaskReport {
19
19
  * processes (it left `server &` running) has the session killed in the same exec (so under the
20
20
  * caller's lease), and is reported without a process once the session was confirmed empty: the
21
21
  * task ends when its command exits, and nothing of it runs on untracked. The tail (base64) is read
22
- * only for a task that exited or has no process left. Relative to `/workspace`.
22
+ * only for a task that exited or has no process left. Relative to the workspace root.
23
23
  */
24
24
  export declare function pollScript(tasks: {
25
25
  id: string;
@@ -8,7 +8,7 @@ import { exitedAs } from "./stop.js";
8
8
  * processes (it left `server &` running) has the session killed in the same exec (so under the
9
9
  * caller's lease), and is reported without a process once the session was confirmed empty: the
10
10
  * task ends when its command exits, and nothing of it runs on untracked. The tail (base64) is read
11
- * only for a task that exited or has no process left. Relative to `/workspace`.
11
+ * only for a task that exited or has no process left. Relative to the workspace root.
12
12
  */
13
13
  export function pollScript(tasks) {
14
14
  const lines = tasks.map(({ id, sid }) => {
@@ -7,6 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
+ import { rootsOf } from "@capybara-hq/plugin-sdk";
10
11
  import { Inject, Injectable } from "@nestjs/common";
11
12
  import { KernelDb } from "../infra/database/kernel-db.js";
12
13
  import { KERNEL_LOGGER } from "../infra/tokens.js";
@@ -24,7 +25,7 @@ const TAIL_EXEC_TIMEOUT_MS = 10_000;
24
25
  /**
25
26
  * Exit code 0 with `exited <code>` when the task had already finished (its exit file exists),
26
27
  * else sends SIGTERM to the session and waits up to {@link STOP_GRACE_MS} for it to empty, then
27
- * prints `term`. Relative to `/workspace`.
28
+ * prints `term`. Relative to the workspace root.
28
29
  */
29
30
  function termScript(id, sid) {
30
31
  const f = `${TASKS_DIR}/${id}`;
@@ -146,7 +147,7 @@ let BackgroundTaskStopper = class BackgroundTaskStopper {
146
147
  const p = actingAs(tp, taskActor(cur));
147
148
  const term = await p.exec(h, {
148
149
  command: termScript(cur.id, sid),
149
- cwd: "/workspace",
150
+ cwd: rootsOf(h).workspace,
150
151
  timeoutMs: TERM_EXEC_TIMEOUT_MS,
151
152
  maxOutputBytes: 4096,
152
153
  signal: sig,
@@ -161,7 +162,7 @@ let BackgroundTaskStopper = class BackgroundTaskStopper {
161
162
  }
162
163
  const tail = await p.exec(h, {
163
164
  command: tailCommand(cur.id),
164
- cwd: "/workspace",
165
+ cwd: rootsOf(h).workspace,
165
166
  timeoutMs: TAIL_EXEC_TIMEOUT_MS,
166
167
  maxOutputBytes: 64 * 1024,
167
168
  signal: sig,
@@ -28,7 +28,11 @@ export interface BackgroundTaskRow {
28
28
  * started: readers that act on the process (the poller, stops) skip such a row.
29
29
  */
30
30
  sid: string | null;
31
- /** Relative to `/workspace`. */
31
+ /**
32
+ * The output file: absolute once the task started (`markStarted` records it under its
33
+ * instance's workspace root), relative to the workspace root while reserved
34
+ * (`taskOutputFile` in launch.ts).
35
+ */
32
36
  outputPath: string;
33
37
  status: BackgroundTaskStatus;
34
38
  lostReason: string | null;
@@ -85,6 +89,7 @@ export declare class BackgroundTaskStore {
85
89
  markStarted(id: string, to: {
86
90
  sid: string;
87
91
  sandboxCreatedAt: Date;
92
+ outputPath?: string;
88
93
  }): Promise<"started" | "stopping" | false>;
89
94
  /**
90
95
  * Finishes a launch whose reservation a stop claimed, once the launch killed what it started:
@@ -86,7 +86,11 @@ let BackgroundTaskStore = class BackgroundTaskStore {
86
86
  async markStarted(id, to) {
87
87
  const r = await this.db
88
88
  .updateTable("background_tasks")
89
- .set({ sid: to.sid, sandbox_created_at: to.sandboxCreatedAt })
89
+ .set({
90
+ sid: to.sid,
91
+ sandbox_created_at: to.sandboxCreatedAt,
92
+ ...(to.outputPath !== undefined && { output_path: to.outputPath }),
93
+ })
90
94
  .where("id", "=", id)
91
95
  .where("status", "=", "running")
92
96
  .where("sid", "is", null)
@@ -46,7 +46,7 @@ export declare class BackgroundTasks {
46
46
  private readonly eventLog;
47
47
  private readonly db;
48
48
  private readonly environments;
49
- constructor(access: Pick<ConversationAccess, "isAccessible">, store: Pick<BackgroundTaskStore, "listFor" | "get">, stopper: Pick<BackgroundTaskStopper, "stop">, sandbox: Pick<SandboxService, "available" | "ifRunning">, eventLog: Pick<EventLog, "findByIdem">, db: KernelDb, environments: Pick<ConversationEnvironments, "forTool">);
49
+ constructor(access: Pick<ConversationAccess, "isAccessible">, store: Pick<BackgroundTaskStore, "listFor" | "get">, stopper: Pick<BackgroundTaskStopper, "stop">, sandbox: Pick<SandboxService, "availableFor" | "ifRunning">, eventLog: Pick<EventLog, "findByIdem">, db: KernelDb, environments: Pick<ConversationEnvironments, "forTool">);
50
50
  /**
51
51
  * The conversation and every sub-agent conversation below it (grandchildren too: their tasks run
52
52
  * in the root's sandbox, agent map spec §6); `conversation.not_found` when not visible.
@@ -7,7 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
- import { isSandboxGoneError } from "@capybara-hq/plugin-sdk";
10
+ import { isSandboxGoneError, rootsOf } from "@capybara-hq/plugin-sdk";
11
11
  import { Inject, Injectable } from "@nestjs/common";
12
12
  import { ConversationAccess } from "../conversations/access.js";
13
13
  import { EventLog } from "../conversations/event-log.js";
@@ -144,7 +144,7 @@ let BackgroundTasks = class BackgroundTasks {
144
144
  if (task.sid !== null &&
145
145
  task.sandboxCreatedAt !== null &&
146
146
  task.environmentId !== null &&
147
- this.sandbox.available &&
147
+ (await this.sandbox.availableFor(task.environmentId)) &&
148
148
  (await this.environmentUsable(orgId, task.conversationId))) {
149
149
  const createdAt = task.sandboxCreatedAt;
150
150
  // the person reading it acts in the task's conversation
@@ -153,7 +153,7 @@ let BackgroundTasks = class BackgroundTasks {
153
153
  const r = await this.sandbox.ifRunning(task.environmentId, { createdAt }, async (p, h, signal) => {
154
154
  const res = await actingAs(p, actor).exec(h, {
155
155
  command: tailScript(task.id, tail),
156
- cwd: "/workspace",
156
+ cwd: rootsOf(h).workspace,
157
157
  timeoutMs: OUTPUT_READ_TIMEOUT_MS,
158
158
  maxOutputBytes: 2 * OUTPUT_TAIL_MAX_BYTES + 4096,
159
159
  signal,
@@ -188,7 +188,7 @@ BackgroundTasks = __decorate([
188
188
  __param(6, Inject(ConversationEnvironments))
189
189
  ], BackgroundTasks);
190
190
  export { BackgroundTasks };
191
- /** Prints `size <bytes>` and `out <base64 of the last `tail` bytes>`. Relative to `/workspace`. */
191
+ /** Prints `size <bytes>` and `out <base64 of the last `tail` bytes>`. Relative to the workspace root. */
192
192
  function tailScript(id, tail) {
193
193
  if (!TASK_ID_RE.test(id))
194
194
  throw new Error(`invalid background task id: ${id}`);
@@ -181,7 +181,7 @@ export declare function headsToResolve(entries: readonly MarketplaceEntry[], imp
181
181
  /** The host versions (an entry's `minKernel` is judged against them). */
182
182
  export declare const HOST_VERSIONS: {
183
183
  readonly kernel: string;
184
- readonly sdk: "0.1.0-rc.5";
184
+ readonly sdk: "0.1.0-rc.6";
185
185
  };
186
186
  /** Whether this deployment satisfies the entry's `minKernel`. */
187
187
  export declare const entryCompatible: (e: Pick<MarketplaceEntry, "minKernel">) => boolean;
@@ -65,6 +65,7 @@ export function renderConfigDocs(schema) {
65
65
  // CAPYBARA_WEB_TRUST_PROXY: apps/web/src/lib/peer.ts; CAPYBARA_WEB_SHUTDOWN_GRACE_MS: apps/web/server.mjs.
66
66
  // CAPYBARA_WEB_KEEP_ALIVE_TIMEOUT_MS: apps/web/src/lib/keep-alive.ts.
67
67
  // CAPYBARA_WEB_OBJECT_STORE_*: apps/web/src/lib/object-store-proxy.ts.
68
+ // CAPYBARA_WEB_GATEWAY_URL: apps/web/src/lib/device-connect-proxy.ts.
68
69
  "## Web tier (apps/web) environment variables",
69
70
  "",
70
71
  "The web tier is a separate process (started through `apps/web/server.mjs`); it does not read the kernel config and only honors the following environment variables:",
@@ -81,6 +82,7 @@ export function renderConfigDocs(schema) {
81
82
  "| `CAPYBARA_WEB_OBJECT_STORE_URL` | Object storage on the web origin: the object store's address as web reaches it (e.g. `http://seaweedfs-s3:8333`). When set, web proxies `/<bucket>/**` (and `/<bucket>` itself for POST only) there, so browsers upload attachments (presigned POST) and download attachments and artifacts (presigned GET) through web; set the object-store extension's `publicEndpoint` to the web origin with `forcePathStyle: true`. Only GET, HEAD and POST; the path is forwarded unchanged; cookies and authorization are not forwarded; responses carry `X-Content-Type-Options: nosniff` and `Content-Security-Policy: sandbox`; uploads are capped at `CAPYBARA_WEB_MAX_PACKAGE_BYTES`. Unset (default): off |",
82
83
  "| `CAPYBARA_WEB_OBJECT_STORE_BUCKET` | With `CAPYBARA_WEB_OBJECT_STORE_URL`: the bucket, equal to the object-store extension's `bucket` (it is the first path segment web proxies) |",
83
84
  "| `CAPYBARA_WEB_OBJECT_STORE_PUBLIC_HOST` | With `CAPYBARA_WEB_OBJECT_STORE_URL`: the host of the extension's `publicEndpoint` (a host such as `capybara.example.com`, or that URL). Sent upstream as `Host` instead of the incoming one, because presigned URLs sign the host they were issued for and front proxies rewrite it |",
85
+ "| `CAPYBARA_WEB_GATEWAY_URL` | Connected machines on the web origin: a `gateway` replica's address as web reaches it (an http(s) URL, e.g. `http://gateway:8080`). When set, WebSocket upgrades on exactly `/device/connect` are forwarded to it (`capybara-device` connects at `<http.publicUrl>/device/connect`); unset, such upgrades get 404. Not needed when an external proxy routes `/device/connect` to the gateway replicas; a malformed value stops the web tier at startup |",
84
86
  "| `CAPYBARA_DEV_UI` | When set to `1`, production builds also serve the `/dev/ui` component catalog page (off by default) |",
85
87
  "",
86
88
  ].join("\n");
@@ -221,6 +221,13 @@ export declare const KernelConfigSchema: z.ZodObject<{
221
221
  }, z.core.$strip>>;
222
222
  environments: z.ZodPrefault<z.ZodObject<{
223
223
  maxPersonal: z.ZodDefault<z.ZodNumber>;
224
+ device: z.ZodPrefault<z.ZodObject<{
225
+ reconnectGraceSeconds: z.ZodDefault<z.ZodNumber>;
226
+ leaseTtlMs: z.ZodDefault<z.ZodNumber>;
227
+ authCodeTtlSeconds: z.ZodDefault<z.ZodNumber>;
228
+ pollIntervalSeconds: z.ZodDefault<z.ZodNumber>;
229
+ maxFrameBytes: z.ZodDefault<z.ZodNumber>;
230
+ }, z.core.$strip>>;
224
231
  }, z.core.$strip>>;
225
232
  evals: z.ZodPrefault<z.ZodObject<{
226
233
  orgConcurrency: z.ZodDefault<z.ZodNumber>;
@@ -1184,6 +1184,51 @@ export const KernelConfigSchema = z.object({
1184
1184
  hot: true,
1185
1185
  description: "Most personal environments a member may have in one org, the default one included; creating one more fails with environment.limit_reached",
1186
1186
  })),
1187
+ device: z
1188
+ .object({
1189
+ reconnectGraceSeconds: z
1190
+ .number()
1191
+ .int()
1192
+ .positive()
1193
+ .default(120)
1194
+ .meta(meta({
1195
+ hot: true,
1196
+ description: "Seconds a connected machine may stay disconnected before its environment is reported offline; also sent to the device program at sign-in",
1197
+ })),
1198
+ leaseTtlMs: z
1199
+ .number()
1200
+ .int()
1201
+ .min(6000)
1202
+ .default(30000)
1203
+ .meta(meta({
1204
+ description: "Milliseconds the replica holding a machine's connection keeps its lease without renewing it; at least 6000",
1205
+ })),
1206
+ authCodeTtlSeconds: z
1207
+ .number()
1208
+ .int()
1209
+ .positive()
1210
+ .default(600)
1211
+ .meta(meta({
1212
+ description: "Seconds a device sign-in code stays valid before the program must start over (device.expired)",
1213
+ })),
1214
+ pollIntervalSeconds: z
1215
+ .number()
1216
+ .int()
1217
+ .positive()
1218
+ .default(5)
1219
+ .meta(meta({
1220
+ description: "Seconds the device program waits between token polls during sign-in; polling faster gets device.slow_down",
1221
+ })),
1222
+ maxFrameBytes: z
1223
+ .number()
1224
+ .int()
1225
+ .positive()
1226
+ .default(16 * 1024 * 1024)
1227
+ .meta(meta({
1228
+ description: "Largest WebSocket frame in bytes accepted from or sent to a connected machine",
1229
+ })),
1230
+ })
1231
+ .prefault({}),
1187
1232
  })
1188
1233
  .prefault({}),
1189
1234
  evals: z
@@ -131,9 +131,10 @@ export declare class EventLog {
131
131
  /** 按幂等键取事件;没有 → undefined。 */
132
132
  findByIdem(conversationId: string, idemKey: string): Promise<ConversationEvent | undefined>;
133
133
  /**
134
- * 会话里某类事件的最新一条(按 seq);没有 → undefined。`skills_catalog` 有部分索引
135
- * (0025,按会话直接取到最新一条);其他类型按 (conversation_id, seq) 主键倒序扫本会话的事件并按 type 过滤,
136
- * 没有该类事件时会扫完整个会话。
134
+ * The conversation's latest event of `type` (by seq); undefined without one. `skills_catalog`
135
+ * (0025) and `environment_paths` (0094) have partial indexes that answer it directly; other
136
+ * types scan the conversation's events backwards on the (conversation_id, seq) key, the whole
137
+ * conversation when it has none.
137
138
  */
138
139
  latestOfType<T extends ConversationEventType>(conversationId: string, type: T): Promise<Extract<ConversationEvent, {
139
140
  type: T;
@@ -245,9 +245,10 @@ let EventLog = class EventLog {
245
245
  return r && toEvent(r);
246
246
  }
247
247
  /**
248
- * 会话里某类事件的最新一条(按 seq);没有 → undefined。`skills_catalog` 有部分索引
249
- * (0025,按会话直接取到最新一条);其他类型按 (conversation_id, seq) 主键倒序扫本会话的事件并按 type 过滤,
250
- * 没有该类事件时会扫完整个会话。
248
+ * The conversation's latest event of `type` (by seq); undefined without one. `skills_catalog`
249
+ * (0025) and `environment_paths` (0094) have partial indexes that answer it directly; other
250
+ * types scan the conversation's events backwards on the (conversation_id, seq) key, the whole
251
+ * conversation when it has none.
251
252
  */
252
253
  async latestOfType(conversationId, type) {
253
254
  const r = await this.db
@@ -515,6 +515,15 @@ export declare const ConversationEventSchema: z.ZodDiscriminatedUnion<[z.ZodObje
515
515
  added: z.ZodArray<z.ZodString>;
516
516
  removed: z.ZodArray<z.ZodString>;
517
517
  }, z.core.$strip>;
518
+ }, z.core.$strip>, z.ZodObject<{
519
+ type: z.ZodLiteral<"environment_paths">;
520
+ payload: z.ZodObject<{
521
+ roots: z.ZodObject<{
522
+ workspace: z.ZodString;
523
+ skills: z.ZodString;
524
+ tmp: z.ZodString;
525
+ }, z.core.$strip>;
526
+ }, z.core.$strip>;
518
527
  }, z.core.$strip>, z.ZodObject<{
519
528
  type: z.ZodLiteral<"tool_call_started">;
520
529
  payload: z.ZodObject<{
@@ -442,6 +442,20 @@ export const ConversationEventSchema = z.discriminatedUnion("type", [
442
442
  * position as a system message (`toolsChangedText`); dropped once a summary covers it.
443
443
  */
444
444
  ev("tools_changed", z.object({ step: count, added: z.array(z.string()), removed: z.array(z.string()) })),
445
+ /**
446
+ * The directories of the instance a sandbox tool call entered, when they differ from the
447
+ * defaults (environments spec §13.1: a connected machine's real paths). Written once per
448
+ * conversation and roots, before that call's result. **Model-visible**: rendered at its position
449
+ * as a system message (`environmentPathsText`); a summary covering it is followed by the latest
450
+ * one again.
451
+ */
452
+ ev("environment_paths", z.object({
453
+ roots: z.object({
454
+ workspace: z.string().min(1).max(4096),
455
+ skills: z.string().min(1).max(4096),
456
+ tmp: z.string().min(1).max(4096),
457
+ }),
458
+ })),
445
459
  /** 工具调用开始执行之前写入(续跑时「有开始无结果」的调用记为结果未知,不再执行)。 */
446
460
  ev("tool_call_started", z.object({ step: count, callId: z.string(), toolKey: z.string(), specHash: z.string() })),
447
461
  /**
@@ -800,7 +800,8 @@ export interface EnvironmentsTable {
800
800
  owner_account_id: string;
801
801
  name: ColumnType<string | null, string | null | undefined, string | null>;
802
802
  is_default: ColumnType<boolean, boolean | undefined, boolean>;
803
- source: "cloud";
803
+ /** 0093: `device` is a connected machine (environment_machines). */
804
+ source: "cloud" | "device";
804
805
  provider: ColumnType<string | null, string | null | undefined, string | null>;
805
806
  created_at: Ts;
806
807
  updated_at: Ts;
@@ -830,6 +831,29 @@ export interface EnvironmentInstancesTable {
830
831
  prev_handle: InstanceHandle;
831
832
  sweep_failed_at: TsNullable;
832
833
  }
834
+ /**
835
+ * 0093_connected_environments.sql: the machine connected to a `device` environment. At most one
836
+ * unrevoked row per environment; revoking sets `revoked_at` and keeps the row.
837
+ */
838
+ export interface EnvironmentMachinesTable {
839
+ id: Generated<string>;
840
+ org_id: string;
841
+ environment_id: string;
842
+ account_id: string;
843
+ /** A hash of the machine credential, never the credential itself. */
844
+ credential_hash: string;
845
+ name: string;
846
+ os: string;
847
+ arch: string;
848
+ program_version: ColumnType<string | null, string | null | undefined, string | null>;
849
+ mode: ColumnType<"os" | "docker" | null, "os" | "docker" | null | undefined, "os" | "docker" | null>;
850
+ /** The roots the program exposes, as it reported them (a JSON array). */
851
+ roots: ColumnType<unknown, string | null | undefined, string | null>;
852
+ session_isolation: ColumnType<boolean, boolean | undefined, boolean>;
853
+ last_seen_at: TsNullable;
854
+ created_at: Ts;
855
+ revoked_at: TsNullable;
856
+ }
833
857
  /** 0088_environments.sql: the environment a member's new conversations use by default. */
834
858
  export interface EnvironmentDefaultsTable {
835
859
  org_id: string;
@@ -1103,6 +1127,7 @@ export interface KernelDatabase {
1103
1127
  environments: EnvironmentsTable;
1104
1128
  environment_instances: EnvironmentInstancesTable;
1105
1129
  environment_defaults: EnvironmentDefaultsTable;
1130
+ environment_machines: EnvironmentMachinesTable;
1106
1131
  background_tasks: BackgroundTasksTable;
1107
1132
  skills: SkillsTable;
1108
1133
  skill_versions: SkillVersionsTable;
@@ -0,0 +1,42 @@
1
+ import type { FastifyReply, FastifyRequest } from "fastify";
2
+ import type { z } from "zod";
3
+ import { type Principal } from "../identity/principal.js";
4
+ import { CredentialThrottler } from "../infra/throttler/credential-throttler.js";
5
+ import { type DeviceAuthorization, DeviceAuthService, type DeviceGrant, type DeviceMachineInfo } from "./device-auth.js";
6
+ import { type DeviceAuthorizeBody, type DeviceConfirmBody, type DeviceTokenBody, type DeviceUserCodeBody, type DeviceUserCodeParams } from "./dto.js";
7
+ /**
8
+ * Device sign-in (`/api/v1/device`, environments spec §13.3; the shape of RFC 8628).
9
+ *
10
+ * The program's routes are public (it has no credential yet); like every public unsafe route
11
+ * they need `X-Capybara-Csrf: 1`, which `capybara-device` sends:
12
+ * - `POST /authorize { name, os, arch, programVersion }` → `{ deviceCode, userCode,
13
+ * verificationUri, verificationUriComplete, interval, expiresIn }` (`device-authorize-ip`);
14
+ * - `POST /token { deviceCode }` → `{ credential, orgId, environmentId, connectUrl,
15
+ * reconnectGraceSeconds, maxFrameBytes }` once, else `device.authorization_pending` / `device.slow_down` /
16
+ * `device.denied` / `device.expired` (`device-token-ip`).
17
+ *
18
+ * The person's routes need a web session (the confirmation page `/device`); each try of a user
19
+ * code counts on `device-code-account` (refunded on success):
20
+ * - `GET /pending/:userCode` → `{ machine, expiresAt }`;
21
+ * - `POST /confirm { userCode, orgId, environmentId | name }` → `{ environmentId }`
22
+ * (`device.expired` / `device.unavailable` when the credential could not be handed over: the
23
+ * machine is revoked again and the person runs login again);
24
+ * - `POST /deny { userCode }` → 204.
25
+ */
26
+ export declare class DeviceAuthController {
27
+ private readonly auth;
28
+ private readonly throttler;
29
+ constructor(auth: DeviceAuthService, throttler: CredentialThrottler);
30
+ authorize(body: z.infer<typeof DeviceAuthorizeBody>): Promise<DeviceAuthorization>;
31
+ token(body: z.infer<typeof DeviceTokenBody>): Promise<DeviceGrant>;
32
+ pending(principal: Principal, params: z.infer<typeof DeviceUserCodeParams>, req: FastifyRequest, reply: FastifyReply): Promise<{
33
+ machine: DeviceMachineInfo;
34
+ expiresAt: string;
35
+ }>;
36
+ confirm(principal: Principal, body: z.infer<typeof DeviceConfirmBody>, req: FastifyRequest, reply: FastifyReply): Promise<{
37
+ environmentId: string;
38
+ }>;
39
+ deny(principal: Principal, body: z.infer<typeof DeviceUserCodeBody>, req: FastifyRequest, reply: FastifyReply): Promise<void>;
40
+ /** Runs `fn` as one counted try of a user code; a success gives the try back. */
41
+ private counted;
42
+ }