@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.
Files changed (42) hide show
  1. package/dist/admin/admin.controller.d.ts +3 -3
  2. package/dist/admin/admin.controller.js +6 -2
  3. package/dist/admin/admin.schemas.d.ts +13 -0
  4. package/dist/background/launch.d.ts +43 -5
  5. package/dist/background/launch.js +88 -8
  6. package/dist/background/session.d.ts +14 -2
  7. package/dist/background/session.js +26 -4
  8. package/dist/background/status.d.ts +1 -1
  9. package/dist/background/status.js +3 -3
  10. package/dist/background/stop.d.ts +0 -2
  11. package/dist/background/stop.js +1 -21
  12. package/dist/catalog/catalog-content.d.ts +5 -0
  13. package/dist/catalog/catalog-content.js +1 -0
  14. package/dist/catalog/marketplaces.service.d.ts +1 -1
  15. package/dist/catalog/requests.service.js +2 -2
  16. package/dist/config/schema.d.ts +3 -3
  17. package/dist/config/schema.js +9 -7
  18. package/dist/devices/devices.service.js +12 -6
  19. package/dist/errors.d.ts +4 -0
  20. package/dist/errors.js +4 -0
  21. package/dist/identity/settings.d.ts +53 -3
  22. package/dist/identity/settings.js +92 -9
  23. package/dist/infra/config/kernel-config.service.d.ts +7 -0
  24. package/dist/infra/config/kernel-config.service.js +20 -0
  25. package/dist/local-devices/local-device-gateway.d.ts +4 -2
  26. package/dist/local-devices/local-device-gateway.js +39 -14
  27. package/dist/local-devices/machines.d.ts +16 -0
  28. package/dist/local-devices/machines.js +51 -13
  29. package/dist/orgs/settings.controller.js +2 -2
  30. package/dist/orgs/settings.service.d.ts +11 -5
  31. package/dist/orgs/settings.service.js +28 -14
  32. package/dist/platform/ops/admin.ops.js +32 -7
  33. package/dist/platform/ops/org-settings.ops.js +3 -3
  34. package/dist/platform/ops/orgs.ops.js +1 -0
  35. package/dist/platform/ops/platform.ops.js +1 -1
  36. package/dist/sandbox/platform-tiers.d.ts +86 -0
  37. package/dist/sandbox/platform-tiers.js +97 -0
  38. package/dist/sandbox/sandbox.service.d.ts +1 -1
  39. package/dist/sandbox/sandbox.service.js +20 -6
  40. package/dist/sandbox/tiers.d.ts +14 -2
  41. package/dist/sandbox/tiers.js +8 -0
  42. 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 { limits: lim, tiers } = this.config.get().sandbox;
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.deployment().sandboxEgress.mode === "none") {
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.config.get().sandbox.tiers;
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
- /** The effective sandbox policy for the org's settings and a device's own tier (null = none). */
325
- sandboxPolicy(settings, deviceTier) {
326
- return effectiveSandboxPolicy(this.config.get().sandbox, settings, deviceTier);
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 b = this.sandboxPolicy(before, null);
345
- const a = this.sandboxPolicy({ ...before, sandboxTier: next }, null);
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, and the per-org limit overrides keyed by org id (orgs.get reads one org's).",
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.get(),
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
- return r.success
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
- : { ok: false, code: "validation_failed", message: r.error.issues[0]?.message ?? "" };
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;
@@ -161,6 +161,7 @@ let OrgsOperations = class OrgsOperations {
161
161
  "uiTheme",
162
162
  "uiArrangement",
163
163
  "clientExtensions",
164
+ "sandboxTiers",
164
165
  ],
165
166
  extras: ["orgId", "runConcurrency", "sandbox", "subagents"],
166
167
  confirm: "platform",
@@ -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>`, then clears their flags. Tasks of another instance have no process
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 cfg = this.config.get().sandbox;
282
- const [settings, tier] = await Promise.all([
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(cfg, settings, tier);
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>`, then clears their flags. Tasks of another instance have no process
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 = `rc=0; for sid in ${mine.map((r) => r.sid).join(" ")}; do pkill -KILL -s "$sid"; c=$?; [ "$c" -le 1 ] || rc=$c; done; exit $rc`;
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,
@@ -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
- /** The deployment config the policy reads (`sandbox.limits` network fields and `sandbox.tiers`). */
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: KernelConfig["sandbox"]["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
  /**
@@ -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.22",
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.16"
68
+ "@capybara-hq/plugin-sdk": "^0.1.0-rc.17"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@langchain/langgraph-checkpoint-validation": "^1.2.0",