@desplega.ai/agent-swarm 2.4.0 → 2.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{acp-adapter-kedstxx6.js → acp-adapter-dv7hmx4r.js} +9 -9
- package/dist/{actions-vget6r1k.js → actions-51nc1gny.js} +21 -21
- package/dist/{amp-adapter-xeb5mktv.js → amp-adapter-abp5tdx8.js} +4 -4
- package/dist/{app-mqap8t8m.js → app-9h14qvr6.js} +3 -3
- package/dist/{assistant-h0gv97mv.js → assistant-wv1gz3mt.js} +22 -22
- package/dist/{boot-reembed-qr8a894z.js → boot-reembed-6yn53ms9.js} +1 -1
- package/dist/{boot-reembed-rj85jjhw.js → boot-reembed-7fcka4z5.js} +1 -1
- package/dist/{boot-scrub-sweep-eyfvn6v9.js → boot-scrub-sweep-pm1580ax.js} +4 -4
- package/dist/{boot-seeds-xs9a8dx1.js → boot-seeds-g5hfc1x8.js} +4 -4
- package/dist/{claude-adapter-bm23cye7.js → claude-adapter-gf3kxhf0.js} +9 -9
- package/dist/{claude-managed-adapter-1mwrsm9d.js → claude-managed-adapter-h2y889ge.js} +2 -2
- package/dist/{claude-managed-setup-gkfgks4j.js → claude-managed-setup-h00hsgtz.js} +2 -2
- package/dist/{claude-sdk-session-apb4ypz3.js → claude-sdk-session-s2k1qd8y.js} +13 -13
- package/dist/{cli-tj4j2ts4.js → cli-02qm7src.js} +8 -8
- package/dist/{cli-1cevz46c.js → cli-0qvtffe9.js} +6 -6
- package/dist/{cli-avmqhatn.js → cli-10hhg0km.js} +8 -8
- package/dist/{cli-7gh7020n.js → cli-1dmcq2eh.js} +1 -1
- package/dist/{cli-kz116kvw.js → cli-239yjk27.js} +7 -7
- package/dist/{cli-7ayprxp7.js → cli-2dfbv8qr.js} +8 -0
- package/dist/{cli-a49vpbf3.js → cli-5kqnemen.js} +1 -1
- package/dist/{cli-x719j3vs.js → cli-5rk36sjf.js} +442 -205
- package/dist/{cli-fb6t92v8.js → cli-6x9ma5w5.js} +1 -1
- package/dist/{cli-c8fwpgqb.js → cli-7225gwj7.js} +2 -2
- package/dist/{cli-5s5hx57y.js → cli-7awf1fp8.js} +1 -1
- package/dist/{cli-bn26xbcs.js → cli-8nyhf7za.js} +1 -1
- package/dist/{cli-ywyrffff.js → cli-8sxqsbbq.js} +1 -1
- package/dist/{cli-rsd7ga7v.js → cli-8zwe9mpk.js} +11 -11
- package/dist/{cli-y83zk5sb.js → cli-a0kcty51.js} +4 -4
- package/dist/{cli-8b9wvjps.js → cli-a4gvwttf.js} +4 -4
- package/dist/{cli-jhp7w1fy.js → cli-a5ebc95n.js} +2 -2
- package/dist/{cli-mvg2hf8p.js → cli-ahtpy9kf.js} +14 -7
- package/dist/{cli-x1p5dhq2.js → cli-bcj753sq.js} +3 -3
- package/dist/{cli-p22zdvnt.js → cli-bn0rewsy.js} +3 -3
- package/dist/{cli-p1m2b6gc.js → cli-d2mb5s8a.js} +7 -7
- package/dist/{cli-rg7ntypm.js → cli-d3qqdy4c.js} +456 -94
- package/dist/{cli-c5sbmw1v.js → cli-d82btycs.js} +94 -15
- package/dist/{cli-02vhhssk.js → cli-efaqv1c7.js} +5 -5
- package/dist/{cli-vjfpshn3.js → cli-ejs3m1sx.js} +1 -1
- package/dist/{cli-kk2r80n5.js → cli-faypxz7q.js} +4 -4
- package/dist/{cli-a1esmmee.js → cli-fpmsqq59.js} +7 -2
- package/dist/{cli-wrv09v7q.js → cli-ftyypbg5.js} +1 -1
- package/dist/{cli-zvze9rbm.js → cli-fyzjmseb.js} +3 -3
- package/dist/{cli-8rwvksbc.js → cli-g0hgtz4p.js} +5 -5
- package/dist/{cli-nrrg0fvy.js → cli-h38rjv87.js} +133 -12
- package/dist/{cli-tg9ksbn8.js → cli-h93c9vb3.js} +2 -2
- package/dist/{cli-eqgfs202.js → cli-hqhzg1qq.js} +2 -2
- package/dist/{cli-bhwa8rzc.js → cli-hyfw2j1e.js} +13 -12
- package/dist/{cli-3wjs22x3.js → cli-j4rqdf2n.js} +2 -2
- package/dist/{cli-6ftrrwm1.js → cli-n85dqg6d.js} +5 -5
- package/dist/{cli-8cxkw1f5.js → cli-nmqdc2t3.js} +2 -2
- package/dist/{cli-4fhyq711.js → cli-nn47d7e8.js} +13 -13
- package/dist/{cli-x3en6ct3.js → cli-nwyw3zhg.js} +1 -1
- package/dist/{cli-3avfh5az.js → cli-pemywhfq.js} +2 -2
- package/dist/{cli-xfe63zx3.js → cli-pkht5hap.js} +2 -2
- package/dist/{cli-sppc0cmw.js → cli-pnp2j5zg.js} +2 -2
- package/dist/{cli-wk8ccrz6.js → cli-pw7v69vp.js} +2 -2
- package/dist/{cli-tgvpmnwz.js → cli-r99kc9yq.js} +5 -5
- package/dist/{cli-bfc3k1x5.js → cli-rdyw3mct.js} +9 -9
- package/dist/{cli-b80a1100.js → cli-s6vr7r2d.js} +2 -2
- package/dist/{cli-zv4vy2b0.js → cli-s95dknxh.js} +1 -1
- package/dist/{cli-ttaz6c7m.js → cli-saw9a6f6.js} +6 -5
- package/dist/{cli-mz1ezzzh.js → cli-shvfedvm.js} +3 -3
- package/dist/{cli-rdr166ht.js → cli-smvcw9z5.js} +1 -1
- package/dist/{cli-2rwet3r7.js → cli-sv5gb0yb.js} +1 -1
- package/dist/{cli-5ff2sjyt.js → cli-t30yj95q.js} +2 -2
- package/dist/{cli-ptkpaqfy.js → cli-wt645v7q.js} +1 -1
- package/dist/{cli-f3nx1h8z.js → cli-wxbn3yfx.js} +2 -2
- package/dist/{cli-wyrr38xm.js → cli-x5mq1fyq.js} +1 -1
- package/dist/{cli-ccn0eybf.js → cli-xbzvet96.js} +6 -6
- package/dist/{cli-4mkvwgzr.js → cli-xf234ws8.js} +1 -1
- package/dist/{cli-nme7wery.js → cli-y2x52gs2.js} +46 -13
- package/dist/{cli-ffbrkh1r.js → cli-y8n5k515.js} +46 -38
- package/dist/{cli-ecshf504.js → cli-z4wd2ftv.js} +7 -7
- package/dist/{cli-dsgj3e7a.js → cli-zfg9enh9.js} +2 -2
- package/dist/{cli-yxqag76b.js → cli-zw88zjba.js} +1 -1
- package/dist/cli.js +20 -20
- package/dist/{codex-adapter-pp2gq8gr.js → codex-adapter-ttygw382.js} +6 -6
- package/dist/{codex-hook-qwzccd64.js → codex-hook-x7nejyrr.js} +7 -4
- package/dist/{codex-login-35p4sk85.js → codex-login-m819c41r.js} +1 -1
- package/dist/{codex-session-runner-0vzn09t8.js → codex-session-runner-60b58vv3.js} +7 -7
- package/dist/{commands-psqhkttb.js → commands-edmdwa4x.js} +2 -2
- package/dist/{controller-t20vz2k8.js → controller-m263fg43.js} +4 -4
- package/dist/{controllers-connect-gkxmqw9d.js → controllers-connect-5fym8sw1.js} +23 -23
- package/dist/{cursor-adapter-3v542qe5.js → cursor-adapter-cz6nm6mj.js} +3 -3
- package/dist/{db-nc1rd869.js → db-2y0mgstc.js} +3 -3
- package/dist/{db-2xbyenjr.js → db-nn0hcmx5.js} +6 -2
- package/dist/{devin-adapter-7egzmt0k.js → devin-adapter-zgqadvnm.js} +1 -1
- package/dist/{dispatcher-ewceb8ts.js → dispatcher-0a9wwjcx.js} +20 -20
- package/dist/{docker-njavd3fb.js → docker-fkgtr75t.js} +2 -2
- package/dist/{dsh-adapter-75xwh78t.js → dsh-adapter-3n3net33.js} +3 -3
- package/dist/{e2b-5r6a5jeb.js → e2b-7am6tmq6.js} +2 -2
- package/dist/{e2b-pw1nvtmh.js → e2b-thdcxtjd.js} +2 -2
- package/dist/{easter-egg-3d-xsc47jwf.js → easter-egg-3d-5cgynaja.js} +1 -1
- package/dist/{event-bus-101fenkh.js → event-bus-9vpk8gmq.js} +1 -1
- package/dist/{execute-gzmw1mnw.js → execute-t4e6m3rw.js} +1 -1
- package/dist/{external-channel-a5dsmxn4.js → external-channel-zhvt95n3.js} +2 -2
- package/dist/{grok-adapter-vj88xr23.js → grok-adapter-qnr1hp2y.js} +10 -10
- package/dist/handlers-dkcarkd0.js +39 -0
- package/dist/{heartbeat-t2m9r3wa.js → heartbeat-gz97r7ac.js} +23 -23
- package/dist/{hook-xa677f8r.js → hook-1mmdgjqs.js} +16 -9
- package/dist/{hook-8femx1bg.js → hook-x8rthca4.js} +6 -6
- package/dist/{http-xpmmq028.js → http-r8f85xr9.js} +274 -160
- package/dist/index-5jecgp3n.js +41 -0
- package/dist/index-aex0eba9.js +40 -0
- package/dist/{index-syztqdt6.js → index-g6tbb6ve.js} +1 -1
- package/dist/{index-cdzyfrsp.js → index-smxyk04s.js} +1 -1
- package/dist/{index-dg2tmj99.js → index-vzvfqzfj.js} +20 -20
- package/dist/{index-s7ckdajq.js → index-y7rbcrc1.js} +10 -10
- package/dist/{keepalive-3nr4st3r.js → keepalive-64c0f34c.js} +4 -4
- package/dist/lead-b1da0f97.js +78 -0
- package/dist/{maintenance-verwpe2c.js → maintenance-mnxpar12.js} +1 -1
- package/dist/{oauth-refresh-sweep-9q2t0y3q.js → oauth-refresh-sweep-36hs2v4h.js} +3 -3
- package/dist/{onboard-aytqedmq.js → onboard-xes8de61.js} +3 -3
- package/dist/{opencode-adapter-f4hryjny.js → opencode-adapter-3s7ax02e.js} +7 -7
- package/dist/{otel-impl-hckw4wt1.js → otel-impl-fzc57kcq.js} +2 -2
- package/dist/{pi-mono-adapter-hxby0ce1.js → pi-mono-adapter-ytp7gz8b.js} +22 -11
- package/dist/{pricing-refresh-5nvme3sm.js → pricing-refresh-1nek2w7b.js} +3 -3
- package/dist/{rbac-roles-vbpy21mg.js → rbac-roles-sqefb5hw.js} +2 -2
- package/dist/{rbac-roles-pd8c26m1.js → rbac-roles-ttyfaep2.js} +1 -1
- package/dist/{render-v2-7nqgvk2q.js → render-v2-vb527jwn.js} +3 -3
- package/dist/{resume-b6djcj3c.js → resume-jcdzg1bn.js} +10 -8
- package/dist/{runtime-txrxc9za.js → runtime-xe3aqy4x.js} +1 -1
- package/dist/{session-access-fc3f9y12.js → session-access-zsp4wsp0.js} +4 -4
- package/dist/{setup-n39szm6b.js → setup-1nb2zqd3.js} +1 -1
- package/dist/{virtual-modules-wyp2k6z3.js → virtual-modules-4r3zkc20.js} +2 -2
- package/dist/{work-objects-jem2fre0.js → work-objects-61w08fwr.js} +23 -23
- package/dist/worker-c1e68hpa.js +78 -0
- package/dist/{x-8ewt8gxn.js → x-y45mthwn.js} +2 -2
- package/openapi.json +491 -52
- package/package.json +3 -3
- package/src/apps/action-runner.ts +146 -0
- package/src/apps/catalog.generated.json +3 -2
- package/src/apps/definition.ts +152 -4
- package/src/apps/row-store.ts +23 -9
- package/src/apps/schema-migrate.ts +14 -6
- package/src/apps/script-types.ts +2 -1
- package/src/apps/sync.ts +20 -19
- package/src/be/db/tasks/read.ts +112 -0
- package/src/be/db.ts +16 -4
- package/src/be/fleet-benchmark-stats.ts +10 -5
- package/src/be/models-catalog.ts +4 -0
- package/src/be/onboarding.ts +6 -0
- package/src/be/scripts/typecheck.ts +13 -0
- package/src/be/swarm-config-guard.ts +25 -1
- package/src/be/telemetry-enable.ts +1 -1
- package/src/commands/provider-credentials.ts +12 -2
- package/src/commands/runner.ts +3 -0
- package/src/heartbeat/heartbeat.ts +2 -2
- package/src/hooks/codex-hook.ts +6 -2
- package/src/hooks/hook.ts +7 -0
- package/src/hooks/pr-body-guard.ts +38 -11
- package/src/hooks/pr-room-guard.ts +88 -0
- package/src/http/agents.ts +1 -0
- package/src/http/apps.ts +99 -183
- package/src/http/fleet-benchmark.ts +79 -30
- package/src/http/global-config.ts +7 -0
- package/src/http/ranking.ts +24 -40
- package/src/http/status.ts +49 -43
- package/src/http/tasks.ts +49 -1
- package/src/http/telemetry-ui.ts +1 -1
- package/src/http/used-by.ts +55 -19
- package/src/http/users.ts +2 -2
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +15 -0
- package/src/providers/reasoning-effort.ts +2 -1
- package/src/providers/types.ts +2 -0
- package/src/ranking/telemetry-ranking.ts +5 -33
- package/src/scripts-runtime/sdk-allowlist.ts +7 -0
- package/src/scripts-runtime/types/stdlib.d.ts +31 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +31 -0
- package/src/server.ts +4 -0
- package/src/telemetry-context.ts +19 -1
- package/src/telemetry-contract/EVENTS.md +42 -8
- package/src/telemetry-contract/SOURCE.json +6 -5
- package/src/telemetry-contract/catalog.json +225 -5
- package/src/telemetry-contract/ranking/fixtures/valid/no-claim.json +10 -0
- package/src/telemetry-contract/ranking/listing.v1.schema.json +2 -2
- package/src/telemetry-contract/types.gen.ts +225 -27
- package/src/telemetry-identity.ts +47 -9
- package/src/telemetry-snapshot.ts +9 -1
- package/src/telemetry-ui-events.ts +9 -2
- package/src/telemetry.ts +29 -6
- package/src/tests/app-action-run-tool.test.ts +178 -0
- package/src/tests/app-row-tools.test.ts +385 -0
- package/src/tests/apps-json-column.test.ts +611 -0
- package/src/tests/apps-script-types.test.ts +2 -0
- package/src/tests/apps-sync-engine.test.ts +42 -0
- package/src/tests/check-pr-body.test.ts +0 -23
- package/src/tests/credential-check.test.ts +38 -0
- package/src/tests/db-task-read-contract.test.ts +4 -0
- package/src/tests/db-task-write-contract.test.ts +2 -0
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/fleet-benchmark.test.ts +122 -2
- package/src/tests/http-users.test.ts +62 -1
- package/src/tests/model-catalog-table.test.ts +63 -0
- package/src/tests/model-profiles-api.test.ts +19 -7
- package/src/tests/models-catalog.test.ts +50 -0
- package/src/tests/onboarding.test.ts +21 -0
- package/src/tests/org-website.test.ts +62 -0
- package/src/tests/pi-mono-adapter.test.ts +23 -0
- package/src/tests/pr-body-leaks.test.ts +4 -0
- package/src/tests/pr-room-guard.test.ts +242 -0
- package/src/tests/ranking-listing.test.ts +8 -23
- package/src/tests/ranking-route.test.ts +49 -47
- package/src/tests/reasoning-effort.test.ts +39 -3
- package/src/tests/runner-harness-telemetry-env.test.ts +62 -1
- package/src/tests/status.test.ts +158 -9
- package/src/tests/swarm-tool-result-gate.test.ts +2 -4
- package/src/tests/task-lineage.test.ts +180 -0
- package/src/tests/telemetry-context.test.ts +128 -2
- package/src/tests/telemetry-init.test.ts +62 -5
- package/src/tests/telemetry-ui-route.test.ts +123 -0
- package/src/tests/tool-annotations.test.ts +3 -2
- package/src/tests/used-by-route.test.ts +144 -10
- package/src/tests/user-token-rest-auth.test.ts +38 -0
- package/src/tests/workflow-agent-task-retry.test.ts +130 -4
- package/src/tests/workflow-wait-recovery.test.ts +116 -4
- package/src/tools/app-action-run.ts +63 -0
- package/src/tools/app-rows.ts +239 -0
- package/src/tools/model-catalog/index.ts +1 -1
- package/src/tools/tool-config.ts +8 -0
- package/src/tools/utils.ts +1 -1
- package/src/tools/workflows/retry-workflow-run.ts +1 -1
- package/src/types.ts +17 -0
- package/src/utils/company-profile.ts +24 -0
- package/src/utils/org-website.ts +58 -21
- package/src/workflows/recovery.ts +10 -22
- package/src/workflows/resume.ts +16 -0
- package/src/workflows/task-step-routing.ts +47 -7
- package/templates/skills/apps/SKILL.md +44 -4
- package/templates/skills/apps/content.md +44 -4
- package/dist/handlers-zbs90bny.js +0 -39
- package/dist/index-8pwc1jc8.js +0 -40
- package/dist/index-nxa6me0q.js +0 -41
- package/dist/lead-td8h2sfn.js +0 -78
- package/dist/worker-wr6xgfr2.js +0 -78
- package/src/tests/check-ranking-entry.test.ts +0 -146
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desplega.ai/agent-swarm",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.2",
|
|
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",
|
|
@@ -200,7 +200,7 @@
|
|
|
200
200
|
"dependencies": {
|
|
201
201
|
"@agentclientprotocol/sdk": "1.9.0",
|
|
202
202
|
"@ai-sdk/openai": "^3.0.41",
|
|
203
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.
|
|
203
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.303",
|
|
204
204
|
"@anthropic-ai/sdk": "^0.135.0",
|
|
205
205
|
"@asteasolutions/zod-to-openapi": "^8.0.0",
|
|
206
206
|
"@aws-sdk/client-bedrock": "3.1048.0",
|
|
@@ -209,7 +209,7 @@
|
|
|
209
209
|
"@chat-adapter/teams": "4.41.1",
|
|
210
210
|
"@chat-adapter/telegram": "4.41.1",
|
|
211
211
|
"@cursor/sdk": "1.0.38",
|
|
212
|
-
"@desplega.ai/agent-fs-just-bash": "0.18.
|
|
212
|
+
"@desplega.ai/agent-fs-just-bash": "0.18.5",
|
|
213
213
|
"@desplega.ai/business-use": "^0.4.2",
|
|
214
214
|
"@earendil-works/pi-agent-core": "^1.3.0",
|
|
215
215
|
"@earendil-works/pi-ai": "^1.3.0",
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { normalizeAssetKey } from "../assets/key";
|
|
2
|
+
import { getLeadAgent } from "../be/db";
|
|
3
|
+
import { getScriptById } from "../be/scripts/db";
|
|
4
|
+
import { getSavedScriptOwnerAgentId, runSavedScriptAsAgent } from "../be/scripts/run-saved";
|
|
5
|
+
import { resolveTemplate } from "../prompts/resolver";
|
|
6
|
+
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
7
|
+
import { scrubObject } from "../utils/secret-scrubber";
|
|
8
|
+
import type { AppActionCaller } from "./action-caller";
|
|
9
|
+
import type { AppRecord } from "./store";
|
|
10
|
+
import { runAppSync } from "./sync";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One `error` string for a sync result, or undefined when every pass succeeded.
|
|
14
|
+
* Each failed pair is named so a multi-pair sync says which one broke; the pass
|
|
15
|
+
* list in `result.passes` carries the rest.
|
|
16
|
+
*/
|
|
17
|
+
function syncPassError(sync: Awaited<ReturnType<typeof runAppSync>>): string | undefined {
|
|
18
|
+
const failed = sync.passes.filter((pass) => pass.error !== undefined);
|
|
19
|
+
if (failed.length === 0) return undefined;
|
|
20
|
+
return failed.map((pass) => `${pass.model}.${pass.source}: ${pass.error}`).join("; ");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runAppAction({
|
|
24
|
+
app,
|
|
25
|
+
name,
|
|
26
|
+
input = {},
|
|
27
|
+
caller,
|
|
28
|
+
actor,
|
|
29
|
+
}: {
|
|
30
|
+
app: AppRecord;
|
|
31
|
+
name: string;
|
|
32
|
+
input?: Record<string, unknown>;
|
|
33
|
+
caller: AppActionCaller;
|
|
34
|
+
actor: string;
|
|
35
|
+
}) {
|
|
36
|
+
const actions = app.definition.actions;
|
|
37
|
+
if (!actions || !Object.hasOwn(actions, name)) {
|
|
38
|
+
return { status: 404 as const, body: { error: "app action not found" } };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const action = actions[name]!;
|
|
42
|
+
|
|
43
|
+
if (action.kind === "script") {
|
|
44
|
+
const script = await getScriptById(action.scriptId);
|
|
45
|
+
if (!script) {
|
|
46
|
+
return {
|
|
47
|
+
status: 400 as const,
|
|
48
|
+
body: {
|
|
49
|
+
error: "invalid app action",
|
|
50
|
+
issues: [
|
|
51
|
+
{
|
|
52
|
+
path: `actions.${name}.scriptId`,
|
|
53
|
+
message: `script "${action.scriptId}" no longer exists`,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Script actions use the owner's bindings. Definition validation checks
|
|
61
|
+
// which scripts an agent can wire into an app.
|
|
62
|
+
const runAsAgentId = getSavedScriptOwnerAgentId(script);
|
|
63
|
+
if (!runAsAgentId) {
|
|
64
|
+
return {
|
|
65
|
+
status: 400 as const,
|
|
66
|
+
body: { error: "agentId is required: this script has no owning agent to run as" },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const output = await runSavedScriptAsAgent({
|
|
71
|
+
script,
|
|
72
|
+
input: { ...action.args, ...input, app: { id: app.id }, caller },
|
|
73
|
+
agentId: runAsAgentId,
|
|
74
|
+
});
|
|
75
|
+
const ok = output.exitCode === 0 && !output.error && !output.runtimeError;
|
|
76
|
+
const error = ok
|
|
77
|
+
? undefined
|
|
78
|
+
: output.runtimeError
|
|
79
|
+
? `${output.runtimeError.name}: ${output.runtimeError.message}`
|
|
80
|
+
: (output.error ?? `Script exited with code ${output.exitCode}`);
|
|
81
|
+
return {
|
|
82
|
+
status: 200 as const,
|
|
83
|
+
body: scrubObject({
|
|
84
|
+
ok,
|
|
85
|
+
result: output.result,
|
|
86
|
+
stdout: output.stdout,
|
|
87
|
+
...(error === undefined ? {} : { error }),
|
|
88
|
+
durationMs: output.durationMs,
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (action.kind === "sync") {
|
|
94
|
+
const startedAt = Date.now();
|
|
95
|
+
const sync = await runAppSync({
|
|
96
|
+
appId: app.id,
|
|
97
|
+
...(action.model === undefined ? {} : { model: action.model }),
|
|
98
|
+
...(action.source === undefined ? {} : { source: action.source }),
|
|
99
|
+
invokedBy: actor,
|
|
100
|
+
});
|
|
101
|
+
const durationMs = Date.now() - startedAt;
|
|
102
|
+
if (sync.issues && sync.issues.length > 0) {
|
|
103
|
+
return { status: 400 as const, body: { error: "invalid sync action", issues: sync.issues } };
|
|
104
|
+
}
|
|
105
|
+
// Deliberately the SCRIPT-action response shape, minus `taskId`:
|
|
106
|
+
// app-surface.tsx branches on `taskId` first, so omitting it gives the
|
|
107
|
+
// dashboard running -> ok/error + refetchAll() with zero UI changes.
|
|
108
|
+
const error = syncPassError(sync);
|
|
109
|
+
return {
|
|
110
|
+
status: 200 as const,
|
|
111
|
+
body: scrubObject({
|
|
112
|
+
ok: sync.ok,
|
|
113
|
+
result: { passes: sync.passes },
|
|
114
|
+
...(error === undefined ? {} : { error }),
|
|
115
|
+
durationMs,
|
|
116
|
+
}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const lead = action.agentId ? null : await getLeadAgent();
|
|
121
|
+
const taskPrompt = resolveTemplate("task.app.action", {
|
|
122
|
+
prompt: action.prompt,
|
|
123
|
+
app_id: app.id,
|
|
124
|
+
action_name: name,
|
|
125
|
+
input_json: JSON.stringify(input),
|
|
126
|
+
caller_json: JSON.stringify(caller),
|
|
127
|
+
});
|
|
128
|
+
const task = await createTaskWithSiblingAwareness(
|
|
129
|
+
taskPrompt.text,
|
|
130
|
+
{
|
|
131
|
+
source: "api",
|
|
132
|
+
agentId: action.agentId ?? lead?.id,
|
|
133
|
+
routingReason: action.agentId ? "human_pinned" : lead ? "skill" : undefined,
|
|
134
|
+
routingSource: action.agentId || lead ? "engine_default" : undefined,
|
|
135
|
+
// App-spawned tasks group under the app's asset namespace so a
|
|
136
|
+
// swarm-tasks source can pull them back via config.assetKey.
|
|
137
|
+
key: normalizeAssetKey(`shared/app:${app.id}/action:${name}/`),
|
|
138
|
+
...(actor.startsWith("user:") ? { requestedByUserId: actor.slice("user:".length) } : {}),
|
|
139
|
+
},
|
|
140
|
+
{ origin: "app" },
|
|
141
|
+
);
|
|
142
|
+
return {
|
|
143
|
+
status: 200 as const,
|
|
144
|
+
body: { ok: true as const, taskId: task.id, status: task.status },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -1243,7 +1243,7 @@
|
|
|
1243
1243
|
}
|
|
1244
1244
|
},
|
|
1245
1245
|
"Table": {
|
|
1246
|
-
"description": "Data table. Bind `data`/`loading`/`error` to a named query (`/queries/<name>/...`). `rowActions` chains receive `{ \"$row\": \"<col>\" }` (or `{ \"$row\": \"\" }` for the whole row) and `{ \"$rowIndex\": true }`. Destructive row actions always confirm via a dialog; override the copy with `confirm: { title, description, confirmLabel }`. `search` (case-insensitive substring across every listed column) and `filters` (per-column equality, `null`/`\"\"` disables one; an array means \"one of\", which is what a multiple `Chips` writes, and an empty array disables it) narrow the polled rows client-side — bind them to a `SearchInput` / `Select` via `/ui/<id>/value`, or pin a constant (e.g. `\"filters\": { \"pinned\": true }`). Columns take `pinned: \"left\"|\"right\"` to survive horizontal scroll; `pagination` pages large row sets (auto past 200 rows); `density: \"compact\"` tightens row height. Column kinds: `status` (lifecycle values like pending/in_progress/completed/failed get the dashboard status icon, anything else a `tones` pill), `user` / `agent` (avatar + name), `relative` (smart time with the exact time on hover), `duration` (ms → 1m 23s), `progress` (bar, `max` default 100), `copy` (value + copy button), `switch` (runs the column's `onChange` chain with `$row` set to the new value; read-only without it), `link` (http(s) URL, opens in a new tab). Columns also take `align`, `truncate: \"middle\"` (ids, paths), a header `icon` and `tooltip` (a row field shown on hover). `onRowClick` runs a chain with `$row` when a row is clicked — pair it with `app.navigate` + a Drawer and bind `selectedId` to the drawer param to highlight the open row. `rowId` names the id field (default `id`). Below 768px the table renders as a phone list (`mobile: \"list\"`, fields picked by `mobileFields: { title, subtitle?, meta?, status? }`); `mobile: \"scroll\"` keeps the grid. `emptyState: { icon?, title, body? }` replaces the empty message. Row actions take `icon` and `placement: \"menu\"` (collected into a More menu).",
|
|
1246
|
+
"description": "Data table. Bind `data`/`loading`/`error` to a named query (`/queries/<name>/...`). `rowActions` chains receive `{ \"$row\": \"<col>\" }` (or `{ \"$row\": \"\" }` for the whole row) and `{ \"$rowIndex\": true }`. Destructive row actions always confirm via a dialog; override the copy with `confirm: { title, description, confirmLabel }`. `search` (case-insensitive substring across every listed column) and `filters` (per-column equality, `null`/`\"\"` disables one; an array means \"one of\", which is what a multiple `Chips` writes, and an empty array disables it) narrow the polled rows client-side — bind them to a `SearchInput` / `Select` via `/ui/<id>/value`, or pin a constant (e.g. `\"filters\": { \"pinned\": true }`). Columns take `pinned: \"left\"|\"right\"` to survive horizontal scroll; `pagination` pages large row sets (auto past 200 rows); `density: \"compact\"` tightens row height. Column kinds: `status` (lifecycle values like pending/in_progress/completed/failed get the dashboard status icon, anything else a `tones` pill), `user` / `agent` (avatar + name), `relative` (smart time with the exact time on hover), `duration` (ms → 1m 23s), `progress` (bar, `max` default 100), `copy` (value + copy button), `switch` (runs the column's `onChange` chain with `$row` set to the new value; read-only without it), `link` (http(s) URL, opens in a new tab), `json` (compact single-line `JSON.stringify` preview, truncated). Columns also take `align`, `truncate: \"middle\"` (ids, paths), a header `icon` and `tooltip` (a row field shown on hover). `onRowClick` runs a chain with `$row` when a row is clicked — pair it with `app.navigate` + a Drawer and bind `selectedId` to the drawer param to highlight the open row. `rowId` names the id field (default `id`). Below 768px the table renders as a phone list (`mobile: \"list\"`, fields picked by `mobileFields: { title, subtitle?, meta?, status? }`); `mobile: \"scroll\"` keeps the grid. `emptyState: { icon?, title, body? }` replaces the empty message. Row actions take `icon` and `placement: \"menu\"` (collected into a More menu).",
|
|
1247
1247
|
"props": {
|
|
1248
1248
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
1249
1249
|
"type": "object",
|
|
@@ -1287,7 +1287,8 @@
|
|
|
1287
1287
|
"progress",
|
|
1288
1288
|
"copy",
|
|
1289
1289
|
"switch",
|
|
1290
|
-
"link"
|
|
1290
|
+
"link",
|
|
1291
|
+
"json"
|
|
1291
1292
|
]
|
|
1292
1293
|
},
|
|
1293
1294
|
"tones": {
|
package/src/apps/definition.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as z from "zod";
|
|
|
2
2
|
import { listScriptConnections } from "../be/script-connections";
|
|
3
3
|
import { getScriptById } from "../be/scripts/db";
|
|
4
4
|
import { getSavedScriptOwnerAgentId } from "../be/scripts/run-saved";
|
|
5
|
+
import { APPS_JSON_COLUMN_MAX_BYTES_BOUNDS } from "../be/swarm-config-guard";
|
|
5
6
|
import { checkQuickJSCompat, quickJSCompatWarnings } from "../scripts-runtime/quickjs-compat";
|
|
6
7
|
import catalog from "./catalog.generated.json";
|
|
7
8
|
import {
|
|
@@ -16,7 +17,13 @@ export const AppNameSchema = z.string().regex(/^[a-z][a-zA-Z0-9_]{0,39}$/, {
|
|
|
16
17
|
message: "must start with a lowercase letter and contain only letters, numbers, or underscores",
|
|
17
18
|
});
|
|
18
19
|
|
|
19
|
-
export const ColumnKindSchema = z.enum(["string", "number", "boolean", "date", "enum"]);
|
|
20
|
+
export const ColumnKindSchema = z.enum(["string", "number", "boolean", "date", "enum", "json"]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Column kinds allowed where values must stay scalar (user-config fields and
|
|
24
|
+
* element props) — `json` is model-column only.
|
|
25
|
+
*/
|
|
26
|
+
export const ScalarColumnKindSchema = ColumnKindSchema.exclude(["json"]);
|
|
20
27
|
|
|
21
28
|
const ISO_8601_PREFIX = /^\d{4}-\d{2}-\d{2}(?:T.*)?$/;
|
|
22
29
|
|
|
@@ -24,6 +31,77 @@ export function isIso8601Date(value: string): boolean {
|
|
|
24
31
|
return ISO_8601_PREFIX.test(value) && !Number.isNaN(Date.parse(value));
|
|
25
32
|
}
|
|
26
33
|
|
|
34
|
+
/** Default serialized-size cap for json column values AND defaults: 256 KB. */
|
|
35
|
+
export const DEFAULT_JSON_COLUMN_MAX_BYTES = 256 * 1024;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The serialized-size cap for json column values and defaults, read on every
|
|
39
|
+
* call — never at module load — so a stored `APPS_JSON_COLUMN_MAX_BYTES`
|
|
40
|
+
* override, injected over the process env on config reload, applies without
|
|
41
|
+
* a restart. Mirrors the retention knobs: a non-digit or out-of-range value
|
|
42
|
+
* falls back to the default, and the config API rejects writes outside the
|
|
43
|
+
* same `APPS_JSON_COLUMN_MAX_BYTES_BOUNDS`.
|
|
44
|
+
*/
|
|
45
|
+
export function jsonColumnMaxBytes(env: NodeJS.ProcessEnv = process.env): number {
|
|
46
|
+
const raw = env.APPS_JSON_COLUMN_MAX_BYTES?.trim();
|
|
47
|
+
if (!raw || !/^\d+$/.test(raw)) return DEFAULT_JSON_COLUMN_MAX_BYTES;
|
|
48
|
+
const value = Number(raw);
|
|
49
|
+
const { min, max } = APPS_JSON_COLUMN_MAX_BYTES_BOUNDS;
|
|
50
|
+
return Number.isSafeInteger(value) && value >= min && value <= max
|
|
51
|
+
? value
|
|
52
|
+
: DEFAULT_JSON_COLUMN_MAX_BYTES;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The cap as a message label: whole KB when it divides evenly (`256KB`), else bytes. */
|
|
56
|
+
export function jsonColumnSizeLabel(bytes: number = jsonColumnMaxBytes()): string {
|
|
57
|
+
return bytes % 1024 === 0 ? `${bytes / 1024}KB` : `${bytes} bytes`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type JsonColumnValueIssue = "unserializable" | "non-finite" | "oversized";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Why a value cannot live in a json column — not JSON-serializable, carrying a
|
|
64
|
+
* non-finite number anywhere inside it, or over `jsonColumnMaxBytes()` once
|
|
65
|
+
* serialized — or `null` when it fits. The single implementation of the cap:
|
|
66
|
+
* it serializes exactly once, flagging non-finite numbers on that same walk
|
|
67
|
+
* (a lenient parser turns `1e400` into `Infinity`, which `JSON.stringify`
|
|
68
|
+
* would quietly store as `null`), and each caller maps the reason to its own
|
|
69
|
+
* message.
|
|
70
|
+
*/
|
|
71
|
+
export function jsonColumnValueIssue(value: unknown): JsonColumnValueIssue | null {
|
|
72
|
+
let nonFinite = false;
|
|
73
|
+
let serialized: string | undefined;
|
|
74
|
+
try {
|
|
75
|
+
serialized = JSON.stringify(value, (_key, entry: unknown) => {
|
|
76
|
+
if (typeof entry === "number" && !Number.isFinite(entry)) nonFinite = true;
|
|
77
|
+
return entry;
|
|
78
|
+
});
|
|
79
|
+
} catch {
|
|
80
|
+
return "unserializable"; // circular reference / BigInt / throwing toJSON
|
|
81
|
+
}
|
|
82
|
+
if (serialized === undefined) return "unserializable";
|
|
83
|
+
if (nonFinite) return "non-finite";
|
|
84
|
+
return Buffer.byteLength(serialized, "utf8") > jsonColumnMaxBytes() ? "oversized" : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The one per-kind check for a NON-null column value, shared by the row store,
|
|
89
|
+
* schema migrations and the sync engine so a value the store accepts is a
|
|
90
|
+
* value a migration directive or a projection accepts too. Null policy
|
|
91
|
+
* (required / hidden / nulling a projection) stays with each caller.
|
|
92
|
+
*/
|
|
93
|
+
export function isValidColumnValue(
|
|
94
|
+
column: Pick<ColumnDef, "kind" | "enum">,
|
|
95
|
+
value: unknown,
|
|
96
|
+
): boolean {
|
|
97
|
+
if (column.kind === "string") return typeof value === "string";
|
|
98
|
+
if (column.kind === "number") return typeof value === "number" && Number.isFinite(value);
|
|
99
|
+
if (column.kind === "boolean") return typeof value === "boolean";
|
|
100
|
+
if (column.kind === "date") return typeof value === "string" && isIso8601Date(value);
|
|
101
|
+
if (column.kind === "json") return jsonColumnValueIssue(value) === null;
|
|
102
|
+
return typeof value === "string" && Boolean(column.enum?.includes(value));
|
|
103
|
+
}
|
|
104
|
+
|
|
27
105
|
export const SourceTransformSchema = z.enum(["slug", "lower", "upper", "cents", "date-parse"]);
|
|
28
106
|
|
|
29
107
|
/** Column kind each transform may target — the sync engine projects into it. */
|
|
@@ -49,11 +127,23 @@ const ColumnDefSchema = z
|
|
|
49
127
|
required: z.boolean().optional(),
|
|
50
128
|
enum: z.array(z.string()).optional(),
|
|
51
129
|
index: z.boolean().optional(),
|
|
52
|
-
default: z
|
|
130
|
+
default: z
|
|
131
|
+
.union([
|
|
132
|
+
z.string(),
|
|
133
|
+
z.number(),
|
|
134
|
+
z.boolean(),
|
|
135
|
+
z.record(z.string(), z.unknown()),
|
|
136
|
+
z.array(z.unknown()),
|
|
137
|
+
])
|
|
138
|
+
.optional(),
|
|
53
139
|
hidden: z.boolean().optional(),
|
|
54
140
|
source: ColumnSourceBindingSchema.optional(),
|
|
55
141
|
})
|
|
56
142
|
.superRefine((column, ctx) => {
|
|
143
|
+
if (column.kind === "json" && column.index) {
|
|
144
|
+
ctx.addIssue({ code: "custom", path: ["index"], message: "json columns cannot be indexed" });
|
|
145
|
+
}
|
|
146
|
+
|
|
57
147
|
if (column.kind === "enum") {
|
|
58
148
|
if (!column.enum || column.enum.length === 0) {
|
|
59
149
|
ctx.addIssue({ code: "custom", path: ["enum"], message: "enum values are required" });
|
|
@@ -71,6 +161,24 @@ const ColumnDefSchema = z
|
|
|
71
161
|
}
|
|
72
162
|
|
|
73
163
|
if (column.default === undefined) return;
|
|
164
|
+
if (column.kind === "json") {
|
|
165
|
+
// Structural checks only; the size cap is write-time (`jsonColumnDefaultSizeIssues`).
|
|
166
|
+
const issue = jsonColumnValueIssue(column.default);
|
|
167
|
+
if (issue === "unserializable") {
|
|
168
|
+
ctx.addIssue({
|
|
169
|
+
code: "custom",
|
|
170
|
+
path: ["default"],
|
|
171
|
+
message: "json column default must be JSON-serializable",
|
|
172
|
+
});
|
|
173
|
+
} else if (issue === "non-finite") {
|
|
174
|
+
ctx.addIssue({
|
|
175
|
+
code: "custom",
|
|
176
|
+
path: ["default"],
|
|
177
|
+
message: "json column default contains a non-finite number",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
74
182
|
const valid =
|
|
75
183
|
(column.kind === "string" && typeof column.default === "string") ||
|
|
76
184
|
(column.kind === "number" &&
|
|
@@ -94,7 +202,7 @@ const ColumnDefSchema = z
|
|
|
94
202
|
|
|
95
203
|
const UserConfigFieldSchema = z
|
|
96
204
|
.object({
|
|
97
|
-
kind:
|
|
205
|
+
kind: ScalarColumnKindSchema,
|
|
98
206
|
default: z.union([z.string(), z.number(), z.boolean()]).optional(),
|
|
99
207
|
enum: z.array(z.string()).optional(),
|
|
100
208
|
label: z.string().optional(),
|
|
@@ -276,7 +384,7 @@ const AppPageSchema = z
|
|
|
276
384
|
|
|
277
385
|
const ElementPropDefSchema = z
|
|
278
386
|
.object({
|
|
279
|
-
kind:
|
|
387
|
+
kind: ScalarColumnKindSchema,
|
|
280
388
|
required: z.boolean().optional(),
|
|
281
389
|
enum: z.array(z.string()).min(1).optional(),
|
|
282
390
|
default: z.union([z.string(), z.number(), z.boolean()]).optional(),
|
|
@@ -439,6 +547,14 @@ export const AppDefinitionSchema = z
|
|
|
439
547
|
});
|
|
440
548
|
continue;
|
|
441
549
|
}
|
|
550
|
+
if (columnDefinition.kind === "json") {
|
|
551
|
+
ctx.addIssue({
|
|
552
|
+
code: "custom",
|
|
553
|
+
path: ["queries", queryName, "filter", column],
|
|
554
|
+
message: "json columns cannot be filtered",
|
|
555
|
+
});
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
442
558
|
if (typeof value === "object") continue;
|
|
443
559
|
const valid =
|
|
444
560
|
(columnDefinition.kind === "string" && typeof value === "string") ||
|
|
@@ -469,6 +585,12 @@ export const AppDefinitionSchema = z
|
|
|
469
585
|
path: ["queries", queryName, "sort", "column"],
|
|
470
586
|
message: `unknown or hidden column "${sortColumn}"`,
|
|
471
587
|
});
|
|
588
|
+
} else if (sortColumn && model.columns[sortColumn]?.kind === "json") {
|
|
589
|
+
ctx.addIssue({
|
|
590
|
+
code: "custom",
|
|
591
|
+
path: ["queries", queryName, "sort", "column"],
|
|
592
|
+
message: "json columns cannot be sorted",
|
|
593
|
+
});
|
|
472
594
|
}
|
|
473
595
|
}
|
|
474
596
|
});
|
|
@@ -836,6 +958,31 @@ function syncActionIssues(
|
|
|
836
958
|
return [{ path: `actions.${name}`, message: "no model declares a source to sync" }];
|
|
837
959
|
}
|
|
838
960
|
|
|
961
|
+
/**
|
|
962
|
+
* The serialized-size cap on json column defaults, applied on definition
|
|
963
|
+
* WRITES only. It lives outside `AppDefinitionSchema` on purpose: the schema
|
|
964
|
+
* also decodes persisted definitions (`decodeAppDefinition`, the row store's
|
|
965
|
+
* model lookup), and a cap lowered after an app was saved must not turn that
|
|
966
|
+
* app into one that "needs repair" on every read. Writing the definition
|
|
967
|
+
* again re-applies the current cap. An oversized default would otherwise be
|
|
968
|
+
* accepted only to fail every row create that relies on it.
|
|
969
|
+
*/
|
|
970
|
+
function jsonColumnDefaultSizeIssues(definition: AppDefinition): AppValidationIssue[] {
|
|
971
|
+
const issues: AppValidationIssue[] = [];
|
|
972
|
+
for (const [modelName, model] of Object.entries(definition.models)) {
|
|
973
|
+
for (const [columnName, column] of Object.entries(model.columns)) {
|
|
974
|
+
if (column.kind !== "json" || column.default === undefined) continue;
|
|
975
|
+
if (jsonColumnValueIssue(column.default) === "oversized") {
|
|
976
|
+
issues.push({
|
|
977
|
+
path: `models.${modelName}.columns.${columnName}.default`,
|
|
978
|
+
message: `json column default exceeds the ${jsonColumnSizeLabel()} size limit`,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return issues;
|
|
984
|
+
}
|
|
985
|
+
|
|
839
986
|
export async function parseAppDefinition(
|
|
840
987
|
input: unknown,
|
|
841
988
|
elementContext: AppDefinitionParseContext = {},
|
|
@@ -871,6 +1018,7 @@ export async function parseAppDefinition(
|
|
|
871
1018
|
if (!parsed.success) return { success: false, issues: appDefinitionIssues(parsed.error) };
|
|
872
1019
|
|
|
873
1020
|
const issues = [
|
|
1021
|
+
...jsonColumnDefaultSizeIssues(parsed.data),
|
|
874
1022
|
...Object.keys(parsed.data.pages).flatMap((pageName) =>
|
|
875
1023
|
validatePage(parsed.data, catalog, pageName),
|
|
876
1024
|
),
|
package/src/apps/row-store.ts
CHANGED
|
@@ -4,7 +4,9 @@ import {
|
|
|
4
4
|
AppDefinitionSchema,
|
|
5
5
|
type AppValidationIssue,
|
|
6
6
|
type ColumnDef,
|
|
7
|
-
|
|
7
|
+
isValidColumnValue,
|
|
8
|
+
jsonColumnSizeLabel,
|
|
9
|
+
jsonColumnValueIssue,
|
|
8
10
|
type ModelDef,
|
|
9
11
|
} from "./definition";
|
|
10
12
|
import { upgradeAppDefinition } from "./format-upgrades";
|
|
@@ -94,6 +96,7 @@ export function appIndexKey(model: string, column: string, value: unknown, rowId
|
|
|
94
96
|
|
|
95
97
|
function isIndexed(column: ColumnDef): boolean {
|
|
96
98
|
if (column.hidden === true) return false;
|
|
99
|
+
if (column.kind === "json") return false;
|
|
97
100
|
if (column.kind === "enum") return true;
|
|
98
101
|
return column.index === true && (column.kind === "string" || column.kind === "boolean");
|
|
99
102
|
}
|
|
@@ -109,13 +112,9 @@ function indexKeys(model: string, definition: ModelDef, row: AppRow): string[] {
|
|
|
109
112
|
return keys;
|
|
110
113
|
}
|
|
111
114
|
|
|
112
|
-
function validValue(column: ColumnDef, value: unknown): boolean {
|
|
115
|
+
export function validValue(column: ColumnDef, value: unknown): boolean {
|
|
113
116
|
if (value === null) return column.required !== true;
|
|
114
|
-
|
|
115
|
-
if (column.kind === "number") return typeof value === "number" && Number.isFinite(value);
|
|
116
|
-
if (column.kind === "boolean") return typeof value === "boolean";
|
|
117
|
-
if (column.kind === "date") return typeof value === "string" && isIso8601Date(value);
|
|
118
|
-
return typeof value === "string" && Boolean(column.enum?.includes(value));
|
|
117
|
+
return isValidColumnValue(column, value);
|
|
119
118
|
}
|
|
120
119
|
|
|
121
120
|
function joinKeyColumnNames(definition: ModelDef): Set<string> {
|
|
@@ -185,7 +184,20 @@ function prepareValues(
|
|
|
185
184
|
for (const [name, value] of Object.entries(prepared)) {
|
|
186
185
|
if (!Object.hasOwn(definition.columns, name)) continue;
|
|
187
186
|
const column = definition.columns[name]!;
|
|
188
|
-
if (
|
|
187
|
+
if (column.kind === "json" && value !== null) {
|
|
188
|
+
// Asked directly so the value is serialized once and the size cap gets its own message.
|
|
189
|
+
const issue = jsonColumnValueIssue(value);
|
|
190
|
+
if (issue === "unserializable") {
|
|
191
|
+
issues.push({ path: `values.${name}`, message: "must be a valid json value" });
|
|
192
|
+
} else if (issue === "non-finite") {
|
|
193
|
+
issues.push({ path: `values.${name}`, message: "json value contains a non-finite number" });
|
|
194
|
+
} else if (issue === "oversized") {
|
|
195
|
+
issues.push({
|
|
196
|
+
path: `values.${name}`,
|
|
197
|
+
message: `json value exceeds the ${jsonColumnSizeLabel()} size limit`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
} else if (!validValue(column, value)) {
|
|
189
201
|
issues.push({ path: `values.${name}`, message: `must be a valid ${column.kind} value` });
|
|
190
202
|
}
|
|
191
203
|
}
|
|
@@ -526,18 +538,20 @@ export async function rebuildAppColumnIndexUnlocked(
|
|
|
526
538
|
}
|
|
527
539
|
}
|
|
528
540
|
|
|
541
|
+
/** `matches` re-checks the row under the lock; a row that no longer matches is kept. */
|
|
529
542
|
export function deleteAppRow(
|
|
530
543
|
appId: string,
|
|
531
544
|
model: string,
|
|
532
545
|
_definition: ModelDef,
|
|
533
546
|
rowId: string,
|
|
547
|
+
matches?: (row: AppRow) => boolean,
|
|
534
548
|
): Promise<boolean> {
|
|
535
549
|
return withMutationLock(appId, model, async () => {
|
|
536
550
|
const currentDefinition = await currentModelDefinition(appId, model);
|
|
537
551
|
if (!currentDefinition) return false;
|
|
538
552
|
const namespace = appsNamespace(appId);
|
|
539
553
|
const row = await getAppRow(appId, model, rowId);
|
|
540
|
-
if (!row) return false;
|
|
554
|
+
if (!row || (matches && !matches(row))) return false;
|
|
541
555
|
for (const key of indexKeys(model, currentDefinition, row)) await deleteKv(namespace, key);
|
|
542
556
|
await deleteKv(namespace, rowKey(model, rowId));
|
|
543
557
|
return true;
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type AppValidationIssue,
|
|
9
9
|
type ColumnDef,
|
|
10
10
|
isIso8601Date,
|
|
11
|
+
isValidColumnValue,
|
|
11
12
|
type ModelDef,
|
|
12
13
|
parseAppDefinition,
|
|
13
14
|
type SourceDef,
|
|
@@ -23,7 +24,18 @@ import {
|
|
|
23
24
|
} from "./row-store";
|
|
24
25
|
import { invalidateChangedSyncStatus } from "./sync";
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Literal directive values (`set`, `map` targets, `else`): scalars, plus the
|
|
29
|
+
* objects and arrays a `json` column holds. The per-column kind check in
|
|
30
|
+
* `validateDirectiveValues` is what rejects a shape the target column cannot take.
|
|
31
|
+
*/
|
|
32
|
+
const MigrationValueSchema = z.union([
|
|
33
|
+
z.string(),
|
|
34
|
+
z.number(),
|
|
35
|
+
z.boolean(),
|
|
36
|
+
z.record(z.string(), z.unknown()),
|
|
37
|
+
z.array(z.unknown()),
|
|
38
|
+
]);
|
|
27
39
|
|
|
28
40
|
export const AppMigrationDirectiveSchema = z.union([
|
|
29
41
|
z.object({ set: MigrationValueSchema }).strict(),
|
|
@@ -368,11 +380,7 @@ function isPurgeDirective(
|
|
|
368
380
|
|
|
369
381
|
function columnAccepts(column: ColumnDef, value: unknown): boolean {
|
|
370
382
|
if (value === null) return column.hidden === true || column.required !== true;
|
|
371
|
-
|
|
372
|
-
if (column.kind === "number") return typeof value === "number" && Number.isFinite(value);
|
|
373
|
-
if (column.kind === "boolean") return typeof value === "boolean";
|
|
374
|
-
if (column.kind === "date") return typeof value === "string" && isIso8601Date(value);
|
|
375
|
-
return typeof value === "string" && Boolean(column.enum?.includes(value));
|
|
383
|
+
return isValidColumnValue(column, value);
|
|
376
384
|
}
|
|
377
385
|
|
|
378
386
|
function coerceValue(
|
package/src/apps/script-types.ts
CHANGED
|
@@ -69,6 +69,7 @@ function commentSafe(value: string): string {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
function tsTypeForColumn(column: Pick<ColumnDef, "kind" | "enum">): string {
|
|
72
|
+
if (column.kind === "json") return "unknown";
|
|
72
73
|
if (column.kind !== "enum") return column.kind === "date" ? "string" : column.kind;
|
|
73
74
|
return column.enum?.map((value) => JSON.stringify(value)).join(" | ") || "string";
|
|
74
75
|
}
|
|
@@ -206,7 +207,7 @@ function renderApp(app: AppRecord, namespace: string): string {
|
|
|
206
207
|
export namespace ${namespace} {
|
|
207
208
|
${renderedModels.map((model) => model.source).join("\n\n")}
|
|
208
209
|
|
|
209
|
-
/** Declared actions
|
|
210
|
+
/** Scripts can write rows. Declared actions are callable through REST or MCP only. */
|
|
210
211
|
export type ActionName = ${actionType};
|
|
211
212
|
|
|
212
213
|
/** Script action arguments include server-stamped app and caller context. */
|
package/src/apps/sync.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
type AppDefinition,
|
|
10
10
|
type AppValidationIssue,
|
|
11
11
|
type ColumnDef,
|
|
12
|
-
|
|
12
|
+
isValidColumnValue,
|
|
13
13
|
type ModelDef,
|
|
14
14
|
type SourceDef,
|
|
15
15
|
type SourceTransform,
|
|
@@ -484,19 +484,6 @@ function applyTransform(value: unknown, transform: SourceTransform | undefined):
|
|
|
484
484
|
}
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
-
/**
|
|
488
|
-
* Mirrors the row-store's per-kind value check. Projections are pre-validated
|
|
489
|
-
* here so one bad field nulls one column instead of throwing mid-reconcile and
|
|
490
|
-
* leaving the pass half-applied.
|
|
491
|
-
*/
|
|
492
|
-
function validForKind(column: ColumnDef, value: unknown): boolean {
|
|
493
|
-
if (column.kind === "string") return typeof value === "string";
|
|
494
|
-
if (column.kind === "number") return typeof value === "number" && Number.isFinite(value);
|
|
495
|
-
if (column.kind === "boolean") return typeof value === "boolean";
|
|
496
|
-
if (column.kind === "date") return typeof value === "string" && isIso8601Date(value);
|
|
497
|
-
return typeof value === "string" && Boolean(column.enum?.includes(value));
|
|
498
|
-
}
|
|
499
|
-
|
|
500
487
|
function projectRecord(args: {
|
|
501
488
|
record: SourceRecord;
|
|
502
489
|
bindings: Array<[string, ColumnDef]>;
|
|
@@ -518,7 +505,10 @@ function projectRecord(args: {
|
|
|
518
505
|
);
|
|
519
506
|
value = null;
|
|
520
507
|
}
|
|
521
|
-
|
|
508
|
+
// Projections are pre-validated with the row store's own kind check so one
|
|
509
|
+
// bad field nulls one column instead of throwing mid-reconcile and leaving
|
|
510
|
+
// the pass half-applied.
|
|
511
|
+
if (value !== null && !isValidColumnValue(column, value)) {
|
|
522
512
|
warn(
|
|
523
513
|
args.warnings,
|
|
524
514
|
`column "${name}": field "${binding.field}" produced a value that is not a valid ${column.kind}`,
|
|
@@ -530,9 +520,20 @@ function projectRecord(args: {
|
|
|
530
520
|
return values;
|
|
531
521
|
}
|
|
532
522
|
|
|
533
|
-
/**
|
|
534
|
-
|
|
535
|
-
|
|
523
|
+
/**
|
|
524
|
+
* A missing column and an explicit null are the same absence of a value. A
|
|
525
|
+
* json column compares structurally — the value decoded from storage is never
|
|
526
|
+
* the same object as a fresh projection — every other kind by identity.
|
|
527
|
+
*/
|
|
528
|
+
function sameValue(
|
|
529
|
+
column: ModelDef["columns"][string] | undefined,
|
|
530
|
+
existing: unknown,
|
|
531
|
+
projected: unknown,
|
|
532
|
+
): boolean {
|
|
533
|
+
const before = existing ?? null;
|
|
534
|
+
const after = projected ?? null;
|
|
535
|
+
if (column?.kind === "json") return Bun.deepEquals(before, after);
|
|
536
|
+
return Object.is(before, after);
|
|
536
537
|
}
|
|
537
538
|
|
|
538
539
|
// ---------------------------------------------------------------------------
|
|
@@ -701,7 +702,7 @@ function reconcile(args: {
|
|
|
701
702
|
return;
|
|
702
703
|
}
|
|
703
704
|
const differs = Object.entries(values).some(
|
|
704
|
-
([name, value]) => !sameValue(existing[name], value),
|
|
705
|
+
([name, value]) => !sameValue(modelDef.columns[name], existing[name], value),
|
|
705
706
|
);
|
|
706
707
|
const updated = await patchAppRowUnlocked(appId, model, modelDef, existing.id, values, {
|
|
707
708
|
allowSourceManaged: true,
|