@desplega.ai/agent-swarm 2.5.1 → 2.5.3
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/dist/{acp-adapter-7g6bpwbk.js → acp-adapter-gr2fcyyj.js} +3 -3
- package/dist/{actions-w8xd014k.js → actions-wv766ghf.js} +18 -18
- package/dist/{amp-adapter-z4qvhacj.js → amp-adapter-speb906t.js} +3 -3
- package/dist/{app-nndcvftq.js → app-ag29n14f.js} +3 -3
- package/dist/{assistant-hjmqyscx.js → assistant-3x8bz3eg.js} +19 -19
- package/dist/{boot-reembed-r8rhf9h8.js → boot-reembed-5b3d2eea.js} +1 -1
- package/dist/{boot-reembed-vwcj2y5d.js → boot-reembed-jsmfrpzf.js} +1 -1
- package/dist/{boot-scrub-sweep-fr9q36ne.js → boot-scrub-sweep-s73gerxy.js} +2 -2
- package/dist/{boot-seeds-9jqwnegq.js → boot-seeds-5zmax2wr.js} +4 -4
- package/dist/{claude-adapter-2f8b85df.js → claude-adapter-hswdnm3t.js} +3 -3
- package/dist/{claude-managed-adapter-gt6pa35d.js → claude-managed-adapter-cvbzj1h6.js} +2 -2
- package/dist/{claude-managed-setup-c3hp36ph.js → claude-managed-setup-3k8m3hhc.js} +2 -2
- package/dist/{claude-sdk-session-05kat8rv.js → claude-sdk-session-fkbyk66x.js} +6905 -6855
- package/dist/{cli-y3zyaee5.js → cli-1fc85mvt.js} +5 -5
- package/dist/{cli-v2g93k4v.js → cli-210xd1f4.js} +4 -4
- package/dist/{cli-sn1vbtg2.js → cli-25j01dpj.js} +3 -3
- package/dist/{cli-n6kmqngw.js → cli-35gtk1p6.js} +1 -1
- package/dist/{cli-r9fy0ddc.js → cli-3ngdk5te.js} +1 -1
- package/dist/{cli-z63a269p.js → cli-40jsnrjm.js} +2 -2
- package/dist/{cli-ba8ne1es.js → cli-4rvr7zfx.js} +10 -0
- package/dist/{cli-qfm6my86.js → cli-502eqv93.js} +1 -1
- package/dist/{cli-xd4eqtr2.js → cli-6n8axjv1.js} +1 -1
- package/dist/{cli-hadv0725.js → cli-8s5wx3nz.js} +2 -2
- package/dist/{cli-z2dv25mf.js → cli-99xgw26r.js} +116 -5
- package/dist/{cli-jpxsrxcb.js → cli-9s05422w.js} +1 -1
- package/dist/{cli-kcsd1ez8.js → cli-aecdcr77.js} +343 -325
- package/dist/{cli-w9etnnxk.js → cli-bf8ma3f1.js} +120 -50
- package/dist/{cli-e9btqw4y.js → cli-ce0ak9ms.js} +18 -13
- package/dist/{cli-nj12tn2c.js → cli-e2ar4bp1.js} +71 -20
- package/dist/{cli-gwcf52gz.js → cli-e8jyq7d8.js} +11 -7
- package/dist/{cli-rngde2jv.js → cli-egjggehm.js} +1 -1
- package/dist/{cli-qfjm4gvt.js → cli-ewm3pwbq.js} +2 -2
- package/dist/{cli-9mm5ade4.js → cli-fsdprwrg.js} +1 -1
- package/dist/{cli-3ee5z0q0.js → cli-fzcn3d87.js} +1 -1
- package/dist/{cli-eym3jg7w.js → cli-gpsz7g88.js} +6 -6
- package/dist/{cli-cwyafhw1.js → cli-hcpj2xqz.js} +1 -1
- package/dist/{cli-j1eghqfm.js → cli-hhxjnjzs.js} +1 -1
- package/dist/{cli-pyy4c9ch.js → cli-jjznk3f7.js} +881 -232
- package/dist/{cli-q9pzwxvk.js → cli-kxawjrxg.js} +222 -37
- package/dist/{cli-rh5smt8t.js → cli-mz9afxxf.js} +14 -3
- package/dist/{cli-mz2ygeyq.js → cli-n45mr7zh.js} +2 -2
- package/dist/{cli-fes51ya2.js → cli-p0rzjkya.js} +2 -2
- package/dist/{cli-3qv8naq8.js → cli-p6zs9h4y.js} +827 -107
- package/dist/{cli-mq7kg1b1.js → cli-p9rafdrb.js} +41 -6
- package/dist/{cli-cah5detr.js → cli-qgf8emrn.js} +5 -5
- package/dist/{cli-2wba1ac9.js → cli-qzme5949.js} +26 -26
- package/dist/{cli-znvq0k3t.js → cli-s49hpn37.js} +2 -2
- package/dist/{cli-xc65x52q.js → cli-s53vp709.js} +1 -1
- package/dist/{cli-0jy85zz4.js → cli-svrj71pg.js} +18 -16
- package/dist/{cli-1dp1zj04.js → cli-vwn81jbs.js} +3 -3
- package/dist/{cli-ajqhxtnz.js → cli-w1zwctj2.js} +1 -1
- package/dist/{cli-22rmcjkg.js → cli-y561h5a9.js} +7 -6
- package/dist/{cli-2jg61wab.js → cli-z21whrrg.js} +15 -15
- package/dist/cli.js +19 -18
- package/dist/{codex-adapter-p4ptwar5.js → codex-adapter-n8ka66vf.js} +2 -2
- package/dist/{codex-session-runner-964c7dnk.js → codex-session-runner-fsebts37.js} +1 -1
- package/dist/{commands-mbvhbrcy.js → commands-t04rt2pc.js} +2 -2
- package/dist/{controller-gnq1k1mf.js → controller-rgna04r2.js} +2 -2
- package/dist/{controllers-connect-6dkm71tg.js → controllers-connect-bqm1rtft.js} +20 -20
- package/dist/{cursor-adapter-yq5xgfcg.js → cursor-adapter-swr2k95y.js} +1 -1
- package/dist/{db-88wcymxt.js → db-24ck09xy.js} +1 -1
- package/dist/{db-af9y8nkf.js → db-a4m7haw0.js} +3 -3
- package/dist/{dispatcher-606ydx5v.js → dispatcher-nwf9ss0q.js} +17 -17
- package/dist/{dsh-adapter-tvhc4k2d.js → dsh-adapter-kpdnhjst.js} +2 -2
- package/dist/{external-channel-y8q5z8z2.js → external-channel-j1e2p34p.js} +2 -2
- package/dist/{grok-adapter-39aha52t.js → grok-adapter-qtjz0whk.js} +4 -4
- package/dist/{handlers-rk9dqech.js → handlers-19xr6dnz.js} +20 -20
- package/dist/{heartbeat-jfe9zfjr.js → heartbeat-vtesda5d.js} +20 -20
- package/dist/{hook-b1mez1m8.js → hook-8wqaszda.js} +1 -1
- package/dist/{hook-ffevzed1.js → hook-b2hhj6rd.js} +1 -1
- package/dist/{http-d5b44f1r.js → http-mxcqhhcs.js} +3020 -2463
- package/dist/{index-5en23s5z.js → index-1r8wfpzs.js} +1 -1
- package/dist/{index-cbxkmfc5.js → index-9pn1pfkf.js} +1 -1
- package/dist/{index-mj46rk9k.js → index-kw2mmat7.js} +20 -20
- package/dist/{index-nq8hhv28.js → index-n1vbvta9.js} +19 -10
- package/dist/{index-6srwvd5b.js → index-p4xfz37z.js} +1 -1
- package/dist/{index-d2qeem97.js → index-spwxjgay.js} +17 -17
- package/dist/{index-4q3vj1yr.js → index-srg522m6.js} +17 -17
- package/dist/{keepalive-wsby1btq.js → keepalive-dsd8rtgx.js} +3 -3
- package/dist/{lead-paa141j6.js → lead-zyerk1x2.js} +30 -30
- package/dist/{maintenance-vnshcyv9.js → maintenance-jqmqwnet.js} +1 -1
- package/dist/{oauth-refresh-sweep-d6sqn5np.js → oauth-refresh-sweep-q2c3bnvm.js} +2 -2
- package/dist/{onboard-nr4xhpzy.js → onboard-w4yd7s8t.js} +2 -2
- package/dist/{opencode-adapter-e56086m1.js → opencode-adapter-9tppvxar.js} +2 -2
- package/dist/{otel-impl-ytn9wyv9.js → otel-impl-f36byzbd.js} +1 -1
- package/dist/{pricing-refresh-pva04zee.js → pricing-refresh-xvak5p4v.js} +3 -3
- package/dist/{rbac-roles-7rc1q3g4.js → rbac-roles-682k2mt5.js} +12 -2
- package/dist/rbac-roles-gfn30fb8.js +26 -0
- package/dist/{render-v2-pyfsg9zk.js → render-v2-hgytz8nn.js} +3 -3
- package/dist/{resume-phj8xhqr.js → resume-2dvq348g.js} +7 -5
- package/dist/{session-access-jzx2z9h5.js → session-access-7f21ekq9.js} +4 -4
- package/dist/{setup-rfg0f0sg.js → setup-epffxtbk.js} +1 -1
- package/dist/{work-objects-44z2yjg8.js → work-objects-xz1a5rqk.js} +20 -20
- package/dist/{worker-zsfqzxhm.js → worker-42t8emjt.js} +30 -30
- package/openapi.json +3163 -2114
- package/package.json +7 -6
- package/src/be/approval-config-question.ts +389 -0
- package/src/be/automation-preflight.ts +37 -3
- package/src/be/db/session-tree.ts +158 -13
- package/src/be/db/session-visibility.ts +31 -0
- package/src/be/db/skill-search.ts +87 -0
- package/src/be/db/tasks/read.ts +47 -7
- package/src/be/db/workflow-runs.ts +54 -7
- package/src/be/db.ts +27 -5
- package/src/be/migrations/233_script_run_session_tokens.sql +8 -0
- package/src/be/migrations/235_chat_relay_defer.sql +86 -0
- package/src/be/migrations/237_session_inbox_cleanup.sql +75 -0
- package/src/be/migrations/239_skills_fts.sql +40 -0
- package/src/be/migrations/240_workflow_run_parent_step_attempt.sql +37 -0
- package/src/be/migrations/241_skills_meta_fts.sql +39 -0
- package/src/be/rbac-roles.ts +138 -0
- package/src/be/scripts/run-credential.ts +64 -0
- package/src/be/scripts/run-saved.ts +3 -2
- package/src/be/swarm-config-guard.ts +23 -0
- package/src/be/users.ts +28 -4
- package/src/extensions/catalog.generated.json +4 -4
- package/src/http/all-routes.ts +1 -0
- package/src/http/approval-requests.ts +254 -43
- package/src/http/config.ts +22 -37
- package/src/http/index.ts +2 -0
- package/src/http/schedules.ts +26 -0
- package/src/http/scripts.ts +2 -2
- package/src/http/sessions.ts +118 -2
- package/src/http/user-roles.ts +173 -0
- package/src/http/users.ts +9 -0
- package/src/http/x.ts +2 -2
- package/src/integrations/chat-sdk/commands.ts +14 -1
- package/src/integrations/chat-sdk/inbound.ts +42 -6
- package/src/integrations/chat-sdk/outbound.ts +35 -10
- package/src/integrations/chat-sdk/reactions.ts +107 -0
- package/src/integrations/chat-sdk/relay.ts +75 -7
- package/src/integrations/chat-sdk/thread-buffer.ts +2 -0
- package/src/integrations/reaction-events.ts +20 -0
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/rbac/legacy-policy.ts +3 -0
- package/src/rbac/permissions.ts +8 -0
- package/src/scheduler/schedule-task.ts +44 -3
- package/src/scheduler/scheduler.ts +4 -4
- package/src/scripts-runtime/loader.ts +8 -1
- package/src/slack/ack.ts +2 -1
- package/src/slack/reaction-shortcode.ts +4 -7
- package/src/tasks/worker-follow-up.ts +30 -0
- package/src/tests/approval-config-question.test.ts +702 -0
- package/src/tests/approval-requests-notifications.test.ts +189 -0
- package/src/tests/chat-sdk-commands.test.ts +12 -0
- package/src/tests/chat-sdk-inbound.test.ts +183 -9
- package/src/tests/chat-sdk-outbound-files.test.ts +9 -2
- package/src/tests/chat-sdk-relay.test.ts +361 -10
- package/src/tests/chat-sdk-tools.test.ts +104 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/entrypoint-gitlab-credential-helper.test.ts +273 -0
- package/src/tests/extensions-example-comms-adherence-gate.test.ts +321 -12
- package/src/tests/http-users.test.ts +38 -0
- package/src/tests/list-endpoint-slimming.test.ts +33 -0
- package/src/tests/migration-237-session-inbox-cleanup.test.ts +136 -0
- package/src/tests/migration-240-workflow-run-parent-step-attempt.test.ts +55 -0
- package/src/tests/rbac-engine.test.ts +2 -0
- package/src/tests/rbac-roles.test.ts +10 -0
- package/src/tests/schedule-http-triage-tools.test.ts +44 -0
- package/src/tests/script-api-credential.test.ts +173 -0
- package/src/tests/script-connections.test.ts +10 -0
- package/src/tests/script-run-config-scope.test.ts +120 -0
- package/src/tests/script-run-token.test.ts +75 -0
- package/src/tests/scripts-http.test.ts +19 -0
- package/src/tests/sessions-tree.test.ts +410 -1
- package/src/tests/skill-search-long-content.test.ts +193 -0
- package/src/tests/skill-search.test.ts +173 -22
- package/src/tests/user-roles-http.test.ts +204 -0
- package/src/tests/workflow-hitl-dynamic-questions.test.ts +1 -1
- package/src/tests/workflow-sub-workflow.test.ts +538 -1
- package/src/tools/chat-reply.ts +2 -0
- package/src/tools/request-human-input.ts +27 -5
- package/src/tools/schedules/patch-schedule.ts +18 -0
- package/src/tools/schedules/update-schedule.ts +18 -0
- package/src/types.ts +23 -0
- package/src/workflows/approval-notifications.ts +221 -0
- package/src/workflows/checkpoint.ts +69 -35
- package/src/workflows/engine.ts +234 -87
- package/src/workflows/executors/base.ts +6 -0
- package/src/workflows/executors/human-in-the-loop.ts +36 -173
- package/src/workflows/executors/sub-workflow.ts +45 -16
- package/src/workflows/executors/swarm-script.ts +2 -2
- package/src/workflows/recovery.ts +63 -1
- package/src/workflows/resume.ts +60 -21
- package/src/workflows/retry-poller.ts +25 -4
- package/templates/extensions/comms-adherence-gate/README.md +38 -13
- package/templates/extensions/comms-adherence-gate/hooks.ts +272 -69
- package/templates/extensions/comms-adherence-gate/manifest.json +2 -2
- package/templates/schedules/weekly-harness-upgrade-check/content.md +1 -1
- package/templates/skills/code-quality/content.md +1 -1
- package/templates/skills/turso-interaction/content.md +1 -1
- package/templates/skills/workflow-iterate/content.md +8 -1
- package/dist/rbac-roles-ff1ffmec.js +0 -12
- /package/dist/{cli-stxdrqs2.js → cli-avdvgcay.js} +0 -0
- /package/dist/{pi-mono-adapter-tzps8f38.js → pi-mono-adapter-tqsge1ay.js} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desplega.ai/agent-swarm",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.3",
|
|
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",
|
|
@@ -113,6 +113,7 @@
|
|
|
113
113
|
"tsc:check": "bun check",
|
|
114
114
|
"tsc:check:tsc": "bun tsc --noEmit",
|
|
115
115
|
"check:db-boundary": "bash scripts/check-db-boundary.sh",
|
|
116
|
+
"check:codex-default-model": "bash scripts/check-codex-default-model.sh",
|
|
116
117
|
"check:test-spawn-sync": "bash scripts/check-test-spawn-sync.sh",
|
|
117
118
|
"check:dep-graph": "depcruise src packages/model-routing --config .dependency-cruiser.cjs",
|
|
118
119
|
"check:api-key-boundary": "bash scripts/check-api-key-boundary.sh",
|
|
@@ -179,8 +180,8 @@
|
|
|
179
180
|
"@desplega/model-catalog": "workspace:*",
|
|
180
181
|
"@desplega/model-routing": "workspace:*",
|
|
181
182
|
"@faker-js/faker": "^10.4.0",
|
|
182
|
-
"@opencode/plugin": "2.1.
|
|
183
|
-
"@opencode/sdk": "2.1.
|
|
183
|
+
"@opencode/plugin": "2.1.4",
|
|
184
|
+
"@opencode/sdk": "2.1.4",
|
|
184
185
|
"@oxlint/plugins": "1.78.0",
|
|
185
186
|
"@types/bun": "latest",
|
|
186
187
|
"dependency-cruiser": "^18.0.0",
|
|
@@ -200,8 +201,8 @@
|
|
|
200
201
|
"dependencies": {
|
|
201
202
|
"@agentclientprotocol/sdk": "2.0.1",
|
|
202
203
|
"@ai-sdk/openai": "^3.0.41",
|
|
203
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.
|
|
204
|
-
"@anthropic-ai/sdk": "^1.
|
|
204
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.309",
|
|
205
|
+
"@anthropic-ai/sdk": "^1.2.0",
|
|
205
206
|
"@asteasolutions/zod-to-openapi": "^8.0.0",
|
|
206
207
|
"@aws-sdk/client-bedrock": "3.1048.0",
|
|
207
208
|
"@chat-adapter/discord": "4.41.1",
|
|
@@ -218,7 +219,7 @@
|
|
|
218
219
|
"@jitl/quickjs-singlefile-mjs-release-sync": "0.32.0",
|
|
219
220
|
"@linear/sdk": "^77.0.0",
|
|
220
221
|
"@modelcontextprotocol/sdk": "1.32.1",
|
|
221
|
-
"@openai/codex": "0.
|
|
222
|
+
"@openai/codex": "0.164.0",
|
|
222
223
|
"@openfort/openfort-node": "^0.9.1",
|
|
223
224
|
"@opentelemetry/api": "^1.9.1",
|
|
224
225
|
"@opentelemetry/api-logs": "^0.218.0",
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HITL `config` questions: ask a person to set one swarm config key (a secret
|
|
3
|
+
* or a plain value) from the dashboard. The answer is written to
|
|
4
|
+
* `swarm_config` by the respond handler, and only a {@link ConfigResult} is
|
|
5
|
+
* ever stored on the request or sent onward, so a secret never reaches the
|
|
6
|
+
* agent, the workflow run, the follow-up task or any notification.
|
|
7
|
+
*/
|
|
8
|
+
import * as z from "zod";
|
|
9
|
+
import { getDbClient } from "./db";
|
|
10
|
+
import { upsertSwarmConfigWithPolicyMirror } from "./multi-runtime";
|
|
11
|
+
import {
|
|
12
|
+
internalConfigKeyError,
|
|
13
|
+
isApiOnlyConfigKey,
|
|
14
|
+
isInternalConfigKey,
|
|
15
|
+
isReservedConfigKey,
|
|
16
|
+
reservedKeyError,
|
|
17
|
+
validateConfigEntry,
|
|
18
|
+
} from "./swarm-config-guard";
|
|
19
|
+
|
|
20
|
+
export const ConfigQuestionSettingsSchema = z.object({
|
|
21
|
+
key: z
|
|
22
|
+
.string()
|
|
23
|
+
.min(1)
|
|
24
|
+
.max(255)
|
|
25
|
+
.regex(/^\S+$/, "must not contain whitespace")
|
|
26
|
+
.describe("The swarm config key to set, e.g. BITBUCKET_TOKEN"),
|
|
27
|
+
scope: z.enum(["global", "agent", "repo"]).describe("Config scope the value is written to"),
|
|
28
|
+
scopeId: z
|
|
29
|
+
.string()
|
|
30
|
+
.min(1)
|
|
31
|
+
.optional()
|
|
32
|
+
.describe(
|
|
33
|
+
"Agent or repo id for agent/repo scope. From request-human-input, agent scope defaults to the calling agent; pass it explicitly when asking on behalf of another agent.",
|
|
34
|
+
),
|
|
35
|
+
isSecret: z
|
|
36
|
+
.boolean()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe(
|
|
39
|
+
"Store encrypted and never show the value (default true). An existing secret row always stays secret.",
|
|
40
|
+
),
|
|
41
|
+
helpUrl: z
|
|
42
|
+
.string()
|
|
43
|
+
.url()
|
|
44
|
+
.refine((url) => /^https?:\/\//i.test(url), "must be an http(s) URL")
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("Link shown next to the field, e.g. where to create the token"),
|
|
47
|
+
options: z
|
|
48
|
+
.array(
|
|
49
|
+
z.object({
|
|
50
|
+
value: z.string(),
|
|
51
|
+
label: z.string(),
|
|
52
|
+
description: z.string().optional(),
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
55
|
+
.optional()
|
|
56
|
+
.describe("Allowed values for a non-secret key, rendered as a choice"),
|
|
57
|
+
defaultValue: z.string().optional().describe("Prefilled value for a non-secret key"),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export type ConfigQuestionSettings = z.infer<typeof ConfigQuestionSettingsSchema>;
|
|
61
|
+
|
|
62
|
+
/** Settings after create-time normalization: `isSecret` is always resolved. */
|
|
63
|
+
export type NormalizedConfigSettings = ConfigQuestionSettings & { isSecret: boolean };
|
|
64
|
+
|
|
65
|
+
/** What the dashboard sends for a config question (D6). */
|
|
66
|
+
export const ConfigAnswerSchema = z.discriminatedUnion("action", [
|
|
67
|
+
z.object({ action: z.literal("set"), value: z.string() }),
|
|
68
|
+
z.object({ action: z.literal("keep") }),
|
|
69
|
+
z.object({ action: z.literal("skip") }),
|
|
70
|
+
]);
|
|
71
|
+
export type ConfigAnswer = z.infer<typeof ConfigAnswerSchema>;
|
|
72
|
+
|
|
73
|
+
/** The only form of a config answer that is ever stored or sent (D7). */
|
|
74
|
+
export const ConfigResultSchema = z.object({
|
|
75
|
+
key: z.string(),
|
|
76
|
+
scope: z.enum(["global", "agent", "repo"]),
|
|
77
|
+
scopeId: z.string().nullable(),
|
|
78
|
+
isSecret: z.boolean(),
|
|
79
|
+
status: z.enum(["set", "kept", "skipped"]),
|
|
80
|
+
updatedAt: z.string().nullable(),
|
|
81
|
+
value: z.string().optional(),
|
|
82
|
+
});
|
|
83
|
+
export type ConfigResult = z.infer<typeof ConfigResultSchema>;
|
|
84
|
+
|
|
85
|
+
/** Live state of the key a config question targets (never persisted). */
|
|
86
|
+
export const ConfigStateSchema = z.object({
|
|
87
|
+
exists: z.boolean(),
|
|
88
|
+
isSecret: z.boolean(),
|
|
89
|
+
updatedAt: z.string().nullable(),
|
|
90
|
+
value: z.string().optional().describe("Current value; present only for non-secret rows"),
|
|
91
|
+
});
|
|
92
|
+
export type ConfigState = z.infer<typeof ConfigStateSchema>;
|
|
93
|
+
|
|
94
|
+
type ConfigQuestionInput = {
|
|
95
|
+
id: string;
|
|
96
|
+
type: string;
|
|
97
|
+
label?: string;
|
|
98
|
+
required?: boolean;
|
|
99
|
+
config?: ConfigQuestionSettings;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type NormalizedConfigQuestion<Q extends ConfigQuestionInput> = Omit<Q, "config"> & {
|
|
103
|
+
label: string;
|
|
104
|
+
required: boolean;
|
|
105
|
+
config: NormalizedConfigSettings;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
type ApproversLike = { policy: unknown };
|
|
109
|
+
|
|
110
|
+
interface ConfigRowMeta {
|
|
111
|
+
isSecret: boolean;
|
|
112
|
+
updatedAt: string;
|
|
113
|
+
envPath: string | null;
|
|
114
|
+
description: string | null;
|
|
115
|
+
value?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The exact `(scope, scopeId, key)` row, without decrypting anything. */
|
|
119
|
+
async function readConfigRow(
|
|
120
|
+
settings: Pick<ConfigQuestionSettings, "scope" | "scopeId" | "key">,
|
|
121
|
+
): Promise<ConfigRowMeta | null> {
|
|
122
|
+
const scopeId = settings.scope === "global" ? null : (settings.scopeId ?? null);
|
|
123
|
+
const row = await getDbClient().get<{
|
|
124
|
+
isSecret: number;
|
|
125
|
+
lastUpdatedAt: string;
|
|
126
|
+
envPath: string | null;
|
|
127
|
+
description: string | null;
|
|
128
|
+
plainValue: string | null;
|
|
129
|
+
}>(
|
|
130
|
+
`SELECT isSecret, lastUpdatedAt, envPath, description,
|
|
131
|
+
CASE WHEN isSecret = 1 THEN NULL ELSE value END AS plainValue
|
|
132
|
+
FROM swarm_config
|
|
133
|
+
WHERE scope = ? AND ${scopeId === null ? "scopeId IS NULL" : "scopeId = ?"} AND key = ?`,
|
|
134
|
+
scopeId === null ? [settings.scope, settings.key] : [settings.scope, scopeId, settings.key],
|
|
135
|
+
);
|
|
136
|
+
if (!row) return null;
|
|
137
|
+
// API-only keys are never readable over HTTP, so they count as secret here.
|
|
138
|
+
const isSecret = row.isSecret === 1 || isApiOnlyConfigKey(settings.key);
|
|
139
|
+
return {
|
|
140
|
+
isSecret,
|
|
141
|
+
updatedAt: row.lastUpdatedAt,
|
|
142
|
+
envPath: row.envPath,
|
|
143
|
+
description: row.description,
|
|
144
|
+
...(isSecret || row.plainValue === null ? {} : { value: row.plainValue }),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function readConfigState(settings: ConfigQuestionSettings): Promise<ConfigState> {
|
|
149
|
+
const row = await readConfigRow(settings);
|
|
150
|
+
if (!row) return { exists: false, isSecret: settings.isSecret !== false, updatedAt: null };
|
|
151
|
+
const isSecret = row.isSecret || settings.isSecret !== false;
|
|
152
|
+
return {
|
|
153
|
+
exists: true,
|
|
154
|
+
isSecret,
|
|
155
|
+
updatedAt: row.updatedAt,
|
|
156
|
+
...(!isSecret && row.value !== undefined ? { value: row.value } : {}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Create-time rules for one config question (D2, D3, D5). Defaults `label` to
|
|
162
|
+
* the key and `required` to true, resolves the effective `isSecret` against
|
|
163
|
+
* the live row, and rejects keys and shapes the config API would refuse.
|
|
164
|
+
*/
|
|
165
|
+
export async function normalizeConfigQuestion<Q extends ConfigQuestionInput>(
|
|
166
|
+
question: Q,
|
|
167
|
+
ctx: { approvers: ApproversLike; callerAgentId?: string },
|
|
168
|
+
): Promise<{ ok: true; question: NormalizedConfigQuestion<Q> } | { ok: false; error: string }> {
|
|
169
|
+
const fail = (reason: string) => ({
|
|
170
|
+
ok: false as const,
|
|
171
|
+
error: `config question "${question.id}": ${reason}`,
|
|
172
|
+
});
|
|
173
|
+
const parsed = ConfigQuestionSettingsSchema.safeParse(question.config);
|
|
174
|
+
if (!parsed.success) {
|
|
175
|
+
const issue = parsed.error.issues[0];
|
|
176
|
+
const path = issue?.path.length ? `config.${issue.path.map(String).join(".")}` : "config";
|
|
177
|
+
return fail(`${path}: ${issue?.message ?? "invalid"}`);
|
|
178
|
+
}
|
|
179
|
+
if (ctx.approvers.policy !== "any") {
|
|
180
|
+
return fail('a config question needs approvers.policy "any" (a config value has one author)');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const settings = { ...parsed.data };
|
|
184
|
+
if (settings.scope === "agent" && !settings.scopeId && ctx.callerAgentId) {
|
|
185
|
+
settings.scopeId = ctx.callerAgentId;
|
|
186
|
+
}
|
|
187
|
+
if (settings.scope === "global" && settings.scopeId) {
|
|
188
|
+
return fail("global scope must not have scopeId");
|
|
189
|
+
}
|
|
190
|
+
if (settings.scope !== "global" && !settings.scopeId) {
|
|
191
|
+
return fail(`${settings.scope} scope requires scopeId`);
|
|
192
|
+
}
|
|
193
|
+
if (isReservedConfigKey(settings.key)) return fail(reservedKeyError(settings.key).message);
|
|
194
|
+
if (isInternalConfigKey(settings.key)) return fail(internalConfigKeyError(settings.key).message);
|
|
195
|
+
|
|
196
|
+
const row = await readConfigRow(settings);
|
|
197
|
+
const isSecret =
|
|
198
|
+
settings.isSecret !== false || row?.isSecret === true || isApiOnlyConfigKey(settings.key);
|
|
199
|
+
if (isSecret && (settings.options || settings.defaultValue !== undefined)) {
|
|
200
|
+
return fail(
|
|
201
|
+
"options and defaultValue are only allowed when isSecret is false (and the key is not already stored as a secret)",
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (settings.options?.length === 0) return fail("options must not be empty");
|
|
205
|
+
if (
|
|
206
|
+
settings.options &&
|
|
207
|
+
settings.defaultValue !== undefined &&
|
|
208
|
+
!settings.options.some((option) => option.value === settings.defaultValue)
|
|
209
|
+
) {
|
|
210
|
+
return fail("defaultValue must be one of options");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const label = question.label?.trim() ? question.label : settings.key;
|
|
214
|
+
return {
|
|
215
|
+
ok: true,
|
|
216
|
+
question: {
|
|
217
|
+
...question,
|
|
218
|
+
label,
|
|
219
|
+
required: question.required ?? true,
|
|
220
|
+
config: { ...settings, isSecret },
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
type AnyQuestion = { id: string; type: string; label?: string; config?: ConfigQuestionSettings };
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Normalizes every config question in a list and checks that the others carry
|
|
229
|
+
* a label and no `config` block. Non-config questions pass through unchanged.
|
|
230
|
+
*/
|
|
231
|
+
export async function normalizeApprovalQuestions<Q extends AnyQuestion>(
|
|
232
|
+
questions: Q[],
|
|
233
|
+
ctx: { approvers: ApproversLike; callerAgentId?: string },
|
|
234
|
+
): Promise<
|
|
235
|
+
| { ok: true; questions: ((Q & { label: string }) | NormalizedConfigQuestion<Q>)[] }
|
|
236
|
+
| { ok: false; error: string }
|
|
237
|
+
> {
|
|
238
|
+
const out: ((Q & { label: string }) | NormalizedConfigQuestion<Q>)[] = [];
|
|
239
|
+
for (const question of questions) {
|
|
240
|
+
if (question.type !== "config") {
|
|
241
|
+
if (question.config !== undefined) {
|
|
242
|
+
return {
|
|
243
|
+
ok: false,
|
|
244
|
+
error: `question "${question.id}": the config block is only valid on type "config"`,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (typeof question.label !== "string") {
|
|
248
|
+
return { ok: false, error: `question "${question.id}": label is required` };
|
|
249
|
+
}
|
|
250
|
+
out.push({ ...question, label: question.label });
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const normalized = await normalizeConfigQuestion(question, ctx);
|
|
254
|
+
if (!normalized.ok) return normalized;
|
|
255
|
+
out.push(normalized.question);
|
|
256
|
+
}
|
|
257
|
+
return { ok: true, questions: out };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** A stored config question, as read back from `approval_requests.questions`. */
|
|
261
|
+
export type StoredConfigQuestion = {
|
|
262
|
+
id: string;
|
|
263
|
+
type: "config";
|
|
264
|
+
label: string;
|
|
265
|
+
required?: boolean;
|
|
266
|
+
config: NormalizedConfigSettings;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
export function isConfigQuestion(question: {
|
|
270
|
+
type: string;
|
|
271
|
+
config?: unknown;
|
|
272
|
+
}): question is StoredConfigQuestion {
|
|
273
|
+
return question.type === "config" && typeof question.config === "object" && !!question.config;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Validates one raw answer against D6 and the config API's rules. Errors name
|
|
278
|
+
* the question and the rule, never the value.
|
|
279
|
+
*/
|
|
280
|
+
export function parseConfigAnswer(
|
|
281
|
+
question: StoredConfigQuestion,
|
|
282
|
+
raw: unknown,
|
|
283
|
+
state: ConfigState,
|
|
284
|
+
): { ok: true; answer: ConfigAnswer } | { ok: false; error: string } {
|
|
285
|
+
const fail = (reason: string) => ({
|
|
286
|
+
ok: false as const,
|
|
287
|
+
error: `config question "${question.id}": ${reason}`,
|
|
288
|
+
});
|
|
289
|
+
const required = question.required !== false;
|
|
290
|
+
if (raw === undefined || raw === null) {
|
|
291
|
+
return required ? fail("an answer is required") : { ok: true, answer: { action: "skip" } };
|
|
292
|
+
}
|
|
293
|
+
const parsed = ConfigAnswerSchema.safeParse(raw);
|
|
294
|
+
if (!parsed.success) {
|
|
295
|
+
return fail(
|
|
296
|
+
'answer must be { action: "set", value } , { action: "keep" } or { action: "skip" }',
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
const answer = parsed.data;
|
|
300
|
+
if (answer.action === "keep") {
|
|
301
|
+
return state.exists ? { ok: true, answer } : fail("cannot keep a key that is not set");
|
|
302
|
+
}
|
|
303
|
+
if (answer.action === "skip") {
|
|
304
|
+
return required ? fail("a required question cannot be skipped") : { ok: true, answer };
|
|
305
|
+
}
|
|
306
|
+
if (answer.value.trim().length === 0) return fail("value must not be empty");
|
|
307
|
+
const { config } = question;
|
|
308
|
+
const isSecret = config.isSecret || state.isSecret;
|
|
309
|
+
if (!isSecret && config.options && !config.options.some((o) => o.value === answer.value)) {
|
|
310
|
+
return fail("value must be one of options");
|
|
311
|
+
}
|
|
312
|
+
const error = validateConfigEntry({
|
|
313
|
+
scope: config.scope,
|
|
314
|
+
scopeId: config.scopeId ?? null,
|
|
315
|
+
key: config.key,
|
|
316
|
+
value: answer.value,
|
|
317
|
+
});
|
|
318
|
+
if (error) return fail(isSecret ? `value rejected by ${config.key} validation` : error);
|
|
319
|
+
return { ok: true, answer };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function result(
|
|
323
|
+
question: StoredConfigQuestion,
|
|
324
|
+
status: ConfigResult["status"],
|
|
325
|
+
isSecret: boolean,
|
|
326
|
+
updatedAt: string | null,
|
|
327
|
+
value?: string,
|
|
328
|
+
): ConfigResult {
|
|
329
|
+
const { key, scope, scopeId } = question.config;
|
|
330
|
+
return {
|
|
331
|
+
key,
|
|
332
|
+
scope,
|
|
333
|
+
scopeId: scope === "global" ? null : (scopeId ?? null),
|
|
334
|
+
isSecret,
|
|
335
|
+
status,
|
|
336
|
+
updatedAt,
|
|
337
|
+
...(!isSecret && value !== undefined ? { value } : {}),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function skippedConfigResult(question: StoredConfigQuestion): ConfigResult {
|
|
342
|
+
return result(question, "skipped", question.config.isSecret, null);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** `keep`: nothing is written. Null when the row disappeared since validation. */
|
|
346
|
+
export async function keptConfigResult(
|
|
347
|
+
question: StoredConfigQuestion,
|
|
348
|
+
): Promise<ConfigResult | null> {
|
|
349
|
+
const row = await readConfigRow(question.config);
|
|
350
|
+
if (!row) return null;
|
|
351
|
+
const isSecret = question.config.isSecret || row.isSecret;
|
|
352
|
+
return result(question, "kept", isSecret, row.updatedAt, row.value);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* `set`: writes the value inside the caller's transaction. Carries the row's
|
|
357
|
+
* `envPath` and `description` forward (the upsert would null them) and keeps
|
|
358
|
+
* an existing secret secret. The caller runs the HTTP side effects after
|
|
359
|
+
* commit.
|
|
360
|
+
*/
|
|
361
|
+
export async function applyConfigAnswer(
|
|
362
|
+
question: StoredConfigQuestion,
|
|
363
|
+
value: string,
|
|
364
|
+
): Promise<ConfigResult> {
|
|
365
|
+
const { config } = question;
|
|
366
|
+
const existing = await readConfigRow(config);
|
|
367
|
+
const isSecret = config.isSecret || existing?.isSecret === true;
|
|
368
|
+
const written = await upsertSwarmConfigWithPolicyMirror({
|
|
369
|
+
scope: config.scope,
|
|
370
|
+
scopeId: config.scope === "global" ? null : (config.scopeId ?? null),
|
|
371
|
+
key: config.key,
|
|
372
|
+
value,
|
|
373
|
+
isSecret,
|
|
374
|
+
envPath: existing?.envPath ?? null,
|
|
375
|
+
description: existing?.description ?? null,
|
|
376
|
+
});
|
|
377
|
+
return result(question, "set", isSecret, written.lastUpdatedAt, value);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** One line for the hitl-follow-up task text. Never carries a secret. */
|
|
381
|
+
export function formatConfigResult(value: unknown): string | null {
|
|
382
|
+
const parsed = ConfigResultSchema.safeParse(value);
|
|
383
|
+
if (!parsed.success) return null;
|
|
384
|
+
const r = parsed.data;
|
|
385
|
+
const where = `${r.scope}${r.scopeId ? `:${r.scopeId}` : ""}`;
|
|
386
|
+
const updated = r.updatedAt ? `, updated ${r.updatedAt}` : "";
|
|
387
|
+
const shown = !r.isSecret && r.value !== undefined ? ` = ${r.value}` : "";
|
|
388
|
+
return `${r.key} (${where}, ${r.isSecret ? "secret" : "plain"}): ${r.status}${updated}${shown}`;
|
|
389
|
+
}
|
|
@@ -345,6 +345,29 @@ function utcDayBounds(now: Date): { start: string; end: string } {
|
|
|
345
345
|
return { start: start.toISOString(), end: new Date(start.getTime() + 86_400_000).toISOString() };
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
+
/**
|
|
349
|
+
* True when `runId` is the workflow's only setup-failure run so far this UTC
|
|
350
|
+
* day, top-level or child. Call it in the transaction that inserted `runId`:
|
|
351
|
+
* transactions serialize on the write lock, so of several concurrent failures
|
|
352
|
+
* exactly one sees no other and alerts.
|
|
353
|
+
*/
|
|
354
|
+
export async function isFirstWorkflowPreflightFailureToday(
|
|
355
|
+
workflowId: string,
|
|
356
|
+
runId: string,
|
|
357
|
+
now: Date = new Date(),
|
|
358
|
+
): Promise<boolean> {
|
|
359
|
+
const bounds = utcDayBounds(now);
|
|
360
|
+
const row = await getDbClient().get<{ found: number }>(
|
|
361
|
+
`SELECT 1 AS found FROM workflow_runs
|
|
362
|
+
WHERE workflowId = ? AND id != ? AND status = 'failed'
|
|
363
|
+
AND error LIKE 'needs_setup:%'
|
|
364
|
+
AND startedAt >= ? AND startedAt < ?
|
|
365
|
+
LIMIT 1`,
|
|
366
|
+
[workflowId, runId, bounds.start, bounds.end],
|
|
367
|
+
);
|
|
368
|
+
return row == null;
|
|
369
|
+
}
|
|
370
|
+
|
|
348
371
|
/** Record at most one copy of the same setup failure on a schedule per UTC day. */
|
|
349
372
|
export async function recordSchedulePreflightFailure(
|
|
350
373
|
scheduleId: string,
|
|
@@ -376,7 +399,11 @@ export async function recordSchedulePreflightFailure(
|
|
|
376
399
|
});
|
|
377
400
|
}
|
|
378
401
|
|
|
379
|
-
/**
|
|
402
|
+
/**
|
|
403
|
+
* Create and finish at most one failed top-level preflight run per workflow
|
|
404
|
+
* and UTC day. `recorded` is true when the caller should alert: this call
|
|
405
|
+
* inserted the run and no child failure run of the workflow came first today.
|
|
406
|
+
*/
|
|
380
407
|
export async function recordWorkflowPreflightFailure(input: {
|
|
381
408
|
workflowId: string;
|
|
382
409
|
triggerType: "schedule" | "manual" | "event" | "api";
|
|
@@ -388,9 +415,11 @@ export async function recordWorkflowPreflightFailure(input: {
|
|
|
388
415
|
const now = input.now ?? new Date();
|
|
389
416
|
const bounds = utcDayBounds(now);
|
|
390
417
|
return await getDbClient().transaction(async (tx) => {
|
|
418
|
+
// Child runs record their own linked failure (`startWorkflowExecution`);
|
|
419
|
+
// a top-level trigger never reuses one.
|
|
391
420
|
const existing = await tx.get<{ id: string }>(
|
|
392
421
|
`SELECT id FROM workflow_runs
|
|
393
|
-
WHERE workflowId = ? AND status = 'failed'
|
|
422
|
+
WHERE workflowId = ? AND status = 'failed' AND parentStepId IS NULL
|
|
394
423
|
AND error LIKE 'needs_setup:%'
|
|
395
424
|
AND startedAt >= ? AND startedAt < ?
|
|
396
425
|
ORDER BY startedAt ASC LIMIT 1`,
|
|
@@ -417,6 +446,11 @@ export async function recordWorkflowPreflightFailure(input: {
|
|
|
417
446
|
timestamp,
|
|
418
447
|
],
|
|
419
448
|
);
|
|
420
|
-
|
|
449
|
+
// Claimed in this transaction, which also sees every child failure run
|
|
450
|
+
// committed before it: concurrent failures alert exactly once a day.
|
|
451
|
+
return {
|
|
452
|
+
runId,
|
|
453
|
+
recorded: await isFirstWorkflowPreflightFailureToday(input.workflowId, runId, now),
|
|
454
|
+
};
|
|
421
455
|
});
|
|
422
456
|
}
|