@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/http/assets.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { getApp } from "../apps/store";
|
|
3
4
|
import { auditAssetKeys } from "../be/asset-key-audit";
|
|
4
5
|
import { AssetKeyAuthorizationError, authorizeAssetKeyWrite } from "../be/asset-key-auth";
|
|
5
6
|
import { resolveHttpAuditUserId } from "../be/audit-user";
|
|
@@ -11,11 +12,41 @@ import {
|
|
|
11
12
|
moveAssetKey,
|
|
12
13
|
upsertAssetKeyMapping,
|
|
13
14
|
} from "../be/db";
|
|
15
|
+
import { getScriptById } from "../be/scripts/db";
|
|
14
16
|
import { can, type RbacPrincipal, type RbacResource } from "../rbac";
|
|
15
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
type AssetEntityType,
|
|
19
|
+
AssetEntityTypeSchema,
|
|
20
|
+
AssetKeyMappingSchema,
|
|
21
|
+
AssetKeySchema,
|
|
22
|
+
AssetSummarySchema,
|
|
23
|
+
} from "../types";
|
|
16
24
|
import { getRequestAuth } from "../utils/request-auth-context";
|
|
17
25
|
import { route } from "./route-def";
|
|
18
|
-
import {
|
|
26
|
+
import { jsonError } from "./utils";
|
|
27
|
+
|
|
28
|
+
const AssetKeyAuditIssueSchema = z.object({
|
|
29
|
+
severity: z.enum(["fatal", "warning"]),
|
|
30
|
+
code: z.enum([
|
|
31
|
+
"missing-key",
|
|
32
|
+
"noncanonical-key",
|
|
33
|
+
"unknown-personal-user",
|
|
34
|
+
"missing-provider-mapping",
|
|
35
|
+
"provider-mapping-drift",
|
|
36
|
+
]),
|
|
37
|
+
entityType: AssetEntityTypeSchema,
|
|
38
|
+
entityId: z.string(),
|
|
39
|
+
message: z.string(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const AssetKeyAuditResultSchema = z.object({
|
|
43
|
+
ok: z.boolean(),
|
|
44
|
+
structuralValid: z.boolean(),
|
|
45
|
+
checked: z.number().int().min(0),
|
|
46
|
+
fatalCount: z.number().int().min(0),
|
|
47
|
+
warningCount: z.number().int().min(0),
|
|
48
|
+
issues: z.array(AssetKeyAuditIssueSchema),
|
|
49
|
+
});
|
|
19
50
|
|
|
20
51
|
const keyAuditRoute = route({
|
|
21
52
|
method: "get",
|
|
@@ -26,7 +57,7 @@ const keyAuditRoute = route({
|
|
|
26
57
|
"Operator-only check for structural key validity, personal-user references, and logical provider mapping drift. Repeated logical keys are valid and are never reported as conflicts.",
|
|
27
58
|
tags: ["Assets"],
|
|
28
59
|
responses: {
|
|
29
|
-
200: { description: "Asset namespace audit result" },
|
|
60
|
+
200: { description: "Asset namespace audit result", schema: AssetKeyAuditResultSchema },
|
|
30
61
|
403: { description: "Operator access required" },
|
|
31
62
|
},
|
|
32
63
|
});
|
|
@@ -41,11 +72,17 @@ const listAssetsRoute = route({
|
|
|
41
72
|
tags: ["Assets"],
|
|
42
73
|
query: z.object({
|
|
43
74
|
keyPrefix: AssetKeySchema.optional(),
|
|
44
|
-
types: z
|
|
75
|
+
types: z
|
|
76
|
+
.string()
|
|
77
|
+
.optional()
|
|
78
|
+
.describe("Comma-separated task,workflow,schedule,page,app,script,file list"),
|
|
45
79
|
limit: z.coerce.number().int().min(1).max(1000).optional(),
|
|
46
80
|
}),
|
|
47
81
|
responses: {
|
|
48
|
-
200: {
|
|
82
|
+
200: {
|
|
83
|
+
description: "Lightweight asset summary list",
|
|
84
|
+
schema: z.object({ assets: z.array(AssetSummarySchema), count: z.number().int() }),
|
|
85
|
+
},
|
|
49
86
|
400: { description: "Invalid entity type" },
|
|
50
87
|
},
|
|
51
88
|
});
|
|
@@ -66,7 +103,7 @@ const registerMappingRoute = route({
|
|
|
66
103
|
key: AssetKeySchema.optional(),
|
|
67
104
|
}),
|
|
68
105
|
responses: {
|
|
69
|
-
200: { description: "Mapping registered" },
|
|
106
|
+
200: { description: "Mapping registered", schema: AssetKeyMappingSchema },
|
|
70
107
|
400: { description: "Invalid provider tuple or namespace" },
|
|
71
108
|
403: { description: "Operator access required or personal namespace not authorized" },
|
|
72
109
|
},
|
|
@@ -81,6 +118,46 @@ function ensureOperator(req: IncomingMessage, res: ServerResponse): boolean {
|
|
|
81
118
|
return false;
|
|
82
119
|
}
|
|
83
120
|
|
|
121
|
+
const moveAssetBodySchema = z.object({ key: AssetKeySchema });
|
|
122
|
+
const moveAssetSuccessSchema = z.object({
|
|
123
|
+
entityType: AssetEntityTypeSchema,
|
|
124
|
+
id: z.string(),
|
|
125
|
+
key: AssetKeySchema,
|
|
126
|
+
});
|
|
127
|
+
const moveAssetResponses = {
|
|
128
|
+
200: { description: "Asset namespace updated", schema: moveAssetSuccessSchema },
|
|
129
|
+
400: { description: "Invalid namespace" },
|
|
130
|
+
403: { description: "Move not authorized" },
|
|
131
|
+
404: { description: "Asset not found" },
|
|
132
|
+
409: { description: "Moves blocked until audit warnings are repaired" },
|
|
133
|
+
} as const;
|
|
134
|
+
|
|
135
|
+
// Register permission-specific paths before the polymorphic fallback so RBAC
|
|
136
|
+
// admission can preserve each resource's mutation posture for scoped roles.
|
|
137
|
+
const moveAppAssetRoute = route({
|
|
138
|
+
method: "patch",
|
|
139
|
+
path: "/api/assets/app/{id}/key",
|
|
140
|
+
pattern: ["api", "assets", "app", null, "key"],
|
|
141
|
+
summary: "Move an app to another logical namespace",
|
|
142
|
+
tags: ["Assets"],
|
|
143
|
+
params: z.object({ id: z.string().min(1) }),
|
|
144
|
+
body: moveAssetBodySchema,
|
|
145
|
+
responses: moveAssetResponses,
|
|
146
|
+
rbac: { permission: "app.manage" },
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const moveScriptAssetRoute = route({
|
|
150
|
+
method: "patch",
|
|
151
|
+
path: "/api/assets/script/{id}/key",
|
|
152
|
+
pattern: ["api", "assets", "script", null, "key"],
|
|
153
|
+
summary: "Move a script to another logical namespace",
|
|
154
|
+
tags: ["Assets"],
|
|
155
|
+
params: z.object({ id: z.string().min(1) }),
|
|
156
|
+
body: moveAssetBodySchema,
|
|
157
|
+
responses: moveAssetResponses,
|
|
158
|
+
rbac: { permission: "script.global.write" },
|
|
159
|
+
});
|
|
160
|
+
|
|
84
161
|
const moveAssetRoute = route({
|
|
85
162
|
method: "patch",
|
|
86
163
|
path: "/api/assets/{entityType}/{id}/key",
|
|
@@ -90,20 +167,26 @@ const moveAssetRoute = route({
|
|
|
90
167
|
"Updates namespace metadata only. Provider-backed files keep the same provider key, org, and drive; no remote move occurs. Personal keys are labels, not a privacy guarantee.",
|
|
91
168
|
tags: ["Assets"],
|
|
92
169
|
params: z.object({ entityType: AssetEntityTypeSchema, id: z.string().min(1) }),
|
|
93
|
-
body:
|
|
94
|
-
responses:
|
|
95
|
-
200: { description: "Asset namespace updated" },
|
|
96
|
-
400: { description: "Invalid namespace" },
|
|
97
|
-
403: { description: "Move not authorized" },
|
|
98
|
-
404: { description: "Asset not found" },
|
|
99
|
-
409: { description: "Moves blocked until audit warnings are repaired" },
|
|
100
|
-
},
|
|
170
|
+
body: moveAssetBodySchema,
|
|
171
|
+
responses: moveAssetResponses,
|
|
101
172
|
rbac: {
|
|
102
173
|
ungated:
|
|
103
|
-
"preserves each entity's current mutation posture; task moves
|
|
174
|
+
"preserves each entity's current mutation posture; task moves use task.fs.mutate, app moves use app.manage, scripts preserve agent ownership/global-lead rules, file moves require operator authentication, and personal destinations require a matching trusted user",
|
|
104
175
|
},
|
|
105
176
|
});
|
|
106
177
|
|
|
178
|
+
function assetMovePrincipal(
|
|
179
|
+
req: IncomingMessage,
|
|
180
|
+
myAgentId: string | undefined,
|
|
181
|
+
): RbacPrincipal | null {
|
|
182
|
+
const auth = getRequestAuth(req);
|
|
183
|
+
if (auth?.kind === "operator") return { kind: "operator" };
|
|
184
|
+
if (auth?.kind === "user") return { kind: "user", userId: auth.userId };
|
|
185
|
+
if (!myAgentId) return null;
|
|
186
|
+
const agent = getAgentById(myAgentId);
|
|
187
|
+
return { kind: "agent", agentId: myAgentId, isLead: agent?.isLead ?? false };
|
|
188
|
+
}
|
|
189
|
+
|
|
107
190
|
function canMutateTaskNamespace(
|
|
108
191
|
task: { id: string; agentId: string | null; creatorAgentId?: string },
|
|
109
192
|
myAgentId: string | undefined,
|
|
@@ -115,20 +198,42 @@ function canMutateTaskNamespace(
|
|
|
115
198
|
agentId: task.agentId,
|
|
116
199
|
creatorAgentId: task.creatorAgentId,
|
|
117
200
|
};
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
if (auth?.kind === "operator") {
|
|
121
|
-
principal = { kind: "operator" };
|
|
122
|
-
} else if (auth?.kind === "user") {
|
|
123
|
-
principal = { kind: "user", userId: auth.userId };
|
|
124
|
-
} else {
|
|
125
|
-
if (!myAgentId) return false;
|
|
126
|
-
const agent = getAgentById(myAgentId);
|
|
127
|
-
principal = { kind: "agent", agentId: myAgentId, isLead: agent?.isLead ?? false };
|
|
128
|
-
}
|
|
201
|
+
const principal = assetMovePrincipal(req, myAgentId);
|
|
202
|
+
if (!principal) return false;
|
|
129
203
|
return can({ principal, verb: "task.fs.mutate", resource, source: "http" }).allow;
|
|
130
204
|
}
|
|
131
205
|
|
|
206
|
+
function canManageAppNamespace(
|
|
207
|
+
id: string,
|
|
208
|
+
req: IncomingMessage,
|
|
209
|
+
myAgentId: string | undefined,
|
|
210
|
+
): boolean {
|
|
211
|
+
const principal = assetMovePrincipal(req, myAgentId);
|
|
212
|
+
return (
|
|
213
|
+
!!principal &&
|
|
214
|
+
can({ principal, verb: "app.manage", resource: { kind: "app", appId: id }, source: "http" })
|
|
215
|
+
.allow
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function canManageScriptNamespace(
|
|
220
|
+
script: NonNullable<ReturnType<typeof getScriptById>>,
|
|
221
|
+
req: IncomingMessage,
|
|
222
|
+
myAgentId: string | undefined,
|
|
223
|
+
): boolean {
|
|
224
|
+
const principal = assetMovePrincipal(req, myAgentId);
|
|
225
|
+
if (!principal) return false;
|
|
226
|
+
if (principal.kind === "operator") return true;
|
|
227
|
+
if (principal.kind !== "agent") return false;
|
|
228
|
+
if (script.scope === "agent") return script.scopeId === principal.agentId;
|
|
229
|
+
return can({
|
|
230
|
+
principal,
|
|
231
|
+
verb: "script.global.write",
|
|
232
|
+
resource: { kind: "owned", scope: "global" },
|
|
233
|
+
source: "http",
|
|
234
|
+
}).allow;
|
|
235
|
+
}
|
|
236
|
+
|
|
132
237
|
export async function handleAssets(
|
|
133
238
|
req: IncomingMessage,
|
|
134
239
|
res: ServerResponse,
|
|
@@ -140,7 +245,7 @@ export async function handleAssets(
|
|
|
140
245
|
const parsed = await keyAuditRoute.parse(req, res, pathSegments, queryParams);
|
|
141
246
|
if (!parsed) return true;
|
|
142
247
|
if (!ensureOperator(req, res)) return true;
|
|
143
|
-
|
|
248
|
+
keyAuditRoute.respond(res, 200, auditAssetKeys(getDb()));
|
|
144
249
|
return true;
|
|
145
250
|
}
|
|
146
251
|
|
|
@@ -162,7 +267,7 @@ export async function handleAssets(
|
|
|
162
267
|
types: types.length > 0 ? types : undefined,
|
|
163
268
|
limit: parsed.query.limit,
|
|
164
269
|
});
|
|
165
|
-
|
|
270
|
+
listAssetsRoute.respond(res, 200, { assets, count: assets.length });
|
|
166
271
|
return true;
|
|
167
272
|
}
|
|
168
273
|
|
|
@@ -182,7 +287,7 @@ export async function handleAssets(
|
|
|
182
287
|
createdBy: actor ?? undefined,
|
|
183
288
|
updatedBy: actor ?? undefined,
|
|
184
289
|
});
|
|
185
|
-
|
|
290
|
+
registerMappingRoute.respond(res, 200, mapping);
|
|
186
291
|
} catch (error) {
|
|
187
292
|
if (error instanceof AssetKeyAuthorizationError) {
|
|
188
293
|
jsonError(res, error.message, error.statusCode);
|
|
@@ -193,7 +298,11 @@ export async function handleAssets(
|
|
|
193
298
|
return true;
|
|
194
299
|
}
|
|
195
300
|
|
|
196
|
-
if (
|
|
301
|
+
if (
|
|
302
|
+
moveAppAssetRoute.match(req.method, pathSegments) ||
|
|
303
|
+
moveScriptAssetRoute.match(req.method, pathSegments) ||
|
|
304
|
+
moveAssetRoute.match(req.method, pathSegments)
|
|
305
|
+
) {
|
|
197
306
|
const parsed = await moveAssetRoute.parse(req, res, pathSegments, queryParams);
|
|
198
307
|
if (!parsed) return true;
|
|
199
308
|
if (parsed.params.entityType === "file" && !ensureOperator(req, res)) return true;
|
|
@@ -208,6 +317,27 @@ export async function handleAssets(
|
|
|
208
317
|
return true;
|
|
209
318
|
}
|
|
210
319
|
}
|
|
320
|
+
if (parsed.params.entityType === "app") {
|
|
321
|
+
if (!getApp(parsed.params.id)) {
|
|
322
|
+
jsonError(res, "Asset not found", 404);
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
if (!canManageAppNamespace(parsed.params.id, req, myAgentId)) {
|
|
326
|
+
jsonError(res, "Not authorized to move this app namespace", 403);
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (parsed.params.entityType === "script") {
|
|
331
|
+
const script = getScriptById(parsed.params.id);
|
|
332
|
+
if (!script) {
|
|
333
|
+
jsonError(res, "Asset not found", 404);
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
if (!canManageScriptNamespace(script, req, myAgentId)) {
|
|
337
|
+
jsonError(res, "Not authorized to move this script namespace", 403);
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
211
341
|
|
|
212
342
|
try {
|
|
213
343
|
const actor = resolveHttpAuditUserId(req, myAgentId);
|
|
@@ -222,7 +352,11 @@ export async function handleAssets(
|
|
|
222
352
|
jsonError(res, "Asset not found", 404);
|
|
223
353
|
return true;
|
|
224
354
|
}
|
|
225
|
-
|
|
355
|
+
moveAssetRoute.respond(res, 200, {
|
|
356
|
+
entityType: parsed.params.entityType,
|
|
357
|
+
id: parsed.params.id,
|
|
358
|
+
key,
|
|
359
|
+
});
|
|
226
360
|
} catch (error) {
|
|
227
361
|
if (error instanceof AssetKeyAuthorizationError) {
|
|
228
362
|
jsonError(res, error.message, error.statusCode);
|
package/src/http/budgets.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import { BudgetRefusalNotificationSchema, BudgetSchema, BudgetScopeSchema } from "../types";
|
|
21
21
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
22
22
|
import { route } from "./route-def";
|
|
23
|
-
import {
|
|
23
|
+
import { jsonError } from "./utils";
|
|
24
24
|
|
|
25
25
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
26
26
|
|
|
@@ -128,7 +128,7 @@ export async function handleBudgets(
|
|
|
128
128
|
if (listBudgets.match(req.method, pathSegments)) {
|
|
129
129
|
const parsed = await listBudgets.parse(req, res, pathSegments, queryParams);
|
|
130
130
|
if (!parsed) return true;
|
|
131
|
-
|
|
131
|
+
listBudgets.respond(res, 200, { budgets: getBudgets() });
|
|
132
132
|
return true;
|
|
133
133
|
}
|
|
134
134
|
|
|
@@ -139,7 +139,9 @@ export async function handleBudgets(
|
|
|
139
139
|
const parsed = await listBudgetRefusals.parse(req, res, pathSegments, queryParams);
|
|
140
140
|
if (!parsed) return true;
|
|
141
141
|
const limit = parsed.query.limit ?? 50;
|
|
142
|
-
|
|
142
|
+
listBudgetRefusals.respond(res, 200, {
|
|
143
|
+
refusals: getRecentBudgetRefusalNotifications(limit),
|
|
144
|
+
});
|
|
143
145
|
return true;
|
|
144
146
|
}
|
|
145
147
|
|
|
@@ -161,7 +163,7 @@ export async function handleBudgets(
|
|
|
161
163
|
jsonError(res, "Budget not configured", 404);
|
|
162
164
|
return true;
|
|
163
165
|
}
|
|
164
|
-
|
|
166
|
+
getBudgetByScope.respond(res, 200, row);
|
|
165
167
|
return true;
|
|
166
168
|
}
|
|
167
169
|
|
|
@@ -184,7 +186,7 @@ export async function handleBudgets(
|
|
|
184
186
|
},
|
|
185
187
|
});
|
|
186
188
|
|
|
187
|
-
|
|
189
|
+
upsertBudgetRoute.respond(res, 200, updated);
|
|
188
190
|
return true;
|
|
189
191
|
}
|
|
190
192
|
|
|
@@ -23,12 +23,13 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
26
|
+
import { z } from "zod";
|
|
26
27
|
import { getKv } from "../be/db";
|
|
27
28
|
import { deriveCodexKeySuffix } from "../providers/codex-oauth/auth-json.js";
|
|
28
29
|
import { getValidCodexOAuth, loadAllCodexOAuthSlots } from "../providers/codex-oauth/storage.js";
|
|
29
30
|
import { getApiKey } from "../utils/api-key";
|
|
30
31
|
import { route } from "./route-def";
|
|
31
|
-
import { deriveApiBaseUrl
|
|
32
|
+
import { deriveApiBaseUrl } from "./utils";
|
|
32
33
|
|
|
33
34
|
/** ~weekly refresh cadence, comfortably inside OpenAI's ~8-day staleness window given the 10-day TTL. */
|
|
34
35
|
const KEEP_WARM_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -42,6 +43,20 @@ type SlotOutcome =
|
|
|
42
43
|
| { slot: number; outcome: "no-credentials" }
|
|
43
44
|
| { slot: number; keySuffix: string; outcome: "failed"; reason: string };
|
|
44
45
|
|
|
46
|
+
/** Mirrors the `SlotOutcome` union above — split so each branch has a single discriminant literal. */
|
|
47
|
+
const slotOutcomeSchema = z.discriminatedUnion("outcome", [
|
|
48
|
+
z.object({ slot: z.number(), keySuffix: z.string(), outcome: z.literal("warm") }),
|
|
49
|
+
z.object({ slot: z.number(), keySuffix: z.string(), outcome: z.literal("refreshed") }),
|
|
50
|
+
z.object({ slot: z.number(), keySuffix: z.string(), outcome: z.literal("skipped-benched") }),
|
|
51
|
+
z.object({ slot: z.number(), outcome: z.literal("no-credentials") }),
|
|
52
|
+
z.object({
|
|
53
|
+
slot: z.number(),
|
|
54
|
+
keySuffix: z.string(),
|
|
55
|
+
outcome: z.literal("failed"),
|
|
56
|
+
reason: z.string(),
|
|
57
|
+
}),
|
|
58
|
+
]);
|
|
59
|
+
|
|
45
60
|
const keepWarmRoute = route({
|
|
46
61
|
method: "post",
|
|
47
62
|
path: "/api/oauth/keep-warm/codex",
|
|
@@ -51,7 +66,10 @@ const keepWarmRoute = route({
|
|
|
51
66
|
"Enumerates codex_oauth_* slots and refreshes any older than ~7 days through the same locked getValidCodexOAuth path used at task time. Skips slots already benched by codex-auth-expiry-watch.",
|
|
52
67
|
tags: ["OAuth"],
|
|
53
68
|
responses: {
|
|
54
|
-
200: {
|
|
69
|
+
200: {
|
|
70
|
+
description: "Per-slot keep-warm outcomes",
|
|
71
|
+
schema: z.object({ results: z.array(slotOutcomeSchema) }),
|
|
72
|
+
},
|
|
55
73
|
},
|
|
56
74
|
});
|
|
57
75
|
|
|
@@ -102,6 +120,6 @@ export async function handleCodexOAuthKeepWarm(
|
|
|
102
120
|
await new Promise((resolve) => setTimeout(resolve, 100 + Math.random() * 200));
|
|
103
121
|
}
|
|
104
122
|
|
|
105
|
-
|
|
123
|
+
keepWarmRoute.respond(res, 200, { results });
|
|
106
124
|
return true;
|
|
107
125
|
}
|
package/src/http/config.ts
CHANGED
|
@@ -17,11 +17,12 @@ import {
|
|
|
17
17
|
validateConfigValue,
|
|
18
18
|
} from "../be/swarm-config-guard";
|
|
19
19
|
import { can, isRbacEnabled, type PermissionVerb } from "../rbac";
|
|
20
|
+
import { SwarmConfigSchema } from "../types";
|
|
20
21
|
import { getRequestAuth } from "../utils/request-auth-context";
|
|
21
22
|
import { registerVolatileSecret } from "../utils/secret-scrubber";
|
|
22
23
|
import { reloadGlobalConfigsAndIntegrations, scheduleIntegrationsReload } from "./core";
|
|
23
24
|
import { route } from "./route-def";
|
|
24
|
-
import {
|
|
25
|
+
import { jsonError } from "./utils";
|
|
25
26
|
|
|
26
27
|
const MAX_ENV_PRESENCE_KEYS = 200;
|
|
27
28
|
const SECRETS_FORCE_MASK_NOTE =
|
|
@@ -119,7 +120,13 @@ const getResolvedConfigRoute = route({
|
|
|
119
120
|
includeSecrets: z.enum(["true", "false"]).optional(),
|
|
120
121
|
}),
|
|
121
122
|
responses: {
|
|
122
|
-
200: {
|
|
123
|
+
200: {
|
|
124
|
+
description: "Resolved config entries",
|
|
125
|
+
schema: z.object({
|
|
126
|
+
configs: z.array(SwarmConfigSchema),
|
|
127
|
+
message: z.string().optional(),
|
|
128
|
+
}),
|
|
129
|
+
},
|
|
123
130
|
},
|
|
124
131
|
});
|
|
125
132
|
|
|
@@ -134,7 +141,10 @@ const envPresence = route({
|
|
|
134
141
|
keys: z.string().min(1),
|
|
135
142
|
}),
|
|
136
143
|
responses: {
|
|
137
|
-
200: {
|
|
144
|
+
200: {
|
|
145
|
+
description: "Map of key -> boolean (true iff set in process.env)",
|
|
146
|
+
schema: z.object({ presence: z.record(z.string(), z.boolean()) }),
|
|
147
|
+
},
|
|
138
148
|
400: { description: "Validation error" },
|
|
139
149
|
},
|
|
140
150
|
});
|
|
@@ -148,7 +158,15 @@ const reloadConfigRoute = route({
|
|
|
148
158
|
tags: ["Config"],
|
|
149
159
|
body: z.object({}).optional(),
|
|
150
160
|
responses: {
|
|
151
|
-
200: {
|
|
161
|
+
200: {
|
|
162
|
+
description: "Reload result",
|
|
163
|
+
schema: z.object({
|
|
164
|
+
success: z.literal(true),
|
|
165
|
+
configsLoaded: z.number(),
|
|
166
|
+
keysUpdated: z.array(z.string()),
|
|
167
|
+
integrationsReinitialized: z.array(z.string()),
|
|
168
|
+
}),
|
|
169
|
+
},
|
|
152
170
|
500: { description: "Reload failed" },
|
|
153
171
|
},
|
|
154
172
|
});
|
|
@@ -164,7 +182,10 @@ const getConfigById = route({
|
|
|
164
182
|
includeSecrets: z.enum(["true", "false"]).optional(),
|
|
165
183
|
}),
|
|
166
184
|
responses: {
|
|
167
|
-
200: {
|
|
185
|
+
200: {
|
|
186
|
+
description: "Config entry",
|
|
187
|
+
schema: SwarmConfigSchema.extend({ message: z.string().optional() }),
|
|
188
|
+
},
|
|
168
189
|
404: { description: "Config not found" },
|
|
169
190
|
},
|
|
170
191
|
});
|
|
@@ -181,7 +202,13 @@ const listConfig = route({
|
|
|
181
202
|
includeSecrets: z.enum(["true", "false"]).optional(),
|
|
182
203
|
}),
|
|
183
204
|
responses: {
|
|
184
|
-
200: {
|
|
205
|
+
200: {
|
|
206
|
+
description: "List of config entries",
|
|
207
|
+
schema: z.object({
|
|
208
|
+
configs: z.array(SwarmConfigSchema),
|
|
209
|
+
message: z.string().optional(),
|
|
210
|
+
}),
|
|
211
|
+
},
|
|
185
212
|
},
|
|
186
213
|
});
|
|
187
214
|
|
|
@@ -203,7 +230,7 @@ const upsertConfig = route({
|
|
|
203
230
|
description: z.string().nullish(),
|
|
204
231
|
}),
|
|
205
232
|
responses: {
|
|
206
|
-
200: { description: "Config entry upserted" },
|
|
233
|
+
200: { description: "Config entry upserted", schema: SwarmConfigSchema },
|
|
207
234
|
400: { description: "Validation error" },
|
|
208
235
|
},
|
|
209
236
|
});
|
|
@@ -218,7 +245,10 @@ const deleteConfig = route({
|
|
|
218
245
|
rbac: { permission: "config.delete.any" },
|
|
219
246
|
params: z.object({ id: z.string() }),
|
|
220
247
|
responses: {
|
|
221
|
-
200: {
|
|
248
|
+
200: {
|
|
249
|
+
description: "Config deleted",
|
|
250
|
+
schema: z.object({ success: z.literal(true) }),
|
|
251
|
+
},
|
|
222
252
|
404: { description: "Config not found" },
|
|
223
253
|
},
|
|
224
254
|
});
|
|
@@ -247,7 +277,7 @@ export async function handleConfig(
|
|
|
247
277
|
}
|
|
248
278
|
}
|
|
249
279
|
}
|
|
250
|
-
|
|
280
|
+
getResolvedConfigRoute.respond(res, 200, {
|
|
251
281
|
configs: result,
|
|
252
282
|
...(secretsNote ? { message: `Found ${result.length} config(s).${secretsNote}` } : {}),
|
|
253
283
|
});
|
|
@@ -269,7 +299,7 @@ export async function handleConfig(
|
|
|
269
299
|
for (const key of keys) {
|
|
270
300
|
presence[key] = process.env[key] !== undefined && process.env[key] !== "";
|
|
271
301
|
}
|
|
272
|
-
|
|
302
|
+
envPresence.respond(res, 200, { presence });
|
|
273
303
|
return true;
|
|
274
304
|
}
|
|
275
305
|
|
|
@@ -279,7 +309,7 @@ export async function handleConfig(
|
|
|
279
309
|
console.log(
|
|
280
310
|
`[reload-config] Loaded ${result.configsLoaded} config(s), re-initialized: ${result.integrationsReinitialized.join(", ") || "none"}`,
|
|
281
311
|
);
|
|
282
|
-
|
|
312
|
+
reloadConfigRoute.respond(res, 200, { success: true, ...result });
|
|
283
313
|
} catch (e) {
|
|
284
314
|
const message = e instanceof Error ? e.message : String(e);
|
|
285
315
|
console.error("[reload-config] Failed:", message);
|
|
@@ -302,7 +332,7 @@ export async function handleConfig(
|
|
|
302
332
|
if (effectiveIncludeSecrets && singleResult.isSecret && singleResult.value) {
|
|
303
333
|
registerVolatileSecret(singleResult.value, `config:${singleResult.key}`);
|
|
304
334
|
}
|
|
305
|
-
|
|
335
|
+
getConfigById.respond(res, 200, {
|
|
306
336
|
...singleResult,
|
|
307
337
|
...(secretsNote ? { message: `Found 1 config(s).${secretsNote}` } : {}),
|
|
308
338
|
});
|
|
@@ -328,7 +358,7 @@ export async function handleConfig(
|
|
|
328
358
|
}
|
|
329
359
|
}
|
|
330
360
|
}
|
|
331
|
-
|
|
361
|
+
listConfig.respond(res, 200, {
|
|
332
362
|
configs: listResult,
|
|
333
363
|
...(secretsNote ? { message: `Found ${listResult.length} config(s).${secretsNote}` } : {}),
|
|
334
364
|
});
|
|
@@ -380,8 +410,8 @@ export async function handleConfig(
|
|
|
380
410
|
if (scope === "global") {
|
|
381
411
|
scheduleIntegrationsReload();
|
|
382
412
|
}
|
|
383
|
-
const result = includeSecrets || !config.isSecret ? config : maskSecrets([config])[0]
|
|
384
|
-
|
|
413
|
+
const result = includeSecrets || !config.isSecret ? config : maskSecrets([config])[0]!;
|
|
414
|
+
upsertConfig.respond(res, 200, result);
|
|
385
415
|
} catch (_error) {
|
|
386
416
|
jsonError(res, "Failed to upsert config", 500);
|
|
387
417
|
}
|
|
@@ -405,7 +435,7 @@ export async function handleConfig(
|
|
|
405
435
|
if (existing.scope === "global") {
|
|
406
436
|
scheduleIntegrationsReload();
|
|
407
437
|
}
|
|
408
|
-
|
|
438
|
+
deleteConfig.respond(res, 200, { success: true });
|
|
409
439
|
return true;
|
|
410
440
|
}
|
|
411
441
|
|
package/src/http/context.ts
CHANGED
|
@@ -6,9 +6,36 @@ import {
|
|
|
6
6
|
getContextSummaryByTaskId,
|
|
7
7
|
getTaskById,
|
|
8
8
|
} from "../be/db";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
ContextFormulaSchema,
|
|
11
|
+
ContextSnapshotEventTypeSchema,
|
|
12
|
+
ContextSnapshotSchema,
|
|
13
|
+
} from "../types";
|
|
10
14
|
import { route } from "./route-def";
|
|
11
|
-
import {
|
|
15
|
+
import { jsonError } from "./utils";
|
|
16
|
+
|
|
17
|
+
// ─── Response Schemas ───────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const PostContextResponseSchema = z.object({
|
|
20
|
+
ok: z.literal(true),
|
|
21
|
+
snapshotId: z.uuid(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Mirrors the `ContextSummary` interface returned by
|
|
25
|
+
// `getContextSummaryByTaskId` in src/be/db.ts — not a named entity schema in
|
|
26
|
+
// types.ts, so defined locally.
|
|
27
|
+
const ContextSummarySchema = z.object({
|
|
28
|
+
compactionCount: z.number().int().min(0),
|
|
29
|
+
peakContextPercent: z.number().min(0).max(100).nullable(),
|
|
30
|
+
peakContextTokens: z.number().int().min(0).nullable(),
|
|
31
|
+
contextWindowSize: z.number().int().min(0).nullable(),
|
|
32
|
+
snapshotCount: z.number().int().min(0),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const GetContextResponseSchema = z.object({
|
|
36
|
+
snapshots: z.array(ContextSnapshotSchema),
|
|
37
|
+
summary: ContextSummarySchema,
|
|
38
|
+
});
|
|
12
39
|
|
|
13
40
|
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
14
41
|
|
|
@@ -34,7 +61,7 @@ const postContext = route({
|
|
|
34
61
|
contextFormula: ContextFormulaSchema.optional(),
|
|
35
62
|
}),
|
|
36
63
|
responses: {
|
|
37
|
-
200: { description: "Snapshot recorded" },
|
|
64
|
+
200: { description: "Snapshot recorded", schema: PostContextResponseSchema },
|
|
38
65
|
400: { description: "Validation error" },
|
|
39
66
|
404: { description: "Task not found" },
|
|
40
67
|
},
|
|
@@ -52,7 +79,7 @@ const getContext = route({
|
|
|
52
79
|
limit: z.coerce.number().int().min(1).max(500).default(100),
|
|
53
80
|
}),
|
|
54
81
|
responses: {
|
|
55
|
-
200: { description: "Context snapshot history" },
|
|
82
|
+
200: { description: "Context snapshot history", schema: GetContextResponseSchema },
|
|
56
83
|
404: { description: "Task not found" },
|
|
57
84
|
},
|
|
58
85
|
auth: { apiKey: true },
|
|
@@ -97,7 +124,7 @@ export async function handleContext(
|
|
|
97
124
|
contextFormula: parsed.body.contextFormula,
|
|
98
125
|
});
|
|
99
126
|
|
|
100
|
-
|
|
127
|
+
postContext.respond(res, 200, { ok: true, snapshotId: snapshot.id });
|
|
101
128
|
return true;
|
|
102
129
|
}
|
|
103
130
|
|
|
@@ -114,7 +141,7 @@ export async function handleContext(
|
|
|
114
141
|
const snapshots = getContextSnapshotsByTaskId(parsed.params.id, parsed.query.limit);
|
|
115
142
|
const summary = getContextSummaryByTaskId(parsed.params.id);
|
|
116
143
|
|
|
117
|
-
|
|
144
|
+
getContext.respond(res, 200, { snapshots, summary });
|
|
118
145
|
return true;
|
|
119
146
|
}
|
|
120
147
|
|