@capybara-hq/kernel 0.1.0-rc.21 → 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 (242) hide show
  1. package/dist/agents/admin-agents.d.ts +16 -3
  2. package/dist/agents/admin-agents.js +65 -7
  3. package/dist/agents/agents.schemas.d.ts +24 -6
  4. package/dist/agents/agents.service.d.ts +4 -3
  5. package/dist/agents/agents.service.js +39 -26
  6. package/dist/agents/definition.d.ts +44 -6
  7. package/dist/agents/definition.js +86 -18
  8. package/dist/agents/from-template.d.ts +6 -3
  9. package/dist/agents/from-template.js +57 -44
  10. package/dist/agents/org-templates.js +97 -41
  11. package/dist/agents/templates.d.ts +6 -0
  12. package/dist/agents/templates.js +15 -1
  13. package/dist/catalog/admin-catalog.controller.d.ts +2 -4
  14. package/dist/catalog/admin-catalog.controller.js +2 -6
  15. package/dist/catalog/catalog.module.js +3 -0
  16. package/dist/catalog/claude/report.d.ts +9 -2
  17. package/dist/catalog/claude/report.js +1 -0
  18. package/dist/catalog/claude/translate.js +1 -1
  19. package/dist/catalog/component-promotion.js +1 -2
  20. package/dist/catalog/component-state.d.ts +13 -1
  21. package/dist/catalog/component-state.js +14 -5
  22. package/dist/catalog/marketplaces.service.d.ts +1 -1
  23. package/dist/catalog/marketplaces.service.js +2 -1
  24. package/dist/catalog/org-catalog.controller.d.ts +13 -3
  25. package/dist/catalog/org-catalog.controller.js +26 -4
  26. package/dist/catalog/org-catalog.schemas.d.ts +13 -0
  27. package/dist/catalog/org-catalog.schemas.js +34 -0
  28. package/dist/catalog/org-plugin-uploads.controller.js +0 -1
  29. package/dist/catalog/org-plugin-uploads.d.ts +0 -1
  30. package/dist/catalog/org-plugin-uploads.js +0 -3
  31. package/dist/catalog/org-plugin-uploads.schemas.d.ts +0 -1
  32. package/dist/catalog/org-plugin-uploads.schemas.js +0 -2
  33. package/dist/catalog/platform-components.d.ts +0 -1
  34. package/dist/catalog/platform-components.js +3 -8
  35. package/dist/catalog/plugin-import.d.ts +0 -8
  36. package/dist/catalog/plugin-import.js +4 -35
  37. package/dist/catalog/plugin-install.d.ts +2 -2
  38. package/dist/catalog/plugin-install.js +2 -2
  39. package/dist/catalog/plugin-package.d.ts +7 -0
  40. package/dist/catalog/plugin-package.js +27 -1
  41. package/dist/catalog/plugin-upgrade.d.ts +5 -6
  42. package/dist/catalog/plugin-upgrade.js +5 -23
  43. package/dist/catalog/requests.service.d.ts +66 -10
  44. package/dist/catalog/requests.service.js +196 -15
  45. package/dist/channels/actions.d.ts +2 -0
  46. package/dist/channels/actions.js +19 -4
  47. package/dist/channels/cards.d.ts +16 -2
  48. package/dist/channels/cards.js +40 -7
  49. package/dist/channels/channels.module.js +3 -0
  50. package/dist/channels/outbound.d.ts +22 -1
  51. package/dist/channels/outbound.js +116 -7
  52. package/dist/channels/phrases.d.ts +25 -0
  53. package/dist/channels/phrases.js +19 -0
  54. package/dist/channels/threads.d.ts +12 -0
  55. package/dist/channels/threads.js +34 -0
  56. package/dist/config/schema.d.ts +23 -1
  57. package/dist/config/schema.js +105 -5
  58. package/dist/conversations/conversations.service.d.ts +42 -9
  59. package/dist/conversations/conversations.service.js +57 -25
  60. package/dist/conversations/events.d.ts +106 -11
  61. package/dist/conversations/events.js +121 -18
  62. package/dist/conversations/message-context.d.ts +6 -3
  63. package/dist/conversations/message-context.js +14 -10
  64. package/dist/conversations/queue.d.ts +15 -2
  65. package/dist/conversations/queue.js +22 -3
  66. package/dist/conversations/queue.service.d.ts +12 -6
  67. package/dist/conversations/queue.service.js +36 -13
  68. package/dist/db/kernel-schema.d.ts +105 -3
  69. package/dist/devices/conversation-devices.d.ts +8 -1
  70. package/dist/devices/conversation-devices.js +8 -0
  71. package/dist/devices/devices.controller.d.ts +21 -4
  72. package/dist/devices/devices.controller.js +46 -8
  73. package/dist/devices/devices.schemas.d.ts +9 -1
  74. package/dist/devices/devices.schemas.js +11 -3
  75. package/dist/devices/devices.service.d.ts +176 -4
  76. package/dist/devices/devices.service.js +341 -6
  77. package/dist/devices/dto.d.ts +56 -3
  78. package/dist/devices/dto.js +51 -1
  79. package/dist/devices/public-suffix.d.ts +6 -0
  80. package/dist/devices/public-suffix.js +79 -0
  81. package/dist/errors.d.ts +46 -5
  82. package/dist/errors.js +46 -5
  83. package/dist/evals/reconstruct.d.ts +18 -13
  84. package/dist/evals/reconstruct.js +50 -36
  85. package/dist/evals/recorded-tools.d.ts +7 -7
  86. package/dist/evals/recorded-tools.js +4 -3
  87. package/dist/evals/trajectory.d.ts +6 -1
  88. package/dist/evals/trajectory.js +7 -0
  89. package/dist/extensions/package.js +2 -2
  90. package/dist/http/api.module.js +5 -0
  91. package/dist/inbox/inbox.module.js +2 -1
  92. package/dist/inbox/inbox.service.d.ts +19 -4
  93. package/dist/inbox/inbox.service.js +14 -3
  94. package/dist/infra/config/kernel-config.service.js +11 -0
  95. package/dist/infra/events/kernel-events.d.ts +2 -1
  96. package/dist/local-devices/device-policy.service.d.ts +65 -0
  97. package/dist/local-devices/device-policy.service.js +217 -0
  98. package/dist/local-devices/local-device-gateway.js +4 -1
  99. package/dist/local-devices/local-device-provider.d.ts +40 -3
  100. package/dist/local-devices/local-device-provider.js +109 -7
  101. package/dist/local-devices/local-devices.module.d.ts +2 -1
  102. package/dist/local-devices/local-devices.module.js +19 -3
  103. package/dist/local-devices/machines.d.ts +14 -3
  104. package/dist/local-devices/machines.js +41 -3
  105. package/dist/local-devices/network-grants.d.ts +104 -0
  106. package/dist/local-devices/network-grants.js +288 -0
  107. package/dist/local-devices/policy-summary.d.ts +20 -0
  108. package/dist/local-devices/policy-summary.js +63 -0
  109. package/dist/orgs/orgs.module.js +4 -10
  110. package/dist/orgs/roles.d.ts +1 -1
  111. package/dist/orgs/roles.js +4 -0
  112. package/dist/orgs/settings.controller.js +2 -1
  113. package/dist/orgs/settings.service.d.ts +164 -12
  114. package/dist/orgs/settings.service.js +212 -24
  115. package/dist/platform/exclusions.js +11 -1
  116. package/dist/platform/operation.js +4 -0
  117. package/dist/platform/ops/admin-items.ops.js +1 -1
  118. package/dist/platform/ops/agent-skills.ops.js +2 -2
  119. package/dist/platform/ops/agent-templates.ops.js +2 -2
  120. package/dist/platform/ops/agents.ops.js +11 -15
  121. package/dist/platform/ops/devices.ops.js +3 -1
  122. package/dist/platform/ops/inbox.ops.js +19 -0
  123. package/dist/platform/ops/org-settings.ops.js +14 -5
  124. package/dist/platform/ops/platform.ops.d.ts +9 -1
  125. package/dist/platform/ops/platform.ops.js +34 -12
  126. package/dist/platform/ops/requests.ops.js +3 -2
  127. package/dist/platform/ops/schedules.ops.d.ts +21 -0
  128. package/dist/platform/ops/schedules.ops.js +470 -0
  129. package/dist/platform/ops/skills.ops.js +3 -3
  130. package/dist/platform/platform-ops.module.d.ts +2 -1
  131. package/dist/platform/platform-ops.module.js +5 -0
  132. package/dist/platform/route-schemas.js +3 -0
  133. package/dist/roles/worker.module.js +3 -0
  134. package/dist/runtime/context/build.js +13 -4
  135. package/dist/runtime/context/compaction.js +6 -3
  136. package/dist/runtime/context/device-paths.d.ts +43 -21
  137. package/dist/runtime/context/device-paths.js +104 -21
  138. package/dist/runtime/context/run-context.d.ts +9 -0
  139. package/dist/runtime/context/run-context.js +8 -0
  140. package/dist/runtime/context/skills.d.ts +14 -1
  141. package/dist/runtime/context/skills.js +36 -5
  142. package/dist/runtime/maintenance.d.ts +4 -2
  143. package/dist/runtime/maintenance.js +16 -5
  144. package/dist/runtime/run-finish.d.ts +6 -1
  145. package/dist/runtime/run-finish.js +16 -2
  146. package/dist/runtime/run-queue.js +21 -2
  147. package/dist/runtime/run-worker.js +14 -33
  148. package/dist/runtime/runtime.module.js +3 -0
  149. package/dist/runtime/subagents/definition.d.ts +32 -14
  150. package/dist/runtime/subagents/definition.js +106 -109
  151. package/dist/runtime/subagents/dispatch.js +4 -3
  152. package/dist/runtime/subagents/preamble.d.ts +2 -1
  153. package/dist/runtime/subagents/preamble.js +2 -1
  154. package/dist/runtime/waits.js +14 -4
  155. package/dist/sandbox/sandbox.service.d.ts +120 -10
  156. package/dist/sandbox/sandbox.service.js +335 -28
  157. package/dist/sandbox/tiers.d.ts +86 -0
  158. package/dist/sandbox/tiers.js +96 -0
  159. package/dist/schedules/effective-settings.d.ts +16 -0
  160. package/dist/schedules/effective-settings.js +17 -0
  161. package/dist/schedules/errors.d.ts +8 -0
  162. package/dist/schedules/errors.js +8 -0
  163. package/dist/schedules/fire.d.ts +118 -0
  164. package/dist/schedules/fire.js +625 -0
  165. package/dist/schedules/grant-rules.d.ts +94 -0
  166. package/dist/schedules/grant-rules.js +126 -0
  167. package/dist/schedules/job-ids.d.ts +32 -0
  168. package/dist/schedules/job-ids.js +27 -0
  169. package/dist/schedules/pause.d.ts +9 -0
  170. package/dist/schedules/pause.js +33 -0
  171. package/dist/schedules/run-tracker.d.ts +81 -0
  172. package/dist/schedules/run-tracker.js +333 -0
  173. package/dist/schedules/scan.job.d.ts +64 -0
  174. package/dist/schedules/scan.job.js +303 -0
  175. package/dist/schedules/schedule-math.d.ts +31 -0
  176. package/dist/schedules/schedule-math.js +223 -0
  177. package/dist/schedules/scheduled-tasks.service.d.ts +351 -0
  178. package/dist/schedules/scheduled-tasks.service.js +1193 -0
  179. package/dist/schedules/schedules.controller.d.ts +68 -0
  180. package/dist/schedules/schedules.controller.js +242 -0
  181. package/dist/schedules/schedules.module.d.ts +16 -0
  182. package/dist/schedules/schedules.module.js +65 -0
  183. package/dist/schedules/schedules.schemas.d.ts +316 -0
  184. package/dist/schedules/schedules.schemas.js +186 -0
  185. package/dist/schedules/store.d.ts +81 -0
  186. package/dist/schedules/store.js +390 -0
  187. package/dist/schedules/store.module.d.ts +6 -0
  188. package/dist/schedules/store.module.js +21 -0
  189. package/dist/schedules/types.d.ts +108 -0
  190. package/dist/schedules/types.js +1 -0
  191. package/dist/schedules/unattended.d.ts +117 -0
  192. package/dist/schedules/unattended.js +228 -0
  193. package/dist/setup/oauth-token-probe.js +8 -4
  194. package/dist/skills/agent-skills.service.d.ts +4 -8
  195. package/dist/skills/agent-skills.service.js +18 -53
  196. package/dist/skills/catalog.d.ts +65 -62
  197. package/dist/skills/catalog.js +109 -206
  198. package/dist/skills/conversation-catalog.d.ts +3 -3
  199. package/dist/skills/conversation-catalog.js +5 -12
  200. package/dist/skills/invoke.d.ts +2 -2
  201. package/dist/skills/skills.controller.d.ts +3 -1
  202. package/dist/skills/skills.service.d.ts +38 -37
  203. package/dist/skills/skills.service.js +168 -107
  204. package/dist/skills/usable.d.ts +57 -8
  205. package/dist/skills/usable.js +172 -38
  206. package/dist/tools/approvals.controller.d.ts +4 -2
  207. package/dist/tools/approvals.controller.js +17 -2
  208. package/dist/tools/approvals.schemas.d.ts +1 -0
  209. package/dist/tools/approvals.schemas.js +3 -1
  210. package/dist/tools/approvals.service.d.ts +182 -7
  211. package/dist/tools/approvals.service.js +597 -13
  212. package/dist/tools/builtin-names.d.ts +25 -4
  213. package/dist/tools/builtin-names.js +45 -4
  214. package/dist/tools/builtins/builtin-tools.d.ts +8 -0
  215. package/dist/tools/builtins/builtin-tools.js +23 -2
  216. package/dist/tools/builtins/sandbox/bash.js +16 -7
  217. package/dist/tools/builtins/sandbox/common.d.ts +1 -1
  218. package/dist/tools/builtins/sandbox/common.js +33 -10
  219. package/dist/tools/builtins/sandbox/edit.js +7 -3
  220. package/dist/tools/builtins/sandbox/file-state.d.ts +31 -1
  221. package/dist/tools/builtins/sandbox/file-state.js +33 -3
  222. package/dist/tools/builtins/sandbox/write.js +8 -4
  223. package/dist/tools/builtins/schedules.d.ts +107 -0
  224. package/dist/tools/builtins/schedules.js +789 -0
  225. package/dist/tools/elicitation.d.ts +2 -0
  226. package/dist/tools/elicitation.js +1 -1
  227. package/dist/tools/fixed-ask.d.ts +7 -0
  228. package/dist/tools/fixed-ask.js +9 -0
  229. package/dist/tools/network-asks.d.ts +109 -0
  230. package/dist/tools/network-asks.js +221 -0
  231. package/dist/tools/pipeline.d.ts +53 -6
  232. package/dist/tools/pipeline.js +202 -13
  233. package/dist/tools/policy.d.ts +21 -8
  234. package/dist/tools/policy.js +63 -9
  235. package/dist/tools/registry.d.ts +10 -1
  236. package/dist/tools/tools.module.js +12 -0
  237. package/package.json +2 -2
  238. package/src/migrations/0113_scheduled_tasks.sql +108 -0
  239. package/src/migrations/0114_network_asks.sql +23 -0
  240. package/src/migrations/0115_network_requests.sql +28 -0
  241. package/src/migrations/0116_skills_default_on.sql +37 -0
  242. package/src/migrations/0117_sandbox_tiers.sql +11 -0
@@ -7,9 +7,14 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
+ import { minTier, ToolError, UNSANDBOXED_NOT_ALLOWED_MESSAGE, } from "@capybara-hq/plugin-sdk";
10
11
  import { Inject, Injectable } from "@nestjs/common";
12
+ import { ApiError } from "../errors.js";
11
13
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
14
+ import { KERNEL_LOGGER } from "../infra/tokens.js";
12
15
  import { deviceNotFound, SandboxService } from "../sandbox/sandbox.service.js";
16
+ import { TIER_OPEN_GRANT_KEY, UNATTENDED_UNSANDBOXED_ERROR } from "../schedules/unattended.js";
17
+ import { grantsWithin, NetworkGrants, relayer } from "./network-grants.js";
13
18
  import { deviceOffline, LocalDeviceRelay } from "./relay.js";
14
19
  import { DeviceTargets, sameRoots } from "./targets.js";
15
20
  /** Extra time the machine gets beyond a command's own timeout (spawn, output, the trip back). */
@@ -25,6 +30,16 @@ export const FRAME_ENVELOPE_BYTES = 64 * 1024;
25
30
  /** The most raw bytes whose base64 plus {@link FRAME_ENVELOPE_BYTES} fit in `maxFrameBytes`. */
26
31
  export const maxReadBytes = (maxFrameBytes) => Math.max(0, Math.floor((maxFrameBytes - FRAME_ENVELOPE_BYTES) / 4) * 3);
27
32
  const NAME_PREFIX = "cap-device-";
33
+ /**
34
+ * The tool error of a device that runs commands without a sandbox the org does not allow (§4.3);
35
+ * also what a machine answers when it refuses such a command itself.
36
+ */
37
+ export const UNSANDBOXED_NOT_ALLOWED_ERROR = UNSANDBOXED_NOT_ALLOWED_MESSAGE;
38
+ /**
39
+ * The relay's error for a machine that refused a command with {@link UNSANDBOXED_NOT_ALLOWED_MESSAGE}
40
+ * (a `policy` error: the relay prefixes the machine's message, `relay.ts`).
41
+ */
42
+ const MACHINE_UNSANDBOXED_REFUSAL = `Refused by the machine's local policy: ${UNSANDBOXED_NOT_ALLOWED_MESSAGE}`;
28
43
  /** The device id of a `cap-device-<deviceId>` name; undefined for any other name. */
29
44
  export const deviceIdOfName = (name) => name.startsWith(NAME_PREFIX) && name.length > NAME_PREFIX.length
30
45
  ? name.slice(NAME_PREFIX.length)
@@ -46,13 +61,17 @@ let LocalDeviceProvider = class LocalDeviceProvider {
46
61
  targets;
47
62
  sandbox;
48
63
  config;
64
+ grants;
65
+ log;
49
66
  concurrentExec = true;
50
67
  sessionIsolation = false;
51
- constructor(relay, targets, sandbox, config) {
68
+ constructor(relay, targets, sandbox, config, grants, log) {
52
69
  this.relay = relay;
53
70
  this.targets = targets;
54
71
  this.sandbox = sandbox;
55
72
  this.config = config;
73
+ this.grants = grants;
74
+ this.log = log;
56
75
  }
57
76
  async createFileSystem(args) {
58
77
  args.signal.throwIfAborted();
@@ -103,13 +122,36 @@ let LocalDeviceProvider = class LocalDeviceProvider {
103
122
  async killTagged(h, tags, signal) {
104
123
  await this.relay.call(h.id, "killTagged", { tags }, { deadlineMs: OP_DEADLINE_MS, signal });
105
124
  }
125
+ /**
126
+ * Runs the command on the machine with the org's limits now (ruling R27) and, for a command of a
127
+ * run, the hosts the kernel allows that run on this device beyond the local policy (network
128
+ * ask-to-allow spec §6.3; only those the org's limits admit, sent only when there are some). The device's report of refused hosts is re-layered
129
+ * against the limits this exec carried: the kernel never takes the device's word that the org
130
+ * allows a host (spec §4.1).
131
+ *
132
+ * The limits are the device's **ceiling** (sandbox dev tiers spec §4.2): `limits.tier` is the
133
+ * highest tier the org allows, its network that tier's, `defaultTier` the org's default (the
134
+ * device runs at `min(local tier ?? defaultTier, tier)`) and `devHosts` the dev hosts. A run of
135
+ * a scheduled task without a person and without the task's `tier:open` grant is capped at `dev`
136
+ * (spec §11.4). Before relaying, a machine that last reported running commands without a sandbox
137
+ * is refused when the org does not allow it, or for such a run (`device.unsandboxed_not_allowed`,
138
+ * spec §4.3). The kernel knows the local policy only from the last hello or policy op, so the
139
+ * same decision also travels in `limits.allowUnsandboxed` and the machine enforces it itself;
140
+ * its refusal ({@link UNSANDBOXED_NOT_ALLOWED_MESSAGE}) becomes the same error.
141
+ */
106
142
  async exec(h, args) {
107
143
  const { signal, ...rest } = args;
108
144
  signal.throwIfAborted();
109
- const limits = await this.currentLimits(h.id);
145
+ const runId = rest.actor?.runId;
146
+ const { orgId, limits, cap } = await this.currentLimits(h.id, runId);
147
+ // the run's grants for this device, cut to what the org allows now
148
+ const grants = runId !== undefined
149
+ ? grantsWithin(await this.grants.forExec({ orgId, runId, deviceId: h.id }), limits)
150
+ : undefined;
110
151
  signal.throwIfAborted();
152
+ let result;
111
153
  try {
112
- return await this.relay.call(h.id, "exec", { ...rest, limits }, { deadlineMs: args.timeoutMs + EXEC_DEADLINE_MARGIN_MS, signal });
154
+ result = await this.relay.call(h.id, "exec", { ...rest, limits, ...(grants !== undefined && { grants }) }, { deadlineMs: args.timeoutMs + EXEC_DEADLINE_MARGIN_MS, signal });
113
155
  }
114
156
  catch (e) {
115
157
  // the contract: an exec aborted after it was sent resolves with exitCode null (the relay
@@ -117,8 +159,19 @@ let LocalDeviceProvider = class LocalDeviceProvider {
117
159
  if (signal.aborted && e === signal.reason) {
118
160
  return { exitCode: null, stdout: "", stderr: "", truncated: false, timedOut: false };
119
161
  }
162
+ // the machine refused to run it unsandboxed (its local policy changed since it reported)
163
+ if (e instanceof ToolError && e.message === MACHINE_UNSANDBOXED_REFUSAL) {
164
+ this.log.warn("the machine refused a command without a sandbox", {
165
+ deviceId: h.id,
166
+ reason: cap !== undefined ? "unattended" : "not_allowed",
167
+ });
168
+ throw await this.unsandboxedRefusal(h.id, cap);
169
+ }
120
170
  throw e;
121
171
  }
172
+ if (result.network?.blocked === undefined)
173
+ return result;
174
+ return { ...result, network: { blocked: relayer(result.network.blocked, limits) } };
122
175
  }
123
176
  async readFile(h, path, opts) {
124
177
  // the answer must fit one frame: a larger file comes back truncated (Deliver: too large)
@@ -133,12 +186,59 @@ let LocalDeviceProvider = class LocalDeviceProvider {
133
186
  async stat(h, path, signal) {
134
187
  return this.relay.call(h.id, "stat", { path }, { deadlineMs: OP_DEADLINE_MS, signal });
135
188
  }
136
- /** The org's effective limits now (ruling R27); a deleted device is not found. */
137
- async currentLimits(deviceId) {
189
+ /**
190
+ * The device's org and the limits of one exec now (ruling R27): the ceiling, capped at `dev` for
191
+ * an unattended run without the `tier:open` grant (`cap`, set only when the platform and the org
192
+ * would otherwise allow an unsandboxed command); `allowUnsandboxed` whether this command may run
193
+ * without a sandbox. A deleted device is not found. Refuses an unsandboxed machine the org does
194
+ * not allow (or such a run), see {@link exec}.
195
+ */
196
+ async currentLimits(deviceId, runId) {
138
197
  const orgId = await this.targets.orgOf(deviceId);
139
198
  if (orgId === null)
140
199
  throw deviceNotFound();
141
- return this.sandbox.orgLimits(orgId);
200
+ const [policy, unattended, unsandboxed] = await Promise.all([
201
+ this.sandbox.devicePolicy({ orgId, deviceId }),
202
+ runId === undefined ? null : this.grants.unattendedTier({ runId, deviceId }),
203
+ this.grants.unsandboxed(deviceId),
204
+ ]);
205
+ const capped = unattended !== null && !unattended.openGranted;
206
+ // what an unsandboxed refusal reports: the run's missing grant only when it alone refused
207
+ const cap = capped && policy.allowUnsandboxed ? { taskRunId: unattended.taskRunId } : undefined;
208
+ if (unsandboxed && (!policy.allowUnsandboxed || capped)) {
209
+ this.log.warn("command refused on a device that runs without a sandbox", {
210
+ deviceId,
211
+ reason: policy.allowUnsandboxed ? "unattended" : "not_allowed",
212
+ });
213
+ throw await this.unsandboxedRefusal(deviceId, cap);
214
+ }
215
+ const tier = capped ? minTier(policy.ceiling, "dev") : policy.ceiling;
216
+ const limits = await this.sandbox.deviceLimits({ orgId, deviceId }, { policy, tier });
217
+ return {
218
+ orgId,
219
+ limits: {
220
+ ...limits,
221
+ defaultTier: minTier(policy.defaultTier, tier),
222
+ // the platform and the org allow it (which needs an open ceiling) and no cap applies
223
+ allowUnsandboxed: policy.allowUnsandboxed && !capped,
224
+ },
225
+ cap,
226
+ };
227
+ }
228
+ /**
229
+ * The `device.unsandboxed_not_allowed` error (spec §4.3); for an unattended run refused only for
230
+ * its missing `tier:open` grant, the run's text, with the missing grant recorded on the task run.
231
+ */
232
+ async unsandboxedRefusal(deviceId, cap) {
233
+ if (cap === undefined) {
234
+ return new ApiError("device.unsandboxed_not_allowed", UNSANDBOXED_NOT_ALLOWED_ERROR);
235
+ }
236
+ await this.grants
237
+ .recordMissing(cap.taskRunId, TIER_OPEN_GRANT_KEY)
238
+ .catch((err) => this.log.warn("cannot record a missing task grant", { deviceId, err }));
239
+ return new ApiError("device.unsandboxed_not_allowed", UNATTENDED_UNSANDBOXED_ERROR, [
240
+ { reason: "unattended" },
241
+ ]);
142
242
  }
143
243
  };
144
244
  LocalDeviceProvider = __decorate([
@@ -146,6 +246,8 @@ LocalDeviceProvider = __decorate([
146
246
  __param(0, Inject(LocalDeviceRelay)),
147
247
  __param(1, Inject(DeviceTargets)),
148
248
  __param(2, Inject(SandboxService)),
149
- __param(3, Inject(KernelConfigService))
249
+ __param(3, Inject(KernelConfigService)),
250
+ __param(4, Inject(NetworkGrants)),
251
+ __param(5, Inject(KERNEL_LOGGER))
150
252
  ], LocalDeviceProvider);
151
253
  export { LocalDeviceProvider };
@@ -4,7 +4,8 @@ import { LocalDeviceProvider } from "./local-device-provider.js";
4
4
  /**
5
5
  * Connected machines (environments spec §6, §13): the caller side of the device relay and the
6
6
  * kernel-internal `@local` sandbox provider, registered with {@link SandboxProviders} on init, and
7
- * the machines and their credentials ({@link MachinesService}).
7
+ * the machines and their credentials ({@link MachinesService}), changes to a device's network
8
+ * policy ({@link DevicePolicyService}) and the run grants sent with each exec ({@link NetworkGrants}).
8
9
  * Imported on every role: any replica may run a sandbox operation on a local device
9
10
  * (the socket itself is held by the gateway role).
10
11
  */
@@ -10,14 +10,17 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
10
10
  import { Inject, Module } from "@nestjs/common";
11
11
  import { LOCAL_PROVIDER, SandboxProviders } from "../sandbox/providers.js";
12
12
  import { SandboxModule } from "../sandbox/sandbox.module.js";
13
+ import { DevicePolicyService } from "./device-policy.service.js";
13
14
  import { LocalDeviceProvider } from "./local-device-provider.js";
14
15
  import { MachinesService } from "./machines.js";
16
+ import { NetworkGrants } from "./network-grants.js";
15
17
  import { LocalDeviceRelay } from "./relay.js";
16
18
  import { DeviceTargets } from "./targets.js";
17
19
  /**
18
20
  * Connected machines (environments spec §6, §13): the caller side of the device relay and the
19
21
  * kernel-internal `@local` sandbox provider, registered with {@link SandboxProviders} on init, and
20
- * the machines and their credentials ({@link MachinesService}).
22
+ * the machines and their credentials ({@link MachinesService}), changes to a device's network
23
+ * policy ({@link DevicePolicyService}) and the run grants sent with each exec ({@link NetworkGrants}).
21
24
  * Imported on every role: any replica may run a sandbox operation on a local device
22
25
  * (the socket itself is held by the gateway role).
23
26
  */
@@ -35,8 +38,21 @@ let LocalDevicesModule = class LocalDevicesModule {
35
38
  LocalDevicesModule = __decorate([
36
39
  Module({
37
40
  imports: [SandboxModule],
38
- providers: [DeviceTargets, LocalDeviceRelay, LocalDeviceProvider, MachinesService],
39
- exports: [LocalDeviceRelay, LocalDeviceProvider, MachinesService],
41
+ providers: [
42
+ DeviceTargets,
43
+ LocalDeviceRelay,
44
+ LocalDeviceProvider,
45
+ MachinesService,
46
+ DevicePolicyService,
47
+ NetworkGrants,
48
+ ],
49
+ exports: [
50
+ LocalDeviceRelay,
51
+ LocalDeviceProvider,
52
+ MachinesService,
53
+ DevicePolicyService,
54
+ NetworkGrants,
55
+ ],
40
56
  }),
41
57
  __param(0, Inject(SandboxProviders)),
42
58
  __param(1, Inject(LocalDeviceProvider))
@@ -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(["device_id", "name", "os", "arch", "roots", "created_at"])
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;