@desplega.ai/agent-swarm 2.4.0 → 2.4.1
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.
- package/README.md +1 -1
- package/dist/{acp-adapter-kedstxx6.js → acp-adapter-zvzr538d.js} +9 -9
- package/dist/{actions-vget6r1k.js → actions-jhtqdw69.js} +21 -21
- package/dist/{amp-adapter-xeb5mktv.js → amp-adapter-bd84rhs2.js} +4 -4
- package/dist/{app-mqap8t8m.js → app-kreq1gwg.js} +3 -3
- package/dist/{assistant-h0gv97mv.js → assistant-3k22sbsq.js} +22 -22
- package/dist/{boot-reembed-qr8a894z.js → boot-reembed-e53x6b9v.js} +1 -1
- package/dist/{boot-reembed-rj85jjhw.js → boot-reembed-f6z923ny.js} +1 -1
- package/dist/{boot-scrub-sweep-eyfvn6v9.js → boot-scrub-sweep-0538vsb7.js} +4 -4
- package/dist/{boot-seeds-xs9a8dx1.js → boot-seeds-6wxqkfk2.js} +4 -4
- package/dist/{claude-adapter-bm23cye7.js → claude-adapter-gfts5044.js} +9 -9
- package/dist/{claude-managed-adapter-1mwrsm9d.js → claude-managed-adapter-h2y889ge.js} +2 -2
- package/dist/{claude-managed-setup-gkfgks4j.js → claude-managed-setup-h00hsgtz.js} +2 -2
- package/dist/{claude-sdk-session-apb4ypz3.js → claude-sdk-session-56besmfz.js} +13 -13
- package/dist/{cli-nrrg0fvy.js → cli-0bh477zn.js} +18 -5
- package/dist/{cli-7gh7020n.js → cli-1dmcq2eh.js} +1 -1
- package/dist/{cli-7ayprxp7.js → cli-2dfbv8qr.js} +8 -0
- package/dist/{cli-1cevz46c.js → cli-3kh4kpqy.js} +6 -6
- package/dist/{cli-tg9ksbn8.js → cli-3p8fqcnh.js} +2 -2
- package/dist/{cli-x1p5dhq2.js → cli-3r448dqn.js} +3 -3
- package/dist/{cli-ecshf504.js → cli-3tnpj2bh.js} +7 -7
- package/dist/{cli-ffbrkh1r.js → cli-471qk6s2.js} +46 -38
- package/dist/{cli-a49vpbf3.js → cli-5kqnemen.js} +1 -1
- package/dist/{cli-zvze9rbm.js → cli-6ncjvx55.js} +3 -3
- package/dist/{cli-fb6t92v8.js → cli-6x9ma5w5.js} +1 -1
- package/dist/{cli-c8fwpgqb.js → cli-7225gwj7.js} +2 -2
- package/dist/{cli-bn26xbcs.js → cli-8nyhf7za.js} +1 -1
- package/dist/{cli-y83zk5sb.js → cli-a0kcty51.js} +4 -4
- package/dist/{cli-8b9wvjps.js → cli-a4gvwttf.js} +4 -4
- package/dist/{cli-jhp7w1fy.js → cli-a5ebc95n.js} +2 -2
- package/dist/{cli-mz1ezzzh.js → cli-ash6xxk7.js} +2 -2
- package/dist/{cli-rsd7ga7v.js → cli-axh29tyr.js} +11 -11
- package/dist/{cli-p22zdvnt.js → cli-bn0rewsy.js} +3 -3
- package/dist/{cli-6ftrrwm1.js → cli-bn5kevnf.js} +5 -5
- package/dist/{cli-p1m2b6gc.js → cli-d2mb5s8a.js} +7 -7
- package/dist/{cli-c5sbmw1v.js → cli-d82btycs.js} +94 -15
- package/dist/{cli-vjfpshn3.js → cli-ejs3m1sx.js} +1 -1
- package/dist/{cli-a1esmmee.js → cli-fpmsqq59.js} +7 -2
- package/dist/{cli-8cxkw1f5.js → cli-fz9kewbh.js} +2 -2
- package/dist/{cli-8rwvksbc.js → cli-g0hgtz4p.js} +5 -5
- package/dist/{cli-kz116kvw.js → cli-gf0kfg81.js} +7 -7
- package/dist/{cli-eqgfs202.js → cli-hqhzg1qq.js} +2 -2
- package/dist/{cli-3wjs22x3.js → cli-j4rqdf2n.js} +2 -2
- package/dist/{cli-ywyrffff.js → cli-jh0qcrga.js} +1 -1
- package/dist/{cli-ttaz6c7m.js → cli-k9z4zd4p.js} +6 -5
- package/dist/{cli-nme7wery.js → cli-kmqgmcnb.js} +46 -13
- package/dist/{cli-x719j3vs.js → cli-kr2pmfqx.js} +392 -168
- package/dist/{cli-wrv09v7q.js → cli-m2qgh5sv.js} +1 -1
- package/dist/{cli-xfe63zx3.js → cli-mfe7f7b5.js} +2 -2
- package/dist/{cli-x3en6ct3.js → cli-nwyw3zhg.js} +1 -1
- package/dist/{cli-5s5hx57y.js → cli-nx462e4v.js} +1 -1
- package/dist/{cli-bfc3k1x5.js → cli-p0rakbrk.js} +9 -9
- package/dist/{cli-avmqhatn.js → cli-p6argxh9.js} +8 -8
- package/dist/{cli-3avfh5az.js → cli-pemywhfq.js} +2 -2
- package/dist/{cli-sppc0cmw.js → cli-pnp2j5zg.js} +2 -2
- package/dist/{cli-wk8ccrz6.js → cli-pw7v69vp.js} +2 -2
- package/dist/{cli-bhwa8rzc.js → cli-rdq7tzwm.js} +13 -12
- package/dist/{cli-rg7ntypm.js → cli-rqqsfz30.js} +379 -87
- package/dist/{cli-kk2r80n5.js → cli-s48yvzb8.js} +4 -4
- package/dist/{cli-zv4vy2b0.js → cli-s95dknxh.js} +1 -1
- package/dist/{cli-b80a1100.js → cli-sjyetj21.js} +2 -2
- package/dist/{cli-rdr166ht.js → cli-smvcw9z5.js} +1 -1
- package/dist/{cli-2rwet3r7.js → cli-sv5gb0yb.js} +1 -1
- package/dist/{cli-5ff2sjyt.js → cli-syhq378v.js} +2 -2
- package/dist/{cli-tj4j2ts4.js → cli-tbhwrxpt.js} +8 -8
- package/dist/{cli-4fhyq711.js → cli-vy0fay6k.js} +13 -13
- package/dist/{cli-ptkpaqfy.js → cli-wt645v7q.js} +1 -1
- package/dist/{cli-f3nx1h8z.js → cli-wxbn3yfx.js} +2 -2
- package/dist/{cli-wyrr38xm.js → cli-x5mq1fyq.js} +1 -1
- package/dist/{cli-ccn0eybf.js → cli-xbzvet96.js} +6 -6
- package/dist/{cli-4mkvwgzr.js → cli-xf234ws8.js} +1 -1
- package/dist/{cli-tgvpmnwz.js → cli-xftrvxag.js} +5 -5
- package/dist/{cli-02vhhssk.js → cli-z071fb95.js} +5 -5
- package/dist/{cli-mvg2hf8p.js → cli-z1ew8vbq.js} +14 -7
- package/dist/{cli-dsgj3e7a.js → cli-zfg9enh9.js} +2 -2
- package/dist/{cli-yxqag76b.js → cli-zw88zjba.js} +1 -1
- package/dist/cli.js +19 -19
- package/dist/{codex-adapter-pp2gq8gr.js → codex-adapter-p6rmm7vv.js} +6 -6
- package/dist/{codex-hook-qwzccd64.js → codex-hook-x7nejyrr.js} +7 -4
- package/dist/{codex-login-35p4sk85.js → codex-login-m819c41r.js} +1 -1
- package/dist/{codex-session-runner-0vzn09t8.js → codex-session-runner-8vd5t9t0.js} +7 -7
- package/dist/{commands-psqhkttb.js → commands-7ems2p13.js} +2 -2
- package/dist/{controller-t20vz2k8.js → controller-m263fg43.js} +4 -4
- package/dist/{controllers-connect-gkxmqw9d.js → controllers-connect-bg33ae53.js} +23 -23
- package/dist/{cursor-adapter-3v542qe5.js → cursor-adapter-cz6nm6mj.js} +3 -3
- package/dist/{db-2xbyenjr.js → db-w505dwa8.js} +2 -2
- package/dist/{db-nc1rd869.js → db-ys2rbdft.js} +3 -3
- package/dist/{devin-adapter-7egzmt0k.js → devin-adapter-zgqadvnm.js} +1 -1
- package/dist/{dispatcher-ewceb8ts.js → dispatcher-0s0mckqr.js} +20 -20
- package/dist/{docker-njavd3fb.js → docker-fkgtr75t.js} +2 -2
- package/dist/{dsh-adapter-75xwh78t.js → dsh-adapter-3n3net33.js} +3 -3
- package/dist/{e2b-5r6a5jeb.js → e2b-7am6tmq6.js} +2 -2
- package/dist/{e2b-pw1nvtmh.js → e2b-thdcxtjd.js} +2 -2
- package/dist/{easter-egg-3d-xsc47jwf.js → easter-egg-3d-5cgynaja.js} +1 -1
- package/dist/{event-bus-101fenkh.js → event-bus-9vpk8gmq.js} +1 -1
- package/dist/{execute-gzmw1mnw.js → execute-t4e6m3rw.js} +1 -1
- package/dist/{external-channel-a5dsmxn4.js → external-channel-5b23fv8z.js} +2 -2
- package/dist/{grok-adapter-vj88xr23.js → grok-adapter-q9fghe5q.js} +10 -10
- package/dist/handlers-mqrym347.js +39 -0
- package/dist/{heartbeat-t2m9r3wa.js → heartbeat-tww6yx1w.js} +23 -23
- package/dist/{hook-xa677f8r.js → hook-dx0pkvar.js} +16 -9
- package/dist/{hook-8femx1bg.js → hook-pvmh9b6m.js} +6 -6
- package/dist/{http-xpmmq028.js → http-k2ejbt05.js} +199 -131
- package/dist/{index-s7ckdajq.js → index-2z710v4x.js} +10 -10
- package/dist/{index-dg2tmj99.js → index-dgz61nff.js} +20 -20
- package/dist/{index-syztqdt6.js → index-g6tbb6ve.js} +1 -1
- package/dist/index-h2ek08d6.js +40 -0
- package/dist/index-pqfwh9nn.js +41 -0
- package/dist/{index-cdzyfrsp.js → index-x5aktmp8.js} +1 -1
- package/dist/{keepalive-3nr4st3r.js → keepalive-nhvpw10z.js} +4 -4
- package/dist/lead-jb6sf69z.js +78 -0
- package/dist/{maintenance-verwpe2c.js → maintenance-agakc519.js} +1 -1
- package/dist/{oauth-refresh-sweep-9q2t0y3q.js → oauth-refresh-sweep-4dcbhtn0.js} +3 -3
- package/dist/{onboard-aytqedmq.js → onboard-4n2kwa4v.js} +3 -3
- package/dist/{opencode-adapter-f4hryjny.js → opencode-adapter-3s7ax02e.js} +7 -7
- package/dist/{otel-impl-hckw4wt1.js → otel-impl-tfa12080.js} +2 -2
- package/dist/{pi-mono-adapter-hxby0ce1.js → pi-mono-adapter-ytp7gz8b.js} +22 -11
- package/dist/{pricing-refresh-5nvme3sm.js → pricing-refresh-3cqx3f3x.js} +3 -3
- package/dist/{rbac-roles-pd8c26m1.js → rbac-roles-9fae5rnv.js} +1 -1
- package/dist/{rbac-roles-vbpy21mg.js → rbac-roles-y6zbfzve.js} +2 -2
- package/dist/{render-v2-7nqgvk2q.js → render-v2-emawjvc4.js} +3 -3
- package/dist/{resume-b6djcj3c.js → resume-zhgts7h7.js} +10 -8
- package/dist/{runtime-txrxc9za.js → runtime-xe3aqy4x.js} +1 -1
- package/dist/{session-access-fc3f9y12.js → session-access-cm823e06.js} +4 -4
- package/dist/{setup-n39szm6b.js → setup-tkp99w1e.js} +1 -1
- package/dist/{virtual-modules-wyp2k6z3.js → virtual-modules-4r3zkc20.js} +2 -2
- package/dist/{work-objects-jem2fre0.js → work-objects-f722h263.js} +23 -23
- package/dist/worker-p7f336rq.js +78 -0
- package/dist/{x-8ewt8gxn.js → x-y45mthwn.js} +2 -2
- package/openapi.json +99 -52
- package/package.json +2 -2
- package/src/apps/action-runner.ts +146 -0
- package/src/apps/row-store.ts +4 -2
- package/src/apps/script-types.ts +1 -1
- package/src/be/db.ts +14 -4
- package/src/be/fleet-benchmark-stats.ts +10 -5
- package/src/be/models-catalog.ts +4 -0
- package/src/be/onboarding.ts +6 -0
- package/src/be/scripts/typecheck.ts +13 -0
- package/src/be/swarm-config-guard.ts +10 -1
- package/src/be/telemetry-enable.ts +1 -1
- package/src/commands/provider-credentials.ts +12 -2
- package/src/commands/runner.ts +3 -0
- package/src/heartbeat/heartbeat.ts +2 -2
- package/src/hooks/codex-hook.ts +6 -2
- package/src/hooks/hook.ts +7 -0
- package/src/hooks/pr-body-guard.ts +38 -11
- package/src/hooks/pr-room-guard.ts +88 -0
- package/src/http/agents.ts +1 -0
- package/src/http/apps.ts +84 -181
- package/src/http/fleet-benchmark.ts +79 -30
- package/src/http/global-config.ts +7 -0
- package/src/http/ranking.ts +24 -40
- package/src/http/tasks.ts +2 -1
- package/src/http/telemetry-ui.ts +1 -1
- package/src/http/used-by.ts +55 -19
- package/src/http/users.ts +2 -2
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +15 -0
- package/src/providers/reasoning-effort.ts +2 -1
- package/src/providers/types.ts +2 -0
- package/src/ranking/telemetry-ranking.ts +5 -33
- package/src/scripts-runtime/sdk-allowlist.ts +7 -0
- package/src/scripts-runtime/types/stdlib.d.ts +31 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +31 -0
- package/src/server.ts +4 -0
- package/src/telemetry-context.ts +19 -1
- package/src/telemetry-contract/EVENTS.md +42 -8
- package/src/telemetry-contract/SOURCE.json +6 -5
- package/src/telemetry-contract/catalog.json +225 -5
- package/src/telemetry-contract/ranking/fixtures/valid/no-claim.json +10 -0
- package/src/telemetry-contract/ranking/listing.v1.schema.json +2 -2
- package/src/telemetry-contract/types.gen.ts +225 -27
- package/src/telemetry-identity.ts +47 -9
- package/src/telemetry-snapshot.ts +9 -1
- package/src/telemetry-ui-events.ts +9 -2
- package/src/telemetry.ts +14 -3
- package/src/tests/app-action-run-tool.test.ts +178 -0
- package/src/tests/app-row-tools.test.ts +360 -0
- package/src/tests/check-pr-body.test.ts +0 -23
- package/src/tests/credential-check.test.ts +38 -0
- package/src/tests/fleet-benchmark.test.ts +122 -2
- package/src/tests/http-users.test.ts +62 -1
- package/src/tests/model-catalog-table.test.ts +63 -0
- package/src/tests/model-profiles-api.test.ts +19 -7
- package/src/tests/models-catalog.test.ts +50 -0
- package/src/tests/onboarding.test.ts +21 -0
- package/src/tests/org-website.test.ts +62 -0
- package/src/tests/pi-mono-adapter.test.ts +23 -0
- package/src/tests/pr-body-leaks.test.ts +4 -0
- package/src/tests/pr-room-guard.test.ts +242 -0
- package/src/tests/ranking-listing.test.ts +8 -23
- package/src/tests/ranking-route.test.ts +49 -47
- package/src/tests/reasoning-effort.test.ts +39 -3
- package/src/tests/runner-harness-telemetry-env.test.ts +56 -1
- package/src/tests/swarm-tool-result-gate.test.ts +2 -4
- package/src/tests/telemetry-context.test.ts +128 -2
- package/src/tests/telemetry-ui-route.test.ts +123 -0
- package/src/tests/tool-annotations.test.ts +3 -2
- package/src/tests/used-by-route.test.ts +144 -10
- package/src/tests/workflow-agent-task-retry.test.ts +130 -4
- package/src/tests/workflow-wait-recovery.test.ts +116 -4
- package/src/tools/app-action-run.ts +63 -0
- package/src/tools/app-rows.ts +234 -0
- package/src/tools/model-catalog/index.ts +1 -1
- package/src/tools/tool-config.ts +8 -0
- package/src/tools/utils.ts +1 -1
- package/src/tools/workflows/retry-workflow-run.ts +1 -1
- package/src/types.ts +17 -0
- package/src/utils/company-profile.ts +24 -0
- package/src/utils/org-website.ts +58 -21
- package/src/workflows/recovery.ts +10 -22
- package/src/workflows/resume.ts +16 -0
- package/src/workflows/task-step-routing.ts +47 -7
- package/templates/skills/apps/SKILL.md +41 -2
- package/templates/skills/apps/content.md +41 -2
- package/dist/handlers-zbs90bny.js +0 -39
- package/dist/index-8pwc1jc8.js +0 -40
- package/dist/index-nxa6me0q.js +0 -41
- package/dist/lead-td8h2sfn.js +0 -78
- package/dist/worker-wr6xgfr2.js +0 -78
- package/src/tests/check-ranking-entry.test.ts +0 -146
package/src/telemetry.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
validOrgDomain,
|
|
23
23
|
validOrgId,
|
|
24
24
|
} from "./telemetry-context";
|
|
25
|
-
import type { PropsFor, TelemetryEventName } from "./telemetry-contract";
|
|
25
|
+
import type { EventContext, PropsFor, TelemetryEventName } from "./telemetry-contract";
|
|
26
26
|
import { isEnvFlagEnabled } from "./utils/env-flag";
|
|
27
27
|
import { getNodeEnv } from "./utils/node-env";
|
|
28
28
|
import { normalizeOrgWebsite } from "./utils/org-website";
|
|
@@ -68,6 +68,7 @@ let cachedOrgId: string | undefined;
|
|
|
68
68
|
/** Email domain of the org's admin, set by the API server (`setTelemetryOrgDomain`). */
|
|
69
69
|
let cachedOrgDomain: string | undefined;
|
|
70
70
|
let roleResolver: ((userId: string) => TelemetryUserRole | undefined) | null = null;
|
|
71
|
+
let functionResolver: ((userId: string) => EventContext["user_function"]) | null = null;
|
|
71
72
|
let identityReloader: (() => Promise<void>) | null = null;
|
|
72
73
|
let lastIdentityLoadAt = 0;
|
|
73
74
|
let source = "unknown";
|
|
@@ -510,6 +511,13 @@ export function setTelemetryRoleResolver(
|
|
|
510
511
|
roleResolver = resolver;
|
|
511
512
|
}
|
|
512
513
|
|
|
514
|
+
/** API server only: cache-backed lookup of the actor's job function. */
|
|
515
|
+
export function setTelemetryFunctionResolver(
|
|
516
|
+
resolver: ((userId: string) => EventContext["user_function"]) | null,
|
|
517
|
+
): void {
|
|
518
|
+
functionResolver = resolver;
|
|
519
|
+
}
|
|
520
|
+
|
|
513
521
|
/**
|
|
514
522
|
* True when `track()` would send: telemetry is on, an install ID exists and an
|
|
515
523
|
* org ID resolved. Emitters that record "already sent" state check this first.
|
|
@@ -664,8 +672,10 @@ function emit(
|
|
|
664
672
|
swarmVersion: pkg.version,
|
|
665
673
|
installMethod: cachedInstallMethod,
|
|
666
674
|
installPreset: cachedInstallPreset,
|
|
667
|
-
|
|
668
|
-
|
|
675
|
+
acquisitionSource: process.env.SWARM_ORG_ACQUISITION_SOURCE,
|
|
676
|
+
teamSize: process.env.SWARM_ORG_TEAM_SIZE,
|
|
677
|
+
useCase: process.env.SWARM_ORG_USE_CASE,
|
|
678
|
+
userFunction: userId ? functionResolver?.(userId) : undefined,
|
|
669
679
|
}),
|
|
670
680
|
properties: normalizeProperties(event, {
|
|
671
681
|
...properties,
|
|
@@ -745,6 +755,7 @@ export function _resetTelemetryStateForTests(): void {
|
|
|
745
755
|
cachedOrgId = undefined;
|
|
746
756
|
cachedOrgDomain = undefined;
|
|
747
757
|
roleResolver = null;
|
|
758
|
+
functionResolver = null;
|
|
748
759
|
identityReloader = null;
|
|
749
760
|
lastIdentityLoadAt = 0;
|
|
750
761
|
}
|
|
@@ -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,360 @@
|
|
|
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("enforces row, bulk, filter, and limit caps before writing", async () => {
|
|
221
|
+
await expect(
|
|
222
|
+
call("app-rows-create", { rows: Array.from({ length: 501 }, () => ({ title: "too-many" })) }),
|
|
223
|
+
).rejects.toThrow();
|
|
224
|
+
await expect(call("app-rows-create", { rows: [] })).rejects.toThrow();
|
|
225
|
+
await expect(call("app-rows-delete", {})).rejects.toThrow();
|
|
226
|
+
await expect(call("app-rows-delete", { equals: {} })).rejects.toThrow();
|
|
227
|
+
await expect(call("app-rows-list", { limit: 1001 })).rejects.toThrow();
|
|
228
|
+
const oversized = "x".repeat(1024 * 1024);
|
|
229
|
+
expect((await call("app-row-create", { values: { title: oversized } })).isError).toBe(true);
|
|
230
|
+
expect((await call("app-rows-create", { rows: [{ title: oversized }] })).isError).toBe(true);
|
|
231
|
+
expect(
|
|
232
|
+
(
|
|
233
|
+
await call("app-rows-create", {
|
|
234
|
+
rows: Array.from({ length: 20 }, () => ({ title: "x".repeat(600_000) })),
|
|
235
|
+
})
|
|
236
|
+
).isError,
|
|
237
|
+
).toBe(true);
|
|
238
|
+
const row = (await call("app-row-create", { values: { title: "cap-patch" } })).structuredContent
|
|
239
|
+
.row;
|
|
240
|
+
expect(
|
|
241
|
+
(await call("app-row-patch", { rowId: row.id, values: { title: oversized } })).isError,
|
|
242
|
+
).toBe(true);
|
|
243
|
+
expect((await call("app-row-get", { rowId: row.id })).structuredContent.row.title).toBe(
|
|
244
|
+
"cap-patch",
|
|
245
|
+
);
|
|
246
|
+
expect(
|
|
247
|
+
(await call("app-rows-list", { equals: { title: "too-many" } })).structuredContent.total,
|
|
248
|
+
).toBe(0);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("denies a principal without app.use for every row operation", async () => {
|
|
252
|
+
const policy = LEGACY_POLICY as unknown as Record<"app.use", LegacyRule>;
|
|
253
|
+
const original = policy["app.use"];
|
|
254
|
+
policy["app.use"] = {
|
|
255
|
+
name: "deny-row-tools",
|
|
256
|
+
denyReason: "denied row tools",
|
|
257
|
+
evaluate: () => false,
|
|
258
|
+
};
|
|
259
|
+
try {
|
|
260
|
+
for (const [name, input] of [
|
|
261
|
+
["app-rows-list", {}],
|
|
262
|
+
["app-row-get", { rowId: "missing" }],
|
|
263
|
+
["app-row-create", { values: { title: "denied" } }],
|
|
264
|
+
["app-rows-create", { rows: [{ title: "denied" }] }],
|
|
265
|
+
["app-row-patch", { rowId: "missing", values: { title: "denied" } }],
|
|
266
|
+
["app-row-delete", { rowId: "missing" }],
|
|
267
|
+
["app-rows-delete", { equals: { title: "denied" } }],
|
|
268
|
+
] as const)
|
|
269
|
+
expect((await call(name, input)).isError).toBe(true);
|
|
270
|
+
} finally {
|
|
271
|
+
policy["app.use"] = original;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("saved scripts can typecheck all seven row methods and cannot invoke actions", async () => {
|
|
276
|
+
const valid = await typecheckScript(`
|
|
277
|
+
import type { ScriptContext } from "swarm-sdk";
|
|
278
|
+
export default async function(args: unknown, ctx: ScriptContext) {
|
|
279
|
+
const base = { appId: "app", model: "note" };
|
|
280
|
+
await ctx.swarm.app_row_create({ ...base, values: { title: "test" } });
|
|
281
|
+
await ctx.swarm.app_rows_create({ ...base, rows: [{ title: "test" }] });
|
|
282
|
+
await ctx.swarm.app_rows_list({ ...base, equals: { title: "test" }, sort: "createdAt:desc", limit: 1 });
|
|
283
|
+
await ctx.swarm.app_row_get({ ...base, rowId: "row" });
|
|
284
|
+
await ctx.swarm.app_row_patch({ ...base, rowId: "row", values: { title: "changed", rank: null } });
|
|
285
|
+
await ctx.swarm.app_row_delete({ ...base, rowId: "row" });
|
|
286
|
+
await ctx.swarm.app_rows_delete({ ...base, equals: { title: "test" } });
|
|
287
|
+
}
|
|
288
|
+
`);
|
|
289
|
+
expect(valid).toEqual({ ok: true });
|
|
290
|
+
const invalid = await typecheckScript(`
|
|
291
|
+
import type { ScriptContext } from "swarm-sdk";
|
|
292
|
+
export default async function(args: unknown, ctx: ScriptContext) {
|
|
293
|
+
await ctx.swarm.app_action_run({ appId: "app", action: "write" });
|
|
294
|
+
}
|
|
295
|
+
`);
|
|
296
|
+
expect(invalid.ok).toBe(false);
|
|
297
|
+
if (!invalid.ok) expect(invalid.diagnostics.join("\n")).toContain("app_action_run");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test.skipIf(SKIP_SANDBOX_SPAWN_TESTS)(
|
|
301
|
+
"a saved script creates and lists rows through ctx.swarm",
|
|
302
|
+
async () => {
|
|
303
|
+
const server = createHttpServer(async (req, res) => {
|
|
304
|
+
const id = req.headers["x-agent-id"] as string | undefined;
|
|
305
|
+
if (await handleCore(req, res, id, apiKey)) return;
|
|
306
|
+
if (
|
|
307
|
+
await handleMcpBridge(
|
|
308
|
+
req,
|
|
309
|
+
res,
|
|
310
|
+
getPathSegments(req.url ?? ""),
|
|
311
|
+
parseQueryParams(req.url ?? ""),
|
|
312
|
+
id,
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
return;
|
|
316
|
+
res.writeHead(404).end();
|
|
317
|
+
});
|
|
318
|
+
try {
|
|
319
|
+
const port = await listenOnFreePort(server);
|
|
320
|
+
const script = await import("../be/scripts/db").then(({ upsertScriptByName }) =>
|
|
321
|
+
upsertScriptByName({
|
|
322
|
+
name: "row_tools_sdk",
|
|
323
|
+
scope: "agent",
|
|
324
|
+
scopeId: agentId,
|
|
325
|
+
agentId,
|
|
326
|
+
source: `export default async function(args, ctx) {
|
|
327
|
+
const created = await ctx.swarm.app_row_create({ appId: args.appId, model: "note", values: { title: "script-sdk", rank: 1 } });
|
|
328
|
+
const listed = await ctx.swarm.app_rows_list({ appId: args.appId, model: "note", equals: { title: "script-sdk" } });
|
|
329
|
+
const rowId = (created?.data ?? created).row.id;
|
|
330
|
+
const cleared = await ctx.swarm.app_row_patch({ appId: args.appId, model: "note", rowId, values: { rank: null } });
|
|
331
|
+
return { created: created?.data ?? created, listed: listed?.data ?? listed, cleared: cleared?.data ?? cleared };
|
|
332
|
+
}`,
|
|
333
|
+
description: "SDK row fixture",
|
|
334
|
+
intent: "Verify row tools through the script SDK",
|
|
335
|
+
signatureJson: "{}",
|
|
336
|
+
typeChecked: true,
|
|
337
|
+
embeddingMode: "skip",
|
|
338
|
+
}),
|
|
339
|
+
);
|
|
340
|
+
const output = await runScript({
|
|
341
|
+
source: script.script.source,
|
|
342
|
+
args: { appId },
|
|
343
|
+
agentId,
|
|
344
|
+
mcpBaseUrl: `http://localhost:${port}`,
|
|
345
|
+
});
|
|
346
|
+
expect(output.error).toBeUndefined();
|
|
347
|
+
expect(output.runtimeError).toBeUndefined();
|
|
348
|
+
expect(output.result).toMatchObject({
|
|
349
|
+
created: { row: { createdBy: `agent:${agentId}` } },
|
|
350
|
+
listed: { total: 1, rows: [{ title: "script-sdk" }] },
|
|
351
|
+
cleared: { row: { title: "script-sdk" } },
|
|
352
|
+
});
|
|
353
|
+
const cleared = (output.result as { cleared: { row: Record<string, unknown> } }).cleared.row;
|
|
354
|
+
expect(Object.hasOwn(cleared, "rank")).toBe(false);
|
|
355
|
+
} finally {
|
|
356
|
+
if (server.listening) await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
360
|
+
);
|
|
@@ -2,7 +2,6 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import {
|
|
3
3
|
checkPrBody,
|
|
4
4
|
isFixTitle,
|
|
5
|
-
isRankingEntryPr,
|
|
6
5
|
pickedChoices,
|
|
7
6
|
prBodyWarnings,
|
|
8
7
|
proseWordCount,
|
|
@@ -35,28 +34,6 @@ const TEMPLATE = `<!-- top comment -->
|
|
|
35
34
|
|
|
36
35
|
const URGENT = "## Urgency\n- [ ] asap\n- [x] nice to have\n";
|
|
37
36
|
|
|
38
|
-
describe("isRankingEntryPr", () => {
|
|
39
|
-
test("exempts only a one-file entry PR with the matching title", () => {
|
|
40
|
-
expect(
|
|
41
|
-
isRankingEntryPr("chore(ranking): add acme.dev", ["community/ranking/acme.dev.json"]),
|
|
42
|
-
).toBe(true);
|
|
43
|
-
expect(
|
|
44
|
-
isRankingEntryPr("chore(ranking): remove acme.dev", ["community/ranking/acme.dev.json"]),
|
|
45
|
-
).toBe(true);
|
|
46
|
-
expect(
|
|
47
|
-
isRankingEntryPr("chore(ranking): add acme.dev", ["community/ranking/acme.com.json"]),
|
|
48
|
-
).toBe(false);
|
|
49
|
-
expect(
|
|
50
|
-
isRankingEntryPr("chore(ranking): add acme.dev", [
|
|
51
|
-
"community/ranking/acme.dev.json",
|
|
52
|
-
"src/server.ts",
|
|
53
|
-
]),
|
|
54
|
-
).toBe(false);
|
|
55
|
-
expect(isRankingEntryPr("feat: add acme.dev", ["community/ranking/acme.dev.json"])).toBe(false);
|
|
56
|
-
expect(isRankingEntryPr("chore(ranking): add acme.dev", undefined)).toBe(false);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
37
|
describe("check-pr-body", () => {
|
|
61
38
|
test("template markers set when a section is required", () => {
|
|
62
39
|
expect(templateSections(TEMPLATE)).toEqual([
|
|
@@ -236,6 +236,26 @@ describe("checkPiMonoCredentials", () => {
|
|
|
236
236
|
).toBe(false);
|
|
237
237
|
});
|
|
238
238
|
|
|
239
|
+
test("strict: MODEL_OVERRIDE=opencode-go/... requires OPENCODE_API_KEY", async () => {
|
|
240
|
+
const env = { MODEL_OVERRIDE: "opencode-go/deepseek-v4.1-flash" };
|
|
241
|
+
expect(
|
|
242
|
+
(
|
|
243
|
+
await checkPiMonoCredentials(
|
|
244
|
+
{ ...env, OPENCODE_API_KEY: "x" },
|
|
245
|
+
{ homeDir: HOME, fs: noFiles },
|
|
246
|
+
)
|
|
247
|
+
).ready,
|
|
248
|
+
).toBe(true);
|
|
249
|
+
expect(
|
|
250
|
+
(
|
|
251
|
+
await checkPiMonoCredentials(
|
|
252
|
+
{ ...env, ANTHROPIC_API_KEY: "x" },
|
|
253
|
+
{ homeDir: HOME, fs: noFiles },
|
|
254
|
+
)
|
|
255
|
+
).ready,
|
|
256
|
+
).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
|
|
239
259
|
test("strict: MODEL_OVERRIDE=google/... accepts GEMINI_API_KEY", async () => {
|
|
240
260
|
const env = {
|
|
241
261
|
MODEL_OVERRIDE: "google/gemini-3-flash-preview",
|
|
@@ -1078,6 +1098,24 @@ describe("isCredCheckDisabled", () => {
|
|
|
1078
1098
|
describe("buildCredStatusReport", () => {
|
|
1079
1099
|
const HOME = "/home/worker";
|
|
1080
1100
|
|
|
1101
|
+
test("pi reports the opencode-go ids its pi-ai resolves", async () => {
|
|
1102
|
+
const snap = await buildCredStatusReport(
|
|
1103
|
+
"pi",
|
|
1104
|
+
{ HOME },
|
|
1105
|
+
{ homeDir: HOME, fs: fsWith(new Set([`${HOME}/.pi/agent/auth.json`])) },
|
|
1106
|
+
"boot",
|
|
1107
|
+
);
|
|
1108
|
+
expect(snap.opencodeGo?.models).toContain("deepseek-v4.1-flash");
|
|
1109
|
+
expect(snap.opencodeGo?.models).not.toContain("deepseek-v4-flash");
|
|
1110
|
+
const other = await buildCredStatusReport(
|
|
1111
|
+
"opencode",
|
|
1112
|
+
{ HOME },
|
|
1113
|
+
{ homeDir: HOME, fs: fsWith(new Set([`${HOME}/.local/share/opencode/auth.json`])) },
|
|
1114
|
+
"boot",
|
|
1115
|
+
);
|
|
1116
|
+
expect(other.opencodeGo).toBeNull();
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1081
1119
|
for (const [provider, authPath] of [
|
|
1082
1120
|
["pi", ".pi/agent/auth.json"],
|
|
1083
1121
|
["opencode", ".local/share/opencode/auth.json"],
|