@desplega.ai/agent-swarm 2.4.0 → 2.4.2

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 (239) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-adapter-kedstxx6.js → acp-adapter-dv7hmx4r.js} +9 -9
  3. package/dist/{actions-vget6r1k.js → actions-51nc1gny.js} +21 -21
  4. package/dist/{amp-adapter-xeb5mktv.js → amp-adapter-abp5tdx8.js} +4 -4
  5. package/dist/{app-mqap8t8m.js → app-9h14qvr6.js} +3 -3
  6. package/dist/{assistant-h0gv97mv.js → assistant-wv1gz3mt.js} +22 -22
  7. package/dist/{boot-reembed-qr8a894z.js → boot-reembed-6yn53ms9.js} +1 -1
  8. package/dist/{boot-reembed-rj85jjhw.js → boot-reembed-7fcka4z5.js} +1 -1
  9. package/dist/{boot-scrub-sweep-eyfvn6v9.js → boot-scrub-sweep-pm1580ax.js} +4 -4
  10. package/dist/{boot-seeds-xs9a8dx1.js → boot-seeds-g5hfc1x8.js} +4 -4
  11. package/dist/{claude-adapter-bm23cye7.js → claude-adapter-gf3kxhf0.js} +9 -9
  12. package/dist/{claude-managed-adapter-1mwrsm9d.js → claude-managed-adapter-h2y889ge.js} +2 -2
  13. package/dist/{claude-managed-setup-gkfgks4j.js → claude-managed-setup-h00hsgtz.js} +2 -2
  14. package/dist/{claude-sdk-session-apb4ypz3.js → claude-sdk-session-s2k1qd8y.js} +13 -13
  15. package/dist/{cli-tj4j2ts4.js → cli-02qm7src.js} +8 -8
  16. package/dist/{cli-1cevz46c.js → cli-0qvtffe9.js} +6 -6
  17. package/dist/{cli-avmqhatn.js → cli-10hhg0km.js} +8 -8
  18. package/dist/{cli-7gh7020n.js → cli-1dmcq2eh.js} +1 -1
  19. package/dist/{cli-kz116kvw.js → cli-239yjk27.js} +7 -7
  20. package/dist/{cli-7ayprxp7.js → cli-2dfbv8qr.js} +8 -0
  21. package/dist/{cli-a49vpbf3.js → cli-5kqnemen.js} +1 -1
  22. package/dist/{cli-x719j3vs.js → cli-5rk36sjf.js} +442 -205
  23. package/dist/{cli-fb6t92v8.js → cli-6x9ma5w5.js} +1 -1
  24. package/dist/{cli-c8fwpgqb.js → cli-7225gwj7.js} +2 -2
  25. package/dist/{cli-5s5hx57y.js → cli-7awf1fp8.js} +1 -1
  26. package/dist/{cli-bn26xbcs.js → cli-8nyhf7za.js} +1 -1
  27. package/dist/{cli-ywyrffff.js → cli-8sxqsbbq.js} +1 -1
  28. package/dist/{cli-rsd7ga7v.js → cli-8zwe9mpk.js} +11 -11
  29. package/dist/{cli-y83zk5sb.js → cli-a0kcty51.js} +4 -4
  30. package/dist/{cli-8b9wvjps.js → cli-a4gvwttf.js} +4 -4
  31. package/dist/{cli-jhp7w1fy.js → cli-a5ebc95n.js} +2 -2
  32. package/dist/{cli-mvg2hf8p.js → cli-ahtpy9kf.js} +14 -7
  33. package/dist/{cli-x1p5dhq2.js → cli-bcj753sq.js} +3 -3
  34. package/dist/{cli-p22zdvnt.js → cli-bn0rewsy.js} +3 -3
  35. package/dist/{cli-p1m2b6gc.js → cli-d2mb5s8a.js} +7 -7
  36. package/dist/{cli-rg7ntypm.js → cli-d3qqdy4c.js} +456 -94
  37. package/dist/{cli-c5sbmw1v.js → cli-d82btycs.js} +94 -15
  38. package/dist/{cli-02vhhssk.js → cli-efaqv1c7.js} +5 -5
  39. package/dist/{cli-vjfpshn3.js → cli-ejs3m1sx.js} +1 -1
  40. package/dist/{cli-kk2r80n5.js → cli-faypxz7q.js} +4 -4
  41. package/dist/{cli-a1esmmee.js → cli-fpmsqq59.js} +7 -2
  42. package/dist/{cli-wrv09v7q.js → cli-ftyypbg5.js} +1 -1
  43. package/dist/{cli-zvze9rbm.js → cli-fyzjmseb.js} +3 -3
  44. package/dist/{cli-8rwvksbc.js → cli-g0hgtz4p.js} +5 -5
  45. package/dist/{cli-nrrg0fvy.js → cli-h38rjv87.js} +133 -12
  46. package/dist/{cli-tg9ksbn8.js → cli-h93c9vb3.js} +2 -2
  47. package/dist/{cli-eqgfs202.js → cli-hqhzg1qq.js} +2 -2
  48. package/dist/{cli-bhwa8rzc.js → cli-hyfw2j1e.js} +13 -12
  49. package/dist/{cli-3wjs22x3.js → cli-j4rqdf2n.js} +2 -2
  50. package/dist/{cli-6ftrrwm1.js → cli-n85dqg6d.js} +5 -5
  51. package/dist/{cli-8cxkw1f5.js → cli-nmqdc2t3.js} +2 -2
  52. package/dist/{cli-4fhyq711.js → cli-nn47d7e8.js} +13 -13
  53. package/dist/{cli-x3en6ct3.js → cli-nwyw3zhg.js} +1 -1
  54. package/dist/{cli-3avfh5az.js → cli-pemywhfq.js} +2 -2
  55. package/dist/{cli-xfe63zx3.js → cli-pkht5hap.js} +2 -2
  56. package/dist/{cli-sppc0cmw.js → cli-pnp2j5zg.js} +2 -2
  57. package/dist/{cli-wk8ccrz6.js → cli-pw7v69vp.js} +2 -2
  58. package/dist/{cli-tgvpmnwz.js → cli-r99kc9yq.js} +5 -5
  59. package/dist/{cli-bfc3k1x5.js → cli-rdyw3mct.js} +9 -9
  60. package/dist/{cli-b80a1100.js → cli-s6vr7r2d.js} +2 -2
  61. package/dist/{cli-zv4vy2b0.js → cli-s95dknxh.js} +1 -1
  62. package/dist/{cli-ttaz6c7m.js → cli-saw9a6f6.js} +6 -5
  63. package/dist/{cli-mz1ezzzh.js → cli-shvfedvm.js} +3 -3
  64. package/dist/{cli-rdr166ht.js → cli-smvcw9z5.js} +1 -1
  65. package/dist/{cli-2rwet3r7.js → cli-sv5gb0yb.js} +1 -1
  66. package/dist/{cli-5ff2sjyt.js → cli-t30yj95q.js} +2 -2
  67. package/dist/{cli-ptkpaqfy.js → cli-wt645v7q.js} +1 -1
  68. package/dist/{cli-f3nx1h8z.js → cli-wxbn3yfx.js} +2 -2
  69. package/dist/{cli-wyrr38xm.js → cli-x5mq1fyq.js} +1 -1
  70. package/dist/{cli-ccn0eybf.js → cli-xbzvet96.js} +6 -6
  71. package/dist/{cli-4mkvwgzr.js → cli-xf234ws8.js} +1 -1
  72. package/dist/{cli-nme7wery.js → cli-y2x52gs2.js} +46 -13
  73. package/dist/{cli-ffbrkh1r.js → cli-y8n5k515.js} +46 -38
  74. package/dist/{cli-ecshf504.js → cli-z4wd2ftv.js} +7 -7
  75. package/dist/{cli-dsgj3e7a.js → cli-zfg9enh9.js} +2 -2
  76. package/dist/{cli-yxqag76b.js → cli-zw88zjba.js} +1 -1
  77. package/dist/cli.js +20 -20
  78. package/dist/{codex-adapter-pp2gq8gr.js → codex-adapter-ttygw382.js} +6 -6
  79. package/dist/{codex-hook-qwzccd64.js → codex-hook-x7nejyrr.js} +7 -4
  80. package/dist/{codex-login-35p4sk85.js → codex-login-m819c41r.js} +1 -1
  81. package/dist/{codex-session-runner-0vzn09t8.js → codex-session-runner-60b58vv3.js} +7 -7
  82. package/dist/{commands-psqhkttb.js → commands-edmdwa4x.js} +2 -2
  83. package/dist/{controller-t20vz2k8.js → controller-m263fg43.js} +4 -4
  84. package/dist/{controllers-connect-gkxmqw9d.js → controllers-connect-5fym8sw1.js} +23 -23
  85. package/dist/{cursor-adapter-3v542qe5.js → cursor-adapter-cz6nm6mj.js} +3 -3
  86. package/dist/{db-nc1rd869.js → db-2y0mgstc.js} +3 -3
  87. package/dist/{db-2xbyenjr.js → db-nn0hcmx5.js} +6 -2
  88. package/dist/{devin-adapter-7egzmt0k.js → devin-adapter-zgqadvnm.js} +1 -1
  89. package/dist/{dispatcher-ewceb8ts.js → dispatcher-0a9wwjcx.js} +20 -20
  90. package/dist/{docker-njavd3fb.js → docker-fkgtr75t.js} +2 -2
  91. package/dist/{dsh-adapter-75xwh78t.js → dsh-adapter-3n3net33.js} +3 -3
  92. package/dist/{e2b-5r6a5jeb.js → e2b-7am6tmq6.js} +2 -2
  93. package/dist/{e2b-pw1nvtmh.js → e2b-thdcxtjd.js} +2 -2
  94. package/dist/{easter-egg-3d-xsc47jwf.js → easter-egg-3d-5cgynaja.js} +1 -1
  95. package/dist/{event-bus-101fenkh.js → event-bus-9vpk8gmq.js} +1 -1
  96. package/dist/{execute-gzmw1mnw.js → execute-t4e6m3rw.js} +1 -1
  97. package/dist/{external-channel-a5dsmxn4.js → external-channel-zhvt95n3.js} +2 -2
  98. package/dist/{grok-adapter-vj88xr23.js → grok-adapter-qnr1hp2y.js} +10 -10
  99. package/dist/handlers-dkcarkd0.js +39 -0
  100. package/dist/{heartbeat-t2m9r3wa.js → heartbeat-gz97r7ac.js} +23 -23
  101. package/dist/{hook-xa677f8r.js → hook-1mmdgjqs.js} +16 -9
  102. package/dist/{hook-8femx1bg.js → hook-x8rthca4.js} +6 -6
  103. package/dist/{http-xpmmq028.js → http-r8f85xr9.js} +274 -160
  104. package/dist/index-5jecgp3n.js +41 -0
  105. package/dist/index-aex0eba9.js +40 -0
  106. package/dist/{index-syztqdt6.js → index-g6tbb6ve.js} +1 -1
  107. package/dist/{index-cdzyfrsp.js → index-smxyk04s.js} +1 -1
  108. package/dist/{index-dg2tmj99.js → index-vzvfqzfj.js} +20 -20
  109. package/dist/{index-s7ckdajq.js → index-y7rbcrc1.js} +10 -10
  110. package/dist/{keepalive-3nr4st3r.js → keepalive-64c0f34c.js} +4 -4
  111. package/dist/lead-b1da0f97.js +78 -0
  112. package/dist/{maintenance-verwpe2c.js → maintenance-mnxpar12.js} +1 -1
  113. package/dist/{oauth-refresh-sweep-9q2t0y3q.js → oauth-refresh-sweep-36hs2v4h.js} +3 -3
  114. package/dist/{onboard-aytqedmq.js → onboard-xes8de61.js} +3 -3
  115. package/dist/{opencode-adapter-f4hryjny.js → opencode-adapter-3s7ax02e.js} +7 -7
  116. package/dist/{otel-impl-hckw4wt1.js → otel-impl-fzc57kcq.js} +2 -2
  117. package/dist/{pi-mono-adapter-hxby0ce1.js → pi-mono-adapter-ytp7gz8b.js} +22 -11
  118. package/dist/{pricing-refresh-5nvme3sm.js → pricing-refresh-1nek2w7b.js} +3 -3
  119. package/dist/{rbac-roles-vbpy21mg.js → rbac-roles-sqefb5hw.js} +2 -2
  120. package/dist/{rbac-roles-pd8c26m1.js → rbac-roles-ttyfaep2.js} +1 -1
  121. package/dist/{render-v2-7nqgvk2q.js → render-v2-vb527jwn.js} +3 -3
  122. package/dist/{resume-b6djcj3c.js → resume-jcdzg1bn.js} +10 -8
  123. package/dist/{runtime-txrxc9za.js → runtime-xe3aqy4x.js} +1 -1
  124. package/dist/{session-access-fc3f9y12.js → session-access-zsp4wsp0.js} +4 -4
  125. package/dist/{setup-n39szm6b.js → setup-1nb2zqd3.js} +1 -1
  126. package/dist/{virtual-modules-wyp2k6z3.js → virtual-modules-4r3zkc20.js} +2 -2
  127. package/dist/{work-objects-jem2fre0.js → work-objects-61w08fwr.js} +23 -23
  128. package/dist/worker-c1e68hpa.js +78 -0
  129. package/dist/{x-8ewt8gxn.js → x-y45mthwn.js} +2 -2
  130. package/openapi.json +491 -52
  131. package/package.json +3 -3
  132. package/src/apps/action-runner.ts +146 -0
  133. package/src/apps/catalog.generated.json +3 -2
  134. package/src/apps/definition.ts +152 -4
  135. package/src/apps/row-store.ts +23 -9
  136. package/src/apps/schema-migrate.ts +14 -6
  137. package/src/apps/script-types.ts +2 -1
  138. package/src/apps/sync.ts +20 -19
  139. package/src/be/db/tasks/read.ts +112 -0
  140. package/src/be/db.ts +16 -4
  141. package/src/be/fleet-benchmark-stats.ts +10 -5
  142. package/src/be/models-catalog.ts +4 -0
  143. package/src/be/onboarding.ts +6 -0
  144. package/src/be/scripts/typecheck.ts +13 -0
  145. package/src/be/swarm-config-guard.ts +25 -1
  146. package/src/be/telemetry-enable.ts +1 -1
  147. package/src/commands/provider-credentials.ts +12 -2
  148. package/src/commands/runner.ts +3 -0
  149. package/src/heartbeat/heartbeat.ts +2 -2
  150. package/src/hooks/codex-hook.ts +6 -2
  151. package/src/hooks/hook.ts +7 -0
  152. package/src/hooks/pr-body-guard.ts +38 -11
  153. package/src/hooks/pr-room-guard.ts +88 -0
  154. package/src/http/agents.ts +1 -0
  155. package/src/http/apps.ts +99 -183
  156. package/src/http/fleet-benchmark.ts +79 -30
  157. package/src/http/global-config.ts +7 -0
  158. package/src/http/ranking.ts +24 -40
  159. package/src/http/status.ts +49 -43
  160. package/src/http/tasks.ts +49 -1
  161. package/src/http/telemetry-ui.ts +1 -1
  162. package/src/http/used-by.ts +55 -19
  163. package/src/http/users.ts +2 -2
  164. package/src/providers/acp-targets.ts +1 -1
  165. package/src/providers/amp-adapter.ts +1 -1
  166. package/src/providers/pi-mono-adapter.ts +15 -0
  167. package/src/providers/reasoning-effort.ts +2 -1
  168. package/src/providers/types.ts +2 -0
  169. package/src/ranking/telemetry-ranking.ts +5 -33
  170. package/src/scripts-runtime/sdk-allowlist.ts +7 -0
  171. package/src/scripts-runtime/types/stdlib.d.ts +31 -0
  172. package/src/scripts-runtime/types/swarm-sdk.d.ts +31 -0
  173. package/src/server.ts +4 -0
  174. package/src/telemetry-context.ts +19 -1
  175. package/src/telemetry-contract/EVENTS.md +42 -8
  176. package/src/telemetry-contract/SOURCE.json +6 -5
  177. package/src/telemetry-contract/catalog.json +225 -5
  178. package/src/telemetry-contract/ranking/fixtures/valid/no-claim.json +10 -0
  179. package/src/telemetry-contract/ranking/listing.v1.schema.json +2 -2
  180. package/src/telemetry-contract/types.gen.ts +225 -27
  181. package/src/telemetry-identity.ts +47 -9
  182. package/src/telemetry-snapshot.ts +9 -1
  183. package/src/telemetry-ui-events.ts +9 -2
  184. package/src/telemetry.ts +29 -6
  185. package/src/tests/app-action-run-tool.test.ts +178 -0
  186. package/src/tests/app-row-tools.test.ts +385 -0
  187. package/src/tests/apps-json-column.test.ts +611 -0
  188. package/src/tests/apps-script-types.test.ts +2 -0
  189. package/src/tests/apps-sync-engine.test.ts +42 -0
  190. package/src/tests/check-pr-body.test.ts +0 -23
  191. package/src/tests/credential-check.test.ts +38 -0
  192. package/src/tests/db-task-read-contract.test.ts +4 -0
  193. package/src/tests/db-task-write-contract.test.ts +2 -0
  194. package/src/tests/docs-index-footprint.test.js +2 -2
  195. package/src/tests/fleet-benchmark.test.ts +122 -2
  196. package/src/tests/http-users.test.ts +62 -1
  197. package/src/tests/model-catalog-table.test.ts +63 -0
  198. package/src/tests/model-profiles-api.test.ts +19 -7
  199. package/src/tests/models-catalog.test.ts +50 -0
  200. package/src/tests/onboarding.test.ts +21 -0
  201. package/src/tests/org-website.test.ts +62 -0
  202. package/src/tests/pi-mono-adapter.test.ts +23 -0
  203. package/src/tests/pr-body-leaks.test.ts +4 -0
  204. package/src/tests/pr-room-guard.test.ts +242 -0
  205. package/src/tests/ranking-listing.test.ts +8 -23
  206. package/src/tests/ranking-route.test.ts +49 -47
  207. package/src/tests/reasoning-effort.test.ts +39 -3
  208. package/src/tests/runner-harness-telemetry-env.test.ts +62 -1
  209. package/src/tests/status.test.ts +158 -9
  210. package/src/tests/swarm-tool-result-gate.test.ts +2 -4
  211. package/src/tests/task-lineage.test.ts +180 -0
  212. package/src/tests/telemetry-context.test.ts +128 -2
  213. package/src/tests/telemetry-init.test.ts +62 -5
  214. package/src/tests/telemetry-ui-route.test.ts +123 -0
  215. package/src/tests/tool-annotations.test.ts +3 -2
  216. package/src/tests/used-by-route.test.ts +144 -10
  217. package/src/tests/user-token-rest-auth.test.ts +38 -0
  218. package/src/tests/workflow-agent-task-retry.test.ts +130 -4
  219. package/src/tests/workflow-wait-recovery.test.ts +116 -4
  220. package/src/tools/app-action-run.ts +63 -0
  221. package/src/tools/app-rows.ts +239 -0
  222. package/src/tools/model-catalog/index.ts +1 -1
  223. package/src/tools/tool-config.ts +8 -0
  224. package/src/tools/utils.ts +1 -1
  225. package/src/tools/workflows/retry-workflow-run.ts +1 -1
  226. package/src/types.ts +17 -0
  227. package/src/utils/company-profile.ts +24 -0
  228. package/src/utils/org-website.ts +58 -21
  229. package/src/workflows/recovery.ts +10 -22
  230. package/src/workflows/resume.ts +16 -0
  231. package/src/workflows/task-step-routing.ts +47 -7
  232. package/templates/skills/apps/SKILL.md +44 -4
  233. package/templates/skills/apps/content.md +44 -4
  234. package/dist/handlers-zbs90bny.js +0 -39
  235. package/dist/index-8pwc1jc8.js +0 -40
  236. package/dist/index-nxa6me0q.js +0 -41
  237. package/dist/lead-td8h2sfn.js +0 -78
  238. package/dist/worker-wr6xgfr2.js +0 -78
  239. package/src/tests/check-ranking-entry.test.ts +0 -146
package/src/telemetry.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * - Opt-out via ANONYMIZED_TELEMETRY=false
5
5
  * - Fire-and-forget: never throws, never blocks
6
- * - No DB and no external services (global fetch + node:crypto only)
6
+ * - No DB and no external services (global fetch + built-in APIs only)
7
7
  * - Importable from both API server and workers
8
8
  * - Every event is schema_version 2: a top-level `context` identity envelope
9
9
  * plus properties typed against the proxy's event catalog
@@ -11,6 +11,7 @@
11
11
  * catalog fails `tsc`, and the proxy rejects it at ingest.
12
12
  */
13
13
  import { randomUUID } from "node:crypto";
14
+ import { existsSync } from "node:fs";
14
15
  import pkg from "../package.json";
15
16
  import { isSlackConfigured } from "./slack/config";
16
17
  import {
@@ -22,7 +23,7 @@ import {
22
23
  validOrgDomain,
23
24
  validOrgId,
24
25
  } from "./telemetry-context";
25
- import type { PropsFor, TelemetryEventName } from "./telemetry-contract";
26
+ import type { EventContext, PropsFor, TelemetryEventName } from "./telemetry-contract";
26
27
  import { isEnvFlagEnabled } from "./utils/env-flag";
27
28
  import { getNodeEnv } from "./utils/node-env";
28
29
  import { normalizeOrgWebsite } from "./utils/org-website";
@@ -68,6 +69,7 @@ let cachedOrgId: string | undefined;
68
69
  /** Email domain of the org's admin, set by the API server (`setTelemetryOrgDomain`). */
69
70
  let cachedOrgDomain: string | undefined;
70
71
  let roleResolver: ((userId: string) => TelemetryUserRole | undefined) | null = null;
72
+ let functionResolver: ((userId: string) => EventContext["user_function"]) | null = null;
71
73
  let identityReloader: (() => Promise<void>) | null = null;
72
74
  let lastIdentityLoadAt = 0;
73
75
  let source = "unknown";
@@ -510,6 +512,13 @@ export function setTelemetryRoleResolver(
510
512
  roleResolver = resolver;
511
513
  }
512
514
 
515
+ /** API server only: cache-backed lookup of the actor's job function. */
516
+ export function setTelemetryFunctionResolver(
517
+ resolver: ((userId: string) => EventContext["user_function"]) | null,
518
+ ): void {
519
+ functionResolver = resolver;
520
+ }
521
+
513
522
  /**
514
523
  * True when `track()` would send: telemetry is on, an install ID exists and an
515
524
  * org ID resolved. Emitters that record "already sent" state check this first.
@@ -583,14 +592,24 @@ function isCloudDeployment(): boolean {
583
592
  return raw === "true" || raw === "1";
584
593
  }
585
594
 
586
- function getTelemetryEnvironment(): string {
595
+ let cachedIsGitCheckout: boolean | undefined;
596
+
597
+ export function getTelemetryEnvironment(packageRoot = new URL("../", import.meta.url)): string {
587
598
  const explicit = process.env.DESPLEGA_TELEMETRY_ENV?.trim();
588
599
  if (explicit) return explicit;
589
600
 
590
601
  // Do not default from NODE_ENV: shipped Bun/npm installs can report
591
602
  // "development" even when the operator did not choose a telemetry cohort.
592
603
  if (getNodeEnv() === "test") return "test";
593
- return "production";
604
+ // Git checkouts (including worktrees) are development; shipped packages omit .git.
605
+ if (cachedIsGitCheckout === undefined) {
606
+ try {
607
+ cachedIsGitCheckout = existsSync(new URL(".git", packageRoot));
608
+ } catch {
609
+ cachedIsGitCheckout = false;
610
+ }
611
+ }
612
+ return cachedIsGitCheckout ? "development" : "production";
594
613
  }
595
614
 
596
615
  /**
@@ -664,8 +683,10 @@ function emit(
664
683
  swarmVersion: pkg.version,
665
684
  installMethod: cachedInstallMethod,
666
685
  installPreset: cachedInstallPreset,
667
- // Stays null until the dashboard reports where the visitor came from.
668
- acquisitionSource: null,
686
+ acquisitionSource: process.env.SWARM_ORG_ACQUISITION_SOURCE,
687
+ teamSize: process.env.SWARM_ORG_TEAM_SIZE,
688
+ useCase: process.env.SWARM_ORG_USE_CASE,
689
+ userFunction: userId ? functionResolver?.(userId) : undefined,
669
690
  }),
670
691
  properties: normalizeProperties(event, {
671
692
  ...properties,
@@ -731,6 +752,7 @@ function emit(
731
752
  * Do not call from production code.
732
753
  */
733
754
  export function _resetTelemetryStateForTests(): void {
755
+ cachedIsGitCheckout = undefined;
734
756
  installationId = null;
735
757
  installedAt = null;
736
758
  ingestObserver = null;
@@ -745,6 +767,7 @@ export function _resetTelemetryStateForTests(): void {
745
767
  cachedOrgId = undefined;
746
768
  cachedOrgDomain = undefined;
747
769
  roleResolver = null;
770
+ functionResolver = null;
748
771
  identityReloader = null;
749
772
  lastIdentityLoadAt = 0;
750
773
  }
@@ -0,0 +1,178 @@
1
+ import { afterAll, beforeAll, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { createServer as createHttpServer } from "node:http";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { type AppRecord, createApp } from "../apps/store";
6
+ import { closeDb, createAgent, getTaskById, initDb } from "../be/db";
7
+ import { upsertScriptByName } from "../be/scripts/db";
8
+ import { handleCore } from "../http/core";
9
+ import { handleMcpBridge } from "../http/mcp-bridge";
10
+ import { getPathSegments, parseQueryParams } from "../http/utils";
11
+ import { LEGACY_POLICY, type LegacyRule } from "../rbac";
12
+ import { isMcpToolAllowedForScripts } from "../scripts-runtime/sdk-allowlist";
13
+ import { registerAppActionRunTool } from "../tools/app-action-run";
14
+ import { markExtensionRequestOrigin, markScriptSdkRequestOrigin } from "../tools/utils";
15
+ import { SKIP_SANDBOX_SPAWN_TESTS } from "./sandbox-spawn-test-helpers";
16
+ import { listenOnFreePort } from "./test-net";
17
+ import { CHILD_PROCESS_TEST_BUDGET_MS } from "./test-proc";
18
+
19
+ const dbPath = `/private/tmp/test-app-action-run-tool-${process.pid}.sqlite`;
20
+ const agentId = crypto.randomUUID();
21
+ const ownerId = crypto.randomUUID();
22
+ const apiKey = "example-app-action-run-key";
23
+ const previousKey = process.env.AGENT_SWARM_API_KEY;
24
+ const previousBase = process.env.MCP_BASE_URL;
25
+ const definition: AppRecord["definition"] = {
26
+ schemaVersion: 1,
27
+ models: { note: { columns: { title: { kind: "string" } } } },
28
+ actions: { triage: { kind: "task", prompt: "Triage the notes." } },
29
+ pages: { main: { root: "root", elements: { root: { type: "Container", props: {} } } } },
30
+ defaultPage: "main",
31
+ };
32
+ type ToolResult = {
33
+ isError?: boolean;
34
+ structuredContent: {
35
+ message: string;
36
+ data: { ok: boolean; taskId: string; status: string; result?: unknown };
37
+ };
38
+ };
39
+ let handler: (args: unknown, extra: unknown) => Promise<ToolResult>;
40
+ let appId: string;
41
+
42
+ async function removeDb() {
43
+ for (const suffix of ["", "-wal", "-shm"]) await unlink(dbPath + suffix).catch(() => undefined);
44
+ }
45
+ beforeAll(async () => {
46
+ process.env.AGENT_SWARM_API_KEY = apiKey;
47
+ await removeDb();
48
+ initDb(dbPath);
49
+ await createAgent({ id: agentId, name: "Action invoker", isLead: false, status: "idle" });
50
+ await createAgent({ id: ownerId, name: "Script owner", isLead: true, status: "idle" });
51
+ appId = (await createApp({ name: "Action tool", definition })).id;
52
+ const server = new McpServer({ name: "app-action-run-tool", version: "1.0.0" });
53
+ registerAppActionRunTool(server);
54
+ handler = (server as unknown as { _registeredTools: Record<string, { handler: typeof handler }> })
55
+ ._registeredTools["app-action-run"]!.handler;
56
+ });
57
+ afterAll(async () => {
58
+ closeDb();
59
+ await removeDb();
60
+ if (previousKey === undefined) delete process.env.AGENT_SWARM_API_KEY;
61
+ else process.env.AGENT_SWARM_API_KEY = previousKey;
62
+ if (previousBase === undefined) delete process.env.MCP_BASE_URL;
63
+ else process.env.MCP_BASE_URL = previousBase;
64
+ });
65
+ function call(
66
+ action: string,
67
+ input: Record<string, unknown> = {},
68
+ origin?: "script-sdk" | "extension",
69
+ id = agentId,
70
+ target = appId,
71
+ ) {
72
+ const extra = { sessionId: "action-tool", requestInfo: { headers: { "x-agent-id": id } } };
73
+ if (origin === "script-sdk") markScriptSdkRequestOrigin(extra);
74
+ if (origin === "extension") markExtensionRequestOrigin(extra);
75
+ return handler({ appId: target, action, input }, extra);
76
+ }
77
+
78
+ test("runs task actions with the verified caller and the REST response shape", async () => {
79
+ const output = await call("triage", { caller: { kind: "guest", id: "forged" } });
80
+ expect(output.isError).not.toBe(true);
81
+ expect(output.structuredContent).toMatchObject({ ok: true, status: "pending" });
82
+ const task = await getTaskById(output.structuredContent.taskId);
83
+ expect(task?.task).toContain(`"kind":"agent","id":"${agentId}","name":"Action invoker"`);
84
+ });
85
+
86
+ test("refuses script and extension origins and stays outside the SDK allowlist", async () => {
87
+ for (const origin of ["script-sdk", "extension"] as const) {
88
+ const output = await call("triage", {}, origin);
89
+ expect(output.isError).toBe(true);
90
+ expect(output.structuredContent.message).toContain("cannot run app actions");
91
+ }
92
+ expect(isMcpToolAllowedForScripts("app-action-run")).toBe(false);
93
+ });
94
+
95
+ test("rejects missing agents, apps, actions, and denied principals", async () => {
96
+ expect((await call("triage", {}, undefined, "")).isError).toBe(true);
97
+ expect((await call("triage", {}, undefined, agentId, "missing")).isError).toBe(true);
98
+ expect((await call("missing")).isError).toBe(true);
99
+ const policy = LEGACY_POLICY as unknown as Record<"app.use", LegacyRule>;
100
+ const original = policy["app.use"];
101
+ policy["app.use"] = { name: "deny-actions", denyReason: "denied actions", evaluate: () => false };
102
+ try {
103
+ expect((await call("triage")).isError).toBe(true);
104
+ } finally {
105
+ policy["app.use"] = original;
106
+ }
107
+ });
108
+
109
+ test.skipIf(SKIP_SANDBOX_SPAWN_TESTS)(
110
+ "runs script actions as the owner while preserving the invoking agent caller",
111
+ async () => {
112
+ const server = createHttpServer(async (req, res) => {
113
+ const id = req.headers["x-agent-id"] as string | undefined;
114
+ if (await handleCore(req, res, id, apiKey)) return;
115
+ if (
116
+ await handleMcpBridge(
117
+ req,
118
+ res,
119
+ getPathSegments(req.url ?? ""),
120
+ parseQueryParams(req.url ?? ""),
121
+ id,
122
+ )
123
+ )
124
+ return;
125
+ res.writeHead(404).end();
126
+ });
127
+ try {
128
+ const port = await listenOnFreePort(server);
129
+ process.env.MCP_BASE_URL = `http://localhost:${port}`;
130
+ const saved = await upsertScriptByName({
131
+ name: "action_tool_owner",
132
+ scope: "agent",
133
+ scopeId: ownerId,
134
+ agentId: ownerId,
135
+ source: `export default async function(args, ctx) {
136
+ const response = await ctx.swarm.app_row_create({ appId: args.app.id, model: "note", values: { title: "owner-write" } });
137
+ return { caller: args.caller, row: (response?.data ?? response).row };
138
+ }`,
139
+ description: "Owner action fixture",
140
+ intent: "Verify script owner and caller",
141
+ signatureJson: "{}",
142
+ typeChecked: true,
143
+ embeddingMode: "skip",
144
+ });
145
+ const app = await createApp({
146
+ name: "Script action",
147
+ definition: {
148
+ ...definition,
149
+ actions: {
150
+ write: {
151
+ kind: "script",
152
+ scriptId: saved.script.id,
153
+ args: { caller: { kind: "guest", id: "default-forgery" } },
154
+ },
155
+ },
156
+ },
157
+ });
158
+ const output = await call(
159
+ "write",
160
+ { caller: { kind: "operator", id: "forged" } },
161
+ undefined,
162
+ agentId,
163
+ app.id,
164
+ );
165
+ expect(output.isError).not.toBe(true);
166
+ expect(output.structuredContent).toMatchObject({
167
+ ok: true,
168
+ result: {
169
+ caller: { kind: "agent", id: agentId, name: "Action invoker" },
170
+ row: { title: "owner-write", createdBy: `agent:${ownerId}` },
171
+ },
172
+ });
173
+ } finally {
174
+ if (server.listening) await new Promise<void>((resolve) => server.close(() => resolve()));
175
+ }
176
+ },
177
+ CHILD_PROCESS_TEST_BUDGET_MS,
178
+ );
@@ -0,0 +1,385 @@
1
+ import { afterAll, beforeAll, expect, spyOn, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { createServer as createHttpServer } from "node:http";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import {
6
+ type AnySchema,
7
+ normalizeObjectSchema,
8
+ safeParseAsync,
9
+ } from "@modelcontextprotocol/sdk/server/zod-compat.js";
10
+ import * as rowStore from "../apps/row-store";
11
+ import { type AppRecord, createApp } from "../apps/store";
12
+ import { closeDb, createAgent, initDb } from "../be/db";
13
+ import { typecheckScript } from "../be/scripts/typecheck";
14
+ import { handleCore } from "../http/core";
15
+ import { handleMcpBridge } from "../http/mcp-bridge";
16
+ import { getPathSegments, parseQueryParams } from "../http/utils";
17
+ import { LEGACY_POLICY, type LegacyRule } from "../rbac";
18
+ import { runScript } from "../scripts-runtime/loader";
19
+ import { registerAppRowTools } from "../tools/app-rows";
20
+ import { markScriptSdkRequestOrigin } from "../tools/utils";
21
+ import { SKIP_SANDBOX_SPAWN_TESTS } from "./sandbox-spawn-test-helpers";
22
+ import { listenOnFreePort } from "./test-net";
23
+ import { CHILD_PROCESS_TEST_BUDGET_MS } from "./test-proc";
24
+
25
+ const dbPath = `/private/tmp/test-app-row-tools-${process.pid}.sqlite`;
26
+ const agentId = crypto.randomUUID();
27
+ const apiKey = "example-app-row-tools-key";
28
+ const previousKey = process.env.AGENT_SWARM_API_KEY;
29
+ const definition: AppRecord["definition"] = {
30
+ schemaVersion: 1,
31
+ models: {
32
+ note: {
33
+ columns: {
34
+ title: { kind: "string" },
35
+ rank: { kind: "number" },
36
+ active: { kind: "boolean" },
37
+ state: { kind: "enum", enum: ["open", "closed"] },
38
+ externalId: { kind: "string" },
39
+ projected: { kind: "string", source: { of: "external", field: "title" } },
40
+ },
41
+ sources: { external: { connector: "swarm-tasks", joinKey: "externalId", config: {} } },
42
+ },
43
+ },
44
+ pages: { main: { root: "root", elements: { root: { type: "Container", props: {} } } } },
45
+ defaultPage: "main",
46
+ };
47
+
48
+ type ToolResult = {
49
+ isError?: boolean;
50
+ structuredContent: {
51
+ message: string;
52
+ row: import("../apps/row-store").AppRow;
53
+ rows: import("../apps/row-store").AppRow[];
54
+ total: number;
55
+ deleted?: number;
56
+ ok?: boolean;
57
+ };
58
+ };
59
+ type RegisteredTool = {
60
+ inputSchema: AnySchema;
61
+ handler: (args: unknown, extra: unknown) => Promise<ToolResult>;
62
+ };
63
+ let tools: Record<string, RegisteredTool>;
64
+ let appId: string;
65
+
66
+ async function removeDb() {
67
+ for (const suffix of ["", "-wal", "-shm"]) await unlink(dbPath + suffix).catch(() => undefined);
68
+ }
69
+
70
+ beforeAll(async () => {
71
+ process.env.AGENT_SWARM_API_KEY = apiKey;
72
+ await removeDb();
73
+ initDb(dbPath);
74
+ await createAgent({ id: agentId, name: "row-tool-agent", isLead: false, status: "idle" });
75
+ appId = (await createApp({ name: "Row tools", definition })).id;
76
+ const server = new McpServer({ name: "app-row-tools", version: "1.0.0" });
77
+ registerAppRowTools(server);
78
+ tools = (server as unknown as { _registeredTools: Record<string, RegisteredTool> })
79
+ ._registeredTools;
80
+ });
81
+
82
+ afterAll(async () => {
83
+ closeDb();
84
+ await removeDb();
85
+ if (previousKey === undefined) delete process.env.AGENT_SWARM_API_KEY;
86
+ else process.env.AGENT_SWARM_API_KEY = previousKey;
87
+ });
88
+
89
+ async function call(
90
+ name: string,
91
+ input: Record<string, unknown>,
92
+ id: string | undefined = agentId,
93
+ ) {
94
+ const tool = tools[name]!;
95
+ const schema = normalizeObjectSchema(tool.inputSchema) ?? tool.inputSchema;
96
+ const args = { appId, model: "note", ...input };
97
+ const parsed = await safeParseAsync(schema, args);
98
+ if (!parsed.success) throw new Error(JSON.stringify(parsed.error));
99
+ // Exercise the registrar with the same trusted origin marker as the script bridge.
100
+ return tool.handler(
101
+ parsed.data,
102
+ markScriptSdkRequestOrigin({
103
+ sessionId: "app-row-tools",
104
+ requestInfo: { headers: id ? { "x-agent-id": id } : {} },
105
+ }),
106
+ );
107
+ }
108
+
109
+ test("creates, gets, filters, sorts, limits, patches, and deletes flat rows with agent provenance", async () => {
110
+ const result = await call("app-row-create", {
111
+ values: { title: "single", rank: 1, active: true },
112
+ });
113
+ expect(result.isError).not.toBe(true);
114
+ const row = result.structuredContent.row;
115
+ expect(row).toMatchObject({
116
+ title: "single",
117
+ rank: 1,
118
+ createdBy: `agent:${agentId}`,
119
+ updatedBy: `agent:${agentId}`,
120
+ });
121
+ expect(row.values).toBeUndefined();
122
+ expect((await call("app-row-get", { rowId: row.id })).structuredContent.row).toEqual(row);
123
+ const patched = await call("app-row-patch", { rowId: row.id, values: { title: "patched" } });
124
+ expect(patched.structuredContent.row).toMatchObject({
125
+ title: "patched",
126
+ createdBy: row.createdBy,
127
+ updatedBy: row.createdBy,
128
+ });
129
+ await call("app-rows-create", {
130
+ rows: [
131
+ { title: "bulk", rank: 2, active: true },
132
+ { title: "bulk", rank: 3, active: false },
133
+ { title: "bulk", rank: 4, active: true },
134
+ ],
135
+ });
136
+ const listed = await call("app-rows-list", {
137
+ equals: { title: "bulk", active: true },
138
+ sort: "rank:desc",
139
+ limit: 1,
140
+ });
141
+ expect(listed.structuredContent.total).toBe(2);
142
+ expect(listed.structuredContent.rows).toHaveLength(1);
143
+ expect(listed.structuredContent.rows[0].rank).toBe(4);
144
+ const systemFilter = await call("app-rows-list", {
145
+ equals: { id: row.id, createdBy: `agent:${agentId}` },
146
+ sort: "createdAt:asc",
147
+ });
148
+ expect(systemFilter.structuredContent.total).toBe(1);
149
+ expect((await call("app-row-delete", { rowId: row.id })).structuredContent).toMatchObject({
150
+ ok: true,
151
+ });
152
+ expect((await call("app-row-get", { rowId: row.id })).isError).toBe(true);
153
+ expect(
154
+ (await call("app-rows-delete", { equals: { title: "bulk" }, limit: 2 })).structuredContent,
155
+ ).toMatchObject({ deleted: 2 });
156
+ expect(
157
+ (await call("app-rows-delete", { equals: { title: "bulk" } })).structuredContent,
158
+ ).toMatchObject({
159
+ deleted: 1,
160
+ });
161
+ });
162
+
163
+ test("null in values clears an optional column on create, bulk create, and patch", async () => {
164
+ const created = await call("app-row-create", { values: { title: "null-create", rank: null } });
165
+ expect(created.isError).not.toBe(true);
166
+ expect(created.structuredContent.row.rank).toBeNull();
167
+ const bulk = await call("app-rows-create", {
168
+ rows: [{ title: "null-bulk", rank: null, state: null }],
169
+ });
170
+ expect(bulk.isError).not.toBe(true);
171
+ expect(bulk.structuredContent.rows[0]).toMatchObject({ rank: null, state: null });
172
+ const row = (await call("app-row-create", { values: { title: "null-patch", rank: 5 } }))
173
+ .structuredContent.row;
174
+ const patched = await call("app-row-patch", { rowId: row.id, values: { rank: null } });
175
+ expect(patched.isError).not.toBe(true);
176
+ expect(Object.hasOwn(patched.structuredContent.row, "rank")).toBe(false);
177
+ expect(
178
+ Object.hasOwn((await call("app-row-get", { rowId: row.id })).structuredContent.row, "rank"),
179
+ ).toBe(false);
180
+ });
181
+
182
+ test("app-rows-delete skips a row patched out of the filter after the select", async () => {
183
+ const row = (await call("app-row-create", { values: { title: "delete" } })).structuredContent.row;
184
+ const original = rowStore.listAppRows;
185
+ // The select sees the row as "delete"; the patch lands before the per-row delete lock.
186
+ const spy = spyOn(rowStore, "listAppRows").mockImplementation(async (...args) => {
187
+ const rows = await original(...args);
188
+ await rowStore.patchAppRow(appId, "note", definition.models.note!, row.id, { title: "keep" });
189
+ return rows;
190
+ });
191
+ let result: ToolResult;
192
+ try {
193
+ result = await call("app-rows-delete", { equals: { title: "delete" } });
194
+ } finally {
195
+ spy.mockRestore();
196
+ }
197
+ expect(result.isError).not.toBe(true);
198
+ expect(result.structuredContent.deleted).toBe(0);
199
+ expect((await call("app-row-get", { rowId: row.id })).structuredContent.row.title).toBe("keep");
200
+ await call("app-row-delete", { rowId: row.id });
201
+ });
202
+
203
+ test("rejects invalid filters, sorts, values, source projections, join keys, and missing agents", async () => {
204
+ for (const values of [
205
+ { projected: "blocked" },
206
+ { externalId: "blocked" },
207
+ { state: "invalid" },
208
+ { rank: "wrong" },
209
+ { createdBy: "forged" },
210
+ ]) {
211
+ expect((await call("app-row-create", { values })).isError).toBe(true);
212
+ }
213
+ for (const equals of [{ missing: "x" }, { active: 1 }, { rank: "1" }]) {
214
+ expect((await call("app-rows-list", { equals })).isError).toBe(true);
215
+ }
216
+ expect((await call("app-rows-list", { sort: "rank:sideways" })).isError).toBe(true);
217
+ expect((await call("app-rows-list", {}, "")).isError).toBe(true);
218
+ });
219
+
220
+ test("app-rows-list rejects equals on a json column, like REST filters and named queries", async () => {
221
+ const jsonApp = await createApp({
222
+ name: "Row tools json",
223
+ definition: {
224
+ ...definition,
225
+ models: { note: { columns: { title: { kind: "string" }, payload: { kind: "json" } } } },
226
+ },
227
+ });
228
+ const created = await call("app-row-create", {
229
+ appId: jsonApp.id,
230
+ values: { title: "json", payload: { x: 1 } },
231
+ });
232
+ expect(created.isError).not.toBe(true);
233
+
234
+ const listed = await call("app-rows-list", { appId: jsonApp.id, equals: { payload: "x" } });
235
+ expect(listed.isError).toBe(true);
236
+ expect(listed.structuredContent).toMatchObject({
237
+ issues: [{ path: "equals.payload", message: "json columns cannot be filtered" }],
238
+ });
239
+ // A scalar column on the same model still filters.
240
+ const byTitle = await call("app-rows-list", { appId: jsonApp.id, equals: { title: "json" } });
241
+ expect(byTitle.isError).not.toBe(true);
242
+ expect(byTitle.structuredContent.total).toBe(1);
243
+ });
244
+
245
+ test("enforces row, bulk, filter, and limit caps before writing", async () => {
246
+ await expect(
247
+ call("app-rows-create", { rows: Array.from({ length: 501 }, () => ({ title: "too-many" })) }),
248
+ ).rejects.toThrow();
249
+ await expect(call("app-rows-create", { rows: [] })).rejects.toThrow();
250
+ await expect(call("app-rows-delete", {})).rejects.toThrow();
251
+ await expect(call("app-rows-delete", { equals: {} })).rejects.toThrow();
252
+ await expect(call("app-rows-list", { limit: 1001 })).rejects.toThrow();
253
+ const oversized = "x".repeat(1024 * 1024);
254
+ expect((await call("app-row-create", { values: { title: oversized } })).isError).toBe(true);
255
+ expect((await call("app-rows-create", { rows: [{ title: oversized }] })).isError).toBe(true);
256
+ expect(
257
+ (
258
+ await call("app-rows-create", {
259
+ rows: Array.from({ length: 20 }, () => ({ title: "x".repeat(600_000) })),
260
+ })
261
+ ).isError,
262
+ ).toBe(true);
263
+ const row = (await call("app-row-create", { values: { title: "cap-patch" } })).structuredContent
264
+ .row;
265
+ expect(
266
+ (await call("app-row-patch", { rowId: row.id, values: { title: oversized } })).isError,
267
+ ).toBe(true);
268
+ expect((await call("app-row-get", { rowId: row.id })).structuredContent.row.title).toBe(
269
+ "cap-patch",
270
+ );
271
+ expect(
272
+ (await call("app-rows-list", { equals: { title: "too-many" } })).structuredContent.total,
273
+ ).toBe(0);
274
+ });
275
+
276
+ test("denies a principal without app.use for every row operation", async () => {
277
+ const policy = LEGACY_POLICY as unknown as Record<"app.use", LegacyRule>;
278
+ const original = policy["app.use"];
279
+ policy["app.use"] = {
280
+ name: "deny-row-tools",
281
+ denyReason: "denied row tools",
282
+ evaluate: () => false,
283
+ };
284
+ try {
285
+ for (const [name, input] of [
286
+ ["app-rows-list", {}],
287
+ ["app-row-get", { rowId: "missing" }],
288
+ ["app-row-create", { values: { title: "denied" } }],
289
+ ["app-rows-create", { rows: [{ title: "denied" }] }],
290
+ ["app-row-patch", { rowId: "missing", values: { title: "denied" } }],
291
+ ["app-row-delete", { rowId: "missing" }],
292
+ ["app-rows-delete", { equals: { title: "denied" } }],
293
+ ] as const)
294
+ expect((await call(name, input)).isError).toBe(true);
295
+ } finally {
296
+ policy["app.use"] = original;
297
+ }
298
+ });
299
+
300
+ test("saved scripts can typecheck all seven row methods and cannot invoke actions", async () => {
301
+ const valid = await typecheckScript(`
302
+ import type { ScriptContext } from "swarm-sdk";
303
+ export default async function(args: unknown, ctx: ScriptContext) {
304
+ const base = { appId: "app", model: "note" };
305
+ await ctx.swarm.app_row_create({ ...base, values: { title: "test" } });
306
+ await ctx.swarm.app_rows_create({ ...base, rows: [{ title: "test" }] });
307
+ await ctx.swarm.app_rows_list({ ...base, equals: { title: "test" }, sort: "createdAt:desc", limit: 1 });
308
+ await ctx.swarm.app_row_get({ ...base, rowId: "row" });
309
+ await ctx.swarm.app_row_patch({ ...base, rowId: "row", values: { title: "changed", rank: null } });
310
+ await ctx.swarm.app_row_delete({ ...base, rowId: "row" });
311
+ await ctx.swarm.app_rows_delete({ ...base, equals: { title: "test" } });
312
+ }
313
+ `);
314
+ expect(valid).toEqual({ ok: true });
315
+ const invalid = await typecheckScript(`
316
+ import type { ScriptContext } from "swarm-sdk";
317
+ export default async function(args: unknown, ctx: ScriptContext) {
318
+ await ctx.swarm.app_action_run({ appId: "app", action: "write" });
319
+ }
320
+ `);
321
+ expect(invalid.ok).toBe(false);
322
+ if (!invalid.ok) expect(invalid.diagnostics.join("\n")).toContain("app_action_run");
323
+ });
324
+
325
+ test.skipIf(SKIP_SANDBOX_SPAWN_TESTS)(
326
+ "a saved script creates and lists rows through ctx.swarm",
327
+ async () => {
328
+ const server = createHttpServer(async (req, res) => {
329
+ const id = req.headers["x-agent-id"] as string | undefined;
330
+ if (await handleCore(req, res, id, apiKey)) return;
331
+ if (
332
+ await handleMcpBridge(
333
+ req,
334
+ res,
335
+ getPathSegments(req.url ?? ""),
336
+ parseQueryParams(req.url ?? ""),
337
+ id,
338
+ )
339
+ )
340
+ return;
341
+ res.writeHead(404).end();
342
+ });
343
+ try {
344
+ const port = await listenOnFreePort(server);
345
+ const script = await import("../be/scripts/db").then(({ upsertScriptByName }) =>
346
+ upsertScriptByName({
347
+ name: "row_tools_sdk",
348
+ scope: "agent",
349
+ scopeId: agentId,
350
+ agentId,
351
+ source: `export default async function(args, ctx) {
352
+ const created = await ctx.swarm.app_row_create({ appId: args.appId, model: "note", values: { title: "script-sdk", rank: 1 } });
353
+ const listed = await ctx.swarm.app_rows_list({ appId: args.appId, model: "note", equals: { title: "script-sdk" } });
354
+ const rowId = (created?.data ?? created).row.id;
355
+ const cleared = await ctx.swarm.app_row_patch({ appId: args.appId, model: "note", rowId, values: { rank: null } });
356
+ return { created: created?.data ?? created, listed: listed?.data ?? listed, cleared: cleared?.data ?? cleared };
357
+ }`,
358
+ description: "SDK row fixture",
359
+ intent: "Verify row tools through the script SDK",
360
+ signatureJson: "{}",
361
+ typeChecked: true,
362
+ embeddingMode: "skip",
363
+ }),
364
+ );
365
+ const output = await runScript({
366
+ source: script.script.source,
367
+ args: { appId },
368
+ agentId,
369
+ mcpBaseUrl: `http://localhost:${port}`,
370
+ });
371
+ expect(output.error).toBeUndefined();
372
+ expect(output.runtimeError).toBeUndefined();
373
+ expect(output.result).toMatchObject({
374
+ created: { row: { createdBy: `agent:${agentId}` } },
375
+ listed: { total: 1, rows: [{ title: "script-sdk" }] },
376
+ cleared: { row: { title: "script-sdk" } },
377
+ });
378
+ const cleared = (output.result as { cleared: { row: Record<string, unknown> } }).cleared.row;
379
+ expect(Object.hasOwn(cleared, "rank")).toBe(false);
380
+ } finally {
381
+ if (server.listening) await new Promise<void>((resolve) => server.close(() => resolve()));
382
+ }
383
+ },
384
+ CHILD_PROCESS_TEST_BUDGET_MS,
385
+ );