@desplega.ai/agent-swarm 1.129.0 → 1.131.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 +2 -2
- package/dist/{actions-kfv2dawe.js → actions-w53mbns9.js} +11 -11
- package/dist/{anthropic-messages-3xgme8my.js → anthropic-messages-rjmd2e7n.js} +5 -5
- package/dist/{app-q8vjk6e8.js → app-m9gwqsqv.js} +8 -8
- package/dist/{artifact-hg4t1nx7.js → artifact-jysj3wrg.js} +2 -2
- package/dist/{assistant-g037r345.js → assistant-bp65m6f3.js} +13 -13
- package/dist/{azure-openai-responses-xa4sh9f6.js → azure-openai-responses-c0yjmbfd.js} +8 -5
- package/dist/{boot-reembed-18pzsrxa.js → boot-reembed-62p0at2k.js} +6 -6
- package/dist/{boot-reembed-wpks2cj8.js → boot-reembed-xmfp6wdh.js} +7 -7
- package/dist/{boot-scrub-logs-n7szd57r.js → boot-scrub-logs-rr8q2dms.js} +5 -5
- package/dist/{claude-adapter-qz4r0ajt.js → claude-adapter-2y47mrkd.js} +9 -9
- package/dist/{cli-mhjnbgb6.js → cli-0rcyq6d9.js} +450 -186
- package/dist/{cli-ttkmehy0.js → cli-2q72qz2s.js} +274 -25
- package/dist/{cli-gztgc3hz.js → cli-2wadx7p2.js} +2 -2
- package/dist/cli-3fj60etf.js +153 -0
- package/dist/{cli-7ecgm5v0.js → cli-3kf51j15.js} +1 -1
- package/dist/{cli-vgexpfsr.js → cli-57khth04.js} +6 -3
- package/dist/{cli-2mbe7vk0.js → cli-5dsbcjjw.js} +1 -1
- package/dist/{cli-etr62zy6.js → cli-5kx36evn.js} +2 -2
- package/dist/{cli-v2tr6amf.js → cli-5n6e5yfn.js} +30 -8
- package/dist/{cli-hx02s9zq.js → cli-5v4rw6yd.js} +2 -2
- package/dist/{cli-v37yr6mf.js → cli-5ze9rv97.js} +4 -4
- package/dist/{cli-ch7t5v7n.js → cli-6map995v.js} +9 -4
- package/dist/{cli-h0457a67.js → cli-70xqj0z3.js} +3 -3
- package/dist/{cli-kk4f9r17.js → cli-72q8m8gk.js} +8 -6
- package/dist/{cli-7padk69a.js → cli-78xwvkhj.js} +2 -2
- package/dist/{cli-dxtxacy7.js → cli-9dr5qzc0.js} +45 -8
- package/dist/{cli-17zxdgsq.js → cli-a40rnf71.js} +6 -6
- package/dist/{cli-t53vnnxz.js → cli-a4jzdpa3.js} +2514 -1055
- package/dist/{cli-w10fkrf1.js → cli-akdvwc9v.js} +354 -115
- package/dist/{cli-1qkk5er0.js → cli-bmze986d.js} +1 -1
- package/dist/{cli-ga12dc1d.js → cli-bqw0mt1n.js} +3 -3
- package/dist/{cli-0sf92efp.js → cli-cdeg08wx.js} +1 -1
- package/dist/{cli-a8ynhkyj.js → cli-cvgjwnh1.js} +17 -15
- package/dist/{cli-9fxg3v5m.js → cli-d2tfkcga.js} +4 -4
- package/dist/{cli-nhcd7x43.js → cli-d3x3x1c9.js} +2 -2
- package/dist/{cli-6nh2qxvh.js → cli-darcwax3.js} +1 -1
- package/dist/{cli-yjbgmysx.js → cli-e7ckcg4v.js} +12 -12
- package/dist/{cli-4n2gcjpd.js → cli-efxfnq6q.js} +4 -3
- package/dist/{cli-z9xqqhfk.js → cli-er3a4bb0.js} +1 -1
- package/dist/{cli-xg2eezyv.js → cli-f1am9szv.js} +1 -1
- package/dist/{cli-xy4cwsm9.js → cli-fywms69x.js} +1 -1
- package/dist/{cli-33jj76y8.js → cli-g6b8x1h8.js} +1 -1
- package/dist/{cli-epsvdckx.js → cli-gm9cbhbq.js} +20 -10
- package/dist/{cli-rkndnn90.js → cli-k9qkf1hw.js} +6 -4
- package/dist/{cli-zejvt395.js → cli-kh3ba2zf.js} +1 -1
- package/dist/{cli-gb25p1h2.js → cli-nm5gtp1x.js} +2 -2
- package/dist/{cli-akxnpx17.js → cli-pm6xbh84.js} +1829 -1629
- package/dist/{cli-2k0ztbfp.js → cli-psh3c4va.js} +1 -1
- package/dist/{cli-39vgrsep.js → cli-qv61hp37.js} +2 -2
- package/dist/{cli-hv2fndhm.js → cli-r6kqqbj0.js} +6 -4
- package/dist/{cli-1y1cm570.js → cli-rzbpmxdr.js} +1 -1
- package/dist/{cli-mmrk00vp.js → cli-t1xze5qh.js} +1 -1
- package/dist/{cli-g252a9ky.js → cli-t55yw3a1.js} +2 -2
- package/dist/{cli-7bz8dzqa.js → cli-t76vw5db.js} +1184 -1522
- package/dist/{cli-4n4mdv21.js → cli-xpxde7rm.js} +7 -3
- package/dist/{cli-fpaa5k2q.js → cli-yw8vghx2.js} +54 -18
- package/dist/cli-zsspvvm2.js +490 -0
- package/dist/cli.js +27 -25
- package/dist/{codex-adapter-7v6t7ff6.js → codex-adapter-d24q75wk.js} +7 -7
- package/dist/{codex-hook-amndawyb.js → codex-hook-hq54t9ft.js} +2 -2
- package/dist/{codex-session-runner-e7j4khe2.js → codex-session-runner-g50vcsex.js} +7 -7
- package/dist/{commands-ex0k5tbf.js → commands-c3nrc5hg.js} +5 -5
- package/dist/{db-df7jatfc.js → db-nt7fg3tw.js} +5 -5
- package/dist/{google-generative-ai-y42hnw9p.js → google-generative-ai-hysrd8qx.js} +7 -5
- package/dist/{google-vertex-4sg66gb6.js → google-vertex-p5arrga0.js} +7 -5
- package/dist/{handlers-1j2cfqt2.js → handlers-fkr6swat.js} +13 -13
- package/dist/{hook-nx9a0mj3.js → hook-77wpvnjc.js} +7 -7
- package/dist/{hook-w6yaf6sy.js → hook-g019yehy.js} +8 -8
- package/dist/{http-4tz5h8wg.js → http-jewj0syk.js} +2993 -716
- package/dist/{index-0bpkzb09.js → index-070v4eyb.js} +3 -3
- package/dist/{index-0s67qv7h.js → index-6gm5he8h.js} +11 -11
- package/dist/{index-35gtv6gj.js → index-7n2xpbd4.js} +310 -40
- package/dist/{index-ker3gccb.js → index-9znhar3x.js} +4 -4
- package/dist/{index-5n2c8yay.js → index-hase6zbe.js} +2 -2
- package/dist/{index-kcf3tm9h.js → index-hfvggd0z.js} +12 -12
- package/dist/{index-7c0jqna0.js → index-hp1kdbjj.js} +13 -13
- package/dist/{keepalive-3j4a9168.js → keepalive-j0fjvqfd.js} +7 -7
- package/dist/{lead-6kpevymb.js → lead-1n8x30xe.js} +33 -33
- package/dist/{maintenance-m1k82ean.js → maintenance-gvev11sa.js} +7 -7
- package/dist/{mistral-conversations-bjkdfb6z.js → mistral-conversations-81fwav0j.js} +6 -6
- package/dist/{oauth-refresh-sweep-881k8e4q.js → oauth-refresh-sweep-s9ecsvh0.js} +6 -6
- package/dist/{onboard-c0ykffjn.js → onboard-pvm5b38w.js} +2 -2
- package/dist/{openai-codex-responses-xetseqvt.js → openai-codex-responses-5g27pd6f.js} +43 -23
- package/dist/{openai-completions-5r2g5axa.js → openai-completions-3vkmfz2a.js} +55 -11
- package/dist/{openai-responses-ce67hd23.js → openai-responses-dg4t7j7a.js} +8 -5
- package/dist/{openrouter-images-pzfp22jb.js → openrouter-images-g3gm8931.js} +1 -1
- package/dist/{otel-impl-sqagjerv.js → otel-impl-p6jvgd9c.js} +6 -4
- package/dist/{otel-3mp81ppd.js → otel-nqa12wf3.js} +2 -2
- package/dist/{pi-mono-adapter-k584k16g.js → pi-mono-adapter-t091s27g.js} +17117 -7517
- package/dist/{pricing-refresh-y82whbf9.js → pricing-refresh-k49rcyed.js} +7 -7
- package/dist/{rbac-roles-nvkqmzyd.js → rbac-roles-30pspvy7.js} +7 -7
- package/dist/{rbac-roles-4ht8gegc.js → rbac-roles-cr80g96t.js} +6 -6
- package/dist/{render-v2-wvqdgvvp.js → render-v2-xm457ng1.js} +8 -8
- package/dist/{seed-pricing-fy3rxhde.js → seed-pricing-yhpv35fr.js} +6 -6
- package/dist/{setup-ak5cnc6f.js → setup-nytnmwwr.js} +2 -2
- package/dist/{worker-d8769q8a.js → worker-msytr2cw.js} +33 -33
- package/openapi.json +34039 -11036
- package/package.json +8 -6
- package/src/apps/definition.ts +368 -26
- package/src/apps/row-store.ts +66 -10
- package/src/apps/schema-migrate.ts +95 -1
- package/src/apps/store.ts +13 -4
- package/src/apps/sync-run-as.ts +18 -0
- package/src/apps/sync.ts +934 -0
- package/src/apps/version.ts +16 -2
- package/src/assets/key.ts +8 -1
- package/src/be/asset-key-audit.ts +3 -1
- package/src/be/db-queries/mcp-oauth.ts +297 -24
- package/src/be/db.ts +42 -1
- package/src/be/migrations/129_asset_keys_apps_scripts.sql +161 -0
- package/src/be/scripts/db.ts +5 -2
- package/src/be/scripts/typecheck.ts +1 -0
- package/src/be/seed-scripts/catalog/app-sync-run.ts +32 -0
- package/src/be/seed-scripts/catalog/github-issues-pull.ts +102 -0
- package/src/be/seed-scripts/index.ts +18 -0
- package/src/be/seed-skills/index.ts +7 -0
- package/src/http/active-sessions.ts +26 -18
- package/src/http/agents.ts +94 -27
- package/src/http/api-keys.ts +91 -17
- package/src/http/approval-requests.ts +87 -10
- package/src/http/apps.ts +361 -53
- package/src/http/assets.ts +165 -31
- package/src/http/budgets.ts +7 -5
- package/src/http/codex-oauth-keep-warm.ts +21 -3
- package/src/http/config.ts +46 -16
- package/src/http/context.ts +33 -6
- package/src/http/ecosystem.ts +20 -6
- package/src/http/events.ts +24 -9
- package/src/http/favorites.ts +20 -6
- package/src/http/fs.ts +60 -17
- package/src/http/heartbeat.ts +18 -4
- package/src/http/inbox-state.ts +12 -6
- package/src/http/integrations.ts +25 -5
- package/src/http/kv.ts +36 -15
- package/src/http/mcp-bridge.ts +5 -1
- package/src/http/mcp-oauth.ts +332 -17
- package/src/http/mcp-servers.ts +56 -18
- package/src/http/memory.ts +282 -29
- package/src/http/metrics.ts +80 -17
- package/src/http/oauth-callback.ts +10 -4
- package/src/http/oauth-generic.ts +5 -1
- package/src/http/oauth-locks.ts +6 -3
- package/src/http/openapi.ts +57 -9
- package/src/http/page-proxy.ts +25 -5
- package/src/http/pages-public.ts +37 -20
- package/src/http/pages.ts +93 -28
- package/src/http/poll.ts +89 -5
- package/src/http/pricing.ts +9 -6
- package/src/http/prompt-templates.ts +85 -21
- package/src/http/route-def.ts +67 -4
- package/src/http/schedules.ts +63 -14
- package/src/http/script-connection-proxy.ts +30 -4
- package/src/http/script-connections.ts +393 -43
- package/src/http/script-runs.ts +73 -19
- package/src/http/scripts.ts +257 -39
- package/src/http/session-data.ts +87 -14
- package/src/http/sessions.ts +80 -5
- package/src/http/skills.ts +115 -35
- package/src/http/stats.ts +94 -13
- package/src/http/task-templates.ts +6 -4
- package/src/http/tasks.ts +230 -61
- package/src/http/trackers/jira.ts +90 -30
- package/src/http/trackers/linear.ts +46 -12
- package/src/http/users.ts +162 -25
- package/src/http/utils.ts +12 -2
- package/src/http/webhooks.ts +30 -16
- package/src/http/workflow-events.ts +18 -5
- package/src/http/workflows.ts +128 -39
- package/src/http/x.ts +37 -6
- package/src/oauth/ensure-mcp-token.ts +16 -1
- package/src/oauth/mcp-wrapper.ts +281 -26
- package/src/providers/pi-mono-adapter.ts +1 -1
- package/src/scripts-runtime/credential-broker/README.md +1 -1
- package/src/scripts-runtime/credential-broker/fetch-patch.ts +12 -2
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/stdlib/fetch.ts +6 -0
- package/src/scripts-runtime/types/stdlib.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
- package/src/server.ts +2 -0
- package/src/tests/apps-rbac.test.ts +110 -0
- package/src/tests/apps-spike2.test.ts +4 -1
- package/src/tests/apps-sync-engine.test.ts +2540 -0
- package/src/tests/apps-sync.test.ts +1627 -0
- package/src/tests/asset-key-api.test.ts +48 -2
- package/src/tests/asset-key-invariants.test.ts +92 -0
- package/src/tests/asset-key-migration.test.ts +174 -8
- package/src/tests/asset-key-normalization.test.ts +2 -0
- package/src/tests/credential-broker.test.ts +27 -2
- package/src/tests/mcp-oauth-dcr-reuse.test.ts +743 -0
- package/src/tests/mcp-oauth-ensure-token.test.ts +45 -0
- package/src/tests/mcp-oauth-manual-client.test.ts +135 -2
- package/src/tests/mcp-oauth-queries.test.ts +153 -0
- package/src/tests/mcp-oauth-wrapper.test.ts +432 -0
- package/src/tests/memory-http-recall-gating.test.ts +2 -0
- package/src/tests/openapi-response-contract.test.ts +138 -0
- package/src/tests/rbac-admission-e2e.test.ts +68 -0
- package/src/tests/script-connections.test.ts +3 -0
- package/src/tests/scripts-runtime-fetch.test.ts +121 -0
- package/src/tests/scripts-runtime-secret-egress.test.ts +4 -2
- package/src/tests/seed-scripts.test.ts +2 -2
- package/src/tests/swarm-tool-result-gate.test.ts +32 -0
- package/src/tests/task-working-dir.test.ts +8 -4
- package/src/tests/whoami-route.test.ts +127 -0
- package/src/tests/workflow-engine-v2.test.ts +80 -7
- package/src/tests/workflow-swarm-script.test.ts +104 -0
- package/src/tools/app-get.ts +10 -2
- package/src/tools/app-rollback.ts +1 -0
- package/src/tools/app-sync.ts +112 -0
- package/src/tools/script-common.ts +20 -0
- package/src/tools/tool-config.ts +2 -1
- package/src/tools/utils.ts +11 -0
- package/src/tools/workflows/create-workflow.ts +4 -1
- package/src/types.ts +1394 -1206
- package/src/utils/template.ts +8 -1
- package/src/utils/zod-openapi.ts +11 -0
- package/src/workflows/engine.ts +27 -4
- package/src/workflows/executors/swarm-script.ts +28 -0
- package/src/workflows/retry-poller.ts +19 -2
- package/templates/skills/apps/content.md +94 -5
- package/templates/skills/asset-namespaces/content.md +2 -2
- package/templates/skills/scheduled-task-resilience/SKILL.md +102 -0
- package/templates/skills/scheduled-task-resilience/config.json +14 -0
- package/templates/skills/scheduled-task-resilience/content.md +97 -0
- package/dist/cli-7wmybrt5.js +0 -115
- package/dist/cli-d91pvmq1.js +0 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desplega.ai/agent-swarm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.131.0",
|
|
4
4
|
"description": "agent-swarm.dev — an open-source operating system for AI work: a lead agent breaks goals into tasks, routes them to specialized workers such as Claude Code or Codex, runs each worker in an isolated container, and preserves shared memory, tools, schedules, and review gates so delegated work compounds across sessions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-swarm.dev",
|
|
@@ -92,6 +92,7 @@
|
|
|
92
92
|
"check:api-key-boundary": "bash scripts/check-api-key-boundary.sh",
|
|
93
93
|
"check:vendored-openapi": "bun scripts/check-vendored-openapi.ts",
|
|
94
94
|
"check:rbac-coverage": "bun scripts/check-rbac-coverage.ts",
|
|
95
|
+
"check:openapi-response-coverage": "bun scripts/check-openapi-response-coverage.ts",
|
|
95
96
|
"check:script-types": "bash scripts/check-script-types-freshness.sh",
|
|
96
97
|
"check:audit-columns": "bash scripts/check-audit-columns.sh",
|
|
97
98
|
"lint:structure": "konsistent check",
|
|
@@ -151,6 +152,7 @@
|
|
|
151
152
|
"@types/bun": "latest",
|
|
152
153
|
"dependency-cruiser": "^18.0.0",
|
|
153
154
|
"konsistent": "1.0.0-beta.1",
|
|
155
|
+
"openapi-typescript": "^7",
|
|
154
156
|
"ts-morph": "^28.0.0",
|
|
155
157
|
"turbo": "^2.10.0"
|
|
156
158
|
},
|
|
@@ -167,14 +169,14 @@
|
|
|
167
169
|
"@aws-sdk/client-bedrock": "3.1048.0",
|
|
168
170
|
"@desplega.ai/business-use": "^0.4.2",
|
|
169
171
|
"@desplega.ai/localtunnel": "^2.2.0",
|
|
170
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
171
|
-
"@earendil-works/pi-ai": "^0.
|
|
172
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
172
|
+
"@earendil-works/pi-agent-core": "^0.84.1",
|
|
173
|
+
"@earendil-works/pi-ai": "^0.84.1",
|
|
174
|
+
"@earendil-works/pi-coding-agent": "^0.84.1",
|
|
173
175
|
"@inkjs/ui": "^2.0.0",
|
|
174
176
|
"@linear/sdk": "^77.0.0",
|
|
175
177
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
176
|
-
"@openai/codex-sdk": "^0.
|
|
177
|
-
"@opencode-ai/sdk": "^1.18.
|
|
178
|
+
"@openai/codex-sdk": "^0.147.0",
|
|
179
|
+
"@opencode-ai/sdk": "^1.18.15",
|
|
178
180
|
"@openfort/openfort-node": "^0.9.1",
|
|
179
181
|
"@opentelemetry/api": "^1.9.1",
|
|
180
182
|
"@opentelemetry/exporter-metrics-otlp-http": "^0.218.0",
|
package/src/apps/definition.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
+
import { listScriptConnections } from "../be/script-connections";
|
|
2
3
|
import { getScriptById } from "../be/scripts/db";
|
|
3
4
|
import { getSavedScriptOwnerAgentId } from "../be/scripts/run-saved";
|
|
4
5
|
import catalog from "./catalog.generated.json";
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
elementDefinitionIssues,
|
|
9
10
|
validatePage,
|
|
10
11
|
} from "./page-validator";
|
|
12
|
+
import { resolveSyncRunAs } from "./sync-run-as";
|
|
11
13
|
|
|
12
14
|
export const AppNameSchema = z.string().regex(/^[a-z][a-zA-Z0-9_]{0,39}$/, {
|
|
13
15
|
message: "must start with a lowercase letter and contain only letters, numbers, or underscores",
|
|
@@ -21,6 +23,25 @@ export function isIso8601Date(value: string): boolean {
|
|
|
21
23
|
return ISO_8601_PREFIX.test(value) && !Number.isNaN(Date.parse(value));
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
export const SourceTransformSchema = z.enum(["slug", "lower", "upper", "cents", "date-parse"]);
|
|
27
|
+
|
|
28
|
+
/** Column kind each transform may target — the sync engine projects into it. */
|
|
29
|
+
const TRANSFORM_COLUMN_KIND: Record<z.infer<typeof SourceTransformSchema>, ColumnKind> = {
|
|
30
|
+
slug: "string",
|
|
31
|
+
lower: "string",
|
|
32
|
+
upper: "string",
|
|
33
|
+
cents: "number",
|
|
34
|
+
"date-parse": "date",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const ColumnSourceBindingSchema = z
|
|
38
|
+
.object({
|
|
39
|
+
of: AppNameSchema,
|
|
40
|
+
field: z.string().min(1),
|
|
41
|
+
transform: SourceTransformSchema.optional(),
|
|
42
|
+
})
|
|
43
|
+
.strict();
|
|
44
|
+
|
|
24
45
|
const ColumnDefSchema = z
|
|
25
46
|
.object({
|
|
26
47
|
kind: ColumnKindSchema,
|
|
@@ -29,6 +50,7 @@ const ColumnDefSchema = z
|
|
|
29
50
|
index: z.boolean().optional(),
|
|
30
51
|
default: z.union([z.string(), z.number(), z.boolean()]).optional(),
|
|
31
52
|
hidden: z.boolean().optional(),
|
|
53
|
+
source: ColumnSourceBindingSchema.optional(),
|
|
32
54
|
})
|
|
33
55
|
.superRefine((column, ctx) => {
|
|
34
56
|
if (column.kind === "enum") {
|
|
@@ -119,9 +141,36 @@ const UserConfigFieldSchema = z
|
|
|
119
141
|
|
|
120
142
|
export const UserConfigSchema = z.record(AppNameSchema, UserConfigFieldSchema);
|
|
121
143
|
|
|
144
|
+
/**
|
|
145
|
+
* An inbound projection of an external system into a model. Script sources are
|
|
146
|
+
* the extensible default; `swarm-tasks` is the one native connector. The union
|
|
147
|
+
* is discriminated so a merge patch can never splice two connector shapes.
|
|
148
|
+
*/
|
|
149
|
+
const SourceDefSchema = z.discriminatedUnion("connector", [
|
|
150
|
+
// Strict: a typo'd key (e.g. `connnection`) silently stripped here would
|
|
151
|
+
// surface only as an uncredentialed pull at sync time.
|
|
152
|
+
z
|
|
153
|
+
.object({
|
|
154
|
+
connector: z.literal("script"),
|
|
155
|
+
scriptId: z.string().uuid(),
|
|
156
|
+
joinKey: AppNameSchema,
|
|
157
|
+
args: z.record(z.string(), z.unknown()).optional(),
|
|
158
|
+
connection: z.string().min(1).optional(),
|
|
159
|
+
})
|
|
160
|
+
.strict(),
|
|
161
|
+
z
|
|
162
|
+
.object({
|
|
163
|
+
connector: z.literal("swarm-tasks"),
|
|
164
|
+
joinKey: AppNameSchema,
|
|
165
|
+
config: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
166
|
+
})
|
|
167
|
+
.strict(),
|
|
168
|
+
]);
|
|
169
|
+
|
|
122
170
|
const ModelDefSchema = z
|
|
123
171
|
.object({
|
|
124
172
|
columns: z.record(AppNameSchema, ColumnDefSchema),
|
|
173
|
+
sources: z.record(AppNameSchema, SourceDefSchema).optional(),
|
|
125
174
|
})
|
|
126
175
|
.superRefine((model, ctx) => {
|
|
127
176
|
const count = Object.keys(model.columns).length;
|
|
@@ -137,6 +186,9 @@ const ModelDefSchema = z
|
|
|
137
186
|
});
|
|
138
187
|
}
|
|
139
188
|
}
|
|
189
|
+
if (Object.keys(model.sources ?? {}).length > 4) {
|
|
190
|
+
ctx.addIssue({ code: "custom", path: ["sources"], message: "must define at most 4 sources" });
|
|
191
|
+
}
|
|
140
192
|
});
|
|
141
193
|
|
|
142
194
|
const AppQueryParamRefSchema = z
|
|
@@ -156,8 +208,18 @@ export const SYSTEM_COLUMN_KINDS: Record<string, "string" | "date" | "boolean">
|
|
|
156
208
|
updatedAt: "date",
|
|
157
209
|
createdBy: "string",
|
|
158
210
|
updatedBy: "string",
|
|
211
|
+
// Sync provenance, present only on source-owned rows.
|
|
212
|
+
source: "string",
|
|
213
|
+
syncedAt: "date",
|
|
214
|
+
stale: "boolean",
|
|
159
215
|
};
|
|
160
216
|
|
|
217
|
+
/**
|
|
218
|
+
* System row fields a sort may target alongside declared model columns. All are
|
|
219
|
+
* ISO-8601 dates, so membership also selects date-aware comparison.
|
|
220
|
+
*/
|
|
221
|
+
export const SORTABLE_SYSTEM_DATE_COLUMNS = new Set(["createdAt", "updatedAt", "syncedAt"]);
|
|
222
|
+
|
|
161
223
|
const AppQueryDefSchema = z.object({
|
|
162
224
|
model: AppNameSchema,
|
|
163
225
|
filter: z
|
|
@@ -185,6 +247,12 @@ const AppActionDefSchema = z.discriminatedUnion("kind", [
|
|
|
185
247
|
// non-UUID (custom stable ids), so no format pin here.
|
|
186
248
|
agentId: z.string().min(1).optional(),
|
|
187
249
|
}),
|
|
250
|
+
z.object({
|
|
251
|
+
kind: z.literal("sync"),
|
|
252
|
+
// Omitting both fans out to every (model x source) pair the app declares.
|
|
253
|
+
model: AppNameSchema.optional(),
|
|
254
|
+
source: AppNameSchema.optional(),
|
|
255
|
+
}),
|
|
188
256
|
]);
|
|
189
257
|
|
|
190
258
|
const AppPageParamSchema = z
|
|
@@ -392,8 +460,7 @@ export const AppDefinitionSchema = z
|
|
|
392
460
|
const sortColumn = query.sort?.column;
|
|
393
461
|
if (
|
|
394
462
|
sortColumn &&
|
|
395
|
-
sortColumn
|
|
396
|
-
sortColumn !== "updatedAt" &&
|
|
463
|
+
!SORTABLE_SYSTEM_DATE_COLUMNS.has(sortColumn) &&
|
|
397
464
|
(!Object.hasOwn(model.columns, sortColumn) || model.columns[sortColumn]!.hidden === true)
|
|
398
465
|
) {
|
|
399
466
|
ctx.addIssue({
|
|
@@ -407,6 +474,9 @@ export const AppDefinitionSchema = z
|
|
|
407
474
|
|
|
408
475
|
export type ColumnKind = z.infer<typeof ColumnKindSchema>;
|
|
409
476
|
export type ColumnDef = z.infer<typeof ColumnDefSchema>;
|
|
477
|
+
export type ColumnSourceBinding = z.infer<typeof ColumnSourceBindingSchema>;
|
|
478
|
+
export type SourceTransform = z.infer<typeof SourceTransformSchema>;
|
|
479
|
+
export type SourceDef = z.infer<typeof SourceDefSchema>;
|
|
410
480
|
export type UserConfigField = z.infer<typeof UserConfigFieldSchema>;
|
|
411
481
|
export type ModelDef = z.infer<typeof ModelDefSchema>;
|
|
412
482
|
export type AppQueryDef = z.infer<typeof AppQueryDefSchema>;
|
|
@@ -467,25 +537,287 @@ export type AppDefinitionParseContext = ElementReferenceContext & {
|
|
|
467
537
|
*/
|
|
468
538
|
writerAgentId?: string | null;
|
|
469
539
|
/**
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
540
|
+
* True when the writer is an authenticated web USER. Users own no scripts,
|
|
541
|
+
* so every fresh agent-scoped reference is foreign for them — without this
|
|
542
|
+
* flag a user write (which carries no agent id) would skip the ownership
|
|
543
|
+
* gate exactly like the trusted operator.
|
|
544
|
+
*/
|
|
545
|
+
writerIsUser?: boolean;
|
|
546
|
+
/**
|
|
547
|
+
* The app's current stored definition, for grandfathering: a script already
|
|
548
|
+
* wired into the app stays referenceable AT ITS EXISTING PATH so an agent can
|
|
549
|
+
* keep editing an app that legitimately carries another owner's script. The
|
|
550
|
+
* bare id is never grandfathered — a stored foreign action must not seed a
|
|
551
|
+
* new foreign source (or vice versa) with fresh args or a connection.
|
|
473
552
|
*/
|
|
474
553
|
existingDefinition?: unknown;
|
|
475
554
|
};
|
|
476
555
|
|
|
477
|
-
/**
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
556
|
+
/**
|
|
557
|
+
* Defensively collect every script reference a possibly-broken definition
|
|
558
|
+
* carries — script actions and model sources alike — keyed by script id with
|
|
559
|
+
* the definition paths that name it. Tolerant by design: it walks raw JSON, so
|
|
560
|
+
* an app whose definition no longer parses still reports its references.
|
|
561
|
+
*
|
|
562
|
+
* Two callers: grandfathering on write (an agent editing an app that
|
|
563
|
+
* legitimately carries another owner's script must not be locked out) and the
|
|
564
|
+
* script-delete guard in the scripts API.
|
|
565
|
+
*/
|
|
566
|
+
export function collectScriptReferences(definition: unknown): Map<string, string[]> {
|
|
567
|
+
const references = new Map<string, string[]>();
|
|
568
|
+
const add = (scriptId: unknown, path: string): void => {
|
|
569
|
+
if (typeof scriptId !== "string" || scriptId.length === 0) return;
|
|
570
|
+
const paths = references.get(scriptId);
|
|
571
|
+
if (paths) paths.push(path);
|
|
572
|
+
else references.set(scriptId, [path]);
|
|
573
|
+
};
|
|
574
|
+
if (!isMergePatchObject(definition)) return references;
|
|
575
|
+
const actions = definition.actions;
|
|
576
|
+
if (isMergePatchObject(actions)) {
|
|
577
|
+
for (const [actionName, action] of Object.entries(actions)) {
|
|
578
|
+
if (isMergePatchObject(action)) add(action.scriptId, `actions.${actionName}`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const models = definition.models;
|
|
582
|
+
if (isMergePatchObject(models)) {
|
|
583
|
+
for (const [modelName, model] of Object.entries(models)) {
|
|
584
|
+
if (!isMergePatchObject(model) || !isMergePatchObject(model.sources)) continue;
|
|
585
|
+
for (const [sourceName, source] of Object.entries(model.sources)) {
|
|
586
|
+
if (isMergePatchObject(source)) {
|
|
587
|
+
add(source.scriptId, `models.${modelName}.sources.${sourceName}`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return references;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Is this agent-scoped script foreign to the writing principal? Operators
|
|
597
|
+
* (no writer identity at all) may wire anything; an agent may wire its own;
|
|
598
|
+
* a web user owns no scripts, so every agent-scoped script is foreign to it.
|
|
599
|
+
*/
|
|
600
|
+
function foreignScriptForWriter(
|
|
601
|
+
context: AppDefinitionParseContext,
|
|
602
|
+
script: NonNullable<ReturnType<typeof getScriptById>>,
|
|
603
|
+
): boolean {
|
|
604
|
+
if (script.scope !== "agent") return false;
|
|
605
|
+
if (context.writerIsUser === true) return true;
|
|
606
|
+
if (!context.writerAgentId) return false;
|
|
607
|
+
return getSavedScriptOwnerAgentId(script) !== context.writerAgentId;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Canonical form of everything security-relevant about a script reference:
|
|
612
|
+
* which script runs, with what arguments, over which connection. Grandfathering
|
|
613
|
+
* compares THIS, not the bare id — an id-only match would let a non-owner keep
|
|
614
|
+
* a stored foreign reference while swapping in attacker-chosen args or a
|
|
615
|
+
* different connection, all executed under the owner's credentials.
|
|
616
|
+
*/
|
|
617
|
+
function scriptRefKey(scriptId: unknown, args: unknown, connection: unknown): string | null {
|
|
618
|
+
if (typeof scriptId !== "string" || scriptId.length === 0) return null;
|
|
619
|
+
return JSON.stringify({ scriptId, args: args ?? null, connection: connection ?? null });
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Path-exact grandfathering index: `definition path -> scriptRefKey`. A
|
|
624
|
+
* reference is grandfathered only when the stored definition already carries
|
|
625
|
+
* the SAME script, args, and connection at the SAME path.
|
|
626
|
+
*/
|
|
627
|
+
function collectScriptReferencePathMap(definition: unknown): Map<string, string> {
|
|
628
|
+
const byPath = new Map<string, string>();
|
|
629
|
+
if (!isMergePatchObject(definition)) return byPath;
|
|
481
630
|
const actions = definition.actions;
|
|
482
|
-
if (
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
631
|
+
if (isMergePatchObject(actions)) {
|
|
632
|
+
for (const [actionName, action] of Object.entries(actions)) {
|
|
633
|
+
if (!isMergePatchObject(action)) continue;
|
|
634
|
+
const key = scriptRefKey(action.scriptId, action.args, undefined);
|
|
635
|
+
if (key) byPath.set(`actions.${actionName}`, key);
|
|
486
636
|
}
|
|
487
637
|
}
|
|
488
|
-
|
|
638
|
+
const models = definition.models;
|
|
639
|
+
if (isMergePatchObject(models)) {
|
|
640
|
+
for (const [modelName, model] of Object.entries(models)) {
|
|
641
|
+
if (!isMergePatchObject(model) || !isMergePatchObject(model.sources)) continue;
|
|
642
|
+
for (const [sourceName, source] of Object.entries(model.sources)) {
|
|
643
|
+
if (!isMergePatchObject(source)) continue;
|
|
644
|
+
const key = scriptRefKey(source.scriptId, source.args, source.connection);
|
|
645
|
+
if (key) byPath.set(`models.${modelName}.sources.${sourceName}`, key);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return byPath;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** Semantic checks for a model's sources and its columns' source bindings. */
|
|
653
|
+
function modelSourceIssues(
|
|
654
|
+
modelName: string,
|
|
655
|
+
model: ModelDef,
|
|
656
|
+
context: AppDefinitionParseContext,
|
|
657
|
+
grandfatheredScriptRefs: Map<string, string>,
|
|
658
|
+
): AppValidationIssue[] {
|
|
659
|
+
const issues: AppValidationIssue[] = [];
|
|
660
|
+
const sources = model.sources ?? {};
|
|
661
|
+
const sourceNames = new Set(Object.keys(sources));
|
|
662
|
+
|
|
663
|
+
for (const [sourceName, source] of Object.entries(sources)) {
|
|
664
|
+
const path = `models.${modelName}.sources.${sourceName}`;
|
|
665
|
+
const joinColumn = Object.hasOwn(model.columns, source.joinKey)
|
|
666
|
+
? model.columns[source.joinKey]!
|
|
667
|
+
: undefined;
|
|
668
|
+
if (!joinColumn || joinColumn.hidden === true) {
|
|
669
|
+
issues.push({
|
|
670
|
+
path: `${path}.joinKey`,
|
|
671
|
+
message: `unknown or hidden column "${source.joinKey}"`,
|
|
672
|
+
});
|
|
673
|
+
} else if (joinColumn.kind !== "string") {
|
|
674
|
+
issues.push({
|
|
675
|
+
path: `${path}.joinKey`,
|
|
676
|
+
message: `join key column "${source.joinKey}" must be a string column`,
|
|
677
|
+
});
|
|
678
|
+
} else {
|
|
679
|
+
if (joinColumn.source) {
|
|
680
|
+
issues.push({
|
|
681
|
+
path: `${path}.joinKey`,
|
|
682
|
+
message: `join key column "${source.joinKey}" must not be bound to a source`,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
if (joinColumn.required === true) {
|
|
686
|
+
issues.push({
|
|
687
|
+
path: `${path}.joinKey`,
|
|
688
|
+
message: `join key column "${source.joinKey}" must not be required`,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
if (joinColumn.default !== undefined) {
|
|
692
|
+
issues.push({
|
|
693
|
+
path: `${path}.joinKey`,
|
|
694
|
+
message: `join key column "${source.joinKey}" must not declare a default`,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (source.connector !== "script") continue;
|
|
700
|
+
const script = getScriptById(source.scriptId);
|
|
701
|
+
if (!script) {
|
|
702
|
+
issues.push({ path: `${path}.scriptId`, message: `script "${source.scriptId}" not found` });
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
const runAs = resolveSyncRunAs(script);
|
|
706
|
+
const grandfatheredRef =
|
|
707
|
+
grandfatheredScriptRefs.get(path) ===
|
|
708
|
+
scriptRefKey(source.scriptId, source.args, source.connection);
|
|
709
|
+
// A pull runs the script with its OWNER's bindings, so a writer may only
|
|
710
|
+
// wire scripts it owns (or global ones) — the script-action rule.
|
|
711
|
+
if (foreignScriptForWriter(context, script) && !grandfatheredRef) {
|
|
712
|
+
issues.push({
|
|
713
|
+
path: `${path}.scriptId`,
|
|
714
|
+
message: `script "${source.scriptId}" is agent-scoped to another agent — reference a script you own or a global script`,
|
|
715
|
+
});
|
|
716
|
+
} else if (
|
|
717
|
+
// Owner-less (and foreign-owned) GLOBAL scripts sync with runAs's
|
|
718
|
+
// credential bindings — for the catalog that is the LEAD. Introducing or
|
|
719
|
+
// altering such a source is that identity's (or the operator's)
|
|
720
|
+
// privilege; every other writer keeps only pinned stored references.
|
|
721
|
+
script.scope !== "agent" &&
|
|
722
|
+
(context.writerIsUser === true || typeof context.writerAgentId === "string") &&
|
|
723
|
+
context.writerAgentId !== runAs &&
|
|
724
|
+
!grandfatheredRef
|
|
725
|
+
) {
|
|
726
|
+
issues.push({
|
|
727
|
+
path: `${path}.scriptId`,
|
|
728
|
+
message: `script "${source.scriptId}" syncs with agent "${runAs}"'s credentials — only that agent or the operator may wire or alter this source`,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
if (source.connection !== undefined) {
|
|
732
|
+
const reachable = listScriptConnections({ agentId: runAs });
|
|
733
|
+
if (!reachable.some((connection) => connection.slug === source.connection)) {
|
|
734
|
+
issues.push({
|
|
735
|
+
path: `${path}.connection`,
|
|
736
|
+
message: `connection "${source.connection}" not found or disabled for the sync run-as identity`,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
for (const [columnName, column] of Object.entries(model.columns)) {
|
|
743
|
+
const path = `models.${modelName}.columns.${columnName}`;
|
|
744
|
+
if (!column.source) {
|
|
745
|
+
// Sync creates rows from projected fields alone, so every required column
|
|
746
|
+
// it does NOT own has to be satisfiable without a writer.
|
|
747
|
+
if (
|
|
748
|
+
sourceNames.size > 0 &&
|
|
749
|
+
column.required === true &&
|
|
750
|
+
column.hidden !== true &&
|
|
751
|
+
column.default === undefined
|
|
752
|
+
) {
|
|
753
|
+
issues.push({
|
|
754
|
+
path,
|
|
755
|
+
message:
|
|
756
|
+
"required column on a model with sources must declare a default — sync-created rows cannot supply it",
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
if (!sourceNames.has(column.source.of)) {
|
|
762
|
+
issues.push({ path: `${path}.source.of`, message: `unknown source "${column.source.of}"` });
|
|
763
|
+
}
|
|
764
|
+
const transform = column.source.transform;
|
|
765
|
+
if (transform && TRANSFORM_COLUMN_KIND[transform] !== column.kind) {
|
|
766
|
+
issues.push({
|
|
767
|
+
path: `${path}.source.transform`,
|
|
768
|
+
message: `transform "${transform}" requires a ${TRANSFORM_COLUMN_KIND[transform]} column`,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
if (column.required === true) {
|
|
772
|
+
issues.push({
|
|
773
|
+
path: `${path}.required`,
|
|
774
|
+
message: "source-bound column must not be required",
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
if (column.default !== undefined) {
|
|
778
|
+
issues.push({
|
|
779
|
+
path: `${path}.default`,
|
|
780
|
+
message: "source-bound column must not declare a default",
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return issues;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/** A `sync` action must resolve to at least one (model x source) pair. */
|
|
789
|
+
function syncActionIssues(
|
|
790
|
+
name: string,
|
|
791
|
+
action: { model?: string; source?: string },
|
|
792
|
+
models: Record<string, ModelDef>,
|
|
793
|
+
): AppValidationIssue[] {
|
|
794
|
+
if (action.model !== undefined && !Object.hasOwn(models, action.model)) {
|
|
795
|
+
return [{ path: `actions.${name}.model`, message: `unknown model "${action.model}"` }];
|
|
796
|
+
}
|
|
797
|
+
const candidates = action.model !== undefined ? [action.model] : Object.keys(models);
|
|
798
|
+
const pairs = candidates.flatMap((modelName) =>
|
|
799
|
+
Object.keys(models[modelName]?.sources ?? {}).filter(
|
|
800
|
+
(sourceName) => action.source === undefined || sourceName === action.source,
|
|
801
|
+
),
|
|
802
|
+
);
|
|
803
|
+
if (pairs.length > 0) return [];
|
|
804
|
+
if (action.source !== undefined) {
|
|
805
|
+
return [
|
|
806
|
+
{
|
|
807
|
+
path: `actions.${name}.source`,
|
|
808
|
+
message:
|
|
809
|
+
action.model !== undefined
|
|
810
|
+
? `unknown source "${action.source}" on model "${action.model}"`
|
|
811
|
+
: `unknown source "${action.source}" — no model declares it`,
|
|
812
|
+
},
|
|
813
|
+
];
|
|
814
|
+
}
|
|
815
|
+
if (action.model !== undefined) {
|
|
816
|
+
return [
|
|
817
|
+
{ path: `actions.${name}.model`, message: `model "${action.model}" declares no sources` },
|
|
818
|
+
];
|
|
819
|
+
}
|
|
820
|
+
return [{ path: `actions.${name}`, message: "no model declares a source to sync" }];
|
|
489
821
|
}
|
|
490
822
|
|
|
491
823
|
export function parseAppDefinition(
|
|
@@ -527,8 +859,15 @@ export function parseAppDefinition(
|
|
|
527
859
|
...crossPageDefinitionIssues(parsed.data, catalog),
|
|
528
860
|
...elementDefinitionIssues(parsed.data, catalog, elementContext),
|
|
529
861
|
];
|
|
530
|
-
const
|
|
862
|
+
const grandfatheredScriptRefs = collectScriptReferencePathMap(elementContext.existingDefinition);
|
|
863
|
+
for (const [modelName, model] of Object.entries(parsed.data.models)) {
|
|
864
|
+
issues.push(...modelSourceIssues(modelName, model, elementContext, grandfatheredScriptRefs));
|
|
865
|
+
}
|
|
531
866
|
for (const [name, action] of Object.entries(parsed.data.actions ?? {})) {
|
|
867
|
+
if (action.kind === "sync") {
|
|
868
|
+
issues.push(...syncActionIssues(name, action, parsed.data.models));
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
532
871
|
if (action.kind !== "script") continue;
|
|
533
872
|
const script = getScriptById(action.scriptId);
|
|
534
873
|
if (!script) {
|
|
@@ -538,15 +877,15 @@ export function parseAppDefinition(
|
|
|
538
877
|
});
|
|
539
878
|
continue;
|
|
540
879
|
}
|
|
541
|
-
// Invoke-time runs the script with the OWNER's bindings, so
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
// editable
|
|
880
|
+
// Invoke-time runs the script with the OWNER's bindings, so a writer may
|
|
881
|
+
// only wire scripts it owns (or global ones). A reference the stored
|
|
882
|
+
// definition already carries at this exact path is grandfathered so
|
|
883
|
+
// foreign-authored apps stay editable — the bare id is not, so an existing
|
|
884
|
+
// foreign reference cannot seed a new one elsewhere.
|
|
545
885
|
if (
|
|
546
|
-
elementContext
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
!grandfatheredScriptIds.has(action.scriptId)
|
|
886
|
+
foreignScriptForWriter(elementContext, script) &&
|
|
887
|
+
grandfatheredScriptRefs.get(`actions.${name}`) !==
|
|
888
|
+
scriptRefKey(action.scriptId, action.args, undefined)
|
|
550
889
|
) {
|
|
551
890
|
issues.push({
|
|
552
891
|
path: `actions.${name}.scriptId`,
|
|
@@ -649,6 +988,7 @@ function applyMergePatch(target: unknown, patch: unknown, path: string[]): unkno
|
|
|
649
988
|
(path.length === 1 && path[0] === "elements") ||
|
|
650
989
|
(path.length === 1 && path[0] === "userConfig") ||
|
|
651
990
|
(path.length === 3 && path[0] === "models" && path[2] === "columns") ||
|
|
991
|
+
(path.length === 3 && path[0] === "models" && path[2] === "sources") ||
|
|
652
992
|
(path.length === 3 && path[0] === "elements" && path[2] === "elements") ||
|
|
653
993
|
(path.length === 3 && path[0] === "pages" && (path[2] === "elements" || path[2] === "params"));
|
|
654
994
|
|
|
@@ -677,9 +1017,11 @@ function applyMergePatch(target: unknown, patch: unknown, path: string[]): unkno
|
|
|
677
1017
|
|
|
678
1018
|
/**
|
|
679
1019
|
* Apply RFC 7396 JSON Merge Patch semantics to an app definition without
|
|
680
|
-
* mutating either input. Individual action, page-element,
|
|
681
|
-
* entries are intentionally atomic
|
|
682
|
-
*
|
|
1020
|
+
* mutating either input. Individual action, page-element, model-column, and
|
|
1021
|
+
* model-source entries are intentionally atomic — whole-replaced, never
|
|
1022
|
+
* key-merged — so a patch can never splice two connector shapes together.
|
|
1023
|
+
* A reusable-element patch containing only `elements` merges node-by-node;
|
|
1024
|
+
* any other key makes it a full replacement.
|
|
683
1025
|
*/
|
|
684
1026
|
export function applyAppDefinitionPatch(
|
|
685
1027
|
stored: AppDefinition,
|