@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
@@ -1,8 +1,10 @@
1
1
  import { z } from "zod";
2
2
  import type { KernelSchemaDb } from "../db/kernel-schema.js";
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";
5
6
  import type { KernelLogger } from "../log.js";
7
+ import { type PlatformTiers, type PlatformTiersPatch, type PlatformTiersStored, type PlatformTiersView } from "../sandbox/platform-tiers.js";
6
8
  import { type UiArrangement } from "../ui/arrangement.js";
7
9
  import { SuperAdminLock } from "./super-admin.js";
8
10
  /** 沙箱 cpu(核)与内存(MB)覆盖的范围(C19)。 */
@@ -105,6 +107,14 @@ export declare const DEPLOYMENT_SETTINGS: z.ZodObject<{
105
107
  }, z.core.$strip>>;
106
108
  }, z.core.$strip>;
107
109
  export type DeploymentSettings = z.infer<typeof DEPLOYMENT_SETTINGS>;
110
+ /**
111
+ * What `GET /api/v1/admin/settings` answers: the stored settings, and the platform's sandbox tiers
112
+ * as they are in effect with where each field comes from (`env` / `file` / `nacos`: the startup
113
+ * config, read-only here; `db`: chosen in the admin console; `default`).
114
+ */
115
+ export type DeploymentSettingsView = DeploymentSettings & {
116
+ sandboxTiers: PlatformTiersView;
117
+ };
108
118
  /**
109
119
  * 设置的修改(PUT /api/v1/admin/settings 的 body):只改给出的键。
110
120
  * `runConcurrencyOverrides` 按组织合并:值为正整数 → 设置该组织的上限;值为 null → 删除该组织的覆盖
@@ -147,6 +157,19 @@ export declare const DEPLOYMENT_SETTINGS_PATCH: z.ZodObject<{
147
157
  enabled: z.ZodOptional<z.ZodBoolean>;
148
158
  disabled: z.ZodOptional<z.ZodArray<z.ZodString>>;
149
159
  }, z.core.$strict>>;
160
+ sandboxTiers: z.ZodOptional<z.ZodObject<{
161
+ max: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
162
+ dev: "dev";
163
+ open: "open";
164
+ strict: "strict";
165
+ }>>>;
166
+ default: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
167
+ dev: "dev";
168
+ open: "open";
169
+ strict: "strict";
170
+ }>>>;
171
+ allowUnsandboxed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
172
+ }, z.core.$strict>>;
150
173
  }, z.core.$strict>;
151
174
  export type DeploymentSettingsPatch = z.infer<typeof DEPLOYMENT_SETTINGS_PATCH>;
152
175
  /**
@@ -198,7 +221,29 @@ export declare class SettingsService {
198
221
  readonly txHost: TransactionHost;
199
222
  private readonly adminLock;
200
223
  private readonly log;
201
- constructor(db: KernelDb, txHost: TransactionHost, adminLock: SuperAdminLock, log: Pick<KernelLogger, "warn">);
224
+ /** The startup config's `sandbox.tiers`, which wins over the stored choice. */
225
+ private readonly config;
226
+ constructor(db: KernelDb, txHost: TransactionHost, adminLock: SuperAdminLock, log: Pick<KernelLogger, "warn">,
227
+ /** The startup config's `sandbox.tiers`, which wins over the stored choice. */
228
+ config: Pick<KernelConfigService, "get" | "fieldSource">);
229
+ /** The settings with the platform's sandbox tiers in effect and their sources. */
230
+ view(): Promise<DeploymentSettingsView>;
231
+ private tiersView;
232
+ /**
233
+ * The refusals of a `sandboxTiers` change against the stored choice: a field set in the
234
+ * startup config (`deployment.setting_readonly`), a default above the max
235
+ * (`validation_failed`), and with `onlyNarrows` (an API token, the Platform tool) any widening:
236
+ * a higher max or default, unsandboxed allowed, in effect or in the stored choice (which
237
+ * applies once the startup config stops setting a field). Returns the next stored choice and
238
+ * the effective tiers before and after.
239
+ */
240
+ checkSandboxTiers(patch: PlatformTiersPatch, opts?: {
241
+ onlyNarrows?: boolean;
242
+ }): Promise<{
243
+ next: PlatformTiersStored;
244
+ before: PlatformTiers;
245
+ after: PlatformTiers;
246
+ }>;
202
247
  /**
203
248
  * Reads every setting, in the context's transaction when there is one (a caller holding a
204
249
  * transaction connection does not take a second one from the pool). Each key is validated on
@@ -206,9 +251,14 @@ export declare class SettingsService {
206
251
  * valid keys apply; unknown keys (written by a newer version) are ignored.
207
252
  */
208
253
  get(): Promise<DeploymentSettings>;
209
- /** Updates settings under the super admin lock, re-checking the actor (else `forbidden`). */
254
+ /**
255
+ * Updates settings under the super admin lock, re-checking the actor (else `forbidden`).
256
+ * `onlyNarrows` (an API token, the Platform tool): `sandboxTiers` may only narrow.
257
+ */
210
258
  update(patch: DeploymentSettingsPatch, actorAccountId: string, actorTokenId?: string,
211
259
  /** Merged into the audit detail (the Platform tool's `via: "agent"`). */
212
- auditDetail?: Record<string, unknown>): Promise<DeploymentSettings>;
260
+ auditDetail?: Record<string, unknown>, opts?: {
261
+ onlyNarrows?: boolean;
262
+ }): Promise<DeploymentSettingsView>;
213
263
  private write;
214
264
  }
@@ -11,10 +11,12 @@ import { EXTENSION_NAME_RE } from "@capybara-hq/plugin-sdk";
11
11
  import { Inject, Injectable } from "@nestjs/common";
12
12
  import { z } from "zod";
13
13
  import { ApiError } from "../errors.js";
14
+ import { KernelConfigService } from "../infra/config/kernel-config.service.js";
14
15
  import { KernelDb } from "../infra/database/kernel-db.js";
15
16
  import { TransactionHost } from "../infra/database/transaction-host.js";
16
17
  import { Transactional } from "../infra/database/transactional.decorator.js";
17
18
  import { KERNEL_LOGGER } from "../infra/tokens.js";
19
+ import { applyPlatformTiersPatch, chosenPlatformDefault, PLATFORM_TIER_FIELDS, PLATFORM_TIERS_PATCH, PLATFORM_TIERS_SETTING, platformTiersWiden, readStoredPlatformTiers, resolvePlatformTiers, } from "../sandbox/platform-tiers.js";
18
20
  import { assertPlatformDefaultEligible, lockThemeShared } from "../themes/theme-state.js";
19
21
  import { mergeArrangement, UiArrangementPatchSchema, UiArrangementSchema, } from "../ui/arrangement.js";
20
22
  import { audit } from "./audit.js";
@@ -153,6 +155,7 @@ export const DEPLOYMENT_SETTINGS = z.object({
153
155
  clientExtensions: ClientExtensionsSetting.default(clientExtensionsDefault),
154
156
  });
155
157
  const DEFAULTS = DEPLOYMENT_SETTINGS.parse({});
158
+ const tierWidenRefused = () => new ApiError("deployment.sandbox_tier_not_allowed", "Raising the platform's sandbox level (sandboxTiers) needs a super admin in the admin console; an API token or the Platform tool may only lower it.");
156
159
  /**
157
160
  * 设置的修改(PUT /api/v1/admin/settings 的 body):只改给出的键。
158
161
  * `runConcurrencyOverrides` 按组织合并:值为正整数 → 设置该组织的上限;值为 null → 删除该组织的覆盖
@@ -198,6 +201,14 @@ export const DEPLOYMENT_SETTINGS_PATCH = z
198
201
  message: "give enabled, disabled or both",
199
202
  })
200
203
  .optional(),
204
+ /**
205
+ * The platform's sandbox tiers (sandbox dev tiers spec §4.1): each given field replaces the
206
+ * stored one, null clears it (back to the default: max dev, default dev, unsandboxed allowed).
207
+ * A field set in the startup config is read-only (409 `deployment.setting_readonly`); the
208
+ * default may not be above the max; an API token or the Platform tool may only narrow (400
209
+ * `deployment.sandbox_tier_not_allowed`).
210
+ */
211
+ sandboxTiers: PLATFORM_TIERS_PATCH.optional(),
201
212
  })
202
213
  .strict();
203
214
  /**
@@ -350,11 +361,66 @@ let SettingsService = class SettingsService {
350
361
  txHost;
351
362
  adminLock;
352
363
  log;
353
- constructor(db, txHost, adminLock, log) {
364
+ config;
365
+ constructor(db, txHost, adminLock, log,
366
+ /** The startup config's `sandbox.tiers`, which wins over the stored choice. */
367
+ config) {
354
368
  this.db = db;
355
369
  this.txHost = txHost;
356
370
  this.adminLock = adminLock;
357
371
  this.log = log;
372
+ this.config = config;
373
+ }
374
+ /** The settings with the platform's sandbox tiers in effect and their sources. */
375
+ async view() {
376
+ const [settings, stored] = await Promise.all([
377
+ this.get(),
378
+ readStoredPlatformTiers(this.db.db, this.log),
379
+ ]);
380
+ return { ...settings, sandboxTiers: this.tiersView(stored) };
381
+ }
382
+ tiersView(stored) {
383
+ const config = this.config.get().sandbox.tiers;
384
+ const sources = Object.fromEntries(PLATFORM_TIER_FIELDS.map((f) => [
385
+ f,
386
+ this.config.fieldSource(`sandbox.tiers.${f}`) ??
387
+ (stored[f] !== undefined ? "db" : "default"),
388
+ ]));
389
+ return { ...resolvePlatformTiers(config, stored), sources };
390
+ }
391
+ /**
392
+ * The refusals of a `sandboxTiers` change against the stored choice: a field set in the
393
+ * startup config (`deployment.setting_readonly`), a default above the max
394
+ * (`validation_failed`), and with `onlyNarrows` (an API token, the Platform tool) any widening:
395
+ * a higher max or default, unsandboxed allowed, in effect or in the stored choice (which
396
+ * applies once the startup config stops setting a field). Returns the next stored choice and
397
+ * the effective tiers before and after.
398
+ */
399
+ async checkSandboxTiers(patch, opts = {}) {
400
+ const readonly = PLATFORM_TIER_FIELDS.filter((f) => patch[f] !== undefined && this.config.fieldSource(`sandbox.tiers.${f}`) !== undefined);
401
+ if (readonly.length > 0) {
402
+ throw new ApiError("deployment.setting_readonly", `These sandbox tier settings are set in the startup config (file, Nacos or environment) and are read-only here: ${readonly.map((f) => `sandbox.tiers.${f}`).join(", ")}`, readonly.map((f) => ({
403
+ path: `sandboxTiers.${f}`,
404
+ source: this.config.fieldSource(`sandbox.tiers.${f}`),
405
+ })));
406
+ }
407
+ const config = this.config.get().sandbox.tiers;
408
+ const stored = await readStoredPlatformTiers(this.db.db, this.log);
409
+ const next = applyPlatformTiersPatch(stored, patch);
410
+ const before = resolvePlatformTiers(config, stored);
411
+ const after = resolvePlatformTiers(config, next);
412
+ const chosen = chosenPlatformDefault(config, next);
413
+ if ((patch.max !== undefined || patch.default !== undefined) && chosen !== after.default) {
414
+ throw new ApiError("validation_failed", `The default sandbox tier (${chosen}) cannot be above the highest (${after.max}); lower sandboxTiers.default as well`, [{ path: "sandboxTiers.default", message: `must be at most ${after.max}` }]);
415
+ }
416
+ if (opts.onlyNarrows) {
417
+ const none = {};
418
+ if (platformTiersWiden(before, after) ||
419
+ platformTiersWiden(resolvePlatformTiers(none, stored), resolvePlatformTiers(none, next))) {
420
+ throw tierWidenRefused();
421
+ }
422
+ }
423
+ return { next, before, after };
358
424
  }
359
425
  /**
360
426
  * Reads every setting, in the context's transaction when there is one (a caller holding a
@@ -382,18 +448,21 @@ let SettingsService = class SettingsService {
382
448
  }
383
449
  return out;
384
450
  }
385
- /** Updates settings under the super admin lock, re-checking the actor (else `forbidden`). */
451
+ /**
452
+ * Updates settings under the super admin lock, re-checking the actor (else `forbidden`).
453
+ * `onlyNarrows` (an API token, the Platform tool): `sandboxTiers` may only narrow.
454
+ */
386
455
  async update(patch, actorAccountId, actorTokenId,
387
456
  /** Merged into the audit detail (the Platform tool's `via: "agent"`). */
388
- auditDetail) {
457
+ auditDetail, opts = {}) {
389
458
  const check = DEPLOYMENT_SETTINGS_PATCH.safeParse(patch);
390
459
  if (!check.success) {
391
460
  throw new ApiError("validation_failed", "Invalid deployment settings", check.error.issues.map((i) => ({ path: i.path.join("."), message: i.message })));
392
461
  }
393
- await this.write(check.data, actorAccountId, actorTokenId, auditDetail);
394
- return this.get();
462
+ await this.write(check.data, actorAccountId, actorTokenId, auditDetail, opts);
463
+ return this.view();
395
464
  }
396
- async write(patch, actorAccountId, actorTokenId, auditDetail) {
465
+ async write(patch, actorAccountId, actorTokenId, auditDetail, opts) {
397
466
  await this.adminLock.lockAndAuthorize(actorAccountId);
398
467
  // The super admin lock serializes every settings write: the current overrides are read
399
468
  // and merged under it, so no concurrent change is lost
@@ -452,7 +521,16 @@ let SettingsService = class SettingsService {
452
521
  next.subagentLimitOverrides = mergeByOrg(current.subagentLimitOverrides, subagentPatch);
453
522
  }
454
523
  }
455
- for (const [key, value] of Object.entries(next)) {
524
+ // the platform's sandbox tiers: checked against the stored choice under the lock; lowering
525
+ // the max needs no org rewrite, the effective policy cuts every org's ceiling and default
526
+ let tiersChange;
527
+ const writes = Object.entries(next);
528
+ if (patch.sandboxTiers !== undefined) {
529
+ const r = await this.checkSandboxTiers(patch.sandboxTiers, opts);
530
+ tiersChange = { before: r.before, after: r.after };
531
+ writes.push([PLATFORM_TIERS_SETTING, r.next]);
532
+ }
533
+ for (const [key, value] of writes) {
456
534
  if (value === undefined)
457
535
  continue;
458
536
  const json = JSON.stringify(value);
@@ -468,7 +546,11 @@ let SettingsService = class SettingsService {
468
546
  actorAccountId,
469
547
  ...(actorTokenId !== undefined && { actorTokenId }),
470
548
  action: "deployment.settings.updated",
471
- detail: { ...patch, ...auditDetail },
549
+ detail: {
550
+ ...patch,
551
+ ...(tiersChange !== undefined && { sandboxTiersEffective: tiersChange }),
552
+ ...auditDetail,
553
+ },
472
554
  });
473
555
  }
474
556
  };
@@ -480,6 +562,7 @@ SettingsService = __decorate([
480
562
  __param(0, Inject(KernelDb)),
481
563
  __param(1, Inject(TransactionHost)),
482
564
  __param(2, Inject(SuperAdminLock)),
483
- __param(3, Inject(KERNEL_LOGGER))
565
+ __param(3, Inject(KERNEL_LOGGER)),
566
+ __param(4, Inject(KernelConfigService))
484
567
  ], SettingsService);
485
568
  export { SettingsService };
@@ -15,6 +15,13 @@ export declare class KernelConfigService {
15
15
  private readonly listeners;
16
16
  constructor(input: KernelConfigInput, log: KernelLogger);
17
17
  get(): KernelConfig;
18
+ /**
19
+ * Where the current value of a field without a schema default comes from: `env` when an
20
+ * environment variable sets it, else the config source (`file` / `nacos`) when it is set at all;
21
+ * undefined when nothing sets it (a layer below the startup config decides, e.g. the deployment
22
+ * settings for `sandbox.tiers.*`). Such fields are read-only in the admin console.
23
+ */
24
+ fieldSource(path: string): "env" | "file" | "nacos" | undefined;
18
25
  /** Subscribes to applied changes (hot fields only); returns the unsubscribe function. */
19
26
  onChange(listener: ConfigChangeListener): () => void;
20
27
  /**
@@ -8,9 +8,11 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
10
  import { Inject, Injectable } from "@nestjs/common";
11
+ import { envOverrides } from "../../config/env.js";
11
12
  import { changedPaths, configFields, getPath, secretValues, setPath } from "../../config/fields.js";
12
13
  import { resolveConfig } from "../../config/load.js";
13
14
  import { KernelConfigSchema } from "../../config/schema.js";
15
+ import { configSourceKind } from "../../config/sources.js";
14
16
  import { KERNEL_CONFIG_INPUT, KERNEL_LOGGER } from "../tokens.js";
15
17
  /**
16
18
  * Appliers of the hot-reloadable fields: the key is a dotted path marked `hot` in the schema. A
@@ -101,6 +103,24 @@ let KernelConfigService = class KernelConfigService {
101
103
  get() {
102
104
  return this.current;
103
105
  }
106
+ /**
107
+ * Where the current value of a field without a schema default comes from: `env` when an
108
+ * environment variable sets it, else the config source (`file` / `nacos`) when it is set at all;
109
+ * undefined when nothing sets it (a layer below the startup config decides, e.g. the deployment
110
+ * settings for `sandbox.tiers.*`). Such fields are read-only in the admin console.
111
+ */
112
+ fieldSource(path) {
113
+ if (getPath(this.current, path) === undefined)
114
+ return undefined;
115
+ let fromEnv = false;
116
+ try {
117
+ fromEnv = getPath(envOverrides(this.env), path) !== undefined;
118
+ }
119
+ catch {
120
+ // conflicting variables fail the start; a later change here only loses the label
121
+ }
122
+ return fromEnv ? "env" : configSourceKind(this.env);
123
+ }
104
124
  /** Subscribes to applied changes (hot fields only); returns the unsubscribe function. */
105
125
  onChange(listener) {
106
126
  this.listeners.add(listener);
@@ -55,6 +55,8 @@ declare class DeviceConnection {
55
55
  private chain;
56
56
  /** Resolves once the lease is handed back (or was never held). */
57
57
  private released;
58
+ /** The hello's record, then each policy frame's, one after another (never rejects). */
59
+ private recorded;
58
60
  constructor(gw: LocalDeviceGateway, socket: WebSocket, identity: MachineIdentity, credential: string);
59
61
  get deviceId(): string;
60
62
  start(): void;
@@ -101,7 +103,7 @@ declare class DeviceConnection {
101
103
  * sockets are this process's own; everything other replicas need is in Redis.
102
104
  */
103
105
  export declare class LocalDeviceGateway {
104
- readonly machines: Pick<MachinesService, "authenticate" | "recordHello" | "touch">;
106
+ readonly machines: Pick<MachinesService, "authenticate" | "recordHello" | "recordPolicy" | "touch">;
105
107
  readonly locks: Pick<LockService, "hold">;
106
108
  readonly bus: ExtensionContext["bus"];
107
109
  readonly redis: Redis;
@@ -112,7 +114,7 @@ export declare class LocalDeviceGateway {
112
114
  private stopping;
113
115
  /** How often connections are pinged ({@link PING_INTERVAL_MS}; a test seam). */
114
116
  pingIntervalMs: number;
115
- constructor(machines: Pick<MachinesService, "authenticate" | "recordHello" | "touch">, locks: Pick<LockService, "hold">, bus: ExtensionContext["bus"], redis: Redis, config: KernelConfigService, log: Pick<KernelLogger, "info" | "warn">);
117
+ constructor(machines: Pick<MachinesService, "authenticate" | "recordHello" | "recordPolicy" | "touch">, locks: Pick<LockService, "hold">, bus: ExtensionContext["bus"], redis: Redis, config: KernelConfigService, log: Pick<KernelLogger, "info" | "warn">);
116
118
  graceMs(): number;
117
119
  leaseTtlMs(): number;
118
120
  maxFrameBytes(): number;
@@ -114,6 +114,8 @@ class DeviceConnection {
114
114
  chain = Promise.resolve();
115
115
  /** Resolves once the lease is handed back (or was never held). */
116
116
  released;
117
+ /** The hello's record, then each policy frame's, one after another (never rejects). */
118
+ recorded = Promise.resolve();
117
119
  constructor(gw, socket, identity, credential) {
118
120
  this.gw = gw;
119
121
  this.socket = socket;
@@ -174,20 +176,43 @@ class DeviceConnection {
174
176
  }
175
177
  clearTimeout(this.helloTimer);
176
178
  this.state = "claiming";
177
- if (!(await this.claim()))
178
- return;
179
- try {
180
- // the raw summary too: the frame parser drops one it cannot read whole, and its
181
- // isolation must still reach the kernel
182
- const rawPolicy = JSON.parse(text).policy;
183
- await this.gw.machines.recordHello(this.identity.machineId, frame, rawPolicy);
184
- }
185
- catch (e) {
186
- this.gw.log.warn("could not record a device hello", {
187
- deviceId: this.deviceId,
188
- error: errMsg(e),
189
- });
190
- }
179
+ // the raw summary too: the frame parser drops one it cannot read whole, and its
180
+ // isolation must still reach the kernel
181
+ const rawPolicy = JSON.parse(text).policy;
182
+ // set before the first await: a policy frame that follows is recorded after the hello
183
+ this.recorded = (async () => {
184
+ if (!(await this.claim()))
185
+ return;
186
+ try {
187
+ await this.gw.machines.recordHello(this.identity.machineId, frame, rawPolicy);
188
+ }
189
+ catch (e) {
190
+ this.gw.log.warn("could not record a device hello", {
191
+ deviceId: this.deviceId,
192
+ error: errMsg(e),
193
+ });
194
+ }
195
+ })();
196
+ await this.recorded;
197
+ return;
198
+ }
199
+ if (frame.type === "policy" && this.state !== "hello") {
200
+ // after the hello's record and the earlier policy frames, in the order they came
201
+ const rawPolicy = JSON.parse(text).policy;
202
+ this.recorded = this.recorded.then(async () => {
203
+ if (this.state === "closed")
204
+ return;
205
+ try {
206
+ await this.gw.machines.recordPolicy(this.identity.machineId, frame.policy, rawPolicy);
207
+ }
208
+ catch (e) {
209
+ this.gw.log.warn("could not record a device policy change", {
210
+ deviceId: this.deviceId,
211
+ error: errMsg(e),
212
+ });
213
+ }
214
+ });
215
+ await this.recorded;
191
216
  return;
192
217
  }
193
218
  if (frame.type !== "res" || this.state === "hello") {
@@ -97,6 +97,22 @@ export declare class MachinesService {
97
97
  * `device.isolation_reported` (sandbox dev tiers spec §10.4), once per change.
98
98
  */
99
99
  recordHello(machineId: string, hello: DeviceHello, rawPolicy?: unknown): Promise<void>;
100
+ /**
101
+ * Stores the local policy summary a connected program pushed after its hello (a `policy` frame:
102
+ * its policy file changed on the machine, its localhost proxy guard flipped, a new tier
103
+ * ceiling), read and audited as the hello's ({@link recordHello}): an unreadable summary keeps
104
+ * its isolation alone (`rawPolicy`), a changed isolation is audited `device.isolation_reported`.
105
+ * The next sandbox call then records a new `device_paths` event when the facts the model is told
106
+ * changed. A revoked machine is left alone.
107
+ */
108
+ recordPolicy(machineId: string, summary: DeviceLocalPolicySummary | undefined, rawPolicy?: unknown): Promise<void>;
109
+ /**
110
+ * The summary to store for one a program reported (hello or `policy` frame): the parsed one,
111
+ * else its raw isolation alone, else null. Audits `device.isolation_reported` when the isolation
112
+ * differs from the stored one (none stored counts as `sandbox`). Runs in the caller's
113
+ * transaction, with the machine's row locked.
114
+ */
115
+ private reportedPolicy;
100
116
  /** Marks the machine seen (a ping); writes at most once every 60 seconds. */
101
117
  touch(machineId: string): Promise<void>;
102
118
  /**
@@ -207,19 +207,7 @@ let MachinesService = class MachinesService {
207
207
  .execute();
208
208
  }
209
209
  }
210
- const summary = localPolicyOf(hello.policy);
211
- const isolation = summary?.isolation ?? isolationOf(rawPolicy);
212
- const policy = summary ?? (isolation === null ? null : { isolation });
213
- const before = isolationOf(self.local_policy) ?? "sandbox";
214
- if (isolation !== null && isolation !== before) {
215
- await audit(this.db.db, {
216
- actorAccountId: self.account_id,
217
- orgId: self.org_id,
218
- action: "device.isolation_reported",
219
- target: self.device_id,
220
- detail: { machineId, isolation, previous: before },
221
- });
222
- }
210
+ const policy = await this.reportedPolicy(machineId, self, hello.policy, rawPolicy);
223
211
  await this.db
224
212
  .updateTable("device_machines")
225
213
  .set({
@@ -237,6 +225,53 @@ let MachinesService = class MachinesService {
237
225
  .where("revoked_at", "is", null)
238
226
  .execute();
239
227
  }
228
+ /**
229
+ * Stores the local policy summary a connected program pushed after its hello (a `policy` frame:
230
+ * its policy file changed on the machine, its localhost proxy guard flipped, a new tier
231
+ * ceiling), read and audited as the hello's ({@link recordHello}): an unreadable summary keeps
232
+ * its isolation alone (`rawPolicy`), a changed isolation is audited `device.isolation_reported`.
233
+ * The next sandbox call then records a new `device_paths` event when the facts the model is told
234
+ * changed. A revoked machine is left alone.
235
+ */
236
+ async recordPolicy(machineId, summary, rawPolicy = summary) {
237
+ const self = await this.db
238
+ .selectFrom("device_machines")
239
+ .select(["device_id", "org_id", "account_id", "local_policy"])
240
+ .where("id", "=", machineId)
241
+ .where("revoked_at", "is", null)
242
+ .forUpdate()
243
+ .executeTakeFirst();
244
+ if (self === undefined)
245
+ return;
246
+ const policy = await this.reportedPolicy(machineId, self, summary, rawPolicy);
247
+ await this.db
248
+ .updateTable("device_machines")
249
+ .set({ local_policy: jsonOrNull(policy), last_seen_at: sql `now()` })
250
+ .where("id", "=", machineId)
251
+ .where("revoked_at", "is", null)
252
+ .execute();
253
+ }
254
+ /**
255
+ * The summary to store for one a program reported (hello or `policy` frame): the parsed one,
256
+ * else its raw isolation alone, else null. Audits `device.isolation_reported` when the isolation
257
+ * differs from the stored one (none stored counts as `sandbox`). Runs in the caller's
258
+ * transaction, with the machine's row locked.
259
+ */
260
+ async reportedPolicy(machineId, self, reported, rawPolicy) {
261
+ const summary = localPolicyOf(reported);
262
+ const isolation = summary?.isolation ?? isolationOf(rawPolicy);
263
+ const before = isolationOf(self.local_policy) ?? "sandbox";
264
+ if (isolation !== null && isolation !== before) {
265
+ await audit(this.db.db, {
266
+ actorAccountId: self.account_id,
267
+ orgId: self.org_id,
268
+ action: "device.isolation_reported",
269
+ target: self.device_id,
270
+ detail: { machineId, isolation, previous: before },
271
+ });
272
+ }
273
+ return summary ?? (isolation === null ? null : { isolation });
274
+ }
240
275
  /** Marks the machine seen (a ping); writes at most once every 60 seconds. */
241
276
  async touch(machineId) {
242
277
  await this.db
@@ -385,6 +420,9 @@ __decorate([
385
420
  __decorate([
386
421
  Transactional()
387
422
  ], MachinesService.prototype, "recordHello", null);
423
+ __decorate([
424
+ Transactional()
425
+ ], MachinesService.prototype, "recordPolicy", null);
388
426
  __decorate([
389
427
  Transactional()
390
428
  ], MachinesService.prototype, "revoke", null);
@@ -23,7 +23,7 @@ let OrgSettingsController = class OrgSettingsController {
23
23
  async get(org) {
24
24
  return {
25
25
  settings: await this.settings.get(org.orgId),
26
- deployment: this.settings.deployment(),
26
+ deployment: await this.settings.deployment(),
27
27
  sandboxLimits: await this.settings.sandboxLimits(org.orgId),
28
28
  };
29
29
  }
@@ -32,7 +32,7 @@ let OrgSettingsController = class OrgSettingsController {
32
32
  return {
33
33
  // an API token may narrow the sandbox, never widen it (sandbox dev tiers spec §2)
34
34
  settings: await this.settings.update(org.orgId, body, p.account.id, patTokenId(p), undefined, { onlyNarrows: patTokenId(p) !== undefined }),
35
- deployment: this.settings.deployment(),
35
+ deployment: await this.settings.deployment(),
36
36
  sandboxLimits: await this.settings.sandboxLimits(org.orgId),
37
37
  };
38
38
  }
@@ -5,6 +5,7 @@ import { KernelConfigService } from "../infra/config/kernel-config.service.js";
5
5
  import { KernelDb } from "../infra/database/kernel-db.js";
6
6
  import { TransactionHost } from "../infra/database/transaction-host.js";
7
7
  import type { KernelLogger } from "../log.js";
8
+ import { type PlatformTiers } from "../sandbox/platform-tiers.js";
8
9
  import { type EffectiveSandboxPolicy } from "../sandbox/tiers.js";
9
10
  import { OrgLock } from "./org-lock.js";
10
11
  /**
@@ -296,13 +297,15 @@ export declare class OrgSettingsService {
296
297
  config: Pick<KernelConfigService, "get">, log?: KernelLogger | undefined);
297
298
  /** Parses stored settings, warning when an invalid `sandboxTier` narrows to `{ max: "strict" }`. */
298
299
  private parseStored;
300
+ /** The platform's effective sandbox tiers (the startup config over the admin console's choice). */
301
+ platformTiers(): Promise<PlatformTiers>;
299
302
  /** 部署默认值(设置页展示「跟随部署」时的实际取值)。 */
300
- deployment(): OrgSettingsDeployment;
303
+ deployment(): Promise<OrgSettingsDeployment>;
301
304
  /** 组织实际生效的沙箱 cpu / 内存(只读;由超级管理员在部署设置里覆盖)。 */
302
305
  sandboxLimits(orgId: string): Promise<OrgSandboxLimits>;
303
306
  get(orgId: string): Promise<OrgSettings>;
304
307
  /** The refusals of {@link update} that need no read of the org (the Platform tool's precheck). */
305
- check(patch: OrgSettingsPatch): void;
308
+ check(patch: OrgSettingsPatch): Promise<void>;
306
309
  update(orgId: string, patch: OrgSettingsPatch, actorAccountId: string, actorTokenId?: string,
307
310
  /** Merged into the audit detail (the Platform tool's `via: "agent"`). */
308
311
  auditDetail?: Record<string, unknown>,
@@ -313,8 +316,11 @@ export declare class OrgSettingsService {
313
316
  opts?: {
314
317
  onlyNarrows?: boolean;
315
318
  }): Promise<OrgSettings>;
316
- /** The effective sandbox policy for the org's settings and a device's own tier (null = none). */
317
- sandboxPolicy(settings: OrgSettings, deviceTier: SandboxTier | null): EffectiveSandboxPolicy;
319
+ /**
320
+ * The effective sandbox policy for the org's settings and a device's own tier (null = none);
321
+ * `tiers`: the platform's tiers when the caller already read them.
322
+ */
323
+ sandboxPolicy(settings: OrgSettings, deviceTier: SandboxTier | null, tiers?: PlatformTiers): Promise<EffectiveSandboxPolicy>;
318
324
  /**
319
325
  * Refuses a patch that widens the org's sandbox (sandbox dev tiers spec §2, §7.2: only a person
320
326
  * widens it, never the Platform tool or an API token):
@@ -327,7 +333,7 @@ export declare class OrgSettingsService {
327
333
  * - `sandboxEgress`: the base list grows: from none to anything else, or to hosts (the
328
334
  * deployment's when the org gives none) not all in the current list.
329
335
  */
330
- assertOnlyNarrows(before: OrgSettings, patch: OrgSettingsPatch): void;
336
+ assertOnlyNarrows(before: OrgSettings, patch: OrgSettingsPatch): Promise<void>;
331
337
  private updateIn;
332
338
  }
333
339
  /**