@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
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
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
|
-
/**
|
|
317
|
-
|
|
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
|
/**
|