@capybara-hq/kernel 0.1.0-rc.20 → 0.1.0-rc.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/bootstrap.json +10 -1
  2. package/dist/agents/admin-agents.d.ts +16 -3
  3. package/dist/agents/admin-agents.js +65 -7
  4. package/dist/agents/agents.schemas.d.ts +24 -6
  5. package/dist/agents/agents.service.d.ts +4 -3
  6. package/dist/agents/agents.service.js +39 -26
  7. package/dist/agents/definition.d.ts +44 -6
  8. package/dist/agents/definition.js +86 -18
  9. package/dist/agents/from-template.d.ts +6 -3
  10. package/dist/agents/from-template.js +57 -44
  11. package/dist/agents/org-templates.js +97 -41
  12. package/dist/agents/templates.d.ts +6 -0
  13. package/dist/agents/templates.js +15 -1
  14. package/dist/catalog/admin-catalog.controller.d.ts +2 -4
  15. package/dist/catalog/admin-catalog.controller.js +2 -6
  16. package/dist/catalog/catalog.module.js +3 -0
  17. package/dist/catalog/claude/report.d.ts +9 -2
  18. package/dist/catalog/claude/report.js +1 -0
  19. package/dist/catalog/claude/translate.js +1 -1
  20. package/dist/catalog/component-promotion.js +1 -2
  21. package/dist/catalog/component-state.d.ts +13 -1
  22. package/dist/catalog/component-state.js +14 -5
  23. package/dist/catalog/marketplaces.service.d.ts +1 -1
  24. package/dist/catalog/marketplaces.service.js +2 -1
  25. package/dist/catalog/org-catalog.controller.d.ts +13 -3
  26. package/dist/catalog/org-catalog.controller.js +26 -4
  27. package/dist/catalog/org-catalog.schemas.d.ts +13 -0
  28. package/dist/catalog/org-catalog.schemas.js +34 -0
  29. package/dist/catalog/org-plugin-uploads.controller.js +0 -1
  30. package/dist/catalog/org-plugin-uploads.d.ts +0 -1
  31. package/dist/catalog/org-plugin-uploads.js +0 -3
  32. package/dist/catalog/org-plugin-uploads.schemas.d.ts +0 -1
  33. package/dist/catalog/org-plugin-uploads.schemas.js +0 -2
  34. package/dist/catalog/platform-components.d.ts +0 -1
  35. package/dist/catalog/platform-components.js +3 -8
  36. package/dist/catalog/plugin-import.d.ts +0 -8
  37. package/dist/catalog/plugin-import.js +4 -35
  38. package/dist/catalog/plugin-install.d.ts +2 -2
  39. package/dist/catalog/plugin-install.js +2 -2
  40. package/dist/catalog/plugin-package.d.ts +7 -0
  41. package/dist/catalog/plugin-package.js +27 -1
  42. package/dist/catalog/plugin-upgrade.d.ts +5 -6
  43. package/dist/catalog/plugin-upgrade.js +5 -23
  44. package/dist/catalog/requests.service.d.ts +66 -10
  45. package/dist/catalog/requests.service.js +196 -15
  46. package/dist/channels/actions.d.ts +2 -0
  47. package/dist/channels/actions.js +19 -4
  48. package/dist/channels/cards.d.ts +16 -2
  49. package/dist/channels/cards.js +40 -7
  50. package/dist/channels/channels.module.js +3 -0
  51. package/dist/channels/outbound.d.ts +22 -1
  52. package/dist/channels/outbound.js +116 -7
  53. package/dist/channels/phrases.d.ts +25 -0
  54. package/dist/channels/phrases.js +19 -0
  55. package/dist/channels/threads.d.ts +12 -0
  56. package/dist/channels/threads.js +34 -0
  57. package/dist/config/schema.d.ts +23 -1
  58. package/dist/config/schema.js +105 -5
  59. package/dist/conversations/conversations.service.d.ts +42 -9
  60. package/dist/conversations/conversations.service.js +57 -25
  61. package/dist/conversations/events.d.ts +106 -11
  62. package/dist/conversations/events.js +121 -18
  63. package/dist/conversations/message-context.d.ts +6 -3
  64. package/dist/conversations/message-context.js +14 -10
  65. package/dist/conversations/queue.d.ts +15 -2
  66. package/dist/conversations/queue.js +22 -3
  67. package/dist/conversations/queue.service.d.ts +12 -6
  68. package/dist/conversations/queue.service.js +36 -13
  69. package/dist/db/kernel-schema.d.ts +105 -3
  70. package/dist/devices/conversation-devices.d.ts +8 -1
  71. package/dist/devices/conversation-devices.js +8 -0
  72. package/dist/devices/devices.controller.d.ts +21 -4
  73. package/dist/devices/devices.controller.js +46 -8
  74. package/dist/devices/devices.schemas.d.ts +9 -1
  75. package/dist/devices/devices.schemas.js +11 -3
  76. package/dist/devices/devices.service.d.ts +176 -4
  77. package/dist/devices/devices.service.js +341 -6
  78. package/dist/devices/dto.d.ts +56 -3
  79. package/dist/devices/dto.js +51 -1
  80. package/dist/devices/public-suffix.d.ts +6 -0
  81. package/dist/devices/public-suffix.js +79 -0
  82. package/dist/errors.d.ts +46 -5
  83. package/dist/errors.js +46 -5
  84. package/dist/evals/reconstruct.d.ts +18 -13
  85. package/dist/evals/reconstruct.js +50 -36
  86. package/dist/evals/recorded-tools.d.ts +7 -7
  87. package/dist/evals/recorded-tools.js +4 -3
  88. package/dist/evals/trajectory.d.ts +6 -1
  89. package/dist/evals/trajectory.js +7 -0
  90. package/dist/extensions/bootstrap.d.ts +49 -10
  91. package/dist/extensions/bootstrap.js +104 -27
  92. package/dist/extensions/package.js +2 -2
  93. package/dist/http/api.module.js +5 -0
  94. package/dist/inbox/inbox.module.js +2 -1
  95. package/dist/inbox/inbox.service.d.ts +19 -4
  96. package/dist/inbox/inbox.service.js +14 -3
  97. package/dist/infra/config/kernel-config.service.js +11 -0
  98. package/dist/infra/events/kernel-events.d.ts +2 -1
  99. package/dist/lifecycle/kernel-lifecycle.d.ts +5 -1
  100. package/dist/lifecycle/kernel-lifecycle.js +16 -4
  101. package/dist/local-devices/device-policy.service.d.ts +65 -0
  102. package/dist/local-devices/device-policy.service.js +217 -0
  103. package/dist/local-devices/local-device-gateway.js +4 -1
  104. package/dist/local-devices/local-device-provider.d.ts +40 -3
  105. package/dist/local-devices/local-device-provider.js +109 -7
  106. package/dist/local-devices/local-devices.module.d.ts +2 -1
  107. package/dist/local-devices/local-devices.module.js +19 -3
  108. package/dist/local-devices/machines.d.ts +14 -3
  109. package/dist/local-devices/machines.js +41 -3
  110. package/dist/local-devices/network-grants.d.ts +104 -0
  111. package/dist/local-devices/network-grants.js +288 -0
  112. package/dist/local-devices/policy-summary.d.ts +20 -0
  113. package/dist/local-devices/policy-summary.js +63 -0
  114. package/dist/orgs/orgs.module.js +4 -10
  115. package/dist/orgs/roles.d.ts +1 -1
  116. package/dist/orgs/roles.js +4 -0
  117. package/dist/orgs/settings.controller.js +2 -1
  118. package/dist/orgs/settings.service.d.ts +164 -12
  119. package/dist/orgs/settings.service.js +212 -24
  120. package/dist/platform/exclusions.js +11 -1
  121. package/dist/platform/operation.js +4 -0
  122. package/dist/platform/ops/admin-items.ops.js +1 -1
  123. package/dist/platform/ops/agent-skills.ops.js +2 -2
  124. package/dist/platform/ops/agent-templates.ops.js +2 -2
  125. package/dist/platform/ops/agents.ops.js +11 -15
  126. package/dist/platform/ops/devices.ops.js +3 -1
  127. package/dist/platform/ops/inbox.ops.js +19 -0
  128. package/dist/platform/ops/org-settings.ops.js +14 -5
  129. package/dist/platform/ops/platform.ops.d.ts +9 -1
  130. package/dist/platform/ops/platform.ops.js +34 -12
  131. package/dist/platform/ops/requests.ops.js +3 -2
  132. package/dist/platform/ops/schedules.ops.d.ts +21 -0
  133. package/dist/platform/ops/schedules.ops.js +470 -0
  134. package/dist/platform/ops/skills.ops.js +3 -3
  135. package/dist/platform/platform-ops.module.d.ts +2 -1
  136. package/dist/platform/platform-ops.module.js +5 -0
  137. package/dist/platform/route-schemas.js +3 -0
  138. package/dist/roles/worker.module.js +3 -0
  139. package/dist/runtime/context/build.js +13 -4
  140. package/dist/runtime/context/compaction.js +6 -3
  141. package/dist/runtime/context/device-paths.d.ts +43 -21
  142. package/dist/runtime/context/device-paths.js +104 -21
  143. package/dist/runtime/context/run-context.d.ts +9 -0
  144. package/dist/runtime/context/run-context.js +8 -0
  145. package/dist/runtime/context/skills.d.ts +14 -1
  146. package/dist/runtime/context/skills.js +36 -5
  147. package/dist/runtime/maintenance.d.ts +4 -2
  148. package/dist/runtime/maintenance.js +16 -5
  149. package/dist/runtime/run-finish.d.ts +6 -1
  150. package/dist/runtime/run-finish.js +16 -2
  151. package/dist/runtime/run-queue.js +21 -2
  152. package/dist/runtime/run-worker.js +14 -33
  153. package/dist/runtime/runtime.module.js +3 -0
  154. package/dist/runtime/subagents/definition.d.ts +32 -14
  155. package/dist/runtime/subagents/definition.js +106 -109
  156. package/dist/runtime/subagents/dispatch.js +4 -3
  157. package/dist/runtime/subagents/preamble.d.ts +2 -1
  158. package/dist/runtime/subagents/preamble.js +2 -1
  159. package/dist/runtime/waits.js +14 -4
  160. package/dist/sandbox/sandbox.service.d.ts +120 -10
  161. package/dist/sandbox/sandbox.service.js +335 -28
  162. package/dist/sandbox/tiers.d.ts +86 -0
  163. package/dist/sandbox/tiers.js +96 -0
  164. package/dist/schedules/effective-settings.d.ts +16 -0
  165. package/dist/schedules/effective-settings.js +17 -0
  166. package/dist/schedules/errors.d.ts +8 -0
  167. package/dist/schedules/errors.js +8 -0
  168. package/dist/schedules/fire.d.ts +118 -0
  169. package/dist/schedules/fire.js +625 -0
  170. package/dist/schedules/grant-rules.d.ts +94 -0
  171. package/dist/schedules/grant-rules.js +126 -0
  172. package/dist/schedules/job-ids.d.ts +32 -0
  173. package/dist/schedules/job-ids.js +27 -0
  174. package/dist/schedules/pause.d.ts +9 -0
  175. package/dist/schedules/pause.js +33 -0
  176. package/dist/schedules/run-tracker.d.ts +81 -0
  177. package/dist/schedules/run-tracker.js +333 -0
  178. package/dist/schedules/scan.job.d.ts +64 -0
  179. package/dist/schedules/scan.job.js +303 -0
  180. package/dist/schedules/schedule-math.d.ts +31 -0
  181. package/dist/schedules/schedule-math.js +223 -0
  182. package/dist/schedules/scheduled-tasks.service.d.ts +351 -0
  183. package/dist/schedules/scheduled-tasks.service.js +1193 -0
  184. package/dist/schedules/schedules.controller.d.ts +68 -0
  185. package/dist/schedules/schedules.controller.js +242 -0
  186. package/dist/schedules/schedules.module.d.ts +16 -0
  187. package/dist/schedules/schedules.module.js +65 -0
  188. package/dist/schedules/schedules.schemas.d.ts +316 -0
  189. package/dist/schedules/schedules.schemas.js +186 -0
  190. package/dist/schedules/store.d.ts +81 -0
  191. package/dist/schedules/store.js +390 -0
  192. package/dist/schedules/store.module.d.ts +6 -0
  193. package/dist/schedules/store.module.js +21 -0
  194. package/dist/schedules/types.d.ts +108 -0
  195. package/dist/schedules/types.js +1 -0
  196. package/dist/schedules/unattended.d.ts +117 -0
  197. package/dist/schedules/unattended.js +228 -0
  198. package/dist/setup/oauth-token-probe.js +8 -4
  199. package/dist/skills/agent-skills.service.d.ts +4 -8
  200. package/dist/skills/agent-skills.service.js +18 -53
  201. package/dist/skills/catalog.d.ts +65 -62
  202. package/dist/skills/catalog.js +109 -206
  203. package/dist/skills/conversation-catalog.d.ts +3 -3
  204. package/dist/skills/conversation-catalog.js +5 -12
  205. package/dist/skills/invoke.d.ts +2 -2
  206. package/dist/skills/skills.controller.d.ts +3 -1
  207. package/dist/skills/skills.service.d.ts +38 -37
  208. package/dist/skills/skills.service.js +168 -107
  209. package/dist/skills/usable.d.ts +57 -8
  210. package/dist/skills/usable.js +172 -38
  211. package/dist/tools/approvals.controller.d.ts +4 -2
  212. package/dist/tools/approvals.controller.js +17 -2
  213. package/dist/tools/approvals.schemas.d.ts +1 -0
  214. package/dist/tools/approvals.schemas.js +3 -1
  215. package/dist/tools/approvals.service.d.ts +182 -7
  216. package/dist/tools/approvals.service.js +597 -13
  217. package/dist/tools/builtin-names.d.ts +25 -4
  218. package/dist/tools/builtin-names.js +45 -4
  219. package/dist/tools/builtins/builtin-tools.d.ts +8 -0
  220. package/dist/tools/builtins/builtin-tools.js +23 -2
  221. package/dist/tools/builtins/sandbox/bash.js +16 -7
  222. package/dist/tools/builtins/sandbox/common.d.ts +1 -1
  223. package/dist/tools/builtins/sandbox/common.js +33 -10
  224. package/dist/tools/builtins/sandbox/edit.js +7 -3
  225. package/dist/tools/builtins/sandbox/file-state.d.ts +31 -1
  226. package/dist/tools/builtins/sandbox/file-state.js +33 -3
  227. package/dist/tools/builtins/sandbox/write.js +8 -4
  228. package/dist/tools/builtins/schedules.d.ts +107 -0
  229. package/dist/tools/builtins/schedules.js +789 -0
  230. package/dist/tools/elicitation.d.ts +2 -0
  231. package/dist/tools/elicitation.js +1 -1
  232. package/dist/tools/fixed-ask.d.ts +7 -0
  233. package/dist/tools/fixed-ask.js +9 -0
  234. package/dist/tools/network-asks.d.ts +109 -0
  235. package/dist/tools/network-asks.js +221 -0
  236. package/dist/tools/pipeline.d.ts +53 -6
  237. package/dist/tools/pipeline.js +202 -13
  238. package/dist/tools/policy.d.ts +21 -8
  239. package/dist/tools/policy.js +63 -9
  240. package/dist/tools/registry.d.ts +10 -1
  241. package/dist/tools/tools.module.js +12 -0
  242. package/package.json +2 -2
  243. package/src/migrations/0113_scheduled_tasks.sql +108 -0
  244. package/src/migrations/0114_network_asks.sql +23 -0
  245. package/src/migrations/0115_network_requests.sql +28 -0
  246. package/src/migrations/0116_skills_default_on.sql +37 -0
  247. package/src/migrations/0117_sandbox_tiers.sql +11 -0
@@ -5,13 +5,31 @@ import { KernelDb } from "../infra/database/kernel-db.js";
5
5
  import { TransactionHost } from "../infra/database/transaction-host.js";
6
6
  import { LockService } from "../infra/lock/lock.service.js";
7
7
  import { OrgLock } from "../orgs/org-lock.js";
8
+ import { OrgSettingsService } from "../orgs/settings.service.js";
8
9
  import { CatalogService, type MarketplaceImportResult } from "./catalog.service.js";
9
10
  import { CatalogScopeService } from "./catalog-scope.service.js";
10
11
  import { OrgCatalogService } from "./org-catalog.service.js";
11
12
  import { PluginImport } from "./plugin-import.js";
12
13
  import type { RequestActorOpts } from "./request-rules.js";
13
- export type CatalogRequestKind = "access" | "import" | "enable" | "connector_setup";
14
+ export type CatalogRequestKind = "access" | "import" | "enable" | "connector_setup" | "network_allowlist";
14
15
  export type CatalogRequestStatus = "pending" | "approved" | "rejected" | "cancelled";
16
+ /**
17
+ * What a `network_allowlist` request asks for (network ask-to-allow spec §5.3): the hosts (sorted,
18
+ * lowercased, exact) and the conversation and agent of the blocked command, when the member gave
19
+ * them. Stored in `catalog_requests.detail`; the member's note is the request's `note`.
20
+ */
21
+ export interface NetworkRequestDetail {
22
+ hosts: string[];
23
+ conversationId: string | null;
24
+ agentId: string | null;
25
+ }
26
+ /** A member's network request input (`POST /orgs/:orgId/catalog-requests/network`). */
27
+ export interface NetworkRequestInput {
28
+ hosts: string[];
29
+ note?: string | undefined;
30
+ conversationId?: string | undefined;
31
+ agentId?: string | undefined;
32
+ }
15
33
  /** A pending connector a `connector_setup` request waits for (ruling P4-T6-5). */
16
34
  export interface SetupRequestConnector {
17
35
  /** The agent's `pendingConnectors[].key` (the bind route's `:key`). */
@@ -81,6 +99,8 @@ export interface CatalogRequestView {
81
99
  * other kinds.
82
100
  */
83
101
  connectors: SetupRequestConnector[] | null;
102
+ /** The hosts, conversation and agent of a `network_allowlist` request; null for other kinds. */
103
+ network: NetworkRequestDetail | null;
84
104
  status: CatalogRequestStatus;
85
105
  note: string;
86
106
  /** The optional reason of a rejection. */
@@ -99,6 +119,8 @@ export interface CatalogDecision {
99
119
  approve: boolean;
100
120
  reason?: string | undefined;
101
121
  }
122
+ /** The most pending `network_allowlist` requests one member may have in an org. */
123
+ export declare const NETWORK_REQUESTS_PENDING_MAX = 10;
102
124
  /** 申请列表一次最多返回的条数。 */
103
125
  export declare const CATALOG_REQUESTS_LIST_MAX = 200;
104
126
  /**
@@ -115,7 +137,9 @@ export declare const CATALOG_REQUESTS_LIST_MAX = 200;
115
137
  * - `connector_setup` (ruling P4-7): one per agent (`agent_id`, `pending_keys`), asking the org
116
138
  * admins to set up an agent's pending connectors; written by the agents module
117
139
  * (`PendingConnectorRequests`), closed by binding (`approved`) or the agent's deletion
118
- * (`cancelled`); here it is listed and rejected.
140
+ * (`cancelled`); here it is listed and rejected;
141
+ * - `network_allowlist` (network ask-to-allow spec §5.3): a member asks the org admins to add hosts
142
+ * to the org's sandbox egress allowlist; approving merges them into the org's `sandboxEgress`.
119
143
  * A rejection stores an optional reason. Decided requests show in the requester's inbox until
120
144
  * dismissed. No in-process state.
121
145
  */
@@ -129,12 +153,16 @@ export declare class CatalogRequestsService {
129
153
  private readonly catalog;
130
154
  private readonly config;
131
155
  private readonly pluginImport;
156
+ /** Approving a network request writes the org's `sandboxEgress` through the settings path. */
157
+ private readonly orgSettings;
132
158
  readonly txHost: TransactionHost;
133
159
  /** Serializes the approvals of one `import` request (`@WithLock`). */
134
160
  readonly lockService: LockService;
135
161
  constructor(kdb: KernelDb,
136
162
  /** The org row and super admin locks: both join the caller's transaction. */
137
- orgLock: OrgLock, adminLock: SuperAdminLock, orgCatalog: OrgCatalogService, scope: CatalogScopeService, catalog: CatalogService, config: KernelConfigService, pluginImport: PluginImport, txHost: TransactionHost,
163
+ orgLock: OrgLock, adminLock: SuperAdminLock, orgCatalog: OrgCatalogService, scope: CatalogScopeService, catalog: CatalogService, config: KernelConfigService, pluginImport: PluginImport,
164
+ /** Approving a network request writes the org's `sandboxEgress` through the settings path. */
165
+ orgSettings: OrgSettingsService, txHost: TransactionHost,
138
166
  /** Serializes the approvals of one `import` request (`@WithLock`). */
139
167
  lockService: LockService);
140
168
  /** The kernel-schema handle of the current context (the transaction, else the pool). */
@@ -155,6 +183,19 @@ export declare class CatalogRequestsService {
155
183
  /** The role check alone, before any item error (a member cannot probe items by error codes). */
156
184
  private authorize;
157
185
  private writeItemRequest;
186
+ /**
187
+ * A member asks the org admins to allow hosts in the org's sandbox egress (`POST /orgs/:orgId/
188
+ * catalog-requests/network`, `catalog.request`; spec §5.3). The hosts arrive validated (exact,
189
+ * lowercased) and are deduped and sorted here. The deployment egress `none` ->
190
+ * `catalog.request_invalid` (only a super admin can turn sandbox network on); the org's own
191
+ * egress `none` -> `catalog.request_invalid`; {@link NETWORK_REQUESTS_PENDING_MAX} pending
192
+ * requests of the member already -> `catalog.request_invalid`; every host already
193
+ * allowed by the org's effective egress -> `catalog.request_invalid`; a conversation or agent not
194
+ * in the org -> `catalog.request_invalid`. A pending request of the same requester for the same
195
+ * host set is returned as is (no second request). Audits `catalog.requested`.
196
+ */
197
+ requestNetwork(orgId: string, actor: string, input: NetworkRequestInput, opts?: RequestActorOpts): Promise<CatalogRequestView>;
198
+ private writeNetworkRequest;
158
199
  /**
159
200
  * An org admin asks for a marketplace entry to be imported (`POST /orgs/:orgId/
160
201
  * marketplace-requests`, `catalog.decide`). The deployment setting `orgMarketplaceBrowse` off ->
@@ -173,17 +214,21 @@ export declare class CatalogRequestsService {
173
214
  }, opts?: RequestActorOpts): Promise<CatalogRequestView>;
174
215
  private writeImportRequest;
175
216
  /**
176
- * An org admin decides a member's `enable` or `connector_setup` request (`catalog.decide`). Not
177
- * this org's request of those kinds -> `catalog.request_not_found`; decided ->
178
- * `catalog.request_decided`. Approving an `enable` request switches the item on in the org as the
179
- * deciding admin ({@link OrgCatalogService.setSwitch}: its errors, e.g. inputs the install
217
+ * An org admin decides a member's `enable`, `network_allowlist` or `connector_setup` request
218
+ * (`catalog.decide`). Not this org's request of those kinds -> `catalog.request_not_found`;
219
+ * decided -> `catalog.request_decided`. Approving a `network_allowlist` request merges its hosts
220
+ * into the org's `sandboxEgress` (`approveNetwork`). Approving an `enable` request switches the
221
+ * item on in the org as the deciding admin ({@link OrgCatalogService.setSwitch}: its errors, e.g. inputs the install
180
222
  * needs, leave the request pending); the install itself resolves the request. A
181
223
  * `connector_setup` request is approved only by binding its connectors (ruling P4-T6-6):
182
224
  * approving it -> `catalog.request_invalid`. Rejecting stores the reason. Audits
183
225
  * `catalog.request_decided`.
184
226
  */
185
227
  decideOrg(orgId: string, actor: string, requestId: string, decision: CatalogDecision, opts?: RequestActorOpts): Promise<CatalogRequestView>;
186
- /** The item of a pending `enable` request of the org (the decider re-checked). */
228
+ /**
229
+ * The item of a pending `enable` request of the org, null for a `network_allowlist` one (the
230
+ * decider re-checked).
231
+ */
187
232
  private pendingOrgRequest;
188
233
  /**
189
234
  * A super admin decides an `access` or `import` request (re-checked inside the super
@@ -218,6 +263,17 @@ export declare class CatalogRequestsService {
218
263
  * approval of an `enable` request may find it approved already by the install it triggered.
219
264
  */
220
265
  private writeDecision;
266
+ /**
267
+ * Approves a `network_allowlist` request inside its decision's transaction: the hosts the org's
268
+ * effective egress does not allow yet are added to the org's `sandboxEgress.allowHosts` through
269
+ * {@link OrgSettingsService.update} (audited `org.settings_updated` with `requestId`). An org
270
+ * following the deployment default starts from the deployment's list (it keeps its own list from
271
+ * then on). The deployment egress `none`, or the org's `none` -> `catalog.request_invalid`; more
272
+ * than {@link ORG_SANDBOX_ALLOW_HOSTS_MAX} hosts -> `catalog.request_invalid`; decided with an API
273
+ * token -> `org.sandbox_egress_not_allowed` (only a person widens the network). Hosts all allowed
274
+ * already: nothing is written.
275
+ */
276
+ private approveNetwork;
221
277
  /**
222
278
  * The requester dismisses a decided request from the inbox (`POST /orgs/:orgId/
223
279
  * catalog-requests/:id/dismiss`). Not the requester's decided request in this org ->
@@ -232,8 +288,8 @@ export declare class CatalogRequestsService {
232
288
  countPendingDeployment(): Promise<number>;
233
289
  /**
234
290
  * The inbox: when the account is an org admin of the org (the role read live), the members'
235
- * pending `enable` requests and the pending `connector_setup` requests, newest first; otherwise
236
- * none.
291
+ * pending `enable` and `network_allowlist` requests and the pending `connector_setup` requests,
292
+ * newest first; otherwise none.
237
293
  */
238
294
  listForOrgAdmin(orgId: string, accountId: string): Promise<CatalogRequestView[]>;
239
295
  /** The inbox: the account's decided requests in the org it has not dismissed, newest first. */
@@ -20,14 +20,19 @@ import { TransactionHost } from "../infra/database/transaction-host.js";
20
20
  import { Transactional } from "../infra/database/transactional.decorator.js";
21
21
  import { LockService } from "../infra/lock/lock.service.js";
22
22
  import { WithLock } from "../infra/lock/with-lock.decorator.js";
23
+ import { orgAllows } from "../local-devices/network-grants.js";
23
24
  import { OrgLock } from "../orgs/org-lock.js";
24
25
  import { roleAtLeast } from "../orgs/roles.js";
26
+ import { ORG_SANDBOX_ALLOW_HOSTS_MAX, OrgSettingsService, readOrgSettings, } from "../orgs/settings.service.js";
27
+ import { baseSandboxEgress } from "../sandbox/tiers.js";
25
28
  import { CatalogService } from "./catalog.service.js";
26
29
  import { CatalogScopeService } from "./catalog-scope.service.js";
27
30
  import { assertEntryNotDeprecated, loadMarketplaceEntry } from "./marketplaces.service.js";
28
31
  import { OrgCatalogService } from "./org-catalog.service.js";
29
32
  import { PluginImport } from "./plugin-import.js";
30
33
  import { itemsSeenBy } from "./scope-state.js";
34
+ /** The most pending `network_allowlist` requests one member may have in an org. */
35
+ export const NETWORK_REQUESTS_PENDING_MAX = 10;
31
36
  /** 申请列表一次最多返回的条数。 */
32
37
  export const CATALOG_REQUESTS_LIST_MAX = 200;
33
38
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -36,12 +41,13 @@ const itemNotFound = () => new ApiError("catalog.item_not_found", "Catalog item
36
41
  const itemDisabled = () => new ApiError("catalog.item_disabled", "This catalog item is disabled by the super admin and cannot be requested");
37
42
  const alreadyDecided = () => new ApiError("catalog.request_decided", "This request has already been decided");
38
43
  const pendingExists = () => new ApiError("catalog.request_pending", "This organization already has a pending request of this kind for it");
44
+ const deploymentEgressNone = () => new ApiError("catalog.request_invalid", "The platform turned sandbox network off; only a super admin can turn it on");
39
45
  /** A unique violation (the partial index of pending requests). */
40
46
  const isUniqueViolation = (e) => typeof e === "object" && e !== null && e.code === "23505";
41
47
  /** The deployment-level kinds (decided by the super admin). */
42
48
  const DEPLOYMENT_KINDS = ["access", "import"];
43
49
  /** The org-level kinds (decided by the org admins). */
44
- const ORG_KINDS = ["enable", "connector_setup"];
50
+ const ORG_KINDS = ["enable", "connector_setup", "network_allowlist"];
45
51
  /**
46
52
  * Catalog requests under plugin scope (spec §5.1). Three kinds, one pending request per (org,
47
53
  * kind, item or entry) (partial unique indexes; org-side writes run inside the org row lock):
@@ -56,7 +62,9 @@ const ORG_KINDS = ["enable", "connector_setup"];
56
62
  * - `connector_setup` (ruling P4-7): one per agent (`agent_id`, `pending_keys`), asking the org
57
63
  * admins to set up an agent's pending connectors; written by the agents module
58
64
  * (`PendingConnectorRequests`), closed by binding (`approved`) or the agent's deletion
59
- * (`cancelled`); here it is listed and rejected.
65
+ * (`cancelled`); here it is listed and rejected;
66
+ * - `network_allowlist` (network ask-to-allow spec §5.3): a member asks the org admins to add hosts
67
+ * to the org's sandbox egress allowlist; approving merges them into the org's `sandboxEgress`.
60
68
  * A rejection stores an optional reason. Decided requests show in the requester's inbox until
61
69
  * dismissed. No in-process state.
62
70
  */
@@ -69,11 +77,14 @@ let CatalogRequestsService = class CatalogRequestsService {
69
77
  catalog;
70
78
  config;
71
79
  pluginImport;
80
+ orgSettings;
72
81
  txHost;
73
82
  lockService;
74
83
  constructor(kdb,
75
84
  /** The org row and super admin locks: both join the caller's transaction. */
76
- orgLock, adminLock, orgCatalog, scope, catalog, config, pluginImport, txHost,
85
+ orgLock, adminLock, orgCatalog, scope, catalog, config, pluginImport,
86
+ /** Approving a network request writes the org's `sandboxEgress` through the settings path. */
87
+ orgSettings, txHost,
77
88
  /** Serializes the approvals of one `import` request (`@WithLock`). */
78
89
  lockService) {
79
90
  this.kdb = kdb;
@@ -84,6 +95,7 @@ let CatalogRequestsService = class CatalogRequestsService {
84
95
  this.catalog = catalog;
85
96
  this.config = config;
86
97
  this.pluginImport = pluginImport;
98
+ this.orgSettings = orgSettings;
87
99
  this.txHost = txHost;
88
100
  this.lockService = lockService;
89
101
  }
@@ -170,6 +182,113 @@ let CatalogRequestsService = class CatalogRequestsService {
170
182
  });
171
183
  return id;
172
184
  }
185
+ /**
186
+ * A member asks the org admins to allow hosts in the org's sandbox egress (`POST /orgs/:orgId/
187
+ * catalog-requests/network`, `catalog.request`; spec §5.3). The hosts arrive validated (exact,
188
+ * lowercased) and are deduped and sorted here. The deployment egress `none` ->
189
+ * `catalog.request_invalid` (only a super admin can turn sandbox network on); the org's own
190
+ * egress `none` -> `catalog.request_invalid`; {@link NETWORK_REQUESTS_PENDING_MAX} pending
191
+ * requests of the member already -> `catalog.request_invalid`; every host already
192
+ * allowed by the org's effective egress -> `catalog.request_invalid`; a conversation or agent not
193
+ * in the org -> `catalog.request_invalid`. A pending request of the same requester for the same
194
+ * host set is returned as is (no second request). Audits `catalog.requested`.
195
+ */
196
+ async requestNetwork(orgId, actor, input, opts = {}) {
197
+ const id = await this.writeNetworkRequest(orgId, actor, input, opts);
198
+ return this.view(id);
199
+ }
200
+ async writeNetworkRequest(orgId, actor, input, opts) {
201
+ const t = this.db;
202
+ await this.orgLock.lockAndAuthorize(orgId, actor, "catalog.request");
203
+ const hosts = [...new Set(input.hosts.map((h) => h.toLowerCase()))].sort();
204
+ const deployment = this.orgSettings.deployment().sandboxEgress;
205
+ if (deployment.mode === "none")
206
+ throw deploymentEgressNone();
207
+ const settings = await readOrgSettings(t, orgId);
208
+ if (settings.sandboxEgress?.mode === "none") {
209
+ throw new ApiError("catalog.request_invalid", "The organization has sandbox network turned off; ask an admin to turn it on first.");
210
+ }
211
+ // the org's base list: the hosts a strict device reaches (the tiers only widen it)
212
+ const effective = baseSandboxEgress({ egress: deployment.mode, allowHosts: deployment.allowHosts }, settings.sandboxEgress);
213
+ if (hosts.every((h) => orgAllows(h, effective))) {
214
+ throw new ApiError("catalog.request_invalid", "This organization's sandbox network already allows these hosts; nothing to request");
215
+ }
216
+ if (input.conversationId !== undefined) {
217
+ const c = await t
218
+ .selectFrom("conversations")
219
+ .select("id")
220
+ .where("id", "=", input.conversationId)
221
+ .where("org_id", "=", orgId)
222
+ .executeTakeFirst();
223
+ if (!c) {
224
+ throw new ApiError("catalog.request_invalid", "The conversation is not in this organization");
225
+ }
226
+ }
227
+ if (input.agentId !== undefined) {
228
+ const a = await t
229
+ .selectFrom("agents")
230
+ .select("id")
231
+ .where("id", "=", input.agentId)
232
+ .where("org_id", "=", orgId)
233
+ .executeTakeFirst();
234
+ if (!a) {
235
+ throw new ApiError("catalog.request_invalid", "The agent is not in this organization");
236
+ }
237
+ }
238
+ // The same requester asking for the same hosts again gets the pending request back (the org
239
+ // row lock above serializes concurrent asks)
240
+ const existing = await t
241
+ .selectFrom("catalog_requests")
242
+ .select("id")
243
+ .where("org_id", "=", orgId)
244
+ .where("kind", "=", "network_allowlist")
245
+ .where("requested_by", "=", actor)
246
+ .where("status", "=", "pending")
247
+ .where(sql `detail -> 'hosts' = ${JSON.stringify(hosts)}::jsonb`)
248
+ .executeTakeFirst();
249
+ if (existing)
250
+ return existing.id;
251
+ const pending = await t
252
+ .selectFrom("catalog_requests")
253
+ .select(sql `count(*)`.as("n"))
254
+ .where("org_id", "=", orgId)
255
+ .where("kind", "=", "network_allowlist")
256
+ .where("requested_by", "=", actor)
257
+ .where("status", "=", "pending")
258
+ .executeTakeFirstOrThrow();
259
+ if (Number(pending.n) >= NETWORK_REQUESTS_PENDING_MAX) {
260
+ throw new ApiError("catalog.request_invalid", "You have too many pending domain requests; wait for an admin to handle them.");
261
+ }
262
+ const detail = {
263
+ hosts,
264
+ conversationId: input.conversationId ?? null,
265
+ agentId: input.agentId ?? null,
266
+ };
267
+ const id = randomUUID();
268
+ await t
269
+ .insertInto("catalog_requests")
270
+ .values({
271
+ id,
272
+ org_id: orgId,
273
+ item_id: null,
274
+ kind: "network_allowlist",
275
+ // still written for the old NOT NULL / CHECK, never read
276
+ stage: "org",
277
+ detail: JSON.stringify(detail),
278
+ requested_by: actor,
279
+ note: input.note ?? "",
280
+ })
281
+ .execute();
282
+ await audit(t, {
283
+ actorAccountId: actor,
284
+ ...(opts.actorTokenId !== undefined && { actorTokenId: opts.actorTokenId }),
285
+ orgId,
286
+ action: "catalog.requested",
287
+ target: id,
288
+ detail: { kind: "network_allowlist", hosts, ...opts.auditDetail },
289
+ });
290
+ return id;
291
+ }
173
292
  /**
174
293
  * An org admin asks for a marketplace entry to be imported (`POST /orgs/:orgId/
175
294
  * marketplace-requests`, `catalog.decide`). The deployment setting `orgMarketplaceBrowse` off ->
@@ -254,10 +373,11 @@ let CatalogRequestsService = class CatalogRequestsService {
254
373
  return id;
255
374
  }
256
375
  /**
257
- * An org admin decides a member's `enable` or `connector_setup` request (`catalog.decide`). Not
258
- * this org's request of those kinds -> `catalog.request_not_found`; decided ->
259
- * `catalog.request_decided`. Approving an `enable` request switches the item on in the org as the
260
- * deciding admin ({@link OrgCatalogService.setSwitch}: its errors, e.g. inputs the install
376
+ * An org admin decides a member's `enable`, `network_allowlist` or `connector_setup` request
377
+ * (`catalog.decide`). Not this org's request of those kinds -> `catalog.request_not_found`;
378
+ * decided -> `catalog.request_decided`. Approving a `network_allowlist` request merges its hosts
379
+ * into the org's `sandboxEgress` (`approveNetwork`). Approving an `enable` request switches the
380
+ * item on in the org as the deciding admin ({@link OrgCatalogService.setSwitch}: its errors, e.g. inputs the install
261
381
  * needs, leave the request pending); the install itself resolves the request. A
262
382
  * `connector_setup` request is approved only by binding its connectors (ruling P4-T6-6):
263
383
  * approving it -> `catalog.request_invalid`. Rejecting stores the reason. Audits
@@ -268,12 +388,18 @@ let CatalogRequestsService = class CatalogRequestsService {
268
388
  throw requestNotFound();
269
389
  if (decision.approve) {
270
390
  const itemId = await this.pendingOrgRequest(orgId, actor, requestId);
271
- await this.orgCatalog.setSwitch(orgId, actor, itemId, { enabled: true }, opts.actorTokenId);
391
+ // a network request is approved inside the decision (writeDecision), under the row lock
392
+ if (itemId !== null) {
393
+ await this.orgCatalog.setSwitch(orgId, actor, itemId, { enabled: true }, opts.actorTokenId);
394
+ }
272
395
  }
273
396
  await this.writeDecision(requestId, actor, decision, opts, { orgId });
274
397
  return this.view(requestId);
275
398
  }
276
- /** The item of a pending `enable` request of the org (the decider re-checked). */
399
+ /**
400
+ * The item of a pending `enable` request of the org, null for a `network_allowlist` one (the
401
+ * decider re-checked).
402
+ */
277
403
  async pendingOrgRequest(orgId, actor, requestId) {
278
404
  await this.orgLock.lockAndAuthorize(orgId, actor, "catalog.decide");
279
405
  const row = await this.db
@@ -290,6 +416,8 @@ let CatalogRequestsService = class CatalogRequestsService {
290
416
  if (row.kind === "connector_setup") {
291
417
  throw new ApiError("catalog.request_invalid", "A connector setup request is resolved by binding the agent's connectors; reject it to refuse");
292
418
  }
419
+ if (row.kind === "network_allowlist")
420
+ return null;
293
421
  if (row.item_id === null)
294
422
  throw requestNotFound();
295
423
  return row.item_id;
@@ -416,7 +544,7 @@ let CatalogRequestsService = class CatalogRequestsService {
416
544
  await this.adminLock.lockAndAuthorize(actor);
417
545
  let q = t
418
546
  .selectFrom("catalog_requests")
419
- .select(["id", "org_id", "item_id", "kind", "status", "decided_by"])
547
+ .select(["id", "org_id", "item_id", "kind", "status", "decided_by", "detail"])
420
548
  .where("id", "=", requestId);
421
549
  q =
422
550
  "orgId" in side
@@ -448,6 +576,9 @@ let CatalogRequestsService = class CatalogRequestsService {
448
576
  throw requestNotFound();
449
577
  await this.scope.addOrg(actor, row.item_id, row.org_id, { ...opts, requestId });
450
578
  }
579
+ if (decision.approve && row.kind === "network_allowlist") {
580
+ await this.approveNetwork(row.org_id, actor, requestId, row.detail, opts);
581
+ }
451
582
  // An `import` request whose entry is in the catalog already: the org joins that item's list
452
583
  const grantItem = "grantItem" in side ? side.grantItem : undefined;
453
584
  if (decision.approve && row.kind === "import" && grantItem !== undefined) {
@@ -478,6 +609,39 @@ let CatalogRequestsService = class CatalogRequestsService {
478
609
  },
479
610
  });
480
611
  }
612
+ /**
613
+ * Approves a `network_allowlist` request inside its decision's transaction: the hosts the org's
614
+ * effective egress does not allow yet are added to the org's `sandboxEgress.allowHosts` through
615
+ * {@link OrgSettingsService.update} (audited `org.settings_updated` with `requestId`). An org
616
+ * following the deployment default starts from the deployment's list (it keeps its own list from
617
+ * then on). The deployment egress `none`, or the org's `none` -> `catalog.request_invalid`; more
618
+ * than {@link ORG_SANDBOX_ALLOW_HOSTS_MAX} hosts -> `catalog.request_invalid`; decided with an API
619
+ * token -> `org.sandbox_egress_not_allowed` (only a person widens the network). Hosts all allowed
620
+ * already: nothing is written.
621
+ */
622
+ async approveNetwork(orgId, actor, requestId, detail, opts) {
623
+ const hosts = detail?.hosts ?? [];
624
+ const deployment = this.orgSettings.deployment().sandboxEgress;
625
+ if (deployment.mode === "none")
626
+ throw deploymentEgressNone();
627
+ const current = (await readOrgSettings(this.db, orgId)).sandboxEgress;
628
+ if (current?.mode === "none") {
629
+ throw new ApiError("catalog.request_invalid", "The organization has sandbox network turned off; turn it on in the organization settings first");
630
+ }
631
+ // the org's base list: the hosts a strict device reaches (the tiers only widen it)
632
+ const effective = baseSandboxEgress({ egress: deployment.mode, allowHosts: deployment.allowHosts }, current);
633
+ const missing = hosts.filter((h) => !orgAllows(h, effective));
634
+ if (missing.length === 0)
635
+ return;
636
+ const base = current?.allowHosts ?? deployment.allowHosts;
637
+ const allowHosts = [...new Set([...base, ...missing])];
638
+ if (allowHosts.length > ORG_SANDBOX_ALLOW_HOSTS_MAX) {
639
+ throw new ApiError("catalog.request_invalid", `The organization's sandbox allowlist would exceed ${ORG_SANDBOX_ALLOW_HOSTS_MAX} hosts; remove some in the organization settings first`);
640
+ }
641
+ // approving widens the org's base list: only a person in a session does that, never an API
642
+ // token (sandbox dev tiers spec §7.2; the Platform tool has no decide operation at all)
643
+ await this.orgSettings.update(orgId, { sandboxEgress: { mode: "allowlist", allowHosts } }, actor, opts.actorTokenId, { requestId }, { onlyNarrows: opts.actorTokenId !== undefined });
644
+ }
481
645
  /**
482
646
  * The requester dismisses a decided request from the inbox (`POST /orgs/:orgId/
483
647
  * catalog-requests/:id/dismiss`). Not the requester's decided request in this org ->
@@ -524,8 +688,8 @@ let CatalogRequestsService = class CatalogRequestsService {
524
688
  }
525
689
  /**
526
690
  * The inbox: when the account is an org admin of the org (the role read live), the members'
527
- * pending `enable` requests and the pending `connector_setup` requests, newest first; otherwise
528
- * none.
691
+ * pending `enable` and `network_allowlist` requests and the pending `connector_setup` requests,
692
+ * newest first; otherwise none.
529
693
  */
530
694
  async listForOrgAdmin(orgId, accountId) {
531
695
  const m = await this.db
@@ -550,7 +714,7 @@ let CatalogRequestsService = class CatalogRequestsService {
550
714
  const seen = await itemsSeenBy(this.db, orgId, ids);
551
715
  return this.toViews(rows.filter((r) => r.kind === "connector_setup"
552
716
  ? r.agent_id !== null && r.agent_deleted_at === null
553
- : r.item_id !== null && seen.has(r.item_id)));
717
+ : r.kind === "network_allowlist" || (r.item_id !== null && seen.has(r.item_id))));
554
718
  }
555
719
  /** The inbox: the account's decided requests in the org it has not dismissed, newest first. */
556
720
  async listDecidedForRequester(orgId, accountId) {
@@ -644,6 +808,7 @@ let CatalogRequestsService = class CatalogRequestsService {
644
808
  "r.decided_by",
645
809
  "r.decided_at",
646
810
  "r.staging_id",
811
+ "r.detail",
647
812
  "r.created_at",
648
813
  ]));
649
814
  }
@@ -654,6 +819,9 @@ __decorate([
654
819
  __decorate([
655
820
  Transactional()
656
821
  ], CatalogRequestsService.prototype, "writeItemRequest", null);
822
+ __decorate([
823
+ Transactional()
824
+ ], CatalogRequestsService.prototype, "writeNetworkRequest", null);
657
825
  __decorate([
658
826
  Transactional()
659
827
  ], CatalogRequestsService.prototype, "writeImportRequest", null);
@@ -682,8 +850,9 @@ CatalogRequestsService = __decorate([
682
850
  __param(5, Inject(CatalogService)),
683
851
  __param(6, Inject(KernelConfigService)),
684
852
  __param(7, Inject(PluginImport)),
685
- __param(8, Inject(TransactionHost)),
686
- __param(9, Inject(LockService))
853
+ __param(8, Inject(OrgSettingsService)),
854
+ __param(9, Inject(TransactionHost)),
855
+ __param(10, Inject(LockService))
687
856
  ], CatalogRequestsService);
688
857
  export { CatalogRequestsService };
689
858
  /** The view fields of a `connector_setup` request (ruling P4-T6-5); null for other kinds. */
@@ -721,6 +890,17 @@ function setupFields(r, templateNames) {
721
890
  connectors,
722
891
  };
723
892
  }
893
+ /** The detail of a `network_allowlist` request; null for other kinds. */
894
+ function networkFields(r) {
895
+ if (r.kind !== "network_allowlist")
896
+ return null;
897
+ const d = (r.detail ?? {});
898
+ return {
899
+ hosts: Array.isArray(d.hosts) ? d.hosts.filter((h) => typeof h === "string") : [],
900
+ conversationId: typeof d.conversationId === "string" ? d.conversationId : null,
901
+ agentId: typeof d.agentId === "string" ? d.agentId : null,
902
+ };
903
+ }
724
904
  function toView(r, templateNames) {
725
905
  return {
726
906
  id: r.id,
@@ -734,6 +914,7 @@ function toView(r, templateNames) {
734
914
  ? { marketplaceId: r.marketplace_id, name: r.entry_name, version: r.entry_version }
735
915
  : null,
736
916
  ...setupFields(r, templateNames),
917
+ network: networkFields(r),
737
918
  status: r.status,
738
919
  note: r.note,
739
920
  reason: r.reason,
@@ -35,5 +35,7 @@ export declare class ChannelCardActions {
35
35
  private readonly deps;
36
36
  constructor(deps: ChannelCardActionsDeps);
37
37
  handle(connectionId: string, e: CardActionEvent): Promise<void>;
38
+ /** Whether the approval `id` of the org is a network ask. */
39
+ private isNetworkAsk;
38
40
  }
39
41
  export {};
@@ -12,6 +12,8 @@ const REPLY_FOR = {
12
12
  "question.already_answered": "alreadyHandled",
13
13
  validation_failed: "invalidAction",
14
14
  "approval.scope_not_allowed": "invalidAction",
15
+ // a network ask clicked by someone other than the device's owner
16
+ "approval.not_yours": "cannotHandle",
15
17
  };
16
18
  /**
17
19
  * IM 卡片上的按钮操作(Review Focus 2):点击者按入站同样的规则映射到账号(未绑定 → 私聊回绑定卡片、群里只提示去私聊),
@@ -97,14 +99,17 @@ export class ChannelCardActions {
97
99
  await reply("cannotHandle");
98
100
  return;
99
101
  }
102
+ // a network ask is the device owner's, decided by them in person (network ask-to-allow
103
+ // spec §5.2): the shared guest account never decides one, whoever clicks
104
+ if (a.cardKind === "approval" && (await this.isNetworkAsk(row.orgId, a.cardId))) {
105
+ await reply("cannotHandle");
106
+ return;
107
+ }
100
108
  }
101
109
  try {
102
110
  if (a.cardKind === "approval" && (a.action === "approve" || a.action === "deny")) {
103
111
  const scope = anonymous ? "once" : a.scope;
104
- await this.deps.approvals.decide(row.orgId, a.cardId, who.accountId, {
105
- decision: a.action,
106
- ...(scope !== undefined && { scope }),
107
- });
112
+ await this.deps.approvals.decide(row.orgId, a.cardId, who.accountId, { decision: a.action, ...(scope !== undefined && { scope }) }, undefined, "im");
108
113
  }
109
114
  else if (a.cardKind === "question" && a.action === "answer") {
110
115
  await this.deps.questions.answer(a.cardId, who.accountId, a.answer ?? {}, row.orgId);
@@ -125,4 +130,14 @@ export class ChannelCardActions {
125
130
  // row is the child's, which has no delivery of its own
126
131
  await this.deps.outbound.enqueue(card.conversation_id);
127
132
  }
133
+ /** Whether the approval `id` of the org is a network ask. */
134
+ async isNetworkAsk(orgId, id) {
135
+ const r = await this.deps.db
136
+ .selectFrom("approvals")
137
+ .select("kind")
138
+ .where("id", "=", id)
139
+ .where("org_id", "=", orgId)
140
+ .executeTakeFirst();
141
+ return r?.kind === "network";
142
+ }
128
143
  }
@@ -1,6 +1,6 @@
1
1
  import { type ChannelCard, type FormAnswer, type InputForm, type OutboundMessage } from "@capybara-hq/plugin-sdk";
2
2
  import type { KernelSchemaDb } from "../db/kernel-schema.js";
3
- import type { ApprovalsService, ApprovalView } from "../tools/approvals.service.js";
3
+ import type { ApprovalsService, ApprovalView, NetworkAskView } from "../tools/approvals.service.js";
4
4
  import { type ToolInfo } from "../tools/info.js";
5
5
  import type { ToolRegistry } from "../tools/registry.js";
6
6
  import { type ChannelLocale } from "./phrases.js";
@@ -49,9 +49,23 @@ export interface CardDeps {
49
49
  /** The tools' display metadata (title, key arguments) for a call without a summary. */
50
50
  registry: Pick<ToolRegistry, "builtinSpecs" | "extensionToolSpecs">;
51
51
  }
52
+ /** What a network ask's card shows instead of a tool call (network ask-to-allow spec §6.4, §6.5). */
53
+ export interface NetworkCardText {
54
+ title: string;
55
+ inputPreview: string;
56
+ scopeLabels: Partial<Record<"once" | "always", string>>;
57
+ }
58
+ /**
59
+ * A network ask's card text: the title names the host (or the number of hosts); the preview lists
60
+ * the hosts, then the command clipped to {@link CARD_SUMMARY_MAX} and, for several hosts, that the
61
+ * decision applies to all of them (an IM card has no per-host choice). Model-controlled values
62
+ * are redacted by `redact` (the connection's secret values); the description stays on the web.
63
+ */
64
+ export declare function networkCardText(detail: Pick<NetworkAskView, "hosts" | "command">, locale: ChannelLocale, redact?: (s: string) => string): NetworkCardText;
52
65
  /**
53
66
  * The approval card in the approval's current state; undefined when the approval does not exist.
54
- * A role escalation offers only "once". `webUrl` = `http.publicUrl` + the conversation path.
67
+ * A role escalation offers only "once". A network ask names its hosts and command and labels its
68
+ * scopes ({@link networkCardText}). `webUrl` = `http.publicUrl` + the conversation path.
55
69
  */
56
70
  export declare function approvalCard(d: CardDeps, approvalId: string, base: string, locale: ChannelLocale, redact?: (s: string) => string): Promise<Extract<ChannelCard, {
57
71
  kind: "approval";