@desplega.ai/agent-swarm 2.5.2 → 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-csh0zz3b.js → acp-adapter-gr2fcyyj.js} +3 -3
- package/dist/{actions-6xx079d1.js → actions-wv766ghf.js} +18 -18
- package/dist/{amp-adapter-8n6f0hz8.js → amp-adapter-speb906t.js} +3 -3
- package/dist/{app-ft6r6v23.js → app-ag29n14f.js} +3 -3
- package/dist/{assistant-vt2cyfzd.js → assistant-3x8bz3eg.js} +19 -19
- package/dist/{boot-reembed-r3e808ky.js → boot-reembed-5b3d2eea.js} +1 -1
- package/dist/{boot-reembed-tgrfy27t.js → boot-reembed-jsmfrpzf.js} +1 -1
- package/dist/{boot-scrub-sweep-masdwxkm.js → boot-scrub-sweep-s73gerxy.js} +2 -2
- package/dist/{boot-seeds-0n82qc7q.js → boot-seeds-5zmax2wr.js} +4 -4
- package/dist/{claude-adapter-s7j47k06.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-3hzrgrnk.js → claude-managed-setup-3k8m3hhc.js} +2 -2
- package/dist/{claude-sdk-session-0zrdzyx2.js → claude-sdk-session-fkbyk66x.js} +2 -2
- package/dist/{cli-d172svcw.js → cli-1fc85mvt.js} +4 -4
- package/dist/{cli-3e9vs93g.js → cli-210xd1f4.js} +4 -4
- package/dist/{cli-vaxw3bg0.js → cli-25j01dpj.js} +3 -3
- package/dist/{cli-n6kmqngw.js → cli-35gtk1p6.js} +1 -1
- package/dist/{cli-fgn7yg5r.js → cli-3ngdk5te.js} +1 -1
- package/dist/{cli-7ccf7akh.js → cli-40jsnrjm.js} +2 -2
- package/dist/{cli-b1tv22wr.js → cli-502eqv93.js} +1 -1
- package/dist/{cli-ydg6ep51.js → cli-6n8axjv1.js} +1 -1
- package/dist/{cli-f55b6ky0.js → cli-8s5wx3nz.js} +2 -2
- package/dist/{cli-86zpfje5.js → cli-99xgw26r.js} +1 -1
- package/dist/{cli-xw9q173h.js → cli-9s05422w.js} +1 -1
- package/dist/{cli-h5d22pfs.js → cli-aecdcr77.js} +119 -282
- package/dist/{cli-w9etnnxk.js → cli-bf8ma3f1.js} +120 -50
- package/dist/{cli-15xd0j7w.js → cli-ce0ak9ms.js} +11 -11
- package/dist/{cli-y3dzwyy3.js → cli-e2ar4bp1.js} +71 -20
- package/dist/{cli-b6tnjdvx.js → cli-e8jyq7d8.js} +1 -1
- package/dist/{cli-6y43v79h.js → cli-egjggehm.js} +1 -1
- package/dist/{cli-cf63dnxj.js → cli-ewm3pwbq.js} +2 -2
- package/dist/{cli-bpbcpd8b.js → cli-fsdprwrg.js} +1 -1
- package/dist/{cli-qrjv7t18.js → cli-fzcn3d87.js} +1 -1
- package/dist/{cli-14v1sxxy.js → cli-gpsz7g88.js} +6 -6
- package/dist/{cli-wadzapwb.js → cli-hcpj2xqz.js} +1 -1
- package/dist/{cli-jmcjbjkt.js → cli-hhxjnjzs.js} +1 -1
- package/dist/{cli-j3dwd4kz.js → cli-jjznk3f7.js} +718 -108
- package/dist/{cli-csh8fnbg.js → cli-kxawjrxg.js} +147 -28
- package/dist/{cli-mntxtkhn.js → cli-mz9afxxf.js} +2 -1
- package/dist/{cli-0vgy5wfw.js → cli-n45mr7zh.js} +2 -2
- package/dist/{cli-aaqnqpyw.js → cli-p0rzjkya.js} +2 -2
- package/dist/{cli-3qv8naq8.js → cli-p6zs9h4y.js} +827 -107
- package/dist/{cli-e91vshb1.js → cli-p9rafdrb.js} +4 -4
- package/dist/{cli-edtm741g.js → cli-qgf8emrn.js} +5 -5
- package/dist/{cli-ha22h0eg.js → cli-qzme5949.js} +26 -26
- package/dist/{cli-xzge46sj.js → cli-s49hpn37.js} +2 -2
- package/dist/{cli-b63d08rs.js → cli-s53vp709.js} +1 -1
- package/dist/{cli-cyfgxnvb.js → cli-svrj71pg.js} +18 -16
- package/dist/{cli-4qkg9968.js → cli-vwn81jbs.js} +3 -3
- package/dist/{cli-f3pfn4x3.js → cli-w1zwctj2.js} +1 -1
- package/dist/{cli-kcnwn85g.js → cli-y561h5a9.js} +4 -4
- package/dist/{cli-n2918sed.js → cli-z21whrrg.js} +15 -15
- package/dist/cli.js +16 -16
- package/dist/{codex-adapter-brh2tcxr.js → codex-adapter-n8ka66vf.js} +2 -2
- package/dist/{codex-session-runner-0dnj21qs.js → codex-session-runner-fsebts37.js} +1 -1
- package/dist/{commands-1cnvseqx.js → commands-t04rt2pc.js} +2 -2
- package/dist/{controller-kc7s9xhn.js → controller-rgna04r2.js} +2 -2
- package/dist/{controllers-connect-jzx7t4bn.js → controllers-connect-bqm1rtft.js} +20 -20
- package/dist/{cursor-adapter-qpwp5sfg.js → cursor-adapter-swr2k95y.js} +1 -1
- package/dist/{db-v5ytg5pw.js → db-24ck09xy.js} +1 -1
- package/dist/{db-4vq88jkc.js → db-a4m7haw0.js} +3 -3
- package/dist/{dispatcher-trtcrqae.js → dispatcher-nwf9ss0q.js} +17 -17
- package/dist/{dsh-adapter-f20xwn1z.js → dsh-adapter-kpdnhjst.js} +2 -2
- package/dist/{external-channel-n9bymc2n.js → external-channel-j1e2p34p.js} +2 -2
- package/dist/{grok-adapter-4kpmmnr6.js → grok-adapter-qtjz0whk.js} +4 -4
- package/dist/{handlers-ptsraqa8.js → handlers-19xr6dnz.js} +20 -20
- package/dist/{heartbeat-60qtxtjf.js → heartbeat-vtesda5d.js} +20 -20
- package/dist/{hook-rbktcstg.js → hook-8wqaszda.js} +1 -1
- package/dist/{hook-ngtvh729.js → hook-b2hhj6rd.js} +1 -1
- package/dist/{http-z8xy72gp.js → http-mxcqhhcs.js} +2757 -2448
- package/dist/{index-a9nsavhx.js → index-1r8wfpzs.js} +1 -1
- package/dist/{index-cbxkmfc5.js → index-9pn1pfkf.js} +1 -1
- package/dist/{index-s193j5fh.js → index-kw2mmat7.js} +20 -20
- package/dist/{index-06qkmcx6.js → index-n1vbvta9.js} +17 -9
- package/dist/{index-6srwvd5b.js → index-p4xfz37z.js} +1 -1
- package/dist/{index-1cscynkb.js → index-spwxjgay.js} +17 -17
- package/dist/{index-r70q0xjx.js → index-srg522m6.js} +17 -17
- package/dist/{keepalive-a9srcphj.js → keepalive-dsd8rtgx.js} +3 -3
- package/dist/{lead-3devneqy.js → lead-zyerk1x2.js} +30 -30
- package/dist/{maintenance-0er16h2y.js → maintenance-jqmqwnet.js} +1 -1
- package/dist/{oauth-refresh-sweep-b65jcyw2.js → oauth-refresh-sweep-q2c3bnvm.js} +2 -2
- package/dist/{onboard-wqywz5py.js → onboard-w4yd7s8t.js} +2 -2
- package/dist/{opencode-adapter-e56086m1.js → opencode-adapter-9tppvxar.js} +2 -2
- package/dist/{otel-impl-vk32s884.js → otel-impl-f36byzbd.js} +1 -1
- package/dist/{pricing-refresh-t27anv4z.js → pricing-refresh-xvak5p4v.js} +3 -3
- package/dist/{rbac-roles-bmcgsxjn.js → rbac-roles-682k2mt5.js} +1 -1
- package/dist/{rbac-roles-452665y0.js → rbac-roles-gfn30fb8.js} +2 -2
- package/dist/{render-v2-0ttsk2j4.js → render-v2-hgytz8nn.js} +3 -3
- package/dist/{resume-a26kvr41.js → resume-2dvq348g.js} +7 -5
- package/dist/{session-access-dgh9nkwk.js → session-access-7f21ekq9.js} +4 -4
- package/dist/{setup-9vc9qahm.js → setup-epffxtbk.js} +1 -1
- package/dist/{work-objects-vn8nkxwn.js → work-objects-xz1a5rqk.js} +20 -20
- package/dist/{worker-1x1h8vnm.js → worker-42t8emjt.js} +30 -30
- package/openapi.json +3045 -2456
- package/package.json +4 -4
- package/src/be/approval-config-question.ts +389 -0
- package/src/be/automation-preflight.ts +37 -3
- package/src/be/db/session-tree.ts +73 -15
- package/src/be/db/skill-search.ts +87 -0
- package/src/be/db/tasks/read.ts +29 -2
- package/src/be/db/workflow-runs.ts +54 -7
- package/src/be/db.ts +25 -5
- 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/swarm-config-guard.ts +23 -0
- package/src/extensions/catalog.generated.json +4 -4
- package/src/http/approval-requests.ts +209 -38
- package/src/http/config.ts +22 -37
- package/src/http/sessions.ts +27 -2
- package/src/integrations/chat-sdk/inbound.ts +5 -4
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/tests/approval-config-question.test.ts +702 -0
- package/src/tests/chat-sdk-inbound.test.ts +20 -0
- 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/list-endpoint-slimming.test.ts +33 -0
- package/src/tests/migration-240-workflow-run-parent-step-attempt.test.ts +55 -0
- package/src/tests/script-connections.test.ts +6 -0
- package/src/tests/sessions-tree.test.ts +168 -1
- package/src/tests/skill-search-long-content.test.ts +193 -0
- package/src/tests/skill-search.test.ts +173 -22
- package/src/tests/workflow-sub-workflow.test.ts +538 -1
- package/src/tools/request-human-input.ts +27 -5
- package/src/types.ts +7 -0
- package/src/workflows/approval-notifications.ts +10 -3
- 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 +25 -1
- package/src/workflows/executors/sub-workflow.ts +45 -16
- 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/skills/turso-interaction/content.md +1 -1
- package/templates/skills/workflow-iterate/content.md +8 -1
- /package/dist/{cli-stxdrqs2.js → cli-avdvgcay.js} +0 -0
- /package/dist/{pi-mono-adapter-tzps8f38.js → pi-mono-adapter-tqsge1ay.js} +0 -0
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
applyConfigAnswer,
|
|
5
|
+
type ConfigAnswer,
|
|
6
|
+
ConfigQuestionSettingsSchema,
|
|
7
|
+
type ConfigResult,
|
|
8
|
+
type ConfigState,
|
|
9
|
+
ConfigStateSchema,
|
|
10
|
+
isConfigQuestion,
|
|
11
|
+
keptConfigResult,
|
|
12
|
+
normalizeApprovalQuestions,
|
|
13
|
+
parseConfigAnswer,
|
|
14
|
+
readConfigState,
|
|
15
|
+
type StoredConfigQuestion,
|
|
16
|
+
skippedConfigResult,
|
|
17
|
+
} from "../be/approval-config-question";
|
|
3
18
|
import { resolveTaskAuditUserId } from "../be/audit-user";
|
|
4
19
|
import {
|
|
5
20
|
type ApprovalRequest,
|
|
@@ -16,8 +31,9 @@ import {
|
|
|
16
31
|
recordApprovalVotes,
|
|
17
32
|
resolveApprovalRequest,
|
|
18
33
|
} from "../be/db";
|
|
34
|
+
import { getUserGrant } from "../be/rbac-roles";
|
|
19
35
|
import { findUserById } from "../be/users";
|
|
20
|
-
import { can, type RbacPrincipal } from "../rbac";
|
|
36
|
+
import { can, isRbacEnabled, type RbacPrincipal } from "../rbac";
|
|
21
37
|
import type { User } from "../types";
|
|
22
38
|
import { getRequestAuth } from "../utils/request-auth-context";
|
|
23
39
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
@@ -27,6 +43,7 @@ import {
|
|
|
27
43
|
dispatchApprovalNotifications,
|
|
28
44
|
} from "../workflows/approval-notifications";
|
|
29
45
|
import { workflowEventBus } from "../workflows/event-bus";
|
|
46
|
+
import { ensureConfigAdmin, runConfigWriteSideEffects } from "./config";
|
|
30
47
|
import { bodyTask, route } from "./route-def";
|
|
31
48
|
import { jsonError } from "./utils";
|
|
32
49
|
|
|
@@ -34,7 +51,7 @@ import { jsonError } from "./utils";
|
|
|
34
51
|
|
|
35
52
|
const QuestionSchema = z.object({
|
|
36
53
|
id: z.string(),
|
|
37
|
-
type: z.enum(["approval", "text", "single-select", "multi-select", "boolean"]),
|
|
54
|
+
type: z.enum(["approval", "text", "single-select", "multi-select", "boolean", "config"]),
|
|
38
55
|
label: z.string(),
|
|
39
56
|
required: z.boolean().optional(),
|
|
40
57
|
description: z.string().optional(),
|
|
@@ -52,8 +69,14 @@ const QuestionSchema = z.object({
|
|
|
52
69
|
minSelections: z.number().int().min(0).optional(),
|
|
53
70
|
maxSelections: z.number().int().min(1).optional(),
|
|
54
71
|
defaultValue: z.boolean().optional(),
|
|
72
|
+
config: ConfigQuestionSettingsSchema.optional().describe(
|
|
73
|
+
"Config question settings. Answers are written to swarm_config and replaced by a result that never holds a secret value.",
|
|
74
|
+
),
|
|
55
75
|
});
|
|
56
76
|
|
|
77
|
+
/** Create input: `label` may be omitted on a config question (it defaults to the key). */
|
|
78
|
+
const QuestionInputSchema = QuestionSchema.extend({ label: z.string().optional() });
|
|
79
|
+
|
|
57
80
|
export type ApprovalQuestion = z.infer<typeof QuestionSchema>;
|
|
58
81
|
|
|
59
82
|
// ─── Response Schemas ────────────────────────────────────────────────────────
|
|
@@ -133,6 +156,12 @@ const ApprovalRequestSchema = z.object({
|
|
|
133
156
|
createdBy: z.string().optional(),
|
|
134
157
|
createdAt: z.string(),
|
|
135
158
|
updatedAt: z.string(),
|
|
159
|
+
configState: z
|
|
160
|
+
.record(z.string(), ConfigStateSchema)
|
|
161
|
+
.optional()
|
|
162
|
+
.describe(
|
|
163
|
+
"Live state of each config question's key, by question id. Read at request time and never stored. `value` appears only for non-secret keys.",
|
|
164
|
+
),
|
|
136
165
|
});
|
|
137
166
|
|
|
138
167
|
/** Slim list row (`?fields=slim`) — see `ApprovalRequestSummary`. */
|
|
@@ -143,6 +172,7 @@ const ApprovalRequestSummarySchema = ApprovalRequestSchema.omit({
|
|
|
143
172
|
approvals: true,
|
|
144
173
|
resolutionReason: true,
|
|
145
174
|
notificationChannels: true,
|
|
175
|
+
configState: true,
|
|
146
176
|
}).extend({ questionCount: z.number().int() });
|
|
147
177
|
|
|
148
178
|
/**
|
|
@@ -152,11 +182,16 @@ const ApprovalRequestSummarySchema = ApprovalRequestSchema.omit({
|
|
|
152
182
|
*/
|
|
153
183
|
async function toApprovalRequestResponse(
|
|
154
184
|
request: ApprovalRequest,
|
|
185
|
+
options: { withConfigState?: boolean } = {},
|
|
155
186
|
): Promise<z.infer<typeof ApprovalRequestSchema>> {
|
|
156
187
|
const approvers = request.approvers as ApproversShape;
|
|
157
188
|
const approvals = request.approvals ?? null;
|
|
189
|
+
const configState = options.withConfigState
|
|
190
|
+
? await readConfigStates(request.questions as ApprovalQuestion[])
|
|
191
|
+
: undefined;
|
|
158
192
|
return {
|
|
159
193
|
...request,
|
|
194
|
+
...(configState ? { configState } : {}),
|
|
160
195
|
questions: request.questions as ApprovalQuestion[],
|
|
161
196
|
approvers,
|
|
162
197
|
responses: request.responses as Record<string, unknown> | null,
|
|
@@ -188,6 +223,28 @@ async function withApprovalProgress(
|
|
|
188
223
|
);
|
|
189
224
|
}
|
|
190
225
|
|
|
226
|
+
/** Live state per config question id, or undefined when there are none. */
|
|
227
|
+
async function readConfigStates(
|
|
228
|
+
questions: ApprovalQuestion[],
|
|
229
|
+
): Promise<Record<string, ConfigState> | undefined> {
|
|
230
|
+
const configQuestions = questions.filter(isConfigQuestion);
|
|
231
|
+
if (configQuestions.length === 0) return undefined;
|
|
232
|
+
const entries = await Promise.all(
|
|
233
|
+
configQuestions.map(async (q) => [q.id, await readConfigState(q.config)] as const),
|
|
234
|
+
);
|
|
235
|
+
return Object.fromEntries(entries);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Thrown inside the respond transaction to roll back config writes. */
|
|
239
|
+
class ConfigRespondAbort extends Error {
|
|
240
|
+
constructor(
|
|
241
|
+
readonly kind: "unavailable" | "conflict",
|
|
242
|
+
message: string = kind,
|
|
243
|
+
) {
|
|
244
|
+
super(message);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
191
248
|
export async function getWorkflowApprovalUnavailableReason(
|
|
192
249
|
request: ApprovalRequest,
|
|
193
250
|
): Promise<string | null> {
|
|
@@ -241,6 +298,15 @@ function hasRequiredResponse(question: ApprovalQuestion, response: unknown): boo
|
|
|
241
298
|
}
|
|
242
299
|
case "boolean":
|
|
243
300
|
return typeof response === "boolean";
|
|
301
|
+
case "config": {
|
|
302
|
+
// Shape only. The respond handler checks `keep` against the live row.
|
|
303
|
+
if (typeof response !== "object" || response === null) return false;
|
|
304
|
+
const answer = response as { action?: unknown; value?: unknown };
|
|
305
|
+
if (answer.action === "keep") return true;
|
|
306
|
+
return (
|
|
307
|
+
answer.action === "set" && typeof answer.value === "string" && answer.value.trim() !== ""
|
|
308
|
+
);
|
|
309
|
+
}
|
|
244
310
|
}
|
|
245
311
|
}
|
|
246
312
|
|
|
@@ -264,7 +330,12 @@ const createRoute = route({
|
|
|
264
330
|
tags: ["ApprovalRequests"],
|
|
265
331
|
body: z.object({
|
|
266
332
|
title: z.string().min(1),
|
|
267
|
-
questions: z
|
|
333
|
+
questions: z
|
|
334
|
+
.array(QuestionInputSchema)
|
|
335
|
+
.min(1)
|
|
336
|
+
.describe(
|
|
337
|
+
'A request with a `config` question needs `approvers.policy: "any"`. The answer is written to swarm_config; the request stores only `{ key, scope, scopeId, isSecret, status, updatedAt }` (plus `value` for non-secret keys).',
|
|
338
|
+
),
|
|
268
339
|
approvers: z.object({
|
|
269
340
|
users: z.array(z.string()).optional(),
|
|
270
341
|
roles: z.array(z.string()).optional(),
|
|
@@ -323,6 +394,8 @@ const getByIdRoute = route({
|
|
|
323
394
|
404: { description: "Not found" },
|
|
324
395
|
},
|
|
325
396
|
auth: { apiKey: true },
|
|
397
|
+
description:
|
|
398
|
+
"Includes `configState` when the request has `config` questions: the live state of each target key, read now.",
|
|
326
399
|
});
|
|
327
400
|
|
|
328
401
|
const respondRoute = route({
|
|
@@ -331,7 +404,7 @@ const respondRoute = route({
|
|
|
331
404
|
pattern: ["api", "approval-requests", null, "respond"],
|
|
332
405
|
summary: "Submit a response to an approval request",
|
|
333
406
|
description:
|
|
334
|
-
|
|
407
|
+
'Only a person may answer: a user token, a page session signed for a user, or the shared key with no agent identity (recorded as `operator`). The responder is taken from the credential. When the request lists `approvers.users` or `approvers.roles`, a user must match one of them. A rejection resolves the request at once; approvals resolve it when the `any`, `all` or `{ min: N }` policy is met, and until then it stays pending with the answer recorded in `approvals`. A `config` question takes `{ action: "set", value }`, `{ action: "keep" }` (key must exist) or `{ action: "skip" }` (optional questions only); `set` also needs the config write permission. The value is written only when the request is approved, and the stored answer is the result, never a secret value.',
|
|
335
408
|
tags: ["ApprovalRequests"],
|
|
336
409
|
params: z.object({ id: z.string().uuid() }),
|
|
337
410
|
body: z.object({
|
|
@@ -487,11 +560,47 @@ export async function handleApprovalRequests(
|
|
|
487
560
|
return true;
|
|
488
561
|
}
|
|
489
562
|
|
|
563
|
+
// Config answers (D6) are checked here and replaced by their results (D7)
|
|
564
|
+
// before the vote exists, so no plaintext reaches any stored or sent copy.
|
|
565
|
+
const configQuestions = questions.filter(isConfigQuestion);
|
|
566
|
+
const configAnswers = new Map<
|
|
567
|
+
string,
|
|
568
|
+
{ question: StoredConfigQuestion; answer: ConfigAnswer }
|
|
569
|
+
>();
|
|
570
|
+
for (const question of configQuestions) {
|
|
571
|
+
const state = await readConfigState(question.config);
|
|
572
|
+
const answer = parseConfigAnswer(question, parsed.body.responses[question.id], state);
|
|
573
|
+
if (!answer.ok) {
|
|
574
|
+
jsonError(res, answer.error, 400);
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
configAnswers.set(question.id, { question, answer: answer.answer });
|
|
578
|
+
}
|
|
579
|
+
if (configAnswers.size > 0 && approvers.policy !== "any") {
|
|
580
|
+
jsonError(res, 'Config questions need approvers.policy "any"', 409);
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
if ([...configAnswers.values()].some(({ answer }) => answer.action === "set")) {
|
|
584
|
+
if (!(await ensureConfigAdmin(req, res, "config.write.any"))) return true;
|
|
585
|
+
// Admission only checked `approval.respond`; `ensureConfigAdmin` passes
|
|
586
|
+
// every user, so check the config write grant here as PUT /api/config would.
|
|
587
|
+
const auth = getRequestAuth(req);
|
|
588
|
+
if (auth?.kind === "user" && isRbacEnabled()) {
|
|
589
|
+
const grant = await getUserGrant(auth.userId);
|
|
590
|
+
if (!grant.grantsAll && !grant.verbs.has("config.write.any")) {
|
|
591
|
+
jsonError(res, "Forbidden: setting a config value needs 'config.write.any'", 403);
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
const safeResponses: Record<string, unknown> = { ...parsed.body.responses };
|
|
597
|
+
for (const id of configAnswers.keys()) delete safeResponses[id];
|
|
598
|
+
|
|
490
599
|
// Determine status from responses: if any approval question has approved: false → rejected
|
|
491
600
|
let status: "approved" | "rejected" = "approved";
|
|
492
601
|
for (const q of questions) {
|
|
493
602
|
if (q.type === "approval") {
|
|
494
|
-
const answer =
|
|
603
|
+
const answer = safeResponses[q.id] as { approved?: boolean } | undefined;
|
|
495
604
|
if (answer && answer.approved === false) {
|
|
496
605
|
status = "rejected";
|
|
497
606
|
break;
|
|
@@ -500,38 +609,89 @@ export async function handleApprovalRequests(
|
|
|
500
609
|
}
|
|
501
610
|
|
|
502
611
|
const responderId = responderKey(responder.responder);
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
approved: status === "approved",
|
|
506
|
-
responses: parsed.body.responses,
|
|
507
|
-
...(parsed.body.respondedBy ? { claimedRespondedBy: parsed.body.respondedBy } : {}),
|
|
508
|
-
respondedAt: new Date().toISOString(),
|
|
509
|
-
};
|
|
612
|
+
const respondedAt = new Date().toISOString();
|
|
613
|
+
const configWrites: { result: ConfigResult; value: string }[] = [];
|
|
510
614
|
// One write transaction (BEGIN IMMEDIATE) so concurrent answers to an
|
|
511
615
|
// `all` / `{ min: N }` request append in turn and never drop each other.
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
616
|
+
// Config writes share it, so a lost race writes nothing.
|
|
617
|
+
const outcome = await getDbClient()
|
|
618
|
+
.transaction(async () => {
|
|
619
|
+
const current = await getApprovalRequestById(parsed.params.id);
|
|
620
|
+
if (!current || current.status !== "pending") return { kind: "unavailable" as const };
|
|
621
|
+
const prior = current.approvals ?? [];
|
|
622
|
+
if (prior.some((v) => v.responder === responderId)) return { kind: "duplicate" as const };
|
|
623
|
+
|
|
624
|
+
// Config values are written only when the request resolves approved (D8).
|
|
625
|
+
const responses = { ...safeResponses };
|
|
626
|
+
for (const [id, { question, answer }] of configAnswers) {
|
|
627
|
+
if (status === "rejected" || answer.action === "skip") {
|
|
628
|
+
responses[id] = skippedConfigResult(question);
|
|
629
|
+
} else if (answer.action === "keep") {
|
|
630
|
+
const kept = await keptConfigResult(question);
|
|
631
|
+
if (!kept) {
|
|
632
|
+
throw new ConfigRespondAbort(
|
|
633
|
+
"conflict",
|
|
634
|
+
`config question "${id}": the key was removed before the answer was saved`,
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
responses[id] = kept;
|
|
638
|
+
} else {
|
|
639
|
+
const written = await applyConfigAnswer(question, answer.value);
|
|
640
|
+
configWrites.push({ result: written, value: answer.value });
|
|
641
|
+
responses[id] = written;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const vote: ApprovalVote = {
|
|
646
|
+
responder: responderId,
|
|
647
|
+
approved: status === "approved",
|
|
648
|
+
responses,
|
|
649
|
+
...(parsed.body.respondedBy ? { claimedRespondedBy: parsed.body.respondedBy } : {}),
|
|
650
|
+
respondedAt,
|
|
651
|
+
};
|
|
652
|
+
const votes = [...prior, vote];
|
|
653
|
+
if (status === "rejected" || (await approvalQuorumMet(approvers, votes))) {
|
|
654
|
+
const resolved = await resolveApprovalRequest(
|
|
655
|
+
parsed.params.id,
|
|
656
|
+
{ status, responses, approvals: votes, resolvedBy: responderId },
|
|
657
|
+
{ requireActionableWorkflow: true },
|
|
658
|
+
);
|
|
659
|
+
if (!resolved && configWrites.length > 0) throw new ConfigRespondAbort("unavailable");
|
|
660
|
+
return resolved
|
|
661
|
+
? { kind: "resolved" as const, request: resolved }
|
|
662
|
+
: { kind: "unavailable" as const };
|
|
663
|
+
}
|
|
664
|
+
const recorded = (await recordApprovalVotes(parsed.params.id, votes))
|
|
665
|
+
? await getApprovalRequestById(parsed.params.id)
|
|
666
|
+
: null;
|
|
667
|
+
return recorded
|
|
668
|
+
? { kind: "recorded" as const, request: recorded }
|
|
526
669
|
: { kind: "unavailable" as const };
|
|
670
|
+
})
|
|
671
|
+
.catch((error: unknown) => {
|
|
672
|
+
if (!(error instanceof ConfigRespondAbort)) throw error;
|
|
673
|
+
// Rolled back: nothing was written.
|
|
674
|
+
configWrites.length = 0;
|
|
675
|
+
return error.kind === "conflict"
|
|
676
|
+
? { kind: "conflict" as const, message: error.message }
|
|
677
|
+
: { kind: "unavailable" as const };
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
if (outcome.kind === "conflict") {
|
|
681
|
+
jsonError(res, outcome.message, 409);
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
|
+
// The transaction has committed: run what PUT /api/config runs after a write.
|
|
685
|
+
for (const { result, value } of configWrites) {
|
|
686
|
+
try {
|
|
687
|
+
await runConfigWriteSideEffects(result.scope, result.key, value);
|
|
688
|
+
} catch (err) {
|
|
689
|
+
console.error(
|
|
690
|
+
"[approval-requests] Config side effects failed:",
|
|
691
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
692
|
+
);
|
|
527
693
|
}
|
|
528
|
-
|
|
529
|
-
? await getApprovalRequestById(parsed.params.id)
|
|
530
|
-
: null;
|
|
531
|
-
return recorded
|
|
532
|
-
? { kind: "recorded" as const, request: recorded }
|
|
533
|
-
: { kind: "unavailable" as const };
|
|
534
|
-
});
|
|
694
|
+
}
|
|
535
695
|
|
|
536
696
|
if (outcome.kind === "duplicate") {
|
|
537
697
|
jsonError(res, "You already answered this approval request", 409);
|
|
@@ -576,7 +736,7 @@ export async function handleApprovalRequests(
|
|
|
576
736
|
await createApprovalFollowUpTask(updated, "hitl.follow_up");
|
|
577
737
|
|
|
578
738
|
respondRoute.respond(res, 200, {
|
|
579
|
-
approvalRequest: await toApprovalRequestResponse(updated),
|
|
739
|
+
approvalRequest: await toApprovalRequestResponse(updated, { withConfigState: true }),
|
|
580
740
|
});
|
|
581
741
|
return true;
|
|
582
742
|
}
|
|
@@ -617,7 +777,7 @@ export async function handleApprovalRequests(
|
|
|
617
777
|
}
|
|
618
778
|
|
|
619
779
|
getByIdRoute.respond(res, 200, {
|
|
620
|
-
approvalRequest: await toApprovalRequestResponse(request),
|
|
780
|
+
approvalRequest: await toApprovalRequestResponse(request, { withConfigState: true }),
|
|
621
781
|
});
|
|
622
782
|
return true;
|
|
623
783
|
}
|
|
@@ -638,6 +798,15 @@ export async function handleApprovalRequests(
|
|
|
638
798
|
}
|
|
639
799
|
}
|
|
640
800
|
|
|
801
|
+
const normalized = await normalizeApprovalQuestions(parsed.body.questions, {
|
|
802
|
+
approvers: parsed.body.approvers,
|
|
803
|
+
});
|
|
804
|
+
if (!normalized.ok) {
|
|
805
|
+
jsonError(res, normalized.error, 400);
|
|
806
|
+
return true;
|
|
807
|
+
}
|
|
808
|
+
const questions = normalized.questions;
|
|
809
|
+
|
|
641
810
|
const id = crypto.randomUUID();
|
|
642
811
|
// Prefer a trusted authenticated user (never client-controlled); else fall
|
|
643
812
|
// back to the ownership-validated sourceTaskId the request body carries.
|
|
@@ -651,7 +820,7 @@ export async function handleApprovalRequests(
|
|
|
651
820
|
const request = await createApprovalRequest({
|
|
652
821
|
id,
|
|
653
822
|
title: parsed.body.title,
|
|
654
|
-
questions
|
|
823
|
+
questions,
|
|
655
824
|
approvers: parsed.body.approvers,
|
|
656
825
|
workflowRunId: parsed.body.workflowRunId,
|
|
657
826
|
workflowRunStepId: parsed.body.workflowRunStepId,
|
|
@@ -668,7 +837,7 @@ export async function handleApprovalRequests(
|
|
|
668
837
|
dispatchApprovalNotifications({
|
|
669
838
|
requestId: request.id,
|
|
670
839
|
title: request.title,
|
|
671
|
-
questions
|
|
840
|
+
questions,
|
|
672
841
|
timeoutSeconds: parsed.body.timeoutSeconds,
|
|
673
842
|
notifications,
|
|
674
843
|
}).catch((err) => {
|
|
@@ -705,7 +874,9 @@ export async function handleApprovalRequests(
|
|
|
705
874
|
|
|
706
875
|
const requests = await listApprovalRequests(filters);
|
|
707
876
|
listRoute.respond(res, 200, {
|
|
708
|
-
approvalRequests: await Promise.all(
|
|
877
|
+
approvalRequests: await Promise.all(
|
|
878
|
+
requests.map((request) => toApprovalRequestResponse(request)),
|
|
879
|
+
),
|
|
709
880
|
});
|
|
710
881
|
return true;
|
|
711
882
|
}
|
package/src/http/config.ts
CHANGED
|
@@ -21,10 +21,7 @@ import {
|
|
|
21
21
|
internalConfigKeyError,
|
|
22
22
|
isApiOnlyConfigKey,
|
|
23
23
|
isInternalConfigKey,
|
|
24
|
-
|
|
25
|
-
reservedKeyError,
|
|
26
|
-
validateConfigValue,
|
|
27
|
-
validateExportToEnv,
|
|
24
|
+
validateConfigEntry,
|
|
28
25
|
} from "../be/swarm-config-guard";
|
|
29
26
|
import { can, isRbacEnabled, type PermissionVerb } from "../rbac";
|
|
30
27
|
import { SwarmConfigSchema } from "../types";
|
|
@@ -145,6 +142,25 @@ export async function ensureConfigAdmin(
|
|
|
145
142
|
);
|
|
146
143
|
}
|
|
147
144
|
|
|
145
|
+
/**
|
|
146
|
+
* What a committed config write triggers beyond the row itself. Shared by
|
|
147
|
+
* `PUT /api/config` and HITL `config` answers.
|
|
148
|
+
*/
|
|
149
|
+
export async function runConfigWriteSideEffects(
|
|
150
|
+
scope: "global" | "agent" | "repo",
|
|
151
|
+
key: string,
|
|
152
|
+
value: string,
|
|
153
|
+
): Promise<void> {
|
|
154
|
+
if (scope !== "global") return;
|
|
155
|
+
if (key === "SWARM_ORG_WEBSITE") await fillOrgLogoFromWebsite(value);
|
|
156
|
+
await syncRankingAfterConfigChange(key, value);
|
|
157
|
+
// Auto-reload integrations when a global-scoped config changes so callers
|
|
158
|
+
// (CLI, automation, dashboard) don't have to remember to hit /reload.
|
|
159
|
+
// Debounced to coalesce sequential single-key upserts from the dashboard
|
|
160
|
+
// batch save (no bulk endpoint exists).
|
|
161
|
+
scheduleIntegrationsReload();
|
|
162
|
+
}
|
|
163
|
+
|
|
148
164
|
/**
|
|
149
165
|
* The general form of `ensureConfigAdmin`: operators and users pass, guests
|
|
150
166
|
* get a plain 403, and an agent principal (session identity first, then the
|
|
@@ -484,28 +500,7 @@ export async function handleConfig(
|
|
|
484
500
|
if (!(await ensureConfigAdmin(req, res, "config.write.any"))) return true;
|
|
485
501
|
const { scope, scopeId, key, value, isSecret, envPath, description, exportToEnv } = parsed.body;
|
|
486
502
|
|
|
487
|
-
|
|
488
|
-
jsonError(res, "Global scope must not have scopeId", 400);
|
|
489
|
-
return true;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
if ((scope === "agent" || scope === "repo") && !scopeId) {
|
|
493
|
-
jsonError(res, "Agent/repo scope requires scopeId", 400);
|
|
494
|
-
return true;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
if (isReservedConfigKey(key)) {
|
|
498
|
-
jsonError(res, reservedKeyError(key).message, 400);
|
|
499
|
-
return true;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
if (isInternalConfigKey(key)) {
|
|
503
|
-
jsonError(res, internalConfigKeyError(key).message, 400);
|
|
504
|
-
return true;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const validationError =
|
|
508
|
-
validateConfigValue(key, value) ?? validateExportToEnv(key, exportToEnv);
|
|
503
|
+
const validationError = validateConfigEntry({ scope, scopeId, key, value, exportToEnv });
|
|
509
504
|
if (validationError) {
|
|
510
505
|
jsonError(res, validationError, 400);
|
|
511
506
|
return true;
|
|
@@ -523,17 +518,7 @@ export async function handleConfig(
|
|
|
523
518
|
description: description || null,
|
|
524
519
|
exportToEnv,
|
|
525
520
|
});
|
|
526
|
-
|
|
527
|
-
// (CLI, automation, dashboard) don't have to remember to hit /reload.
|
|
528
|
-
// Debounced to coalesce sequential single-key upserts from the dashboard
|
|
529
|
-
// batch save (no bulk endpoint exists).
|
|
530
|
-
if (scope === "global" && key === "SWARM_ORG_WEBSITE") {
|
|
531
|
-
await fillOrgLogoFromWebsite(String(value));
|
|
532
|
-
}
|
|
533
|
-
if (scope === "global") await syncRankingAfterConfigChange(key, String(value));
|
|
534
|
-
if (scope === "global") {
|
|
535
|
-
scheduleIntegrationsReload();
|
|
536
|
-
}
|
|
521
|
+
await runConfigWriteSideEffects(scope, key, String(value));
|
|
537
522
|
const result = includeSecrets || !config.isSecret ? config : maskSecrets([config])[0]!;
|
|
538
523
|
upsertConfig.respond(res, 200, result);
|
|
539
524
|
} catch (error) {
|
package/src/http/sessions.ts
CHANGED
|
@@ -502,7 +502,22 @@ const SessionHideSourceSchema = z
|
|
|
502
502
|
.filter(Boolean),
|
|
503
503
|
)
|
|
504
504
|
.describe(
|
|
505
|
-
"Comma-separated root sources to leave out
|
|
505
|
+
"Comma-separated root sources to leave out, e.g. `slack,chat`. Older sessions v2 clients send this; `showSource` replaces it.",
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
const SessionShowSourceSchema = z
|
|
509
|
+
.string()
|
|
510
|
+
// The same cap as the `showSource` body field of `POST /api/session-triage`.
|
|
511
|
+
.refine((value) => value.split(",").length <= 32, "showSource takes at most 32 sources.")
|
|
512
|
+
.optional()
|
|
513
|
+
.transform((value) =>
|
|
514
|
+
value
|
|
515
|
+
?.split(",")
|
|
516
|
+
.map((source) => source.trim())
|
|
517
|
+
.filter(Boolean),
|
|
518
|
+
)
|
|
519
|
+
.describe(
|
|
520
|
+
"Comma-separated root sources to keep (the sessions v2 source filter), e.g. `ui,slack`. Present and empty keeps none; omit to keep every source.",
|
|
506
521
|
);
|
|
507
522
|
|
|
508
523
|
const listSessions = route({
|
|
@@ -568,6 +583,8 @@ const listSessions = route({
|
|
|
568
583
|
show: SessionShowSchema,
|
|
569
584
|
/** Root sources to leave out. Must match the tree's `hideSource`. */
|
|
570
585
|
hideSource: SessionHideSourceSchema,
|
|
586
|
+
/** Root sources to keep. Must match the tree's `showSource`. */
|
|
587
|
+
showSource: SessionShowSourceSchema,
|
|
571
588
|
}),
|
|
572
589
|
responses: {
|
|
573
590
|
200: {
|
|
@@ -590,7 +607,11 @@ const sessionTree = route({
|
|
|
590
607
|
pattern: ["api", "session-tree"],
|
|
591
608
|
summary: "Get session groups and triage counts",
|
|
592
609
|
tags: ["Sessions"],
|
|
593
|
-
query: z.object({
|
|
610
|
+
query: z.object({
|
|
611
|
+
show: SessionShowSchema,
|
|
612
|
+
hideSource: SessionHideSourceSchema,
|
|
613
|
+
showSource: SessionShowSourceSchema,
|
|
614
|
+
}),
|
|
594
615
|
responses: {
|
|
595
616
|
200: { description: "Visible groups and section counts", schema: SessionTreeResponseSchema },
|
|
596
617
|
401: { description: "Unauthorized" },
|
|
@@ -615,6 +636,8 @@ const triageSessionListRoute = route({
|
|
|
615
636
|
groupKey: SessionKeyFilterSchema.optional(),
|
|
616
637
|
show: SessionShowSchema,
|
|
617
638
|
hideSource: z.array(z.string().min(1)).max(32).optional(),
|
|
639
|
+
/** Root sources to keep; an empty array keeps none. */
|
|
640
|
+
showSource: z.array(z.string().min(1)).max(32).optional(),
|
|
618
641
|
})
|
|
619
642
|
.refine((body) => [body.triage, body.noGroup, body.groupKey].filter(Boolean).length === 1, {
|
|
620
643
|
message: "Name exactly one list: triage, noGroup, or groupKey",
|
|
@@ -941,6 +964,7 @@ export async function handleSessions(
|
|
|
941
964
|
const filterOpts = {
|
|
942
965
|
source: sources,
|
|
943
966
|
hideSource: parsed.query.hideSource,
|
|
967
|
+
showSource: parsed.query.showSource,
|
|
944
968
|
q: parsed.query.q,
|
|
945
969
|
requestedByUserId: parsed.query.requestedByUserId,
|
|
946
970
|
contextKeyPrefix: parsed.query.contextKeyPrefix,
|
|
@@ -994,6 +1018,7 @@ export async function handleSessions(
|
|
|
994
1018
|
await getSessionTree(actor, undefined, {
|
|
995
1019
|
mine: parsed.query.show !== "all",
|
|
996
1020
|
hideSource: parsed.query.hideSource,
|
|
1021
|
+
showSource: parsed.query.showSource,
|
|
997
1022
|
}),
|
|
998
1023
|
);
|
|
999
1024
|
return true;
|
|
@@ -188,9 +188,10 @@ export async function handleChatInbound(
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
if (text || attachments.length > 0) {
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
191
|
+
// An unaddressed subscribed-thread message may be dropped by the router at
|
|
192
|
+
// flush time; it gets typing and a reaction once a task exists (see
|
|
193
|
+
// flushChatThread). A mention, reply to the bot, or DM is acknowledged now.
|
|
194
|
+
const acknowledge = kind !== "subscribed" || isDirectMessage || mentioned || facts.replyToBot;
|
|
194
195
|
bufferChatMessage(thread.id, {
|
|
195
196
|
messageId: message.id,
|
|
196
197
|
channelId: thread.channelId,
|
|
@@ -580,7 +581,7 @@ export async function flushChatThread(
|
|
|
580
581
|
if (created > 0) {
|
|
581
582
|
await rememberChatThreadRequester(threadId, requester.userId);
|
|
582
583
|
await subscribe(thread);
|
|
583
|
-
//
|
|
584
|
+
// Unaddressed subscribed-thread messages start typing only here, once a task exists.
|
|
584
585
|
startTypingKeepalive(adapter, threadId);
|
|
585
586
|
await clearChatMessageReactions(
|
|
586
587
|
adapter,
|
|
@@ -44,7 +44,7 @@ import type {
|
|
|
44
44
|
* default; the image installs exactly this version and the per-task settings
|
|
45
45
|
* set `amp.updates.mode: "disabled"`. Bump it deliberately, with a live run.
|
|
46
46
|
*/
|
|
47
|
-
export const AMP_PACKAGE = "@ampcode/cli@0.0.
|
|
47
|
+
export const AMP_PACKAGE = "@ampcode/cli@0.0.1792756861-g6187a2";
|
|
48
48
|
|
|
49
49
|
/** MCP server name. Amp exposes its tools as `agent_swarm` / `store_progress` (dashes become underscores). */
|
|
50
50
|
const AMP_MCP_SERVER_NAME = "agent-swarm";
|
|
@@ -37,7 +37,7 @@ import type {
|
|
|
37
37
|
} from "./types";
|
|
38
38
|
|
|
39
39
|
// npm's `latest` is older and lacks the JSON/stdin contract used here.
|
|
40
|
-
export const DSH_PACKAGE = "@deepseek-ai/dsh@0.2.1-rc.
|
|
40
|
+
export const DSH_PACKAGE = "@deepseek-ai/dsh@0.2.1-rc.3";
|
|
41
41
|
|
|
42
42
|
export function checkDshCredentials(env: Record<string, string | undefined>): CredStatus {
|
|
43
43
|
return env.DEEPSEEK_API_KEY?.trim() || env.OPENROUTER_API_KEY?.trim()
|