@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
@@ -4,6 +4,7 @@ import { renderBackgroundFinished } from "../../background/render.js";
4
4
  import { isAbsorbable, } from "../../conversations/events.js";
5
5
  import { catalogText, isAlwaysLoaded } from "../../tools/builtins/tool-search.js";
6
6
  import { renderSubagentDelivery } from "../subagents/render.js";
7
+ import { environmentPathsAt, environmentPathsText } from "./environment-paths.js";
7
8
  import { LEGACY_CATALOG_HEADER, LEGACY_SKILL_TEXTS, usesLegacyToolNames } from "./legacy-text.js";
8
9
  import { currentRunContext, runContextEvents, runContextFullText, runContextText, senderLines, } from "./run-context.js";
9
10
  import { invokedSkillNames, invokedSkillsText, SKILL_TEXTS, skillsCatalogEvents, snapshotChangeText, snapshotCurrentText, snapshotSystemText, systemSkillsCatalog, } from "./skills.js";
@@ -336,6 +337,8 @@ export function summaryText(summary) {
336
337
  * `user_message` / `assistant_message`(content 原样,含 opaque;aborted 的也回放;媒体引用按
337
338
  * {@link resolveMedia} 换成 base64 或文字占位)/ `reminder`
338
339
  * (`role: "system"`,放在它所在的位置)/ `tools_changed` (`role: "system"`, the text of {@link toolsChangedText}, at its position) /
340
+ * `environment_paths` (`role: "system"` at its position, `environmentPathsText`; covered by the
341
+ * summary: the latest one follows the summary) /
339
342
  * `run_context` (`role: "system"` at its position, `runContextText`; covered by the summary: the
340
343
  * current context follows the summary whole; a user message's sender line by `senderLines`) /
341
344
  * `background_task_finished`(`role: "system"`, the text of
@@ -547,6 +550,12 @@ export function buildModelRequest(input) {
547
550
  content: runContextFullText(currentRunContext(events, after)),
548
551
  });
549
552
  }
553
+ // environments spec §13.1: the instance's directories told before the boundary follow the
554
+ // summary again (the model keeps using the real paths)
555
+ const paths = environmentPathsAt(events, after);
556
+ if (paths) {
557
+ messages.push({ role: "system", content: environmentPathsText(paths.payload.roots) });
558
+ }
550
559
  // 被摘要覆盖的变更提醒不再发送:摘要时生效的目录与 system 里的不同 → 紧随摘要补上完整的当前目录
551
560
  const covered = skillSnaps.filter((e) => e.seq <= after).at(-1);
552
561
  // (phase B: plus the current pinned skills and tool-name map when they differ from system's)
@@ -620,6 +629,11 @@ export function buildModelRequest(input) {
620
629
  case "run_context":
621
630
  messages.push({ role: "system", content: runContextText(events, e) });
622
631
  break;
632
+ // the instance's directories (environments spec §13.1): logged during a step's tool call,
633
+ // so rendered after that step's tool results
634
+ case "environment_paths":
635
+ messages.push({ role: "system", content: environmentPathsText(e.payload.roots) });
636
+ break;
623
637
  // hook_denied is not model-visible: the model sees only the denied tool call's tool_result
624
638
  // (and nothing for a denied model request), so request building skips it
625
639
  case "hook_denied":
@@ -134,6 +134,7 @@ function transcriptLine(e) {
134
134
  // phase D: a forked skill's report is user content of the request (build.ts)
135
135
  case "skill_fork_result":
136
136
  return `User: ${e.payload.text}`;
137
+ // environment_paths is left out too: the latest one follows the summary (build.ts)
137
138
  // run_context is left out: its current values follow the summary anyway (build.ts), and
138
139
  // stale names should not be summarised as current
139
140
  case "tool_result":
@@ -0,0 +1,40 @@
1
+ import type { SandboxRoots } from "@capybara-hq/plugin-sdk";
2
+ import type { ConversationEvent } from "../../conversations/events.js";
3
+ /**
4
+ * The `environment_paths` event (environments spec §13.1, ruling R22): the first sandbox tool call
5
+ * of every conversation records the instance's roots (the defaults included), and a later call
6
+ * records them again whenever they differ from the conversation's latest recorded ones (a
7
+ * connected machine's real paths, a mode switch back to the defaults). Model-visible: rendered at
8
+ * its position as a system message; in the log, so it needs no `DriftReason`.
9
+ */
10
+ /** The system line of an `environment_paths` event (model-visible, English). */
11
+ export declare function environmentPathsText(r: SandboxRoots): string;
12
+ /** Whether two sets of roots are the same paths. */
13
+ export declare const sameRoots: (a: SandboxRoots, b: SandboxRoots) => boolean;
14
+ /**
15
+ * The idempotency key of an `environment_paths` event for `r` that follows the conversation's
16
+ * event at `prevSeq` (0: none). Unique per change (A → B → A gets three events), while parallel
17
+ * calls that saw the same latest event write it once.
18
+ */
19
+ export declare const environmentPathsIdemKey: (r: SandboxRoots, prevSeq: number) => string;
20
+ /**
21
+ * The `environment_paths` event to append when a sandbox call holds an instance with `roots` and
22
+ * the conversation's latest such event is `latest`; null when nothing changed.
23
+ */
24
+ export declare function environmentPathsChange(latest: {
25
+ seq: number;
26
+ payload: {
27
+ roots: SandboxRoots;
28
+ };
29
+ } | undefined, roots: SandboxRoots, runId: string | undefined): {
30
+ type: "environment_paths";
31
+ runId?: string;
32
+ idemKey: string;
33
+ payload: {
34
+ roots: SandboxRoots;
35
+ };
36
+ } | null;
37
+ /** The latest `environment_paths` event at or before `seq`; undefined without one. */
38
+ export declare function environmentPathsAt(events: readonly ConversationEvent[], seq: number): Extract<ConversationEvent, {
39
+ type: "environment_paths";
40
+ }> | undefined;
@@ -0,0 +1,48 @@
1
+ import { createHash } from "node:crypto";
2
+ /**
3
+ * The `environment_paths` event (environments spec §13.1, ruling R22): the first sandbox tool call
4
+ * of every conversation records the instance's roots (the defaults included), and a later call
5
+ * records them again whenever they differ from the conversation's latest recorded ones (a
6
+ * connected machine's real paths, a mode switch back to the defaults). Model-visible: rendered at
7
+ * its position as a system message; in the log, so it needs no `DriftReason`.
8
+ */
9
+ /** The system line of an `environment_paths` event (model-visible, English). */
10
+ export function environmentPathsText(r) {
11
+ return `This environment's directories: workspace ${r.workspace}, skills ${r.skills} (read only), temporary files ${r.tmp}. Use these paths in commands and tool calls.`;
12
+ }
13
+ /** Whether two sets of roots are the same paths. */
14
+ export const sameRoots = (a, b) => a.workspace === b.workspace && a.skills === b.skills && a.tmp === b.tmp;
15
+ /**
16
+ * The idempotency key of an `environment_paths` event for `r` that follows the conversation's
17
+ * event at `prevSeq` (0: none). Unique per change (A → B → A gets three events), while parallel
18
+ * calls that saw the same latest event write it once.
19
+ */
20
+ export const environmentPathsIdemKey = (r, prevSeq) => `environment_paths:${createHash("sha256")
21
+ .update(JSON.stringify([r.workspace, r.skills, r.tmp]))
22
+ .digest("hex")
23
+ .slice(0, 16)}:${prevSeq}`;
24
+ /**
25
+ * The `environment_paths` event to append when a sandbox call holds an instance with `roots` and
26
+ * the conversation's latest such event is `latest`; null when nothing changed.
27
+ */
28
+ export function environmentPathsChange(latest, roots, runId) {
29
+ if (latest && sameRoots(latest.payload.roots, roots))
30
+ return null;
31
+ return {
32
+ type: "environment_paths",
33
+ ...(runId !== undefined && { runId }),
34
+ idemKey: environmentPathsIdemKey(roots, latest?.seq ?? 0),
35
+ payload: { roots: { workspace: roots.workspace, skills: roots.skills, tmp: roots.tmp } },
36
+ };
37
+ }
38
+ /** The latest `environment_paths` event at or before `seq`; undefined without one. */
39
+ export function environmentPathsAt(events, seq) {
40
+ let found;
41
+ for (const e of events) {
42
+ if (e.seq > seq)
43
+ break;
44
+ if (e.type === "environment_paths")
45
+ found = e;
46
+ }
47
+ return found;
48
+ }
@@ -163,6 +163,8 @@ wrapUpReason) {
163
163
  subagents: input.agent.subagents,
164
164
  child: input.parent !== undefined || input.parentRunId !== undefined || input.subagent === true,
165
165
  forkChild: input.parent?.forSkill === true,
166
+ sandboxConversationId: input.parent?.rootConversationId ?? input.conversationId,
167
+ replay: input.conversationKind === "eval",
166
168
  skillLoad: skillLoadOffered(events.findLast((e) => e.type === "skills_catalog"), hasInvocation(events)),
167
169
  });
168
170
  const tools = {
@@ -1,19 +1,41 @@
1
1
  import { type SandboxProvider } from "@capybara-hq/plugin-sdk";
2
2
  import { ExtensionRegistry } from "../extensions/registry.js";
3
- /** 当前的 `sandbox@2` 提供方:插件名与实现。 */
3
+ /** The kernel-internal provider of connected machines (`device` environments). */
4
+ export declare const DEVICE_PROVIDER = "@device";
5
+ /**
6
+ * A kernel-internal provider name: it starts with `@`, which no extension name can
7
+ * (`EXTENSION_NAME_RE`), so it never collides with a loaded `sandbox@2` plugin.
8
+ */
9
+ export declare const isInternalProvider: (name: string) => boolean;
10
+ /** A sandbox provider: its name (plugin name, or an internal `@…` name) and implementation. */
4
11
  export interface SandboxProviderEntry {
5
12
  plugin: string;
6
13
  impl: SandboxProvider;
7
14
  }
8
15
  /**
9
- * The `sandbox@2` providers of this process, read from the {@link ExtensionRegistry} at every
10
- * call: providers are ready only after the extension load, and change with installs.
16
+ * The sandbox providers of this process. The cloud provider is the loaded `sandbox@2` extension,
17
+ * read from the {@link ExtensionRegistry} at every call (providers are ready only after the
18
+ * extension load, and change with installs). Kernel-internal providers (`@device`) are registered
19
+ * by their module on init, in every replica: they are code, not shared state.
11
20
  */
12
21
  export declare class SandboxProviders {
13
22
  private readonly registry;
23
+ private readonly internal;
14
24
  constructor(registry: Pick<ExtensionRegistry, "list">);
15
- /** The current provider; undefined without one. */
16
- current(): SandboxProviderEntry | undefined;
17
- /** A still-loaded provider by plugin name (to destroy an earlier provider's instance). */
18
- lookup(plugin: string): SandboxProvider | undefined;
25
+ /** Registers a kernel-internal provider (an `@…` name); a later registration replaces it. */
26
+ register(entry: SandboxProviderEntry): void;
27
+ /** The cloud provider (the loaded `sandbox@2` extension); undefined without one. */
28
+ cloud(): SandboxProviderEntry | undefined;
29
+ /**
30
+ * A provider by name: a registered internal provider, else a still-loaded `sandbox@2` extension
31
+ * of that plugin name (to destroy an earlier provider's resources).
32
+ */
33
+ forName(name: string): SandboxProviderEntry | undefined;
19
34
  }
35
+ /**
36
+ * The provider an environment works with, from its `environments.provider`: an internal one
37
+ * (`@device`) by that name only, never falling back to the cloud provider; any other environment
38
+ * is a cloud environment and uses the current cloud provider (files kept by another provider are
39
+ * refused by the sandbox service, `environment.unavailable`).
40
+ */
41
+ export declare function providerForEnvironment(providers: Pick<SandboxProviders, "cloud" | "forName">, provider: string | null): SandboxProviderEntry | undefined;
@@ -10,25 +10,49 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
10
10
  import { SANDBOX_EXTENSION_POINT } from "@capybara-hq/plugin-sdk";
11
11
  import { Inject, Injectable } from "@nestjs/common";
12
12
  import { ExtensionRegistry } from "../extensions/registry.js";
13
+ /** The kernel-internal provider of connected machines (`device` environments). */
14
+ export const DEVICE_PROVIDER = "@device";
13
15
  /**
14
- * The `sandbox@2` providers of this process, read from the {@link ExtensionRegistry} at every
15
- * call: providers are ready only after the extension load, and change with installs.
16
+ * A kernel-internal provider name: it starts with `@`, which no extension name can
17
+ * (`EXTENSION_NAME_RE`), so it never collides with a loaded `sandbox@2` plugin.
18
+ */
19
+ export const isInternalProvider = (name) => name.startsWith("@");
20
+ /**
21
+ * The sandbox providers of this process. The cloud provider is the loaded `sandbox@2` extension,
22
+ * read from the {@link ExtensionRegistry} at every call (providers are ready only after the
23
+ * extension load, and change with installs). Kernel-internal providers (`@device`) are registered
24
+ * by their module on init, in every replica: they are code, not shared state.
16
25
  */
17
26
  let SandboxProviders = class SandboxProviders {
18
27
  registry;
28
+ internal = new Map();
19
29
  constructor(registry) {
20
30
  this.registry = registry;
21
31
  }
22
- /** The current provider; undefined without one. */
23
- current() {
32
+ /** Registers a kernel-internal provider (an `@…` name); a later registration replaces it. */
33
+ register(entry) {
34
+ if (!isInternalProvider(entry.plugin)) {
35
+ throw new Error(`an internal sandbox provider name starts with @: ${entry.plugin}`);
36
+ }
37
+ this.internal.set(entry.plugin, entry);
38
+ }
39
+ /** The cloud provider (the loaded `sandbox@2` extension); undefined without one. */
40
+ cloud() {
24
41
  const e = this.registry.list("sandbox").find((x) => x.capability === SANDBOX_EXTENSION_POINT);
25
42
  return e ? { plugin: e.plugin, impl: e.impl } : undefined;
26
43
  }
27
- /** A still-loaded provider by plugin name (to destroy an earlier provider's instance). */
28
- lookup(plugin) {
29
- return this.registry
44
+ /**
45
+ * A provider by name: a registered internal provider, else a still-loaded `sandbox@2` extension
46
+ * of that plugin name (to destroy an earlier provider's resources).
47
+ */
48
+ forName(name) {
49
+ const own = this.internal.get(name);
50
+ if (own)
51
+ return own;
52
+ const e = this.registry
30
53
  .list("sandbox")
31
- .find((x) => x.capability === SANDBOX_EXTENSION_POINT && x.plugin === plugin)?.impl;
54
+ .find((x) => x.capability === SANDBOX_EXTENSION_POINT && x.plugin === name);
55
+ return e ? { plugin: e.plugin, impl: e.impl } : undefined;
32
56
  }
33
57
  };
34
58
  SandboxProviders = __decorate([
@@ -36,3 +60,14 @@ SandboxProviders = __decorate([
36
60
  __param(0, Inject(ExtensionRegistry))
37
61
  ], SandboxProviders);
38
62
  export { SandboxProviders };
63
+ /**
64
+ * The provider an environment works with, from its `environments.provider`: an internal one
65
+ * (`@device`) by that name only, never falling back to the cloud provider; any other environment
66
+ * is a cloud environment and uses the current cloud provider (files kept by another provider are
67
+ * refused by the sandbox service, `environment.unavailable`).
68
+ */
69
+ export function providerForEnvironment(providers, provider) {
70
+ if (provider !== null && isInternalProvider(provider))
71
+ return providers.forName(provider);
72
+ return providers.cloud();
73
+ }
@@ -1,4 +1,4 @@
1
- import { type ExecResult, type SandboxActor, type SandboxHandle, type SandboxLimits, type SandboxProvider } from "@capybara-hq/plugin-sdk";
1
+ import { type ExecResult, type SandboxActor, type SandboxHandle, type SandboxLimits, type SandboxProvider, type SandboxRoots } from "@capybara-hq/plugin-sdk";
2
2
  import type { Redis } from "ioredis";
3
3
  import { type Selectable } from "kysely";
4
4
  import { BackgroundTaskStore } from "../background/store.js";
@@ -21,6 +21,11 @@ export interface SandboxUseInfo {
21
21
  * new instance on the kept files, so earlier processes are gone.
22
22
  */
23
23
  restarted: boolean;
24
+ /**
25
+ * The instance's roots ({@link rootsOf} of its handle): every model-visible path is built from
26
+ * them (environments spec §13.1; a connected machine's real paths).
27
+ */
28
+ roots: SandboxRoots;
24
29
  }
25
30
  /** Who uses the sandbox in {@link SandboxService.withSandbox}. */
26
31
  export interface SandboxUseContext {
@@ -78,8 +83,12 @@ export type IfRunningResult<T> = {
78
83
  export declare const environmentResourceName: (environmentId: string) => string;
79
84
  /** The environment is missing, deleted, or not in the caller's org (404; ids cannot be probed). */
80
85
  export declare const environmentNotFound: () => ApiError;
81
- /** The environment's file system is gone at the provider (409); never replaced silently. */
82
- export declare const environmentFilesLost: () => ApiError;
86
+ /**
87
+ * The environment's file system is gone at the provider (409); never replaced silently. On a
88
+ * connected machine (`device`, details `[{ source: "device" }]`) the directory is the machine
89
+ * owner's: `capybara-device logout`, then `login` into the environment, recreates it (ruling R24).
90
+ */
91
+ export declare const environmentFilesLost: (device?: boolean) => ApiError;
83
92
  /**
84
93
  * The row still records the instance `gone` (same handle and same `created_at`). Handles compare
85
94
  * canonically: a jsonb round trip reorders a multi-key handle's keys (final review m9).
@@ -146,18 +155,25 @@ export declare const sandboxUnavailable: () => ApiError;
146
155
  * row's `created_at` (set by the fenced write after each `create`); the handle alone is the same
147
156
  * after a rebuild on providers whose handle is the instance name (sandbox-docker).
148
157
  *
158
+ * - **Provider per environment** (environments spec §6): each call resolves the provider from
159
+ * the environment's `environments.provider` ({@link providerForEnvironment}): a `device`
160
+ * environment runs on the kernel-internal `@device` provider (never the cloud one), a cloud
161
+ * environment on the loaded `sandbox@2` extension. A `@device` instance is never suspended or
162
+ * destroyed for inactivity; a deleted `@device` environment (or one of a deleted org) is
163
+ * destroyed by the sweep like any other.
164
+ *
149
165
  * Files kept by another provider than the current one make the environment unusable
150
166
  * (`environment.unavailable`) until that provider is back; deleting it still works while that
151
- * provider is loaded ({@link SandboxProviders.lookup}). Without a provider {@link available} is false and
152
- * uses throw `sandbox.unavailable` (503). No in-process state: replicas share it through PG and
153
- * Redis.
167
+ * provider is loaded ({@link SandboxProviders.forName}). Without the environment's provider
168
+ * {@link availableFor} is false and uses throw `sandbox.unavailable` (503). No in-process state:
169
+ * replicas share it through PG and Redis.
154
170
  */
155
171
  export declare class SandboxService {
156
172
  private readonly db;
157
173
  /** The record of the instance each holder last used (`capk:sandbox-seen:…`). */
158
174
  private readonly redis;
159
175
  private readonly leases;
160
- /** The `sandbox@2` provider (read at every call: providers are ready after the extension load). */
176
+ /** The providers (read at every call: providers are ready after the extension load). */
161
177
  private readonly providers;
162
178
  private readonly config;
163
179
  private readonly log;
@@ -166,14 +182,28 @@ export declare class SandboxService {
166
182
  constructor(db: KernelDb,
167
183
  /** The record of the instance each holder last used (`capk:sandbox-seen:…`). */
168
184
  redis: Pick<Redis, "set">, leases: SandboxLeases,
169
- /** The `sandbox@2` provider (read at every call: providers are ready after the extension load). */
170
- providers: Pick<SandboxProviders, "current" | "lookup">, config: Pick<KernelConfigService, "get">, log: Pick<KernelLogger, "info" | "warn">,
185
+ /** The providers (read at every call: providers are ready after the extension load). */
186
+ providers: Pick<SandboxProviders, "cloud" | "forName">, config: Pick<KernelConfigService, "get">, log: Pick<KernelLogger, "info" | "warn">,
171
187
  /** Background tasks lost while their process may live on (killed after a resume). */
172
188
  background: Pick<BackgroundTaskStore, "pendingKills" | "clearKill">);
173
189
  /** Poll interval while waiting for a lease (the leases' own). */
174
190
  private get pollMs();
175
- /** Whether there is a sandbox provider. */
176
- get available(): boolean;
191
+ /**
192
+ * Whether there is a cloud sandbox provider (a loaded `sandbox@2` extension). For gates that have
193
+ * no environment yet (tool listing, skill files); a call on an environment asks
194
+ * {@link availableFor}.
195
+ */
196
+ get cloudAvailable(): boolean;
197
+ /** Whether the environment's provider is there (`@device` registered, or a cloud provider). */
198
+ availableFor(environmentId: string): Promise<boolean>;
199
+ /**
200
+ * The provider the environment works with, read from its row ({@link providerForEnvironment});
201
+ * a missing row (or one of another org, with `orgId`) reads as a cloud environment, whose use
202
+ * then fails as not found.
203
+ */
204
+ private providerOf;
205
+ /** {@link providerOf}, or `sandbox.unavailable` (503) without it. */
206
+ private require;
177
207
  /** Concurrent holders of one environment (`sandbox.maxConcurrentCommands`, read at every use). */
178
208
  private get cap();
179
209
  /**
@@ -233,11 +263,20 @@ export declare class SandboxService {
233
263
  * - When `fn` throws `SandboxGoneError` (the instance is gone) the instance is rebuilt on the
234
264
  * kept files (or the one another holder rebuilt is taken) and `fn` runs once more with
235
265
  * `restarted: true`; a second `gone` is thrown. Other errors are thrown as they are.
236
- * - Lost files throw `environment.files_lost` (409).
266
+ * - Lost files throw `environment.files_lost` (409), also when `fn` throws
267
+ * `FileSystemGoneError` (the files are then recorded lost, ruling R24).
237
268
  * - When `ctx.holderConversationId` last used another instance (another holder rebuilt it
238
269
  * meanwhile), `info.restarted` is true, so it learns its processes are gone (final review m5).
239
270
  */
240
271
  withSandbox<T>(ctx: SandboxUseContext, fn: (h: SandboxHandle, p: HeldProvider, info: SandboxUseInfo, signal: AbortSignal) => Promise<T>, signal: AbortSignal): Promise<T>;
272
+ /**
273
+ * Ruling R24: an operation on the held instance `e` found its file system gone. Records
274
+ * `fs_status = 'lost'` while the row still records that instance on its file system (the
275
+ * instance handle stays, so a reset still destroys it), moving the fence on, and returns
276
+ * `environment.files_lost` to throw. Uses then fail until the files are reset (a connected
277
+ * machine: until a new login, which resets the row). A failed write only logs.
278
+ */
279
+ private filesGone;
241
280
  /**
242
281
  * Records that `holder` uses the instance of `e` (`capk:sandbox-seen:<env>:<holder>` =
243
282
  * `<created_at>:<file system id>`, with a TTL of the inactivity destruction plus a day). When the
@@ -258,7 +297,10 @@ export declare class SandboxService {
258
297
  private readRow;
259
298
  /** The instance row belongs to another org: reported as not found, so ids cannot be probed. */
260
299
  private foreign;
261
- /** Recorded running with the current provider, and not the instance found gone. */
300
+ /**
301
+ * Recorded running with the current provider on files not lost (lost ones go through the
302
+ * exclusive path, which refuses them), and not the instance found gone.
303
+ */
262
304
  private usable;
263
305
  /**
264
306
  * Under a just-taken shared lease: confirms the row is still `accept`ed (else releases and
@@ -295,15 +337,19 @@ export declare class SandboxService {
295
337
  * (the instance only; the file system is kept and the next use builds a new instance on it); an
296
338
  * environment that was deleted, or whose org was deleted, loses its instance and its file system
297
339
  * and its row is deleted (as {@link destroyEnvironment} does). Environments with any holder are
298
- * skipped. Returns how many it destroyed. Called hourly by the `sandbox-destroy-inactive` cron.
340
+ * skipped. Inactivity never destroys a `@device` instance; a gone `@device` environment is
341
+ * destroyed like any other while `@device` is registered. Returns how many it destroyed. Called
342
+ * hourly by the `sandbox-destroy-inactive` cron.
299
343
  */
300
344
  destroyInactive(now: Date): Promise<number>;
301
345
  /**
302
346
  * Destroys the environment's instance, then its file system, then deletes its instance row
303
347
  * (environment deletion, offboarding); nothing to do without a row. Waits for the exclusive lease
304
- * (with intent, so running commands finish and new ones wait) up to 10 minutes. Without a
305
- * provider it only warns (the destruction sweep finishes a deleted environment later). Files kept
306
- * by another provider that is not loaded cannot be destroyed: it throws and the row stays.
348
+ * (with intent, so running commands finish and new ones wait) up to 10 minutes. Without the
349
+ * environment's provider it throws and the row stays, marked `sweep_failed_at` (the job is
350
+ * retried, and the destruction sweep finishes a deleted environment once the provider is back).
351
+ * Files kept by another provider that is not loaded cannot be destroyed: it throws and the row
352
+ * stays.
307
353
  */
308
354
  destroyEnvironment(environmentId: string): Promise<void>;
309
355
  /**
@@ -329,7 +375,6 @@ export declare class SandboxService {
329
375
  * and leaves the flags of this instance's tasks for the next resume.
330
376
  */
331
377
  private killPendingTasks;
332
- private require;
333
378
  /**
334
379
  * A fenced write: only while the lease holds and the row's generation is still the one read
335
380
  * under it (and incremented); otherwise {@link SandboxLockLostError}.
@@ -390,6 +435,13 @@ export declare class SandboxService {
390
435
  private destroyRow;
391
436
  /** Due for suspension: made by this provider, running, and idle or running for too long. */
392
437
  private suspendDue;
438
+ /** The provider of an instance row: an internal one (`@device`) by name, else the cloud one. */
439
+ private rowProvider;
440
+ /**
441
+ * A row the idle sweeps may touch: not on a kernel-internal provider (`@device` instances live on
442
+ * people's machines and are never suspended or destroyed for inactivity).
443
+ */
444
+ private cloudRow;
393
445
  /** The environment was deleted, or its org was: the instance and the files go. */
394
446
  private environmentGone;
395
447
  /** Due for instance destruction: an instance (or a create in progress) unused for too long. */