@desplega.ai/agent-swarm 2.2.0 → 2.4.0
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 -0
- package/dist/{acp-adapter-k0vwfyvn.js → acp-adapter-kedstxx6.js} +6 -6
- package/dist/{actions-w4pnb8vz.js → actions-vget6r1k.js} +18 -16
- package/dist/{amp-adapter-0cmxb2t5.js → amp-adapter-xeb5mktv.js} +2 -2
- package/dist/{app-zneem7g9.js → app-mqap8t8m.js} +3 -3
- package/dist/{assistant-wbzd2rnr.js → assistant-h0gv97mv.js} +20 -18
- package/dist/{boot-reembed-4kx69j3g.js → boot-reembed-qr8a894z.js} +1 -1
- package/dist/{boot-reembed-db0wwgy1.js → boot-reembed-rj85jjhw.js} +1 -1
- package/dist/{boot-scrub-sweep-tfv4d3mm.js → boot-scrub-sweep-eyfvn6v9.js} +3 -3
- package/dist/{boot-seeds-w2hmkxn5.js → boot-seeds-xs9a8dx1.js} +4 -4
- package/dist/{claude-adapter-5qrc2paq.js → claude-adapter-bm23cye7.js} +5 -5
- package/dist/{claude-managed-adapter-n7ahjyx6.js → claude-managed-adapter-1mwrsm9d.js} +2 -2
- package/dist/{claude-managed-setup-7ra32em4.js → claude-managed-setup-gkfgks4j.js} +1 -1
- package/dist/{claude-sdk-session-cd02gc76.js → claude-sdk-session-apb4ypz3.js} +5 -5
- package/dist/cli-02vhhssk.js +848 -0
- package/dist/{cli-4ync7m60.js → cli-0ebey9hg.js} +1 -1
- package/dist/{cli-665j3feb.js → cli-0xk505e8.js} +1 -1
- package/dist/{cli-8c1m9mhy.js → cli-1cevz46c.js} +5 -5
- package/dist/{cli-gzvb3d4a.js → cli-1jx0srjt.js} +3 -3
- package/dist/{cli-zj4d6afs.js → cli-1pa3rz69.js} +21 -6
- package/dist/{cli-vmbakbq0.js → cli-2rwet3r7.js} +1 -1
- package/dist/{cli-azs9bhc5.js → cli-3wjs22x3.js} +2 -2
- package/dist/{cli-tba8fxf9.js → cli-4fhyq711.js} +45 -36
- package/dist/{cli-ak4c12mh.js → cli-5ff2sjyt.js} +1 -1
- package/dist/{cli-6q7pyd3k.js → cli-5s5hx57y.js} +5 -3
- package/dist/{cli-h23mr2s6.js → cli-6ftrrwm1.js} +9 -371
- package/dist/{cli-9ac0mkg5.js → cli-8b9wvjps.js} +1 -1
- package/dist/cli-8cxkw1f5.js +382 -0
- package/dist/{cli-xhgtay7w.js → cli-8rwvksbc.js} +2 -2
- package/dist/{cli-e09hj50q.js → cli-8wkb09q5.js} +11 -11
- package/dist/{cli-ksrphveb.js → cli-a1esmmee.js} +14 -2
- package/dist/{cli-xnp2vkx7.js → cli-avmqhatn.js} +7 -7
- package/dist/{cli-xx13gqf3.js → cli-b80a1100.js} +1 -1
- package/dist/{cli-z1hh99j0.js → cli-bfc3k1x5.js} +3 -3
- package/dist/{cli-0k9rcwfy.js → cli-bhwa8rzc.js} +33 -15
- package/dist/{cli-8cvvytad.js → cli-bn26xbcs.js} +2 -2
- package/dist/{cli-k28p6xh7.js → cli-c8fwpgqb.js} +1 -1
- package/dist/{cli-rc3r0bnh.js → cli-ccn0eybf.js} +1 -1
- package/dist/{cli-9td5ss65.js → cli-dsgj3e7a.js} +2 -2
- package/dist/{cli-jrk8mj0t.js → cli-ecshf504.js} +2 -2
- package/dist/{cli-raejy0mp.js → cli-eqgfs202.js} +1 -1
- package/dist/{cli-ppapy34w.js → cli-f3nx1h8z.js} +2 -2
- package/dist/{cli-c9vrwcpb.js → cli-ffbrkh1r.js} +74 -35
- package/dist/{cli-mnry16kh.js → cli-kk2r80n5.js} +4 -4
- package/dist/{cli-c0dpzwnx.js → cli-kz116kvw.js} +3 -3
- package/dist/{cli-e5y285rg.js → cli-mvg2hf8p.js} +4 -4
- package/dist/{cli-2jm992rm.js → cli-mz1ezzzh.js} +2 -2
- package/dist/{cli-dw0njd9q.js → cli-nme7wery.js} +6 -6
- package/dist/{cli-mvsfbbgq.js → cli-nrrg0fvy.js} +18 -4
- package/dist/{cli-8jtrekpy.js → cli-p1m2b6gc.js} +7 -7
- package/dist/{cli-4c7k75rq.js → cli-rg7ntypm.js} +378 -84
- package/dist/{cli-z3dk189k.js → cli-rsd7ga7v.js} +10 -8
- package/dist/{cli-etvn5ej2.js → cli-sppc0cmw.js} +1 -1
- package/dist/{cli-8w65vwee.js → cli-tg9ksbn8.js} +1 -1
- package/dist/{cli-th8krqbf.js → cli-tgvpmnwz.js} +2 -2
- package/dist/{cli-hwc4a9xm.js → cli-tj4j2ts4.js} +6 -6
- package/dist/{cli-jhhb2v9n.js → cli-ttaz6c7m.js} +2 -2
- package/dist/{cli-w7z94e6c.js → cli-tx00c0h6.js} +4 -2
- package/dist/{cli-4g629sgr.js → cli-wrv09v7q.js} +20 -1
- package/dist/{cli-cby329ny.js → cli-wyrr38xm.js} +2 -2
- package/dist/{cli-5ks6x36q.js → cli-x1p5dhq2.js} +3 -3
- package/dist/{cli-bddxamg7.js → cli-x719j3vs.js} +220 -629
- package/dist/{cli-ztcxv5nv.js → cli-xfe63zx3.js} +1 -1
- package/dist/{cli-q6gw45wj.js → cli-ywyrffff.js} +1 -1
- package/dist/{cli-p46xdr1m.js → cli-zvze9rbm.js} +1 -1
- package/dist/cli.js +15 -15
- package/dist/{codex-adapter-m0fx3v3s.js → codex-adapter-pp2gq8gr.js} +3 -3
- package/dist/{codex-hook-x6tm6y5m.js → codex-hook-qwzccd64.js} +1 -1
- package/dist/{codex-session-runner-j9m2gfyy.js → codex-session-runner-0vzn09t8.js} +3 -3
- package/dist/{commands-ktnqwrv4.js → commands-psqhkttb.js} +2 -2
- package/dist/{controller-nex2he8v.js → controller-t20vz2k8.js} +2 -2
- package/dist/{controllers-connect-6yhwqv9v.js → controllers-connect-gkxmqw9d.js} +24 -20
- package/dist/{cursor-adapter-02s5ekva.js → cursor-adapter-3v542qe5.js} +1 -1
- package/dist/{db-jvr86v7f.js → db-2xbyenjr.js} +2 -2
- package/dist/{db-19tw9r5s.js → db-nc1rd869.js} +3 -2
- package/dist/{dispatcher-d3mnsg2y.js → dispatcher-ewceb8ts.js} +17 -15
- package/dist/{dsh-adapter-63rvej88.js → dsh-adapter-75xwh78t.js} +1 -1
- package/dist/{easter-egg-3d-6hg2r942.js → easter-egg-3d-xsc47jwf.js} +1 -1
- package/dist/{execute-bcqc69a5.js → execute-gzmw1mnw.js} +1 -1
- package/dist/{external-channel-44cwer32.js → external-channel-a5dsmxn4.js} +2 -2
- package/dist/{google-generative-ai-phpmwxvg.js → google-generative-ai-bmq8gkf3.js} +1 -1
- package/dist/{google-vertex-es9ybgdy.js → google-vertex-b1rvp52s.js} +1 -1
- package/dist/{grok-adapter-ysp907f6.js → grok-adapter-vj88xr23.js} +7 -7
- package/dist/handlers-zbs90bny.js +39 -0
- package/dist/{heartbeat-2etacsqq.js → heartbeat-t2m9r3wa.js} +20 -18
- package/dist/{hook-mzxxs5n2.js → hook-8femx1bg.js} +4 -4
- package/dist/{hook-3kvmax6d.js → hook-xa677f8r.js} +5 -5
- package/dist/{http-fn978m0g.js → http-xpmmq028.js} +940 -269
- package/dist/{index-e4tsa1h7.js → index-8pwc1jc8.js} +17 -15
- package/dist/{index-w2wr89xd.js → index-cdzyfrsp.js} +1 -1
- package/dist/{index-1r8hw4vd.js → index-dg2tmj99.js} +17 -15
- package/dist/index-nxa6me0q.js +41 -0
- package/dist/{index-039x2sag.js → index-s7ckdajq.js} +42 -14
- package/dist/{index-wgsw9xcz.js → index-syztqdt6.js} +1 -1
- package/dist/{index-x0q2a4f9.js → index-ysd45kgc.js} +1 -1
- package/dist/{index-g909y089.js → index-zgvqzjsw.js} +8 -8
- package/dist/{keepalive-d4nzs76x.js → keepalive-3nr4st3r.js} +3 -3
- package/dist/{lead-8h5n9k5f.js → lead-td8h2sfn.js} +33 -31
- package/dist/{maintenance-ea6jfw42.js → maintenance-verwpe2c.js} +1 -1
- package/dist/{oauth-refresh-sweep-3f25djq3.js → oauth-refresh-sweep-9q2t0y3q.js} +2 -2
- package/dist/{onboard-pvk7hdb2.js → onboard-aytqedmq.js} +2 -2
- package/dist/{opencode-adapter-eeesgfqk.js → opencode-adapter-f4hryjny.js} +4 -4
- package/dist/{otel-impl-qb7nhgdp.js → otel-impl-hckw4wt1.js} +1 -1
- package/dist/{pi-mono-adapter-zasp3ze3.js → pi-mono-adapter-hxby0ce1.js} +6 -6
- package/dist/{pricing-refresh-6ehwkcmp.js → pricing-refresh-5nvme3sm.js} +3 -3
- package/dist/{rbac-roles-km0f9cmm.js → rbac-roles-pd8c26m1.js} +5 -3
- package/dist/{rbac-roles-9648nh00.js → rbac-roles-vbpy21mg.js} +2 -2
- package/dist/{render-v2-xkze1wef.js → render-v2-7nqgvk2q.js} +3 -3
- package/dist/{resume-tq0bbvt5.js → resume-b6djcj3c.js} +6 -6
- package/dist/{runtime-ar8310ba.js → runtime-txrxc9za.js} +1 -1
- package/dist/session-access-fc3f9y12.js +25 -0
- package/dist/{setup-rnrazxvd.js → setup-n39szm6b.js} +1 -1
- package/dist/{virtual-modules-wzk6jf3a.js → virtual-modules-wyp2k6z3.js} +2 -2
- package/dist/{work-objects-e75q2rqs.js → work-objects-jem2fre0.js} +20 -18
- package/dist/{worker-4ww4hjef.js → worker-wr6xgfr2.js} +33 -31
- package/openapi.json +1011 -115
- package/package.json +2 -2
- package/src/apps/action-caller.ts +6 -0
- package/src/apps/script-types.ts +16 -3
- package/src/be/asset-key-audit.ts +4 -13
- package/src/be/db/active-sessions.ts +5 -0
- package/src/be/db/session-visibility.ts +29 -0
- package/src/be/db/tasks/read.ts +61 -3
- package/src/be/db/tasks/write.ts +15 -6
- package/src/be/db.ts +149 -66
- package/src/be/events.ts +11 -0
- package/src/be/migrations/227_inbox_item_state_last_read_at.sql +1 -0
- package/src/be/migrations/230_session_shares.sql +23 -0
- package/src/be/rbac-roles.ts +2 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +22 -4
- package/src/be/session-shares.ts +244 -0
- package/src/be/swarm-config-guard.ts +23 -2
- package/src/commands/runner.ts +11 -0
- package/src/heartbeat/heartbeat.ts +25 -7
- package/src/http/active-sessions.ts +13 -2
- package/src/http/agents.ts +10 -3
- package/src/http/all-routes.ts +1 -0
- package/src/http/api-keys.ts +3 -1
- package/src/http/approval-requests.ts +2 -1
- package/src/http/apps.ts +36 -11
- package/src/http/assets.ts +26 -4
- package/src/http/config.ts +3 -0
- package/src/http/context.ts +2 -0
- package/src/http/core.ts +39 -2
- package/src/http/events.ts +6 -2
- package/src/http/favorite-owner.ts +1 -1
- package/src/http/fs.ts +38 -3
- package/src/http/inbox-state.ts +58 -8
- package/src/http/index.ts +3 -1
- package/src/http/memory.ts +3 -1
- package/src/http/ranking.ts +367 -0
- package/src/http/realtime.ts +26 -21
- package/src/http/route-def.ts +77 -11
- package/src/http/script-runs.ts +2 -1
- package/src/http/scripts.ts +8 -0
- package/src/http/session-access.ts +179 -0
- package/src/http/session-data.ts +10 -2
- package/src/http/sessions.ts +212 -12
- package/src/http/stats.ts +7 -3
- package/src/http/tasks.ts +83 -7
- package/src/http/used-by.ts +11 -3
- package/src/kv-reserved-namespaces.ts +1 -0
- package/src/prompts/session-templates.ts +3 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/ranking/telemetry-ranking.ts +147 -0
- package/src/ranking/usage.ts +57 -0
- package/src/rbac/admission.ts +22 -7
- package/src/rbac/legacy-policy.ts +2 -0
- package/src/rbac/permissions.ts +9 -0
- package/src/realtime/auth.ts +69 -0
- package/src/realtime/tickets.ts +4 -1
- package/src/realtime/transport.ts +77 -9
- package/src/slack/blocks.ts +18 -0
- package/src/slack/dm.ts +29 -0
- package/src/telemetry-contract/EVENTS.md +3 -0
- package/src/telemetry-contract/SOURCE.json +37 -4
- package/src/telemetry-contract/catalog.json +69 -0
- package/src/telemetry-contract/ranking/fixtures/expected.json +30 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/bad-category.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/bad-claim.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/blank-description.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/blank-name.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/description-too-long.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/domain-uppercase.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-scheme.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-www.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/http-source-url.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/missing-description.json +10 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/missing-version.json +10 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/name-not-string.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/name-too-long.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/not-object.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/null.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/other-with-subcategory.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/oversize.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/short-claim.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/subcategory-other-category.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/trailing-data.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/unknown-field.json +12 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/unsupported-version.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-not-date.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-offset.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/wrong-case-field.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/category-without-subcategory.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/full.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/minimal.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/padded-text.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/unicode-name.json +11 -0
- package/src/telemetry-contract/ranking/listing.v1.schema.json +61 -0
- package/src/telemetry-contract/ranking/taxonomy.json +8 -0
- package/src/telemetry-contract/ranking-taxonomy.gen.ts +11 -0
- package/src/telemetry-contract/types.gen.ts +708 -1
- package/src/telemetry.ts +26 -2
- package/src/tests/apps-rbac.test.ts +189 -2
- package/src/tests/apps-script-types.test.ts +16 -3
- package/src/tests/asset-key-api.test.ts +128 -0
- package/src/tests/asset-key-invariants.test.ts +179 -3
- package/src/tests/asset-key-migration.test.ts +35 -0
- package/src/tests/check-pr-body.test.ts +23 -0
- package/src/tests/check-ranking-entry.test.ts +146 -0
- package/src/tests/db-extraction-contract.test.ts +1 -0
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/heartbeat-checklist.test.ts +88 -0
- package/src/tests/heartbeat-jev-gate.test.ts +394 -10
- package/src/tests/inbox-state.test.ts +167 -0
- package/src/tests/kv-tool.test.ts +15 -0
- package/src/tests/org-identity.test.ts +44 -0
- package/src/tests/ranking-listing.test.ts +112 -0
- package/src/tests/ranking-route.test.ts +319 -0
- package/src/tests/ranking-usage.test.ts +50 -0
- package/src/tests/rbac-admission-e2e.test.ts +8 -0
- package/src/tests/rbac-admission.test.ts +38 -0
- package/src/tests/rbac-charact-http.test.ts +4 -3
- package/src/tests/rbac-engine.test.ts +2 -0
- package/src/tests/rbac-roles.test.ts +2 -0
- package/src/tests/realtime-room-surfaces.test.ts +3 -1
- package/src/tests/realtime-session-join-race.test.ts +142 -0
- package/src/tests/realtime-tickets.test.ts +24 -17
- package/src/tests/realtime-transport.test.ts +91 -0
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/session-route-posture.test.ts +639 -0
- package/src/tests/session-sharing-routes.test.ts +282 -0
- package/src/tests/session-sharing.test.ts +526 -0
- package/src/tests/sessions-detail-batch.test.ts +7 -1
- package/src/tests/slack-tool-blocks.test.ts +101 -0
- package/src/tests/slack-update.test.ts +55 -0
- package/src/tests/slack-user-dm.test.ts +177 -0
- package/src/tests/used-by-route.test.ts +10 -0
- package/src/tools/slack-post.ts +18 -12
- package/src/tools/slack-read.ts +13 -1
- package/src/tools/slack-reply.ts +7 -10
- package/src/tools/slack-start-thread.ts +18 -11
- package/src/tools/slack-update.ts +2 -9
- package/src/types.ts +37 -0
- package/src/utils/org-identity.ts +33 -0
- package/templates/skills/apps/SKILL.md +17 -0
- package/templates/skills/apps/content.md +17 -0
- package/templates/skills/code-quality/content.md +2 -2
- package/templates/skills/pages/content.md +4 -0
- package/templates/skills/swarm-scripts/SKILL.md +6 -0
- package/templates/skills/swarm-scripts/content.md +6 -0
- package/dist/handlers-t5pbabwf.js +0 -37
- package/dist/index-qyjbb7br.js +0 -39
- package/dist/{cli-n5mrsg0f.js → cli-60cgh1rv.js} +3 -3
- /package/dist/{cli-j0v75y13.js → cli-y83zk5sb.js} +0 -0
package/src/telemetry.ts
CHANGED
|
@@ -630,6 +630,7 @@ function emit(
|
|
|
630
630
|
metadata?: Record<string, unknown>,
|
|
631
631
|
actor?: TelemetryActor,
|
|
632
632
|
emitter: string = source,
|
|
633
|
+
identity?: { orgName: string; orgWebsite: string },
|
|
633
634
|
): void {
|
|
634
635
|
if (!isTelemetryEnabled() || !installationId) return;
|
|
635
636
|
maybeRefreshOrgIdentity();
|
|
@@ -639,7 +640,9 @@ function emit(
|
|
|
639
640
|
const occurredAt = new Date().toISOString();
|
|
640
641
|
const sentFor = installationId;
|
|
641
642
|
const isCloud = cachedIsCloud || isCloudDeployment();
|
|
642
|
-
const orgIdentity =
|
|
643
|
+
const orgIdentity = identity
|
|
644
|
+
? { ...getOrgIdentity(), organization_name: identity.orgName }
|
|
645
|
+
: getOrgIdentity();
|
|
643
646
|
const userId = actor?.userId?.trim() || undefined;
|
|
644
647
|
const payload = {
|
|
645
648
|
schema_version: 2 as const,
|
|
@@ -653,7 +656,7 @@ function emit(
|
|
|
653
656
|
orgId,
|
|
654
657
|
orgName: orgIdentity.organization_name,
|
|
655
658
|
orgDomain: cachedOrgDomain,
|
|
656
|
-
orgWebsite: normalizeOrgWebsite(process.env.SWARM_ORG_WEBSITE),
|
|
659
|
+
orgWebsite: identity?.orgWebsite ?? normalizeOrgWebsite(process.env.SWARM_ORG_WEBSITE),
|
|
657
660
|
installationId,
|
|
658
661
|
actor: userId ? { userId, role: actor?.role ?? roleResolver?.(userId) ?? null } : undefined,
|
|
659
662
|
isCloud,
|
|
@@ -820,6 +823,27 @@ export const telemetry = {
|
|
|
820
823
|
);
|
|
821
824
|
},
|
|
822
825
|
|
|
826
|
+
/**
|
|
827
|
+
* Public-ranking events. The API passes the org identity it just read from
|
|
828
|
+
* config, so a listing built right after an identity edit still matches the
|
|
829
|
+
* envelope's `org_name` / `org_website` (the proxy rejects a mismatch) before
|
|
830
|
+
* the debounced config reload updates `process.env`.
|
|
831
|
+
*/
|
|
832
|
+
ranking<S extends EventSuffix<"ranking">>(
|
|
833
|
+
event: S,
|
|
834
|
+
props: FamilyProps<"ranking", S>,
|
|
835
|
+
identity?: { orgName: string; orgWebsite: string },
|
|
836
|
+
): void {
|
|
837
|
+
emit(
|
|
838
|
+
`ranking.${event}`,
|
|
839
|
+
props as Record<string, unknown>,
|
|
840
|
+
undefined,
|
|
841
|
+
undefined,
|
|
842
|
+
source,
|
|
843
|
+
identity,
|
|
844
|
+
);
|
|
845
|
+
},
|
|
846
|
+
|
|
823
847
|
org<S extends EventSuffix<"org">>(event: S, props: FamilyProps<"org", S>): void {
|
|
824
848
|
emit(`org.${event}`, props as Record<string, unknown>, undefined);
|
|
825
849
|
},
|
|
@@ -7,9 +7,20 @@ import {
|
|
|
7
7
|
type ServerResponse,
|
|
8
8
|
} from "node:http";
|
|
9
9
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
-
import {
|
|
10
|
+
import type { AppActionCaller } from "../apps/action-caller";
|
|
11
|
+
import {
|
|
12
|
+
closeDb,
|
|
13
|
+
createAgent,
|
|
14
|
+
createPage,
|
|
15
|
+
createUser,
|
|
16
|
+
getDbClient,
|
|
17
|
+
getTaskById,
|
|
18
|
+
initDb,
|
|
19
|
+
} from "../be/db";
|
|
20
|
+
import { createRuntimeController } from "../be/runtime-pools/db";
|
|
21
|
+
import { mintControllerToken } from "../be/runtime-pools/tokens";
|
|
11
22
|
import { upsertScriptByName } from "../be/scripts/db";
|
|
12
|
-
import { type IdentityActor, mintToken } from "../be/users";
|
|
23
|
+
import { type IdentityActor, mintRuntimeToken, mintToken } from "../be/users";
|
|
13
24
|
import { handleApps } from "../http/apps";
|
|
14
25
|
import { resolveHttpRequestAuth } from "../http/auth";
|
|
15
26
|
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
@@ -19,13 +30,18 @@ import { registerAppDiffTool } from "../tools/app-diff";
|
|
|
19
30
|
import { registerAppGetTool, registerAppQueryTool } from "../tools/app-get";
|
|
20
31
|
import { registerAppHistoryTool } from "../tools/app-history";
|
|
21
32
|
import { registerAppPatchTool } from "../tools/app-patch";
|
|
33
|
+
import { signPageSession } from "../utils/page-session";
|
|
22
34
|
import { setRequestAuth } from "../utils/request-auth-context";
|
|
35
|
+
import { SKIP_SANDBOX_SPAWN_TESTS } from "./sandbox-spawn-test-helpers";
|
|
36
|
+
import { CHILD_PROCESS_TEST_BUDGET_MS } from "./test-proc";
|
|
23
37
|
|
|
24
38
|
const TEST_DB_PATH = `/private/tmp/test-apps-rbac-${process.pid}.sqlite`;
|
|
25
39
|
const API_KEY = "example-apps-rbac-test-key";
|
|
26
40
|
const AGENT_ID = crypto.randomUUID();
|
|
27
41
|
const LEAD_ID = crypto.randomUUID();
|
|
28
42
|
const OPERATOR_ACTOR: IdentityActor = { kind: "operator", id: "apps-rbac-test" };
|
|
43
|
+
const previousPageSecret = process.env.PAGE_SESSION_SECRET;
|
|
44
|
+
const previousSwarmKey = process.env.AGENT_SWARM_API_KEY;
|
|
29
45
|
|
|
30
46
|
const definition = {
|
|
31
47
|
models: { note: { columns: { title: { kind: "string" } } } },
|
|
@@ -45,6 +61,7 @@ let server: Server;
|
|
|
45
61
|
let base = "";
|
|
46
62
|
let userId = "";
|
|
47
63
|
let userToken = "";
|
|
64
|
+
let agentToken = "";
|
|
48
65
|
let appId = "";
|
|
49
66
|
let tools: Record<string, RegisteredTool>;
|
|
50
67
|
|
|
@@ -144,12 +161,15 @@ async function callTool(name: string, args: unknown): Promise<unknown> {
|
|
|
144
161
|
}
|
|
145
162
|
|
|
146
163
|
beforeAll(async () => {
|
|
164
|
+
process.env.PAGE_SESSION_SECRET = "example-apps-rbac-page-session-secret";
|
|
165
|
+
process.env.AGENT_SWARM_API_KEY = API_KEY;
|
|
147
166
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
148
167
|
await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => undefined);
|
|
149
168
|
}
|
|
150
169
|
initDb(TEST_DB_PATH);
|
|
151
170
|
await createAgent({ id: AGENT_ID, name: "apps-rbac-worker", isLead: false, status: "idle" });
|
|
152
171
|
await createAgent({ id: LEAD_ID, name: "apps-rbac-lead", isLead: true, status: "idle" });
|
|
172
|
+
agentToken = (await mintRuntimeToken(AGENT_ID, crypto.randomUUID())).plaintext;
|
|
153
173
|
const user = await createUser({ name: "Apps RBAC User" });
|
|
154
174
|
userId = user.id;
|
|
155
175
|
userToken = (await mintToken(user.id, "apps-rbac", OPERATOR_ACTOR)).plaintext;
|
|
@@ -177,6 +197,10 @@ afterEach(() => {
|
|
|
177
197
|
afterAll(async () => {
|
|
178
198
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
179
199
|
closeDb();
|
|
200
|
+
if (previousPageSecret === undefined) delete process.env.PAGE_SESSION_SECRET;
|
|
201
|
+
else process.env.PAGE_SESSION_SECRET = previousPageSecret;
|
|
202
|
+
if (previousSwarmKey === undefined) delete process.env.AGENT_SWARM_API_KEY;
|
|
203
|
+
else process.env.AGENT_SWARM_API_KEY = previousSwarmKey;
|
|
180
204
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
181
205
|
await unlink(`${TEST_DB_PATH}${suffix}`).catch(() => undefined);
|
|
182
206
|
}
|
|
@@ -323,6 +347,9 @@ describe("app viewer identity", () => {
|
|
|
323
347
|
});
|
|
324
348
|
expect(action.status).toBe(200);
|
|
325
349
|
expect((await getTaskById(action.body.taskId))?.requestedByUserId).toBe(userId);
|
|
350
|
+
expect((await getTaskById(action.body.taskId))?.task).toContain(
|
|
351
|
+
`Caller (server-verified): ${JSON.stringify({ kind: "user", id: userId, name: "Apps RBAC User" })}`,
|
|
352
|
+
);
|
|
326
353
|
|
|
327
354
|
const operatorAction = await request<{ taskId: string }>(
|
|
328
355
|
`/api/apps/${appId}/actions/triage`,
|
|
@@ -342,6 +369,166 @@ describe("app viewer identity", () => {
|
|
|
342
369
|
});
|
|
343
370
|
});
|
|
344
371
|
|
|
372
|
+
describe("verified app action caller", () => {
|
|
373
|
+
beforeEach(async () => {
|
|
374
|
+
const saved = await upsertScriptByName({
|
|
375
|
+
name: `apps_caller_${crypto.randomUUID().replaceAll("-", "")}`,
|
|
376
|
+
scope: "agent",
|
|
377
|
+
scopeId: AGENT_ID,
|
|
378
|
+
agentId: AGENT_ID,
|
|
379
|
+
source: "export default function run(args: Record<string, unknown>) { return args; }",
|
|
380
|
+
description: "Action caller fixture",
|
|
381
|
+
intent: "Return the arguments received by a script action",
|
|
382
|
+
signatureJson: JSON.stringify({ args: { type: "object" }, result: { type: "object" } }),
|
|
383
|
+
typeChecked: true,
|
|
384
|
+
embeddingMode: "skip",
|
|
385
|
+
});
|
|
386
|
+
const patched = await request(`/api/apps/${appId}`, "operator", {
|
|
387
|
+
method: "PATCH",
|
|
388
|
+
body: JSON.stringify({
|
|
389
|
+
definition: {
|
|
390
|
+
actions: {
|
|
391
|
+
echo: {
|
|
392
|
+
kind: "script",
|
|
393
|
+
scriptId: saved.script.id,
|
|
394
|
+
args: { caller: { kind: "guest", id: "default-forgery" }, base: 2 },
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
}),
|
|
399
|
+
});
|
|
400
|
+
expect(patched.status).toBe(200);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const scriptTest = test.skipIf(SKIP_SANDBOX_SPAWN_TESTS);
|
|
404
|
+
scriptTest.each([
|
|
405
|
+
["user", undefined],
|
|
406
|
+
["operator", AGENT_ID],
|
|
407
|
+
["operator", crypto.randomUUID()],
|
|
408
|
+
["operator", undefined],
|
|
409
|
+
["agent", AGENT_ID],
|
|
410
|
+
] as const)(
|
|
411
|
+
"%s caller with agent header %s preserves identity and overwrites forged input",
|
|
412
|
+
async (principal, agentId) => {
|
|
413
|
+
const caller: AppActionCaller =
|
|
414
|
+
principal === "user"
|
|
415
|
+
? { kind: "user", id: userId, name: "Apps RBAC User" }
|
|
416
|
+
: agentId === AGENT_ID
|
|
417
|
+
? { kind: "agent", id: AGENT_ID, name: "apps-rbac-worker" }
|
|
418
|
+
: { kind: "operator", id: null };
|
|
419
|
+
const requestHeaders = {
|
|
420
|
+
...(agentId ? { "X-Agent-ID": agentId } : {}),
|
|
421
|
+
...(principal === "agent" ? { Authorization: `Bearer ${agentToken}` } : {}),
|
|
422
|
+
};
|
|
423
|
+
const checks: RbacCheck[] = [];
|
|
424
|
+
setAuditSink((check) => checks.push(check));
|
|
425
|
+
const row = await request<{ row: { createdBy: string } }>(
|
|
426
|
+
`/api/apps/${appId}/models/note/rows`,
|
|
427
|
+
principal,
|
|
428
|
+
{
|
|
429
|
+
method: "POST",
|
|
430
|
+
headers: requestHeaders,
|
|
431
|
+
body: JSON.stringify({ values: { title: "caller row" } }),
|
|
432
|
+
},
|
|
433
|
+
);
|
|
434
|
+
expect(row.status).toBe(201);
|
|
435
|
+
expect(row.body.row.createdBy).toBe(caller.id ? `${caller.kind}:${caller.id}` : "operator");
|
|
436
|
+
|
|
437
|
+
const action = await request<{
|
|
438
|
+
ok: boolean;
|
|
439
|
+
result: { caller: AppActionCaller; app: { id: string }; base: number; add: number };
|
|
440
|
+
}>(`/api/apps/${appId}/actions/echo`, principal, {
|
|
441
|
+
method: "POST",
|
|
442
|
+
headers: requestHeaders,
|
|
443
|
+
body: JSON.stringify({
|
|
444
|
+
input: { caller: { kind: "operator", id: "input-forgery" }, app: { id: "fake" }, add: 3 },
|
|
445
|
+
}),
|
|
446
|
+
});
|
|
447
|
+
expect(action.status).toBe(200);
|
|
448
|
+
expect(action.body.ok).toBe(true);
|
|
449
|
+
expect(action.body.result).toEqual({ caller, app: { id: appId }, base: 2, add: 3 });
|
|
450
|
+
|
|
451
|
+
const taskAction = await request<{ taskId: string }>(
|
|
452
|
+
`/api/apps/${appId}/actions/triage`,
|
|
453
|
+
principal,
|
|
454
|
+
{ method: "POST", headers: requestHeaders, body: JSON.stringify({ input: {} }) },
|
|
455
|
+
);
|
|
456
|
+
expect(taskAction.status).toBe(200);
|
|
457
|
+
expect((await getTaskById(taskAction.body.taskId))?.task).toContain(
|
|
458
|
+
`Caller (server-verified): ${JSON.stringify(caller)}`,
|
|
459
|
+
);
|
|
460
|
+
expect(checks).toHaveLength(3);
|
|
461
|
+
expect(checks.every((check) => check.principal.kind === principal)).toBe(true);
|
|
462
|
+
},
|
|
463
|
+
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
scriptTest(
|
|
467
|
+
"a signed page session includes the verified page id and viewer identity",
|
|
468
|
+
async () => {
|
|
469
|
+
const page = await createPage({
|
|
470
|
+
agentId: AGENT_ID,
|
|
471
|
+
slug: `caller-${crypto.randomUUID()}`,
|
|
472
|
+
title: "Caller page",
|
|
473
|
+
contentType: "text/html",
|
|
474
|
+
body: "<p>Caller page</p>",
|
|
475
|
+
});
|
|
476
|
+
const session = await signPageSession({
|
|
477
|
+
pageId: page.id,
|
|
478
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
479
|
+
uid: userId,
|
|
480
|
+
});
|
|
481
|
+
const action = await request<{ ok: boolean; result: { caller: AppActionCaller } }>(
|
|
482
|
+
`/api/apps/${appId}/actions/echo`,
|
|
483
|
+
"operator",
|
|
484
|
+
{
|
|
485
|
+
method: "POST",
|
|
486
|
+
headers: {
|
|
487
|
+
"X-Agent-ID": AGENT_ID,
|
|
488
|
+
"X-Page-ID": page.id,
|
|
489
|
+
"X-Page-Session": session,
|
|
490
|
+
},
|
|
491
|
+
body: JSON.stringify({ input: { caller: { pageId: "fake" } } }),
|
|
492
|
+
},
|
|
493
|
+
);
|
|
494
|
+
expect(action.status).toBe(200);
|
|
495
|
+
expect(action.body.ok).toBe(true);
|
|
496
|
+
expect(action.body.result.caller).toEqual({
|
|
497
|
+
kind: "user",
|
|
498
|
+
id: userId,
|
|
499
|
+
name: "Apps RBAC User",
|
|
500
|
+
pageId: page.id,
|
|
501
|
+
});
|
|
502
|
+
},
|
|
503
|
+
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
test("controller tokens use the guest principal even with a registered agent header", async () => {
|
|
507
|
+
const controller = await createRuntimeController({
|
|
508
|
+
name: "apps-caller-controller",
|
|
509
|
+
kind: "docker",
|
|
510
|
+
placement: "remote",
|
|
511
|
+
});
|
|
512
|
+
const token = await mintControllerToken(controller.id);
|
|
513
|
+
const checks: RbacCheck[] = [];
|
|
514
|
+
setAuditSink((check) => checks.push(check));
|
|
515
|
+
const action = await request(`/api/apps/${appId}/actions/echo`, "operator", {
|
|
516
|
+
method: "POST",
|
|
517
|
+
headers: { Authorization: `Bearer ${token.plaintext}`, "X-Agent-ID": AGENT_ID },
|
|
518
|
+
body: JSON.stringify({ input: {} }),
|
|
519
|
+
});
|
|
520
|
+
expect(action.status).toBe(403);
|
|
521
|
+
expect(checks).toEqual([
|
|
522
|
+
{
|
|
523
|
+
principal: { kind: "guest" },
|
|
524
|
+
verb: "app.use",
|
|
525
|
+
resource: { kind: "app", appId },
|
|
526
|
+
source: "http",
|
|
527
|
+
},
|
|
528
|
+
]);
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
345
532
|
describe("user writers and the script ownership gate", () => {
|
|
346
533
|
async function saveScript(scope: "agent" | "global", ownerId: string | null): Promise<string> {
|
|
347
534
|
const result = await upsertScriptByName({
|
|
@@ -127,9 +127,16 @@ describe("renderAppTypes", () => {
|
|
|
127
127
|
expect(rendered).toContain("params: { value: never };");
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
-
test("renders
|
|
130
|
+
test("renders action names and arguments with server-stamped caller context", () => {
|
|
131
131
|
const rendered = renderAppTypes([app("PM Inbox", issueDefinition())]);
|
|
132
132
|
expect(rendered).toContain('export type ActionName = "closeIssue" | "notifyOwner";');
|
|
133
|
+
expect(rendered).toContain("export interface ActionArgs");
|
|
134
|
+
expect(rendered).toContain('app: { id: "PM Inbox-id" };');
|
|
135
|
+
expect(rendered).toContain("caller: AppActionCaller;");
|
|
136
|
+
expect(rendered).toContain('kind: "user" | "agent" | "operator" | "guest";');
|
|
137
|
+
expect(rendered).toContain("id: string | null;");
|
|
138
|
+
expect(rendered).toContain("name?: string;");
|
|
139
|
+
expect(rendered).toContain("pageId?: string;");
|
|
133
140
|
});
|
|
134
141
|
|
|
135
142
|
test("derives and dedupes namespaces and model interface names", () => {
|
|
@@ -262,14 +269,20 @@ describe("renderAppTypes", () => {
|
|
|
262
269
|
expect(rendered).not.toMatch(/^export const pwned2/m);
|
|
263
270
|
});
|
|
264
271
|
|
|
265
|
-
test("
|
|
272
|
+
test("model names do not collide with generated action types", () => {
|
|
266
273
|
const rendered = renderAppTypes([
|
|
267
274
|
app("Collision", {
|
|
268
|
-
models: {
|
|
275
|
+
models: {
|
|
276
|
+
actionName: { columns: { title: { kind: "string" } } },
|
|
277
|
+
actionArgs: { columns: { title: { kind: "string" } } },
|
|
278
|
+
appActionCaller: { columns: { title: { kind: "string" } } },
|
|
279
|
+
},
|
|
269
280
|
actions: { close: { kind: "task", prompt: "close" } },
|
|
270
281
|
}),
|
|
271
282
|
]);
|
|
272
283
|
expect(rendered).toContain("export interface ActionName_2");
|
|
284
|
+
expect(rendered).toContain("export interface ActionArgs_2");
|
|
285
|
+
expect(rendered).toContain("export interface AppActionCaller_2");
|
|
273
286
|
expect(rendered).toContain('export type ActionName = "close";');
|
|
274
287
|
});
|
|
275
288
|
|
|
@@ -11,8 +11,11 @@ import {
|
|
|
11
11
|
createAgent,
|
|
12
12
|
createTaskExtended,
|
|
13
13
|
createUser,
|
|
14
|
+
getAssetKeyMappingByProvider,
|
|
14
15
|
getDbClient,
|
|
16
|
+
getTaskById,
|
|
15
17
|
initDb,
|
|
18
|
+
insertTaskAttachment,
|
|
16
19
|
} from "../be/db";
|
|
17
20
|
import { insertScript } from "../be/scripts/db";
|
|
18
21
|
import { handleAssets } from "../http/assets";
|
|
@@ -343,4 +346,129 @@ describe("asset namespace REST contract", () => {
|
|
|
343
346
|
expect(audit.body.structuralValid).toBe(true);
|
|
344
347
|
expect(audit.body.warningCount).toBe(0);
|
|
345
348
|
});
|
|
349
|
+
|
|
350
|
+
test("cascade moves descendants and sourced mappings without stamping task activity", async () => {
|
|
351
|
+
const root = await createTaskExtended("cascade root", { agentId, key: "shared/cascade/" });
|
|
352
|
+
const child = await createTaskExtended("cascade child", { agentId, parentTaskId: root.id });
|
|
353
|
+
const leaf = await createTaskExtended("cascade leaf", { agentId, parentTaskId: child.id });
|
|
354
|
+
const unchanged = await createTaskExtended("already moved", {
|
|
355
|
+
agentId,
|
|
356
|
+
parentTaskId: root.id,
|
|
357
|
+
key: "shared/cascade-target/",
|
|
358
|
+
});
|
|
359
|
+
const provider = { providerId: "agent-fs", providerKey: "cascade-file.md" };
|
|
360
|
+
await insertTaskAttachment({
|
|
361
|
+
taskId: leaf.id,
|
|
362
|
+
name: "cascade-file.md",
|
|
363
|
+
kind: "agent-fs",
|
|
364
|
+
...provider,
|
|
365
|
+
});
|
|
366
|
+
const mapping = (await getAssetKeyMappingByProvider(provider))!;
|
|
367
|
+
const lastUpdatedAt = "2026-01-01T00:00:00.000Z";
|
|
368
|
+
for (const task of [root, child, leaf, unchanged]) {
|
|
369
|
+
await getDbClient().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
|
|
370
|
+
lastUpdatedAt,
|
|
371
|
+
task.id,
|
|
372
|
+
]);
|
|
373
|
+
}
|
|
374
|
+
const move = await api(
|
|
375
|
+
"PATCH",
|
|
376
|
+
`/api/assets/task/${root.id}/key`,
|
|
377
|
+
{
|
|
378
|
+
key: "shared/cascade-target/",
|
|
379
|
+
cascade: true,
|
|
380
|
+
},
|
|
381
|
+
{ agentId },
|
|
382
|
+
);
|
|
383
|
+
expect(move.status).toBe(200);
|
|
384
|
+
expect(move.body.movedTaskIds).toEqual([root.id, child.id, leaf.id]);
|
|
385
|
+
for (const task of [root, child, leaf, unchanged]) {
|
|
386
|
+
const moved = (await getTaskById(task.id))!;
|
|
387
|
+
expect(moved.key).toBe("shared/cascade-target/");
|
|
388
|
+
expect(moved.lastUpdatedAt).toBe(lastUpdatedAt);
|
|
389
|
+
}
|
|
390
|
+
expect((await getAssetKeyMappingByProvider(provider))?.key).toBe("shared/cascade-target/");
|
|
391
|
+
const history = await getDbClient().query<{ entity_id: string }>(
|
|
392
|
+
"SELECT entity_id FROM asset_key_history WHERE entity_id IN (?, ?, ?, ?, ?) AND new_key = ?",
|
|
393
|
+
[root.id, child.id, leaf.id, unchanged.id, mapping.id, "shared/cascade-target/"],
|
|
394
|
+
);
|
|
395
|
+
expect(history.map((row) => row.entity_id).sort()).toEqual(
|
|
396
|
+
[root.id, child.id, leaf.id, mapping.id].sort(),
|
|
397
|
+
);
|
|
398
|
+
expect(
|
|
399
|
+
(
|
|
400
|
+
await api(
|
|
401
|
+
"PATCH",
|
|
402
|
+
`/api/assets/task/${root.id}/key`,
|
|
403
|
+
{
|
|
404
|
+
key: "shared/cascade-target/",
|
|
405
|
+
cascade: true,
|
|
406
|
+
},
|
|
407
|
+
{ agentId },
|
|
408
|
+
)
|
|
409
|
+
).body.movedTaskIds,
|
|
410
|
+
).toEqual([]);
|
|
411
|
+
|
|
412
|
+
// The root can already have the destination key while descendants still need a move.
|
|
413
|
+
await getDbClient().run('UPDATE agent_tasks SET "key" = ? WHERE id = ?', [
|
|
414
|
+
"shared/other-child/",
|
|
415
|
+
child.id,
|
|
416
|
+
]);
|
|
417
|
+
expect(
|
|
418
|
+
(
|
|
419
|
+
await api(
|
|
420
|
+
"PATCH",
|
|
421
|
+
`/api/assets/task/${root.id}/key`,
|
|
422
|
+
{
|
|
423
|
+
key: "shared/cascade-target/",
|
|
424
|
+
cascade: true,
|
|
425
|
+
},
|
|
426
|
+
{ agentId },
|
|
427
|
+
)
|
|
428
|
+
).body.movedTaskIds,
|
|
429
|
+
).toEqual([child.id]);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("cascade denies the whole move when a descendant is not authorized", async () => {
|
|
433
|
+
const root = await createTaskExtended("allowed root", { agentId });
|
|
434
|
+
const child = await createTaskExtended("denied child", {
|
|
435
|
+
agentId: secondAgentId,
|
|
436
|
+
creatorAgentId: secondAgentId,
|
|
437
|
+
parentTaskId: root.id,
|
|
438
|
+
});
|
|
439
|
+
const move = await api(
|
|
440
|
+
"PATCH",
|
|
441
|
+
`/api/assets/task/${root.id}/key`,
|
|
442
|
+
{
|
|
443
|
+
key: "shared/must-not-move/",
|
|
444
|
+
cascade: true,
|
|
445
|
+
},
|
|
446
|
+
{ agentId },
|
|
447
|
+
);
|
|
448
|
+
expect(move.status).toBe(403);
|
|
449
|
+
expect((await getTaskById(root.id))?.key).toBe(root.key);
|
|
450
|
+
expect((await getTaskById(child.id))?.key).toBe(child.key);
|
|
451
|
+
expect(
|
|
452
|
+
await getDbClient().query("SELECT id FROM asset_key_history WHERE entity_id IN (?, ?)", [
|
|
453
|
+
root.id,
|
|
454
|
+
child.id,
|
|
455
|
+
]),
|
|
456
|
+
).toEqual([]);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("cascade is rejected for non-task entities", async () => {
|
|
460
|
+
for (const entityType of ["workflow", "schedule", "page", "file", "app", "script"]) {
|
|
461
|
+
const move = await api(
|
|
462
|
+
"PATCH",
|
|
463
|
+
`/api/assets/${entityType}/${crypto.randomUUID()}/key`,
|
|
464
|
+
{
|
|
465
|
+
key: "shared/invalid-cascade/",
|
|
466
|
+
cascade: true,
|
|
467
|
+
},
|
|
468
|
+
{ operator: true },
|
|
469
|
+
);
|
|
470
|
+
expect(move.status).toBe(400);
|
|
471
|
+
expect(move.body.error).toContain("only supported for task");
|
|
472
|
+
}
|
|
473
|
+
});
|
|
346
474
|
});
|