@capybara-hq/kernel 0.1.0-rc.20 → 0.1.0-rc.22
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/bootstrap.json +10 -1
- package/dist/agents/admin-agents.d.ts +16 -3
- package/dist/agents/admin-agents.js +65 -7
- package/dist/agents/agents.schemas.d.ts +24 -6
- package/dist/agents/agents.service.d.ts +4 -3
- package/dist/agents/agents.service.js +39 -26
- package/dist/agents/definition.d.ts +44 -6
- package/dist/agents/definition.js +86 -18
- package/dist/agents/from-template.d.ts +6 -3
- package/dist/agents/from-template.js +57 -44
- package/dist/agents/org-templates.js +97 -41
- package/dist/agents/templates.d.ts +6 -0
- package/dist/agents/templates.js +15 -1
- package/dist/catalog/admin-catalog.controller.d.ts +2 -4
- package/dist/catalog/admin-catalog.controller.js +2 -6
- package/dist/catalog/catalog.module.js +3 -0
- package/dist/catalog/claude/report.d.ts +9 -2
- package/dist/catalog/claude/report.js +1 -0
- package/dist/catalog/claude/translate.js +1 -1
- package/dist/catalog/component-promotion.js +1 -2
- package/dist/catalog/component-state.d.ts +13 -1
- package/dist/catalog/component-state.js +14 -5
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/catalog/marketplaces.service.js +2 -1
- package/dist/catalog/org-catalog.controller.d.ts +13 -3
- package/dist/catalog/org-catalog.controller.js +26 -4
- package/dist/catalog/org-catalog.schemas.d.ts +13 -0
- package/dist/catalog/org-catalog.schemas.js +34 -0
- package/dist/catalog/org-plugin-uploads.controller.js +0 -1
- package/dist/catalog/org-plugin-uploads.d.ts +0 -1
- package/dist/catalog/org-plugin-uploads.js +0 -3
- package/dist/catalog/org-plugin-uploads.schemas.d.ts +0 -1
- package/dist/catalog/org-plugin-uploads.schemas.js +0 -2
- package/dist/catalog/platform-components.d.ts +0 -1
- package/dist/catalog/platform-components.js +3 -8
- package/dist/catalog/plugin-import.d.ts +0 -8
- package/dist/catalog/plugin-import.js +4 -35
- package/dist/catalog/plugin-install.d.ts +2 -2
- package/dist/catalog/plugin-install.js +2 -2
- package/dist/catalog/plugin-package.d.ts +7 -0
- package/dist/catalog/plugin-package.js +27 -1
- package/dist/catalog/plugin-upgrade.d.ts +5 -6
- package/dist/catalog/plugin-upgrade.js +5 -23
- package/dist/catalog/requests.service.d.ts +66 -10
- package/dist/catalog/requests.service.js +196 -15
- package/dist/channels/actions.d.ts +2 -0
- package/dist/channels/actions.js +19 -4
- package/dist/channels/cards.d.ts +16 -2
- package/dist/channels/cards.js +40 -7
- package/dist/channels/channels.module.js +3 -0
- package/dist/channels/outbound.d.ts +22 -1
- package/dist/channels/outbound.js +116 -7
- package/dist/channels/phrases.d.ts +25 -0
- package/dist/channels/phrases.js +19 -0
- package/dist/channels/threads.d.ts +12 -0
- package/dist/channels/threads.js +34 -0
- package/dist/config/schema.d.ts +23 -1
- package/dist/config/schema.js +105 -5
- package/dist/conversations/conversations.service.d.ts +42 -9
- package/dist/conversations/conversations.service.js +57 -25
- package/dist/conversations/events.d.ts +106 -11
- package/dist/conversations/events.js +121 -18
- package/dist/conversations/message-context.d.ts +6 -3
- package/dist/conversations/message-context.js +14 -10
- package/dist/conversations/queue.d.ts +15 -2
- package/dist/conversations/queue.js +22 -3
- package/dist/conversations/queue.service.d.ts +12 -6
- package/dist/conversations/queue.service.js +36 -13
- package/dist/db/kernel-schema.d.ts +105 -3
- package/dist/devices/conversation-devices.d.ts +8 -1
- package/dist/devices/conversation-devices.js +8 -0
- package/dist/devices/devices.controller.d.ts +21 -4
- package/dist/devices/devices.controller.js +46 -8
- package/dist/devices/devices.schemas.d.ts +9 -1
- package/dist/devices/devices.schemas.js +11 -3
- package/dist/devices/devices.service.d.ts +176 -4
- package/dist/devices/devices.service.js +341 -6
- package/dist/devices/dto.d.ts +56 -3
- package/dist/devices/dto.js +51 -1
- package/dist/devices/public-suffix.d.ts +6 -0
- package/dist/devices/public-suffix.js +79 -0
- package/dist/errors.d.ts +46 -5
- package/dist/errors.js +46 -5
- package/dist/evals/reconstruct.d.ts +18 -13
- package/dist/evals/reconstruct.js +50 -36
- package/dist/evals/recorded-tools.d.ts +7 -7
- package/dist/evals/recorded-tools.js +4 -3
- package/dist/evals/trajectory.d.ts +6 -1
- package/dist/evals/trajectory.js +7 -0
- package/dist/extensions/bootstrap.d.ts +49 -10
- package/dist/extensions/bootstrap.js +104 -27
- package/dist/extensions/package.js +2 -2
- package/dist/http/api.module.js +5 -0
- package/dist/inbox/inbox.module.js +2 -1
- package/dist/inbox/inbox.service.d.ts +19 -4
- package/dist/inbox/inbox.service.js +14 -3
- package/dist/infra/config/kernel-config.service.js +11 -0
- package/dist/infra/events/kernel-events.d.ts +2 -1
- package/dist/lifecycle/kernel-lifecycle.d.ts +5 -1
- package/dist/lifecycle/kernel-lifecycle.js +16 -4
- package/dist/local-devices/device-policy.service.d.ts +65 -0
- package/dist/local-devices/device-policy.service.js +217 -0
- package/dist/local-devices/local-device-gateway.js +4 -1
- package/dist/local-devices/local-device-provider.d.ts +40 -3
- package/dist/local-devices/local-device-provider.js +109 -7
- package/dist/local-devices/local-devices.module.d.ts +2 -1
- package/dist/local-devices/local-devices.module.js +19 -3
- package/dist/local-devices/machines.d.ts +14 -3
- package/dist/local-devices/machines.js +41 -3
- package/dist/local-devices/network-grants.d.ts +104 -0
- package/dist/local-devices/network-grants.js +288 -0
- package/dist/local-devices/policy-summary.d.ts +20 -0
- package/dist/local-devices/policy-summary.js +63 -0
- package/dist/orgs/orgs.module.js +4 -10
- package/dist/orgs/roles.d.ts +1 -1
- package/dist/orgs/roles.js +4 -0
- package/dist/orgs/settings.controller.js +2 -1
- package/dist/orgs/settings.service.d.ts +164 -12
- package/dist/orgs/settings.service.js +212 -24
- package/dist/platform/exclusions.js +11 -1
- package/dist/platform/operation.js +4 -0
- package/dist/platform/ops/admin-items.ops.js +1 -1
- package/dist/platform/ops/agent-skills.ops.js +2 -2
- package/dist/platform/ops/agent-templates.ops.js +2 -2
- package/dist/platform/ops/agents.ops.js +11 -15
- package/dist/platform/ops/devices.ops.js +3 -1
- package/dist/platform/ops/inbox.ops.js +19 -0
- package/dist/platform/ops/org-settings.ops.js +14 -5
- package/dist/platform/ops/platform.ops.d.ts +9 -1
- package/dist/platform/ops/platform.ops.js +34 -12
- package/dist/platform/ops/requests.ops.js +3 -2
- package/dist/platform/ops/schedules.ops.d.ts +21 -0
- package/dist/platform/ops/schedules.ops.js +470 -0
- package/dist/platform/ops/skills.ops.js +3 -3
- package/dist/platform/platform-ops.module.d.ts +2 -1
- package/dist/platform/platform-ops.module.js +5 -0
- package/dist/platform/route-schemas.js +3 -0
- package/dist/roles/worker.module.js +3 -0
- package/dist/runtime/context/build.js +13 -4
- package/dist/runtime/context/compaction.js +6 -3
- package/dist/runtime/context/device-paths.d.ts +43 -21
- package/dist/runtime/context/device-paths.js +104 -21
- package/dist/runtime/context/run-context.d.ts +9 -0
- package/dist/runtime/context/run-context.js +8 -0
- package/dist/runtime/context/skills.d.ts +14 -1
- package/dist/runtime/context/skills.js +36 -5
- package/dist/runtime/maintenance.d.ts +4 -2
- package/dist/runtime/maintenance.js +16 -5
- package/dist/runtime/run-finish.d.ts +6 -1
- package/dist/runtime/run-finish.js +16 -2
- package/dist/runtime/run-queue.js +21 -2
- package/dist/runtime/run-worker.js +14 -33
- package/dist/runtime/runtime.module.js +3 -0
- package/dist/runtime/subagents/definition.d.ts +32 -14
- package/dist/runtime/subagents/definition.js +106 -109
- package/dist/runtime/subagents/dispatch.js +4 -3
- package/dist/runtime/subagents/preamble.d.ts +2 -1
- package/dist/runtime/subagents/preamble.js +2 -1
- package/dist/runtime/waits.js +14 -4
- package/dist/sandbox/sandbox.service.d.ts +120 -10
- package/dist/sandbox/sandbox.service.js +335 -28
- package/dist/sandbox/tiers.d.ts +86 -0
- package/dist/sandbox/tiers.js +96 -0
- package/dist/schedules/effective-settings.d.ts +16 -0
- package/dist/schedules/effective-settings.js +17 -0
- package/dist/schedules/errors.d.ts +8 -0
- package/dist/schedules/errors.js +8 -0
- package/dist/schedules/fire.d.ts +118 -0
- package/dist/schedules/fire.js +625 -0
- package/dist/schedules/grant-rules.d.ts +94 -0
- package/dist/schedules/grant-rules.js +126 -0
- package/dist/schedules/job-ids.d.ts +32 -0
- package/dist/schedules/job-ids.js +27 -0
- package/dist/schedules/pause.d.ts +9 -0
- package/dist/schedules/pause.js +33 -0
- package/dist/schedules/run-tracker.d.ts +81 -0
- package/dist/schedules/run-tracker.js +333 -0
- package/dist/schedules/scan.job.d.ts +64 -0
- package/dist/schedules/scan.job.js +303 -0
- package/dist/schedules/schedule-math.d.ts +31 -0
- package/dist/schedules/schedule-math.js +223 -0
- package/dist/schedules/scheduled-tasks.service.d.ts +351 -0
- package/dist/schedules/scheduled-tasks.service.js +1193 -0
- package/dist/schedules/schedules.controller.d.ts +68 -0
- package/dist/schedules/schedules.controller.js +242 -0
- package/dist/schedules/schedules.module.d.ts +16 -0
- package/dist/schedules/schedules.module.js +65 -0
- package/dist/schedules/schedules.schemas.d.ts +316 -0
- package/dist/schedules/schedules.schemas.js +186 -0
- package/dist/schedules/store.d.ts +81 -0
- package/dist/schedules/store.js +390 -0
- package/dist/schedules/store.module.d.ts +6 -0
- package/dist/schedules/store.module.js +21 -0
- package/dist/schedules/types.d.ts +108 -0
- package/dist/schedules/types.js +1 -0
- package/dist/schedules/unattended.d.ts +117 -0
- package/dist/schedules/unattended.js +228 -0
- package/dist/setup/oauth-token-probe.js +8 -4
- package/dist/skills/agent-skills.service.d.ts +4 -8
- package/dist/skills/agent-skills.service.js +18 -53
- package/dist/skills/catalog.d.ts +65 -62
- package/dist/skills/catalog.js +109 -206
- package/dist/skills/conversation-catalog.d.ts +3 -3
- package/dist/skills/conversation-catalog.js +5 -12
- package/dist/skills/invoke.d.ts +2 -2
- package/dist/skills/skills.controller.d.ts +3 -1
- package/dist/skills/skills.service.d.ts +38 -37
- package/dist/skills/skills.service.js +168 -107
- package/dist/skills/usable.d.ts +57 -8
- package/dist/skills/usable.js +172 -38
- package/dist/tools/approvals.controller.d.ts +4 -2
- package/dist/tools/approvals.controller.js +17 -2
- package/dist/tools/approvals.schemas.d.ts +1 -0
- package/dist/tools/approvals.schemas.js +3 -1
- package/dist/tools/approvals.service.d.ts +182 -7
- package/dist/tools/approvals.service.js +597 -13
- package/dist/tools/builtin-names.d.ts +25 -4
- package/dist/tools/builtin-names.js +45 -4
- package/dist/tools/builtins/builtin-tools.d.ts +8 -0
- package/dist/tools/builtins/builtin-tools.js +23 -2
- package/dist/tools/builtins/sandbox/bash.js +16 -7
- package/dist/tools/builtins/sandbox/common.d.ts +1 -1
- package/dist/tools/builtins/sandbox/common.js +33 -10
- package/dist/tools/builtins/sandbox/edit.js +7 -3
- package/dist/tools/builtins/sandbox/file-state.d.ts +31 -1
- package/dist/tools/builtins/sandbox/file-state.js +33 -3
- package/dist/tools/builtins/sandbox/write.js +8 -4
- package/dist/tools/builtins/schedules.d.ts +107 -0
- package/dist/tools/builtins/schedules.js +789 -0
- package/dist/tools/elicitation.d.ts +2 -0
- package/dist/tools/elicitation.js +1 -1
- package/dist/tools/fixed-ask.d.ts +7 -0
- package/dist/tools/fixed-ask.js +9 -0
- package/dist/tools/network-asks.d.ts +109 -0
- package/dist/tools/network-asks.js +221 -0
- package/dist/tools/pipeline.d.ts +53 -6
- package/dist/tools/pipeline.js +202 -13
- package/dist/tools/policy.d.ts +21 -8
- package/dist/tools/policy.js +63 -9
- package/dist/tools/registry.d.ts +10 -1
- package/dist/tools/tools.module.js +12 -0
- package/package.json +2 -2
- package/src/migrations/0113_scheduled_tasks.sql +108 -0
- package/src/migrations/0114_network_asks.sql +23 -0
- package/src/migrations/0115_network_requests.sql +28 -0
- package/src/migrations/0116_skills_default_on.sql +37 -0
- package/src/migrations/0117_sandbox_tiers.sql +11 -0
|
@@ -120,6 +120,8 @@ let KernelLifecycle = class KernelLifecycle {
|
|
|
120
120
|
unwatchConfig;
|
|
121
121
|
/** What the first bootstrap pass did, carried into the second one. */
|
|
122
122
|
bootstrapCarry;
|
|
123
|
+
/** The packages the first bootstrap pass obtained, so the second downloads nothing again. */
|
|
124
|
+
bootstrapPackages;
|
|
123
125
|
constructor(accounts, configService, log, options,
|
|
124
126
|
// biome-ignore lint/suspicious/noExplicitAny: the pool carries no schema types
|
|
125
127
|
pool, redis, kdb, shutdown, host, registry, installs, blobs, extensionLifecycle, replicaStatus, bootstrap, marketplaces, pluginImport, bus,
|
|
@@ -303,7 +305,9 @@ let KernelLifecycle = class KernelLifecycle {
|
|
|
303
305
|
* kernel never serves without a required plugin. A failed optional entry is logged and returns
|
|
304
306
|
* true: the start runs the step once more after the extensions loaded (a declarative entry's
|
|
305
307
|
* catalog files need the object store, which on a fresh deployment is itself a bootstrap plugin),
|
|
306
|
-
* and the next start tries again while it stays incomplete.
|
|
308
|
+
* and the next start tries again while it stays incomplete. The first pass skips the `builtin`
|
|
309
|
+
* entries (they may ship skills and templates, which need the object store) and returns true for
|
|
310
|
+
* them too; the final pass imports them, and one that fails is logged like an optional entry.
|
|
307
311
|
*/
|
|
308
312
|
async runBootstrap(dir, final = false) {
|
|
309
313
|
const manifest = this.options.bootstrapManifest ?? (await readBootstrapManifest());
|
|
@@ -318,17 +322,25 @@ let KernelLifecycle = class KernelLifecycle {
|
|
|
318
322
|
...(dir !== undefined && { dir }),
|
|
319
323
|
signal: this.shutdown.signal,
|
|
320
324
|
...(this.bootstrapCarry !== undefined && { carry: this.bootstrapCarry }),
|
|
325
|
+
...(this.bootstrapPackages !== undefined && { packages: this.bootstrapPackages }),
|
|
326
|
+
deferBuiltin: !final,
|
|
321
327
|
});
|
|
322
328
|
return false;
|
|
323
329
|
}
|
|
324
330
|
catch (e) {
|
|
325
331
|
if (e instanceof BootstrapFailedError && !e.requiredFailed) {
|
|
326
332
|
this.bootstrapCarry = e.results;
|
|
327
|
-
|
|
328
|
-
|
|
333
|
+
this.bootstrapPackages = e.packages;
|
|
334
|
+
if (e.failures.length === 0) {
|
|
335
|
+
this.log.info("bootstrap: builtin plugins are installed once the extensions have loaded", {
|
|
336
|
+
deferred: e.deferred,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
else if (final) {
|
|
340
|
+
this.log.error("bootstrap: optional or builtin plugins could not be installed; retried on the next start", { failures: e.failures });
|
|
329
341
|
}
|
|
330
342
|
else {
|
|
331
|
-
this.log.warn("bootstrap: optional plugins could not be installed yet; retried once the extensions have loaded", { failures: e.failures });
|
|
343
|
+
this.log.warn("bootstrap: optional or builtin plugins could not be installed yet; retried once the extensions have loaded", { failures: e.failures });
|
|
332
344
|
}
|
|
333
345
|
return true;
|
|
334
346
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type DeviceFeature, type DeviceLocalPolicySummary, type DevicePolicyArgs } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
3
|
+
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
4
|
+
import type { KernelLogger } from "../log.js";
|
|
5
|
+
import { MachinesService } from "./machines.js";
|
|
6
|
+
import { LocalDeviceRelay } from "./relay.js";
|
|
7
|
+
/** How long the machine may take to apply a policy change. */
|
|
8
|
+
export declare const POLICY_DEADLINE_MS = 30000;
|
|
9
|
+
/** What became of a policy change. */
|
|
10
|
+
export type PolicyChangeOutcome = {
|
|
11
|
+
applied: true;
|
|
12
|
+
policy: DeviceLocalPolicySummary;
|
|
13
|
+
} | {
|
|
14
|
+
applied: false;
|
|
15
|
+
reason: "offline" | "refused" | "unsupported";
|
|
16
|
+
message: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Changes a local device's policy on its machine (network ask-to-allow spec §5.2; sandbox dev
|
|
20
|
+
* tiers spec §7.3: its hosts, its `tier`, and `isolation` back to `sandbox`): the kernel→device
|
|
21
|
+
* `policy` op over the {@link LocalDeviceRelay}. The machine is the authority: it
|
|
22
|
+
* refuses widening unless its owner allows remote grants, validates every host and answers with
|
|
23
|
+
* the policy as written, which the kernel then keeps on the active machine's row (§5.4) and
|
|
24
|
+
* audits as `device.policy_changed`.
|
|
25
|
+
*
|
|
26
|
+
* The op is sent outside any transaction (a decision records its outcome afterwards, in its own
|
|
27
|
+
* transaction); only the summary write and the audit row are transactional. Callers decide who
|
|
28
|
+
* may change a device's policy (the device owner, in a signed-in session); this service checks
|
|
29
|
+
* only that the device is a live device of the org.
|
|
30
|
+
*/
|
|
31
|
+
export declare class DevicePolicyService {
|
|
32
|
+
private readonly db;
|
|
33
|
+
readonly txHost: TransactionHost;
|
|
34
|
+
private readonly relay;
|
|
35
|
+
private readonly machines;
|
|
36
|
+
private readonly log;
|
|
37
|
+
constructor(db: KernelDb, txHost: TransactionHost, relay: Pick<LocalDeviceRelay, "call" | "holderLive">, machines: Pick<MachinesService, "activeFor">, log: Pick<KernelLogger, "warn">);
|
|
38
|
+
/**
|
|
39
|
+
* Sends the `policy` op (no transaction may be open), stores the returned summary and audits
|
|
40
|
+
* `device.policy_changed`. Never throws for an offline machine, a refusal or a program without
|
|
41
|
+
* the `policyOp` feature (nothing is sent to it); a device that is not a live device of `orgId`
|
|
42
|
+
* is `device.not_found`.
|
|
43
|
+
*/
|
|
44
|
+
apply(input: {
|
|
45
|
+
orgId: string;
|
|
46
|
+
deviceId: string;
|
|
47
|
+
change: Omit<DevicePolicyArgs, "source">;
|
|
48
|
+
source: DevicePolicyArgs["source"];
|
|
49
|
+
actorTokenId?: string;
|
|
50
|
+
}): Promise<PolicyChangeOutcome>;
|
|
51
|
+
/** The active machine's features and policy summary (null when no machine is connected). */
|
|
52
|
+
summary(deviceId: string): Promise<{
|
|
53
|
+
features: DeviceFeature[];
|
|
54
|
+
policy: DeviceLocalPolicySummary | null;
|
|
55
|
+
} | null>;
|
|
56
|
+
/**
|
|
57
|
+
* Keeps the policy the machine wrote on its row and audits the change (`confirmed`); with no
|
|
58
|
+
* policy (the answer never came) only audits the attempt, unconfirmed. The detail's `isolation`
|
|
59
|
+
* is the one asked for, or the one the machine now reports when it changed (a tier change turns
|
|
60
|
+
* the sandbox back on).
|
|
61
|
+
*/
|
|
62
|
+
private record;
|
|
63
|
+
/** The outcome of a failed op: offline, or the machine's refusal; a deleted device is thrown. */
|
|
64
|
+
private notApplied;
|
|
65
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
|
+
};
|
|
10
|
+
import { ToolError, validateAllowHost, } from "@capybara-hq/plugin-sdk";
|
|
11
|
+
import { Inject, Injectable } from "@nestjs/common";
|
|
12
|
+
import { ApiError } from "../errors.js";
|
|
13
|
+
import { audit } from "../identity/audit.js";
|
|
14
|
+
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
15
|
+
import { currentStore, TransactionHost } from "../infra/database/transaction-host.js";
|
|
16
|
+
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
17
|
+
import { KERNEL_LOGGER } from "../infra/tokens.js";
|
|
18
|
+
import { deviceNotFound } from "../sandbox/sandbox.service.js";
|
|
19
|
+
import { MachinesService } from "./machines.js";
|
|
20
|
+
import { isolationOf } from "./policy-summary.js";
|
|
21
|
+
import { LocalDeviceRelay, ON_MACHINE_PREFIX } from "./relay.js";
|
|
22
|
+
/** How long the machine may take to apply a policy change. */
|
|
23
|
+
export const POLICY_DEADLINE_MS = 30_000;
|
|
24
|
+
/** The prefix the relay puts before a machine's `policy`-kind refusal. */
|
|
25
|
+
const REFUSED_PREFIX = "Refused by the machine's local policy: ";
|
|
26
|
+
const OFFLINE_MESSAGE = "The device's machine is offline, so its policy was not changed. Start capybara-device on it.";
|
|
27
|
+
const UNSUPPORTED_MESSAGE = "The device's capybara-device program cannot change its policy from the platform. Update capybara-device on the machine.";
|
|
28
|
+
const errMsg = (e) => (e instanceof Error ? e.message : String(e));
|
|
29
|
+
/**
|
|
30
|
+
* Changes a local device's policy on its machine (network ask-to-allow spec §5.2; sandbox dev
|
|
31
|
+
* tiers spec §7.3: its hosts, its `tier`, and `isolation` back to `sandbox`): the kernel→device
|
|
32
|
+
* `policy` op over the {@link LocalDeviceRelay}. The machine is the authority: it
|
|
33
|
+
* refuses widening unless its owner allows remote grants, validates every host and answers with
|
|
34
|
+
* the policy as written, which the kernel then keeps on the active machine's row (§5.4) and
|
|
35
|
+
* audits as `device.policy_changed`.
|
|
36
|
+
*
|
|
37
|
+
* The op is sent outside any transaction (a decision records its outcome afterwards, in its own
|
|
38
|
+
* transaction); only the summary write and the audit row are transactional. Callers decide who
|
|
39
|
+
* may change a device's policy (the device owner, in a signed-in session); this service checks
|
|
40
|
+
* only that the device is a live device of the org.
|
|
41
|
+
*/
|
|
42
|
+
let DevicePolicyService = class DevicePolicyService {
|
|
43
|
+
db;
|
|
44
|
+
txHost;
|
|
45
|
+
relay;
|
|
46
|
+
machines;
|
|
47
|
+
log;
|
|
48
|
+
constructor(db, txHost, relay, machines, log) {
|
|
49
|
+
this.db = db;
|
|
50
|
+
this.txHost = txHost;
|
|
51
|
+
this.relay = relay;
|
|
52
|
+
this.machines = machines;
|
|
53
|
+
this.log = log;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Sends the `policy` op (no transaction may be open), stores the returned summary and audits
|
|
57
|
+
* `device.policy_changed`. Never throws for an offline machine, a refusal or a program without
|
|
58
|
+
* the `policyOp` feature (nothing is sent to it); a device that is not a live device of `orgId`
|
|
59
|
+
* is `device.not_found`.
|
|
60
|
+
*/
|
|
61
|
+
async apply(input) {
|
|
62
|
+
if (currentStore()) {
|
|
63
|
+
throw new Error("DevicePolicyService.apply() must not run inside a transaction: send the policy op " +
|
|
64
|
+
"first, then record the decision in a transaction.");
|
|
65
|
+
}
|
|
66
|
+
assertHosts(input.change, input.source.approvalId !== undefined);
|
|
67
|
+
const device = await this.db
|
|
68
|
+
.selectFrom("devices as e")
|
|
69
|
+
.innerJoin("orgs as o", "o.id", "e.org_id")
|
|
70
|
+
.select("e.source")
|
|
71
|
+
.where("e.id", "=", input.deviceId)
|
|
72
|
+
.where("e.org_id", "=", input.orgId)
|
|
73
|
+
.where("e.deleted_at", "is", null)
|
|
74
|
+
.where("o.deleted_at", "is", null)
|
|
75
|
+
.executeTakeFirst();
|
|
76
|
+
if (!device)
|
|
77
|
+
throw deviceNotFound();
|
|
78
|
+
if (device.source !== "local") {
|
|
79
|
+
return { applied: false, reason: "unsupported", message: UNSUPPORTED_MESSAGE };
|
|
80
|
+
}
|
|
81
|
+
const machine = await this.machines.activeFor(input.deviceId);
|
|
82
|
+
if (machine === null)
|
|
83
|
+
return { applied: false, reason: "offline", message: OFFLINE_MESSAGE };
|
|
84
|
+
if (!machine.features.includes("policyOp")) {
|
|
85
|
+
return { applied: false, reason: "unsupported", message: UNSUPPORTED_MESSAGE };
|
|
86
|
+
}
|
|
87
|
+
// not connected: nothing is sent (a queued op would wait for a reconnect nobody watches)
|
|
88
|
+
if (!(await this.relay.holderLive(input.deviceId))) {
|
|
89
|
+
return { applied: false, reason: "offline", message: OFFLINE_MESSAGE };
|
|
90
|
+
}
|
|
91
|
+
const args = { ...input.change, source: input.source };
|
|
92
|
+
let policy;
|
|
93
|
+
try {
|
|
94
|
+
({ policy } = await this.relay.call(input.deviceId, "policy", args, {
|
|
95
|
+
deadlineMs: POLICY_DEADLINE_MS,
|
|
96
|
+
// the relay gives up by itself (deadline plus the reconnect grace)
|
|
97
|
+
signal: new AbortController().signal,
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
if (e instanceof ApiError && e.code === "device.offline") {
|
|
102
|
+
// the op was sent and the wait ran out: the machine may still apply it, so the attempt is
|
|
103
|
+
// audited unconfirmed; its next hello reports the policy as it is
|
|
104
|
+
await this.record({ ...input, machineId: machine.id, policy: null });
|
|
105
|
+
}
|
|
106
|
+
return this.notApplied(input.deviceId, e);
|
|
107
|
+
}
|
|
108
|
+
await this.record({ ...input, machineId: machine.id, policy });
|
|
109
|
+
return { applied: true, policy };
|
|
110
|
+
}
|
|
111
|
+
/** The active machine's features and policy summary (null when no machine is connected). */
|
|
112
|
+
async summary(deviceId) {
|
|
113
|
+
const m = await this.machines.activeFor(deviceId);
|
|
114
|
+
return m === null ? null : { features: m.features, policy: m.localPolicy };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Keeps the policy the machine wrote on its row and audits the change (`confirmed`); with no
|
|
118
|
+
* policy (the answer never came) only audits the attempt, unconfirmed. The detail's `isolation`
|
|
119
|
+
* is the one asked for, or the one the machine now reports when it changed (a tier change turns
|
|
120
|
+
* the sandbox back on).
|
|
121
|
+
*/
|
|
122
|
+
async record(input) {
|
|
123
|
+
// the isolation the machine reports now, when it differs from the stored one: a tier change
|
|
124
|
+
// turns an unsandboxed device's sandbox back on without asking for it (spec §5.4, §10.4)
|
|
125
|
+
let isolationNow;
|
|
126
|
+
if (input.policy !== null) {
|
|
127
|
+
const prev = await this.db
|
|
128
|
+
.selectFrom("device_machines")
|
|
129
|
+
.select("local_policy")
|
|
130
|
+
.where("id", "=", input.machineId)
|
|
131
|
+
.where("revoked_at", "is", null)
|
|
132
|
+
.forUpdate()
|
|
133
|
+
.executeTakeFirst();
|
|
134
|
+
const before = isolationOf(prev?.local_policy) ?? "sandbox";
|
|
135
|
+
if (input.policy.isolation !== before)
|
|
136
|
+
isolationNow = input.policy.isolation;
|
|
137
|
+
await this.db
|
|
138
|
+
.updateTable("device_machines")
|
|
139
|
+
.set({ local_policy: JSON.stringify(input.policy) })
|
|
140
|
+
.where("id", "=", input.machineId)
|
|
141
|
+
.where("revoked_at", "is", null)
|
|
142
|
+
.execute();
|
|
143
|
+
}
|
|
144
|
+
const { change, source } = input;
|
|
145
|
+
await audit(this.db.db, {
|
|
146
|
+
actorAccountId: source.accountId,
|
|
147
|
+
...(input.actorTokenId !== undefined && { actorTokenId: input.actorTokenId }),
|
|
148
|
+
orgId: input.orgId,
|
|
149
|
+
action: "device.policy_changed",
|
|
150
|
+
target: input.deviceId,
|
|
151
|
+
detail: {
|
|
152
|
+
add: change.add ?? [],
|
|
153
|
+
remove: change.remove ?? [],
|
|
154
|
+
// only when set (null: follow the org's default); the device page lists the change
|
|
155
|
+
...(change.tier !== undefined && { tier: change.tier }),
|
|
156
|
+
...((isolationNow ?? change.isolation) !== undefined && {
|
|
157
|
+
isolation: isolationNow ?? change.isolation,
|
|
158
|
+
}),
|
|
159
|
+
by: source.accountId,
|
|
160
|
+
via: source.via,
|
|
161
|
+
approvalId: source.approvalId ?? null,
|
|
162
|
+
confirmed: input.policy !== null,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/** The outcome of a failed op: offline, or the machine's refusal; a deleted device is thrown. */
|
|
167
|
+
notApplied(deviceId, e) {
|
|
168
|
+
if (e instanceof ApiError) {
|
|
169
|
+
if (e.code === "device.not_found")
|
|
170
|
+
throw e;
|
|
171
|
+
// device.offline, or device.forbidden (the machine was disconnected meanwhile)
|
|
172
|
+
return { applied: false, reason: "offline", message: OFFLINE_MESSAGE };
|
|
173
|
+
}
|
|
174
|
+
if (e instanceof ToolError && e.message.startsWith(REFUSED_PREFIX)) {
|
|
175
|
+
return { applied: false, reason: "refused", message: e.message.slice(REFUSED_PREFIX.length) };
|
|
176
|
+
}
|
|
177
|
+
// anything else the machine answered (its own error, an invalid result): not applied
|
|
178
|
+
this.log.warn("a device policy change failed on the machine", { deviceId, error: errMsg(e) });
|
|
179
|
+
const message = errMsg(e);
|
|
180
|
+
return {
|
|
181
|
+
applied: false,
|
|
182
|
+
reason: "refused",
|
|
183
|
+
message: message.startsWith(ON_MACHINE_PREFIX)
|
|
184
|
+
? message.slice(ON_MACHINE_PREFIX.length)
|
|
185
|
+
: message,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
__decorate([
|
|
190
|
+
Transactional()
|
|
191
|
+
], DevicePolicyService.prototype, "record", null);
|
|
192
|
+
DevicePolicyService = __decorate([
|
|
193
|
+
Injectable(),
|
|
194
|
+
__param(0, Inject(KernelDb)),
|
|
195
|
+
__param(1, Inject(TransactionHost)),
|
|
196
|
+
__param(2, Inject(LocalDeviceRelay)),
|
|
197
|
+
__param(3, Inject(MachinesService)),
|
|
198
|
+
__param(4, Inject(KERNEL_LOGGER))
|
|
199
|
+
], DevicePolicyService);
|
|
200
|
+
export { DevicePolicyService };
|
|
201
|
+
/**
|
|
202
|
+
* Hosts the platform may send: an approval's `add` takes exact hosts only (a card never grants a
|
|
203
|
+
* wildcard); the owner's own change on the device page (no approval) may add a wildcard (spec §7);
|
|
204
|
+
* `remove` takes any valid entry (narrowing, a wildcard too).
|
|
205
|
+
*/
|
|
206
|
+
function assertHosts(change, fromApproval) {
|
|
207
|
+
const bad = (h) => typeof h !== "string" || validateAllowHost(h).length > 0;
|
|
208
|
+
for (const h of change.add ?? []) {
|
|
209
|
+
if (bad(h) || (fromApproval && h.startsWith("*."))) {
|
|
210
|
+
throw new Error(`not an exact host to add to a device policy: ${JSON.stringify(h)}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const h of change.remove ?? []) {
|
|
214
|
+
if (bad(h))
|
|
215
|
+
throw new Error(`not a host to remove from a device policy: ${JSON.stringify(h)}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -177,7 +177,10 @@ class DeviceConnection {
|
|
|
177
177
|
if (!(await this.claim()))
|
|
178
178
|
return;
|
|
179
179
|
try {
|
|
180
|
-
|
|
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);
|
|
181
184
|
}
|
|
182
185
|
catch (e) {
|
|
183
186
|
this.gw.log.warn("could not record a device hello", {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ExecResult, type FileSystemHandle, type SandboxHandle, type SandboxLimits, type SandboxProvider, type SandboxRoots } from "@capybara-hq/plugin-sdk";
|
|
2
2
|
import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
3
|
+
import type { KernelLogger } from "../log.js";
|
|
3
4
|
import { SandboxService } from "../sandbox/sandbox.service.js";
|
|
5
|
+
import { NetworkGrants } from "./network-grants.js";
|
|
4
6
|
import { LocalDeviceRelay } from "./relay.js";
|
|
5
7
|
import { DeviceTargets } from "./targets.js";
|
|
6
8
|
/** Extra time the machine gets beyond a command's own timeout (spawn, output, the trip back). */
|
|
@@ -15,6 +17,11 @@ export declare const OP_DEADLINE_MS = 30000;
|
|
|
15
17
|
export declare const FRAME_ENVELOPE_BYTES: number;
|
|
16
18
|
/** The most raw bytes whose base64 plus {@link FRAME_ENVELOPE_BYTES} fit in `maxFrameBytes`. */
|
|
17
19
|
export declare const maxReadBytes: (maxFrameBytes: number) => number;
|
|
20
|
+
/**
|
|
21
|
+
* The tool error of a device that runs commands without a sandbox the org does not allow (§4.3);
|
|
22
|
+
* also what a machine answers when it refuses such a command itself.
|
|
23
|
+
*/
|
|
24
|
+
export declare const UNSANDBOXED_NOT_ALLOWED_ERROR = "This device runs commands without a sandbox, which your organization does not allow. Its owner can turn the sandbox back on with `capybara-device policy set isolation sandbox`.";
|
|
18
25
|
/**
|
|
19
26
|
* The handle of an instance on a machine: the device id (ruling R1: the relay is keyed by
|
|
20
27
|
* device), the roots the machine reported at `create`, and the org's effective limits at
|
|
@@ -42,9 +49,11 @@ export declare class LocalDeviceProvider implements SandboxProvider {
|
|
|
42
49
|
private readonly targets;
|
|
43
50
|
private readonly sandbox;
|
|
44
51
|
private readonly config;
|
|
52
|
+
private readonly grants;
|
|
53
|
+
private readonly log;
|
|
45
54
|
readonly concurrentExec: true;
|
|
46
55
|
readonly sessionIsolation = false;
|
|
47
|
-
constructor(relay: LocalDeviceRelay, targets: Pick<DeviceTargets, "orgOf" | "machineRoots">, sandbox: Pick<SandboxService, "
|
|
56
|
+
constructor(relay: LocalDeviceRelay, targets: Pick<DeviceTargets, "orgOf" | "machineRoots">, sandbox: Pick<SandboxService, "devicePolicy" | "deviceLimits">, config: Pick<KernelConfigService, "get">, grants: Pick<NetworkGrants, "forExec" | "unattendedTier" | "unsandboxed" | "recordMissing">, log: Pick<KernelLogger, "warn">);
|
|
48
57
|
createFileSystem(args: {
|
|
49
58
|
name: string;
|
|
50
59
|
limits: SandboxLimits;
|
|
@@ -71,6 +80,23 @@ export declare class LocalDeviceProvider implements SandboxProvider {
|
|
|
71
80
|
destroyByName(_name: string, signal: AbortSignal): Promise<void>;
|
|
72
81
|
killAll(h: SandboxHandle, signal: AbortSignal): Promise<void>;
|
|
73
82
|
killTagged(h: SandboxHandle, tags: string[], signal: AbortSignal): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Runs the command on the machine with the org's limits now (ruling R27) and, for a command of a
|
|
85
|
+
* run, the hosts the kernel allows that run on this device beyond the local policy (network
|
|
86
|
+
* ask-to-allow spec §6.3; only those the org's limits admit, sent only when there are some). The device's report of refused hosts is re-layered
|
|
87
|
+
* against the limits this exec carried: the kernel never takes the device's word that the org
|
|
88
|
+
* allows a host (spec §4.1).
|
|
89
|
+
*
|
|
90
|
+
* The limits are the device's **ceiling** (sandbox dev tiers spec §4.2): `limits.tier` is the
|
|
91
|
+
* highest tier the org allows, its network that tier's, `defaultTier` the org's default (the
|
|
92
|
+
* device runs at `min(local tier ?? defaultTier, tier)`) and `devHosts` the dev hosts. A run of
|
|
93
|
+
* a scheduled task without a person and without the task's `tier:open` grant is capped at `dev`
|
|
94
|
+
* (spec §11.4). Before relaying, a machine that last reported running commands without a sandbox
|
|
95
|
+
* is refused when the org does not allow it, or for such a run (`device.unsandboxed_not_allowed`,
|
|
96
|
+
* spec §4.3). The kernel knows the local policy only from the last hello or policy op, so the
|
|
97
|
+
* same decision also travels in `limits.allowUnsandboxed` and the machine enforces it itself;
|
|
98
|
+
* its refusal ({@link UNSANDBOXED_NOT_ALLOWED_MESSAGE}) becomes the same error.
|
|
99
|
+
*/
|
|
74
100
|
exec(h: SandboxHandle, args: Parameters<SandboxProvider["exec"]>[1]): Promise<ExecResult>;
|
|
75
101
|
readFile(h: SandboxHandle, path: string, opts: {
|
|
76
102
|
maxBytes: number;
|
|
@@ -84,6 +110,17 @@ export declare class LocalDeviceProvider implements SandboxProvider {
|
|
|
84
110
|
size: number;
|
|
85
111
|
isDir: boolean;
|
|
86
112
|
} | null>;
|
|
87
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* The device's org and the limits of one exec now (ruling R27): the ceiling, capped at `dev` for
|
|
115
|
+
* an unattended run without the `tier:open` grant (`cap`, set only when the platform and the org
|
|
116
|
+
* would otherwise allow an unsandboxed command); `allowUnsandboxed` whether this command may run
|
|
117
|
+
* without a sandbox. A deleted device is not found. Refuses an unsandboxed machine the org does
|
|
118
|
+
* not allow (or such a run), see {@link exec}.
|
|
119
|
+
*/
|
|
88
120
|
private currentLimits;
|
|
121
|
+
/**
|
|
122
|
+
* The `device.unsandboxed_not_allowed` error (spec §4.3); for an unattended run refused only for
|
|
123
|
+
* its missing `tier:open` grant, the run's text, with the missing grant recorded on the task run.
|
|
124
|
+
*/
|
|
125
|
+
private unsandboxedRefusal;
|
|
89
126
|
}
|
|
@@ -7,9 +7,14 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
8
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
9
|
};
|
|
10
|
+
import { minTier, ToolError, UNSANDBOXED_NOT_ALLOWED_MESSAGE, } from "@capybara-hq/plugin-sdk";
|
|
10
11
|
import { Inject, Injectable } from "@nestjs/common";
|
|
12
|
+
import { ApiError } from "../errors.js";
|
|
11
13
|
import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
14
|
+
import { KERNEL_LOGGER } from "../infra/tokens.js";
|
|
12
15
|
import { deviceNotFound, SandboxService } from "../sandbox/sandbox.service.js";
|
|
16
|
+
import { TIER_OPEN_GRANT_KEY, UNATTENDED_UNSANDBOXED_ERROR } from "../schedules/unattended.js";
|
|
17
|
+
import { grantsWithin, NetworkGrants, relayer } from "./network-grants.js";
|
|
13
18
|
import { deviceOffline, LocalDeviceRelay } from "./relay.js";
|
|
14
19
|
import { DeviceTargets, sameRoots } from "./targets.js";
|
|
15
20
|
/** Extra time the machine gets beyond a command's own timeout (spawn, output, the trip back). */
|
|
@@ -25,6 +30,16 @@ export const FRAME_ENVELOPE_BYTES = 64 * 1024;
|
|
|
25
30
|
/** The most raw bytes whose base64 plus {@link FRAME_ENVELOPE_BYTES} fit in `maxFrameBytes`. */
|
|
26
31
|
export const maxReadBytes = (maxFrameBytes) => Math.max(0, Math.floor((maxFrameBytes - FRAME_ENVELOPE_BYTES) / 4) * 3);
|
|
27
32
|
const NAME_PREFIX = "cap-device-";
|
|
33
|
+
/**
|
|
34
|
+
* The tool error of a device that runs commands without a sandbox the org does not allow (§4.3);
|
|
35
|
+
* also what a machine answers when it refuses such a command itself.
|
|
36
|
+
*/
|
|
37
|
+
export const UNSANDBOXED_NOT_ALLOWED_ERROR = UNSANDBOXED_NOT_ALLOWED_MESSAGE;
|
|
38
|
+
/**
|
|
39
|
+
* The relay's error for a machine that refused a command with {@link UNSANDBOXED_NOT_ALLOWED_MESSAGE}
|
|
40
|
+
* (a `policy` error: the relay prefixes the machine's message, `relay.ts`).
|
|
41
|
+
*/
|
|
42
|
+
const MACHINE_UNSANDBOXED_REFUSAL = `Refused by the machine's local policy: ${UNSANDBOXED_NOT_ALLOWED_MESSAGE}`;
|
|
28
43
|
/** The device id of a `cap-device-<deviceId>` name; undefined for any other name. */
|
|
29
44
|
export const deviceIdOfName = (name) => name.startsWith(NAME_PREFIX) && name.length > NAME_PREFIX.length
|
|
30
45
|
? name.slice(NAME_PREFIX.length)
|
|
@@ -46,13 +61,17 @@ let LocalDeviceProvider = class LocalDeviceProvider {
|
|
|
46
61
|
targets;
|
|
47
62
|
sandbox;
|
|
48
63
|
config;
|
|
64
|
+
grants;
|
|
65
|
+
log;
|
|
49
66
|
concurrentExec = true;
|
|
50
67
|
sessionIsolation = false;
|
|
51
|
-
constructor(relay, targets, sandbox, config) {
|
|
68
|
+
constructor(relay, targets, sandbox, config, grants, log) {
|
|
52
69
|
this.relay = relay;
|
|
53
70
|
this.targets = targets;
|
|
54
71
|
this.sandbox = sandbox;
|
|
55
72
|
this.config = config;
|
|
73
|
+
this.grants = grants;
|
|
74
|
+
this.log = log;
|
|
56
75
|
}
|
|
57
76
|
async createFileSystem(args) {
|
|
58
77
|
args.signal.throwIfAborted();
|
|
@@ -103,13 +122,36 @@ let LocalDeviceProvider = class LocalDeviceProvider {
|
|
|
103
122
|
async killTagged(h, tags, signal) {
|
|
104
123
|
await this.relay.call(h.id, "killTagged", { tags }, { deadlineMs: OP_DEADLINE_MS, signal });
|
|
105
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Runs the command on the machine with the org's limits now (ruling R27) and, for a command of a
|
|
127
|
+
* run, the hosts the kernel allows that run on this device beyond the local policy (network
|
|
128
|
+
* ask-to-allow spec §6.3; only those the org's limits admit, sent only when there are some). The device's report of refused hosts is re-layered
|
|
129
|
+
* against the limits this exec carried: the kernel never takes the device's word that the org
|
|
130
|
+
* allows a host (spec §4.1).
|
|
131
|
+
*
|
|
132
|
+
* The limits are the device's **ceiling** (sandbox dev tiers spec §4.2): `limits.tier` is the
|
|
133
|
+
* highest tier the org allows, its network that tier's, `defaultTier` the org's default (the
|
|
134
|
+
* device runs at `min(local tier ?? defaultTier, tier)`) and `devHosts` the dev hosts. A run of
|
|
135
|
+
* a scheduled task without a person and without the task's `tier:open` grant is capped at `dev`
|
|
136
|
+
* (spec §11.4). Before relaying, a machine that last reported running commands without a sandbox
|
|
137
|
+
* is refused when the org does not allow it, or for such a run (`device.unsandboxed_not_allowed`,
|
|
138
|
+
* spec §4.3). The kernel knows the local policy only from the last hello or policy op, so the
|
|
139
|
+
* same decision also travels in `limits.allowUnsandboxed` and the machine enforces it itself;
|
|
140
|
+
* its refusal ({@link UNSANDBOXED_NOT_ALLOWED_MESSAGE}) becomes the same error.
|
|
141
|
+
*/
|
|
106
142
|
async exec(h, args) {
|
|
107
143
|
const { signal, ...rest } = args;
|
|
108
144
|
signal.throwIfAborted();
|
|
109
|
-
const
|
|
145
|
+
const runId = rest.actor?.runId;
|
|
146
|
+
const { orgId, limits, cap } = await this.currentLimits(h.id, runId);
|
|
147
|
+
// the run's grants for this device, cut to what the org allows now
|
|
148
|
+
const grants = runId !== undefined
|
|
149
|
+
? grantsWithin(await this.grants.forExec({ orgId, runId, deviceId: h.id }), limits)
|
|
150
|
+
: undefined;
|
|
110
151
|
signal.throwIfAborted();
|
|
152
|
+
let result;
|
|
111
153
|
try {
|
|
112
|
-
|
|
154
|
+
result = await this.relay.call(h.id, "exec", { ...rest, limits, ...(grants !== undefined && { grants }) }, { deadlineMs: args.timeoutMs + EXEC_DEADLINE_MARGIN_MS, signal });
|
|
113
155
|
}
|
|
114
156
|
catch (e) {
|
|
115
157
|
// the contract: an exec aborted after it was sent resolves with exitCode null (the relay
|
|
@@ -117,8 +159,19 @@ let LocalDeviceProvider = class LocalDeviceProvider {
|
|
|
117
159
|
if (signal.aborted && e === signal.reason) {
|
|
118
160
|
return { exitCode: null, stdout: "", stderr: "", truncated: false, timedOut: false };
|
|
119
161
|
}
|
|
162
|
+
// the machine refused to run it unsandboxed (its local policy changed since it reported)
|
|
163
|
+
if (e instanceof ToolError && e.message === MACHINE_UNSANDBOXED_REFUSAL) {
|
|
164
|
+
this.log.warn("the machine refused a command without a sandbox", {
|
|
165
|
+
deviceId: h.id,
|
|
166
|
+
reason: cap !== undefined ? "unattended" : "not_allowed",
|
|
167
|
+
});
|
|
168
|
+
throw await this.unsandboxedRefusal(h.id, cap);
|
|
169
|
+
}
|
|
120
170
|
throw e;
|
|
121
171
|
}
|
|
172
|
+
if (result.network?.blocked === undefined)
|
|
173
|
+
return result;
|
|
174
|
+
return { ...result, network: { blocked: relayer(result.network.blocked, limits) } };
|
|
122
175
|
}
|
|
123
176
|
async readFile(h, path, opts) {
|
|
124
177
|
// the answer must fit one frame: a larger file comes back truncated (Deliver: too large)
|
|
@@ -133,12 +186,59 @@ let LocalDeviceProvider = class LocalDeviceProvider {
|
|
|
133
186
|
async stat(h, path, signal) {
|
|
134
187
|
return this.relay.call(h.id, "stat", { path }, { deadlineMs: OP_DEADLINE_MS, signal });
|
|
135
188
|
}
|
|
136
|
-
/**
|
|
137
|
-
|
|
189
|
+
/**
|
|
190
|
+
* The device's org and the limits of one exec now (ruling R27): the ceiling, capped at `dev` for
|
|
191
|
+
* an unattended run without the `tier:open` grant (`cap`, set only when the platform and the org
|
|
192
|
+
* would otherwise allow an unsandboxed command); `allowUnsandboxed` whether this command may run
|
|
193
|
+
* without a sandbox. A deleted device is not found. Refuses an unsandboxed machine the org does
|
|
194
|
+
* not allow (or such a run), see {@link exec}.
|
|
195
|
+
*/
|
|
196
|
+
async currentLimits(deviceId, runId) {
|
|
138
197
|
const orgId = await this.targets.orgOf(deviceId);
|
|
139
198
|
if (orgId === null)
|
|
140
199
|
throw deviceNotFound();
|
|
141
|
-
|
|
200
|
+
const [policy, unattended, unsandboxed] = await Promise.all([
|
|
201
|
+
this.sandbox.devicePolicy({ orgId, deviceId }),
|
|
202
|
+
runId === undefined ? null : this.grants.unattendedTier({ runId, deviceId }),
|
|
203
|
+
this.grants.unsandboxed(deviceId),
|
|
204
|
+
]);
|
|
205
|
+
const capped = unattended !== null && !unattended.openGranted;
|
|
206
|
+
// what an unsandboxed refusal reports: the run's missing grant only when it alone refused
|
|
207
|
+
const cap = capped && policy.allowUnsandboxed ? { taskRunId: unattended.taskRunId } : undefined;
|
|
208
|
+
if (unsandboxed && (!policy.allowUnsandboxed || capped)) {
|
|
209
|
+
this.log.warn("command refused on a device that runs without a sandbox", {
|
|
210
|
+
deviceId,
|
|
211
|
+
reason: policy.allowUnsandboxed ? "unattended" : "not_allowed",
|
|
212
|
+
});
|
|
213
|
+
throw await this.unsandboxedRefusal(deviceId, cap);
|
|
214
|
+
}
|
|
215
|
+
const tier = capped ? minTier(policy.ceiling, "dev") : policy.ceiling;
|
|
216
|
+
const limits = await this.sandbox.deviceLimits({ orgId, deviceId }, { policy, tier });
|
|
217
|
+
return {
|
|
218
|
+
orgId,
|
|
219
|
+
limits: {
|
|
220
|
+
...limits,
|
|
221
|
+
defaultTier: minTier(policy.defaultTier, tier),
|
|
222
|
+
// the platform and the org allow it (which needs an open ceiling) and no cap applies
|
|
223
|
+
allowUnsandboxed: policy.allowUnsandboxed && !capped,
|
|
224
|
+
},
|
|
225
|
+
cap,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The `device.unsandboxed_not_allowed` error (spec §4.3); for an unattended run refused only for
|
|
230
|
+
* its missing `tier:open` grant, the run's text, with the missing grant recorded on the task run.
|
|
231
|
+
*/
|
|
232
|
+
async unsandboxedRefusal(deviceId, cap) {
|
|
233
|
+
if (cap === undefined) {
|
|
234
|
+
return new ApiError("device.unsandboxed_not_allowed", UNSANDBOXED_NOT_ALLOWED_ERROR);
|
|
235
|
+
}
|
|
236
|
+
await this.grants
|
|
237
|
+
.recordMissing(cap.taskRunId, TIER_OPEN_GRANT_KEY)
|
|
238
|
+
.catch((err) => this.log.warn("cannot record a missing task grant", { deviceId, err }));
|
|
239
|
+
return new ApiError("device.unsandboxed_not_allowed", UNATTENDED_UNSANDBOXED_ERROR, [
|
|
240
|
+
{ reason: "unattended" },
|
|
241
|
+
]);
|
|
142
242
|
}
|
|
143
243
|
};
|
|
144
244
|
LocalDeviceProvider = __decorate([
|
|
@@ -146,6 +246,8 @@ LocalDeviceProvider = __decorate([
|
|
|
146
246
|
__param(0, Inject(LocalDeviceRelay)),
|
|
147
247
|
__param(1, Inject(DeviceTargets)),
|
|
148
248
|
__param(2, Inject(SandboxService)),
|
|
149
|
-
__param(3, Inject(KernelConfigService))
|
|
249
|
+
__param(3, Inject(KernelConfigService)),
|
|
250
|
+
__param(4, Inject(NetworkGrants)),
|
|
251
|
+
__param(5, Inject(KERNEL_LOGGER))
|
|
150
252
|
], LocalDeviceProvider);
|
|
151
253
|
export { LocalDeviceProvider };
|
|
@@ -4,7 +4,8 @@ import { LocalDeviceProvider } from "./local-device-provider.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* Connected machines (environments spec §6, §13): the caller side of the device relay and the
|
|
6
6
|
* kernel-internal `@local` sandbox provider, registered with {@link SandboxProviders} on init, and
|
|
7
|
-
* the machines and their credentials ({@link MachinesService})
|
|
7
|
+
* the machines and their credentials ({@link MachinesService}), changes to a device's network
|
|
8
|
+
* policy ({@link DevicePolicyService}) and the run grants sent with each exec ({@link NetworkGrants}).
|
|
8
9
|
* Imported on every role: any replica may run a sandbox operation on a local device
|
|
9
10
|
* (the socket itself is held by the gateway role).
|
|
10
11
|
*/
|
|
@@ -10,14 +10,17 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
10
10
|
import { Inject, Module } from "@nestjs/common";
|
|
11
11
|
import { LOCAL_PROVIDER, SandboxProviders } from "../sandbox/providers.js";
|
|
12
12
|
import { SandboxModule } from "../sandbox/sandbox.module.js";
|
|
13
|
+
import { DevicePolicyService } from "./device-policy.service.js";
|
|
13
14
|
import { LocalDeviceProvider } from "./local-device-provider.js";
|
|
14
15
|
import { MachinesService } from "./machines.js";
|
|
16
|
+
import { NetworkGrants } from "./network-grants.js";
|
|
15
17
|
import { LocalDeviceRelay } from "./relay.js";
|
|
16
18
|
import { DeviceTargets } from "./targets.js";
|
|
17
19
|
/**
|
|
18
20
|
* Connected machines (environments spec §6, §13): the caller side of the device relay and the
|
|
19
21
|
* kernel-internal `@local` sandbox provider, registered with {@link SandboxProviders} on init, and
|
|
20
|
-
* the machines and their credentials ({@link MachinesService})
|
|
22
|
+
* the machines and their credentials ({@link MachinesService}), changes to a device's network
|
|
23
|
+
* policy ({@link DevicePolicyService}) and the run grants sent with each exec ({@link NetworkGrants}).
|
|
21
24
|
* Imported on every role: any replica may run a sandbox operation on a local device
|
|
22
25
|
* (the socket itself is held by the gateway role).
|
|
23
26
|
*/
|
|
@@ -35,8 +38,21 @@ let LocalDevicesModule = class LocalDevicesModule {
|
|
|
35
38
|
LocalDevicesModule = __decorate([
|
|
36
39
|
Module({
|
|
37
40
|
imports: [SandboxModule],
|
|
38
|
-
providers: [
|
|
39
|
-
|
|
41
|
+
providers: [
|
|
42
|
+
DeviceTargets,
|
|
43
|
+
LocalDeviceRelay,
|
|
44
|
+
LocalDeviceProvider,
|
|
45
|
+
MachinesService,
|
|
46
|
+
DevicePolicyService,
|
|
47
|
+
NetworkGrants,
|
|
48
|
+
],
|
|
49
|
+
exports: [
|
|
50
|
+
LocalDeviceRelay,
|
|
51
|
+
LocalDeviceProvider,
|
|
52
|
+
MachinesService,
|
|
53
|
+
DevicePolicyService,
|
|
54
|
+
NetworkGrants,
|
|
55
|
+
],
|
|
40
56
|
}),
|
|
41
57
|
__param(0, Inject(SandboxProviders)),
|
|
42
58
|
__param(1, Inject(LocalDeviceProvider))
|