@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.
Files changed (247) hide show
  1. package/bootstrap.json +10 -1
  2. package/dist/agents/admin-agents.d.ts +16 -3
  3. package/dist/agents/admin-agents.js +65 -7
  4. package/dist/agents/agents.schemas.d.ts +24 -6
  5. package/dist/agents/agents.service.d.ts +4 -3
  6. package/dist/agents/agents.service.js +39 -26
  7. package/dist/agents/definition.d.ts +44 -6
  8. package/dist/agents/definition.js +86 -18
  9. package/dist/agents/from-template.d.ts +6 -3
  10. package/dist/agents/from-template.js +57 -44
  11. package/dist/agents/org-templates.js +97 -41
  12. package/dist/agents/templates.d.ts +6 -0
  13. package/dist/agents/templates.js +15 -1
  14. package/dist/catalog/admin-catalog.controller.d.ts +2 -4
  15. package/dist/catalog/admin-catalog.controller.js +2 -6
  16. package/dist/catalog/catalog.module.js +3 -0
  17. package/dist/catalog/claude/report.d.ts +9 -2
  18. package/dist/catalog/claude/report.js +1 -0
  19. package/dist/catalog/claude/translate.js +1 -1
  20. package/dist/catalog/component-promotion.js +1 -2
  21. package/dist/catalog/component-state.d.ts +13 -1
  22. package/dist/catalog/component-state.js +14 -5
  23. package/dist/catalog/marketplaces.service.d.ts +1 -1
  24. package/dist/catalog/marketplaces.service.js +2 -1
  25. package/dist/catalog/org-catalog.controller.d.ts +13 -3
  26. package/dist/catalog/org-catalog.controller.js +26 -4
  27. package/dist/catalog/org-catalog.schemas.d.ts +13 -0
  28. package/dist/catalog/org-catalog.schemas.js +34 -0
  29. package/dist/catalog/org-plugin-uploads.controller.js +0 -1
  30. package/dist/catalog/org-plugin-uploads.d.ts +0 -1
  31. package/dist/catalog/org-plugin-uploads.js +0 -3
  32. package/dist/catalog/org-plugin-uploads.schemas.d.ts +0 -1
  33. package/dist/catalog/org-plugin-uploads.schemas.js +0 -2
  34. package/dist/catalog/platform-components.d.ts +0 -1
  35. package/dist/catalog/platform-components.js +3 -8
  36. package/dist/catalog/plugin-import.d.ts +0 -8
  37. package/dist/catalog/plugin-import.js +4 -35
  38. package/dist/catalog/plugin-install.d.ts +2 -2
  39. package/dist/catalog/plugin-install.js +2 -2
  40. package/dist/catalog/plugin-package.d.ts +7 -0
  41. package/dist/catalog/plugin-package.js +27 -1
  42. package/dist/catalog/plugin-upgrade.d.ts +5 -6
  43. package/dist/catalog/plugin-upgrade.js +5 -23
  44. package/dist/catalog/requests.service.d.ts +66 -10
  45. package/dist/catalog/requests.service.js +196 -15
  46. package/dist/channels/actions.d.ts +2 -0
  47. package/dist/channels/actions.js +19 -4
  48. package/dist/channels/cards.d.ts +16 -2
  49. package/dist/channels/cards.js +40 -7
  50. package/dist/channels/channels.module.js +3 -0
  51. package/dist/channels/outbound.d.ts +22 -1
  52. package/dist/channels/outbound.js +116 -7
  53. package/dist/channels/phrases.d.ts +25 -0
  54. package/dist/channels/phrases.js +19 -0
  55. package/dist/channels/threads.d.ts +12 -0
  56. package/dist/channels/threads.js +34 -0
  57. package/dist/config/schema.d.ts +23 -1
  58. package/dist/config/schema.js +105 -5
  59. package/dist/conversations/conversations.service.d.ts +42 -9
  60. package/dist/conversations/conversations.service.js +57 -25
  61. package/dist/conversations/events.d.ts +106 -11
  62. package/dist/conversations/events.js +121 -18
  63. package/dist/conversations/message-context.d.ts +6 -3
  64. package/dist/conversations/message-context.js +14 -10
  65. package/dist/conversations/queue.d.ts +15 -2
  66. package/dist/conversations/queue.js +22 -3
  67. package/dist/conversations/queue.service.d.ts +12 -6
  68. package/dist/conversations/queue.service.js +36 -13
  69. package/dist/db/kernel-schema.d.ts +105 -3
  70. package/dist/devices/conversation-devices.d.ts +8 -1
  71. package/dist/devices/conversation-devices.js +8 -0
  72. package/dist/devices/devices.controller.d.ts +21 -4
  73. package/dist/devices/devices.controller.js +46 -8
  74. package/dist/devices/devices.schemas.d.ts +9 -1
  75. package/dist/devices/devices.schemas.js +11 -3
  76. package/dist/devices/devices.service.d.ts +176 -4
  77. package/dist/devices/devices.service.js +341 -6
  78. package/dist/devices/dto.d.ts +56 -3
  79. package/dist/devices/dto.js +51 -1
  80. package/dist/devices/public-suffix.d.ts +6 -0
  81. package/dist/devices/public-suffix.js +79 -0
  82. package/dist/errors.d.ts +46 -5
  83. package/dist/errors.js +46 -5
  84. package/dist/evals/reconstruct.d.ts +18 -13
  85. package/dist/evals/reconstruct.js +50 -36
  86. package/dist/evals/recorded-tools.d.ts +7 -7
  87. package/dist/evals/recorded-tools.js +4 -3
  88. package/dist/evals/trajectory.d.ts +6 -1
  89. package/dist/evals/trajectory.js +7 -0
  90. package/dist/extensions/bootstrap.d.ts +49 -10
  91. package/dist/extensions/bootstrap.js +104 -27
  92. package/dist/extensions/package.js +2 -2
  93. package/dist/http/api.module.js +5 -0
  94. package/dist/inbox/inbox.module.js +2 -1
  95. package/dist/inbox/inbox.service.d.ts +19 -4
  96. package/dist/inbox/inbox.service.js +14 -3
  97. package/dist/infra/config/kernel-config.service.js +11 -0
  98. package/dist/infra/events/kernel-events.d.ts +2 -1
  99. package/dist/lifecycle/kernel-lifecycle.d.ts +5 -1
  100. package/dist/lifecycle/kernel-lifecycle.js +16 -4
  101. package/dist/local-devices/device-policy.service.d.ts +65 -0
  102. package/dist/local-devices/device-policy.service.js +217 -0
  103. package/dist/local-devices/local-device-gateway.js +4 -1
  104. package/dist/local-devices/local-device-provider.d.ts +40 -3
  105. package/dist/local-devices/local-device-provider.js +109 -7
  106. package/dist/local-devices/local-devices.module.d.ts +2 -1
  107. package/dist/local-devices/local-devices.module.js +19 -3
  108. package/dist/local-devices/machines.d.ts +14 -3
  109. package/dist/local-devices/machines.js +41 -3
  110. package/dist/local-devices/network-grants.d.ts +104 -0
  111. package/dist/local-devices/network-grants.js +288 -0
  112. package/dist/local-devices/policy-summary.d.ts +20 -0
  113. package/dist/local-devices/policy-summary.js +63 -0
  114. package/dist/orgs/orgs.module.js +4 -10
  115. package/dist/orgs/roles.d.ts +1 -1
  116. package/dist/orgs/roles.js +4 -0
  117. package/dist/orgs/settings.controller.js +2 -1
  118. package/dist/orgs/settings.service.d.ts +164 -12
  119. package/dist/orgs/settings.service.js +212 -24
  120. package/dist/platform/exclusions.js +11 -1
  121. package/dist/platform/operation.js +4 -0
  122. package/dist/platform/ops/admin-items.ops.js +1 -1
  123. package/dist/platform/ops/agent-skills.ops.js +2 -2
  124. package/dist/platform/ops/agent-templates.ops.js +2 -2
  125. package/dist/platform/ops/agents.ops.js +11 -15
  126. package/dist/platform/ops/devices.ops.js +3 -1
  127. package/dist/platform/ops/inbox.ops.js +19 -0
  128. package/dist/platform/ops/org-settings.ops.js +14 -5
  129. package/dist/platform/ops/platform.ops.d.ts +9 -1
  130. package/dist/platform/ops/platform.ops.js +34 -12
  131. package/dist/platform/ops/requests.ops.js +3 -2
  132. package/dist/platform/ops/schedules.ops.d.ts +21 -0
  133. package/dist/platform/ops/schedules.ops.js +470 -0
  134. package/dist/platform/ops/skills.ops.js +3 -3
  135. package/dist/platform/platform-ops.module.d.ts +2 -1
  136. package/dist/platform/platform-ops.module.js +5 -0
  137. package/dist/platform/route-schemas.js +3 -0
  138. package/dist/roles/worker.module.js +3 -0
  139. package/dist/runtime/context/build.js +13 -4
  140. package/dist/runtime/context/compaction.js +6 -3
  141. package/dist/runtime/context/device-paths.d.ts +43 -21
  142. package/dist/runtime/context/device-paths.js +104 -21
  143. package/dist/runtime/context/run-context.d.ts +9 -0
  144. package/dist/runtime/context/run-context.js +8 -0
  145. package/dist/runtime/context/skills.d.ts +14 -1
  146. package/dist/runtime/context/skills.js +36 -5
  147. package/dist/runtime/maintenance.d.ts +4 -2
  148. package/dist/runtime/maintenance.js +16 -5
  149. package/dist/runtime/run-finish.d.ts +6 -1
  150. package/dist/runtime/run-finish.js +16 -2
  151. package/dist/runtime/run-queue.js +21 -2
  152. package/dist/runtime/run-worker.js +14 -33
  153. package/dist/runtime/runtime.module.js +3 -0
  154. package/dist/runtime/subagents/definition.d.ts +32 -14
  155. package/dist/runtime/subagents/definition.js +106 -109
  156. package/dist/runtime/subagents/dispatch.js +4 -3
  157. package/dist/runtime/subagents/preamble.d.ts +2 -1
  158. package/dist/runtime/subagents/preamble.js +2 -1
  159. package/dist/runtime/waits.js +14 -4
  160. package/dist/sandbox/sandbox.service.d.ts +120 -10
  161. package/dist/sandbox/sandbox.service.js +335 -28
  162. package/dist/sandbox/tiers.d.ts +86 -0
  163. package/dist/sandbox/tiers.js +96 -0
  164. package/dist/schedules/effective-settings.d.ts +16 -0
  165. package/dist/schedules/effective-settings.js +17 -0
  166. package/dist/schedules/errors.d.ts +8 -0
  167. package/dist/schedules/errors.js +8 -0
  168. package/dist/schedules/fire.d.ts +118 -0
  169. package/dist/schedules/fire.js +625 -0
  170. package/dist/schedules/grant-rules.d.ts +94 -0
  171. package/dist/schedules/grant-rules.js +126 -0
  172. package/dist/schedules/job-ids.d.ts +32 -0
  173. package/dist/schedules/job-ids.js +27 -0
  174. package/dist/schedules/pause.d.ts +9 -0
  175. package/dist/schedules/pause.js +33 -0
  176. package/dist/schedules/run-tracker.d.ts +81 -0
  177. package/dist/schedules/run-tracker.js +333 -0
  178. package/dist/schedules/scan.job.d.ts +64 -0
  179. package/dist/schedules/scan.job.js +303 -0
  180. package/dist/schedules/schedule-math.d.ts +31 -0
  181. package/dist/schedules/schedule-math.js +223 -0
  182. package/dist/schedules/scheduled-tasks.service.d.ts +351 -0
  183. package/dist/schedules/scheduled-tasks.service.js +1193 -0
  184. package/dist/schedules/schedules.controller.d.ts +68 -0
  185. package/dist/schedules/schedules.controller.js +242 -0
  186. package/dist/schedules/schedules.module.d.ts +16 -0
  187. package/dist/schedules/schedules.module.js +65 -0
  188. package/dist/schedules/schedules.schemas.d.ts +316 -0
  189. package/dist/schedules/schedules.schemas.js +186 -0
  190. package/dist/schedules/store.d.ts +81 -0
  191. package/dist/schedules/store.js +390 -0
  192. package/dist/schedules/store.module.d.ts +6 -0
  193. package/dist/schedules/store.module.js +21 -0
  194. package/dist/schedules/types.d.ts +108 -0
  195. package/dist/schedules/types.js +1 -0
  196. package/dist/schedules/unattended.d.ts +117 -0
  197. package/dist/schedules/unattended.js +228 -0
  198. package/dist/setup/oauth-token-probe.js +8 -4
  199. package/dist/skills/agent-skills.service.d.ts +4 -8
  200. package/dist/skills/agent-skills.service.js +18 -53
  201. package/dist/skills/catalog.d.ts +65 -62
  202. package/dist/skills/catalog.js +109 -206
  203. package/dist/skills/conversation-catalog.d.ts +3 -3
  204. package/dist/skills/conversation-catalog.js +5 -12
  205. package/dist/skills/invoke.d.ts +2 -2
  206. package/dist/skills/skills.controller.d.ts +3 -1
  207. package/dist/skills/skills.service.d.ts +38 -37
  208. package/dist/skills/skills.service.js +168 -107
  209. package/dist/skills/usable.d.ts +57 -8
  210. package/dist/skills/usable.js +172 -38
  211. package/dist/tools/approvals.controller.d.ts +4 -2
  212. package/dist/tools/approvals.controller.js +17 -2
  213. package/dist/tools/approvals.schemas.d.ts +1 -0
  214. package/dist/tools/approvals.schemas.js +3 -1
  215. package/dist/tools/approvals.service.d.ts +182 -7
  216. package/dist/tools/approvals.service.js +597 -13
  217. package/dist/tools/builtin-names.d.ts +25 -4
  218. package/dist/tools/builtin-names.js +45 -4
  219. package/dist/tools/builtins/builtin-tools.d.ts +8 -0
  220. package/dist/tools/builtins/builtin-tools.js +23 -2
  221. package/dist/tools/builtins/sandbox/bash.js +16 -7
  222. package/dist/tools/builtins/sandbox/common.d.ts +1 -1
  223. package/dist/tools/builtins/sandbox/common.js +33 -10
  224. package/dist/tools/builtins/sandbox/edit.js +7 -3
  225. package/dist/tools/builtins/sandbox/file-state.d.ts +31 -1
  226. package/dist/tools/builtins/sandbox/file-state.js +33 -3
  227. package/dist/tools/builtins/sandbox/write.js +8 -4
  228. package/dist/tools/builtins/schedules.d.ts +107 -0
  229. package/dist/tools/builtins/schedules.js +789 -0
  230. package/dist/tools/elicitation.d.ts +2 -0
  231. package/dist/tools/elicitation.js +1 -1
  232. package/dist/tools/fixed-ask.d.ts +7 -0
  233. package/dist/tools/fixed-ask.js +9 -0
  234. package/dist/tools/network-asks.d.ts +109 -0
  235. package/dist/tools/network-asks.js +221 -0
  236. package/dist/tools/pipeline.d.ts +53 -6
  237. package/dist/tools/pipeline.js +202 -13
  238. package/dist/tools/policy.d.ts +21 -8
  239. package/dist/tools/policy.js +63 -9
  240. package/dist/tools/registry.d.ts +10 -1
  241. package/dist/tools/tools.module.js +12 -0
  242. package/package.json +2 -2
  243. package/src/migrations/0113_scheduled_tasks.sql +108 -0
  244. package/src/migrations/0114_network_asks.sql +23 -0
  245. package/src/migrations/0115_network_requests.sql +28 -0
  246. package/src/migrations/0116_skills_default_on.sql +37 -0
  247. 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 } 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,115 @@ 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
+ return effectiveSandboxPolicy(this.config.get().sandbox, await readOrgSettings(this.db.db, orgId), tier);
297
+ }
298
+ /**
299
+ * The sandbox level a sandbox call in the device runs at now, for the model (`device_paths`,
300
+ * spec §11.5): see {@link sandboxStateOf}, plus
301
+ * - `network: "none"` when the org's or the platform's sandbox network is off;
302
+ * - a cloud device whose new limits wait for a file reset: the tier and network its instance
303
+ * still runs with (`limitsState(...).applied`; open with its network on when unrecorded);
304
+ * - a local device: its machine's localhost and unix socket facts; in a scheduled run without
305
+ * a person (`runId`, {@link unattendedTierOf}) and without the task's `tier:open` grant its
306
+ * tier is capped at `dev`, as the exec args cap it;
307
+ * - `unattended` in such a run: nobody can approve a domain.
308
+ * Null when the device is not a live device of the org.
309
+ */
310
+ async sandboxLevel(orgId, deviceId, runId) {
311
+ const row = await this.db
312
+ .selectFrom("devices as e")
313
+ .leftJoin("device_machines as m", (j) => j.onRef("m.device_id", "=", "e.id").on("m.revoked_at", "is", null))
314
+ .select(["e.source", "e.tier", "m.features", "m.local_policy"])
315
+ .where("e.id", "=", deviceId)
316
+ .where("e.org_id", "=", orgId)
317
+ .where("e.deleted_at", "is", null)
318
+ .executeTakeFirst();
319
+ if (row === undefined)
320
+ return null;
321
+ const [settings, unattended] = await Promise.all([
322
+ readOrgSettings(this.db.db, orgId),
323
+ runId === undefined ? null : unattendedTierOf(this.db.db, runId, deviceId),
324
+ ]);
325
+ const policyOf = (tier) => effectiveSandboxPolicy(this.config.get().sandbox, settings, tier);
326
+ const summary = localPolicyOf(row.local_policy);
327
+ const s = sandboxStateOf(policyOf, {
328
+ source: row.source,
329
+ tier: row.tier,
330
+ features: deviceFeaturesOf(row.features),
331
+ policy: summary,
332
+ });
333
+ let tier = s.effectiveTier;
334
+ let networkOff = policyOf(s.tier).base.egress === "none";
335
+ if (row.source === "cloud") {
336
+ const state = await this.sandbox.limitsState({ orgId, deviceId }, { policy: policyOf(row.tier) });
337
+ if (state.pendingFilesReset) {
338
+ // an unknown record (an instance made before records) may be wider: it counts as open
339
+ // with its network on, as the unattended gate counts it (`mayRunOpen`)
340
+ tier = state.applied?.tier ?? "open";
341
+ networkOff = state.applied?.egress === "none";
342
+ }
343
+ }
344
+ else if (unattended !== null && !unattended.openGranted) {
345
+ tier = minTier(tier, "dev");
346
+ }
347
+ return {
348
+ tier,
349
+ isolation: tier === "open" ? s.isolation : "sandbox",
350
+ ...(networkOff && { network: "none" }),
351
+ ...(row.source === "local" &&
352
+ summary !== null && { localhost: summary.localhost, unixSockets: summary.unixSockets }),
353
+ ...(unattended !== null && { unattended: true }),
354
+ };
355
+ }
356
+ /** Renames the device locked by {@link lockOwn} (`before`); see {@link rename}. */
357
+ async renameLocked(orgId, accountId, id, before, name, actorTokenId, auditDetail) {
239
358
  if (before.name === name)
240
359
  return;
241
360
  // the same name in another case is this device's own: the index allows it
@@ -310,6 +429,55 @@ let DevicesService = class DevicesService {
310
429
  async revokeMachine(orgId, accountId, id, actorTokenId) {
311
430
  await this.machines.revoke(orgId, accountId, id, actorTokenId);
312
431
  }
432
+ /**
433
+ * The owner changes one of their own devices' local policy (network ask-to-allow spec §7, §13
434
+ * Q6; sandbox dev tiers spec §7.3): sent to the machine with the `policy` op, which is the
435
+ * authority and answers with the policy as written. Anyone but the owner gets
436
+ * `device.not_found`. A `tier` above the device's ceiling → `device.tier_not_allowed`. A widening
437
+ * change (`add`; a `tier` that may raise the machine's, judged as the machine judges it) is
438
+ * refused up front with `device.policy_refused` when the machine last reported
439
+ * `remoteGrants: "off"` (otherwise the machine decides); narrowing is always sent, and
440
+ * `isolation` is only ever `sandbox`. The machine offline → `device.offline`; a cloud device or a
441
+ * program without the `policyOp` feature → `device.policy_unsupported`; the machine's refusal →
442
+ * `device.policy_refused` with its message. Callers are human sessions only (no personal access
443
+ * token, no service token, no Platform op).
444
+ */
445
+ async changePolicy(orgId, account, id, change) {
446
+ await this.access.usable(orgId, account.id, id);
447
+ if (change.tier !== undefined && change.tier !== null) {
448
+ const policy = await this.policyFor(orgId, change.tier);
449
+ if (!tierAtMost(change.tier, policy.ceiling))
450
+ throw tierNotAllowed(change.tier, policy);
451
+ }
452
+ const summary = await this.policies.summary(id);
453
+ const known = summary?.policy ?? null;
454
+ const widens = (change.add?.length ?? 0) > 0 || (known !== null && tierWidens(known.tier, change.tier));
455
+ // only a known "off" is refused here; an unknown summary, a machine that cannot take the op
456
+ // or none at all are left to apply (the machine decides, or unsupported / offline)
457
+ const canApply = summary?.features.includes("policyOp") === true;
458
+ if (widens && canApply && known?.remoteGrants === "off") {
459
+ throw new ApiError("device.policy_refused", remoteGrantsOffMessage(change));
460
+ }
461
+ const out = await this.policies.apply({
462
+ orgId,
463
+ deviceId: id,
464
+ change: {
465
+ ...(change.add !== undefined && { add: change.add }),
466
+ ...(change.remove !== undefined && { remove: change.remove }),
467
+ ...(change.tier !== undefined && { tier: change.tier }),
468
+ ...(change.isolation !== undefined && { isolation: change.isolation }),
469
+ },
470
+ source: { accountId: account.id, accountName: account.displayName, via: "web" },
471
+ });
472
+ if (out.applied)
473
+ return out.policy;
474
+ if (out.reason === "offline")
475
+ throw new ApiError("device.offline", out.message);
476
+ if (out.reason === "unsupported") {
477
+ throw new ApiError("device.policy_unsupported", out.message);
478
+ }
479
+ throw new ApiError("device.policy_refused", out.message);
480
+ }
313
481
  /**
314
482
  * Starts one of the person's devices over with empty files (after
315
483
  * `device.files_lost`): records the request (audit), then, after the commit, enqueues the
@@ -590,6 +758,7 @@ let DevicesService = class DevicesService {
590
758
  "e.source",
591
759
  "e.provider",
592
760
  "e.demoted_from_provider",
761
+ "e.tier",
593
762
  "e.created_at",
594
763
  "m.id as machine_id",
595
764
  "m.name as machine_name",
@@ -598,6 +767,8 @@ let DevicesService = class DevicesService {
598
767
  "m.program_version as machine_program_version",
599
768
  "m.last_seen_at as machine_last_seen_at",
600
769
  "m.session_isolation as machine_session_isolation",
770
+ "m.features as machine_features",
771
+ "m.local_policy as machine_local_policy",
601
772
  "i.provider as instance_provider",
602
773
  "i.fs_status",
603
774
  "i.handle",
@@ -636,10 +807,23 @@ let DevicesService = class DevicesService {
636
807
  const demotedProviders = [
637
808
  ...new Set(rows.flatMap((r) => (demoted.get(r.id) ? [demoted.get(r.id)] : []))),
638
809
  ];
639
- const [limits, names] = await Promise.all([
640
- this.sandbox.orgLimits(orgId),
810
+ const [names, recent, settings] = await Promise.all([
641
811
  this.displayNames.many(demotedProviders),
812
+ this.recentSandboxChanges(orgId, rows.map((r) => r.id)),
813
+ readOrgSettings(this.db.db, orgId),
642
814
  ]);
815
+ const policyOf = (tier) => effectiveSandboxPolicy(this.config.get().sandbox, settings, tier);
816
+ // each device's limits (a cloud device's own tier) and its instance's pending change
817
+ const perDevice = new Map(await Promise.all(rows.map(async (r) => {
818
+ const policy = policyOf(r.source === "cloud" ? r.tier : null);
819
+ const [limits, state] = await Promise.all([
820
+ this.sandbox.deviceLimits({ orgId, deviceId: r.id }, { policy }),
821
+ r.source === "cloud"
822
+ ? this.sandbox.limitsState({ orgId, deviceId: r.id }, { policy })
823
+ : { pendingNextUse: false, pendingFilesReset: false },
824
+ ]);
825
+ return [r.id, { limits, state }];
826
+ })));
643
827
  const current = this.providers.cloud()?.plugin;
644
828
  // a local device is online while a replica holds its machine's connection lease
645
829
  const online = new Map(await Promise.all(rows
@@ -677,6 +861,23 @@ let DevicesService = class DevicesService {
677
861
  lastSeenAt: r.machine_last_seen_at?.toISOString() ?? null,
678
862
  }
679
863
  : null,
864
+ network: r.source === "local" && r.machine_id !== null
865
+ ? {
866
+ features: deviceFeaturesOf(r.machine_features),
867
+ policy: localPolicyOf(r.machine_local_policy),
868
+ }
869
+ : null,
870
+ sandbox: {
871
+ ...sandboxStateOf(policyOf, {
872
+ source: r.source,
873
+ tier: r.tier,
874
+ features: r.machine_id === null ? [] : deviceFeaturesOf(r.machine_features),
875
+ policy: r.machine_id === null ? null : localPolicyOf(r.machine_local_policy),
876
+ }),
877
+ pendingNextUse: perDevice.get(r.id)?.state.pendingNextUse ?? false,
878
+ pendingFilesReset: perDevice.get(r.id)?.state.pendingFilesReset ?? false,
879
+ recent: recent.get(r.id) ?? [],
880
+ },
680
881
  // a local device's machine decides it; cloud devices have none in phase 2
681
882
  sessionIsolation: r.machine_session_isolation ?? false,
682
883
  filesLost: r.fs_status === "lost",
@@ -686,11 +887,49 @@ let DevicesService = class DevicesService {
686
887
  lastUsedAt: r.last_used_at.toISOString(),
687
888
  }
688
889
  : null,
689
- limits: { ...limits, allowHosts: [...limits.allowHosts] },
890
+ limits: limitsView(perDevice.get(r.id)?.limits),
690
891
  createdAt: r.created_at.toISOString(),
691
892
  };
692
893
  });
693
894
  }
895
+ /**
896
+ * The last {@link RECENT_POLICY_CHANGES} sandbox changes of each device in the org (audit rows
897
+ * `device.policy_changed`, `device.tier_changed`, `device.isolation_reported`), newest first,
898
+ * with the deciding account's display name.
899
+ */
900
+ async recentSandboxChanges(orgId, deviceIds) {
901
+ const out = new Map();
902
+ if (deviceIds.length === 0)
903
+ return out;
904
+ const ranked = this.db
905
+ .selectFrom("audit_events as a")
906
+ .select([
907
+ "a.target",
908
+ "a.action",
909
+ "a.at",
910
+ "a.detail",
911
+ "a.actor_account_id",
912
+ sql `row_number() OVER (PARTITION BY a.target ORDER BY a.at DESC, a.id DESC)`.as("n"),
913
+ ])
914
+ .where("a.org_id", "=", orgId)
915
+ .where("a.action", "in", [...SANDBOX_CHANGE_ACTIONS])
916
+ .where("a.target", "in", deviceIds);
917
+ const rows = await this.db
918
+ .selectFrom(ranked.as("r"))
919
+ .leftJoin("accounts as ac", "ac.id", "r.actor_account_id")
920
+ .select(["r.target", "r.action", "r.at", "r.detail", "ac.display_name"])
921
+ .where("r.n", "<=", RECENT_POLICY_CHANGES)
922
+ .orderBy("r.at", "desc")
923
+ .execute();
924
+ for (const r of rows) {
925
+ if (r.target === null)
926
+ continue;
927
+ const list = out.get(r.target) ?? [];
928
+ list.push(sandboxChangeOf(r.action, r.at, (r.detail ?? {}), r.display_name ?? null));
929
+ out.set(r.target, list);
930
+ }
931
+ return out;
932
+ }
694
933
  /**
695
934
  * Serializes the person's device writes in the org: the org row (`FOR SHARE`), then their
696
935
  * membership row (`FOR NO KEY UPDATE`, or `FOR SHARE` with `mode` share: enough to keep the
@@ -722,7 +961,7 @@ let DevicesService = class DevicesService {
722
961
  await this.lockMember(orgId, accountId);
723
962
  const row = await this.db
724
963
  .selectFrom("devices")
725
- .select(["name", "is_default"])
964
+ .select(["name", "is_default", "source", "tier"])
726
965
  .where("id", "=", id)
727
966
  .where("org_id", "=", orgId)
728
967
  .where("owner_kind", "=", "personal")
@@ -756,6 +995,9 @@ __decorate([
756
995
  __decorate([
757
996
  Transactional()
758
997
  ], DevicesService.prototype, "renameIn", null);
998
+ __decorate([
999
+ Transactional()
1000
+ ], DevicesService.prototype, "updateIn", null);
759
1001
  __decorate([
760
1002
  Transactional()
761
1003
  ], DevicesService.prototype, "remove", null);
@@ -777,7 +1019,8 @@ DevicesService = __decorate([
777
1019
  __param(7, Inject(ExtensionDisplayNames)),
778
1020
  __param(8, Inject(KERNEL_LOGGER)),
779
1021
  __param(9, Inject(LockService)),
780
- __param(10, Inject(MachinesService))
1022
+ __param(10, Inject(MachinesService)),
1023
+ __param(11, Inject(DevicePolicyService))
781
1024
  ], DevicesService);
782
1025
  export { DevicesService };
783
1026
  /**
@@ -789,3 +1032,95 @@ function refuseDeviceReset(env) {
789
1032
  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
1033
  }
791
1034
  }
1035
+ /** A copy of `l` for a view (its lists not shared). */
1036
+ const limitsView = (l) => ({
1037
+ ...l,
1038
+ allowHosts: [...l.allowHosts],
1039
+ devHosts: [...l.devHosts],
1040
+ });
1041
+ /** The audit actions a device's recent sandbox changes list. */
1042
+ const SANDBOX_CHANGE_ACTIONS = [
1043
+ "device.policy_changed",
1044
+ "device.tier_changed",
1045
+ "device.isolation_reported",
1046
+ ];
1047
+ const tierOrNull = (v) => v === "strict" || v === "dev" || v === "open" ? v : null;
1048
+ /** One audit row of {@link SANDBOX_CHANGE_ACTIONS} as a {@link DeviceSandboxChangeView}. */
1049
+ function sandboxChangeOf(action, at, d, by) {
1050
+ const hosts = (v) => Array.isArray(v) ? v.filter((h) => typeof h === "string") : [];
1051
+ const isolation = d.isolation === "sandbox" || d.isolation === "none" ? d.isolation : null;
1052
+ const base = { at: at.toISOString(), add: [], remove: [], tier: null, isolation: null, by };
1053
+ if (action === "device.tier_changed") {
1054
+ return { ...base, kind: "tier", tier: { to: tierOrNull(d.to) }, via: "web", confirmed: true };
1055
+ }
1056
+ if (action === "device.isolation_reported") {
1057
+ return { ...base, kind: "isolation", isolation, via: "device", confirmed: true };
1058
+ }
1059
+ return {
1060
+ ...base,
1061
+ kind: "policy",
1062
+ add: hosts(d.add),
1063
+ remove: hosts(d.remove),
1064
+ // the key is there only when the change set the tier (null: follow the org's default)
1065
+ tier: "tier" in d ? { to: tierOrNull(d.tier) } : null,
1066
+ isolation,
1067
+ via: d.via === "im" ? "im" : "web",
1068
+ confirmed: d.confirmed === true,
1069
+ };
1070
+ }
1071
+ /**
1072
+ * A device's sandbox level (spec §4.2, §4.4): its chosen tier (a cloud device's stored one, a
1073
+ * local device's reported one) under the org's policy (`policyOf`). A local device whose program
1074
+ * lacks the `tiers` feature runs `strict` (it ignores the tier the kernel sends); it runs without
1075
+ * a sandbox only when it reports `isolation: "none"` and its effective tier is `open` (the device
1076
+ * keeps the sandbox at any lower tier).
1077
+ */
1078
+ export function sandboxStateOf(policyOf, d) {
1079
+ const chosen = d.source === "cloud" ? d.tier : (d.policy?.tier ?? null);
1080
+ const p = policyOf(chosen);
1081
+ const tiersKnown = d.source === "cloud" || d.features.includes("tiers");
1082
+ const effectiveTier = tiersKnown ? p.tier : "strict";
1083
+ return {
1084
+ tier: chosen,
1085
+ effectiveTier,
1086
+ ceiling: p.ceiling,
1087
+ cappedBy: tiersKnown ? p.cappedBy : null,
1088
+ defaultTier: p.defaultTier,
1089
+ allowUnsandboxed: p.allowUnsandboxed,
1090
+ isolation: d.source === "local" && d.policy?.isolation === "none" && effectiveTier === "open"
1091
+ ? "none"
1092
+ : "sandbox",
1093
+ };
1094
+ }
1095
+ /**
1096
+ * Whether setting a local device's tier to `next` (null: follow the org's default) may raise it,
1097
+ * not knowing the org's default at the machine: a tier above the reported one, any tier but
1098
+ * `strict` when it reports none, and following the default when it reports `strict` or `dev`.
1099
+ */
1100
+ export function tierWidens(reported, next) {
1101
+ if (next === undefined || next === "strict")
1102
+ return false;
1103
+ if (next === null)
1104
+ return reported !== undefined && reported !== "open";
1105
+ return reported === undefined || !tierAtMost(next, reported);
1106
+ }
1107
+ /** A tier above the device's ceiling: which ceiling, and the highest tier allowed. */
1108
+ function tierNotAllowed(tier, policy) {
1109
+ const platform = policy.cappedBy === "platform";
1110
+ 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}.`);
1111
+ }
1112
+ /**
1113
+ * The refusal of a widening change on a device whose machine reported `remoteGrants: "off"`: the
1114
+ * command to run on the machine instead.
1115
+ */
1116
+ function remoteGrantsOffMessage(change) {
1117
+ const local = [
1118
+ ...(change.add !== undefined && change.add.length > 0
1119
+ ? [`capybara-device policy add allowedDomains ${change.add.join(",")}`]
1120
+ : []),
1121
+ ...(change.tier !== undefined
1122
+ ? [`capybara-device policy set tier ${change.tier ?? "default"}`]
1123
+ : []),
1124
+ ];
1125
+ return `This device does not accept widening changes from the deployment; change it on the machine: ${local.join("; ")}`;
1126
+ }
@@ -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 DeviceRenameBodyDto_base: import("../infra/validation/create-zod-dto.js").ZodDtoClass<z.ZodObject<{
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 DeviceRenameBodyDto extends DeviceRenameBodyDto_base {
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 {};
@@ -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 DeviceRenameBodyDto extends createZodDto(DeviceNameBody, "DeviceRenameBody") {
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;