@capybara-hq/kernel 0.1.0-rc.22 → 0.1.0-rc.23
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/admin/admin.controller.d.ts +3 -3
- package/dist/admin/admin.controller.js +6 -2
- package/dist/admin/admin.schemas.d.ts +13 -0
- package/dist/background/launch.d.ts +43 -5
- package/dist/background/launch.js +88 -8
- package/dist/background/session.d.ts +14 -2
- package/dist/background/session.js +26 -4
- package/dist/background/status.d.ts +1 -1
- package/dist/background/status.js +3 -3
- package/dist/background/stop.d.ts +0 -2
- package/dist/background/stop.js +1 -21
- package/dist/catalog/catalog-content.d.ts +5 -0
- package/dist/catalog/catalog-content.js +1 -0
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/catalog/requests.service.js +2 -2
- package/dist/config/schema.d.ts +3 -3
- package/dist/config/schema.js +9 -7
- package/dist/devices/devices.service.js +12 -6
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +4 -0
- package/dist/identity/settings.d.ts +53 -3
- package/dist/identity/settings.js +92 -9
- package/dist/infra/config/kernel-config.service.d.ts +7 -0
- package/dist/infra/config/kernel-config.service.js +20 -0
- package/dist/local-devices/local-device-gateway.d.ts +4 -2
- package/dist/local-devices/local-device-gateway.js +39 -14
- package/dist/local-devices/machines.d.ts +16 -0
- package/dist/local-devices/machines.js +51 -13
- package/dist/orgs/settings.controller.js +2 -2
- package/dist/orgs/settings.service.d.ts +11 -5
- package/dist/orgs/settings.service.js +28 -14
- package/dist/platform/ops/admin.ops.js +32 -7
- package/dist/platform/ops/org-settings.ops.js +3 -3
- package/dist/platform/ops/orgs.ops.js +1 -0
- package/dist/platform/ops/platform.ops.js +1 -1
- package/dist/sandbox/platform-tiers.d.ts +86 -0
- package/dist/sandbox/platform-tiers.js +97 -0
- package/dist/sandbox/sandbox.service.d.ts +1 -1
- package/dist/sandbox/sandbox.service.js +20 -6
- package/dist/sandbox/tiers.d.ts +14 -2
- package/dist/sandbox/tiers.js +8 -0
- package/package.json +2 -2
|
@@ -18,6 +18,7 @@ import { KernelDb } from "../infra/database/kernel-db.js";
|
|
|
18
18
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
19
19
|
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
20
20
|
import { KERNEL_LOGGER } from "../infra/tokens.js";
|
|
21
|
+
import { readPlatformTiers } from "../sandbox/platform-tiers.js";
|
|
21
22
|
import { effectiveSandboxPolicy } from "../sandbox/tiers.js";
|
|
22
23
|
import { assertThemeOnIn } from "../themes/theme-state.js";
|
|
23
24
|
import { IanaTimeZone } from "../time-zone.js";
|
|
@@ -255,9 +256,14 @@ let OrgSettingsService = class OrgSettingsService {
|
|
|
255
256
|
}
|
|
256
257
|
return parseOrgSettings(raw);
|
|
257
258
|
}
|
|
259
|
+
/** The platform's effective sandbox tiers (the startup config over the admin console's choice). */
|
|
260
|
+
platformTiers() {
|
|
261
|
+
return readPlatformTiers(this.db.db, this.config, this.log);
|
|
262
|
+
}
|
|
258
263
|
/** 部署默认值(设置页展示「跟随部署」时的实际取值)。 */
|
|
259
|
-
deployment() {
|
|
260
|
-
const
|
|
264
|
+
async deployment() {
|
|
265
|
+
const lim = this.config.get().sandbox.limits;
|
|
266
|
+
const tiers = await this.platformTiers();
|
|
261
267
|
return {
|
|
262
268
|
sandboxEgress: { mode: lim.egress, allowHosts: [...lim.allowHosts] },
|
|
263
269
|
sandboxTiers: {
|
|
@@ -290,14 +296,14 @@ let OrgSettingsService = class OrgSettingsService {
|
|
|
290
296
|
return this.parseStored(orgId, row.settings);
|
|
291
297
|
}
|
|
292
298
|
/** The refusals of {@link update} that need no read of the org (the Platform tool's precheck). */
|
|
293
|
-
check(patch) {
|
|
299
|
+
async check(patch) {
|
|
294
300
|
if (patch.sandboxEgress?.mode === "allowlist" &&
|
|
295
|
-
this.
|
|
301
|
+
this.config.get().sandbox.limits.egress === "none") {
|
|
296
302
|
throw new ApiError("org.sandbox_egress_not_allowed", "This deployment disables sandbox network access (sandbox.limits.egress is none); an organization can only set sandboxEgress to none");
|
|
297
303
|
}
|
|
298
304
|
const t = patch.sandboxTier;
|
|
299
305
|
if (t != null) {
|
|
300
|
-
const platform = this.
|
|
306
|
+
const platform = await this.platformTiers();
|
|
301
307
|
if (t.max !== undefined && !tierAtMost(t.max, platform.max)) {
|
|
302
308
|
throw sandboxTierNotAllowed(`The platform allows sandbox tiers up to ${platform.max}; sandboxTier.max cannot be ${t.max}`);
|
|
303
309
|
}
|
|
@@ -318,12 +324,19 @@ let OrgSettingsService = class OrgSettingsService {
|
|
|
318
324
|
* change that widens (see {@link assertOnlyNarrows}); only a person in a session widens.
|
|
319
325
|
*/
|
|
320
326
|
opts) {
|
|
321
|
-
this.check(patch);
|
|
327
|
+
await this.check(patch);
|
|
322
328
|
return this.updateIn(orgId, patch, actorAccountId, actorTokenId, auditDetail, opts);
|
|
323
329
|
}
|
|
324
|
-
/**
|
|
325
|
-
|
|
326
|
-
|
|
330
|
+
/**
|
|
331
|
+
* The effective sandbox policy for the org's settings and a device's own tier (null = none);
|
|
332
|
+
* `tiers`: the platform's tiers when the caller already read them.
|
|
333
|
+
*/
|
|
334
|
+
async sandboxPolicy(settings, deviceTier, tiers) {
|
|
335
|
+
const deployment = {
|
|
336
|
+
limits: this.config.get().sandbox.limits,
|
|
337
|
+
tiers: tiers ?? (await this.platformTiers()),
|
|
338
|
+
};
|
|
339
|
+
return effectiveSandboxPolicy(deployment, settings, deviceTier);
|
|
327
340
|
}
|
|
328
341
|
/**
|
|
329
342
|
* Refuses a patch that widens the org's sandbox (sandbox dev tiers spec §2, §7.2: only a person
|
|
@@ -337,12 +350,13 @@ let OrgSettingsService = class OrgSettingsService {
|
|
|
337
350
|
* - `sandboxEgress`: the base list grows: from none to anything else, or to hosts (the
|
|
338
351
|
* deployment's when the org gives none) not all in the current list.
|
|
339
352
|
*/
|
|
340
|
-
assertOnlyNarrows(before, patch) {
|
|
353
|
+
async assertOnlyNarrows(before, patch) {
|
|
341
354
|
if (patch.sandboxTier !== undefined) {
|
|
342
355
|
const next = patch.sandboxTier ?? undefined;
|
|
343
356
|
const prev = before.sandboxTier;
|
|
344
|
-
const
|
|
345
|
-
const
|
|
357
|
+
const tiers = await this.platformTiers();
|
|
358
|
+
const b = await this.sandboxPolicy(before, null, tiers);
|
|
359
|
+
const a = await this.sandboxPolicy({ ...before, sandboxTier: next }, null, tiers);
|
|
346
360
|
// a cleared default follows the platform, capped by the ceiling: `max` bounds it then
|
|
347
361
|
const nextDefault = next?.default === undefined && next?.max !== undefined ? next.max : next?.default;
|
|
348
362
|
if (!tierAtMost(a.ceiling, b.ceiling) ||
|
|
@@ -381,10 +395,10 @@ let OrgSettingsService = class OrgSettingsService {
|
|
|
381
395
|
const stored = asObject(row.settings);
|
|
382
396
|
const before = this.parseStored(orgId, stored);
|
|
383
397
|
// again under the org lock: a hot config reload may have lowered the platform since
|
|
384
|
-
this.check(patch);
|
|
398
|
+
await this.check(patch);
|
|
385
399
|
// under the org lock, so the comparison is with the value the write replaces
|
|
386
400
|
if (opts?.onlyNarrows)
|
|
387
|
-
this.assertOnlyNarrows(before, patch);
|
|
401
|
+
await this.assertOnlyNarrows(before, patch);
|
|
388
402
|
const changes = Object.fromEntries(Object.entries(patch)
|
|
389
403
|
.filter(([, v]) => v !== undefined)
|
|
390
404
|
// null clears sandboxEgress / wakeOnBackgroundTask: follow the deployment default again
|
|
@@ -43,6 +43,7 @@ const SettingsInput = DEPLOYMENT_SETTINGS_PATCH.pick({
|
|
|
43
43
|
uiTheme: true,
|
|
44
44
|
uiArrangement: true,
|
|
45
45
|
clientExtensions: true,
|
|
46
|
+
sandboxTiers: true,
|
|
46
47
|
}).strict();
|
|
47
48
|
const ProviderId = ModelProviderParams.shape.provider.describe("The model provider (models.platform-list), e.g. the plugin name of its extension.");
|
|
48
49
|
/** `provider/model` of a purpose target, or null. */
|
|
@@ -182,17 +183,17 @@ let AdminOperations = class AdminOperations {
|
|
|
182
183
|
}),
|
|
183
184
|
defineOperation({
|
|
184
185
|
id: "admin.settings",
|
|
185
|
-
description: "Read the deployment settings (super admin): who may create orgs (orgCreation), the price currency, whether orgs may create their own connectors and skills, whether org admins may browse the marketplaces,
|
|
186
|
+
description: "Read the deployment settings (super admin): who may create orgs (orgCreation), the price currency, whether orgs may create their own connectors and skills, whether org admins may browse the marketplaces, the per-org limit overrides keyed by org id (orgs.get reads one org's), and the platform's sandbox levels in effect (sandboxTiers: max, default, allowUnsandboxed, with sources: env, file or nacos are set in the startup config and read-only, db chosen in the admin console, default).",
|
|
186
187
|
input: z.object({}).strict(),
|
|
187
188
|
example: {},
|
|
188
189
|
authz: { superAdmin: true },
|
|
189
190
|
mirrors: ["GET /api/v1/admin/settings"],
|
|
190
191
|
confirm: "read",
|
|
191
|
-
run: () => this.settings.
|
|
192
|
+
run: () => this.settings.view(),
|
|
192
193
|
}),
|
|
193
194
|
defineOperation({
|
|
194
195
|
id: "admin.update-settings",
|
|
195
|
-
description: "Change deployment settings (super admin); give only the keys to change: orgCreation (anyone or super_admin), priceCurrency (ISO 4217 code), customConnectors / customSkills (allow or deny: whether orgs may create their own instead of installing from the catalog), orgMarketplaceBrowse (true or false), uiTheme ({ id }: the platform default theme, a platform theme whose scope is all, or null for the built-in theme; it is what every org falls back to and what the sign-in pages show), uiArrangement ({ <slot>: { hidden, order } } for a list slot, { <slot>: { use } } for a single slot: the platform default arrangement of UI slots, with contribution keys <extension>/<contribution>; hidden ones are not listed but still open by URL; use names the contribution that replaces the built-in, null for the built-in; null for a slot removes its entry), clientExtensions ({ enabled?, disabled? }: the kill switch of plugin client code; enabled false stops every plugin's interface code in every browser, disabled is the whole list of plugins whose interface code alone is stopped while their tools keep working; read the current list from admin.settings first). Per-org limits are orgs.set-limits.",
|
|
196
|
+
description: "Change deployment settings (super admin); give only the keys to change: orgCreation (anyone or super_admin), priceCurrency (ISO 4217 code), customConnectors / customSkills (allow or deny: whether orgs may create their own instead of installing from the catalog), orgMarketplaceBrowse (true or false), uiTheme ({ id }: the platform default theme, a platform theme whose scope is all, or null for the built-in theme; it is what every org falls back to and what the sign-in pages show), uiArrangement ({ <slot>: { hidden, order } } for a list slot, { <slot>: { use } } for a single slot: the platform default arrangement of UI slots, with contribution keys <extension>/<contribution>; hidden ones are not listed but still open by URL; use names the contribution that replaces the built-in, null for the built-in; null for a slot removes its entry), clientExtensions ({ enabled?, disabled? }: the kill switch of plugin client code; enabled false stops every plugin's interface code in every browser, disabled is the whole list of plugins whose interface code alone is stopped while their tools keep working; read the current list from admin.settings first), sandboxTiers ({ max?, default?, allowUnsandboxed? }: the platform's sandbox levels strict < dev < open; null clears a field back to its default; through this tool they may only be lowered, never raised, and fields set in the startup config are read-only). Per-org limits are orgs.set-limits.",
|
|
196
197
|
input: SettingsInput.refine((b) => Object.keys(b).length > 0, {
|
|
197
198
|
message: "at least one setting is required",
|
|
198
199
|
}),
|
|
@@ -207,6 +208,14 @@ let AdminOperations = class AdminOperations {
|
|
|
207
208
|
code: "validation_failed",
|
|
208
209
|
next: "Fix the values as the message says (admin.settings shows the current ones).",
|
|
209
210
|
},
|
|
211
|
+
{
|
|
212
|
+
code: "deployment.sandbox_tier_not_allowed",
|
|
213
|
+
next: "Through this tool the platform's sandbox levels may only be lowered; to raise them, ask a super admin to change them in the admin console settings on the web.",
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
code: "deployment.setting_readonly",
|
|
217
|
+
next: "That sandbox level is set in the deployment's startup config (environment, file or Nacos; admin.settings shows the source); it can only be changed there, by the operator.",
|
|
218
|
+
},
|
|
210
219
|
{
|
|
211
220
|
code: "theme.not_available",
|
|
212
221
|
next: "The platform default must be a platform theme whose scope is all, not disabled, and on in every organization. When the details list orgs, the theme is off in those organizations: tell the person their names, then choose another theme (plugins.components with kind theme lists them) or have it switched on in those organizations first; { id: null } sets the built-in theme.",
|
|
@@ -214,13 +223,27 @@ let AdminOperations = class AdminOperations {
|
|
|
214
223
|
],
|
|
215
224
|
precheck: async (i) => {
|
|
216
225
|
const r = DeploymentSettingsBody.safeParse(i);
|
|
217
|
-
|
|
226
|
+
if (!r.success) {
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
code: "validation_failed",
|
|
230
|
+
message: r.error.issues[0]?.message ?? "",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const tiers = i.sandboxTiers;
|
|
234
|
+
return tiers === undefined
|
|
218
235
|
? { ok: true }
|
|
219
|
-
:
|
|
236
|
+
: precheckBy(() => this.settings.checkSandboxTiers(tiers, { onlyNarrows: true }));
|
|
220
237
|
},
|
|
221
238
|
summary: async (i) => {
|
|
222
|
-
const { uiTheme, uiArrangement, clientExtensions, ...rest } = i;
|
|
239
|
+
const { uiTheme, uiArrangement, clientExtensions, sandboxTiers, ...rest } = i;
|
|
223
240
|
const parts = Object.entries(rest).map(([k, v]) => `${k}=${String(v)}`);
|
|
241
|
+
if (sandboxTiers !== undefined) {
|
|
242
|
+
parts.push(`sandboxTiers=${Object.entries(sandboxTiers)
|
|
243
|
+
.filter(([, v]) => v !== undefined)
|
|
244
|
+
.map(([k, v]) => `${k}:${String(v)}`)
|
|
245
|
+
.join(" ")}`);
|
|
246
|
+
}
|
|
224
247
|
const zh = [...parts];
|
|
225
248
|
const en = [...parts];
|
|
226
249
|
if (uiTheme !== undefined) {
|
|
@@ -253,7 +276,9 @@ let AdminOperations = class AdminOperations {
|
|
|
253
276
|
en: `Change the deployment settings: ${en.join(", ")}`,
|
|
254
277
|
};
|
|
255
278
|
},
|
|
256
|
-
run: (input, ctx) => this.settings.update(input, ctx.actorAccountId, undefined, viaAgent("admin.update-settings", ctx)
|
|
279
|
+
run: (input, ctx) => this.settings.update(input, ctx.actorAccountId, undefined, viaAgent("admin.update-settings", ctx),
|
|
280
|
+
// a person raises the platform's sandbox levels, never an agent (dev tiers spec §2)
|
|
281
|
+
{ onlyNarrows: true }),
|
|
257
282
|
}),
|
|
258
283
|
defineOperation({
|
|
259
284
|
id: "models.platform-list",
|
|
@@ -47,7 +47,7 @@ let OrgSettingsOperations = class OrgSettingsOperations {
|
|
|
47
47
|
confirm: "read",
|
|
48
48
|
run: async (_input, ctx) => ({
|
|
49
49
|
settings: await this.settings.get(ctx.orgId),
|
|
50
|
-
deployment: this.settings.deployment(),
|
|
50
|
+
deployment: await this.settings.deployment(),
|
|
51
51
|
sandboxLimits: await this.settings.sandboxLimits(ctx.orgId),
|
|
52
52
|
}),
|
|
53
53
|
}),
|
|
@@ -80,8 +80,8 @@ let OrgSettingsOperations = class OrgSettingsOperations {
|
|
|
80
80
|
},
|
|
81
81
|
],
|
|
82
82
|
precheck: (i, ctx) => precheckBy(async () => {
|
|
83
|
-
this.settings.check(i);
|
|
84
|
-
this.settings.assertOnlyNarrows(await this.settings.get(ctx.orgId), i);
|
|
83
|
+
await this.settings.check(i);
|
|
84
|
+
await this.settings.assertOnlyNarrows(await this.settings.get(ctx.orgId), i);
|
|
85
85
|
}),
|
|
86
86
|
summary: async (i, ctx) => {
|
|
87
87
|
const { theme, ...rest } = i;
|
|
@@ -148,7 +148,7 @@ let PlatformOperationsProvider = class PlatformOperationsProvider {
|
|
|
148
148
|
const sandbox = toolKeys.some((k) => SANDBOX_TOOL_NAMES.includes(k));
|
|
149
149
|
const org = await this.settings.get(ctx.orgId);
|
|
150
150
|
// a new device's level: the org's default tier and its network
|
|
151
|
-
const policy = this.settings.sandboxPolicy(org, null);
|
|
151
|
+
const policy = await this.settings.sandboxPolicy(org, null);
|
|
152
152
|
return {
|
|
153
153
|
id: agent.id,
|
|
154
154
|
name: d.name,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type SandboxTier } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { KernelConfig } from "../config/schema.js";
|
|
4
|
+
import type { KernelSchemaDb } from "../db/kernel-schema.js";
|
|
5
|
+
import type { KernelLogger } from "../log.js";
|
|
6
|
+
/**
|
|
7
|
+
* The platform's sandbox tiers (sandbox dev tiers spec §4.1): `max`, `default` and
|
|
8
|
+
* `allowUnsandboxed`. Each field is read env > config file (or Nacos) > the super admin's choice in
|
|
9
|
+
* the admin console (deployment setting {@link PLATFORM_TIERS_SETTING}) > {@link PLATFORM_TIER_DEFAULTS}.
|
|
10
|
+
* A field set in the startup config is read-only in the admin console. Read live from PG at every
|
|
11
|
+
* use, so a change applies on every replica at once.
|
|
12
|
+
*/
|
|
13
|
+
/** The `deployment_settings` key of the super admin's choice. */
|
|
14
|
+
export declare const PLATFORM_TIERS_SETTING = "sandboxTiers";
|
|
15
|
+
/** The fields, in display order. */
|
|
16
|
+
export declare const PLATFORM_TIER_FIELDS: readonly ["max", "default", "allowUnsandboxed"];
|
|
17
|
+
export type PlatformTierField = (typeof PLATFORM_TIER_FIELDS)[number];
|
|
18
|
+
/** The stored choice: each field absent follows the default. */
|
|
19
|
+
export declare const PlatformTiersStoredSchema: z.ZodObject<{
|
|
20
|
+
max: z.ZodOptional<z.ZodEnum<{
|
|
21
|
+
dev: "dev";
|
|
22
|
+
open: "open";
|
|
23
|
+
strict: "strict";
|
|
24
|
+
}>>;
|
|
25
|
+
default: z.ZodOptional<z.ZodEnum<{
|
|
26
|
+
dev: "dev";
|
|
27
|
+
open: "open";
|
|
28
|
+
strict: "strict";
|
|
29
|
+
}>>;
|
|
30
|
+
allowUnsandboxed: z.ZodOptional<z.ZodBoolean>;
|
|
31
|
+
}, z.core.$strict>;
|
|
32
|
+
export type PlatformTiersStored = z.infer<typeof PlatformTiersStoredSchema>;
|
|
33
|
+
/**
|
|
34
|
+
* A change of the stored choice (`PUT /api/v1/admin/settings` `sandboxTiers`): each given field
|
|
35
|
+
* replaces the stored one, null clears it (back to the default); at least one field.
|
|
36
|
+
*/
|
|
37
|
+
export declare const PLATFORM_TIERS_PATCH: z.ZodObject<{
|
|
38
|
+
max: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
39
|
+
dev: "dev";
|
|
40
|
+
open: "open";
|
|
41
|
+
strict: "strict";
|
|
42
|
+
}>>>;
|
|
43
|
+
default: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
44
|
+
dev: "dev";
|
|
45
|
+
open: "open";
|
|
46
|
+
strict: "strict";
|
|
47
|
+
}>>>;
|
|
48
|
+
allowUnsandboxed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
49
|
+
}, z.core.$strict>;
|
|
50
|
+
export type PlatformTiersPatch = z.infer<typeof PLATFORM_TIERS_PATCH>;
|
|
51
|
+
/** The platform's effective tiers. */
|
|
52
|
+
export interface PlatformTiers {
|
|
53
|
+
max: SandboxTier;
|
|
54
|
+
/** At most {@link max}. */
|
|
55
|
+
default: SandboxTier;
|
|
56
|
+
allowUnsandboxed: boolean;
|
|
57
|
+
}
|
|
58
|
+
/** What a field falls back to when neither the startup config nor the admin console sets it. */
|
|
59
|
+
export declare const PLATFORM_TIER_DEFAULTS: Readonly<PlatformTiers>;
|
|
60
|
+
/** Where a field's effective value comes from. */
|
|
61
|
+
export type PlatformTierSource = "env" | "file" | "nacos" | "db" | "default";
|
|
62
|
+
/** The admin console's view: the effective values and where each comes from. */
|
|
63
|
+
export interface PlatformTiersView extends PlatformTiers {
|
|
64
|
+
sources: Record<PlatformTierField, PlatformTierSource>;
|
|
65
|
+
}
|
|
66
|
+
/** The startup config's `sandbox.tiers` (each field undefined when not set there). */
|
|
67
|
+
export type ConfigTiers = KernelConfig["sandbox"]["tiers"];
|
|
68
|
+
/**
|
|
69
|
+
* Parses the stored choice: missing -> `{}`; invalid -> `{ max: "strict" }`, narrower, never
|
|
70
|
+
* wider (the same rule an org's invalid `sandboxTier` follows), with a warning on `log`.
|
|
71
|
+
*/
|
|
72
|
+
export declare function parseStoredPlatformTiers(value: unknown, log?: Pick<KernelLogger, "warn">): PlatformTiersStored;
|
|
73
|
+
/** The default the layers choose before it is cut to the max. */
|
|
74
|
+
export declare const chosenPlatformDefault: (config: ConfigTiers, stored: PlatformTiersStored) => SandboxTier;
|
|
75
|
+
/** The effective tiers of the startup config over the stored choice; the default is cut to the max. */
|
|
76
|
+
export declare function resolvePlatformTiers(config: ConfigTiers, stored: PlatformTiersStored): PlatformTiers;
|
|
77
|
+
/** Whether `next` reaches further than `prev` anywhere: a higher max or default, or unsandboxed allowed. */
|
|
78
|
+
export declare function platformTiersWiden(prev: PlatformTiers, next: PlatformTiers): boolean;
|
|
79
|
+
/** The stored choice with `patch` applied (null clears a field). */
|
|
80
|
+
export declare function applyPlatformTiersPatch(stored: PlatformTiersStored, patch: PlatformTiersPatch): PlatformTiersStored;
|
|
81
|
+
/** The stored choice, read live (in the caller's transaction when there is one). */
|
|
82
|
+
export declare function readStoredPlatformTiers(db: KernelSchemaDb, log?: Pick<KernelLogger, "warn">): Promise<PlatformTiersStored>;
|
|
83
|
+
/** The platform's effective tiers, read live: see {@link resolvePlatformTiers}. */
|
|
84
|
+
export declare function readPlatformTiers(db: KernelSchemaDb, config: {
|
|
85
|
+
get(): Pick<KernelConfig, "sandbox">;
|
|
86
|
+
}, log?: Pick<KernelLogger, "warn">): Promise<PlatformTiers>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { minTier, tierAtMost } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
/**
|
|
4
|
+
* The platform's sandbox tiers (sandbox dev tiers spec §4.1): `max`, `default` and
|
|
5
|
+
* `allowUnsandboxed`. Each field is read env > config file (or Nacos) > the super admin's choice in
|
|
6
|
+
* the admin console (deployment setting {@link PLATFORM_TIERS_SETTING}) > {@link PLATFORM_TIER_DEFAULTS}.
|
|
7
|
+
* A field set in the startup config is read-only in the admin console. Read live from PG at every
|
|
8
|
+
* use, so a change applies on every replica at once.
|
|
9
|
+
*/
|
|
10
|
+
/** The `deployment_settings` key of the super admin's choice. */
|
|
11
|
+
export const PLATFORM_TIERS_SETTING = "sandboxTiers";
|
|
12
|
+
/** The fields, in display order. */
|
|
13
|
+
export const PLATFORM_TIER_FIELDS = ["max", "default", "allowUnsandboxed"];
|
|
14
|
+
const Tier = z.enum(["strict", "dev", "open"]);
|
|
15
|
+
/** The stored choice: each field absent follows the default. */
|
|
16
|
+
export const PlatformTiersStoredSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
max: Tier.optional(),
|
|
19
|
+
default: Tier.optional(),
|
|
20
|
+
allowUnsandboxed: z.boolean().optional(),
|
|
21
|
+
})
|
|
22
|
+
.strict();
|
|
23
|
+
/**
|
|
24
|
+
* A change of the stored choice (`PUT /api/v1/admin/settings` `sandboxTiers`): each given field
|
|
25
|
+
* replaces the stored one, null clears it (back to the default); at least one field.
|
|
26
|
+
*/
|
|
27
|
+
export const PLATFORM_TIERS_PATCH = z
|
|
28
|
+
.object({
|
|
29
|
+
max: Tier.nullable().optional(),
|
|
30
|
+
default: Tier.nullable().optional(),
|
|
31
|
+
allowUnsandboxed: z.boolean().nullable().optional(),
|
|
32
|
+
})
|
|
33
|
+
.strict()
|
|
34
|
+
.refine((o) => Object.values(o).some((v) => v !== undefined), {
|
|
35
|
+
message: "give max, default, allowUnsandboxed or several",
|
|
36
|
+
});
|
|
37
|
+
/** What a field falls back to when neither the startup config nor the admin console sets it. */
|
|
38
|
+
export const PLATFORM_TIER_DEFAULTS = Object.freeze({
|
|
39
|
+
max: "dev",
|
|
40
|
+
default: "dev",
|
|
41
|
+
allowUnsandboxed: true,
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* Parses the stored choice: missing -> `{}`; invalid -> `{ max: "strict" }`, narrower, never
|
|
45
|
+
* wider (the same rule an org's invalid `sandboxTier` follows), with a warning on `log`.
|
|
46
|
+
*/
|
|
47
|
+
export function parseStoredPlatformTiers(value, log) {
|
|
48
|
+
if (value === undefined)
|
|
49
|
+
return {};
|
|
50
|
+
const parsed = PlatformTiersStoredSchema.safeParse(value);
|
|
51
|
+
if (parsed.success)
|
|
52
|
+
return parsed.data;
|
|
53
|
+
log?.warn("invalid deployment setting; it reads as max strict", { key: PLATFORM_TIERS_SETTING });
|
|
54
|
+
return { max: "strict" };
|
|
55
|
+
}
|
|
56
|
+
/** The default the layers choose before it is cut to the max. */
|
|
57
|
+
export const chosenPlatformDefault = (config, stored) => config.default ?? stored.default ?? PLATFORM_TIER_DEFAULTS.default;
|
|
58
|
+
/** The effective tiers of the startup config over the stored choice; the default is cut to the max. */
|
|
59
|
+
export function resolvePlatformTiers(config, stored) {
|
|
60
|
+
const max = config.max ?? stored.max ?? PLATFORM_TIER_DEFAULTS.max;
|
|
61
|
+
return {
|
|
62
|
+
max,
|
|
63
|
+
default: minTier(chosenPlatformDefault(config, stored), max),
|
|
64
|
+
allowUnsandboxed: config.allowUnsandboxed ?? stored.allowUnsandboxed ?? PLATFORM_TIER_DEFAULTS.allowUnsandboxed,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Whether `next` reaches further than `prev` anywhere: a higher max or default, or unsandboxed allowed. */
|
|
68
|
+
export function platformTiersWiden(prev, next) {
|
|
69
|
+
return (!tierAtMost(next.max, prev.max) ||
|
|
70
|
+
!tierAtMost(next.default, prev.default) ||
|
|
71
|
+
(next.allowUnsandboxed && !prev.allowUnsandboxed));
|
|
72
|
+
}
|
|
73
|
+
/** The stored choice with `patch` applied (null clears a field). */
|
|
74
|
+
export function applyPlatformTiersPatch(stored, patch) {
|
|
75
|
+
const out = { ...stored };
|
|
76
|
+
for (const f of PLATFORM_TIER_FIELDS) {
|
|
77
|
+
const v = patch[f];
|
|
78
|
+
if (v === null)
|
|
79
|
+
delete out[f];
|
|
80
|
+
else if (v !== undefined)
|
|
81
|
+
out[f] = v;
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/** The stored choice, read live (in the caller's transaction when there is one). */
|
|
86
|
+
export async function readStoredPlatformTiers(db, log) {
|
|
87
|
+
const row = await db
|
|
88
|
+
.selectFrom("deployment_settings")
|
|
89
|
+
.select("value")
|
|
90
|
+
.where("key", "=", PLATFORM_TIERS_SETTING)
|
|
91
|
+
.executeTakeFirst();
|
|
92
|
+
return parseStoredPlatformTiers(row?.value, log);
|
|
93
|
+
}
|
|
94
|
+
/** The platform's effective tiers, read live: see {@link resolvePlatformTiers}. */
|
|
95
|
+
export async function readPlatformTiers(db, config, log) {
|
|
96
|
+
return resolvePlatformTiers(config.get().sandbox.tiers, await readStoredPlatformTiers(db, log));
|
|
97
|
+
}
|
|
@@ -465,7 +465,7 @@ export declare class SandboxService {
|
|
|
465
465
|
/**
|
|
466
466
|
* Under the exclusive lease, right after a resume or a (re)build: kills the sessions of the
|
|
467
467
|
* background tasks marked lost while their process may live on (`kill_pending`), in one exec of
|
|
468
|
-
* `pkill -KILL -s <sid
|
|
468
|
+
* `pkill -KILL -s <sid>` (`-g` on macOS), then clears their flags. Tasks of another instance have no process
|
|
469
469
|
* here and are only cleared. A gone instance and an abort are thrown; another failure is warned
|
|
470
470
|
* and leaves the flags of this instance's tasks for the next resume.
|
|
471
471
|
*/
|
|
@@ -11,6 +11,7 @@ import { randomUUID } from "node:crypto";
|
|
|
11
11
|
import { compareTiers, isFileSystemGoneError, isSandboxGoneError, rootsOf, } from "@capybara-hq/plugin-sdk";
|
|
12
12
|
import { Inject, Injectable } from "@nestjs/common";
|
|
13
13
|
import { sql, } from "kysely";
|
|
14
|
+
import { SESSION_MODE_SCRIPT } from "../background/launch.js";
|
|
14
15
|
import { BackgroundTaskStore } from "../background/store.js";
|
|
15
16
|
import { DEFAULT_MAX_COMMAND_SECONDS, DEFAULT_MAX_RUN_SECONDS, } from "../config/schema.js";
|
|
16
17
|
import { ApiError } from "../errors.js";
|
|
@@ -23,7 +24,7 @@ import { appendMissingGrant, TIER_OPEN_GRANT_KEY, UNATTENDED_OPEN_TIER_ERROR, un
|
|
|
23
24
|
import { canonicalJson } from "../tools/canonical-json.js";
|
|
24
25
|
import { SandboxLeases, SandboxLockLostError, sandboxBusy, } from "./leases.js";
|
|
25
26
|
import { LOCAL_PROVIDER, providerForDevice, SandboxProviders, } from "./providers.js";
|
|
26
|
-
import { appliedOf, effectiveSandboxPolicy, instanceLimitsJson, limitsForTier, readInstanceLimits, sameApplied, } from "./tiers.js";
|
|
27
|
+
import { appliedOf, effectiveSandboxPolicy, instanceLimitsJson, limitsForTier, readDeploymentSandboxPolicy, readInstanceLimits, sameApplied, } from "./tiers.js";
|
|
27
28
|
// Moved to leases.ts (phase C); re-exported for existing importers
|
|
28
29
|
export { SANDBOX_LOCK_PREFIX, SANDBOX_LOCK_TTL_MS, SandboxLockLostError, sandboxBusy, } from "./leases.js";
|
|
29
30
|
/** Margin (ms) of a holder record's TTL beyond the longest command: by then the in-sandbox limit has ended its commands. */
|
|
@@ -278,12 +279,12 @@ let SandboxService = class SandboxService {
|
|
|
278
279
|
* default.
|
|
279
280
|
*/
|
|
280
281
|
async devicePolicy(device) {
|
|
281
|
-
const
|
|
282
|
-
|
|
282
|
+
const [deployment, settings, tier] = await Promise.all([
|
|
283
|
+
readDeploymentSandboxPolicy(this.db.db, this.config),
|
|
283
284
|
readOrgSettings(this.db.db, device.orgId),
|
|
284
285
|
device.deviceId === undefined ? null : this.deviceTier(device.orgId, device.deviceId),
|
|
285
286
|
]);
|
|
286
|
-
return effectiveSandboxPolicy(
|
|
287
|
+
return effectiveSandboxPolicy(deployment, settings, tier);
|
|
287
288
|
}
|
|
288
289
|
/** The device's own tier (`devices.tier`); null when it follows the org or is not there. */
|
|
289
290
|
async deviceTier(orgId, deviceId) {
|
|
@@ -701,6 +702,17 @@ let SandboxService = class SandboxService {
|
|
|
701
702
|
if (intent)
|
|
702
703
|
await this.clearIntent(env, token);
|
|
703
704
|
intent = false;
|
|
705
|
+
// after a wait, the limits again under the lease: a tier narrowed just before the
|
|
706
|
+
// holder we waited for let go is applied now (asShared checks `ready` with them)
|
|
707
|
+
if (waited) {
|
|
708
|
+
try {
|
|
709
|
+
want = await wantNow();
|
|
710
|
+
}
|
|
711
|
+
catch (err) {
|
|
712
|
+
await lease.release();
|
|
713
|
+
throw err;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
704
716
|
const e = await this.asShared(ctx, p, lease, ready, signal);
|
|
705
717
|
if (e)
|
|
706
718
|
return e;
|
|
@@ -1088,7 +1100,7 @@ let SandboxService = class SandboxService {
|
|
|
1088
1100
|
/**
|
|
1089
1101
|
* Under the exclusive lease, right after a resume or a (re)build: kills the sessions of the
|
|
1090
1102
|
* background tasks marked lost while their process may live on (`kill_pending`), in one exec of
|
|
1091
|
-
* `pkill -KILL -s <sid
|
|
1103
|
+
* `pkill -KILL -s <sid>` (`-g` on macOS), then clears their flags. Tasks of another instance have no process
|
|
1092
1104
|
* here and are only cleared. A gone instance and an abort are thrown; another failure is warned
|
|
1093
1105
|
* and leaves the flags of this instance's tasks for the next resume.
|
|
1094
1106
|
*/
|
|
@@ -1100,10 +1112,12 @@ let SandboxService = class SandboxService {
|
|
|
1100
1112
|
let cleared = rows;
|
|
1101
1113
|
if (mine.length > 0) {
|
|
1102
1114
|
// pkill exits 1 when the session has no process left: only a higher status is a failure
|
|
1103
|
-
const command =
|
|
1115
|
+
const command = `${SESSION_MODE_SCRIPT}\nrc=0; for sid in ${mine.map((r) => r.sid).join(" ")}; do tk_kill KILL "$sid"; c=$?; [ "$c" -le 1 ] || rc=$c; done; exit $rc`;
|
|
1104
1116
|
try {
|
|
1105
1117
|
const r = await p.impl.exec(handle, {
|
|
1106
1118
|
command,
|
|
1119
|
+
// tk_kill's control files are relative to the workspace root (macOS devices)
|
|
1120
|
+
cwd: rootsOf(handle).workspace,
|
|
1107
1121
|
timeoutMs: PENDING_KILL_TIMEOUT_MS,
|
|
1108
1122
|
maxOutputBytes: 4096,
|
|
1109
1123
|
signal,
|
package/dist/sandbox/tiers.d.ts
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
import { type SandboxTier } from "@capybara-hq/plugin-sdk";
|
|
2
2
|
import type { KernelConfig } from "../config/schema.js";
|
|
3
|
+
import type { KernelSchemaDb } from "../db/kernel-schema.js";
|
|
3
4
|
import type { OrgSettings } from "../orgs/settings.service.js";
|
|
5
|
+
import { type PlatformTiers } from "./platform-tiers.js";
|
|
4
6
|
/**
|
|
5
7
|
* Sandbox tiers (sandbox dev tiers spec §4): who may reach which tier, and the network each tier
|
|
6
8
|
* gets. One function, {@link effectiveSandboxPolicy}, combines the platform's ceiling and default
|
|
7
9
|
* (`sandbox.tiers`), the org's (`sandboxTier`) and the device's own choice.
|
|
8
10
|
*/
|
|
9
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* What the policy reads of the platform: the `sandbox.limits` network fields and the effective
|
|
13
|
+
* platform tiers (`readPlatformTiers`: the startup config over the admin console's choice).
|
|
14
|
+
*/
|
|
10
15
|
export interface DeploymentSandboxPolicy {
|
|
11
16
|
limits: Pick<KernelConfig["sandbox"]["limits"], "egress" | "allowHosts" | "devHosts">;
|
|
12
|
-
tiers:
|
|
17
|
+
tiers: PlatformTiers;
|
|
13
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* The platform side of the policy, read live: the `sandbox.limits` network fields of the config
|
|
21
|
+
* and the effective platform tiers (in the caller's transaction when there is one).
|
|
22
|
+
*/
|
|
23
|
+
export declare function readDeploymentSandboxPolicy(db: KernelSchemaDb, config: {
|
|
24
|
+
get(): Pick<KernelConfig, "sandbox">;
|
|
25
|
+
}): Promise<DeploymentSandboxPolicy>;
|
|
14
26
|
/** The org settings the policy reads. */
|
|
15
27
|
export type OrgSandboxSettings = Pick<OrgSettings, "sandboxEgress" | "sandboxTier">;
|
|
16
28
|
/**
|
package/dist/sandbox/tiers.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { isSandboxTier, minTier, tierAtMost } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
import { readPlatformTiers } from "./platform-tiers.js";
|
|
3
|
+
/**
|
|
4
|
+
* The platform side of the policy, read live: the `sandbox.limits` network fields of the config
|
|
5
|
+
* and the effective platform tiers (in the caller's transaction when there is one).
|
|
6
|
+
*/
|
|
7
|
+
export async function readDeploymentSandboxPolicy(db, config) {
|
|
8
|
+
return { limits: config.get().sandbox.limits, tiers: await readPlatformTiers(db, config) };
|
|
9
|
+
}
|
|
2
10
|
/** The org's base list `L`: see {@link BaseSandboxEgress}. */
|
|
3
11
|
export function baseSandboxEgress(deployment, org) {
|
|
4
12
|
if (deployment.egress === "none" || org?.mode === "none")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capybara-hq/kernel",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.23",
|
|
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": {
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"yaml": "^2.9.1",
|
|
66
66
|
"yauzl": "3.4.0",
|
|
67
67
|
"zod": "^4.6.7",
|
|
68
|
-
"@capybara-hq/plugin-sdk": "^0.1.0-rc.
|
|
68
|
+
"@capybara-hq/plugin-sdk": "^0.1.0-rc.17"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@langchain/langgraph-checkpoint-validation": "^1.2.0",
|