@capybara-hq/kernel 0.1.0-rc.21 → 0.1.0-rc.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/dist/admin/admin.controller.d.ts +3 -3
  2. package/dist/admin/admin.controller.js +6 -2
  3. package/dist/admin/admin.schemas.d.ts +13 -0
  4. package/dist/agents/admin-agents.d.ts +16 -3
  5. package/dist/agents/admin-agents.js +65 -7
  6. package/dist/agents/agents.schemas.d.ts +24 -6
  7. package/dist/agents/agents.service.d.ts +4 -3
  8. package/dist/agents/agents.service.js +39 -26
  9. package/dist/agents/definition.d.ts +44 -6
  10. package/dist/agents/definition.js +86 -18
  11. package/dist/agents/from-template.d.ts +6 -3
  12. package/dist/agents/from-template.js +57 -44
  13. package/dist/agents/org-templates.js +97 -41
  14. package/dist/agents/templates.d.ts +6 -0
  15. package/dist/agents/templates.js +15 -1
  16. package/dist/background/launch.d.ts +43 -5
  17. package/dist/background/launch.js +88 -8
  18. package/dist/background/session.d.ts +14 -2
  19. package/dist/background/session.js +26 -4
  20. package/dist/background/status.d.ts +1 -1
  21. package/dist/background/status.js +3 -3
  22. package/dist/background/stop.d.ts +0 -2
  23. package/dist/background/stop.js +1 -21
  24. package/dist/catalog/admin-catalog.controller.d.ts +2 -4
  25. package/dist/catalog/admin-catalog.controller.js +2 -6
  26. package/dist/catalog/catalog-content.d.ts +5 -0
  27. package/dist/catalog/catalog-content.js +1 -0
  28. package/dist/catalog/catalog.module.js +3 -0
  29. package/dist/catalog/claude/report.d.ts +9 -2
  30. package/dist/catalog/claude/report.js +1 -0
  31. package/dist/catalog/claude/translate.js +1 -1
  32. package/dist/catalog/component-promotion.js +1 -2
  33. package/dist/catalog/component-state.d.ts +13 -1
  34. package/dist/catalog/component-state.js +14 -5
  35. package/dist/catalog/marketplaces.service.d.ts +1 -1
  36. package/dist/catalog/marketplaces.service.js +2 -1
  37. package/dist/catalog/org-catalog.controller.d.ts +13 -3
  38. package/dist/catalog/org-catalog.controller.js +26 -4
  39. package/dist/catalog/org-catalog.schemas.d.ts +13 -0
  40. package/dist/catalog/org-catalog.schemas.js +34 -0
  41. package/dist/catalog/org-plugin-uploads.controller.js +0 -1
  42. package/dist/catalog/org-plugin-uploads.d.ts +0 -1
  43. package/dist/catalog/org-plugin-uploads.js +0 -3
  44. package/dist/catalog/org-plugin-uploads.schemas.d.ts +0 -1
  45. package/dist/catalog/org-plugin-uploads.schemas.js +0 -2
  46. package/dist/catalog/platform-components.d.ts +0 -1
  47. package/dist/catalog/platform-components.js +3 -8
  48. package/dist/catalog/plugin-import.d.ts +0 -8
  49. package/dist/catalog/plugin-import.js +4 -35
  50. package/dist/catalog/plugin-install.d.ts +2 -2
  51. package/dist/catalog/plugin-install.js +2 -2
  52. package/dist/catalog/plugin-package.d.ts +7 -0
  53. package/dist/catalog/plugin-package.js +27 -1
  54. package/dist/catalog/plugin-upgrade.d.ts +5 -6
  55. package/dist/catalog/plugin-upgrade.js +5 -23
  56. package/dist/catalog/requests.service.d.ts +66 -10
  57. package/dist/catalog/requests.service.js +196 -15
  58. package/dist/channels/actions.d.ts +2 -0
  59. package/dist/channels/actions.js +19 -4
  60. package/dist/channels/cards.d.ts +16 -2
  61. package/dist/channels/cards.js +40 -7
  62. package/dist/channels/channels.module.js +3 -0
  63. package/dist/channels/outbound.d.ts +22 -1
  64. package/dist/channels/outbound.js +116 -7
  65. package/dist/channels/phrases.d.ts +25 -0
  66. package/dist/channels/phrases.js +19 -0
  67. package/dist/channels/threads.d.ts +12 -0
  68. package/dist/channels/threads.js +34 -0
  69. package/dist/config/schema.d.ts +23 -1
  70. package/dist/config/schema.js +107 -5
  71. package/dist/conversations/conversations.service.d.ts +42 -9
  72. package/dist/conversations/conversations.service.js +57 -25
  73. package/dist/conversations/events.d.ts +106 -11
  74. package/dist/conversations/events.js +121 -18
  75. package/dist/conversations/message-context.d.ts +6 -3
  76. package/dist/conversations/message-context.js +14 -10
  77. package/dist/conversations/queue.d.ts +15 -2
  78. package/dist/conversations/queue.js +22 -3
  79. package/dist/conversations/queue.service.d.ts +12 -6
  80. package/dist/conversations/queue.service.js +36 -13
  81. package/dist/db/kernel-schema.d.ts +105 -3
  82. package/dist/devices/conversation-devices.d.ts +8 -1
  83. package/dist/devices/conversation-devices.js +8 -0
  84. package/dist/devices/devices.controller.d.ts +21 -4
  85. package/dist/devices/devices.controller.js +46 -8
  86. package/dist/devices/devices.schemas.d.ts +9 -1
  87. package/dist/devices/devices.schemas.js +11 -3
  88. package/dist/devices/devices.service.d.ts +176 -4
  89. package/dist/devices/devices.service.js +347 -6
  90. package/dist/devices/dto.d.ts +56 -3
  91. package/dist/devices/dto.js +51 -1
  92. package/dist/devices/public-suffix.d.ts +6 -0
  93. package/dist/devices/public-suffix.js +79 -0
  94. package/dist/errors.d.ts +50 -5
  95. package/dist/errors.js +50 -5
  96. package/dist/evals/reconstruct.d.ts +18 -13
  97. package/dist/evals/reconstruct.js +50 -36
  98. package/dist/evals/recorded-tools.d.ts +7 -7
  99. package/dist/evals/recorded-tools.js +4 -3
  100. package/dist/evals/trajectory.d.ts +6 -1
  101. package/dist/evals/trajectory.js +7 -0
  102. package/dist/extensions/package.js +2 -2
  103. package/dist/http/api.module.js +5 -0
  104. package/dist/identity/settings.d.ts +53 -3
  105. package/dist/identity/settings.js +92 -9
  106. package/dist/inbox/inbox.module.js +2 -1
  107. package/dist/inbox/inbox.service.d.ts +19 -4
  108. package/dist/inbox/inbox.service.js +14 -3
  109. package/dist/infra/config/kernel-config.service.d.ts +7 -0
  110. package/dist/infra/config/kernel-config.service.js +31 -0
  111. package/dist/infra/events/kernel-events.d.ts +2 -1
  112. package/dist/local-devices/device-policy.service.d.ts +65 -0
  113. package/dist/local-devices/device-policy.service.js +217 -0
  114. package/dist/local-devices/local-device-gateway.d.ts +4 -2
  115. package/dist/local-devices/local-device-gateway.js +39 -11
  116. package/dist/local-devices/local-device-provider.d.ts +40 -3
  117. package/dist/local-devices/local-device-provider.js +109 -7
  118. package/dist/local-devices/local-devices.module.d.ts +2 -1
  119. package/dist/local-devices/local-devices.module.js +19 -3
  120. package/dist/local-devices/machines.d.ts +30 -3
  121. package/dist/local-devices/machines.js +79 -3
  122. package/dist/local-devices/network-grants.d.ts +104 -0
  123. package/dist/local-devices/network-grants.js +288 -0
  124. package/dist/local-devices/policy-summary.d.ts +20 -0
  125. package/dist/local-devices/policy-summary.js +63 -0
  126. package/dist/orgs/orgs.module.js +4 -10
  127. package/dist/orgs/roles.d.ts +1 -1
  128. package/dist/orgs/roles.js +4 -0
  129. package/dist/orgs/settings.controller.js +4 -3
  130. package/dist/orgs/settings.service.d.ts +172 -14
  131. package/dist/orgs/settings.service.js +229 -27
  132. package/dist/platform/exclusions.js +11 -1
  133. package/dist/platform/operation.js +4 -0
  134. package/dist/platform/ops/admin-items.ops.js +1 -1
  135. package/dist/platform/ops/admin.ops.js +32 -7
  136. package/dist/platform/ops/agent-skills.ops.js +2 -2
  137. package/dist/platform/ops/agent-templates.ops.js +2 -2
  138. package/dist/platform/ops/agents.ops.js +11 -15
  139. package/dist/platform/ops/devices.ops.js +3 -1
  140. package/dist/platform/ops/inbox.ops.js +19 -0
  141. package/dist/platform/ops/org-settings.ops.js +15 -6
  142. package/dist/platform/ops/orgs.ops.js +1 -0
  143. package/dist/platform/ops/platform.ops.d.ts +9 -1
  144. package/dist/platform/ops/platform.ops.js +34 -12
  145. package/dist/platform/ops/requests.ops.js +3 -2
  146. package/dist/platform/ops/schedules.ops.d.ts +21 -0
  147. package/dist/platform/ops/schedules.ops.js +470 -0
  148. package/dist/platform/ops/skills.ops.js +3 -3
  149. package/dist/platform/platform-ops.module.d.ts +2 -1
  150. package/dist/platform/platform-ops.module.js +5 -0
  151. package/dist/platform/route-schemas.js +3 -0
  152. package/dist/roles/worker.module.js +3 -0
  153. package/dist/runtime/context/build.js +13 -4
  154. package/dist/runtime/context/compaction.js +6 -3
  155. package/dist/runtime/context/device-paths.d.ts +43 -21
  156. package/dist/runtime/context/device-paths.js +104 -21
  157. package/dist/runtime/context/run-context.d.ts +9 -0
  158. package/dist/runtime/context/run-context.js +8 -0
  159. package/dist/runtime/context/skills.d.ts +14 -1
  160. package/dist/runtime/context/skills.js +36 -5
  161. package/dist/runtime/maintenance.d.ts +4 -2
  162. package/dist/runtime/maintenance.js +16 -5
  163. package/dist/runtime/run-finish.d.ts +6 -1
  164. package/dist/runtime/run-finish.js +16 -2
  165. package/dist/runtime/run-queue.js +21 -2
  166. package/dist/runtime/run-worker.js +14 -33
  167. package/dist/runtime/runtime.module.js +3 -0
  168. package/dist/runtime/subagents/definition.d.ts +32 -14
  169. package/dist/runtime/subagents/definition.js +106 -109
  170. package/dist/runtime/subagents/dispatch.js +4 -3
  171. package/dist/runtime/subagents/preamble.d.ts +2 -1
  172. package/dist/runtime/subagents/preamble.js +2 -1
  173. package/dist/runtime/waits.js +14 -4
  174. package/dist/sandbox/platform-tiers.d.ts +86 -0
  175. package/dist/sandbox/platform-tiers.js +97 -0
  176. package/dist/sandbox/sandbox.service.d.ts +121 -11
  177. package/dist/sandbox/sandbox.service.js +351 -30
  178. package/dist/sandbox/tiers.d.ts +98 -0
  179. package/dist/sandbox/tiers.js +104 -0
  180. package/dist/schedules/effective-settings.d.ts +16 -0
  181. package/dist/schedules/effective-settings.js +17 -0
  182. package/dist/schedules/errors.d.ts +8 -0
  183. package/dist/schedules/errors.js +8 -0
  184. package/dist/schedules/fire.d.ts +118 -0
  185. package/dist/schedules/fire.js +625 -0
  186. package/dist/schedules/grant-rules.d.ts +94 -0
  187. package/dist/schedules/grant-rules.js +126 -0
  188. package/dist/schedules/job-ids.d.ts +32 -0
  189. package/dist/schedules/job-ids.js +27 -0
  190. package/dist/schedules/pause.d.ts +9 -0
  191. package/dist/schedules/pause.js +33 -0
  192. package/dist/schedules/run-tracker.d.ts +81 -0
  193. package/dist/schedules/run-tracker.js +333 -0
  194. package/dist/schedules/scan.job.d.ts +64 -0
  195. package/dist/schedules/scan.job.js +303 -0
  196. package/dist/schedules/schedule-math.d.ts +31 -0
  197. package/dist/schedules/schedule-math.js +223 -0
  198. package/dist/schedules/scheduled-tasks.service.d.ts +351 -0
  199. package/dist/schedules/scheduled-tasks.service.js +1193 -0
  200. package/dist/schedules/schedules.controller.d.ts +68 -0
  201. package/dist/schedules/schedules.controller.js +242 -0
  202. package/dist/schedules/schedules.module.d.ts +16 -0
  203. package/dist/schedules/schedules.module.js +65 -0
  204. package/dist/schedules/schedules.schemas.d.ts +316 -0
  205. package/dist/schedules/schedules.schemas.js +186 -0
  206. package/dist/schedules/store.d.ts +81 -0
  207. package/dist/schedules/store.js +390 -0
  208. package/dist/schedules/store.module.d.ts +6 -0
  209. package/dist/schedules/store.module.js +21 -0
  210. package/dist/schedules/types.d.ts +108 -0
  211. package/dist/schedules/types.js +1 -0
  212. package/dist/schedules/unattended.d.ts +117 -0
  213. package/dist/schedules/unattended.js +228 -0
  214. package/dist/setup/oauth-token-probe.js +8 -4
  215. package/dist/skills/agent-skills.service.d.ts +4 -8
  216. package/dist/skills/agent-skills.service.js +18 -53
  217. package/dist/skills/catalog.d.ts +65 -62
  218. package/dist/skills/catalog.js +109 -206
  219. package/dist/skills/conversation-catalog.d.ts +3 -3
  220. package/dist/skills/conversation-catalog.js +5 -12
  221. package/dist/skills/invoke.d.ts +2 -2
  222. package/dist/skills/skills.controller.d.ts +3 -1
  223. package/dist/skills/skills.service.d.ts +38 -37
  224. package/dist/skills/skills.service.js +168 -107
  225. package/dist/skills/usable.d.ts +57 -8
  226. package/dist/skills/usable.js +172 -38
  227. package/dist/tools/approvals.controller.d.ts +4 -2
  228. package/dist/tools/approvals.controller.js +17 -2
  229. package/dist/tools/approvals.schemas.d.ts +1 -0
  230. package/dist/tools/approvals.schemas.js +3 -1
  231. package/dist/tools/approvals.service.d.ts +182 -7
  232. package/dist/tools/approvals.service.js +597 -13
  233. package/dist/tools/builtin-names.d.ts +25 -4
  234. package/dist/tools/builtin-names.js +45 -4
  235. package/dist/tools/builtins/builtin-tools.d.ts +8 -0
  236. package/dist/tools/builtins/builtin-tools.js +23 -2
  237. package/dist/tools/builtins/sandbox/bash.js +16 -7
  238. package/dist/tools/builtins/sandbox/common.d.ts +1 -1
  239. package/dist/tools/builtins/sandbox/common.js +33 -10
  240. package/dist/tools/builtins/sandbox/edit.js +7 -3
  241. package/dist/tools/builtins/sandbox/file-state.d.ts +31 -1
  242. package/dist/tools/builtins/sandbox/file-state.js +33 -3
  243. package/dist/tools/builtins/sandbox/write.js +8 -4
  244. package/dist/tools/builtins/schedules.d.ts +107 -0
  245. package/dist/tools/builtins/schedules.js +789 -0
  246. package/dist/tools/elicitation.d.ts +2 -0
  247. package/dist/tools/elicitation.js +1 -1
  248. package/dist/tools/fixed-ask.d.ts +7 -0
  249. package/dist/tools/fixed-ask.js +9 -0
  250. package/dist/tools/network-asks.d.ts +109 -0
  251. package/dist/tools/network-asks.js +221 -0
  252. package/dist/tools/pipeline.d.ts +53 -6
  253. package/dist/tools/pipeline.js +202 -13
  254. package/dist/tools/policy.d.ts +21 -8
  255. package/dist/tools/policy.js +63 -9
  256. package/dist/tools/registry.d.ts +10 -1
  257. package/dist/tools/tools.module.js +12 -0
  258. package/package.json +2 -2
  259. package/src/migrations/0113_scheduled_tasks.sql +108 -0
  260. package/src/migrations/0114_network_asks.sql +23 -0
  261. package/src/migrations/0115_network_requests.sql +28 -0
  262. package/src/migrations/0116_skills_default_on.sql +37 -0
  263. package/src/migrations/0117_sandbox_tiers.sql +11 -0
@@ -8,10 +8,11 @@ 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 { connectorToolKey, parseToolKey } from "@capybara-hq/plugin-sdk";
11
+ import { connectorToolKey, parseToolKey, validateAllowHost, } from "@capybara-hq/plugin-sdk";
12
12
  import { Inject, Injectable } from "@nestjs/common";
13
13
  import { sql } from "kysely";
14
14
  import { EventLog } from "../conversations/event-log.js";
15
+ import { NETWORK_ASK_MAX_HOSTS, NETWORK_HOST_MAX_CHARS, } from "../conversations/events.js";
15
16
  import { ApiError } from "../errors.js";
16
17
  import { audit } from "../identity/audit.js";
17
18
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
@@ -19,12 +20,38 @@ import { KernelDb } from "../infra/database/kernel-db.js";
19
20
  import { TransactionHost } from "../infra/database/transaction-host.js";
20
21
  import { Transactional } from "../infra/database/transactional.decorator.js";
21
22
  import { KernelEvents } from "../infra/events/kernel-events.js";
23
+ import { LockService } from "../infra/lock/lock.service.js";
24
+ import { WithLock } from "../infra/lock/with-lock.decorator.js";
25
+ import { DevicePolicyService } from "../local-devices/device-policy.service.js";
22
26
  import { ORG_ROLES, roleAtLeast } from "../orgs/roles.js";
23
27
  import { rootConversationOf } from "../runtime/subagents/child.js";
28
+ import { SandboxService } from "../sandbox/sandbox.service.js";
29
+ import { readScheduleSettings } from "../schedules/effective-settings.js";
30
+ import { grantRefusalOf, NETWORK_GRANT_PREFIX, NETWORK_GRANT_SPEC_HASH, networkGrantRefusal, } from "../schedules/grant-rules.js";
31
+ import { ScheduledTaskStore } from "../schedules/store.js";
32
+ import { unattendedTaskRunOf } from "../schedules/unattended.js";
24
33
  import { pluginDisplayNames } from "./display-names.js";
34
+ import { normalizeStoredDefinitionIn } from "./legacy-data.js";
35
+ import { ToolRegistry } from "./registry.js";
25
36
  export { APPROVAL_DENY_REASON_MAX_CHARS } from "../conversations/events.js";
26
37
  /** `list` 一次最多返回的审批数(收件箱据此判断这个来源是否取满)。 */
27
38
  export const APPROVALS_LIST_MAX = 100;
39
+ /**
40
+ * The model-visible note written after a network ask's kept result when its run is stopped before
41
+ * the device owner answered (spec §6.7).
42
+ */
43
+ export const NETWORK_ASK_CANCELLED_NOTE = "[network] The run was stopped before the user answered; access stays blocked.";
44
+ /** The record without a network ask's kept command result (what routes and cards may show). */
45
+ export function publicRecord(r) {
46
+ if (r.detail === null)
47
+ return { ...r, detail: null };
48
+ const { result: _result, ...detail } = r.detail;
49
+ return { ...r, detail };
50
+ }
51
+ /** The tool key a network ask is recorded under (the command ran in `Bash`). */
52
+ export const NETWORK_ASK_TOOL_KEY = "Bash";
53
+ /** The spec hash of a network ask (no tool definition is approved). */
54
+ const NETWORK_ASK_SPEC_HASH = "network";
28
55
  const notFound = () => new ApiError("approval.not_found", "Approval not found");
29
56
  const iso = (v) => new Date(v).toISOString();
30
57
  const isRole = (v) => v !== null && ORG_ROLES.includes(v);
@@ -38,6 +65,8 @@ function toRecord(r) {
38
65
  runId: r.run_id,
39
66
  callId: r.call_id,
40
67
  attempt: r.attempt,
68
+ kind: r.kind,
69
+ detail: r.kind === "network" ? (r.detail ?? null) : null,
41
70
  toolKey: r.tool_key,
42
71
  specHash: r.spec_hash,
43
72
  connectorId: r.connector_id,
@@ -84,12 +113,30 @@ let ApprovalsService = class ApprovalsService {
84
113
  eventLog;
85
114
  events;
86
115
  txHost;
87
- constructor(db, config, eventLog, events, txHost) {
116
+ schedules;
117
+ registry;
118
+ policies;
119
+ lockService;
120
+ sandbox;
121
+ constructor(db, config, eventLog, events, txHost, schedules,
122
+ /** A trial approval is kept only for a tool the task may hold as a grant (its agent's tools). */
123
+ registry,
124
+ /** 「这台设备一直允许」 sends the `policy` op to the device before the decision is recorded. */
125
+ policies,
126
+ /** One decision of a network ask at a time, across replicas (the op is sent before the transaction). */
127
+ lockService,
128
+ /** A trial run's 「这次允许」 becomes a task grant only within the org's egress. */
129
+ sandbox) {
88
130
  this.db = db;
89
131
  this.config = config;
90
132
  this.eventLog = eventLog;
91
133
  this.events = events;
92
134
  this.txHost = txHost;
135
+ this.schedules = schedules;
136
+ this.registry = registry;
137
+ this.policies = policies;
138
+ this.lockService = lockService;
139
+ this.sandbox = sandbox;
93
140
  }
94
141
  /**
95
142
  * 新建 pending 审批(`expires_at` = 现在 + `tools.approvalTimeoutHours`)并写 `approval_requested`,同一事务。
@@ -165,6 +212,97 @@ let ApprovalsService = class ApprovalsService {
165
212
  ]);
166
213
  return { record: rec, toSeq: Math.max(...written.map((e) => e.seq)) };
167
214
  }
215
+ /**
216
+ * A network ask (network ask-to-allow spec §6.2): a pending approval of kind `network` for the
217
+ * device owner, expiring like a tool approval (`tools.approvalTimeoutHours`), and its
218
+ * `approval_requested` (tool key `Bash`, the display-only `network.hosts`), in one transaction.
219
+ * It takes the attempt after the call's other approvals (P-D4: the command ran, so no tool
220
+ * approval of the call follows it). One per call: a second request returns the first.
221
+ */
222
+ async requestNetwork(input) {
223
+ const hosts = input.detail.hosts;
224
+ if (hosts.length === 0 || hosts.length > NETWORK_ASK_MAX_HOSTS) {
225
+ throw new Error(`a network ask lists 1 to ${NETWORK_ASK_MAX_HOSTS} hosts`);
226
+ }
227
+ if (hosts.some((h) => h.host.length === 0 || h.host.length > NETWORK_HOST_MAX_CHARS)) {
228
+ throw new Error("a network ask's host is 1 to 255 characters");
229
+ }
230
+ if (input.scopes.length === 0 || !input.scopes.includes("once")) {
231
+ throw new Error('a network ask offers at least "once"');
232
+ }
233
+ const { record, toSeq } = await this.requestNetworkIn(input);
234
+ if (toSeq !== null)
235
+ await this.eventLog.notify(record.conversationId, toSeq);
236
+ return record;
237
+ }
238
+ async requestNetworkIn(input) {
239
+ const hours = this.config.get().tools.approvalTimeoutHours;
240
+ const t = this.db.db;
241
+ // the conversation row first (as `requestIn`): it also serializes two asks of one call
242
+ await t
243
+ .selectFrom("conversations")
244
+ .select("id")
245
+ .where("id", "=", input.conversationId)
246
+ .forUpdate()
247
+ .execute();
248
+ // only the device's owner is ever asked: never another account the caller names
249
+ const owner = await this.deviceOwnerOf(input.orgId, input.detail.deviceId);
250
+ if (owner === null || owner !== input.requestedFor) {
251
+ throw new Error(`a network ask is for the owner of a live device of the org (device ${input.detail.deviceId})`);
252
+ }
253
+ const existing = await this.networkForCall(input.runId, input.callId);
254
+ if (existing !== null)
255
+ return { record: existing, toSeq: null };
256
+ const last = await t
257
+ .selectFrom("approvals")
258
+ .select(sql `max(attempt)`.as("attempt"))
259
+ .where("run_id", "=", input.runId)
260
+ .where("call_id", "=", input.callId)
261
+ .executeTakeFirst();
262
+ const row = await t
263
+ .insertInto("approvals")
264
+ .values({
265
+ id: randomUUID(),
266
+ org_id: input.orgId,
267
+ conversation_id: input.conversationId,
268
+ run_id: input.runId,
269
+ call_id: input.callId,
270
+ attempt: Number(last?.attempt ?? 0) + 1,
271
+ kind: "network",
272
+ detail: JSON.stringify(input.detail),
273
+ tool_key: NETWORK_ASK_TOOL_KEY,
274
+ spec_hash: NETWORK_ASK_SPEC_HASH,
275
+ connector_id: null,
276
+ input: JSON.stringify({}),
277
+ requested_for: input.requestedFor,
278
+ approver_role: null,
279
+ scopes: [...new Set(input.scopes)],
280
+ scope: null,
281
+ decided_by: null,
282
+ expires_at: sql `now() + ${hours}::integer * interval '1 hour'`,
283
+ })
284
+ .returningAll()
285
+ .executeTakeFirstOrThrow();
286
+ const rec = toRecord(row);
287
+ const written = await this.eventLog.appendIn(rec.conversationId, [
288
+ {
289
+ type: "approval_requested",
290
+ runId: rec.runId,
291
+ idemKey: `approval_requested:${rec.id}`,
292
+ payload: {
293
+ step: input.step,
294
+ callId: rec.callId,
295
+ approvalId: rec.id,
296
+ toolKey: rec.toolKey,
297
+ expiresAt: rec.expiresAt,
298
+ network: { hosts: input.detail.hosts.map((h) => h.host) },
299
+ requestedFor: rec.requestedFor,
300
+ approverRole: null,
301
+ },
302
+ },
303
+ ]);
304
+ return { record: rec, toSeq: Math.max(...written.map((e) => e.seq)) };
305
+ }
168
306
  /** 当前用户能处理的审批(新的在前,最多 {@link APPROVALS_LIST_MAX} 条);`status` 缺省为全部。 */
169
307
  async list(orgId, accountId, status) {
170
308
  const role = await this.roleOf(orgId, accountId);
@@ -220,8 +358,196 @@ let ApprovalsService = class ApprovalsService {
220
358
  * `always` → 发起人 × agent × 工具)、`approval_decided` 事件(幂等键 `approval_decided:<id>`)与审计;
221
359
  * 提交后发布事件并调用 `onDecided`。
222
360
  */
223
- async decide(orgId, id, accountId, body, actorTokenId) {
361
+ async decide(orgId, id, accountId, body, actorTokenId, via = "web") {
362
+ const kind = await this.db
363
+ .selectFrom("approvals")
364
+ .select("kind")
365
+ .where("id", "=", id)
366
+ .where("org_id", "=", orgId)
367
+ .executeTakeFirst();
368
+ if (!kind)
369
+ throw notFound();
370
+ if (kind.kind === "network") {
371
+ return this.decideNetwork(orgId, id, accountId, body, actorTokenId, via);
372
+ }
373
+ if (body.hosts !== undefined) {
374
+ throw new ApiError("validation_failed", "hosts are only for network asks", [
375
+ { path: "hosts", message: "Only a network ask takes hosts" },
376
+ ]);
377
+ }
224
378
  const { record, toSeq } = await this.decideIn(orgId, id, accountId, body, actorTokenId);
379
+ await this.announceDecided(record, toSeq);
380
+ return record;
381
+ }
382
+ /**
383
+ * A network ask's decision (network ask-to-allow spec §5.2), one at a time per ask across
384
+ * replicas: only the device owner (`requested_for`) decides, in a signed-in session (a personal
385
+ * access token is refused; the route already refuses service and embed tokens); the allowed
386
+ * hosts are a subset of those asked. 「这台设备一直允许」 sends the `policy` op first, outside any
387
+ * transaction; when the device cannot be reached or lacks the feature the decision is recorded
388
+ * as a run grant with the reason, and when the device refuses (its owner takes no remote grants)
389
+ * as `always` with outcome `refused` and nothing granted. Then {@link decideIn} records what took
390
+ * effect and the run resumes after the commit; a change the device applied after the ask closed
391
+ * is audited (`late`) and answered with a 409 that says so.
392
+ */
393
+ async decideNetwork(orgId, id, accountId, body, actorTokenId, via) {
394
+ const row = await this.db
395
+ .selectFrom("approvals")
396
+ .selectAll()
397
+ .select(sql `expires_at <= now()`.as("expired"))
398
+ .where("id", "=", id)
399
+ .where("org_id", "=", orgId)
400
+ .executeTakeFirst();
401
+ if (!row)
402
+ throw notFound();
403
+ const ask = toRecord(row);
404
+ const detail = ask.detail;
405
+ if (detail === null)
406
+ throw new Error(`network ask ${id} has no detail`);
407
+ // the device's owner now (an ownership change since the ask is honored), never anyone else
408
+ const owner = await this.deviceOwnerOf(orgId, detail.deviceId);
409
+ if (ask.requestedFor !== accountId || owner !== accountId) {
410
+ // named only to the people the ask is shown to (the run's starter, the asked owner); any
411
+ // other member learns nothing about it, as `get` (approval.not_found)
412
+ const trigger = await this.db
413
+ .selectFrom("runs")
414
+ .select("trigger_account_id")
415
+ .where("id", "=", ask.runId)
416
+ .executeTakeFirst();
417
+ if (accountId !== ask.requestedFor && accountId !== trigger?.trigger_account_id) {
418
+ throw notFound();
419
+ }
420
+ const name = owner === null ? null : await this.displayNameOf(owner);
421
+ throw new ApiError("approval.not_yours", `Only ${name ?? "the device's owner"} can allow network access on this device.`);
422
+ }
423
+ if (actorTokenId !== undefined) {
424
+ throw new ApiError("auth.session_required", "Network access is decided in a signed-in session, not with a personal access token.");
425
+ }
426
+ if (ask.status !== "pending") {
427
+ throw new ApiError("approval.already_decided", "Approval has already been decided");
428
+ }
429
+ if (row.expired)
430
+ throw new ApiError("approval.already_decided", "Approval has expired");
431
+ const asked = [...new Set(detail.hosts.map((h) => h.host))];
432
+ let network;
433
+ if (body.decision === "approve") {
434
+ const scope = body.scope ?? "once";
435
+ if (scope === "session" || !ask.scopes.includes(scope)) {
436
+ throw new ApiError("approval.scope_not_allowed", `Scope "${scope}" is not allowed for this approval; allowed: ${ask.scopes.join(", ")}`);
437
+ }
438
+ const unknown = (body.hosts ?? []).filter((h) => !asked.includes(h));
439
+ if (unknown.length > 0) {
440
+ throw new ApiError("validation_failed", "hosts must be among the hosts asked about", [
441
+ { path: "hosts", message: `Not asked about: ${unknown.join(", ")}` },
442
+ ]);
443
+ }
444
+ // exact hosts only: the device and the run grants never take anything else
445
+ const granted = [...new Set(body.hosts ?? asked)].filter((h) => !h.startsWith("*.") && validateAllowHost(h).length === 0);
446
+ if (granted.length === 0) {
447
+ throw new ApiError("validation_failed", "No host to allow", [
448
+ { path: "hosts", message: "Choose at least one host" },
449
+ ]);
450
+ }
451
+ network = { wanted: granted, granted, outcome: "once", via };
452
+ if (scope === "always") {
453
+ const applied = await this.applyAlways(orgId, detail.deviceId, granted, {
454
+ approvalId: id,
455
+ accountId,
456
+ via,
457
+ });
458
+ network =
459
+ applied === null
460
+ ? { wanted: granted, granted, outcome: "always", via }
461
+ : applied === "refused"
462
+ ? // the device's owner takes no remote grants: nothing is allowed, not even this run
463
+ { wanted: granted, granted: [], outcome: "refused", fallback: applied, via }
464
+ : { wanted: granted, granted, outcome: "once_fallback", fallback: applied, via };
465
+ }
466
+ }
467
+ else {
468
+ if (body.hosts !== undefined) {
469
+ throw new ApiError("validation_failed", "hosts are only given when allowing", [
470
+ { path: "hosts", message: "Only an approval takes hosts" },
471
+ ]);
472
+ }
473
+ network = { wanted: [], granted: [], outcome: null, via };
474
+ }
475
+ let decided;
476
+ try {
477
+ decided = await this.decideIn(orgId, id, accountId, body, actorTokenId, network);
478
+ }
479
+ catch (e) {
480
+ // the device took the hosts, but a reply, the expiry or a stop closed the ask meanwhile:
481
+ // the change stands (the owner chose it), so it is audited and said
482
+ if (network.outcome === "always" &&
483
+ e instanceof ApiError &&
484
+ e.code === "approval.already_decided") {
485
+ await this.auditLateAllow(orgId, ask, network);
486
+ throw new ApiError("approval.already_decided", `The device now allows ${network.granted.join(", ")}; the request had already closed.`);
487
+ }
488
+ throw e;
489
+ }
490
+ await this.announceDecided(decided.record, decided.toSeq);
491
+ return decided.record;
492
+ }
493
+ /** `device.network_allowed` of a policy change that reached the device after the ask closed. */
494
+ async auditLateAllow(orgId, ask, network) {
495
+ if (ask.detail === null)
496
+ return;
497
+ await audit(this.db.db, {
498
+ actorAccountId: ask.requestedFor,
499
+ orgId,
500
+ action: "device.network_allowed",
501
+ target: ask.detail.deviceId,
502
+ detail: {
503
+ approvalId: ask.id,
504
+ deviceId: ask.detail.deviceId,
505
+ hosts: network.granted,
506
+ scope: "always",
507
+ outcome: "always",
508
+ late: true,
509
+ by: ask.requestedFor,
510
+ via: network.via,
511
+ conversationId: ask.conversationId,
512
+ runId: ask.runId,
513
+ },
514
+ });
515
+ }
516
+ /** The owner of a live device of the org (null: none, deleted, or of another org). */
517
+ async deviceOwnerOf(orgId, deviceId) {
518
+ const d = await this.db
519
+ .selectFrom("devices")
520
+ .select("owner_account_id")
521
+ .where("id", "=", deviceId)
522
+ .where("org_id", "=", orgId)
523
+ .where("deleted_at", "is", null)
524
+ .executeTakeFirst();
525
+ return d?.owner_account_id ?? null;
526
+ }
527
+ /**
528
+ * Adds the hosts to the device's local policy (the `policy` op; never inside a transaction).
529
+ * Null when it was applied, else why not: the decision falls back to a run grant.
530
+ */
531
+ async applyAlways(orgId, deviceId, hosts, source) {
532
+ const accountName = (await this.displayNameOf(source.accountId)) ?? source.accountId;
533
+ try {
534
+ const out = await this.policies.apply({
535
+ orgId,
536
+ deviceId,
537
+ change: { add: hosts },
538
+ source: { ...source, accountName },
539
+ });
540
+ return out.applied ? null : out.reason;
541
+ }
542
+ catch (e) {
543
+ // the device was deleted meanwhile: nothing to write to, the run cannot go on there either
544
+ if (e instanceof ApiError && e.code === "device.not_found")
545
+ return "offline";
546
+ throw e;
547
+ }
548
+ }
549
+ /** After the commit: the decision's events reach the page, the run's waits and IM delivery. */
550
+ async announceDecided(record, toSeq) {
225
551
  await this.eventLog.notify(record.conversationId, toSeq);
226
552
  // waits resume the run and its IM delivery is enqueued (handler failures are logged)
227
553
  await this.events.emit("approval.decided", {
@@ -231,11 +557,19 @@ let ApprovalsService = class ApprovalsService {
231
557
  runId: record.runId,
232
558
  status: record.status === "approved" ? "approved" : "denied",
233
559
  });
234
- return record;
560
+ }
561
+ /** An account's display name (null when unknown). */
562
+ async displayNameOf(accountId) {
563
+ const a = await this.db
564
+ .selectFrom("accounts")
565
+ .select("display_name")
566
+ .where("id", "=", accountId)
567
+ .executeTakeFirst();
568
+ return a?.display_name ?? null;
235
569
  }
236
570
  // its own transaction (as the legacy one was): `approval.decided` is emitted only after the
237
571
  // decision committed, even when a caller has a transaction open
238
- async decideIn(orgId, id, accountId, body, actorTokenId) {
572
+ async decideIn(orgId, id, accountId, body, actorTokenId, network) {
239
573
  const t = this.db.db;
240
574
  const locked = await t
241
575
  .selectFrom("approvals")
@@ -267,9 +601,19 @@ let ApprovalsService = class ApprovalsService {
267
601
  if (locked.expired) {
268
602
  throw new ApiError("approval.already_decided", "Approval has expired");
269
603
  }
604
+ // a network ask is decided only through decideNetwork (its policy op went first)
605
+ if ((before.kind === "network") !== (network !== undefined)) {
606
+ throw new Error(`approval ${id} is of kind ${before.kind}`);
607
+ }
270
608
  const approved = body.decision === "approve";
271
609
  const reason = approved ? undefined : body.reason?.trim() || undefined;
272
- const scope = approved ? (body.scope ?? "once") : null;
610
+ const scope = !approved
611
+ ? null
612
+ : network === undefined
613
+ ? (body.scope ?? "once")
614
+ : network.outcome === "always" || network.outcome === "refused"
615
+ ? "always"
616
+ : "once";
273
617
  // 按角色升级的批准只对这一次有效:授权记在发起人名下会让其以后绕过角色要求
274
618
  if (scope !== null && scope !== "once" && before.requestedFor === null) {
275
619
  throw new ApiError("validation_failed", 'Only scope "once" is allowed for role-escalated approvals', [{ path: "scope", message: 'Only "once" is allowed for role-escalated approvals' }]);
@@ -287,6 +631,17 @@ let ApprovalsService = class ApprovalsService {
287
631
  decided_by: accountId,
288
632
  decided_at: sql `now()`,
289
633
  deny_reason: reason ?? null,
634
+ ...(network?.outcome != null &&
635
+ before.detail !== null && {
636
+ // what took effect; run grants read grantedHosts (an always grant's hosts are on
637
+ // the device's policy already, so it carries them harmlessly)
638
+ detail: JSON.stringify({
639
+ ...before.detail,
640
+ grantedHosts: network.granted,
641
+ outcome: network.outcome,
642
+ ...(network.fallback !== undefined && { fallback: network.fallback }),
643
+ }),
644
+ }),
290
645
  })
291
646
  .where("id", "=", id)
292
647
  .returningAll()
@@ -294,7 +649,10 @@ let ApprovalsService = class ApprovalsService {
294
649
  // 授权绑定所批准的定义指纹与来源连接器(同 slug 重新安装的连接器 id 不同,不继承)
295
650
  const hash = before.specHash;
296
651
  const connectorId = before.connectorId;
297
- if (scope === "session") {
652
+ if (network !== undefined) {
653
+ // a network ask grants nothing of the tool kind: its run grant is the row itself
654
+ }
655
+ else if (scope === "session") {
298
656
  // a sub-agent's grant is the root conversation's (agent map spec §7): the root and every
299
657
  // agent of its tree, at any depth, are not asked again for the same call
300
658
  await t
@@ -330,6 +688,14 @@ let ApprovalsService = class ApprovalsService {
330
688
  }))
331
689
  .execute();
332
690
  }
691
+ if (approved && scope !== null && network === undefined) {
692
+ await this.keepTrialGrant(before, accountId);
693
+ }
694
+ // 「这次允许」 in a trial run (also an 「一直允许」 the device could not take): the task's
695
+ // grants; one the device took needs none (network ask-to-allow spec §8.1)
696
+ if (network?.outcome === "once" || network?.outcome === "once_fallback") {
697
+ await this.keepTrialNetworkGrant(before, accountId, network.granted);
698
+ }
333
699
  const written = await this.eventLog.appendIn(before.conversationId, [
334
700
  {
335
701
  type: "approval_decided",
@@ -345,6 +711,31 @@ let ApprovalsService = class ApprovalsService {
345
711
  },
346
712
  },
347
713
  ]);
714
+ if (network !== undefined && before.detail !== null) {
715
+ const asked = [...new Set(before.detail.hosts.map((h) => h.host))];
716
+ const refused = asked.filter((h) => !network.wanted.includes(h));
717
+ await audit(t, {
718
+ actorAccountId: accountId,
719
+ orgId,
720
+ action: approved ? "device.network_allowed" : "device.network_denied",
721
+ target: before.detail.deviceId,
722
+ detail: {
723
+ approvalId: id,
724
+ deviceId: before.detail.deviceId,
725
+ hosts: approved ? network.wanted : asked,
726
+ ...(approved && refused.length > 0 && { refused }),
727
+ scope,
728
+ outcome: approved ? network.outcome : "denied",
729
+ ...(network.fallback !== undefined && { fallback: network.fallback }),
730
+ by: accountId,
731
+ via: network.via,
732
+ conversationId: before.conversationId,
733
+ runId: before.runId,
734
+ ...(reason !== undefined && { reason }),
735
+ },
736
+ });
737
+ return { record: toRecord(updated), toSeq: Math.max(...written.map((e) => e.seq)) };
738
+ }
348
739
  await audit(t, {
349
740
  actorAccountId: accountId,
350
741
  ...(actorTokenId !== undefined && { actorTokenId }),
@@ -365,6 +756,103 @@ let ApprovalsService = class ApprovalsService {
365
756
  });
366
757
  return { record: toRecord(updated), toSeq: Math.max(...written.map((e) => e.seq)) };
367
758
  }
759
+ /**
760
+ * A trial run of a scheduled task (scheduled tasks spec §7): an approval its creator granted, in
761
+ * any scope, is also added to the task's grants with the approved definition's hash, so its
762
+ * unattended runs do not ask again. Kept only by the rule `addGrants` applies
763
+ * (`grantRefusalOf`, against the task agent's tools now): never a role-escalated approval
764
+ * (`requestedFor` null, it stays `once`), one another account decided, one whose scopes never
765
+ * allowed remembering (an org / platform operation: "once" only), a per-call key
766
+ * (`Platform:<operation>`, a skill change among them), a fixed ask, a tool the org forbids (its key or the
767
+ * call's), or a tool the task's agent does not have. In the decision's transaction.
768
+ */
769
+ async keepTrialGrant(a, decidedBy) {
770
+ if (a.requestedFor === null || a.requestedFor !== decidedBy)
771
+ return;
772
+ if (!a.scopes.includes("always"))
773
+ return;
774
+ const t = this.db.db;
775
+ const served = await unattendedTaskRunOf(t, a.runId);
776
+ if (served === null || !served.trial)
777
+ return;
778
+ const task = await t
779
+ .selectFrom("scheduled_tasks as s")
780
+ .innerJoin("agents as ag", "ag.id", "s.agent_id")
781
+ .leftJoin("agent_versions as v", (j) => j.onRef("v.agent_id", "=", "ag.id").onRef("v.version", "=", "ag.current_version"))
782
+ .select(["s.account_id", "v.definition"])
783
+ .where("s.id", "=", served.taskId)
784
+ .where("s.deleted_at", "is", null)
785
+ .executeTakeFirst();
786
+ if (task?.account_id !== decidedBy)
787
+ return;
788
+ const member = await t
789
+ .selectFrom("memberships")
790
+ .select("role")
791
+ .where("org_id", "=", a.orgId)
792
+ .where("account_id", "=", decidedBy)
793
+ .executeTakeFirst();
794
+ if (!member)
795
+ return;
796
+ const def = task.definition === null
797
+ ? undefined
798
+ : await normalizeStoredDefinitionIn(t, a.orgId, task.definition);
799
+ const { tools } = await this.registry.forAgent(a.orgId, {
800
+ disabledTools: def?.disabledTools ?? [],
801
+ subagentTools: def?.subagentTools,
802
+ subagents: def?.subagents,
803
+ });
804
+ const tool = tools.find((x) => x.key === a.toolKey);
805
+ const { forbiddenTools } = await readScheduleSettings(t, this.config, a.orgId);
806
+ const why = grantRefusalOf(a.toolKey, tool, {
807
+ role: member.role,
808
+ forbiddenTools,
809
+ });
810
+ if (why !== null)
811
+ return;
812
+ await this.schedules.addGrant(t, served.taskId, {
813
+ toolKey: a.toolKey,
814
+ specHash: a.specHash,
815
+ connectorId: a.connectorId,
816
+ });
817
+ }
818
+ /**
819
+ * A trial run of a scheduled task (network ask-to-allow spec §8.1): the hosts its creator allowed
820
+ * for this run (「这次允许」) on a network ask about one of their devices become the task's
821
+ * `network:<host>` grants, each only when `networkGrantRefusal` allows it (exact host, within the
822
+ * org's effective egress, the device takes remote grants). Never for an ask another account
823
+ * decided or one asked of someone else. In the decision's transaction.
824
+ */
825
+ async keepTrialNetworkGrant(a, decidedBy, hosts) {
826
+ if (hosts.length === 0 || a.detail === null)
827
+ return;
828
+ if (a.requestedFor === null || a.requestedFor !== decidedBy)
829
+ return;
830
+ const t = this.db.db;
831
+ const served = await unattendedTaskRunOf(t, a.runId);
832
+ if (served === null || !served.trial)
833
+ return;
834
+ const task = await t
835
+ .selectFrom("scheduled_tasks")
836
+ .select("account_id")
837
+ .where("id", "=", served.taskId)
838
+ .where("org_id", "=", a.orgId)
839
+ .where("deleted_at", "is", null)
840
+ .executeTakeFirst();
841
+ if (task?.account_id !== decidedBy)
842
+ return;
843
+ const limits = await this.sandbox.ceilingLimits(a.orgId);
844
+ const summary = await this.policies.summary(a.detail.deviceId);
845
+ for (const host of hosts) {
846
+ if (networkGrantRefusal(host, { limits, deviceSummary: summary?.policy ?? null }) !== null) {
847
+ continue;
848
+ }
849
+ await this.schedules.addGrant(t, served.taskId, {
850
+ toolKey: `${NETWORK_GRANT_PREFIX}${host}`,
851
+ specHash: NETWORK_GRANT_SPEC_HASH,
852
+ connectorId: null,
853
+ });
854
+ }
855
+ }
368
856
  /**
369
857
  * 详情,不做处理人校验(内核内部用:渠道出站投递组装 IM 审批卡片);不存在返回 null。
370
858
  * 不得直接回给请求方——HTTP 接口用 {@link get}。
@@ -380,13 +868,27 @@ let ApprovalsService = class ApprovalsService {
380
868
  const [view] = await this.views([toRecord(row)]);
381
869
  return view ?? null;
382
870
  }
383
- /** 某个工具调用最近一次的审批(attempt 最大者);没有则返回 null。 */
871
+ /** 某个工具调用最近一次的工具审批(attempt 最大者,网络询问除外);没有则返回 null。 */
384
872
  async forCall(runId, callId) {
385
873
  const row = await this.db
386
874
  .selectFrom("approvals")
387
875
  .selectAll()
388
876
  .where("run_id", "=", runId)
389
877
  .where("call_id", "=", callId)
878
+ .where("kind", "=", "tool")
879
+ .orderBy("attempt", "desc")
880
+ .limit(1)
881
+ .executeTakeFirst();
882
+ return row ? toRecord(row) : null;
883
+ }
884
+ /** The network ask of a tool call (at most one per call); null when it has none. */
885
+ async networkForCall(runId, callId) {
886
+ const row = await this.db
887
+ .selectFrom("approvals")
888
+ .selectAll()
889
+ .where("run_id", "=", runId)
890
+ .where("call_id", "=", callId)
891
+ .where("kind", "=", "network")
390
892
  .orderBy("attempt", "desc")
391
893
  .limit(1)
392
894
  .executeTakeFirst();
@@ -487,14 +989,73 @@ let ApprovalsService = class ApprovalsService {
487
989
  }
488
990
  return { records, seqs };
489
991
  }
490
- /** 系统写的 `approval_decided`(超时 / 取消,处理人为 null),在当前事务内;返回事件 seq。 */
491
- async decidedEvent(a, decision) {
992
+ /**
993
+ * In the caller's transaction (`mandatory`; it wrote `authorAccountId`'s message in
994
+ * `conversationId` and holds that conversation's row lock): the message skips the pending
995
+ * network asks it closes (network ask-to-allow spec §6.7), as it skips questions: the asks of
996
+ * active runs `authorAccountId` started, in the conversation and in its sub-agents'
997
+ * conversations at any depth (their asks are shown in the root). Each becomes `skipped` with
998
+ * `approval_decided{skipped}` by the author; access stays blocked. Returns the skipped records and
999
+ * the highest seq written per conversation; the caller notifies and emits
1000
+ * `approval.decided{skipped}` after the commit, so the runs resume.
1001
+ */
1002
+ async skipNetworkIn(conversationId, authorAccountId) {
1003
+ const t = this.db.db;
1004
+ const tree = await this.subtreeOf(conversationId);
1005
+ const rows = await t
1006
+ .selectFrom("approvals as a")
1007
+ .innerJoin("runs as r", "r.id", "a.run_id")
1008
+ .selectAll("a")
1009
+ .where("a.conversation_id", "in", tree)
1010
+ .where("a.kind", "=", "network")
1011
+ .where("a.status", "=", "pending")
1012
+ .where(sql `a.expires_at > now()`)
1013
+ .where("r.status", "in", ["running", "waiting"])
1014
+ .where("r.trigger_account_id", "=", authorAccountId)
1015
+ .orderBy("a.created_at")
1016
+ .orderBy("a.id")
1017
+ .forUpdate("a")
1018
+ .execute();
1019
+ const records = [];
1020
+ const seqs = new Map();
1021
+ for (const row of rows) {
1022
+ const updated = await t
1023
+ .updateTable("approvals")
1024
+ .set({ status: "skipped", decided_by: authorAccountId, decided_at: sql `now()` })
1025
+ .where("id", "=", row.id)
1026
+ .returningAll()
1027
+ .executeTakeFirstOrThrow();
1028
+ const seq = await this.decidedEvent(toRecord(row), "skipped", authorAccountId);
1029
+ seqs.set(row.conversation_id, Math.max(seqs.get(row.conversation_id) ?? 0, seq));
1030
+ records.push(toRecord(updated));
1031
+ }
1032
+ return { records, seqs };
1033
+ }
1034
+ /** `conversationId` and its sub-agents' conversations at any depth (at most 16 levels). */
1035
+ async subtreeOf(conversationId) {
1036
+ const all = [conversationId];
1037
+ let level = [conversationId];
1038
+ for (let depth = 0; depth < 16 && level.length > 0; depth++) {
1039
+ const rows = await this.db
1040
+ .selectFrom("conversations")
1041
+ .select("id")
1042
+ .where("parent_conversation_id", "in", level)
1043
+ .execute();
1044
+ level = rows.map((r) => r.id).filter((id) => !all.includes(id));
1045
+ all.push(...level);
1046
+ }
1047
+ return all;
1048
+ }
1049
+ /**
1050
+ * 系统写的 `approval_decided`(超时 / 取消,处理人为 null;跳过,处理人为消息作者),在当前事务内;返回事件 seq。
1051
+ */
1052
+ async decidedEvent(a, decision, byAccountId = null) {
492
1053
  const written = await this.eventLog.appendIn(a.conversationId, [
493
1054
  {
494
1055
  type: "approval_decided",
495
1056
  runId: a.runId,
496
1057
  idemKey: `approval_decided:${a.id}`,
497
- payload: { approvalId: a.id, callId: a.callId, decision, byAccountId: null },
1058
+ payload: { approvalId: a.id, callId: a.callId, decision, byAccountId },
498
1059
  },
499
1060
  ]);
500
1061
  return Math.max(...written.map((e) => e.seq));
@@ -620,7 +1181,7 @@ let ApprovalsService = class ApprovalsService {
620
1181
  const prefix = app ? connectorToolKey(app.slug, "") : undefined;
621
1182
  return [
622
1183
  {
623
- ...r,
1184
+ ...publicRecord(r),
624
1185
  toolName: prefix !== undefined && r.toolKey.startsWith(prefix)
625
1186
  ? r.toolKey.slice(prefix.length)
626
1187
  : r.toolKey,
@@ -647,9 +1208,24 @@ let ApprovalsService = class ApprovalsService {
647
1208
  __decorate([
648
1209
  Transactional()
649
1210
  ], ApprovalsService.prototype, "requestIn", null);
1211
+ __decorate([
1212
+ Transactional()
1213
+ ], ApprovalsService.prototype, "requestNetworkIn", null);
1214
+ __decorate([
1215
+ WithLock({ key: "network-ask-#{1}", ttl: 60_000, waitTimeout: 45_000 })
1216
+ ], ApprovalsService.prototype, "decideNetwork", null);
1217
+ __decorate([
1218
+ Transactional()
1219
+ ], ApprovalsService.prototype, "auditLateAllow", null);
650
1220
  __decorate([
651
1221
  Transactional({ propagation: "requires_new" })
652
1222
  ], ApprovalsService.prototype, "decideIn", null);
1223
+ __decorate([
1224
+ Transactional({ propagation: "mandatory" })
1225
+ ], ApprovalsService.prototype, "keepTrialGrant", null);
1226
+ __decorate([
1227
+ Transactional({ propagation: "mandatory" })
1228
+ ], ApprovalsService.prototype, "keepTrialNetworkGrant", null);
653
1229
  __decorate([
654
1230
  Transactional()
655
1231
  ], ApprovalsService.prototype, "expireIn", null);
@@ -659,6 +1235,9 @@ __decorate([
659
1235
  __decorate([
660
1236
  Transactional({ propagation: "mandatory" })
661
1237
  ], ApprovalsService.prototype, "cancelPendingFor", null);
1238
+ __decorate([
1239
+ Transactional({ propagation: "mandatory" })
1240
+ ], ApprovalsService.prototype, "skipNetworkIn", null);
662
1241
  __decorate([
663
1242
  Transactional()
664
1243
  ], ApprovalsService.prototype, "revokeGrantIn", null);
@@ -668,6 +1247,11 @@ ApprovalsService = __decorate([
668
1247
  __param(1, Inject(KernelConfigService)),
669
1248
  __param(2, Inject(EventLog)),
670
1249
  __param(3, Inject(KernelEvents)),
671
- __param(4, Inject(TransactionHost))
1250
+ __param(4, Inject(TransactionHost)),
1251
+ __param(5, Inject(ScheduledTaskStore)),
1252
+ __param(6, Inject(ToolRegistry)),
1253
+ __param(7, Inject(DevicePolicyService)),
1254
+ __param(8, Inject(LockService)),
1255
+ __param(9, Inject(SandboxService))
672
1256
  ], ApprovalsService);
673
1257
  export { ApprovalsService };