@capybara-hq/kernel 0.1.0-rc.21 → 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/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/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/admin-catalog.controller.d.ts +2 -4
- package/dist/catalog/admin-catalog.controller.js +2 -6
- package/dist/catalog/catalog-content.d.ts +5 -0
- package/dist/catalog/catalog-content.js +1 -0
- 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 +107 -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 +347 -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 +50 -5
- package/dist/errors.js +50 -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/package.js +2 -2
- package/dist/http/api.module.js +5 -0
- package/dist/identity/settings.d.ts +53 -3
- package/dist/identity/settings.js +92 -9
- 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.d.ts +7 -0
- package/dist/infra/config/kernel-config.service.js +31 -0
- package/dist/infra/events/kernel-events.d.ts +2 -1
- 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.d.ts +4 -2
- package/dist/local-devices/local-device-gateway.js +39 -11
- 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 +30 -3
- package/dist/local-devices/machines.js +79 -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 +4 -3
- package/dist/orgs/settings.service.d.ts +172 -14
- package/dist/orgs/settings.service.js +229 -27
- 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/admin.ops.js +32 -7
- 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 +15 -6
- package/dist/platform/ops/orgs.ops.js +1 -0
- 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/platform-tiers.d.ts +86 -0
- package/dist/sandbox/platform-tiers.js +97 -0
- package/dist/sandbox/sandbox.service.d.ts +121 -11
- package/dist/sandbox/sandbox.service.js +351 -30
- package/dist/sandbox/tiers.d.ts +98 -0
- package/dist/sandbox/tiers.js +104 -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
|
@@ -8,6 +8,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
8
8
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
9
|
};
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { minTier, tierAtMost, } from "@capybara-hq/plugin-sdk";
|
|
11
12
|
import { Inject, Injectable } from "@nestjs/common";
|
|
12
13
|
import { sql } from "kysely";
|
|
13
14
|
import { ApiError } from "../errors.js";
|
|
@@ -20,12 +21,19 @@ import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
|
20
21
|
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
21
22
|
import { LockService } from "../infra/lock/lock.service.js";
|
|
22
23
|
import { KERNEL_LOGGER } from "../infra/tokens.js";
|
|
24
|
+
import { DevicePolicyService } from "../local-devices/device-policy.service.js";
|
|
23
25
|
import { deviceLeaseKey } from "../local-devices/keys.js";
|
|
24
26
|
import { MachinesService } from "../local-devices/machines.js";
|
|
27
|
+
import { deviceFeaturesOf, localPolicyOf } from "../local-devices/policy-summary.js";
|
|
28
|
+
import { readOrgSettings } from "../orgs/settings.service.js";
|
|
25
29
|
import { SandboxJobs } from "../sandbox/jobs.js";
|
|
26
30
|
import { isInternalProvider, LOCAL_PROVIDER, SandboxProviders } from "../sandbox/providers.js";
|
|
27
31
|
import { deviceNotFound, SandboxService } from "../sandbox/sandbox.service.js";
|
|
32
|
+
import { effectiveSandboxPolicy, readDeploymentSandboxPolicy, } from "../sandbox/tiers.js";
|
|
33
|
+
import { unattendedTierOf } from "../schedules/unattended.js";
|
|
28
34
|
import { DeviceAccess, demotedProvider, deviceProvider, deviceUnavailable, inUseForDefault, } from "./access.js";
|
|
35
|
+
/** How many recent sandbox changes a device view lists. */
|
|
36
|
+
export const RECENT_POLICY_CHANGES = 10;
|
|
29
37
|
const isUniqueViolation = (e, constraint) => typeof e === "object" &&
|
|
30
38
|
e !== null &&
|
|
31
39
|
e.code === "23505" &&
|
|
@@ -50,9 +58,10 @@ let DevicesService = class DevicesService {
|
|
|
50
58
|
log;
|
|
51
59
|
locks;
|
|
52
60
|
machines;
|
|
61
|
+
policies;
|
|
53
62
|
constructor(db, txHost, config, providers, sandbox, sandboxJobs, access, displayNames, log,
|
|
54
63
|
/** Reads the device relay's lease (`online`). */
|
|
55
|
-
locks, machines) {
|
|
64
|
+
locks, machines, policies) {
|
|
56
65
|
this.db = db;
|
|
57
66
|
this.txHost = txHost;
|
|
58
67
|
this.config = config;
|
|
@@ -64,6 +73,7 @@ let DevicesService = class DevicesService {
|
|
|
64
73
|
this.log = log;
|
|
65
74
|
this.locks = locks;
|
|
66
75
|
this.machines = machines;
|
|
76
|
+
this.policies = policies;
|
|
67
77
|
}
|
|
68
78
|
/**
|
|
69
79
|
* The person's live devices in the org, the default first, then by creation. With a
|
|
@@ -236,6 +246,120 @@ let DevicesService = class DevicesService {
|
|
|
236
246
|
}
|
|
237
247
|
async renameIn(orgId, accountId, id, name, actorTokenId, auditDetail) {
|
|
238
248
|
const before = await this.lockOwn(orgId, accountId, id);
|
|
249
|
+
await this.renameLocked(orgId, accountId, id, before, name, actorTokenId, auditDetail);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* `PATCH /devices/:id` (sandbox dev tiers spec §7.2): renames one of the person's live devices
|
|
253
|
+
* ({@link rename}) and sets a cloud device's sandbox tier, in one transaction. The tier: null
|
|
254
|
+
* follows the org's default; above the device's ceiling (the org's or the platform's max) →
|
|
255
|
+
* `device.tier_not_allowed`; a local device → `device.tier_on_device` (its tier is its
|
|
256
|
+
* machine's, changed with {@link changePolicy}). A changed tier is audited `device.tier_changed`
|
|
257
|
+
* (`from` / `to`); the device's next use applies it (the instance is updated, or recreated on
|
|
258
|
+
* the kept files). Callers make sure only a person in a session sets the tier.
|
|
259
|
+
*/
|
|
260
|
+
async update(orgId, accountId, id, patch, actorTokenId) {
|
|
261
|
+
await this.updateIn(orgId, accountId, id, patch, actorTokenId);
|
|
262
|
+
return this.get(orgId, accountId, id);
|
|
263
|
+
}
|
|
264
|
+
async updateIn(orgId, accountId, id, patch, actorTokenId) {
|
|
265
|
+
const before = await this.lockOwn(orgId, accountId, id);
|
|
266
|
+
if (patch.tier !== undefined) {
|
|
267
|
+
if (before.source !== "cloud") {
|
|
268
|
+
throw new ApiError("device.tier_on_device", "This device's sandbox level is kept on its connected machine: change it from the device page (sent to the machine) or run capybara-device policy set tier <strict|dev|open|default> there.");
|
|
269
|
+
}
|
|
270
|
+
const policy = await this.policyFor(orgId, patch.tier);
|
|
271
|
+
if (patch.tier !== null && !tierAtMost(patch.tier, policy.ceiling)) {
|
|
272
|
+
throw tierNotAllowed(patch.tier, policy);
|
|
273
|
+
}
|
|
274
|
+
if (patch.tier !== before.tier) {
|
|
275
|
+
await this.db
|
|
276
|
+
.updateTable("devices")
|
|
277
|
+
.set({ tier: patch.tier, updated_at: sql `now()` })
|
|
278
|
+
.where("id", "=", id)
|
|
279
|
+
.execute();
|
|
280
|
+
await audit(this.db.db, {
|
|
281
|
+
actorAccountId: accountId,
|
|
282
|
+
...(actorTokenId !== undefined && { actorTokenId }),
|
|
283
|
+
orgId,
|
|
284
|
+
action: "device.tier_changed",
|
|
285
|
+
target: id,
|
|
286
|
+
detail: { from: before.tier, to: patch.tier },
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (patch.name !== undefined) {
|
|
291
|
+
await this.renameLocked(orgId, accountId, id, before, patch.name, actorTokenId, undefined);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/** The org's sandbox policy for a device choosing `tier` (null: the org's default). */
|
|
295
|
+
async policyFor(orgId, tier) {
|
|
296
|
+
const [deployment, settings] = await Promise.all([
|
|
297
|
+
readDeploymentSandboxPolicy(this.db.db, this.config),
|
|
298
|
+
readOrgSettings(this.db.db, orgId),
|
|
299
|
+
]);
|
|
300
|
+
return effectiveSandboxPolicy(deployment, settings, tier);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* The sandbox level a sandbox call in the device runs at now, for the model (`device_paths`,
|
|
304
|
+
* spec §11.5): see {@link sandboxStateOf}, plus
|
|
305
|
+
* - `network: "none"` when the org's or the platform's sandbox network is off;
|
|
306
|
+
* - a cloud device whose new limits wait for a file reset: the tier and network its instance
|
|
307
|
+
* still runs with (`limitsState(...).applied`; open with its network on when unrecorded);
|
|
308
|
+
* - a local device: its machine's localhost and unix socket facts; in a scheduled run without
|
|
309
|
+
* a person (`runId`, {@link unattendedTierOf}) and without the task's `tier:open` grant its
|
|
310
|
+
* tier is capped at `dev`, as the exec args cap it;
|
|
311
|
+
* - `unattended` in such a run: nobody can approve a domain.
|
|
312
|
+
* Null when the device is not a live device of the org.
|
|
313
|
+
*/
|
|
314
|
+
async sandboxLevel(orgId, deviceId, runId) {
|
|
315
|
+
const row = await this.db
|
|
316
|
+
.selectFrom("devices as e")
|
|
317
|
+
.leftJoin("device_machines as m", (j) => j.onRef("m.device_id", "=", "e.id").on("m.revoked_at", "is", null))
|
|
318
|
+
.select(["e.source", "e.tier", "m.features", "m.local_policy"])
|
|
319
|
+
.where("e.id", "=", deviceId)
|
|
320
|
+
.where("e.org_id", "=", orgId)
|
|
321
|
+
.where("e.deleted_at", "is", null)
|
|
322
|
+
.executeTakeFirst();
|
|
323
|
+
if (row === undefined)
|
|
324
|
+
return null;
|
|
325
|
+
const [deployment, settings, unattended] = await Promise.all([
|
|
326
|
+
readDeploymentSandboxPolicy(this.db.db, this.config),
|
|
327
|
+
readOrgSettings(this.db.db, orgId),
|
|
328
|
+
runId === undefined ? null : unattendedTierOf(this.db.db, runId, deviceId),
|
|
329
|
+
]);
|
|
330
|
+
const policyOf = (tier) => effectiveSandboxPolicy(deployment, settings, tier);
|
|
331
|
+
const summary = localPolicyOf(row.local_policy);
|
|
332
|
+
const s = sandboxStateOf(policyOf, {
|
|
333
|
+
source: row.source,
|
|
334
|
+
tier: row.tier,
|
|
335
|
+
features: deviceFeaturesOf(row.features),
|
|
336
|
+
policy: summary,
|
|
337
|
+
});
|
|
338
|
+
let tier = s.effectiveTier;
|
|
339
|
+
let networkOff = policyOf(s.tier).base.egress === "none";
|
|
340
|
+
if (row.source === "cloud") {
|
|
341
|
+
const state = await this.sandbox.limitsState({ orgId, deviceId }, { policy: policyOf(row.tier) });
|
|
342
|
+
if (state.pendingFilesReset) {
|
|
343
|
+
// an unknown record (an instance made before records) may be wider: it counts as open
|
|
344
|
+
// with its network on, as the unattended gate counts it (`mayRunOpen`)
|
|
345
|
+
tier = state.applied?.tier ?? "open";
|
|
346
|
+
networkOff = state.applied?.egress === "none";
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
else if (unattended !== null && !unattended.openGranted) {
|
|
350
|
+
tier = minTier(tier, "dev");
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
tier,
|
|
354
|
+
isolation: tier === "open" ? s.isolation : "sandbox",
|
|
355
|
+
...(networkOff && { network: "none" }),
|
|
356
|
+
...(row.source === "local" &&
|
|
357
|
+
summary !== null && { localhost: summary.localhost, unixSockets: summary.unixSockets }),
|
|
358
|
+
...(unattended !== null && { unattended: true }),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/** Renames the device locked by {@link lockOwn} (`before`); see {@link rename}. */
|
|
362
|
+
async renameLocked(orgId, accountId, id, before, name, actorTokenId, auditDetail) {
|
|
239
363
|
if (before.name === name)
|
|
240
364
|
return;
|
|
241
365
|
// the same name in another case is this device's own: the index allows it
|
|
@@ -310,6 +434,55 @@ let DevicesService = class DevicesService {
|
|
|
310
434
|
async revokeMachine(orgId, accountId, id, actorTokenId) {
|
|
311
435
|
await this.machines.revoke(orgId, accountId, id, actorTokenId);
|
|
312
436
|
}
|
|
437
|
+
/**
|
|
438
|
+
* The owner changes one of their own devices' local policy (network ask-to-allow spec §7, §13
|
|
439
|
+
* Q6; sandbox dev tiers spec §7.3): sent to the machine with the `policy` op, which is the
|
|
440
|
+
* authority and answers with the policy as written. Anyone but the owner gets
|
|
441
|
+
* `device.not_found`. A `tier` above the device's ceiling → `device.tier_not_allowed`. A widening
|
|
442
|
+
* change (`add`; a `tier` that may raise the machine's, judged as the machine judges it) is
|
|
443
|
+
* refused up front with `device.policy_refused` when the machine last reported
|
|
444
|
+
* `remoteGrants: "off"` (otherwise the machine decides); narrowing is always sent, and
|
|
445
|
+
* `isolation` is only ever `sandbox`. The machine offline → `device.offline`; a cloud device or a
|
|
446
|
+
* program without the `policyOp` feature → `device.policy_unsupported`; the machine's refusal →
|
|
447
|
+
* `device.policy_refused` with its message. Callers are human sessions only (no personal access
|
|
448
|
+
* token, no service token, no Platform op).
|
|
449
|
+
*/
|
|
450
|
+
async changePolicy(orgId, account, id, change) {
|
|
451
|
+
await this.access.usable(orgId, account.id, id);
|
|
452
|
+
if (change.tier !== undefined && change.tier !== null) {
|
|
453
|
+
const policy = await this.policyFor(orgId, change.tier);
|
|
454
|
+
if (!tierAtMost(change.tier, policy.ceiling))
|
|
455
|
+
throw tierNotAllowed(change.tier, policy);
|
|
456
|
+
}
|
|
457
|
+
const summary = await this.policies.summary(id);
|
|
458
|
+
const known = summary?.policy ?? null;
|
|
459
|
+
const widens = (change.add?.length ?? 0) > 0 || (known !== null && tierWidens(known.tier, change.tier));
|
|
460
|
+
// only a known "off" is refused here; an unknown summary, a machine that cannot take the op
|
|
461
|
+
// or none at all are left to apply (the machine decides, or unsupported / offline)
|
|
462
|
+
const canApply = summary?.features.includes("policyOp") === true;
|
|
463
|
+
if (widens && canApply && known?.remoteGrants === "off") {
|
|
464
|
+
throw new ApiError("device.policy_refused", remoteGrantsOffMessage(change));
|
|
465
|
+
}
|
|
466
|
+
const out = await this.policies.apply({
|
|
467
|
+
orgId,
|
|
468
|
+
deviceId: id,
|
|
469
|
+
change: {
|
|
470
|
+
...(change.add !== undefined && { add: change.add }),
|
|
471
|
+
...(change.remove !== undefined && { remove: change.remove }),
|
|
472
|
+
...(change.tier !== undefined && { tier: change.tier }),
|
|
473
|
+
...(change.isolation !== undefined && { isolation: change.isolation }),
|
|
474
|
+
},
|
|
475
|
+
source: { accountId: account.id, accountName: account.displayName, via: "web" },
|
|
476
|
+
});
|
|
477
|
+
if (out.applied)
|
|
478
|
+
return out.policy;
|
|
479
|
+
if (out.reason === "offline")
|
|
480
|
+
throw new ApiError("device.offline", out.message);
|
|
481
|
+
if (out.reason === "unsupported") {
|
|
482
|
+
throw new ApiError("device.policy_unsupported", out.message);
|
|
483
|
+
}
|
|
484
|
+
throw new ApiError("device.policy_refused", out.message);
|
|
485
|
+
}
|
|
313
486
|
/**
|
|
314
487
|
* Starts one of the person's devices over with empty files (after
|
|
315
488
|
* `device.files_lost`): records the request (audit), then, after the commit, enqueues the
|
|
@@ -590,6 +763,7 @@ let DevicesService = class DevicesService {
|
|
|
590
763
|
"e.source",
|
|
591
764
|
"e.provider",
|
|
592
765
|
"e.demoted_from_provider",
|
|
766
|
+
"e.tier",
|
|
593
767
|
"e.created_at",
|
|
594
768
|
"m.id as machine_id",
|
|
595
769
|
"m.name as machine_name",
|
|
@@ -598,6 +772,8 @@ let DevicesService = class DevicesService {
|
|
|
598
772
|
"m.program_version as machine_program_version",
|
|
599
773
|
"m.last_seen_at as machine_last_seen_at",
|
|
600
774
|
"m.session_isolation as machine_session_isolation",
|
|
775
|
+
"m.features as machine_features",
|
|
776
|
+
"m.local_policy as machine_local_policy",
|
|
601
777
|
"i.provider as instance_provider",
|
|
602
778
|
"i.fs_status",
|
|
603
779
|
"i.handle",
|
|
@@ -636,10 +812,24 @@ let DevicesService = class DevicesService {
|
|
|
636
812
|
const demotedProviders = [
|
|
637
813
|
...new Set(rows.flatMap((r) => (demoted.get(r.id) ? [demoted.get(r.id)] : []))),
|
|
638
814
|
];
|
|
639
|
-
const [
|
|
640
|
-
this.sandbox.orgLimits(orgId),
|
|
815
|
+
const [names, recent, settings, deployment] = await Promise.all([
|
|
641
816
|
this.displayNames.many(demotedProviders),
|
|
817
|
+
this.recentSandboxChanges(orgId, rows.map((r) => r.id)),
|
|
818
|
+
readOrgSettings(this.db.db, orgId),
|
|
819
|
+
readDeploymentSandboxPolicy(this.db.db, this.config),
|
|
642
820
|
]);
|
|
821
|
+
const policyOf = (tier) => effectiveSandboxPolicy(deployment, settings, tier);
|
|
822
|
+
// each device's limits (a cloud device's own tier) and its instance's pending change
|
|
823
|
+
const perDevice = new Map(await Promise.all(rows.map(async (r) => {
|
|
824
|
+
const policy = policyOf(r.source === "cloud" ? r.tier : null);
|
|
825
|
+
const [limits, state] = await Promise.all([
|
|
826
|
+
this.sandbox.deviceLimits({ orgId, deviceId: r.id }, { policy }),
|
|
827
|
+
r.source === "cloud"
|
|
828
|
+
? this.sandbox.limitsState({ orgId, deviceId: r.id }, { policy })
|
|
829
|
+
: { pendingNextUse: false, pendingFilesReset: false },
|
|
830
|
+
]);
|
|
831
|
+
return [r.id, { limits, state }];
|
|
832
|
+
})));
|
|
643
833
|
const current = this.providers.cloud()?.plugin;
|
|
644
834
|
// a local device is online while a replica holds its machine's connection lease
|
|
645
835
|
const online = new Map(await Promise.all(rows
|
|
@@ -677,6 +867,23 @@ let DevicesService = class DevicesService {
|
|
|
677
867
|
lastSeenAt: r.machine_last_seen_at?.toISOString() ?? null,
|
|
678
868
|
}
|
|
679
869
|
: null,
|
|
870
|
+
network: r.source === "local" && r.machine_id !== null
|
|
871
|
+
? {
|
|
872
|
+
features: deviceFeaturesOf(r.machine_features),
|
|
873
|
+
policy: localPolicyOf(r.machine_local_policy),
|
|
874
|
+
}
|
|
875
|
+
: null,
|
|
876
|
+
sandbox: {
|
|
877
|
+
...sandboxStateOf(policyOf, {
|
|
878
|
+
source: r.source,
|
|
879
|
+
tier: r.tier,
|
|
880
|
+
features: r.machine_id === null ? [] : deviceFeaturesOf(r.machine_features),
|
|
881
|
+
policy: r.machine_id === null ? null : localPolicyOf(r.machine_local_policy),
|
|
882
|
+
}),
|
|
883
|
+
pendingNextUse: perDevice.get(r.id)?.state.pendingNextUse ?? false,
|
|
884
|
+
pendingFilesReset: perDevice.get(r.id)?.state.pendingFilesReset ?? false,
|
|
885
|
+
recent: recent.get(r.id) ?? [],
|
|
886
|
+
},
|
|
680
887
|
// a local device's machine decides it; cloud devices have none in phase 2
|
|
681
888
|
sessionIsolation: r.machine_session_isolation ?? false,
|
|
682
889
|
filesLost: r.fs_status === "lost",
|
|
@@ -686,11 +893,49 @@ let DevicesService = class DevicesService {
|
|
|
686
893
|
lastUsedAt: r.last_used_at.toISOString(),
|
|
687
894
|
}
|
|
688
895
|
: null,
|
|
689
|
-
limits:
|
|
896
|
+
limits: limitsView(perDevice.get(r.id)?.limits),
|
|
690
897
|
createdAt: r.created_at.toISOString(),
|
|
691
898
|
};
|
|
692
899
|
});
|
|
693
900
|
}
|
|
901
|
+
/**
|
|
902
|
+
* The last {@link RECENT_POLICY_CHANGES} sandbox changes of each device in the org (audit rows
|
|
903
|
+
* `device.policy_changed`, `device.tier_changed`, `device.isolation_reported`), newest first,
|
|
904
|
+
* with the deciding account's display name.
|
|
905
|
+
*/
|
|
906
|
+
async recentSandboxChanges(orgId, deviceIds) {
|
|
907
|
+
const out = new Map();
|
|
908
|
+
if (deviceIds.length === 0)
|
|
909
|
+
return out;
|
|
910
|
+
const ranked = this.db
|
|
911
|
+
.selectFrom("audit_events as a")
|
|
912
|
+
.select([
|
|
913
|
+
"a.target",
|
|
914
|
+
"a.action",
|
|
915
|
+
"a.at",
|
|
916
|
+
"a.detail",
|
|
917
|
+
"a.actor_account_id",
|
|
918
|
+
sql `row_number() OVER (PARTITION BY a.target ORDER BY a.at DESC, a.id DESC)`.as("n"),
|
|
919
|
+
])
|
|
920
|
+
.where("a.org_id", "=", orgId)
|
|
921
|
+
.where("a.action", "in", [...SANDBOX_CHANGE_ACTIONS])
|
|
922
|
+
.where("a.target", "in", deviceIds);
|
|
923
|
+
const rows = await this.db
|
|
924
|
+
.selectFrom(ranked.as("r"))
|
|
925
|
+
.leftJoin("accounts as ac", "ac.id", "r.actor_account_id")
|
|
926
|
+
.select(["r.target", "r.action", "r.at", "r.detail", "ac.display_name"])
|
|
927
|
+
.where("r.n", "<=", RECENT_POLICY_CHANGES)
|
|
928
|
+
.orderBy("r.at", "desc")
|
|
929
|
+
.execute();
|
|
930
|
+
for (const r of rows) {
|
|
931
|
+
if (r.target === null)
|
|
932
|
+
continue;
|
|
933
|
+
const list = out.get(r.target) ?? [];
|
|
934
|
+
list.push(sandboxChangeOf(r.action, r.at, (r.detail ?? {}), r.display_name ?? null));
|
|
935
|
+
out.set(r.target, list);
|
|
936
|
+
}
|
|
937
|
+
return out;
|
|
938
|
+
}
|
|
694
939
|
/**
|
|
695
940
|
* Serializes the person's device writes in the org: the org row (`FOR SHARE`), then their
|
|
696
941
|
* membership row (`FOR NO KEY UPDATE`, or `FOR SHARE` with `mode` share: enough to keep the
|
|
@@ -722,7 +967,7 @@ let DevicesService = class DevicesService {
|
|
|
722
967
|
await this.lockMember(orgId, accountId);
|
|
723
968
|
const row = await this.db
|
|
724
969
|
.selectFrom("devices")
|
|
725
|
-
.select(["name", "is_default"])
|
|
970
|
+
.select(["name", "is_default", "source", "tier"])
|
|
726
971
|
.where("id", "=", id)
|
|
727
972
|
.where("org_id", "=", orgId)
|
|
728
973
|
.where("owner_kind", "=", "personal")
|
|
@@ -756,6 +1001,9 @@ __decorate([
|
|
|
756
1001
|
__decorate([
|
|
757
1002
|
Transactional()
|
|
758
1003
|
], DevicesService.prototype, "renameIn", null);
|
|
1004
|
+
__decorate([
|
|
1005
|
+
Transactional()
|
|
1006
|
+
], DevicesService.prototype, "updateIn", null);
|
|
759
1007
|
__decorate([
|
|
760
1008
|
Transactional()
|
|
761
1009
|
], DevicesService.prototype, "remove", null);
|
|
@@ -777,7 +1025,8 @@ DevicesService = __decorate([
|
|
|
777
1025
|
__param(7, Inject(ExtensionDisplayNames)),
|
|
778
1026
|
__param(8, Inject(KERNEL_LOGGER)),
|
|
779
1027
|
__param(9, Inject(LockService)),
|
|
780
|
-
__param(10, Inject(MachinesService))
|
|
1028
|
+
__param(10, Inject(MachinesService)),
|
|
1029
|
+
__param(11, Inject(DevicePolicyService))
|
|
781
1030
|
], DevicesService);
|
|
782
1031
|
export { DevicesService };
|
|
783
1032
|
/**
|
|
@@ -789,3 +1038,95 @@ function refuseDeviceReset(env) {
|
|
|
789
1038
|
throw new ApiError("device.files_on_device", "This device's files are in a directory on its connected machine and cannot be reset from here. To start over, run capybara-device logout, then capybara-device login into this device, on that machine.");
|
|
790
1039
|
}
|
|
791
1040
|
}
|
|
1041
|
+
/** A copy of `l` for a view (its lists not shared). */
|
|
1042
|
+
const limitsView = (l) => ({
|
|
1043
|
+
...l,
|
|
1044
|
+
allowHosts: [...l.allowHosts],
|
|
1045
|
+
devHosts: [...l.devHosts],
|
|
1046
|
+
});
|
|
1047
|
+
/** The audit actions a device's recent sandbox changes list. */
|
|
1048
|
+
const SANDBOX_CHANGE_ACTIONS = [
|
|
1049
|
+
"device.policy_changed",
|
|
1050
|
+
"device.tier_changed",
|
|
1051
|
+
"device.isolation_reported",
|
|
1052
|
+
];
|
|
1053
|
+
const tierOrNull = (v) => v === "strict" || v === "dev" || v === "open" ? v : null;
|
|
1054
|
+
/** One audit row of {@link SANDBOX_CHANGE_ACTIONS} as a {@link DeviceSandboxChangeView}. */
|
|
1055
|
+
function sandboxChangeOf(action, at, d, by) {
|
|
1056
|
+
const hosts = (v) => Array.isArray(v) ? v.filter((h) => typeof h === "string") : [];
|
|
1057
|
+
const isolation = d.isolation === "sandbox" || d.isolation === "none" ? d.isolation : null;
|
|
1058
|
+
const base = { at: at.toISOString(), add: [], remove: [], tier: null, isolation: null, by };
|
|
1059
|
+
if (action === "device.tier_changed") {
|
|
1060
|
+
return { ...base, kind: "tier", tier: { to: tierOrNull(d.to) }, via: "web", confirmed: true };
|
|
1061
|
+
}
|
|
1062
|
+
if (action === "device.isolation_reported") {
|
|
1063
|
+
return { ...base, kind: "isolation", isolation, via: "device", confirmed: true };
|
|
1064
|
+
}
|
|
1065
|
+
return {
|
|
1066
|
+
...base,
|
|
1067
|
+
kind: "policy",
|
|
1068
|
+
add: hosts(d.add),
|
|
1069
|
+
remove: hosts(d.remove),
|
|
1070
|
+
// the key is there only when the change set the tier (null: follow the org's default)
|
|
1071
|
+
tier: "tier" in d ? { to: tierOrNull(d.tier) } : null,
|
|
1072
|
+
isolation,
|
|
1073
|
+
via: d.via === "im" ? "im" : "web",
|
|
1074
|
+
confirmed: d.confirmed === true,
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* A device's sandbox level (spec §4.2, §4.4): its chosen tier (a cloud device's stored one, a
|
|
1079
|
+
* local device's reported one) under the org's policy (`policyOf`). A local device whose program
|
|
1080
|
+
* lacks the `tiers` feature runs `strict` (it ignores the tier the kernel sends); it runs without
|
|
1081
|
+
* a sandbox only when it reports `isolation: "none"` and its effective tier is `open` (the device
|
|
1082
|
+
* keeps the sandbox at any lower tier).
|
|
1083
|
+
*/
|
|
1084
|
+
export function sandboxStateOf(policyOf, d) {
|
|
1085
|
+
const chosen = d.source === "cloud" ? d.tier : (d.policy?.tier ?? null);
|
|
1086
|
+
const p = policyOf(chosen);
|
|
1087
|
+
const tiersKnown = d.source === "cloud" || d.features.includes("tiers");
|
|
1088
|
+
const effectiveTier = tiersKnown ? p.tier : "strict";
|
|
1089
|
+
return {
|
|
1090
|
+
tier: chosen,
|
|
1091
|
+
effectiveTier,
|
|
1092
|
+
ceiling: p.ceiling,
|
|
1093
|
+
cappedBy: tiersKnown ? p.cappedBy : null,
|
|
1094
|
+
defaultTier: p.defaultTier,
|
|
1095
|
+
allowUnsandboxed: p.allowUnsandboxed,
|
|
1096
|
+
isolation: d.source === "local" && d.policy?.isolation === "none" && effectiveTier === "open"
|
|
1097
|
+
? "none"
|
|
1098
|
+
: "sandbox",
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Whether setting a local device's tier to `next` (null: follow the org's default) may raise it,
|
|
1103
|
+
* not knowing the org's default at the machine: a tier above the reported one, any tier but
|
|
1104
|
+
* `strict` when it reports none, and following the default when it reports `strict` or `dev`.
|
|
1105
|
+
*/
|
|
1106
|
+
export function tierWidens(reported, next) {
|
|
1107
|
+
if (next === undefined || next === "strict")
|
|
1108
|
+
return false;
|
|
1109
|
+
if (next === null)
|
|
1110
|
+
return reported !== undefined && reported !== "open";
|
|
1111
|
+
return reported === undefined || !tierAtMost(next, reported);
|
|
1112
|
+
}
|
|
1113
|
+
/** A tier above the device's ceiling: which ceiling, and the highest tier allowed. */
|
|
1114
|
+
function tierNotAllowed(tier, policy) {
|
|
1115
|
+
const platform = policy.cappedBy === "platform";
|
|
1116
|
+
return new ApiError("device.tier_not_allowed", `${platform ? "The platform" : "Your organization"} allows sandbox levels up to ${policy.ceiling}; this device cannot be set to ${tier}.`);
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* The refusal of a widening change on a device whose machine reported `remoteGrants: "off"`: the
|
|
1120
|
+
* command to run on the machine instead.
|
|
1121
|
+
*/
|
|
1122
|
+
function remoteGrantsOffMessage(change) {
|
|
1123
|
+
const local = [
|
|
1124
|
+
...(change.add !== undefined && change.add.length > 0
|
|
1125
|
+
? [`capybara-device policy add allowedDomains ${change.add.join(",")}`]
|
|
1126
|
+
: []),
|
|
1127
|
+
...(change.tier !== undefined
|
|
1128
|
+
? [`capybara-device policy set tier ${change.tier ?? "default"}`]
|
|
1129
|
+
: []),
|
|
1130
|
+
];
|
|
1131
|
+
return `This device does not accept widening changes from the deployment; change it on the machine: ${local.join("; ")}`;
|
|
1132
|
+
}
|
package/dist/devices/dto.d.ts
CHANGED
|
@@ -7,19 +7,72 @@ export declare const DeviceNameBody: z.ZodObject<{
|
|
|
7
7
|
export declare const DeviceParams: z.ZodObject<{
|
|
8
8
|
id: z.ZodString;
|
|
9
9
|
}, z.core.$strip>;
|
|
10
|
+
/** A sandbox tier (sandbox dev tiers spec §5). */
|
|
11
|
+
export declare const DeviceTier: z.ZodEnum<{
|
|
12
|
+
dev: "dev";
|
|
13
|
+
open: "open";
|
|
14
|
+
strict: "strict";
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* `PATCH /devices/:id`: a new name, and for a cloud device its sandbox tier (null = follow the
|
|
18
|
+
* org's default; sandbox dev tiers spec §7.2). The tier is a person's choice in a web session:
|
|
19
|
+
* an API token gets `auth.session_required` and the Platform tool leaves the field out.
|
|
20
|
+
*/
|
|
21
|
+
export declare const DevicePatchBody: z.ZodObject<{
|
|
22
|
+
name: z.ZodOptional<z.ZodString>;
|
|
23
|
+
tier: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
24
|
+
dev: "dev";
|
|
25
|
+
open: "open";
|
|
26
|
+
strict: "strict";
|
|
27
|
+
}>>>;
|
|
28
|
+
}, z.core.$strict>;
|
|
29
|
+
/**
|
|
30
|
+
* `POST /devices/:id/policy` (network ask-to-allow spec §7, sandbox dev tiers spec §7.3): the
|
|
31
|
+
* device owner's change of the machine's local policy. `add` and a higher `tier` widen it (the
|
|
32
|
+
* machine accepts them only while its local `remoteGrants` is `owner`); `remove` and a lower
|
|
33
|
+
* `tier` narrow it. `tier: null` follows the org's default. `isolation` only turns the sandbox
|
|
34
|
+
* back on: running without one is decided on the machine.
|
|
35
|
+
*/
|
|
36
|
+
export declare const DevicePolicyBody: z.ZodObject<{
|
|
37
|
+
add: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
38
|
+
remove: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
39
|
+
tier: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
40
|
+
dev: "dev";
|
|
41
|
+
open: "open";
|
|
42
|
+
strict: "strict";
|
|
43
|
+
}>>>;
|
|
44
|
+
isolation: z.ZodOptional<z.ZodLiteral<"sandbox">>;
|
|
45
|
+
}, z.core.$strict>;
|
|
10
46
|
declare const DeviceCreateBodyDto_base: import("../infra/validation/create-zod-dto.js").ZodDtoClass<z.ZodObject<{
|
|
11
47
|
name: z.ZodString;
|
|
12
48
|
}, z.core.$strict>>;
|
|
13
49
|
export declare class DeviceCreateBodyDto extends DeviceCreateBodyDto_base {
|
|
14
50
|
}
|
|
15
|
-
declare const
|
|
16
|
-
name: z.ZodString
|
|
51
|
+
declare const DevicePatchBodyDto_base: import("../infra/validation/create-zod-dto.js").ZodDtoClass<z.ZodObject<{
|
|
52
|
+
name: z.ZodOptional<z.ZodString>;
|
|
53
|
+
tier: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
54
|
+
dev: "dev";
|
|
55
|
+
open: "open";
|
|
56
|
+
strict: "strict";
|
|
57
|
+
}>>>;
|
|
17
58
|
}, z.core.$strict>>;
|
|
18
|
-
export declare class
|
|
59
|
+
export declare class DevicePatchBodyDto extends DevicePatchBodyDto_base {
|
|
19
60
|
}
|
|
20
61
|
declare const DeviceParamsDto_base: import("../infra/validation/create-zod-dto.js").ZodDtoClass<z.ZodObject<{
|
|
21
62
|
id: z.ZodString;
|
|
22
63
|
}, z.core.$strip>>;
|
|
23
64
|
export declare class DeviceParamsDto extends DeviceParamsDto_base {
|
|
24
65
|
}
|
|
66
|
+
declare const DevicePolicyBodyDto_base: import("../infra/validation/create-zod-dto.js").ZodDtoClass<z.ZodObject<{
|
|
67
|
+
add: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
68
|
+
remove: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
69
|
+
tier: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
70
|
+
dev: "dev";
|
|
71
|
+
open: "open";
|
|
72
|
+
strict: "strict";
|
|
73
|
+
}>>>;
|
|
74
|
+
isolation: z.ZodOptional<z.ZodLiteral<"sandbox">>;
|
|
75
|
+
}, z.core.$strict>>;
|
|
76
|
+
export declare class DevicePolicyBodyDto extends DevicePolicyBodyDto_base {
|
|
77
|
+
}
|
|
25
78
|
export {};
|
package/dist/devices/dto.js
CHANGED
|
@@ -1,12 +1,62 @@
|
|
|
1
|
+
import { validateAllowHost } from "@capybara-hq/plugin-sdk";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { createZodDto } from "../infra/validation/create-zod-dto.js";
|
|
4
|
+
import { publicSuffixWildcard } from "./public-suffix.js";
|
|
3
5
|
/** A device's name: trimmed, 1-64 characters (unique per person, case-insensitive). */
|
|
4
6
|
export const DeviceName = z.string().trim().min(1).max(64);
|
|
5
7
|
export const DeviceNameBody = z.object({ name: DeviceName }).strict();
|
|
6
8
|
export const DeviceParams = z.object({ id: z.string().min(1).max(100) });
|
|
9
|
+
/** A sandbox tier (sandbox dev tiers spec §5). */
|
|
10
|
+
export const DeviceTier = z.enum(["strict", "dev", "open"]);
|
|
11
|
+
/**
|
|
12
|
+
* `PATCH /devices/:id`: a new name, and for a cloud device its sandbox tier (null = follow the
|
|
13
|
+
* org's default; sandbox dev tiers spec §7.2). The tier is a person's choice in a web session:
|
|
14
|
+
* an API token gets `auth.session_required` and the Platform tool leaves the field out.
|
|
15
|
+
*/
|
|
16
|
+
export const DevicePatchBody = z
|
|
17
|
+
.object({ name: DeviceName.optional(), tier: DeviceTier.nullable().optional() })
|
|
18
|
+
.strict()
|
|
19
|
+
.refine((b) => b.name !== undefined || b.tier !== undefined, {
|
|
20
|
+
message: "Give at least one of name or tier",
|
|
21
|
+
});
|
|
22
|
+
/** One host of a device's network allowlist (`validateAllowHost`: lowercase, `*.` allowed). */
|
|
23
|
+
const PolicyHost = z
|
|
24
|
+
.string()
|
|
25
|
+
.max(253)
|
|
26
|
+
.superRefine((h, ctx) => {
|
|
27
|
+
for (const message of validateAllowHost(h))
|
|
28
|
+
ctx.addIssue({ code: "custom", message });
|
|
29
|
+
});
|
|
30
|
+
/** A host to add: also no `*.` wildcard over a whole public suffix (`*.com`, `*.co.uk`). */
|
|
31
|
+
const AddHost = PolicyHost.superRefine((h, ctx) => {
|
|
32
|
+
const tooBroad = publicSuffixWildcard(h);
|
|
33
|
+
if (tooBroad !== null)
|
|
34
|
+
ctx.addIssue({ code: "custom", message: tooBroad });
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* `POST /devices/:id/policy` (network ask-to-allow spec §7, sandbox dev tiers spec §7.3): the
|
|
38
|
+
* device owner's change of the machine's local policy. `add` and a higher `tier` widen it (the
|
|
39
|
+
* machine accepts them only while its local `remoteGrants` is `owner`); `remove` and a lower
|
|
40
|
+
* `tier` narrow it. `tier: null` follows the org's default. `isolation` only turns the sandbox
|
|
41
|
+
* back on: running without one is decided on the machine.
|
|
42
|
+
*/
|
|
43
|
+
export const DevicePolicyBody = z
|
|
44
|
+
.object({
|
|
45
|
+
add: z.array(AddHost).min(1).max(20).optional(),
|
|
46
|
+
remove: z.array(PolicyHost).min(1).max(100).optional(),
|
|
47
|
+
tier: DeviceTier.nullable().optional(),
|
|
48
|
+
isolation: z.literal("sandbox").optional(),
|
|
49
|
+
})
|
|
50
|
+
.strict()
|
|
51
|
+
.refine((b) => b.add !== undefined ||
|
|
52
|
+
b.remove !== undefined ||
|
|
53
|
+
b.tier !== undefined ||
|
|
54
|
+
b.isolation !== undefined, { message: "Give at least one of add, remove, tier or isolation" });
|
|
7
55
|
export class DeviceCreateBodyDto extends createZodDto(DeviceNameBody, "DeviceCreateBody") {
|
|
8
56
|
}
|
|
9
|
-
export class
|
|
57
|
+
export class DevicePatchBodyDto extends createZodDto(DevicePatchBody, "DevicePatchBody") {
|
|
10
58
|
}
|
|
11
59
|
export class DeviceParamsDto extends createZodDto(DeviceParams, "DeviceParams") {
|
|
12
60
|
}
|
|
61
|
+
export class DevicePolicyBodyDto extends createZodDto(DevicePolicyBody, "DevicePolicyBody") {
|
|
62
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a `*.` wildcard is too broad to add from the device page: its remainder is a whole public
|
|
3
|
+
* suffix (one label such as `com`, or a known multi-label one such as `co.uk`, `github.io`);
|
|
4
|
+
* null for an exact host or a narrower wildcard.
|
|
5
|
+
*/
|
|
6
|
+
export declare function publicSuffixWildcard(pattern: string): string | null;
|