@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/src/apps/row-store.ts
CHANGED
|
@@ -14,12 +14,28 @@ export type AppRow = {
|
|
|
14
14
|
updatedAt: string;
|
|
15
15
|
createdBy?: string;
|
|
16
16
|
updatedBy?: string;
|
|
17
|
+
/** Sync provenance — present only on rows a source owns. */
|
|
18
|
+
source?: string;
|
|
19
|
+
syncedAt?: string;
|
|
20
|
+
stale?: boolean;
|
|
17
21
|
} & Record<string, unknown>;
|
|
18
22
|
|
|
23
|
+
export type AppRowEnvelope = { source: string; syncedAt: string; stale: boolean };
|
|
24
|
+
|
|
19
25
|
export interface AppRowWriteOptions {
|
|
20
26
|
skipUpdatedAt?: boolean;
|
|
21
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Stable id of the acting principal (`user:<id>`, `agent:<id>`, `operator`,
|
|
29
|
+
* or `sync:<source>` for engine writes) for row provenance.
|
|
30
|
+
*/
|
|
22
31
|
actor?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Sync-engine escape hatch. External write paths leave this false so
|
|
34
|
+
* source-bound and join-key columns stay read-only projections.
|
|
35
|
+
*/
|
|
36
|
+
allowSourceManaged?: boolean;
|
|
37
|
+
/** Row provenance stamped after value preparation; honoured only with `allowSourceManaged`. */
|
|
38
|
+
envelope?: AppRowEnvelope;
|
|
23
39
|
}
|
|
24
40
|
|
|
25
41
|
export class AppRowValidationError extends Error {
|
|
@@ -92,18 +108,50 @@ function validValue(column: ColumnDef, value: unknown): boolean {
|
|
|
92
108
|
return typeof value === "string" && Boolean(column.enum?.includes(value));
|
|
93
109
|
}
|
|
94
110
|
|
|
111
|
+
function joinKeyColumnNames(definition: ModelDef): Set<string> {
|
|
112
|
+
const names = new Set<string>();
|
|
113
|
+
for (const source of Object.values(definition.sources ?? {})) names.add(source.joinKey);
|
|
114
|
+
return names;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Row provenance to stamp, when the caller is the sync engine. */
|
|
118
|
+
function sourceEnvelope(options: AppRowWriteOptions): AppRowEnvelope | undefined {
|
|
119
|
+
return options.allowSourceManaged === true ? options.envelope : undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
95
122
|
function prepareValues(
|
|
96
123
|
definition: ModelDef,
|
|
97
124
|
values: Record<string, unknown>,
|
|
98
125
|
mode: "create" | "patch",
|
|
126
|
+
options: AppRowWriteOptions = {},
|
|
99
127
|
): Record<string, unknown> {
|
|
100
128
|
const issues: AppValidationIssue[] = [];
|
|
101
129
|
const prepared: Record<string, unknown> = { ...values };
|
|
130
|
+
const sourceManaged = options.allowSourceManaged === true;
|
|
131
|
+
if (sourceManaged && !options.envelope) {
|
|
132
|
+
// Without provenance the privileged write would produce a row
|
|
133
|
+
// indistinguishable from a user-owned one.
|
|
134
|
+
throw new Error("allowSourceManaged writes must carry an envelope");
|
|
135
|
+
}
|
|
136
|
+
const joinKeys = sourceManaged ? new Set<string>() : joinKeyColumnNames(definition);
|
|
102
137
|
|
|
103
138
|
for (const name of Object.keys(values)) {
|
|
104
139
|
const column = Object.hasOwn(definition.columns, name) ? definition.columns[name] : undefined;
|
|
105
140
|
if (!column || column.hidden === true) {
|
|
106
141
|
issues.push({ path: `values.${name}`, message: `unknown or hidden column "${name}"` });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (sourceManaged) continue;
|
|
145
|
+
if (column.source) {
|
|
146
|
+
issues.push({
|
|
147
|
+
path: `values.${name}`,
|
|
148
|
+
message: `column is a read-only projection from source "${column.source.of}"; mutate it via the source or a sync refresh`,
|
|
149
|
+
});
|
|
150
|
+
} else if (joinKeys.has(name)) {
|
|
151
|
+
issues.push({
|
|
152
|
+
path: `values.${name}`,
|
|
153
|
+
message: "column is the sync join key and is managed by the sync engine",
|
|
154
|
+
});
|
|
107
155
|
}
|
|
108
156
|
}
|
|
109
157
|
|
|
@@ -112,6 +160,9 @@ function prepareValues(
|
|
|
112
160
|
if (column.hidden === true) continue;
|
|
113
161
|
if (!Object.hasOwn(prepared, name) && column.default !== undefined)
|
|
114
162
|
prepared[name] = column.default;
|
|
163
|
+
// A source pull supplies only the fields the record carried; a bound
|
|
164
|
+
// column with no projected value is the source's business, not the row's.
|
|
165
|
+
if (sourceManaged && column.source) continue;
|
|
115
166
|
if (
|
|
116
167
|
column.required === true &&
|
|
117
168
|
(!Object.hasOwn(prepared, name) || prepared[name] === undefined || prepared[name] === null)
|
|
@@ -186,17 +237,19 @@ function createRowUnlocked(
|
|
|
186
237
|
model: string,
|
|
187
238
|
definition: ModelDef,
|
|
188
239
|
prepared: Record<string, unknown>,
|
|
189
|
-
|
|
240
|
+
options: AppRowWriteOptions,
|
|
190
241
|
): AppRow {
|
|
191
242
|
const issuedMs = Math.max(Date.now(), lastCreatedAtMs + 1);
|
|
192
243
|
lastCreatedAtMs = issuedMs;
|
|
193
244
|
const now = new Date(issuedMs).toISOString();
|
|
245
|
+
const actor = options.actor;
|
|
194
246
|
return writeRow(appId, model, definition, {
|
|
195
247
|
id: crypto.randomUUID(),
|
|
196
248
|
createdAt: now,
|
|
197
249
|
updatedAt: now,
|
|
198
250
|
...(actor !== undefined ? { createdBy: actor, updatedBy: actor } : {}),
|
|
199
251
|
...prepared,
|
|
252
|
+
...sourceEnvelope(options),
|
|
200
253
|
});
|
|
201
254
|
}
|
|
202
255
|
|
|
@@ -208,9 +261,9 @@ export function createAppRowUnlocked(
|
|
|
208
261
|
values: Record<string, unknown>,
|
|
209
262
|
options: AppRowWriteOptions = {},
|
|
210
263
|
): AppRow {
|
|
211
|
-
const prepared = prepareValues(definition, values, "create");
|
|
264
|
+
const prepared = prepareValues(definition, values, "create", options);
|
|
212
265
|
if (!appExists(appId)) throw new AppRowAppNotFoundError(appId);
|
|
213
|
-
return createRowUnlocked(appId, model, definition, prepared, options
|
|
266
|
+
return createRowUnlocked(appId, model, definition, prepared, options);
|
|
214
267
|
}
|
|
215
268
|
|
|
216
269
|
export function createAppRow(
|
|
@@ -223,8 +276,8 @@ export function createAppRow(
|
|
|
223
276
|
return withMutationLock(appId, model, () => {
|
|
224
277
|
const currentDefinition = currentModelDefinition(appId, model);
|
|
225
278
|
if (!currentDefinition) throw new AppRowAppNotFoundError(appId);
|
|
226
|
-
const prepared = prepareValues(currentDefinition, values, "create");
|
|
227
|
-
return createRowUnlocked(appId, model, currentDefinition, prepared, options
|
|
279
|
+
const prepared = prepareValues(currentDefinition, values, "create", options);
|
|
280
|
+
return createRowUnlocked(appId, model, currentDefinition, prepared, options);
|
|
228
281
|
});
|
|
229
282
|
}
|
|
230
283
|
|
|
@@ -238,9 +291,11 @@ export function createAppRows(
|
|
|
238
291
|
return withMutationLock(appId, model, () => {
|
|
239
292
|
const currentDefinition = currentModelDefinition(appId, model);
|
|
240
293
|
if (!currentDefinition) throw new AppRowAppNotFoundError(appId);
|
|
241
|
-
const prepared = rows.map((values) =>
|
|
294
|
+
const prepared = rows.map((values) =>
|
|
295
|
+
prepareValues(currentDefinition, values, "create", options),
|
|
296
|
+
);
|
|
242
297
|
return prepared.map((values) =>
|
|
243
|
-
createRowUnlocked(appId, model, currentDefinition, values, options
|
|
298
|
+
createRowUnlocked(appId, model, currentDefinition, values, options),
|
|
244
299
|
);
|
|
245
300
|
});
|
|
246
301
|
}
|
|
@@ -299,7 +354,7 @@ export function patchAppRow(
|
|
|
299
354
|
return withMutationLock(appId, model, () => {
|
|
300
355
|
const currentDefinition = currentModelDefinition(appId, model);
|
|
301
356
|
if (!currentDefinition) throw new AppRowAppNotFoundError(appId);
|
|
302
|
-
const prepared = prepareValues(currentDefinition, values, "patch");
|
|
357
|
+
const prepared = prepareValues(currentDefinition, values, "patch", options);
|
|
303
358
|
return patchPreparedRowUnlocked(appId, model, currentDefinition, rowId, prepared, options);
|
|
304
359
|
});
|
|
305
360
|
}
|
|
@@ -332,6 +387,7 @@ function patchPreparedRowUnlocked(
|
|
|
332
387
|
updated[name] = value;
|
|
333
388
|
}
|
|
334
389
|
}
|
|
390
|
+
Object.assign(updated, sourceEnvelope(options));
|
|
335
391
|
const newKeys = new Set(indexKeys(model, definition, updated));
|
|
336
392
|
const namespace = appsNamespace(appId);
|
|
337
393
|
for (const key of oldKeys) if (!newKeys.has(key)) deleteKv(namespace, key);
|
|
@@ -351,7 +407,7 @@ export function patchAppRowUnlocked(
|
|
|
351
407
|
values: Record<string, unknown>,
|
|
352
408
|
options: AppRowWriteOptions = {},
|
|
353
409
|
): AppRow | null {
|
|
354
|
-
const prepared = prepareValues(definition, values, "patch");
|
|
410
|
+
const prepared = prepareValues(definition, values, "patch", options);
|
|
355
411
|
return patchPreparedRowUnlocked(appId, model, definition, rowId, prepared, options);
|
|
356
412
|
}
|
|
357
413
|
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
isIso8601Date,
|
|
11
11
|
type ModelDef,
|
|
12
12
|
parseAppDefinition,
|
|
13
|
+
type SourceDef,
|
|
13
14
|
SYSTEM_COLUMN_KINDS,
|
|
14
15
|
} from "./definition";
|
|
15
16
|
import { upgradeAppDefinition } from "./format-upgrades";
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
withMutationLock,
|
|
21
22
|
writeAppRowForMigrationUnlocked,
|
|
22
23
|
} from "./row-store";
|
|
24
|
+
import { invalidateChangedSyncStatus } from "./sync";
|
|
23
25
|
|
|
24
26
|
const MigrationValueSchema = z.union([z.string(), z.number(), z.boolean()]);
|
|
25
27
|
|
|
@@ -49,6 +51,7 @@ export const AppMigrationReportSchema = z.object({
|
|
|
49
51
|
elsed: z.number().int().nonnegative(),
|
|
50
52
|
purgedValues: z.number().int().nonnegative(),
|
|
51
53
|
idxRebuilt: z.number().int().nonnegative(),
|
|
54
|
+
detachedRows: z.number().int().nonnegative(),
|
|
52
55
|
orphanFields: z.array(z.string()),
|
|
53
56
|
userConfigChanged: z.array(z.string()),
|
|
54
57
|
});
|
|
@@ -61,6 +64,7 @@ export const AppMigrationReportOutputSchema = z.looseObject({
|
|
|
61
64
|
elsed: z.number().optional(),
|
|
62
65
|
purgedValues: z.number().optional(),
|
|
63
66
|
idxRebuilt: z.number().optional(),
|
|
67
|
+
detachedRows: z.number().optional(),
|
|
64
68
|
orphanFields: z.array(z.string()).optional(),
|
|
65
69
|
userConfigChanged: z.array(z.string()).optional(),
|
|
66
70
|
});
|
|
@@ -102,6 +106,7 @@ const EMPTY_REPORT: AppMigrationReport = {
|
|
|
102
106
|
elsed: 0,
|
|
103
107
|
purgedValues: 0,
|
|
104
108
|
idxRebuilt: 0,
|
|
109
|
+
detachedRows: 0,
|
|
105
110
|
orphanFields: [],
|
|
106
111
|
userConfigChanged: [],
|
|
107
112
|
};
|
|
@@ -637,6 +642,67 @@ function applyDirective(
|
|
|
637
642
|
}
|
|
638
643
|
}
|
|
639
644
|
|
|
645
|
+
function ownSource(model: ModelDef | undefined, sourceName: string): SourceDef | undefined {
|
|
646
|
+
const sources = model?.sources;
|
|
647
|
+
return sources && Object.hasOwn(sources, sourceName) ? sources[sourceName] : undefined;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Source lifecycle classification. Adding a source, and editing its
|
|
652
|
+
* args/config/connection/scriptId, are free. Join-key identity is immutable and
|
|
653
|
+
* a connector swap is refused while the source owns rows — both would silently
|
|
654
|
+
* re-key or orphan the rows the next pass reconciles against. Removing a source
|
|
655
|
+
* detaches its rows instead of destroying them (Invariant I4): the values stay,
|
|
656
|
+
* the `source`/`syncedAt`/`stale` envelope is stripped, and the count is
|
|
657
|
+
* reported. Detached rows ride the plan's ordinary changed-row write, which
|
|
658
|
+
* never touches `updatedAt`/`updatedBy` — a detach is not a data edit.
|
|
659
|
+
*/
|
|
660
|
+
function planSources(
|
|
661
|
+
modelName: string,
|
|
662
|
+
oldModel: ModelDef | undefined,
|
|
663
|
+
nextModel: ModelDef | undefined,
|
|
664
|
+
rows: AppRow[],
|
|
665
|
+
report: AppMigrationReport,
|
|
666
|
+
issues: AppValidationIssue[],
|
|
667
|
+
): void {
|
|
668
|
+
const sourceNames = new Set([
|
|
669
|
+
...Object.keys(oldModel?.sources ?? {}),
|
|
670
|
+
...Object.keys(nextModel?.sources ?? {}),
|
|
671
|
+
]);
|
|
672
|
+
for (const sourceName of [...sourceNames].sort()) {
|
|
673
|
+
const oldSource = ownSource(oldModel, sourceName);
|
|
674
|
+
// A source that did not exist before owns no rows, so every shape it
|
|
675
|
+
// declares is free (Phase-1 validation still gets its say).
|
|
676
|
+
if (!oldSource) continue;
|
|
677
|
+
const nextSource = ownSource(nextModel, sourceName);
|
|
678
|
+
const path = `models.${modelName}.sources.${sourceName}`;
|
|
679
|
+
const owned = rows.filter((row) => row.source === sourceName);
|
|
680
|
+
|
|
681
|
+
if (!nextSource) {
|
|
682
|
+
for (const row of owned) {
|
|
683
|
+
delete row.source;
|
|
684
|
+
delete row.syncedAt;
|
|
685
|
+
delete row.stale;
|
|
686
|
+
report.detachedRows += 1;
|
|
687
|
+
}
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (nextSource.joinKey !== oldSource.joinKey) {
|
|
692
|
+
issues.push({
|
|
693
|
+
path: `${path}.joinKey`,
|
|
694
|
+
message: "join key is immutable; remove the source and add it again",
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
if (nextSource.connector !== oldSource.connector && owned.length > 0) {
|
|
698
|
+
issues.push({
|
|
699
|
+
path: `${path}.connector`,
|
|
700
|
+
message: `connector change would orphan ${owned.length} row(s) this source owns; remove the source and add it again`,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
640
706
|
function planModel(
|
|
641
707
|
appId: string,
|
|
642
708
|
modelName: string,
|
|
@@ -662,6 +728,8 @@ function planModel(
|
|
|
662
728
|
});
|
|
663
729
|
}
|
|
664
730
|
|
|
731
|
+
planSources(modelName, oldModel, nextModel, rows, report, issues);
|
|
732
|
+
|
|
665
733
|
for (const columnName of changedColumns) {
|
|
666
734
|
const oldColumn = ownColumn(oldModel, columnName);
|
|
667
735
|
const nextColumn = ownColumn(nextModel, columnName);
|
|
@@ -696,6 +764,28 @@ function planModel(
|
|
|
696
764
|
continue;
|
|
697
765
|
}
|
|
698
766
|
|
|
767
|
+
// Binding a column that already holds data hands it to the source: the next
|
|
768
|
+
// pass projects over every value it did not write. A REBIND (source.of
|
|
769
|
+
// moving to a different source) is the same hazard — the old source stops
|
|
770
|
+
// projecting the column, the new one only reconciles its own rows, and the
|
|
771
|
+
// stranded values stay read-only forever. Adding the binding on a fresh
|
|
772
|
+
// (or emptied) column is the supported path.
|
|
773
|
+
if (
|
|
774
|
+
nextColumn.source !== undefined &&
|
|
775
|
+
(oldColumn?.source === undefined || oldColumn.source.of !== nextColumn.source.of)
|
|
776
|
+
) {
|
|
777
|
+
const populated = rows.filter(
|
|
778
|
+
(row) => Object.hasOwn(row, columnName) && row[columnName] !== null,
|
|
779
|
+
).length;
|
|
780
|
+
if (populated > 0) {
|
|
781
|
+
issues.push({
|
|
782
|
+
path: `${path}.source`,
|
|
783
|
+
message: `binding an existing column would let the next pass overwrite ${populated} row(s) of existing data; hide or purge the column and add it bound instead`,
|
|
784
|
+
});
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
699
789
|
if (exactUnhide && nextColumn.required === true && (!directive || !("set" in directive))) {
|
|
700
790
|
const missing = rows.filter(
|
|
701
791
|
(row) => !Object.hasOwn(row, columnName) || row[columnName] === null,
|
|
@@ -973,7 +1063,11 @@ export async function migrateAppSchema<T>(input: {
|
|
|
973
1063
|
);
|
|
974
1064
|
}
|
|
975
1065
|
}
|
|
976
|
-
|
|
1066
|
+
const written = input.writeDefinition();
|
|
1067
|
+
// The stored per-pair sync status describes the OLD configuration:
|
|
1068
|
+
// presenting it for a changed pair would claim a pass that never ran.
|
|
1069
|
+
invalidateChangedSyncStatus(input.appId, input.previousDefinition, input.nextDefinition);
|
|
1070
|
+
return written;
|
|
977
1071
|
});
|
|
978
1072
|
return { result: transaction(), migration: plan.report };
|
|
979
1073
|
});
|
package/src/apps/store.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { defaultAssetKey } from "../assets/key";
|
|
1
2
|
import { getDb } from "../be/db";
|
|
2
3
|
import type { AppDefinition, AppValidationIssue } from "./definition";
|
|
3
4
|
import { AppDefinitionSchema, appDefinitionIssues } from "./definition";
|
|
@@ -105,12 +106,20 @@ export function createApp(input: {
|
|
|
105
106
|
const id = input.id ?? crypto.randomUUID();
|
|
106
107
|
const now = nextTimestamp();
|
|
107
108
|
const row = getDb()
|
|
108
|
-
.prepare<AppDbRow, [string, string, string | null, string, string, string]>(
|
|
109
|
-
`INSERT INTO apps (id, name, description, definition, created_at, updated_at)
|
|
110
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
109
|
+
.prepare<AppDbRow, [string, string, string | null, string, string, string, string]>(
|
|
110
|
+
`INSERT INTO apps (id, name, description, definition, created_at, updated_at, "key")
|
|
111
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
111
112
|
RETURNING id, name, description, definition, created_at, updated_at`,
|
|
112
113
|
)
|
|
113
|
-
.get(
|
|
114
|
+
.get(
|
|
115
|
+
id,
|
|
116
|
+
input.name,
|
|
117
|
+
input.description ?? null,
|
|
118
|
+
encodeDefinition(input.definition),
|
|
119
|
+
now,
|
|
120
|
+
now,
|
|
121
|
+
defaultAssetKey("app", id),
|
|
122
|
+
);
|
|
114
123
|
if (!row) throw new Error("Failed to create app");
|
|
115
124
|
return decodeApp(row);
|
|
116
125
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getLeadAgent } from "../be/db";
|
|
2
|
+
import { getSavedScriptOwnerAgentId } from "../be/scripts/run-saved";
|
|
3
|
+
import type { ScriptRecord } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Whose credential and connection bindings a source pull runs with.
|
|
7
|
+
*
|
|
8
|
+
* The script owner when there is one; otherwise the lead, because seeded
|
|
9
|
+
* catalog scripts are owner-less (`scope:"global"`, no `createdByAgentId`) and
|
|
10
|
+
* the whole catalog-source design depends on them being runnable. The literal
|
|
11
|
+
* fallback only applies to a swarm with no lead registered yet.
|
|
12
|
+
*
|
|
13
|
+
* Definition validation and the sync engine both resolve through here so they
|
|
14
|
+
* can never disagree about whose connections count.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveSyncRunAs(script: ScriptRecord): string {
|
|
17
|
+
return getSavedScriptOwnerAgentId(script) ?? getLeadAgent()?.id ?? "app-sync";
|
|
18
|
+
}
|