@capybara-hq/kernel 0.1.0-rc.7 → 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 (118) 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/identity/me.controller.d.ts +1 -3
  67. package/dist/identity/me.controller.js +1 -9
  68. package/dist/identity/me.schemas.d.ts +14 -0
  69. package/dist/identity/me.schemas.js +17 -0
  70. package/dist/infra/config/kernel-config.service.js +2 -0
  71. package/dist/infra/throttler/named-throttlers.d.ts +5 -0
  72. package/dist/infra/throttler/named-throttlers.js +38 -0
  73. package/dist/kernel.module.js +3 -0
  74. package/dist/lifecycle/kernel-lifecycle.js +5 -0
  75. package/dist/platform/exclusions.js +33 -10
  76. package/dist/platform/operation.js +1 -1
  77. package/dist/platform/ops/environments.ops.js +21 -3
  78. package/dist/platform/ops/platform.ops.d.ts +7 -2
  79. package/dist/platform/ops/platform.ops.js +50 -3
  80. package/dist/platform/route-schemas.js +3 -0
  81. package/dist/roles/gateway-role.d.ts +14 -3
  82. package/dist/roles/gateway-role.js +25 -5
  83. package/dist/roles/gateway.module.d.ts +2 -1
  84. package/dist/roles/gateway.module.js +6 -3
  85. package/dist/runtime/context/build.d.ts +2 -0
  86. package/dist/runtime/context/build.js +14 -0
  87. package/dist/runtime/context/compaction.js +1 -0
  88. package/dist/runtime/context/environment-paths.d.ts +40 -0
  89. package/dist/runtime/context/environment-paths.js +48 -0
  90. package/dist/runtime/model-call.js +2 -0
  91. package/dist/sandbox/providers.d.ts +29 -7
  92. package/dist/sandbox/providers.js +43 -8
  93. package/dist/sandbox/sandbox.service.d.ts +70 -18
  94. package/dist/sandbox/sandbox.service.js +176 -59
  95. package/dist/tools/builtins/sandbox/bash.d.ts +2 -2
  96. package/dist/tools/builtins/sandbox/bash.js +39 -27
  97. package/dist/tools/builtins/sandbox/common.d.ts +60 -25
  98. package/dist/tools/builtins/sandbox/common.js +121 -30
  99. package/dist/tools/builtins/sandbox/deliver.js +9 -6
  100. package/dist/tools/builtins/sandbox/edit.js +40 -37
  101. package/dist/tools/builtins/sandbox/glob.js +13 -8
  102. package/dist/tools/builtins/sandbox/grep.js +10 -6
  103. package/dist/tools/builtins/sandbox/index.d.ts +6 -4
  104. package/dist/tools/builtins/sandbox/index.js +6 -4
  105. package/dist/tools/builtins/sandbox/read.js +9 -6
  106. package/dist/tools/builtins/sandbox/task-output.d.ts +7 -3
  107. package/dist/tools/builtins/sandbox/task-output.js +14 -10
  108. package/dist/tools/builtins/sandbox/task-stop.js +3 -3
  109. package/dist/tools/builtins/sandbox/write.js +33 -30
  110. package/dist/tools/builtins/skill-load.d.ts +8 -5
  111. package/dist/tools/builtins/skill-load.js +21 -15
  112. package/dist/tools/builtins/skill-write.js +2 -2
  113. package/dist/tools/pipeline.js +2 -0
  114. package/dist/tools/registry.d.ts +21 -4
  115. package/dist/tools/registry.js +10 -2
  116. package/package.json +6 -3
  117. package/src/migrations/0093_connected_environments.sql +37 -0
  118. package/src/migrations/0094_environment_paths_idx.sql +7 -0
@@ -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;
@@ -12,6 +12,18 @@ import { BuiltinTools } from "./builtins/builtin-tools.js";
12
12
  import { OrgToolSwitches } from "./org-tool-switches.js";
13
13
  export { BUILTIN_TOOL_NAMES } from "./builtin-names.js";
14
14
  export { canonicalJson } from "./canonical-json.js";
15
+ /**
16
+ * What a tool's `unavailable` is asked with: the org, and for a run's listing the root
17
+ * conversation (`conversationId`; the sandbox tools follow its environment, ruling R8), whether
18
+ * the run is an eval replay (`replay`: its tool calls are never executed), and a `memo` shared by
19
+ * the tools of one listing (one answer per question per listing).
20
+ */
21
+ export interface ToolListingScope {
22
+ orgId: string;
23
+ conversationId?: string;
24
+ replay?: boolean;
25
+ memo?: Map<string, unknown>;
26
+ }
15
27
  /** A problem of a saved definition's `disabledTools` (`path` is `disabledTools.<i>`). */
16
28
  export interface ToolRefIssue {
17
29
  path: string;
@@ -82,11 +94,9 @@ export interface BuiltinTool {
82
94
  /**
83
95
  * The factual reason this tool is unavailable for the org right now (English, actionable; it goes
84
96
  * into the "tool unavailable" reminder), or null when it is available. May be async (e.g. a
85
- * per-org extension-point default).
97
+ * per-org extension-point default). See {@link ToolListingScope} for what a listing passes.
86
98
  */
87
- unavailable?: (ctx: {
88
- orgId: string;
89
- }) => Promise<string | null> | string | null;
99
+ unavailable?: (ctx: ToolListingScope) => Promise<string | null> | string | null;
90
100
  /**
91
101
  * A policy key derived from the validated input (Platform tool spec §6.2), or null to use the
92
102
  * tool key. With a key, agent and parent policies, grants and approvals are keyed by it (an
@@ -334,6 +344,13 @@ export declare class ToolRegistry {
334
344
  * it reaches its parent with neither tool (`SendMessage` only to its own sub-agents).
335
345
  */
336
346
  forkChild?: boolean;
347
+ /**
348
+ * The root conversation of the run (a sub-agent's tree root): tools whose availability
349
+ * depends on the conversation's environment (the sandbox tools) are listed by it.
350
+ */
351
+ sandboxConversationId?: string;
352
+ /** An eval replay's listing: its calls are never executed (the sandbox tools are listed). */
353
+ replay?: boolean;
337
354
  }): Promise<AgentTools>;
338
355
  /** 是否启用 `ToolSearch`(见 {@link forAgent});先比阈值,超过才读组织设置。 */
339
356
  private searchActive;
@@ -348,6 +348,14 @@ let ToolRegistry = class ToolRegistry {
348
348
  }
349
349
  async resolve(orgId, def) {
350
350
  const disabled = def.disabledTools ?? [];
351
+ const scope = {
352
+ orgId,
353
+ ...(def.sandboxConversationId !== undefined && {
354
+ conversationId: def.sandboxConversationId,
355
+ }),
356
+ ...(def.replay === true && { replay: true }),
357
+ memo: new Map(),
358
+ };
351
359
  const tools = [];
352
360
  const unavailable = [];
353
361
  const hidden = [];
@@ -373,7 +381,7 @@ let ToolRegistry = class ToolRegistry {
373
381
  const b = this.builtins.find((x) => x.spec.name === name);
374
382
  if (!b)
375
383
  continue;
376
- const why = (await b.unavailable?.({ orgId })) ?? null;
384
+ const why = (await b.unavailable?.(scope)) ?? null;
377
385
  if (why !== null) {
378
386
  hidden.push({ ref: name, reason: why });
379
387
  continue;
@@ -392,7 +400,7 @@ let ToolRegistry = class ToolRegistry {
392
400
  if (name === BUILTIN.Bash) {
393
401
  for (const companion of BASH_COMPANION_TOOLS) {
394
402
  const c = this.builtins.find((x) => x.spec.name === companion);
395
- if (c && ((await c.unavailable?.({ orgId })) ?? null) === null) {
403
+ if (c && ((await c.unavailable?.(scope)) ?? null) === null) {
396
404
  add(builtinTool(c, c.spec));
397
405
  }
398
406
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capybara-hq/kernel",
3
- "version": "0.1.0-rc.7",
3
+ "version": "0.1.0-rc.9",
4
4
  "description": "Capybara kernel: config, database, plugin host and HTTP API of the Capybara agent platform",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -32,6 +32,7 @@
32
32
  "dependencies": {
33
33
  "@fastify/cookie": "^11.1.3",
34
34
  "@fastify/static": "^9.3.0",
35
+ "@fastify/websocket": "^11.3.7",
35
36
  "@langchain/core": "^1.2.18",
36
37
  "@langchain/langgraph": "^1.5.1",
37
38
  "@langchain/langgraph-checkpoint": "^1.2.0",
@@ -64,14 +65,16 @@
64
65
  "yaml": "^2.9.1",
65
66
  "yauzl": "3.4.0",
66
67
  "zod": "^4.6.7",
67
- "@capybara-hq/plugin-sdk": "^0.1.0-rc.5"
68
+ "@capybara-hq/plugin-sdk": "^0.1.0-rc.6"
68
69
  },
69
70
  "devDependencies": {
70
71
  "@langchain/langgraph-checkpoint-validation": "^1.2.0",
71
72
  "@types/pg": "^8.23.1",
72
73
  "@types/semver": "^7.8.0",
74
+ "@types/ws": "^8.18.2",
73
75
  "@types/yauzl": "3.4.0",
74
- "@capybara-hq/testing": "0.1.0-rc.8"
76
+ "ws": "^8.22.0",
77
+ "@capybara-hq/testing": "0.1.0-rc.9"
75
78
  },
76
79
  "scripts": {
77
80
  "build": "tsc -b"
@@ -0,0 +1,37 @@
1
+ -- Environments phase 2 (spec 2026-11-07-environments-design §6, §13): a person's own machine,
2
+ -- connected through the device program, becomes an environment with source 'device'. Expand-only:
3
+ -- the source CHECK list grows and a new table holds the connected machine.
4
+
5
+ -- 0088 left the CHECK unnamed, so Postgres named it environments_source_check; re-add it under
6
+ -- the same name with the longer list.
7
+ ALTER TABLE environments DROP CONSTRAINT IF EXISTS environments_source_check;
8
+ ALTER TABLE environments
9
+ ADD CONSTRAINT environments_source_check CHECK (source IN ('cloud', 'device'));
10
+
11
+ -- The machine connected to a device environment. credential_hash is a hash of the machine's
12
+ -- long-lived credential (cap_dev_...), never the credential itself; revoking a machine sets
13
+ -- revoked_at and keeps the row, so only one unrevoked machine exists per environment.
14
+ -- program_version, mode, roots and session_isolation are what the program reported on its last
15
+ -- connection; last_seen_at is the last time it connected or pinged.
16
+ CREATE TABLE IF NOT EXISTS environment_machines (
17
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
18
+ org_id text NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
19
+ environment_id text NOT NULL REFERENCES environments(id) ON DELETE CASCADE,
20
+ account_id text NOT NULL,
21
+ credential_hash text NOT NULL UNIQUE,
22
+ name text NOT NULL,
23
+ os text NOT NULL,
24
+ arch text NOT NULL,
25
+ program_version text,
26
+ mode text CHECK (mode IN ('os', 'docker')),
27
+ roots jsonb,
28
+ session_isolation boolean NOT NULL DEFAULT false,
29
+ last_seen_at timestamptz,
30
+ created_at timestamptz NOT NULL DEFAULT now(),
31
+ revoked_at timestamptz
32
+ );
33
+ CREATE UNIQUE INDEX IF NOT EXISTS environment_machines_active_uq
34
+ ON environment_machines (environment_id)
35
+ WHERE revoked_at IS NULL;
36
+ -- The org cascade delete looks rows up by org (as environment_instances_org_idx in 0088).
37
+ CREATE INDEX IF NOT EXISTS environment_machines_org_idx ON environment_machines (org_id);
@@ -0,0 +1,7 @@
1
+ -- Environments phase 2 (spec 2026-11-07-environments-design §13.1, ruling R22): every sandbox tool
2
+ -- call reads the conversation's latest environment_paths event; a partial index answers it
3
+ -- without scanning the conversation's events backwards. Not CONCURRENTLY, like the other index
4
+ -- migrations (0025).
5
+ CREATE INDEX IF NOT EXISTS conversation_events_environment_paths_idx
6
+ ON conversation_events (conversation_id, seq)
7
+ WHERE type = 'environment_paths';