@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
@@ -1,5 +1,6 @@
1
1
  import { CatalogRequestsService, type CatalogRequestView } from "../catalog/requests.service.js";
2
2
  import { KernelDb } from "../infra/database/kernel-db.js";
3
+ import { type ScheduleAttention, ScheduledTasksService } from "../schedules/scheduled-tasks.service.js";
3
4
  import { SkillChangesService, type SkillChangeView } from "../skills/changes.service.js";
4
5
  import { ApprovalsService, type ApprovalView } from "../tools/approvals.service.js";
5
6
  import type { OrgNoticeDetails } from "./org-notices.js";
@@ -40,11 +41,16 @@ export declare const INBOX_MAX_ITEMS = 100;
40
41
  * - `tool_approval`:当前用户能处理的待处理工具调用审批(与 `GET /approvals?status=pending` 同一视图);
41
42
  * - `skill_change`: pending skill share requests and proposed versions of shared skills, when the
42
43
  * current user is an org admin (`skillChange.kind`; spec §5.4, §5.5);
43
- * - `catalog_request`: pending `enable` requests from members and pending `connector_setup`
44
- * requests (`request.kind`; `request.agent` and `request.connectors` set, `item` null; ruling
45
- * P4-T6-5), when the current user is an org admin (M6 T26, C22; plugin scope spec §5.1);
44
+ * - `catalog_request`: pending `enable` and `network_allowlist` requests from members and pending
45
+ * `connector_setup` requests (`request.kind`; for `connector_setup` `request.agent` and
46
+ * `request.connectors` set, `item` null, ruling P4-T6-5; for `network_allowlist`
47
+ * `request.network` set, network ask-to-allow spec §5.3), when the current user is an org admin
48
+ * (M6 T26, C22; plugin scope spec §5.1);
46
49
  * - `catalog_request_decided`: the current user's own requests in this org that were approved or
47
50
  * rejected (with the reason), until dismissed (`POST catalog-requests/:id/dismiss`);
51
+ * - `schedule_attention`: the current user's own scheduled task that needs them (spec §8): paused
52
+ * for any reason but their own pause, its latest run failed, or its latest run was refused tools
53
+ * or connections; derived live, gone once the task is resumed or runs clean;
48
54
  * - the org notices (`skill_renamed`, `item_shared`, `shared_item_updated`, `upload_confirmed`,
49
55
  * `upload_rejected`; `type` = the notice's kind): for org admins, until one dismisses it
50
56
  * (`POST inbox/notices/:id/dismiss`).
@@ -70,12 +76,20 @@ export type InboxItem = {
70
76
  /** When it was decided. */
71
77
  createdAt: string;
72
78
  request: CatalogRequestView;
79
+ } | {
80
+ type: "schedule_attention";
81
+ /** `<taskId>:paused`, `<taskId>:failed` or `<taskId>:grants`. */
82
+ id: string;
83
+ createdAt: string;
84
+ attention: ScheduleAttentionView;
73
85
  } | {
74
86
  type: OrgNoticeView["kind"];
75
87
  id: string;
76
88
  createdAt: string;
77
89
  notice: OrgNoticeView;
78
90
  };
91
+ /** A scheduled task that needs its creator ({@link ScheduleAttention}, dates as ISO strings). */
92
+ export type ScheduleAttentionView = Omit<ScheduleAttention, "at">;
79
93
  /** `GET /inbox` 的结果:新的在前,最多 {@link INBOX_MAX_ITEMS} 项;`truncated` 为 true 表示还有更多。 */
80
94
  export interface InboxView {
81
95
  items: InboxItem[];
@@ -87,7 +101,8 @@ export declare class InboxService {
87
101
  private readonly skillChanges;
88
102
  private readonly kdb;
89
103
  private readonly catalogRequests;
90
- constructor(approvals: Pick<ApprovalsService, "list">, skillChanges: Pick<SkillChangesService, "listPending">, kdb: KernelDb, catalogRequests: Pick<CatalogRequestsService, "listForOrgAdmin" | "listDecidedForRequester">);
104
+ private readonly schedules;
105
+ constructor(approvals: Pick<ApprovalsService, "list">, skillChanges: Pick<SkillChangesService, "listPending">, kdb: KernelDb, catalogRequests: Pick<CatalogRequestsService, "listForOrgAdmin" | "listDecidedForRequester">, schedules: Pick<ScheduledTasksService, "attentionFor">);
91
106
  list(orgId: string, accountId: string): Promise<InboxView>;
92
107
  /** The org's open notices when the account is an org admin (else none), newest first. */
93
108
  private notices;
@@ -13,6 +13,7 @@ import { CATALOG_REQUESTS_LIST_MAX, CatalogRequestsService, } from "../catalog/r
13
13
  import { ApiError } from "../errors.js";
14
14
  import { KernelDb } from "../infra/database/kernel-db.js";
15
15
  import { roleAtLeast } from "../orgs/roles.js";
16
+ import { ScheduledTasksService, } from "../schedules/scheduled-tasks.service.js";
16
17
  import { SKILL_CHANGES_LIST_MAX, SkillChangesService, } from "../skills/changes.service.js";
17
18
  import { APPROVALS_LIST_MAX, ApprovalsService, } from "../tools/approvals.service.js";
18
19
  /** The most open org notices one inbox read returns. */
@@ -25,19 +26,22 @@ let InboxService = class InboxService {
25
26
  skillChanges;
26
27
  kdb;
27
28
  catalogRequests;
28
- constructor(approvals, skillChanges, kdb, catalogRequests) {
29
+ schedules;
30
+ constructor(approvals, skillChanges, kdb, catalogRequests, schedules) {
29
31
  this.approvals = approvals;
30
32
  this.skillChanges = skillChanges;
31
33
  this.kdb = kdb;
32
34
  this.catalogRequests = catalogRequests;
35
+ this.schedules = schedules;
33
36
  }
34
37
  async list(orgId, accountId) {
35
- const [approvals, changes, requests, decided, notices] = await Promise.all([
38
+ const [approvals, changes, requests, decided, notices, attention] = await Promise.all([
36
39
  this.approvals.list(orgId, accountId, "pending"),
37
40
  this.skillChanges.listPending(orgId, accountId),
38
41
  this.catalogRequests.listForOrgAdmin(orgId, accountId),
39
42
  this.catalogRequests.listDecidedForRequester(orgId, accountId),
40
43
  this.notices(orgId, accountId),
44
+ this.schedules.attentionFor(orgId, accountId),
41
45
  ]);
42
46
  const all = [
43
47
  ...approvals.map((a) => ({
@@ -64,6 +68,12 @@ let InboxService = class InboxService {
64
68
  createdAt: r.decidedAt ?? r.createdAt,
65
69
  request: r,
66
70
  })),
71
+ ...attention.map(({ at, ...view }) => ({
72
+ type: "schedule_attention",
73
+ id: `${view.taskId}:${view.reason === "missing_grants" ? "grants" : view.reason === "failed" ? "failed" : "paused"}`,
74
+ createdAt: at.toISOString(),
75
+ attention: view,
76
+ })),
67
77
  ...notices.map((n) => ({ type: n.kind, id: n.id, createdAt: n.createdAt, notice: n })),
68
78
  ];
69
79
  // 新的在前;同一时刻按 id 定序(结果稳定)
@@ -142,7 +152,8 @@ InboxService = __decorate([
142
152
  __param(0, Inject(ApprovalsService)),
143
153
  __param(1, Inject(SkillChangesService)),
144
154
  __param(2, Inject(KernelDb)),
145
- __param(3, Inject(CatalogRequestsService))
155
+ __param(3, Inject(CatalogRequestsService)),
156
+ __param(4, Inject(ScheduledTasksService))
146
157
  ], InboxService);
147
158
  export { InboxService };
148
159
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -33,6 +33,10 @@ const HOT_APPLIERS = {
33
33
  "models.embedTimeoutMs": () => { },
34
34
  // SandboxService reads the shared-lease cap at every acquisition.
35
35
  "sandbox.maxConcurrentCommands": () => { },
36
+ // Sandbox tiers are read at every device use, org settings read and Platform call.
37
+ "sandbox.tiers.max": () => { },
38
+ "sandbox.tiers.default": () => { },
39
+ "sandbox.tiers.allowUnsandboxed": () => { },
36
40
  // The devices service reads the personal limit at every create.
37
41
  "devices.maxPersonal": () => { },
38
42
  // The local device relay reads the reconnect grace at every disconnect and token response.
@@ -48,6 +52,13 @@ const HOT_APPLIERS = {
48
52
  "web.fetch.maxAnswerTokens": () => { },
49
53
  "web.fetch.cacheSeconds": () => { },
50
54
  "web.fetch.timeoutMs": () => { },
55
+ // Scheduled-task settings are clamped to these bounds on every read.
56
+ "schedules.minIntervalFloorMinutes": () => { },
57
+ "schedules.maxTasksPerMemberCap": () => { },
58
+ "schedules.maxTasksPerOrgCap": () => { },
59
+ "schedules.maxRunsPerHourCap": () => { },
60
+ // Run start reads the skills catalog budget when it records a snapshot.
61
+ "skills.catalogBudgetChars": () => { },
51
62
  };
52
63
  /**
53
64
  * Environment variables of renamed config keys, with their replacements: warned about once when
@@ -31,7 +31,8 @@ export interface KernelEventMap {
31
31
  orgId: string;
32
32
  conversationId: string;
33
33
  runId: string;
34
- status: "approved" | "denied";
34
+ /** `skipped`: a network ask closed by the run's starter's message (network ask-to-allow §6.7). */
35
+ status: "approved" | "denied" | "skipped";
35
36
  };
36
37
  /**
37
38
  * A person answered a tool call's input request, or skipped it with a message (after the commit,
@@ -0,0 +1,65 @@
1
+ import { type DeviceFeature, type DeviceLocalPolicySummary, type DevicePolicyArgs } from "@capybara-hq/plugin-sdk";
2
+ import { KernelDb } from "../infra/database/kernel-db.js";
3
+ import { TransactionHost } from "../infra/database/transaction-host.js";
4
+ import type { KernelLogger } from "../log.js";
5
+ import { MachinesService } from "./machines.js";
6
+ import { LocalDeviceRelay } from "./relay.js";
7
+ /** How long the machine may take to apply a policy change. */
8
+ export declare const POLICY_DEADLINE_MS = 30000;
9
+ /** What became of a policy change. */
10
+ export type PolicyChangeOutcome = {
11
+ applied: true;
12
+ policy: DeviceLocalPolicySummary;
13
+ } | {
14
+ applied: false;
15
+ reason: "offline" | "refused" | "unsupported";
16
+ message: string;
17
+ };
18
+ /**
19
+ * Changes a local device's policy on its machine (network ask-to-allow spec §5.2; sandbox dev
20
+ * tiers spec §7.3: its hosts, its `tier`, and `isolation` back to `sandbox`): the kernel→device
21
+ * `policy` op over the {@link LocalDeviceRelay}. The machine is the authority: it
22
+ * refuses widening unless its owner allows remote grants, validates every host and answers with
23
+ * the policy as written, which the kernel then keeps on the active machine's row (§5.4) and
24
+ * audits as `device.policy_changed`.
25
+ *
26
+ * The op is sent outside any transaction (a decision records its outcome afterwards, in its own
27
+ * transaction); only the summary write and the audit row are transactional. Callers decide who
28
+ * may change a device's policy (the device owner, in a signed-in session); this service checks
29
+ * only that the device is a live device of the org.
30
+ */
31
+ export declare class DevicePolicyService {
32
+ private readonly db;
33
+ readonly txHost: TransactionHost;
34
+ private readonly relay;
35
+ private readonly machines;
36
+ private readonly log;
37
+ constructor(db: KernelDb, txHost: TransactionHost, relay: Pick<LocalDeviceRelay, "call" | "holderLive">, machines: Pick<MachinesService, "activeFor">, log: Pick<KernelLogger, "warn">);
38
+ /**
39
+ * Sends the `policy` op (no transaction may be open), stores the returned summary and audits
40
+ * `device.policy_changed`. Never throws for an offline machine, a refusal or a program without
41
+ * the `policyOp` feature (nothing is sent to it); a device that is not a live device of `orgId`
42
+ * is `device.not_found`.
43
+ */
44
+ apply(input: {
45
+ orgId: string;
46
+ deviceId: string;
47
+ change: Omit<DevicePolicyArgs, "source">;
48
+ source: DevicePolicyArgs["source"];
49
+ actorTokenId?: string;
50
+ }): Promise<PolicyChangeOutcome>;
51
+ /** The active machine's features and policy summary (null when no machine is connected). */
52
+ summary(deviceId: string): Promise<{
53
+ features: DeviceFeature[];
54
+ policy: DeviceLocalPolicySummary | null;
55
+ } | null>;
56
+ /**
57
+ * Keeps the policy the machine wrote on its row and audits the change (`confirmed`); with no
58
+ * policy (the answer never came) only audits the attempt, unconfirmed. The detail's `isolation`
59
+ * is the one asked for, or the one the machine now reports when it changed (a tier change turns
60
+ * the sandbox back on).
61
+ */
62
+ private record;
63
+ /** The outcome of a failed op: offline, or the machine's refusal; a deleted device is thrown. */
64
+ private notApplied;
65
+ }
@@ -0,0 +1,217 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { ToolError, validateAllowHost, } from "@capybara-hq/plugin-sdk";
11
+ import { Inject, Injectable } from "@nestjs/common";
12
+ import { ApiError } from "../errors.js";
13
+ import { audit } from "../identity/audit.js";
14
+ import { KernelDb } from "../infra/database/kernel-db.js";
15
+ import { currentStore, TransactionHost } from "../infra/database/transaction-host.js";
16
+ import { Transactional } from "../infra/database/transactional.decorator.js";
17
+ import { KERNEL_LOGGER } from "../infra/tokens.js";
18
+ import { deviceNotFound } from "../sandbox/sandbox.service.js";
19
+ import { MachinesService } from "./machines.js";
20
+ import { isolationOf } from "./policy-summary.js";
21
+ import { LocalDeviceRelay, ON_MACHINE_PREFIX } from "./relay.js";
22
+ /** How long the machine may take to apply a policy change. */
23
+ export const POLICY_DEADLINE_MS = 30_000;
24
+ /** The prefix the relay puts before a machine's `policy`-kind refusal. */
25
+ const REFUSED_PREFIX = "Refused by the machine's local policy: ";
26
+ const OFFLINE_MESSAGE = "The device's machine is offline, so its policy was not changed. Start capybara-device on it.";
27
+ const UNSUPPORTED_MESSAGE = "The device's capybara-device program cannot change its policy from the platform. Update capybara-device on the machine.";
28
+ const errMsg = (e) => (e instanceof Error ? e.message : String(e));
29
+ /**
30
+ * Changes a local device's policy on its machine (network ask-to-allow spec §5.2; sandbox dev
31
+ * tiers spec §7.3: its hosts, its `tier`, and `isolation` back to `sandbox`): the kernel→device
32
+ * `policy` op over the {@link LocalDeviceRelay}. The machine is the authority: it
33
+ * refuses widening unless its owner allows remote grants, validates every host and answers with
34
+ * the policy as written, which the kernel then keeps on the active machine's row (§5.4) and
35
+ * audits as `device.policy_changed`.
36
+ *
37
+ * The op is sent outside any transaction (a decision records its outcome afterwards, in its own
38
+ * transaction); only the summary write and the audit row are transactional. Callers decide who
39
+ * may change a device's policy (the device owner, in a signed-in session); this service checks
40
+ * only that the device is a live device of the org.
41
+ */
42
+ let DevicePolicyService = class DevicePolicyService {
43
+ db;
44
+ txHost;
45
+ relay;
46
+ machines;
47
+ log;
48
+ constructor(db, txHost, relay, machines, log) {
49
+ this.db = db;
50
+ this.txHost = txHost;
51
+ this.relay = relay;
52
+ this.machines = machines;
53
+ this.log = log;
54
+ }
55
+ /**
56
+ * Sends the `policy` op (no transaction may be open), stores the returned summary and audits
57
+ * `device.policy_changed`. Never throws for an offline machine, a refusal or a program without
58
+ * the `policyOp` feature (nothing is sent to it); a device that is not a live device of `orgId`
59
+ * is `device.not_found`.
60
+ */
61
+ async apply(input) {
62
+ if (currentStore()) {
63
+ throw new Error("DevicePolicyService.apply() must not run inside a transaction: send the policy op " +
64
+ "first, then record the decision in a transaction.");
65
+ }
66
+ assertHosts(input.change, input.source.approvalId !== undefined);
67
+ const device = await this.db
68
+ .selectFrom("devices as e")
69
+ .innerJoin("orgs as o", "o.id", "e.org_id")
70
+ .select("e.source")
71
+ .where("e.id", "=", input.deviceId)
72
+ .where("e.org_id", "=", input.orgId)
73
+ .where("e.deleted_at", "is", null)
74
+ .where("o.deleted_at", "is", null)
75
+ .executeTakeFirst();
76
+ if (!device)
77
+ throw deviceNotFound();
78
+ if (device.source !== "local") {
79
+ return { applied: false, reason: "unsupported", message: UNSUPPORTED_MESSAGE };
80
+ }
81
+ const machine = await this.machines.activeFor(input.deviceId);
82
+ if (machine === null)
83
+ return { applied: false, reason: "offline", message: OFFLINE_MESSAGE };
84
+ if (!machine.features.includes("policyOp")) {
85
+ return { applied: false, reason: "unsupported", message: UNSUPPORTED_MESSAGE };
86
+ }
87
+ // not connected: nothing is sent (a queued op would wait for a reconnect nobody watches)
88
+ if (!(await this.relay.holderLive(input.deviceId))) {
89
+ return { applied: false, reason: "offline", message: OFFLINE_MESSAGE };
90
+ }
91
+ const args = { ...input.change, source: input.source };
92
+ let policy;
93
+ try {
94
+ ({ policy } = await this.relay.call(input.deviceId, "policy", args, {
95
+ deadlineMs: POLICY_DEADLINE_MS,
96
+ // the relay gives up by itself (deadline plus the reconnect grace)
97
+ signal: new AbortController().signal,
98
+ }));
99
+ }
100
+ catch (e) {
101
+ if (e instanceof ApiError && e.code === "device.offline") {
102
+ // the op was sent and the wait ran out: the machine may still apply it, so the attempt is
103
+ // audited unconfirmed; its next hello reports the policy as it is
104
+ await this.record({ ...input, machineId: machine.id, policy: null });
105
+ }
106
+ return this.notApplied(input.deviceId, e);
107
+ }
108
+ await this.record({ ...input, machineId: machine.id, policy });
109
+ return { applied: true, policy };
110
+ }
111
+ /** The active machine's features and policy summary (null when no machine is connected). */
112
+ async summary(deviceId) {
113
+ const m = await this.machines.activeFor(deviceId);
114
+ return m === null ? null : { features: m.features, policy: m.localPolicy };
115
+ }
116
+ /**
117
+ * Keeps the policy the machine wrote on its row and audits the change (`confirmed`); with no
118
+ * policy (the answer never came) only audits the attempt, unconfirmed. The detail's `isolation`
119
+ * is the one asked for, or the one the machine now reports when it changed (a tier change turns
120
+ * the sandbox back on).
121
+ */
122
+ async record(input) {
123
+ // the isolation the machine reports now, when it differs from the stored one: a tier change
124
+ // turns an unsandboxed device's sandbox back on without asking for it (spec §5.4, §10.4)
125
+ let isolationNow;
126
+ if (input.policy !== null) {
127
+ const prev = await this.db
128
+ .selectFrom("device_machines")
129
+ .select("local_policy")
130
+ .where("id", "=", input.machineId)
131
+ .where("revoked_at", "is", null)
132
+ .forUpdate()
133
+ .executeTakeFirst();
134
+ const before = isolationOf(prev?.local_policy) ?? "sandbox";
135
+ if (input.policy.isolation !== before)
136
+ isolationNow = input.policy.isolation;
137
+ await this.db
138
+ .updateTable("device_machines")
139
+ .set({ local_policy: JSON.stringify(input.policy) })
140
+ .where("id", "=", input.machineId)
141
+ .where("revoked_at", "is", null)
142
+ .execute();
143
+ }
144
+ const { change, source } = input;
145
+ await audit(this.db.db, {
146
+ actorAccountId: source.accountId,
147
+ ...(input.actorTokenId !== undefined && { actorTokenId: input.actorTokenId }),
148
+ orgId: input.orgId,
149
+ action: "device.policy_changed",
150
+ target: input.deviceId,
151
+ detail: {
152
+ add: change.add ?? [],
153
+ remove: change.remove ?? [],
154
+ // only when set (null: follow the org's default); the device page lists the change
155
+ ...(change.tier !== undefined && { tier: change.tier }),
156
+ ...((isolationNow ?? change.isolation) !== undefined && {
157
+ isolation: isolationNow ?? change.isolation,
158
+ }),
159
+ by: source.accountId,
160
+ via: source.via,
161
+ approvalId: source.approvalId ?? null,
162
+ confirmed: input.policy !== null,
163
+ },
164
+ });
165
+ }
166
+ /** The outcome of a failed op: offline, or the machine's refusal; a deleted device is thrown. */
167
+ notApplied(deviceId, e) {
168
+ if (e instanceof ApiError) {
169
+ if (e.code === "device.not_found")
170
+ throw e;
171
+ // device.offline, or device.forbidden (the machine was disconnected meanwhile)
172
+ return { applied: false, reason: "offline", message: OFFLINE_MESSAGE };
173
+ }
174
+ if (e instanceof ToolError && e.message.startsWith(REFUSED_PREFIX)) {
175
+ return { applied: false, reason: "refused", message: e.message.slice(REFUSED_PREFIX.length) };
176
+ }
177
+ // anything else the machine answered (its own error, an invalid result): not applied
178
+ this.log.warn("a device policy change failed on the machine", { deviceId, error: errMsg(e) });
179
+ const message = errMsg(e);
180
+ return {
181
+ applied: false,
182
+ reason: "refused",
183
+ message: message.startsWith(ON_MACHINE_PREFIX)
184
+ ? message.slice(ON_MACHINE_PREFIX.length)
185
+ : message,
186
+ };
187
+ }
188
+ };
189
+ __decorate([
190
+ Transactional()
191
+ ], DevicePolicyService.prototype, "record", null);
192
+ DevicePolicyService = __decorate([
193
+ Injectable(),
194
+ __param(0, Inject(KernelDb)),
195
+ __param(1, Inject(TransactionHost)),
196
+ __param(2, Inject(LocalDeviceRelay)),
197
+ __param(3, Inject(MachinesService)),
198
+ __param(4, Inject(KERNEL_LOGGER))
199
+ ], DevicePolicyService);
200
+ export { DevicePolicyService };
201
+ /**
202
+ * Hosts the platform may send: an approval's `add` takes exact hosts only (a card never grants a
203
+ * wildcard); the owner's own change on the device page (no approval) may add a wildcard (spec §7);
204
+ * `remove` takes any valid entry (narrowing, a wildcard too).
205
+ */
206
+ function assertHosts(change, fromApproval) {
207
+ const bad = (h) => typeof h !== "string" || validateAllowHost(h).length > 0;
208
+ for (const h of change.add ?? []) {
209
+ if (bad(h) || (fromApproval && h.startsWith("*."))) {
210
+ throw new Error(`not an exact host to add to a device policy: ${JSON.stringify(h)}`);
211
+ }
212
+ }
213
+ for (const h of change.remove ?? []) {
214
+ if (bad(h))
215
+ throw new Error(`not a host to remove from a device policy: ${JSON.stringify(h)}`);
216
+ }
217
+ }
@@ -177,7 +177,10 @@ class DeviceConnection {
177
177
  if (!(await this.claim()))
178
178
  return;
179
179
  try {
180
- await this.gw.machines.recordHello(this.identity.machineId, frame);
180
+ // the raw summary too: the frame parser drops one it cannot read whole, and its
181
+ // isolation must still reach the kernel
182
+ const rawPolicy = JSON.parse(text).policy;
183
+ await this.gw.machines.recordHello(this.identity.machineId, frame, rawPolicy);
181
184
  }
182
185
  catch (e) {
183
186
  this.gw.log.warn("could not record a device hello", {
@@ -1,6 +1,8 @@
1
- import type { ExecResult, FileSystemHandle, SandboxHandle, SandboxLimits, SandboxProvider, SandboxRoots } from "@capybara-hq/plugin-sdk";
1
+ import { type ExecResult, type FileSystemHandle, type SandboxHandle, type SandboxLimits, type SandboxProvider, type SandboxRoots } from "@capybara-hq/plugin-sdk";
2
2
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
3
+ import type { KernelLogger } from "../log.js";
3
4
  import { SandboxService } from "../sandbox/sandbox.service.js";
5
+ import { NetworkGrants } from "./network-grants.js";
4
6
  import { LocalDeviceRelay } from "./relay.js";
5
7
  import { DeviceTargets } from "./targets.js";
6
8
  /** Extra time the machine gets beyond a command's own timeout (spawn, output, the trip back). */
@@ -15,6 +17,11 @@ export declare const OP_DEADLINE_MS = 30000;
15
17
  export declare const FRAME_ENVELOPE_BYTES: number;
16
18
  /** The most raw bytes whose base64 plus {@link FRAME_ENVELOPE_BYTES} fit in `maxFrameBytes`. */
17
19
  export declare const maxReadBytes: (maxFrameBytes: number) => number;
20
+ /**
21
+ * The tool error of a device that runs commands without a sandbox the org does not allow (§4.3);
22
+ * also what a machine answers when it refuses such a command itself.
23
+ */
24
+ export declare const UNSANDBOXED_NOT_ALLOWED_ERROR = "This device runs commands without a sandbox, which your organization does not allow. Its owner can turn the sandbox back on with `capybara-device policy set isolation sandbox`.";
18
25
  /**
19
26
  * The handle of an instance on a machine: the device id (ruling R1: the relay is keyed by
20
27
  * device), the roots the machine reported at `create`, and the org's effective limits at
@@ -42,9 +49,11 @@ export declare class LocalDeviceProvider implements SandboxProvider {
42
49
  private readonly targets;
43
50
  private readonly sandbox;
44
51
  private readonly config;
52
+ private readonly grants;
53
+ private readonly log;
45
54
  readonly concurrentExec: true;
46
55
  readonly sessionIsolation = false;
47
- constructor(relay: LocalDeviceRelay, targets: Pick<DeviceTargets, "orgOf" | "machineRoots">, sandbox: Pick<SandboxService, "orgLimits">, config: Pick<KernelConfigService, "get">);
56
+ constructor(relay: LocalDeviceRelay, targets: Pick<DeviceTargets, "orgOf" | "machineRoots">, sandbox: Pick<SandboxService, "devicePolicy" | "deviceLimits">, config: Pick<KernelConfigService, "get">, grants: Pick<NetworkGrants, "forExec" | "unattendedTier" | "unsandboxed" | "recordMissing">, log: Pick<KernelLogger, "warn">);
48
57
  createFileSystem(args: {
49
58
  name: string;
50
59
  limits: SandboxLimits;
@@ -71,6 +80,23 @@ export declare class LocalDeviceProvider implements SandboxProvider {
71
80
  destroyByName(_name: string, signal: AbortSignal): Promise<void>;
72
81
  killAll(h: SandboxHandle, signal: AbortSignal): Promise<void>;
73
82
  killTagged(h: SandboxHandle, tags: string[], signal: AbortSignal): Promise<void>;
83
+ /**
84
+ * Runs the command on the machine with the org's limits now (ruling R27) and, for a command of a
85
+ * run, the hosts the kernel allows that run on this device beyond the local policy (network
86
+ * ask-to-allow spec §6.3; only those the org's limits admit, sent only when there are some). The device's report of refused hosts is re-layered
87
+ * against the limits this exec carried: the kernel never takes the device's word that the org
88
+ * allows a host (spec §4.1).
89
+ *
90
+ * The limits are the device's **ceiling** (sandbox dev tiers spec §4.2): `limits.tier` is the
91
+ * highest tier the org allows, its network that tier's, `defaultTier` the org's default (the
92
+ * device runs at `min(local tier ?? defaultTier, tier)`) and `devHosts` the dev hosts. A run of
93
+ * a scheduled task without a person and without the task's `tier:open` grant is capped at `dev`
94
+ * (spec §11.4). Before relaying, a machine that last reported running commands without a sandbox
95
+ * is refused when the org does not allow it, or for such a run (`device.unsandboxed_not_allowed`,
96
+ * spec §4.3). The kernel knows the local policy only from the last hello or policy op, so the
97
+ * same decision also travels in `limits.allowUnsandboxed` and the machine enforces it itself;
98
+ * its refusal ({@link UNSANDBOXED_NOT_ALLOWED_MESSAGE}) becomes the same error.
99
+ */
74
100
  exec(h: SandboxHandle, args: Parameters<SandboxProvider["exec"]>[1]): Promise<ExecResult>;
75
101
  readFile(h: SandboxHandle, path: string, opts: {
76
102
  maxBytes: number;
@@ -84,6 +110,17 @@ export declare class LocalDeviceProvider implements SandboxProvider {
84
110
  size: number;
85
111
  isDir: boolean;
86
112
  } | null>;
87
- /** The org's effective limits now (ruling R27); a deleted device is not found. */
113
+ /**
114
+ * The device's org and the limits of one exec now (ruling R27): the ceiling, capped at `dev` for
115
+ * an unattended run without the `tier:open` grant (`cap`, set only when the platform and the org
116
+ * would otherwise allow an unsandboxed command); `allowUnsandboxed` whether this command may run
117
+ * without a sandbox. A deleted device is not found. Refuses an unsandboxed machine the org does
118
+ * not allow (or such a run), see {@link exec}.
119
+ */
88
120
  private currentLimits;
121
+ /**
122
+ * The `device.unsandboxed_not_allowed` error (spec §4.3); for an unattended run refused only for
123
+ * its missing `tier:open` grant, the run's text, with the missing grant recorded on the task run.
124
+ */
125
+ private unsandboxedRefusal;
89
126
  }