@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.
- package/dist/background/finish.js +2 -1
- package/dist/background/launch.d.ts +13 -7
- package/dist/background/launch.js +15 -8
- package/dist/background/poll.job.d.ts +1 -1
- package/dist/background/poll.job.js +8 -3
- package/dist/background/session.d.ts +2 -2
- package/dist/background/session.js +3 -2
- package/dist/background/status.d.ts +1 -1
- package/dist/background/status.js +1 -1
- package/dist/background/stop.js +4 -3
- package/dist/background/store.d.ts +6 -1
- package/dist/background/store.js +5 -1
- package/dist/background/tasks.service.d.ts +1 -1
- package/dist/background/tasks.service.js +4 -4
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/config/docs.js +2 -0
- package/dist/config/schema.d.ts +7 -0
- package/dist/config/schema.js +45 -0
- package/dist/conversations/event-log.d.ts +4 -3
- package/dist/conversations/event-log.js +4 -3
- package/dist/conversations/events.d.ts +9 -0
- package/dist/conversations/events.js +14 -0
- package/dist/db/kernel-schema.d.ts +26 -1
- package/dist/devices/device-auth.controller.d.ts +42 -0
- package/dist/devices/device-auth.controller.js +116 -0
- package/dist/devices/device-auth.d.ts +119 -0
- package/dist/devices/device-auth.js +403 -0
- package/dist/devices/device-auth.module.d.ts +7 -0
- package/dist/devices/device-auth.module.js +25 -0
- package/dist/devices/device-gateway.d.ts +128 -0
- package/dist/devices/device-gateway.js +530 -0
- package/dist/devices/device-provider.d.ts +89 -0
- package/dist/devices/device-provider.js +151 -0
- package/dist/devices/device-routes.d.ts +36 -0
- package/dist/devices/device-routes.js +90 -0
- package/dist/devices/devices.module.d.ts +16 -0
- package/dist/devices/devices.module.js +44 -0
- package/dist/devices/dto.d.ts +66 -0
- package/dist/devices/dto.js +44 -0
- package/dist/devices/keys.d.ts +25 -0
- package/dist/devices/keys.js +25 -0
- package/dist/devices/machines.d.ts +106 -0
- package/dist/devices/machines.js +301 -0
- package/dist/devices/relay.d.ts +100 -0
- package/dist/devices/relay.js +373 -0
- package/dist/devices/targets.d.ts +28 -0
- package/dist/devices/targets.js +73 -0
- package/dist/environments/access.d.ts +11 -6
- package/dist/environments/access.js +23 -8
- package/dist/environments/conversation-environments.d.ts +20 -6
- package/dist/environments/conversation-environments.js +70 -9
- package/dist/environments/environments.controller.d.ts +4 -1
- package/dist/environments/environments.controller.js +15 -1
- package/dist/environments/environments.module.js +3 -1
- package/dist/environments/environments.service.d.ts +54 -7
- package/dist/environments/environments.service.js +104 -20
- package/dist/environments/legacy-cleanup.job.d.ts +1 -1
- package/dist/environments/legacy-cleanup.job.js +4 -4
- package/dist/errors.d.ts +24 -0
- package/dist/errors.js +24 -0
- package/dist/evals/recorded-tools.d.ts +9 -0
- package/dist/evals/recorded-tools.js +27 -0
- package/dist/http/api.module.js +4 -0
- package/dist/http/gateway-routes.d.ts +7 -3
- package/dist/http/gateway-routes.js +6 -3
- package/dist/identity/me.controller.d.ts +1 -3
- package/dist/identity/me.controller.js +1 -9
- package/dist/identity/me.schemas.d.ts +14 -0
- package/dist/identity/me.schemas.js +17 -0
- package/dist/infra/config/kernel-config.service.js +2 -0
- package/dist/infra/throttler/named-throttlers.d.ts +5 -0
- package/dist/infra/throttler/named-throttlers.js +38 -0
- package/dist/kernel.module.js +3 -0
- package/dist/lifecycle/kernel-lifecycle.js +5 -0
- package/dist/platform/exclusions.js +33 -10
- package/dist/platform/operation.js +1 -1
- package/dist/platform/ops/environments.ops.js +21 -3
- package/dist/platform/ops/platform.ops.d.ts +7 -2
- package/dist/platform/ops/platform.ops.js +50 -3
- package/dist/platform/route-schemas.js +3 -0
- package/dist/roles/gateway-role.d.ts +14 -3
- package/dist/roles/gateway-role.js +25 -5
- package/dist/roles/gateway.module.d.ts +2 -1
- package/dist/roles/gateway.module.js +6 -3
- package/dist/runtime/context/build.d.ts +2 -0
- package/dist/runtime/context/build.js +14 -0
- package/dist/runtime/context/compaction.js +1 -0
- package/dist/runtime/context/environment-paths.d.ts +40 -0
- package/dist/runtime/context/environment-paths.js +48 -0
- package/dist/runtime/model-call.js +2 -0
- package/dist/sandbox/providers.d.ts +29 -7
- package/dist/sandbox/providers.js +43 -8
- package/dist/sandbox/sandbox.service.d.ts +70 -18
- package/dist/sandbox/sandbox.service.js +176 -59
- package/dist/tools/builtins/sandbox/bash.d.ts +2 -2
- package/dist/tools/builtins/sandbox/bash.js +39 -27
- package/dist/tools/builtins/sandbox/common.d.ts +60 -25
- package/dist/tools/builtins/sandbox/common.js +121 -30
- package/dist/tools/builtins/sandbox/deliver.js +9 -6
- package/dist/tools/builtins/sandbox/edit.js +40 -37
- package/dist/tools/builtins/sandbox/glob.js +13 -8
- package/dist/tools/builtins/sandbox/grep.js +10 -6
- package/dist/tools/builtins/sandbox/index.d.ts +6 -4
- package/dist/tools/builtins/sandbox/index.js +6 -4
- package/dist/tools/builtins/sandbox/read.js +9 -6
- package/dist/tools/builtins/sandbox/task-output.d.ts +7 -3
- package/dist/tools/builtins/sandbox/task-output.js +14 -10
- package/dist/tools/builtins/sandbox/task-stop.js +3 -3
- package/dist/tools/builtins/sandbox/write.js +33 -30
- package/dist/tools/builtins/skill-load.d.ts +8 -5
- package/dist/tools/builtins/skill-load.js +21 -15
- package/dist/tools/builtins/skill-write.js +2 -2
- package/dist/tools/pipeline.js +2 -0
- package/dist/tools/registry.d.ts +21 -4
- package/dist/tools/registry.js +10 -2
- package/package.json +6 -3
- package/src/migrations/0093_connected_environments.sql +37 -0
- package/src/migrations/0094_environment_paths_idx.sql +7 -0
|
@@ -12,7 +12,7 @@ import { ApiError } from "../errors.js";
|
|
|
12
12
|
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
13
13
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
14
14
|
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
15
|
-
import { SandboxProviders } from "../sandbox/providers.js";
|
|
15
|
+
import { isInternalProvider, SandboxProviders } from "../sandbox/providers.js";
|
|
16
16
|
import { environmentNotFound, sandboxUnavailable } from "../sandbox/sandbox.service.js";
|
|
17
17
|
import { EnvironmentAccess } from "./access.js";
|
|
18
18
|
import { EnvironmentsService } from "./environments.service.js";
|
|
@@ -45,13 +45,14 @@ let ConversationEnvironments = class ConversationEnvironments {
|
|
|
45
45
|
* The environment a new conversation of `accountId` is bound to: the chosen one when it is
|
|
46
46
|
* usable by the person (else `environment.not_found` / `environment.unavailable`, and nothing
|
|
47
47
|
* is created); without a choice the IM default (IM only) or the person's default environment.
|
|
48
|
-
* Null (unbound) without a sandbox provider
|
|
48
|
+
* Null (unbound) for anonymous IM senders, and without a cloud sandbox provider when no usable
|
|
49
|
+
* IM default (a connected machine's environment, say) applies.
|
|
49
50
|
*/
|
|
50
51
|
async forNew(orgId, accountId, pick = {}) {
|
|
51
52
|
if (pick.environmentId !== undefined) {
|
|
52
53
|
return (await this.access.usable(orgId, accountId, pick.environmentId)).id;
|
|
53
54
|
}
|
|
54
|
-
if (this.providers.
|
|
55
|
+
if (pick.im !== true && this.providers.cloud() === undefined)
|
|
55
56
|
return null;
|
|
56
57
|
if (await this.isAnonymous(orgId, accountId))
|
|
57
58
|
return null;
|
|
@@ -62,12 +63,12 @@ let ConversationEnvironments = class ConversationEnvironments {
|
|
|
62
63
|
* enters, checked for its owner now. A conversation still unbound (created before
|
|
63
64
|
* environments, or while no provider existed) is bound first, to the owner's IM default when it
|
|
64
65
|
* is an IM thread, else to the owner's default environment; a racing call binds it once.
|
|
65
|
-
* Throws `sandbox.unavailable`
|
|
66
|
-
*
|
|
66
|
+
* Throws `sandbox.unavailable` for a cloud environment (or none to bind) without a cloud
|
|
67
|
+
* provider and for an anonymous IM conversation, and `environment.not_found` /
|
|
68
|
+
* `environment.unavailable` when the owner may not use it now. A conversation bound to a
|
|
69
|
+
* connected machine's environment (`@device`) does not need a cloud provider.
|
|
67
70
|
*/
|
|
68
71
|
async forTool(orgId, rootConversationId) {
|
|
69
|
-
if (this.providers.current() === undefined)
|
|
70
|
-
throw sandboxUnavailable();
|
|
71
72
|
const conv = await this.db
|
|
72
73
|
.selectFrom("conversations")
|
|
73
74
|
.select(["account_id", "environment_id"])
|
|
@@ -82,14 +83,62 @@ let ConversationEnvironments = class ConversationEnvironments {
|
|
|
82
83
|
if (environmentId === null) {
|
|
83
84
|
const im = await this.isImThread(rootConversationId);
|
|
84
85
|
const chosen = await this.defaultFor(orgId, conv.account_id, im);
|
|
86
|
+
if (chosen === null)
|
|
87
|
+
throw sandboxUnavailable();
|
|
85
88
|
environmentId = await this.bind(rootConversationId, chosen);
|
|
86
89
|
}
|
|
90
|
+
if (this.providers.cloud() === undefined && !(await this.isInternal(environmentId))) {
|
|
91
|
+
throw sandboxUnavailable();
|
|
92
|
+
}
|
|
87
93
|
return (await this.access.usable(orgId, conv.account_id, environmentId)).id;
|
|
88
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Whether a sandbox tool call of the tree rooted at `rootConversationId` has a provider to run
|
|
97
|
+
* on, for tool listings (ruling R8; no binding, no access check, never throws): a cloud
|
|
98
|
+
* provider is loaded, or the environment the conversation is bound to (an unbound IM thread:
|
|
99
|
+
* its owner's IM default) runs on a registered kernel provider (`@device`). A forbidden or gone
|
|
100
|
+
* environment still counts: the call then says why.
|
|
101
|
+
*/
|
|
102
|
+
async usableNow(orgId, rootConversationId) {
|
|
103
|
+
if (this.providers.cloud() !== undefined)
|
|
104
|
+
return true;
|
|
105
|
+
const conv = await this.db
|
|
106
|
+
.selectFrom("conversations")
|
|
107
|
+
.select(["account_id", "environment_id"])
|
|
108
|
+
.where("id", "=", rootConversationId)
|
|
109
|
+
.where("org_id", "=", orgId)
|
|
110
|
+
.executeTakeFirst();
|
|
111
|
+
if (!conv)
|
|
112
|
+
return false;
|
|
113
|
+
let environmentId = conv.environment_id;
|
|
114
|
+
if (environmentId === null) {
|
|
115
|
+
if (!(await this.isImThread(rootConversationId)))
|
|
116
|
+
return false;
|
|
117
|
+
const row = await this.db
|
|
118
|
+
.selectFrom("environment_defaults")
|
|
119
|
+
.select("environment_id")
|
|
120
|
+
.where("org_id", "=", orgId)
|
|
121
|
+
.where("account_id", "=", conv.account_id)
|
|
122
|
+
.executeTakeFirst();
|
|
123
|
+
if (!row)
|
|
124
|
+
return false;
|
|
125
|
+
environmentId = row.environment_id;
|
|
126
|
+
}
|
|
127
|
+
const env = await this.db
|
|
128
|
+
.selectFrom("environments")
|
|
129
|
+
.select("provider")
|
|
130
|
+
.where("id", "=", environmentId)
|
|
131
|
+
.executeTakeFirst();
|
|
132
|
+
const provider = env?.provider ?? null;
|
|
133
|
+
return (provider !== null &&
|
|
134
|
+
isInternalProvider(provider) &&
|
|
135
|
+
this.providers.forName?.(provider) !== undefined);
|
|
136
|
+
}
|
|
89
137
|
/**
|
|
90
138
|
* The person's IM default when `im` and it is still theirs, live, in this org and usable (the
|
|
91
|
-
* table has no such guard), else their default environment (created when missing)
|
|
92
|
-
* having left the org
|
|
139
|
+
* table has no such guard), else their default environment (created when missing); null when
|
|
140
|
+
* that is needed and there is no cloud provider to make or use it. The owner having left the org
|
|
141
|
+
* reads as `environment.not_found`.
|
|
93
142
|
*/
|
|
94
143
|
async defaultFor(orgId, accountId, im) {
|
|
95
144
|
if (im) {
|
|
@@ -109,6 +158,8 @@ let ConversationEnvironments = class ConversationEnvironments {
|
|
|
109
158
|
}
|
|
110
159
|
}
|
|
111
160
|
}
|
|
161
|
+
if (this.providers.cloud() === undefined)
|
|
162
|
+
return null;
|
|
112
163
|
try {
|
|
113
164
|
return await this.environments.ensureDefault(orgId, accountId);
|
|
114
165
|
}
|
|
@@ -133,6 +184,16 @@ let ConversationEnvironments = class ConversationEnvironments {
|
|
|
133
184
|
.executeTakeFirstOrThrow();
|
|
134
185
|
return row.environment_id ?? environmentId;
|
|
135
186
|
}
|
|
187
|
+
/** The environment runs on a kernel-internal provider (`@device`). */
|
|
188
|
+
async isInternal(environmentId) {
|
|
189
|
+
const row = await this.db
|
|
190
|
+
.selectFrom("environments")
|
|
191
|
+
.select("provider")
|
|
192
|
+
.where("id", "=", environmentId)
|
|
193
|
+
.executeTakeFirst();
|
|
194
|
+
const provider = row?.provider ?? null;
|
|
195
|
+
return provider !== null && isInternalProvider(provider);
|
|
196
|
+
}
|
|
136
197
|
/** The account is the org's anonymous IM guest (no person behind it). */
|
|
137
198
|
async isAnonymous(orgId, accountId) {
|
|
138
199
|
const row = await this.db
|
|
@@ -13,7 +13,9 @@ import { type EnvironmentList, EnvironmentsService, type EnvironmentView } from
|
|
|
13
13
|
* - `POST / { name }` → 201 `{ environment }`; `GET /:id`, `PATCH /:id { name }` → `{ environment }`;
|
|
14
14
|
* - `DELETE /:id` → 202 (destroyed by a job after the response);
|
|
15
15
|
* - `POST /:id/files/reset` → 202 (the reset is enqueued: a worker empties the files, the next use
|
|
16
|
-
* starts empty)
|
|
16
|
+
* starts empty);
|
|
17
|
+
* - `DELETE /:id/machine` → 204: a device environment's machine is disconnected (its credential
|
|
18
|
+
* revoked; it must sign in again).
|
|
17
19
|
*/
|
|
18
20
|
export declare class EnvironmentsController {
|
|
19
21
|
private readonly environments;
|
|
@@ -30,4 +32,5 @@ export declare class EnvironmentsController {
|
|
|
30
32
|
}>;
|
|
31
33
|
remove(principal: Principal, org: OrgContext, params: z.infer<typeof EnvironmentParams>): Promise<void>;
|
|
32
34
|
resetFiles(principal: Principal, org: OrgContext, params: z.infer<typeof EnvironmentParams>): Promise<void>;
|
|
35
|
+
revokeMachine(principal: Principal, org: OrgContext, params: z.infer<typeof EnvironmentParams>): Promise<void>;
|
|
33
36
|
}
|
|
@@ -23,7 +23,9 @@ import { EnvironmentsService, } from "./environments.service.js";
|
|
|
23
23
|
* - `POST / { name }` → 201 `{ environment }`; `GET /:id`, `PATCH /:id { name }` → `{ environment }`;
|
|
24
24
|
* - `DELETE /:id` → 202 (destroyed by a job after the response);
|
|
25
25
|
* - `POST /:id/files/reset` → 202 (the reset is enqueued: a worker empties the files, the next use
|
|
26
|
-
* starts empty)
|
|
26
|
+
* starts empty);
|
|
27
|
+
* - `DELETE /:id/machine` → 204: a device environment's machine is disconnected (its credential
|
|
28
|
+
* revoked; it must sign in again).
|
|
27
29
|
*/
|
|
28
30
|
let EnvironmentsController = class EnvironmentsController {
|
|
29
31
|
environments;
|
|
@@ -58,6 +60,10 @@ let EnvironmentsController = class EnvironmentsController {
|
|
|
58
60
|
const p = requireUser(principal);
|
|
59
61
|
await this.environments.resetFiles(org.orgId, p.account.id, params.id, patTokenId(p));
|
|
60
62
|
}
|
|
63
|
+
async revokeMachine(principal, org, params) {
|
|
64
|
+
const p = requireUser(principal);
|
|
65
|
+
await this.environments.revokeMachine(org.orgId, p.account.id, params.id, patTokenId(p));
|
|
66
|
+
}
|
|
61
67
|
};
|
|
62
68
|
__decorate([
|
|
63
69
|
Get(),
|
|
@@ -103,6 +109,14 @@ __decorate([
|
|
|
103
109
|
__param(1, CurrentOrg()),
|
|
104
110
|
__param(2, ZParam(EnvironmentParamsDto))
|
|
105
111
|
], EnvironmentsController.prototype, "resetFiles", null);
|
|
112
|
+
__decorate([
|
|
113
|
+
Delete(":id/machine"),
|
|
114
|
+
HttpCode(204),
|
|
115
|
+
OrgAction("agents.use"),
|
|
116
|
+
__param(0, CurrentPrincipal()),
|
|
117
|
+
__param(1, CurrentOrg()),
|
|
118
|
+
__param(2, ZParam(EnvironmentParamsDto))
|
|
119
|
+
], EnvironmentsController.prototype, "revokeMachine", null);
|
|
106
120
|
EnvironmentsController = __decorate([
|
|
107
121
|
Controller("api/v1/orgs/:orgId/environments"),
|
|
108
122
|
__param(0, Inject(EnvironmentsService))
|
|
@@ -5,6 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
7
|
import { Module } from "@nestjs/common";
|
|
8
|
+
import { DevicesModule } from "../devices/devices.module.js";
|
|
8
9
|
import { SandboxModule } from "../sandbox/sandbox.module.js";
|
|
9
10
|
import { EnvironmentAccess } from "./access.js";
|
|
10
11
|
import { ConversationEnvironments } from "./conversation-environments.js";
|
|
@@ -19,7 +20,8 @@ let EnvironmentsModule = class EnvironmentsModule {
|
|
|
19
20
|
};
|
|
20
21
|
EnvironmentsModule = __decorate([
|
|
21
22
|
Module({
|
|
22
|
-
|
|
23
|
+
// DevicesModule: deleting a device environment revokes its machine
|
|
24
|
+
imports: [SandboxModule, DevicesModule],
|
|
23
25
|
providers: [EnvironmentAccess, EnvironmentsService, ConversationEnvironments],
|
|
24
26
|
exports: [EnvironmentAccess, EnvironmentsService, ConversationEnvironments],
|
|
25
27
|
})
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { SandboxLimits } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
import { MachinesService } from "../devices/machines.js";
|
|
2
3
|
import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
3
4
|
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
4
5
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
6
|
+
import { LockService } from "../infra/lock/lock.service.js";
|
|
5
7
|
import type { KernelLogger } from "../log.js";
|
|
6
8
|
import { SandboxJobs } from "../sandbox/jobs.js";
|
|
7
9
|
import { SandboxProviders } from "../sandbox/providers.js";
|
|
@@ -14,11 +16,16 @@ export interface EnvironmentView {
|
|
|
14
16
|
name: string | null;
|
|
15
17
|
isDefault: boolean;
|
|
16
18
|
ownerKind: "personal";
|
|
17
|
-
|
|
19
|
+
/** `device`: a connected machine (provider `@device`). */
|
|
20
|
+
source: "cloud" | "device";
|
|
18
21
|
/** The provider keeping its files, or the one its first use will create them with. */
|
|
19
22
|
provider: string | null;
|
|
20
|
-
/** Its provider is the deployment's current
|
|
23
|
+
/** Its provider is there: the deployment's current cloud provider, or `@device` registered. */
|
|
21
24
|
available: boolean;
|
|
25
|
+
/** A device environment: some replica holds its machine's connection now; null for cloud. */
|
|
26
|
+
online: boolean | null;
|
|
27
|
+
/** A device environment's connected machine (its unrevoked one); null for cloud or none. */
|
|
28
|
+
machine: EnvironmentMachineView | null;
|
|
22
29
|
sessionIsolation: boolean;
|
|
23
30
|
/** Its file system is gone at the provider; reset the files to use it again. */
|
|
24
31
|
filesLost: boolean;
|
|
@@ -30,6 +37,15 @@ export interface EnvironmentView {
|
|
|
30
37
|
limits: SandboxLimits;
|
|
31
38
|
createdAt: string;
|
|
32
39
|
}
|
|
40
|
+
/** The machine connected to a device environment, as its owner sees it. */
|
|
41
|
+
export interface EnvironmentMachineView {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
os: string;
|
|
45
|
+
mode: "os" | "docker" | null;
|
|
46
|
+
programVersion: string | null;
|
|
47
|
+
lastSeenAt: string | null;
|
|
48
|
+
}
|
|
33
49
|
/** The list response: the person's live environments in the org. */
|
|
34
50
|
export interface EnvironmentList {
|
|
35
51
|
items: EnvironmentView[];
|
|
@@ -41,7 +57,8 @@ export interface EnvironmentList {
|
|
|
41
57
|
cloudAvailable: boolean;
|
|
42
58
|
}
|
|
43
59
|
/**
|
|
44
|
-
* Personal
|
|
60
|
+
* Personal environments (spec 2026-11-07-environments-design §4.2 items 7, 12, 13; §6; §9):
|
|
61
|
+
* cloud ones on the deployment's sandbox provider and `device` ones on a connected machine; a
|
|
45
62
|
* person's environments in an org, visible and usable only by their owner (anyone else gets
|
|
46
63
|
* `environment.not_found`). Deleting one soft-deletes it and destroys its instance and files in a
|
|
47
64
|
* `sandbox-destroy-for` job after the commit.
|
|
@@ -55,7 +72,12 @@ export declare class EnvironmentsService {
|
|
|
55
72
|
private readonly sandboxJobs;
|
|
56
73
|
private readonly access;
|
|
57
74
|
private readonly log;
|
|
58
|
-
|
|
75
|
+
/** Reads the device relay's lease (`online`). */
|
|
76
|
+
private readonly locks;
|
|
77
|
+
private readonly machines;
|
|
78
|
+
constructor(db: KernelDb, txHost: TransactionHost, config: Pick<KernelConfigService, "get">, providers: Pick<SandboxProviders, "cloud" | "forName">, sandbox: Pick<SandboxService, "orgLimits">, sandboxJobs: Pick<SandboxJobs, "destroy" | "resetFiles">, access: Pick<EnvironmentAccess, "usable">, log: Pick<KernelLogger, "warn">,
|
|
79
|
+
/** Reads the device relay's lease (`online`). */
|
|
80
|
+
locks: Pick<LockService, "holderOf">, machines: Pick<MachinesService, "revoke" | "revokeActive">);
|
|
59
81
|
/**
|
|
60
82
|
* The person's live environments in the org, the default first, then by creation. With a
|
|
61
83
|
* current sandbox provider the default environment is created first if missing (its first need,
|
|
@@ -74,6 +96,20 @@ export declare class EnvironmentsService {
|
|
|
74
96
|
name: string;
|
|
75
97
|
}, actorTokenId?: string, auditDetail?: Record<string, unknown>): Promise<EnvironmentView>;
|
|
76
98
|
private insert;
|
|
99
|
+
/**
|
|
100
|
+
* A new personal `device` environment for a machine being connected (provider `@device`; the
|
|
101
|
+
* machine row is the caller's). Joins the caller's transaction (the device authorization's).
|
|
102
|
+
* Counted against `environments.maxPersonal` (`environment.limit_reached`); a live name of the
|
|
103
|
+
* person's → `environment.name_taken`. Needs no cloud provider.
|
|
104
|
+
*/
|
|
105
|
+
createDevice(orgId: string, accountId: string, name: string): Promise<{
|
|
106
|
+
id: string;
|
|
107
|
+
}>;
|
|
108
|
+
/**
|
|
109
|
+
* Inserts a named personal environment under the person's membership lock, after the limit
|
|
110
|
+
* and name checks, and audits `environment.created`. Runs in the caller's transaction.
|
|
111
|
+
*/
|
|
112
|
+
private insertNamed;
|
|
77
113
|
/**
|
|
78
114
|
* The refusals {@link create} would raise, read without locks or writes (a precheck before a
|
|
79
115
|
* confirmation card): no sandbox provider, the person's limit, a name already taken. Throws the
|
|
@@ -82,7 +118,10 @@ export declare class EnvironmentsService {
|
|
|
82
118
|
checkCreate(orgId: string, accountId: string, name: string): Promise<void>;
|
|
83
119
|
/** How many of the person's conversations work in the environment (read only). */
|
|
84
120
|
boundConversations(orgId: string, accountId: string, id: string): Promise<number>;
|
|
85
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* The refusals {@link resetFiles} would raise (not found, its provider not current, a connected
|
|
123
|
+
* environment), read only.
|
|
124
|
+
*/
|
|
86
125
|
checkReset(orgId: string, accountId: string, id: string): Promise<void>;
|
|
87
126
|
/** The refusals {@link rename} would raise, read without locks (see {@link checkCreate}). */
|
|
88
127
|
checkRename(orgId: string, accountId: string, id: string, name: string): Promise<void>;
|
|
@@ -93,15 +132,23 @@ export declare class EnvironmentsService {
|
|
|
93
132
|
* Deletes one of the person's live environments: soft delete, its IM default row cleared, and
|
|
94
133
|
* after the commit a `sandbox-destroy-for` job (the worker waits for running commands, then
|
|
95
134
|
* destroys the instance and the files). Conversations bound to it keep pointing at it; their
|
|
96
|
-
* sandbox tools then fail with `environment.not_found`.
|
|
135
|
+
* sandbox tools then fail with `environment.not_found`. A device environment's machine is
|
|
136
|
+
* revoked in the same transaction; after the commit it is told `stop { reason: "deleted" }`
|
|
137
|
+
* (while connected) and its connection is closed.
|
|
97
138
|
*/
|
|
98
139
|
remove(orgId: string, accountId: string, id: string, actorTokenId?: string, auditDetail?: Record<string, unknown>): Promise<void>;
|
|
140
|
+
/**
|
|
141
|
+
* Disconnects the machine of one of the person's own device environments (it must sign in
|
|
142
|
+
* again); see {@link MachinesService.revoke}.
|
|
143
|
+
*/
|
|
144
|
+
revokeMachine(orgId: string, accountId: string, id: string, actorTokenId?: string): Promise<void>;
|
|
99
145
|
/**
|
|
100
146
|
* Starts one of the person's environments over with empty files (after
|
|
101
147
|
* `environment.files_lost`): records the request (audit), then, after the commit, enqueues the
|
|
102
148
|
* `sandbox-reset-files` job (deterministic id per environment), whose worker waits for running
|
|
103
149
|
* commands and destroys any instance and the file system; the next use creates new ones. Its
|
|
104
|
-
* provider must be the current one (`environment.unavailable`)
|
|
150
|
+
* provider must be the current one (`environment.unavailable`); a connected environment's files
|
|
151
|
+
* are its machine owner's (`environment.files_on_device`). An enqueue failure is thrown
|
|
105
152
|
* (the request can be repeated: the reset is idempotent).
|
|
106
153
|
*/
|
|
107
154
|
resetFiles(orgId: string, accountId: string, id: string, actorTokenId?: string, auditDetail?: Record<string, unknown>): Promise<void>;
|
|
@@ -10,6 +10,8 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
11
|
import { Inject, Injectable } from "@nestjs/common";
|
|
12
12
|
import { sql } from "kysely";
|
|
13
|
+
import { deviceLeaseKey } from "../devices/keys.js";
|
|
14
|
+
import { MachinesService } from "../devices/machines.js";
|
|
13
15
|
import { ApiError } from "../errors.js";
|
|
14
16
|
import { audit } from "../identity/audit.js";
|
|
15
17
|
import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
@@ -17,9 +19,10 @@ import { afterCommit } from "../infra/database/after-commit.js";
|
|
|
17
19
|
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
18
20
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
19
21
|
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
22
|
+
import { LockService } from "../infra/lock/lock.service.js";
|
|
20
23
|
import { KERNEL_LOGGER } from "../infra/tokens.js";
|
|
21
24
|
import { SandboxJobs } from "../sandbox/jobs.js";
|
|
22
|
-
import { SandboxProviders } from "../sandbox/providers.js";
|
|
25
|
+
import { DEVICE_PROVIDER, isInternalProvider, SandboxProviders } from "../sandbox/providers.js";
|
|
23
26
|
import { environmentNotFound, SandboxService } from "../sandbox/sandbox.service.js";
|
|
24
27
|
import { EnvironmentAccess, environmentProvider, environmentUnavailable, } from "./access.js";
|
|
25
28
|
const isUniqueViolation = (e, constraint) => typeof e === "object" &&
|
|
@@ -28,7 +31,8 @@ const isUniqueViolation = (e, constraint) => typeof e === "object" &&
|
|
|
28
31
|
e.constraint === constraint;
|
|
29
32
|
const nameTaken = () => new ApiError("environment.name_taken", "You already have an environment with this name");
|
|
30
33
|
/**
|
|
31
|
-
* Personal
|
|
34
|
+
* Personal environments (spec 2026-11-07-environments-design §4.2 items 7, 12, 13; §6; §9):
|
|
35
|
+
* cloud ones on the deployment's sandbox provider and `device` ones on a connected machine; a
|
|
32
36
|
* person's environments in an org, visible and usable only by their owner (anyone else gets
|
|
33
37
|
* `environment.not_found`). Deleting one soft-deletes it and destroys its instance and files in a
|
|
34
38
|
* `sandbox-destroy-for` job after the commit.
|
|
@@ -42,7 +46,11 @@ let EnvironmentsService = class EnvironmentsService {
|
|
|
42
46
|
sandboxJobs;
|
|
43
47
|
access;
|
|
44
48
|
log;
|
|
45
|
-
|
|
49
|
+
locks;
|
|
50
|
+
machines;
|
|
51
|
+
constructor(db, txHost, config, providers, sandbox, sandboxJobs, access, log,
|
|
52
|
+
/** Reads the device relay's lease (`online`). */
|
|
53
|
+
locks, machines) {
|
|
46
54
|
this.db = db;
|
|
47
55
|
this.txHost = txHost;
|
|
48
56
|
this.config = config;
|
|
@@ -51,6 +59,8 @@ let EnvironmentsService = class EnvironmentsService {
|
|
|
51
59
|
this.sandboxJobs = sandboxJobs;
|
|
52
60
|
this.access = access;
|
|
53
61
|
this.log = log;
|
|
62
|
+
this.locks = locks;
|
|
63
|
+
this.machines = machines;
|
|
54
64
|
}
|
|
55
65
|
/**
|
|
56
66
|
* The person's live environments in the org, the default first, then by creation. With a
|
|
@@ -58,7 +68,7 @@ let EnvironmentsService = class EnvironmentsService {
|
|
|
58
68
|
* spec §4.2 item 7; an existing one is only read).
|
|
59
69
|
*/
|
|
60
70
|
async list(orgId, accountId) {
|
|
61
|
-
const cloudAvailable = this.providers.
|
|
71
|
+
const cloudAvailable = this.providers.cloud() !== undefined;
|
|
62
72
|
const defaultId = cloudAvailable ? await this.ensureDefault(orgId, accountId) : null;
|
|
63
73
|
const items = await this.views(orgId, accountId);
|
|
64
74
|
const last = await this.db
|
|
@@ -99,9 +109,28 @@ let EnvironmentsService = class EnvironmentsService {
|
|
|
99
109
|
return this.get(orgId, accountId, id);
|
|
100
110
|
}
|
|
101
111
|
async insert(orgId, accountId, name, actorTokenId, auditDetail) {
|
|
102
|
-
const current = this.providers.
|
|
112
|
+
const current = this.providers.cloud();
|
|
103
113
|
if (!current)
|
|
104
114
|
throw environmentUnavailable(null);
|
|
115
|
+
return this.insertNamed(orgId, accountId, name, "cloud", current.plugin, actorTokenId, auditDetail ?? {});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* A new personal `device` environment for a machine being connected (provider `@device`; the
|
|
119
|
+
* machine row is the caller's). Joins the caller's transaction (the device authorization's).
|
|
120
|
+
* Counted against `environments.maxPersonal` (`environment.limit_reached`); a live name of the
|
|
121
|
+
* person's → `environment.name_taken`. Needs no cloud provider.
|
|
122
|
+
*/
|
|
123
|
+
async createDevice(orgId, accountId, name) {
|
|
124
|
+
const id = await this.insertNamed(orgId, accountId, name, "device", DEVICE_PROVIDER, undefined, {
|
|
125
|
+
source: "device",
|
|
126
|
+
});
|
|
127
|
+
return { id };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Inserts a named personal environment under the person's membership lock, after the limit
|
|
131
|
+
* and name checks, and audits `environment.created`. Runs in the caller's transaction.
|
|
132
|
+
*/
|
|
133
|
+
async insertNamed(orgId, accountId, name, source, provider, actorTokenId, auditDetail) {
|
|
105
134
|
await this.lockMember(orgId, accountId);
|
|
106
135
|
const { n } = await this.db
|
|
107
136
|
.selectFrom("environments")
|
|
@@ -126,8 +155,8 @@ let EnvironmentsService = class EnvironmentsService {
|
|
|
126
155
|
owner_kind: "personal",
|
|
127
156
|
owner_account_id: accountId,
|
|
128
157
|
name,
|
|
129
|
-
source
|
|
130
|
-
provider
|
|
158
|
+
source,
|
|
159
|
+
provider,
|
|
131
160
|
})
|
|
132
161
|
.execute();
|
|
133
162
|
}
|
|
@@ -152,7 +181,7 @@ let EnvironmentsService = class EnvironmentsService {
|
|
|
152
181
|
* same {@link ApiError}s; a race still ends in `create`'s own refusal.
|
|
153
182
|
*/
|
|
154
183
|
async checkCreate(orgId, accountId, name) {
|
|
155
|
-
if (!this.providers.
|
|
184
|
+
if (!this.providers.cloud())
|
|
156
185
|
throw environmentUnavailable(null);
|
|
157
186
|
const { n } = await this.db
|
|
158
187
|
.selectFrom("environments")
|
|
@@ -179,9 +208,12 @@ let EnvironmentsService = class EnvironmentsService {
|
|
|
179
208
|
.executeTakeFirstOrThrow();
|
|
180
209
|
return Number(n);
|
|
181
210
|
}
|
|
182
|
-
/**
|
|
211
|
+
/**
|
|
212
|
+
* The refusals {@link resetFiles} would raise (not found, its provider not current, a connected
|
|
213
|
+
* environment), read only.
|
|
214
|
+
*/
|
|
183
215
|
async checkReset(orgId, accountId, id) {
|
|
184
|
-
await this.access.usable(orgId, accountId, id);
|
|
216
|
+
refuseDeviceReset(await this.access.usable(orgId, accountId, id));
|
|
185
217
|
}
|
|
186
218
|
/** The refusals {@link rename} would raise, read without locks (see {@link checkCreate}). */
|
|
187
219
|
async checkRename(orgId, accountId, id, name) {
|
|
@@ -228,7 +260,9 @@ let EnvironmentsService = class EnvironmentsService {
|
|
|
228
260
|
* Deletes one of the person's live environments: soft delete, its IM default row cleared, and
|
|
229
261
|
* after the commit a `sandbox-destroy-for` job (the worker waits for running commands, then
|
|
230
262
|
* destroys the instance and the files). Conversations bound to it keep pointing at it; their
|
|
231
|
-
* sandbox tools then fail with `environment.not_found`.
|
|
263
|
+
* sandbox tools then fail with `environment.not_found`. A device environment's machine is
|
|
264
|
+
* revoked in the same transaction; after the commit it is told `stop { reason: "deleted" }`
|
|
265
|
+
* (while connected) and its connection is closed.
|
|
232
266
|
*/
|
|
233
267
|
async remove(orgId, accountId, id, actorTokenId, auditDetail) {
|
|
234
268
|
const before = await this.lockOwn(orgId, accountId, id);
|
|
@@ -243,6 +277,7 @@ let EnvironmentsService = class EnvironmentsService {
|
|
|
243
277
|
.where("account_id", "=", accountId)
|
|
244
278
|
.where("environment_id", "=", id)
|
|
245
279
|
.execute();
|
|
280
|
+
await this.machines.revokeActive(id, "deleted");
|
|
246
281
|
await audit(this.db.db, {
|
|
247
282
|
actorAccountId: accountId,
|
|
248
283
|
...(actorTokenId !== undefined && { actorTokenId }),
|
|
@@ -261,12 +296,20 @@ let EnvironmentsService = class EnvironmentsService {
|
|
|
261
296
|
}
|
|
262
297
|
});
|
|
263
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Disconnects the machine of one of the person's own device environments (it must sign in
|
|
301
|
+
* again); see {@link MachinesService.revoke}.
|
|
302
|
+
*/
|
|
303
|
+
async revokeMachine(orgId, accountId, id, actorTokenId) {
|
|
304
|
+
await this.machines.revoke(orgId, accountId, id, actorTokenId);
|
|
305
|
+
}
|
|
264
306
|
/**
|
|
265
307
|
* Starts one of the person's environments over with empty files (after
|
|
266
308
|
* `environment.files_lost`): records the request (audit), then, after the commit, enqueues the
|
|
267
309
|
* `sandbox-reset-files` job (deterministic id per environment), whose worker waits for running
|
|
268
310
|
* commands and destroys any instance and the file system; the next use creates new ones. Its
|
|
269
|
-
* provider must be the current one (`environment.unavailable`)
|
|
311
|
+
* provider must be the current one (`environment.unavailable`); a connected environment's files
|
|
312
|
+
* are its machine owner's (`environment.files_on_device`). An enqueue failure is thrown
|
|
270
313
|
* (the request can be repeated: the reset is idempotent).
|
|
271
314
|
*/
|
|
272
315
|
async resetFiles(orgId, accountId, id, actorTokenId, auditDetail) {
|
|
@@ -275,7 +318,7 @@ let EnvironmentsService = class EnvironmentsService {
|
|
|
275
318
|
}
|
|
276
319
|
/** The check and audit row of {@link resetFiles}, committed before the job is enqueued. */
|
|
277
320
|
async recordReset(orgId, accountId, id, actorTokenId, auditDetail) {
|
|
278
|
-
await this.access.usable(orgId, accountId, id);
|
|
321
|
+
refuseDeviceReset(await this.access.usable(orgId, accountId, id));
|
|
279
322
|
await audit(this.db.db, {
|
|
280
323
|
actorAccountId: accountId,
|
|
281
324
|
...(actorTokenId !== undefined && { actorTokenId }),
|
|
@@ -307,7 +350,7 @@ let EnvironmentsService = class EnvironmentsService {
|
|
|
307
350
|
return this.insertDefault(orgId, accountId);
|
|
308
351
|
}
|
|
309
352
|
async insertDefault(orgId, accountId) {
|
|
310
|
-
const current = this.providers.
|
|
353
|
+
const current = this.providers.cloud();
|
|
311
354
|
if (!current)
|
|
312
355
|
throw environmentUnavailable(null);
|
|
313
356
|
await this.lockMember(orgId, accountId, "share");
|
|
@@ -439,7 +482,7 @@ let EnvironmentsService = class EnvironmentsService {
|
|
|
439
482
|
}
|
|
440
483
|
/** Its files or instance are kept by a provider that is not the current one. */
|
|
441
484
|
stranded(f) {
|
|
442
|
-
const current = this.providers.
|
|
485
|
+
const current = this.providers.cloud()?.plugin;
|
|
443
486
|
return current !== undefined && environmentProvider(f, current) !== current;
|
|
444
487
|
}
|
|
445
488
|
/** The person's live environments (or the one `id`) as views. */
|
|
@@ -447,12 +490,21 @@ let EnvironmentsService = class EnvironmentsService {
|
|
|
447
490
|
const rows = await this.db
|
|
448
491
|
.selectFrom("environments as e")
|
|
449
492
|
.leftJoin("environment_instances as i", "i.environment_id", "e.id")
|
|
493
|
+
.leftJoin("environment_machines as m", (j) => j.onRef("m.environment_id", "=", "e.id").on("m.revoked_at", "is", null))
|
|
450
494
|
.select([
|
|
451
495
|
"e.id",
|
|
452
496
|
"e.name",
|
|
453
497
|
"e.is_default",
|
|
498
|
+
"e.source",
|
|
454
499
|
"e.provider",
|
|
455
500
|
"e.created_at",
|
|
501
|
+
"m.id as machine_id",
|
|
502
|
+
"m.name as machine_name",
|
|
503
|
+
"m.os as machine_os",
|
|
504
|
+
"m.mode as machine_mode",
|
|
505
|
+
"m.program_version as machine_program_version",
|
|
506
|
+
"m.last_seen_at as machine_last_seen_at",
|
|
507
|
+
"m.session_isolation as machine_session_isolation",
|
|
456
508
|
"i.provider as instance_provider",
|
|
457
509
|
"i.fs_status",
|
|
458
510
|
"i.handle",
|
|
@@ -471,7 +523,11 @@ let EnvironmentsService = class EnvironmentsService {
|
|
|
471
523
|
if (rows.length === 0)
|
|
472
524
|
return [];
|
|
473
525
|
const limits = await this.sandbox.orgLimits(orgId);
|
|
474
|
-
const current = this.providers.
|
|
526
|
+
const current = this.providers.cloud()?.plugin;
|
|
527
|
+
// a device environment is online while a replica holds its machine's connection lease
|
|
528
|
+
const online = new Map(await Promise.all(rows
|
|
529
|
+
.filter((r) => r.source === "device")
|
|
530
|
+
.map(async (r) => [r.id, (await this.locks.holderOf(deviceLeaseKey(r.id))) !== null])));
|
|
475
531
|
return rows.map((r) => {
|
|
476
532
|
const provider = environmentProvider({
|
|
477
533
|
provider: r.provider,
|
|
@@ -485,10 +541,24 @@ let EnvironmentsService = class EnvironmentsService {
|
|
|
485
541
|
name: r.name,
|
|
486
542
|
isDefault: r.is_default,
|
|
487
543
|
ownerKind: "personal",
|
|
488
|
-
source:
|
|
544
|
+
source: r.source,
|
|
489
545
|
provider,
|
|
490
|
-
available:
|
|
491
|
-
|
|
546
|
+
available: provider !== null && isInternalProvider(provider)
|
|
547
|
+
? this.providers.forName(provider) !== undefined
|
|
548
|
+
: current !== undefined && provider === current,
|
|
549
|
+
online: r.source === "device" ? (online.get(r.id) ?? false) : null,
|
|
550
|
+
machine: r.machine_id !== null && r.machine_name !== null && r.machine_os !== null
|
|
551
|
+
? {
|
|
552
|
+
id: r.machine_id,
|
|
553
|
+
name: r.machine_name,
|
|
554
|
+
os: r.machine_os,
|
|
555
|
+
mode: r.machine_mode,
|
|
556
|
+
programVersion: r.machine_program_version,
|
|
557
|
+
lastSeenAt: r.machine_last_seen_at?.toISOString() ?? null,
|
|
558
|
+
}
|
|
559
|
+
: null,
|
|
560
|
+
// a device environment's machine decides it; cloud environments have none in phase 2
|
|
561
|
+
sessionIsolation: r.machine_session_isolation ?? false,
|
|
492
562
|
filesLost: r.fs_status === "lost",
|
|
493
563
|
instance: live
|
|
494
564
|
? {
|
|
@@ -560,6 +630,9 @@ let EnvironmentsService = class EnvironmentsService {
|
|
|
560
630
|
__decorate([
|
|
561
631
|
Transactional()
|
|
562
632
|
], EnvironmentsService.prototype, "insert", null);
|
|
633
|
+
__decorate([
|
|
634
|
+
Transactional()
|
|
635
|
+
], EnvironmentsService.prototype, "createDevice", null);
|
|
563
636
|
__decorate([
|
|
564
637
|
Transactional()
|
|
565
638
|
], EnvironmentsService.prototype, "renameIn", null);
|
|
@@ -581,6 +654,17 @@ EnvironmentsService = __decorate([
|
|
|
581
654
|
__param(4, Inject(SandboxService)),
|
|
582
655
|
__param(5, Inject(SandboxJobs)),
|
|
583
656
|
__param(6, Inject(EnvironmentAccess)),
|
|
584
|
-
__param(7, Inject(KERNEL_LOGGER))
|
|
657
|
+
__param(7, Inject(KERNEL_LOGGER)),
|
|
658
|
+
__param(8, Inject(LockService)),
|
|
659
|
+
__param(9, Inject(MachinesService))
|
|
585
660
|
], EnvironmentsService);
|
|
586
661
|
export { EnvironmentsService };
|
|
662
|
+
/**
|
|
663
|
+
* Ruling R24: a connected environment's files are in a directory on its machine, which the
|
|
664
|
+
* platform never wipes; a lost directory comes back with `capybara-device logout` and a new `login` on the machine.
|
|
665
|
+
*/
|
|
666
|
+
function refuseDeviceReset(env) {
|
|
667
|
+
if (env.provider === DEVICE_PROVIDER) {
|
|
668
|
+
throw new ApiError("environment.files_on_device", "This environment's files are in a directory on its connected machine and cannot be reset from here. To start over, run capybara-device logout, then capybara-device login into this environment, on that machine.");
|
|
669
|
+
}
|
|
670
|
+
}
|
|
@@ -27,7 +27,7 @@ export declare class LegacySandboxCleanupJob {
|
|
|
27
27
|
readonly config: KernelConfigService;
|
|
28
28
|
readonly lockService: LockService;
|
|
29
29
|
readonly log: KernelLogger;
|
|
30
|
-
constructor(db: KernelDb, providers: Pick<SandboxProviders, "
|
|
30
|
+
constructor(db: KernelDb, providers: Pick<SandboxProviders, "forName">, config: KernelConfigService, lockService: LockService, log: KernelLogger);
|
|
31
31
|
tick(signal?: AbortSignal): Promise<void>;
|
|
32
32
|
/** One run (see the class); `signal` (the stop, or a lost run lock) is read between rows. */
|
|
33
33
|
runOnce(now?: Date, signal?: AbortSignal): Promise<LegacyCleanupResult>;
|
|
@@ -65,8 +65,8 @@ let LegacySandboxCleanupJob = class LegacySandboxCleanupJob {
|
|
|
65
65
|
.select(["provider", (eb) => eb.fn.countAll().as("n")])
|
|
66
66
|
.groupBy("provider")
|
|
67
67
|
.execute();
|
|
68
|
-
const loaded = byProvider.filter((p) => this.providers.
|
|
69
|
-
const unloaded = byProvider.filter((p) => this.providers.
|
|
68
|
+
const loaded = byProvider.filter((p) => this.providers.forName(p.provider) !== undefined);
|
|
69
|
+
const unloaded = byProvider.filter((p) => this.providers.forName(p.provider) === undefined);
|
|
70
70
|
const kept = unloaded.reduce((sum, p) => sum + Number(p.n), 0);
|
|
71
71
|
if (unloaded.length > 0 && now.getTime() % 3_600_000 < PERIOD_MS) {
|
|
72
72
|
this.log.warn("legacy sandboxes kept: their provider is not loaded", {
|
|
@@ -89,7 +89,7 @@ let LegacySandboxCleanupJob = class LegacySandboxCleanupJob {
|
|
|
89
89
|
if (signal?.aborted)
|
|
90
90
|
break;
|
|
91
91
|
const name = `cap-${row.conversation_id}`;
|
|
92
|
-
const impl = this.providers.
|
|
92
|
+
const impl = this.providers.forName(row.provider)?.impl;
|
|
93
93
|
if (impl === undefined)
|
|
94
94
|
continue; // unloaded since the query: kept for a later tick
|
|
95
95
|
const callSignal = () => signal
|
|
@@ -99,7 +99,7 @@ let LegacySandboxCleanupJob = class LegacySandboxCleanupJob {
|
|
|
99
99
|
await impl.destroyByName(name, callSignal());
|
|
100
100
|
await impl.destroyFileSystemByName(name, callSignal());
|
|
101
101
|
if (row.prev_provider !== null && row.prev_provider !== row.provider) {
|
|
102
|
-
const prev = this.providers.
|
|
102
|
+
const prev = this.providers.forName(row.prev_provider)?.impl;
|
|
103
103
|
if (prev !== undefined)
|
|
104
104
|
await prev.destroyByName(name, callSignal());
|
|
105
105
|
else {
|