@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DeviceHello, DeviceStopArgs, ExtensionContext } from "@capybara-hq/plugin-sdk";
|
|
1
|
+
import type { DeviceFeature, DeviceHello, DeviceLocalPolicySummary, DeviceStopArgs, ExtensionContext } from "@capybara-hq/plugin-sdk";
|
|
2
2
|
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
3
3
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
4
4
|
import type { KernelLogger } from "../log.js";
|
|
@@ -23,6 +23,10 @@ export interface ActiveMachine {
|
|
|
23
23
|
programVersion: string | null;
|
|
24
24
|
lastSeenAt: Date | null;
|
|
25
25
|
createdAt: Date;
|
|
26
|
+
/** The features its program reported in its last hello (known ones only). */
|
|
27
|
+
features: DeviceFeature[];
|
|
28
|
+
/** Its local network policy as last reported (hello or `policy` op); null when unknown. */
|
|
29
|
+
localPolicy: DeviceLocalPolicySummary | null;
|
|
26
30
|
}
|
|
27
31
|
/**
|
|
28
32
|
* Connected machines and their credentials (environments spec §13.2). One active machine per
|
|
@@ -83,9 +87,16 @@ export declare class MachinesService {
|
|
|
83
87
|
* {@link issue} already counted the reset) but serves another directory or mode (other roots,
|
|
84
88
|
* ruling R23), the device's files are other files: `fs_resets` moves on (maintainer
|
|
85
89
|
* decision 2026-11-08), so conversations get the files-reset notice. A previous machine that
|
|
86
|
-
* never said hello has no known roots: nothing to compare.
|
|
90
|
+
* never said hello has no known roots: nothing to compare. The reported features (known ones
|
|
91
|
+
* only) and local policy summary (null when absent or malformed) replace the stored ones, so a
|
|
92
|
+
* program downgraded to one without them is no longer sent what it cannot do. A summary the
|
|
93
|
+
* kernel cannot read (`hello.policy` dropped by the frame parser) is kept as its isolation
|
|
94
|
+
* alone, read from `rawPolicy` (the frame's raw `policy`; {@link isolationOf}: anything but
|
|
95
|
+
* `sandbox` is `none`), so it never hides that commands run without a sandbox. A reported
|
|
96
|
+
* isolation that differs from the stored one (none stored counts as `sandbox`) is audited
|
|
97
|
+
* `device.isolation_reported` (sandbox dev tiers spec §10.4), once per change.
|
|
87
98
|
*/
|
|
88
|
-
recordHello(machineId: string, hello: DeviceHello): Promise<void>;
|
|
99
|
+
recordHello(machineId: string, hello: DeviceHello, rawPolicy?: unknown): Promise<void>;
|
|
89
100
|
/** Marks the machine seen (a ping); writes at most once every 60 seconds. */
|
|
90
101
|
touch(machineId: string): Promise<void>;
|
|
91
102
|
/**
|
|
@@ -20,6 +20,7 @@ import { KERNEL_BUS, KERNEL_LOGGER } from "../infra/tokens.js";
|
|
|
20
20
|
import { LOCAL_PROVIDER } from "../sandbox/providers.js";
|
|
21
21
|
import { deviceNotFound } from "../sandbox/sandbox.service.js";
|
|
22
22
|
import { deviceRevokedTopic } from "./keys.js";
|
|
23
|
+
import { deviceFeaturesOf, isolationOf, localPolicyOf } from "./policy-summary.js";
|
|
23
24
|
import { LocalDeviceRelay } from "./relay.js";
|
|
24
25
|
/** The machine credential's prefix (`cap_dev_` plus 32 random bytes, base64url). */
|
|
25
26
|
export const MACHINE_CREDENTIAL_PREFIX = "cap_dev_";
|
|
@@ -154,12 +155,29 @@ let MachinesService = class MachinesService {
|
|
|
154
155
|
* {@link issue} already counted the reset) but serves another directory or mode (other roots,
|
|
155
156
|
* ruling R23), the device's files are other files: `fs_resets` moves on (maintainer
|
|
156
157
|
* decision 2026-11-08), so conversations get the files-reset notice. A previous machine that
|
|
157
|
-
* never said hello has no known roots: nothing to compare.
|
|
158
|
+
* never said hello has no known roots: nothing to compare. The reported features (known ones
|
|
159
|
+
* only) and local policy summary (null when absent or malformed) replace the stored ones, so a
|
|
160
|
+
* program downgraded to one without them is no longer sent what it cannot do. A summary the
|
|
161
|
+
* kernel cannot read (`hello.policy` dropped by the frame parser) is kept as its isolation
|
|
162
|
+
* alone, read from `rawPolicy` (the frame's raw `policy`; {@link isolationOf}: anything but
|
|
163
|
+
* `sandbox` is `none`), so it never hides that commands run without a sandbox. A reported
|
|
164
|
+
* isolation that differs from the stored one (none stored counts as `sandbox`) is audited
|
|
165
|
+
* `device.isolation_reported` (sandbox dev tiers spec §10.4), once per change.
|
|
158
166
|
*/
|
|
159
|
-
async recordHello(machineId, hello) {
|
|
167
|
+
async recordHello(machineId, hello, rawPolicy = hello.policy) {
|
|
160
168
|
const self = await this.db
|
|
161
169
|
.selectFrom("device_machines")
|
|
162
|
-
.select([
|
|
170
|
+
.select([
|
|
171
|
+
"device_id",
|
|
172
|
+
"org_id",
|
|
173
|
+
"account_id",
|
|
174
|
+
"name",
|
|
175
|
+
"os",
|
|
176
|
+
"arch",
|
|
177
|
+
"roots",
|
|
178
|
+
"created_at",
|
|
179
|
+
"local_policy",
|
|
180
|
+
])
|
|
163
181
|
.where("id", "=", machineId)
|
|
164
182
|
.where("revoked_at", "is", null)
|
|
165
183
|
.forUpdate()
|
|
@@ -189,6 +207,19 @@ let MachinesService = class MachinesService {
|
|
|
189
207
|
.execute();
|
|
190
208
|
}
|
|
191
209
|
}
|
|
210
|
+
const summary = localPolicyOf(hello.policy);
|
|
211
|
+
const isolation = summary?.isolation ?? isolationOf(rawPolicy);
|
|
212
|
+
const policy = summary ?? (isolation === null ? null : { isolation });
|
|
213
|
+
const before = isolationOf(self.local_policy) ?? "sandbox";
|
|
214
|
+
if (isolation !== null && isolation !== before) {
|
|
215
|
+
await audit(this.db.db, {
|
|
216
|
+
actorAccountId: self.account_id,
|
|
217
|
+
orgId: self.org_id,
|
|
218
|
+
action: "device.isolation_reported",
|
|
219
|
+
target: self.device_id,
|
|
220
|
+
detail: { machineId, isolation, previous: before },
|
|
221
|
+
});
|
|
222
|
+
}
|
|
192
223
|
await this.db
|
|
193
224
|
.updateTable("device_machines")
|
|
194
225
|
.set({
|
|
@@ -198,6 +229,8 @@ let MachinesService = class MachinesService {
|
|
|
198
229
|
mode: hello.mode,
|
|
199
230
|
roots: JSON.stringify(hello.roots),
|
|
200
231
|
session_isolation: hello.sessionIsolation,
|
|
232
|
+
features: deviceFeaturesOf(hello.features),
|
|
233
|
+
local_policy: jsonOrNull(policy),
|
|
201
234
|
last_seen_at: sql `now()`,
|
|
202
235
|
})
|
|
203
236
|
.where("id", "=", machineId)
|
|
@@ -302,6 +335,8 @@ let MachinesService = class MachinesService {
|
|
|
302
335
|
"program_version",
|
|
303
336
|
"last_seen_at",
|
|
304
337
|
"created_at",
|
|
338
|
+
"features",
|
|
339
|
+
"local_policy",
|
|
305
340
|
])
|
|
306
341
|
.where("device_id", "=", deviceId)
|
|
307
342
|
.where("revoked_at", "is", null)
|
|
@@ -317,6 +352,8 @@ let MachinesService = class MachinesService {
|
|
|
317
352
|
programVersion: r.program_version,
|
|
318
353
|
lastSeenAt: r.last_seen_at,
|
|
319
354
|
createdAt: r.created_at,
|
|
355
|
+
features: deviceFeaturesOf(r.features),
|
|
356
|
+
localPolicy: localPolicyOf(r.local_policy),
|
|
320
357
|
}
|
|
321
358
|
: null;
|
|
322
359
|
}
|
|
@@ -360,6 +397,7 @@ MachinesService = __decorate([
|
|
|
360
397
|
__param(4, Inject(KERNEL_LOGGER))
|
|
361
398
|
], MachinesService);
|
|
362
399
|
export { MachinesService };
|
|
400
|
+
const jsonOrNull = (v) => (v === null ? null : JSON.stringify(v));
|
|
363
401
|
/** Two machine rows (or a login) name the same machine: same name, OS and arch. */
|
|
364
402
|
function sameMachine(a, b) {
|
|
365
403
|
return a.name === b.name && a.os === b.os && a.arch === b.arch;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { type DeviceExecGrants, type NetworkBlockedHost, type SandboxLimits } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
3
|
+
/** The most hosts one exec's grants carry (the device protocol's limit). */
|
|
4
|
+
export declare const MAX_GRANT_HOSTS = 100;
|
|
5
|
+
/** What a run tree's network asks add up to (network ask-to-allow spec §6.8). */
|
|
6
|
+
export interface NetworkRunState {
|
|
7
|
+
/** Hosts allowed for the rest of the run tree (「这次允许」). */
|
|
8
|
+
granted: Set<string>;
|
|
9
|
+
/** Hosts refused in the run tree (denied, expired, skipped, cancelled, or unchecked): never asked again. */
|
|
10
|
+
refused: Set<string>;
|
|
11
|
+
/** Network asks of this run (the per-run cap counts them). */
|
|
12
|
+
asks: number;
|
|
13
|
+
/** A network ask of the run tree is still pending (at most one at a time). */
|
|
14
|
+
pending: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Hosts the scheduled task the run serves approves on this device (spec §8.1; unattended runs
|
|
17
|
+
* only, read only with a `deviceId`): never asked about nor recorded as missing. Absent: none.
|
|
18
|
+
*/
|
|
19
|
+
taskGranted?: Set<string>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Network hosts the kernel allows a run beyond a device's local policy (network ask-to-allow spec
|
|
23
|
+
* §6.3): the hosts of the run tree's approved `network` asks with scope `once` (「这次允许」), read
|
|
24
|
+
* from PG for every exec, so a grant decided on one replica reaches a command on any other. A run
|
|
25
|
+
* tree is the runs of one budget window (`runs.budget_run_id` lineage): a sub-agent's run sees the
|
|
26
|
+
* root's grants, another run tree never does. The hosts a decision allowed are its
|
|
27
|
+
* `detail.grantedHosts`; a decision without them grants nothing. In an unattended run of a
|
|
28
|
+
* scheduled task, the task's `network:<host>` grants (spec §8.1) join them.
|
|
29
|
+
*/
|
|
30
|
+
export declare class NetworkGrants {
|
|
31
|
+
private readonly db;
|
|
32
|
+
constructor(db: KernelDb);
|
|
33
|
+
/**
|
|
34
|
+
* Hosts allowed for this exec on `deviceId`: the run tree's once-approved network asks in
|
|
35
|
+
* `orgId` about that device, while the tree's root run has not ended (a grant ends with its run;
|
|
36
|
+
* a later run of the same budget window does not inherit it), with the root run as their source
|
|
37
|
+
* and the latest decider as its account; and in an unattended run of a scheduled task, the
|
|
38
|
+
* task's network grants (spec §8.1) when the device is its creator's, with the task as their
|
|
39
|
+
* source and the creator as its account. Both together are one grant: the hosts of both, the
|
|
40
|
+
* task as the source. Undefined when none. The device applies them only when its owner allows
|
|
41
|
+
* remote grants and only within the org's limits.
|
|
42
|
+
*/
|
|
43
|
+
forExec(input: {
|
|
44
|
+
orgId: string;
|
|
45
|
+
runId: string;
|
|
46
|
+
deviceId: string;
|
|
47
|
+
}): Promise<DeviceExecGrants | undefined>;
|
|
48
|
+
/**
|
|
49
|
+
* The run tree's network asks (spec §6.8): what was granted and refused, the run's asks, a
|
|
50
|
+
* pending ask. With `deviceId`, granted and refused count only the asks about that device (a
|
|
51
|
+
* host allowed on one device is not allowed on another); the cap and the pending ask count all.
|
|
52
|
+
*/
|
|
53
|
+
runState(runId: string, deviceId?: string): Promise<NetworkRunState>;
|
|
54
|
+
/** The run tree's once-approved hosts as exec grants (source: the root run). */
|
|
55
|
+
private runGrants;
|
|
56
|
+
/**
|
|
57
|
+
* A scheduled task's network grants for an unattended run (spec §8.1): the task's
|
|
58
|
+
* `network:<host>` grants when the run serves a task run that is no trial run and no person
|
|
59
|
+
* joined (`taskGrantsRunOf`, inherited by its sub-agents), the task is of `orgId` (when given)
|
|
60
|
+
* and `deviceId` is a live device of the task's org owned by the task's creator (a creator
|
|
61
|
+
* approves domains for their own devices only). Exact valid hosts only.
|
|
62
|
+
*/
|
|
63
|
+
private taskGrants;
|
|
64
|
+
/**
|
|
65
|
+
* The tier rule of an unattended run on `deviceId` (sandbox dev tiers spec §11.4), read fresh:
|
|
66
|
+
* null for an attended run; otherwise the task run and whether the task grants `tier:open` on
|
|
67
|
+
* this device (`unattendedTierOf`).
|
|
68
|
+
*/
|
|
69
|
+
unattendedTier(input: {
|
|
70
|
+
runId: string;
|
|
71
|
+
deviceId: string;
|
|
72
|
+
}): Promise<{
|
|
73
|
+
taskRunId: string;
|
|
74
|
+
openGranted: boolean;
|
|
75
|
+
} | null>;
|
|
76
|
+
/** Records `key` in the task run's `missing_grants` (once). */
|
|
77
|
+
recordMissing(taskRunId: string, key: string): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Whether the device's active machine runs commands without a sandbox
|
|
80
|
+
* (`device_machines.local_policy.isolation`, read fresh at each exec, spec §4.3), read from the
|
|
81
|
+
* stored JSON's isolation field alone ({@link isolationOf}). Fails closed for a program that
|
|
82
|
+
* applies tiers (feature `tiers`, which always sends a summary): no summary or any isolation but
|
|
83
|
+
* `sandbox` counts as unsandboxed. False without a machine, or for an older program without
|
|
84
|
+
* tiers (it has no unsandboxed mode).
|
|
85
|
+
*/
|
|
86
|
+
unsandboxed(deviceId: string): Promise<boolean>;
|
|
87
|
+
/** The runs of the tree whose budget run is `root`. */
|
|
88
|
+
private treeOf;
|
|
89
|
+
}
|
|
90
|
+
/** Whether the org's effective egress lets a command reach `host`. */
|
|
91
|
+
export declare function orgAllows(host: string, limits: Pick<SandboxLimits, "egress" | "allowHosts">): boolean;
|
|
92
|
+
/**
|
|
93
|
+
* The kernel's own layer for a device's report (spec §4.1 item 3): it trusts the device's word that
|
|
94
|
+
* a host was refused, never its word that the org allows it. A host the device calls `device` that
|
|
95
|
+
* the org's effective egress (`limits`, read for this exec) does not allow becomes `org`; one that
|
|
96
|
+
* is no valid host name becomes `address`. Other layers are kept.
|
|
97
|
+
*/
|
|
98
|
+
export declare function relayer(blocked: NetworkBlockedHost[], limits: Pick<SandboxLimits, "egress" | "allowHosts">): NetworkBlockedHost[];
|
|
99
|
+
/**
|
|
100
|
+
* `grants` cut to the hosts the org's effective egress (`limits`, read for this exec) admits, or
|
|
101
|
+
* undefined when none is left. The device intersects them with the limits too; the kernel does
|
|
102
|
+
* it first so a grant never reaches a device beyond what the org allows.
|
|
103
|
+
*/
|
|
104
|
+
export declare function grantsWithin(grants: DeviceExecGrants | undefined, limits: Pick<SandboxLimits, "egress" | "allowHosts">): DeviceExecGrants | undefined;
|
|
@@ -0,0 +1,288 @@
|
|
|
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 { validateAllowHost, } from "@capybara-hq/plugin-sdk";
|
|
11
|
+
import { Inject, Injectable } from "@nestjs/common";
|
|
12
|
+
import { sql } from "kysely";
|
|
13
|
+
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
14
|
+
import { budgetRunOf } from "../runtime/subagents/limits.js";
|
|
15
|
+
import { appendMissingGrant, taskGrantsRunOf, unattendedTierOf } from "../schedules/unattended.js";
|
|
16
|
+
import { deviceFeaturesOf, isolationOf } from "./policy-summary.js";
|
|
17
|
+
/** The most hosts one exec's grants carry (the device protocol's limit). */
|
|
18
|
+
export const MAX_GRANT_HOSTS = 100;
|
|
19
|
+
/** The key prefix of a scheduled task's network grant (`schedules/grant-rules.ts`). */
|
|
20
|
+
const NETWORK_KEY_PREFIX = "network:";
|
|
21
|
+
/** The statuses of a run that has not ended: its run grants still apply. */
|
|
22
|
+
const LIVE_RUN = ["queued", "running", "waiting"];
|
|
23
|
+
/** The statuses of a network ask that refuse its hosts for the rest of the run tree (§6.8). */
|
|
24
|
+
const REFUSING = new Set(["denied", "expired", "skipped", "cancelled"]);
|
|
25
|
+
/** An exact host a grant may carry: no wildcard, valid. */
|
|
26
|
+
const exactHost = (h) => typeof h === "string" && !h.startsWith("*.") && validateAllowHost(h).length === 0;
|
|
27
|
+
/** `detail.deviceId` of a network ask: the device it asked about. */
|
|
28
|
+
function deviceOf(detail) {
|
|
29
|
+
const d = detail?.deviceId;
|
|
30
|
+
return typeof d === "string" ? d : undefined;
|
|
31
|
+
}
|
|
32
|
+
/** `detail.grantedHosts` of a decided network ask: what the decision allowed (absent: none). */
|
|
33
|
+
function grantedOf(detail) {
|
|
34
|
+
const g = detail?.grantedHosts;
|
|
35
|
+
return Array.isArray(g) ? g.filter(exactHost) : [];
|
|
36
|
+
}
|
|
37
|
+
/** `detail.hosts[].host` of a network ask: what it asked about. */
|
|
38
|
+
function askedOf(detail) {
|
|
39
|
+
const hs = detail?.hosts;
|
|
40
|
+
if (!Array.isArray(hs))
|
|
41
|
+
return [];
|
|
42
|
+
return hs.flatMap((h) => {
|
|
43
|
+
const host = h?.host;
|
|
44
|
+
return typeof host === "string" ? [host] : [];
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Network hosts the kernel allows a run beyond a device's local policy (network ask-to-allow spec
|
|
49
|
+
* §6.3): the hosts of the run tree's approved `network` asks with scope `once` (「这次允许」), read
|
|
50
|
+
* from PG for every exec, so a grant decided on one replica reaches a command on any other. A run
|
|
51
|
+
* tree is the runs of one budget window (`runs.budget_run_id` lineage): a sub-agent's run sees the
|
|
52
|
+
* root's grants, another run tree never does. The hosts a decision allowed are its
|
|
53
|
+
* `detail.grantedHosts`; a decision without them grants nothing. In an unattended run of a
|
|
54
|
+
* scheduled task, the task's `network:<host>` grants (spec §8.1) join them.
|
|
55
|
+
*/
|
|
56
|
+
let NetworkGrants = class NetworkGrants {
|
|
57
|
+
db;
|
|
58
|
+
constructor(db) {
|
|
59
|
+
this.db = db;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Hosts allowed for this exec on `deviceId`: the run tree's once-approved network asks in
|
|
63
|
+
* `orgId` about that device, while the tree's root run has not ended (a grant ends with its run;
|
|
64
|
+
* a later run of the same budget window does not inherit it), with the root run as their source
|
|
65
|
+
* and the latest decider as its account; and in an unattended run of a scheduled task, the
|
|
66
|
+
* task's network grants (spec §8.1) when the device is its creator's, with the task as their
|
|
67
|
+
* source and the creator as its account. Both together are one grant: the hosts of both, the
|
|
68
|
+
* task as the source. Undefined when none. The device applies them only when its owner allows
|
|
69
|
+
* remote grants and only within the org's limits.
|
|
70
|
+
*/
|
|
71
|
+
async forExec(input) {
|
|
72
|
+
const run = await this.runGrants(input);
|
|
73
|
+
const task = await this.taskGrants(input);
|
|
74
|
+
if (task === undefined)
|
|
75
|
+
return run;
|
|
76
|
+
if (run === undefined)
|
|
77
|
+
return task;
|
|
78
|
+
// one exec frame carries one source: the task's, the standing approval behind an unattended
|
|
79
|
+
// run (the run grants of an unattended run come from asks only in `wait` mode); the device
|
|
80
|
+
// logs it with the command, and the kernel's approvals keep the per-host record
|
|
81
|
+
const hosts = [...new Set([...task.hosts, ...run.hosts])].slice(0, MAX_GRANT_HOSTS);
|
|
82
|
+
return { hosts, source: task.source };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The run tree's network asks (spec §6.8): what was granted and refused, the run's asks, a
|
|
86
|
+
* pending ask. With `deviceId`, granted and refused count only the asks about that device (a
|
|
87
|
+
* host allowed on one device is not allowed on another); the cap and the pending ask count all.
|
|
88
|
+
*/
|
|
89
|
+
async runState(runId, deviceId) {
|
|
90
|
+
const task = deviceId === undefined ? undefined : await this.taskGrants({ runId, deviceId });
|
|
91
|
+
const root = await budgetRunOf(this.db.db, runId);
|
|
92
|
+
const rows = await this.db
|
|
93
|
+
.selectFrom("approvals")
|
|
94
|
+
.select(["run_id", "status", "scope", "detail"])
|
|
95
|
+
.where("kind", "=", "network")
|
|
96
|
+
.where("run_id", "in", this.treeOf(root))
|
|
97
|
+
.execute();
|
|
98
|
+
const state = {
|
|
99
|
+
granted: new Set(),
|
|
100
|
+
refused: new Set(),
|
|
101
|
+
asks: 0,
|
|
102
|
+
pending: false,
|
|
103
|
+
};
|
|
104
|
+
for (const r of rows) {
|
|
105
|
+
if (r.run_id === runId)
|
|
106
|
+
state.asks++;
|
|
107
|
+
if (r.status === "pending") {
|
|
108
|
+
state.pending = true;
|
|
109
|
+
}
|
|
110
|
+
else if (deviceId !== undefined && deviceOf(r.detail) !== deviceId) {
|
|
111
|
+
// another device's decision says nothing about this one
|
|
112
|
+
}
|
|
113
|
+
else if (r.status === "approved") {
|
|
114
|
+
const granted = grantedOf(r.detail);
|
|
115
|
+
if (r.scope === "once")
|
|
116
|
+
for (const h of granted)
|
|
117
|
+
state.granted.add(h);
|
|
118
|
+
// the hosts left unchecked in a partial decision count as denied (P-D5)
|
|
119
|
+
for (const h of askedOf(r.detail))
|
|
120
|
+
if (!granted.includes(h))
|
|
121
|
+
state.refused.add(h);
|
|
122
|
+
}
|
|
123
|
+
else if (REFUSING.has(r.status)) {
|
|
124
|
+
for (const h of askedOf(r.detail))
|
|
125
|
+
state.refused.add(h);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (task !== undefined)
|
|
129
|
+
state.taskGranted = new Set(task.hosts);
|
|
130
|
+
return state;
|
|
131
|
+
}
|
|
132
|
+
/** The run tree's once-approved hosts as exec grants (source: the root run). */
|
|
133
|
+
async runGrants(input) {
|
|
134
|
+
const root = await budgetRunOf(this.db.db, input.runId);
|
|
135
|
+
const live = await this.db
|
|
136
|
+
.selectFrom("runs")
|
|
137
|
+
.select("id")
|
|
138
|
+
.where("id", "=", root)
|
|
139
|
+
.where("status", "in", LIVE_RUN)
|
|
140
|
+
.executeTakeFirst();
|
|
141
|
+
if (live === undefined)
|
|
142
|
+
return undefined;
|
|
143
|
+
const rows = await this.db
|
|
144
|
+
.selectFrom("approvals")
|
|
145
|
+
.select(["decided_by", "detail"])
|
|
146
|
+
.where("kind", "=", "network")
|
|
147
|
+
.where("org_id", "=", input.orgId)
|
|
148
|
+
.where("status", "=", "approved")
|
|
149
|
+
.where("scope", "=", "once")
|
|
150
|
+
.where("decided_by", "is not", null)
|
|
151
|
+
.where(sql `detail->>'deviceId'`, "=", input.deviceId)
|
|
152
|
+
.where("run_id", "in", this.treeOf(root))
|
|
153
|
+
.orderBy("decided_at", "desc")
|
|
154
|
+
.orderBy("id", "desc")
|
|
155
|
+
.execute();
|
|
156
|
+
const hosts = [...new Set(rows.flatMap((r) => grantedOf(r.detail)))].slice(0, MAX_GRANT_HOSTS);
|
|
157
|
+
const latest = rows.find((r) => grantedOf(r.detail).length > 0);
|
|
158
|
+
if (hosts.length === 0 || latest?.decided_by == null)
|
|
159
|
+
return undefined;
|
|
160
|
+
return { hosts, source: { kind: "run", id: root, accountId: latest.decided_by } };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* A scheduled task's network grants for an unattended run (spec §8.1): the task's
|
|
164
|
+
* `network:<host>` grants when the run serves a task run that is no trial run and no person
|
|
165
|
+
* joined (`taskGrantsRunOf`, inherited by its sub-agents), the task is of `orgId` (when given)
|
|
166
|
+
* and `deviceId` is a live device of the task's org owned by the task's creator (a creator
|
|
167
|
+
* approves domains for their own devices only). Exact valid hosts only.
|
|
168
|
+
*/
|
|
169
|
+
async taskGrants(input) {
|
|
170
|
+
const task = await taskGrantsRunOf(this.db.db, input.runId);
|
|
171
|
+
if (task === null || (input.orgId !== undefined && task.orgId !== input.orgId)) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
const device = await this.db
|
|
175
|
+
.selectFrom("devices")
|
|
176
|
+
.select("id")
|
|
177
|
+
.where("id", "=", input.deviceId)
|
|
178
|
+
.where("org_id", "=", task.orgId)
|
|
179
|
+
.where("owner_account_id", "=", task.accountId)
|
|
180
|
+
.where("deleted_at", "is", null)
|
|
181
|
+
.executeTakeFirst();
|
|
182
|
+
if (device === undefined)
|
|
183
|
+
return undefined;
|
|
184
|
+
const rows = await this.db
|
|
185
|
+
.selectFrom("scheduled_task_grants")
|
|
186
|
+
.select("tool_key")
|
|
187
|
+
.where("task_id", "=", task.taskId)
|
|
188
|
+
.where("tool_key", "like", `${NETWORK_KEY_PREFIX}%`)
|
|
189
|
+
.orderBy("tool_key")
|
|
190
|
+
.execute();
|
|
191
|
+
const hosts = rows
|
|
192
|
+
.map((r) => r.tool_key.slice(NETWORK_KEY_PREFIX.length))
|
|
193
|
+
.filter(exactHost)
|
|
194
|
+
.slice(0, MAX_GRANT_HOSTS);
|
|
195
|
+
if (hosts.length === 0)
|
|
196
|
+
return undefined;
|
|
197
|
+
return { hosts, source: { kind: "task", id: task.taskId, accountId: task.accountId } };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The tier rule of an unattended run on `deviceId` (sandbox dev tiers spec §11.4), read fresh:
|
|
201
|
+
* null for an attended run; otherwise the task run and whether the task grants `tier:open` on
|
|
202
|
+
* this device (`unattendedTierOf`).
|
|
203
|
+
*/
|
|
204
|
+
async unattendedTier(input) {
|
|
205
|
+
return await unattendedTierOf(this.db.db, input.runId, input.deviceId);
|
|
206
|
+
}
|
|
207
|
+
/** Records `key` in the task run's `missing_grants` (once). */
|
|
208
|
+
async recordMissing(taskRunId, key) {
|
|
209
|
+
await appendMissingGrant(this.db.db, taskRunId, key);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Whether the device's active machine runs commands without a sandbox
|
|
213
|
+
* (`device_machines.local_policy.isolation`, read fresh at each exec, spec §4.3), read from the
|
|
214
|
+
* stored JSON's isolation field alone ({@link isolationOf}). Fails closed for a program that
|
|
215
|
+
* applies tiers (feature `tiers`, which always sends a summary): no summary or any isolation but
|
|
216
|
+
* `sandbox` counts as unsandboxed. False without a machine, or for an older program without
|
|
217
|
+
* tiers (it has no unsandboxed mode).
|
|
218
|
+
*/
|
|
219
|
+
async unsandboxed(deviceId) {
|
|
220
|
+
const row = await this.db
|
|
221
|
+
.selectFrom("device_machines")
|
|
222
|
+
.select(["local_policy", "features"])
|
|
223
|
+
.where("device_id", "=", deviceId)
|
|
224
|
+
.where("revoked_at", "is", null)
|
|
225
|
+
.executeTakeFirst();
|
|
226
|
+
if (row === undefined)
|
|
227
|
+
return false;
|
|
228
|
+
const isolation = isolationOf(row.local_policy);
|
|
229
|
+
if (isolation !== null)
|
|
230
|
+
return isolation === "none";
|
|
231
|
+
return deviceFeaturesOf(row.features).includes("tiers");
|
|
232
|
+
}
|
|
233
|
+
/** The runs of the tree whose budget run is `root`. */
|
|
234
|
+
treeOf(root) {
|
|
235
|
+
return this.db
|
|
236
|
+
.selectFrom("runs")
|
|
237
|
+
.select("id")
|
|
238
|
+
.where((eb) => eb.or([eb("id", "=", root), eb("budget_run_id", "=", root)]));
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
NetworkGrants = __decorate([
|
|
242
|
+
Injectable(),
|
|
243
|
+
__param(0, Inject(KernelDb))
|
|
244
|
+
], NetworkGrants);
|
|
245
|
+
export { NetworkGrants };
|
|
246
|
+
const normalize = (host) => {
|
|
247
|
+
const h = host.toLowerCase();
|
|
248
|
+
return h.endsWith(".") ? h.slice(0, -1) : h;
|
|
249
|
+
};
|
|
250
|
+
/** Whether the allowlist entry `pattern` admits `host` (`*.x.com`: subdomains, never the apex). */
|
|
251
|
+
const covers = (pattern, host) => pattern === host || (pattern.startsWith("*.") && host.endsWith(pattern.slice(1)));
|
|
252
|
+
/** Whether the org's effective egress lets a command reach `host`. */
|
|
253
|
+
export function orgAllows(host, limits) {
|
|
254
|
+
if (limits.egress === "none")
|
|
255
|
+
return false;
|
|
256
|
+
if (limits.egress === "open")
|
|
257
|
+
return true;
|
|
258
|
+
const h = normalize(host);
|
|
259
|
+
return limits.allowHosts.some((p) => covers(p.toLowerCase(), h));
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The kernel's own layer for a device's report (spec §4.1 item 3): it trusts the device's word that
|
|
263
|
+
* a host was refused, never its word that the org allows it. A host the device calls `device` that
|
|
264
|
+
* the org's effective egress (`limits`, read for this exec) does not allow becomes `org`; one that
|
|
265
|
+
* is no valid host name becomes `address`. Other layers are kept.
|
|
266
|
+
*/
|
|
267
|
+
export function relayer(blocked, limits) {
|
|
268
|
+
return blocked.map((b) => {
|
|
269
|
+
if (b.layer !== "device")
|
|
270
|
+
return b;
|
|
271
|
+
const h = normalize(b.host);
|
|
272
|
+
if (validateAllowHost(h).length > 0 || h.startsWith("*.")) {
|
|
273
|
+
return { ...b, layer: "address" };
|
|
274
|
+
}
|
|
275
|
+
return orgAllows(b.host, limits) ? b : { ...b, layer: "org" };
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* `grants` cut to the hosts the org's effective egress (`limits`, read for this exec) admits, or
|
|
280
|
+
* undefined when none is left. The device intersects them with the limits too; the kernel does
|
|
281
|
+
* it first so a grant never reaches a device beyond what the org allows.
|
|
282
|
+
*/
|
|
283
|
+
export function grantsWithin(grants, limits) {
|
|
284
|
+
if (grants === undefined)
|
|
285
|
+
return undefined;
|
|
286
|
+
const hosts = grants.hosts.filter((h) => orgAllows(h, limits));
|
|
287
|
+
return hosts.length === 0 ? undefined : { ...grants, hosts };
|
|
288
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type DeviceFeature, type DeviceLocalPolicySummary } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
/**
|
|
3
|
+
* The known features in `v` (a hello's `features` or the stored column), in a fixed order and
|
|
4
|
+
* deduped; anything else (unknown names, a non-array) is dropped.
|
|
5
|
+
*/
|
|
6
|
+
export declare function deviceFeaturesOf(v: unknown): DeviceFeature[];
|
|
7
|
+
/**
|
|
8
|
+
* `v` as a local policy summary (network ask-to-allow spec §5.4), or null when it is absent or not
|
|
9
|
+
* a valid one. The device is the authority on its policy: the kernel only keeps what it last said,
|
|
10
|
+
* for the device page and to pick a card's options, never to decide what a command may reach.
|
|
11
|
+
*/
|
|
12
|
+
export declare function localPolicyOf(v: unknown): DeviceLocalPolicySummary | null;
|
|
13
|
+
/**
|
|
14
|
+
* The isolation a policy summary states, read from its `isolation` field alone (a hello's raw
|
|
15
|
+
* `policy` or the stored column), so a summary the kernel cannot otherwise read (a newer program's
|
|
16
|
+
* new value elsewhere, a broken one) still tells whether commands run without a sandbox: `sandbox`
|
|
17
|
+
* only when it says exactly that, `none` for any other present summary (fail closed), null when
|
|
18
|
+
* there is none.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isolationOf(v: unknown): "sandbox" | "none" | null;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { isSandboxTier, } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
const KNOWN_FEATURES = ["networkReport", "policyOp", "tiers"];
|
|
3
|
+
/**
|
|
4
|
+
* The known features in `v` (a hello's `features` or the stored column), in a fixed order and
|
|
5
|
+
* deduped; anything else (unknown names, a non-array) is dropped.
|
|
6
|
+
*/
|
|
7
|
+
export function deviceFeaturesOf(v) {
|
|
8
|
+
if (!Array.isArray(v))
|
|
9
|
+
return [];
|
|
10
|
+
return KNOWN_FEATURES.filter((f) => v.includes(f));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `v` as a local policy summary (network ask-to-allow spec §5.4), or null when it is absent or not
|
|
14
|
+
* a valid one. The device is the authority on its policy: the kernel only keeps what it last said,
|
|
15
|
+
* for the device page and to pick a card's options, never to decide what a command may reach.
|
|
16
|
+
*/
|
|
17
|
+
export function localPolicyOf(v) {
|
|
18
|
+
if (typeof v !== "object" || v === null || Array.isArray(v))
|
|
19
|
+
return null;
|
|
20
|
+
const o = v;
|
|
21
|
+
if (o.tier !== undefined && !isSandboxTier(o.tier))
|
|
22
|
+
return null;
|
|
23
|
+
if (!isSandboxTier(o.effectiveTier))
|
|
24
|
+
return null;
|
|
25
|
+
if (o.isolation !== "sandbox" && o.isolation !== "none")
|
|
26
|
+
return null;
|
|
27
|
+
if (o.localhost !== "on" && o.localhost !== "off-proxy-detected" && o.localhost !== "off") {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (o.unixSockets !== "roots" && o.unixSockets !== "all-masked" && o.unixSockets !== "off") {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (o.remoteGrants !== "owner" && o.remoteGrants !== "off")
|
|
34
|
+
return null;
|
|
35
|
+
if (!Array.isArray(o.allowedDomains))
|
|
36
|
+
return null;
|
|
37
|
+
if (!o.allowedDomains.every((d) => typeof d === "string"))
|
|
38
|
+
return null;
|
|
39
|
+
return {
|
|
40
|
+
...(o.tier !== undefined && { tier: o.tier }),
|
|
41
|
+
effectiveTier: o.effectiveTier,
|
|
42
|
+
isolation: o.isolation,
|
|
43
|
+
localhost: o.localhost,
|
|
44
|
+
unixSockets: o.unixSockets,
|
|
45
|
+
allowedDomains: [...o.allowedDomains],
|
|
46
|
+
remoteGrants: o.remoteGrants,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The isolation a policy summary states, read from its `isolation` field alone (a hello's raw
|
|
51
|
+
* `policy` or the stored column), so a summary the kernel cannot otherwise read (a newer program's
|
|
52
|
+
* new value elsewhere, a broken one) still tells whether commands run without a sandbox: `sandbox`
|
|
53
|
+
* only when it says exactly that, `none` for any other present summary (fail closed), null when
|
|
54
|
+
* there is none.
|
|
55
|
+
*/
|
|
56
|
+
export function isolationOf(v) {
|
|
57
|
+
if (v === undefined || v === null)
|
|
58
|
+
return null;
|
|
59
|
+
if (typeof v === "object" && !Array.isArray(v)) {
|
|
60
|
+
return v.isolation === "sandbox" ? "sandbox" : "none";
|
|
61
|
+
}
|
|
62
|
+
return "none";
|
|
63
|
+
}
|
package/dist/orgs/orgs.module.js
CHANGED
|
@@ -21,7 +21,7 @@ import { MembersService } from "./members.service.js";
|
|
|
21
21
|
import { Offboarding } from "./offboarding.js";
|
|
22
22
|
import { OrgLockModule } from "./org-lock.js";
|
|
23
23
|
import { OrgsService } from "./orgs.service.js";
|
|
24
|
-
import {
|
|
24
|
+
import { OrgSettingsModule } from "./settings.service.js";
|
|
25
25
|
/**
|
|
26
26
|
* Organizations: orgs, org settings, members, invitations, offboarding (with its sweep cron) and
|
|
27
27
|
* directory sync (the hourly scan cron, one-off syncs on the maintenance queue).
|
|
@@ -35,6 +35,7 @@ OrgsModule = __decorate([
|
|
|
35
35
|
// offboarding publishes the events it cancelled
|
|
36
36
|
EventLogModule,
|
|
37
37
|
OrgLockModule,
|
|
38
|
+
OrgSettingsModule,
|
|
38
39
|
ExtensionsModule,
|
|
39
40
|
// offboarding clears credentials and cancels approvals and questions
|
|
40
41
|
ToolsModule,
|
|
@@ -49,18 +50,11 @@ OrgsModule = __decorate([
|
|
|
49
50
|
// the producer side of directory-sync jobs (the maintenance processor consumes them)
|
|
50
51
|
QueueModule.registerQueue(MAINTENANCE_QUEUE_NAME),
|
|
51
52
|
],
|
|
52
|
-
providers: [
|
|
53
|
-
OrgsService,
|
|
54
|
-
OrgSettingsService,
|
|
55
|
-
MembersService,
|
|
56
|
-
InvitationsService,
|
|
57
|
-
Offboarding,
|
|
58
|
-
DirectorySync,
|
|
59
|
-
],
|
|
53
|
+
providers: [OrgsService, MembersService, InvitationsService, Offboarding, DirectorySync],
|
|
60
54
|
exports: [
|
|
61
55
|
OrgLockModule,
|
|
56
|
+
OrgSettingsModule,
|
|
62
57
|
OrgsService,
|
|
63
|
-
OrgSettingsService,
|
|
64
58
|
MembersService,
|
|
65
59
|
InvitationsService,
|
|
66
60
|
Offboarding,
|
package/dist/orgs/roles.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export type OrgRole = (typeof ORG_ROLES)[number];
|
|
|
4
4
|
/** `role` 是否不低于 `min`。 */
|
|
5
5
|
export declare function roleAtLeast(role: OrgRole, min: OrgRole): boolean;
|
|
6
6
|
/** 组织级动作;路由用 `@OrgAction(action)` 声明所需动作。 */
|
|
7
|
-
export type OrgAction = "org.read" | "org.update" | "org.delete" | "org.transfer" | "org.members.read" | "org.members.manage" | "org.invitations.manage" | "org.tokens.manage" | "org.extensions.read" | "org.extensions.manage" | "agents.create" | "agents.use" | "agents.manage" | "connectors.manage" | "connectors.use" | "approvals.decide" | "channels.manage" | "skills.read" | "skills.manage" | "ui.use" | "trajectories.read" | "evals.manage" | "org.models.manage" | "catalog.read" | "catalog.request" | "catalog.decide" | "catalog.upload";
|
|
7
|
+
export type OrgAction = "org.read" | "org.update" | "org.delete" | "org.transfer" | "org.members.read" | "org.members.manage" | "org.invitations.manage" | "org.tokens.manage" | "org.extensions.read" | "org.extensions.manage" | "agents.create" | "agents.use" | "agents.manage" | "connectors.manage" | "connectors.use" | "approvals.decide" | "channels.manage" | "skills.read" | "skills.manage" | "ui.use" | "trajectories.read" | "evals.manage" | "org.models.manage" | "catalog.read" | "catalog.request" | "catalog.decide" | "catalog.upload" | "schedules.use" | "schedules.manage";
|
|
8
8
|
/** 每个动作所需的最低角色。 */
|
|
9
9
|
export declare const ACTION_MIN_ROLE: Record<OrgAction, OrgRole>;
|
|
10
10
|
/** 只允许 user principal 执行的动作:服务令牌即使角色足够也拒绝(除 SERVICE_ALLOWED 外全部)。 */
|