@desplega.ai/agent-swarm 1.145.0 → 1.147.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 -1
- package/dist/{acp-adapter-580t41c0.js → acp-adapter-9n319wqc.js} +61 -6
- package/dist/{actions-m662tjng.js → actions-2vxqvpr9.js} +9 -8
- package/dist/{app-zb3rndm8.js → app-w066xfy0.js} +6 -5
- package/dist/assistant-kt31dj2h.js +236 -0
- package/dist/{boot-reembed-072x00vn.js → boot-reembed-az5rassp.js} +7 -6
- package/dist/{boot-reembed-65zz2smt.js → boot-reembed-j3mm3rfz.js} +8 -7
- package/dist/{boot-scrub-logs-yz0ams04.js → boot-scrub-logs-b6h54817.js} +4 -4
- package/dist/{claude-adapter-paj270y2.js → claude-adapter-w1z04ann.js} +8 -6
- package/dist/{claude-managed-setup-w619jncn.js → claude-managed-setup-f5pmsyrp.js} +24 -3
- package/dist/{claude-sdk-session-swt80j9z.js → claude-sdk-session-f0cc1kec.js} +8 -6
- package/dist/{cli-dt553ktb.js → cli-0b6y7kxm.js} +1 -1
- package/dist/{cli-bxdhw615.js → cli-12fz972k.js} +4 -4
- package/dist/{cli-s2f20r5w.js → cli-1th1728g.js} +2 -2
- package/dist/{cli-rs6ec3b9.js → cli-3tgwnf43.js} +3 -3
- package/dist/{cli-gxnzc6wh.js → cli-4nr988h6.js} +1 -1
- package/dist/{cli-axs8e6d4.js → cli-53s590z8.js} +1 -1
- package/dist/{cli-ygbjp3z8.js → cli-54wwve05.js} +5 -4
- package/dist/{cli-v0asr5yv.js → cli-719k9j2c.js} +2 -2
- package/dist/{cli-anktweqr.js → cli-89xfd7f9.js} +21 -47
- package/dist/{cli-q34a0e10.js → cli-8v0g1yc8.js} +1 -1
- package/dist/cli-9d8ntetj.js +42 -0
- package/dist/{cli-z9fprcrf.js → cli-atcve9y8.js} +881 -738
- package/dist/{cli-jmb7g47h.js → cli-b5bq9crk.js} +14 -31
- package/dist/cli-d8brzxjb.js +190 -0
- package/dist/{cli-gzky0gkr.js → cli-de14znh8.js} +2 -2
- package/dist/{cli-xytwff7f.js → cli-dttph1bw.js} +2 -2
- package/dist/cli-ep19k3mb.js +19 -0
- package/dist/{cli-m1xa8be7.js → cli-f146mn5s.js} +7 -50
- package/dist/{cli-1fj0chdk.js → cli-f7dsy61y.js} +5 -3
- package/dist/{cli-f567d5c1.js → cli-ftht3zzj.js} +2782 -2589
- package/dist/{cli-a0g1gbt5.js → cli-hbv0kq6w.js} +2 -2
- package/dist/{cli-fhsz3twr.js → cli-jsp14b6c.js} +171 -1
- package/dist/{cli-7fnkj1hj.js → cli-kpv03zhj.js} +21 -17
- package/dist/{cli-m4j6trn4.js → cli-m8yrsg97.js} +2 -2
- package/dist/cli-m9sxbkhm.js +779 -0
- package/dist/cli-mmemxxdc.js +823 -0
- package/dist/{cli-tqtvj5pt.js → cli-n8508nre.js} +1 -1
- package/dist/{cli-z96t0thj.js → cli-p1d5b073.js} +37 -2
- package/dist/{cli-gh8y8f36.js → cli-rw51bq3j.js} +13 -10
- package/dist/{cli-59ej1c07.js → cli-smby5m17.js} +7 -6
- package/dist/{cli-65vqzz3v.js → cli-tzvk9haz.js} +94 -372
- package/dist/{cli-dc9k2ba9.js → cli-w9kb3bk1.js} +1 -1
- package/dist/{cli-kpev657q.js → cli-y6b6fb76.js} +25 -3
- package/dist/{cli-123q2get.js → cli-yz3djrm0.js} +9 -9
- package/dist/{cli-wvp6bvy4.js → cli-zhjmqxzh.js} +1 -1
- package/dist/cli.js +13 -13
- package/dist/{codex-adapter-k98vaw2r.js → codex-adapter-zw4dwsx7.js} +2 -2
- package/dist/{codex-hook-9pf19cks.js → codex-hook-bw8p5nh4.js} +2 -2
- package/dist/{codex-session-runner-tngtphx7.js → codex-session-runner-k981s65v.js} +2 -2
- package/dist/{commands-y8nbr3fv.js → commands-sykrxx7e.js} +4 -4
- package/dist/{db-30p4h2t1.js → db-sfbsc8wt.js} +6 -4
- package/dist/handlers-0aspcejm.js +603 -0
- package/dist/{hook-dcw4jfbj.js → hook-4nvqj2sb.js} +7 -5
- package/dist/{hook-21jexvfx.js → hook-hxccs7m5.js} +24 -49
- package/dist/{http-r4kq5g8d.js → http-y06z517d.js} +230 -94
- package/dist/{index-q3qja6ta.js → index-hssww7kj.js} +15 -14
- package/dist/{index-gay66b9w.js → index-k8znfx0z.js} +13 -12
- package/dist/{index-53zyht4z.js → index-tce1rss4.js} +371 -110
- package/dist/{index-nqvf26za.js → index-x5j894zf.js} +14 -13
- package/dist/{keepalive-xs8fa55y.js → keepalive-w733ax66.js} +6 -6
- package/dist/{lead-d9m2t4s0.js → lead-75bjqa9d.js} +29 -27
- package/dist/{maintenance-3zh82g6z.js → maintenance-w4f1zjck.js} +8 -7
- package/dist/{oauth-refresh-sweep-03snsye5.js → oauth-refresh-sweep-wdetky26.js} +5 -5
- package/dist/{onboard-0cg1925x.js → onboard-bb5net8s.js} +2 -2
- package/dist/{otel-impl-0ab95z27.js → otel-impl-0w7c14ap.js} +1 -1
- package/dist/{pi-mono-adapter-dtkz4e7j.js → pi-mono-adapter-2has4gnp.js} +12 -2
- package/dist/{pricing-refresh-e5jhzz4j.js → pricing-refresh-9nzsrgcq.js} +6 -6
- package/dist/{rbac-roles-p1mf40z3.js → rbac-roles-r3hqjp09.js} +5 -5
- package/dist/{rbac-roles-2w3cmrkc.js → rbac-roles-z1jrffbt.js} +4 -4
- package/dist/{registry-9kdck5ye.js → registry-v5wytg0s.js} +1 -2
- package/dist/{render-v2-xhawry3k.js → render-v2-vbr89fnb.js} +6 -5
- package/dist/{seed-pricing-t223sw91.js → seed-pricing-g62hy1pk.js} +7 -5
- package/dist/{setup-zcs0rce7.js → setup-gnfqnptp.js} +2 -2
- package/dist/{worker-qnp2ka62.js → worker-kznxp4qm.js} +29 -27
- package/openapi.json +263 -12
- package/package.json +1 -1
- package/plugin/commands/start-worker.md +4 -9
- package/plugin/pi-skills/start-worker/SKILL.md +4 -9
- package/src/be/db/agents.ts +556 -0
- package/src/be/db/context-versions.ts +117 -0
- package/src/be/db/runtime.ts +406 -0
- package/src/be/db/tasks/read.ts +1167 -0
- package/src/be/db/tasks/write.ts +953 -0
- package/src/be/db.ts +584 -3429
- package/src/be/migrations/147_scheduled_tasks_parent_task.sql +7 -0
- package/src/be/migrations/148_pages_svg.sql +123 -0
- package/src/be/migrations/149_session_tokens.sql +24 -0
- package/src/be/scripts/typecheck.ts +1 -0
- package/src/be/seed-pricing.ts +13 -8
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
- package/src/be/seed-scripts/catalog/star-history-refresh.ts +101 -0
- package/src/be/seed-scripts/catalog/star-history-renderer.ts +119 -0
- package/src/be/seed-scripts/index.ts +17 -0
- package/src/be/seed-skills/index.ts +14 -0
- package/src/be/swarm-config-guard.ts +5 -2
- package/src/be/task-attachment-store.ts +70 -0
- package/src/be/users.ts +77 -0
- package/src/commands/attachments-section.ts +37 -0
- package/src/commands/claude-managed-setup.ts +45 -3
- package/src/commands/claude-md-session.ts +408 -0
- package/src/commands/context-preamble.ts +15 -3
- package/src/commands/profile-sync.ts +205 -5
- package/src/commands/provider-credentials.ts +60 -27
- package/src/commands/runner.ts +63 -58
- package/src/commands/templates.ts +25 -2
- package/src/hooks/hook.ts +26 -67
- package/src/http/agents.ts +40 -1
- package/src/http/auth.ts +13 -1
- package/src/http/fs.ts +21 -51
- package/src/http/index.ts +2 -0
- package/src/http/mcp.ts +32 -0
- package/src/http/pages-public.ts +20 -4
- package/src/http/pages.ts +11 -3
- package/src/http/poll.ts +4 -0
- package/src/http/repos.ts +3 -3
- package/src/http/sessions.ts +82 -0
- package/src/http/tasks.ts +10 -2
- package/src/http/utils.ts +79 -1
- package/src/prompts/session-templates.ts +12 -8
- package/src/providers/acp-adapter.ts +62 -8
- package/src/providers/pi-mono-adapter.ts +23 -6
- package/src/providers/pricing-sources.md +11 -0
- package/src/scheduler/scheduler.test.ts +49 -2
- package/src/scheduler/scheduler.ts +4 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +8 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +8 -0
- package/src/server.ts +2 -0
- package/src/slack/assistant.ts +87 -46
- package/src/slack/handlers.test.ts +1 -3
- package/src/slack/handlers.ts +102 -87
- package/src/slack/inbound-files.ts +466 -0
- package/src/slack/templates.ts +14 -0
- package/src/tasks/task-terminal-effects.ts +158 -0
- package/src/tests/acp-adapter.test.ts +369 -117
- package/src/tests/budget-claim-gate.test.ts +1 -0
- package/src/tests/build-pi-skills.test.ts +5 -7
- package/src/tests/claude-managed-setup.test.ts +52 -4
- package/src/tests/claude-md-session.test.ts +603 -0
- package/src/tests/codex-skill-resolver.test.ts +19 -0
- package/src/tests/context-versioning.test.ts +107 -0
- package/src/tests/cors-allowlist.test.ts +309 -0
- package/src/tests/credential-check.test.ts +157 -14
- package/src/tests/db-agents.test.ts +91 -0
- package/src/tests/db-context-versions.test.ts +37 -0
- package/src/tests/db-extraction-contract.test.ts +45 -0
- package/src/tests/db-runtime.test.ts +70 -0
- package/src/tests/db-task-read-contract.test.ts +624 -0
- package/src/tests/db-task-write-contract.test.ts +653 -0
- package/src/tests/db-tasks-read.test.ts +183 -0
- package/src/tests/db-tasks-write.test.ts +230 -0
- package/src/tests/defer-task.test.ts +389 -0
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/e2e-visuals-stage.test.ts +151 -0
- package/src/tests/file-lock.test.ts +144 -0
- package/src/tests/fixtures/file-lock-holder.ts +13 -0
- package/src/tests/fixtures/hold-file-lock.ts +25 -0
- package/src/tests/heartbeat-expiry.test.ts +188 -0
- package/src/tests/http-api-integration.test.ts +78 -1
- package/src/tests/pages-public-html.test.ts +43 -0
- package/src/tests/pages-svg-migration.test.ts +42 -0
- package/src/tests/preload.ts +4 -1
- package/src/tests/profile-sync-warning.test.ts +16 -0
- package/src/tests/profile-sync.test.ts +126 -0
- package/src/tests/prompt-template-session.test.ts +20 -4
- package/src/tests/provider-registration.test.ts +93 -0
- package/src/tests/rbac-e2e-helpers.test.ts +53 -0
- package/src/tests/rbac-e2e-helpers.ts +19 -2
- package/src/tests/reload-config.test.ts +26 -0
- package/src/tests/routing-affinity-inventory.test.ts +1 -1
- package/src/tests/runner-attachments-section.test.ts +22 -2
- package/src/tests/runner-context-preamble.test.ts +30 -0
- package/src/tests/runner-deployment-resume.test.ts +107 -0
- package/src/tests/runner-repo-autostash.test.ts +12 -0
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/seed-scripts.test.ts +2 -2
- package/src/tests/session-tokens.test.ts +279 -0
- package/src/tests/slack-file-tools-attachments.test.ts +408 -0
- package/src/tests/slack-inbound-files.test.ts +847 -0
- package/src/tests/star-history-refresh.test.ts +102 -0
- package/src/tests/swarm-config-reserved-keys.test.ts +21 -0
- package/src/tests/swarm-repos.test.ts +20 -0
- package/src/tests/tool-annotations.test.ts +4 -3
- package/src/tests/ui-logs-parser.test.ts +176 -0
- package/src/tools/create-page.ts +4 -2
- package/src/tools/defer-task.ts +239 -0
- package/src/tools/repos/update-repo.ts +2 -2
- package/src/tools/schedules/create-schedule.ts +1 -0
- package/src/tools/schedules/list-schedules.ts +1 -0
- package/src/tools/schedules/patch-schedule.ts +1 -0
- package/src/tools/schedules/update-schedule.ts +1 -0
- package/src/tools/slack-download-file.ts +60 -29
- package/src/tools/slack-read.ts +73 -32
- package/src/tools/store-progress.ts +16 -146
- package/src/tools/tool-config.ts +1 -0
- package/src/tools/utils.ts +21 -0
- package/src/types.ts +37 -4
- package/src/utils/acp-session-token.ts +56 -0
- package/src/utils/file-lock.ts +111 -0
- package/src/utils/heartbeat-expiry.ts +78 -0
- package/src/utils/request-auth-context.ts +2 -0
- package/src/utils/task-attachment-links.ts +22 -0
- package/templates/skills/code-quality/content.md +8 -5
- package/templates/skills/download-task-attachment/content.md +2 -1
- package/{plugin/pi-skills → templates/skills}/review-offered-task/SKILL.md +5 -5
- package/templates/skills/review-offered-task/config.json +14 -0
- package/{plugin/commands/review-offered-task.md → templates/skills/review-offered-task/content.md} +3 -8
- package/templates/skills/scheduling/content.md +4 -0
- package/templates/skills/slack-interaction/content.md +2 -2
- package/templates/skills/work-on-task/SKILL.md +16 -0
- package/templates/skills/work-on-task/config.json +14 -0
- package/templates/skills/work-on-task/content.md +11 -0
- package/dist/assistant-0qt42nzg.js +0 -212
- package/dist/cli-fs1kkqgr.js +0 -317
- package/dist/cli-jnf5zghe.js +0 -269
- package/dist/cli-n1q8q14d.js +0 -177
- package/dist/handlers-yfp1qn81.js +0 -592
- package/plugin/commands/work-on-task.md +0 -39
- package/plugin/pi-skills/work-on-task/SKILL.md +0 -39
package/src/http/sessions.ts
CHANGED
|
@@ -2,7 +2,9 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { countSessions, getRootTaskChain, getTaskById, listRecentSessions } from "../be/db";
|
|
4
4
|
import { getTaskSteeringFields } from "../be/steering";
|
|
5
|
+
import { mintSessionToken, revokeSessionToken } from "../be/users";
|
|
5
6
|
import { AgentTaskSchema, AgentTaskStatusSchema, SteerModeSchema } from "../types";
|
|
7
|
+
import { getRequestAuth } from "../utils/request-auth-context";
|
|
6
8
|
import { route } from "./route-def";
|
|
7
9
|
import { jsonError } from "./utils";
|
|
8
10
|
|
|
@@ -68,6 +70,57 @@ const TaskWithSteeringSchema = AgentTaskSchema.extend({
|
|
|
68
70
|
|
|
69
71
|
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
70
72
|
|
|
73
|
+
/** Maximum TTL for an ephemeral session token: 7 days. */
|
|
74
|
+
const MAX_SESSION_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
75
|
+
|
|
76
|
+
const mintSessionTokenRoute = route({
|
|
77
|
+
method: "post",
|
|
78
|
+
path: "/api/sessions/tokens",
|
|
79
|
+
pattern: ["api", "sessions", "tokens"],
|
|
80
|
+
summary: "Mint an ephemeral session token for an ACP provider session",
|
|
81
|
+
description:
|
|
82
|
+
"Returns a short-lived aseph_ bearer for use by the ACP adapter in place of the full " +
|
|
83
|
+
"operator key. The token expires at the requested TTL and is actively revoked when the " +
|
|
84
|
+
"session ends. Only the operator key may mint session tokens.",
|
|
85
|
+
tags: ["Sessions"],
|
|
86
|
+
body: z.object({
|
|
87
|
+
agentId: z.string().min(1),
|
|
88
|
+
taskId: z.string().min(1),
|
|
89
|
+
ttlMs: z.number().int().positive().max(MAX_SESSION_TOKEN_TTL_MS),
|
|
90
|
+
}),
|
|
91
|
+
responses: {
|
|
92
|
+
200: {
|
|
93
|
+
description: "Minted token plaintext (returned once) and its stable token ID",
|
|
94
|
+
schema: z.object({
|
|
95
|
+
tokenId: z.string(),
|
|
96
|
+
plaintext: z.string(),
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
400: { description: "Validation error" },
|
|
100
|
+
401: { description: "Unauthorized" },
|
|
101
|
+
},
|
|
102
|
+
auth: { apiKey: true },
|
|
103
|
+
rbac: { ungated: "operator-only: minting is gated on the full operator key in auth middleware" },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const revokeSessionTokenRoute = route({
|
|
107
|
+
method: "delete",
|
|
108
|
+
path: "/api/sessions/tokens/{tokenId}",
|
|
109
|
+
pattern: ["api", "sessions", "tokens", null],
|
|
110
|
+
summary: "Revoke an ephemeral session token",
|
|
111
|
+
description: "Revokes an aseph_ bearer token by ID. No-op if already revoked.",
|
|
112
|
+
tags: ["Sessions"],
|
|
113
|
+
params: z.object({ tokenId: z.string() }),
|
|
114
|
+
responses: {
|
|
115
|
+
204: { description: "Token revoked (or was already revoked)" },
|
|
116
|
+
401: { description: "Unauthorized" },
|
|
117
|
+
},
|
|
118
|
+
auth: { apiKey: true },
|
|
119
|
+
rbac: {
|
|
120
|
+
ungated: "operator-only: revocation is gated on the full operator key in auth middleware",
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
71
124
|
const listSessions = route({
|
|
72
125
|
method: "get",
|
|
73
126
|
path: "/api/sessions",
|
|
@@ -136,6 +189,35 @@ export async function handleSessions(
|
|
|
136
189
|
pathSegments: string[],
|
|
137
190
|
queryParams: URLSearchParams,
|
|
138
191
|
): Promise<boolean> {
|
|
192
|
+
if (mintSessionTokenRoute.match(req.method, pathSegments)) {
|
|
193
|
+
// Restrict to the full operator key — aseph_ and aswt_ bearers must not be
|
|
194
|
+
// able to mint new session tokens.
|
|
195
|
+
if (getRequestAuth(req)?.kind !== "operator") {
|
|
196
|
+
jsonError(res, "Unauthorized", 401);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
const parsed = await mintSessionTokenRoute.parse(req, res, pathSegments, queryParams);
|
|
200
|
+
if (!parsed) return true;
|
|
201
|
+
const { agentId, taskId, ttlMs } = parsed.body;
|
|
202
|
+
const token = await mintSessionToken(agentId, taskId, ttlMs);
|
|
203
|
+
mintSessionTokenRoute.respond(res, 200, { tokenId: token.tokenId, plaintext: token.plaintext });
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (revokeSessionTokenRoute.match(req.method, pathSegments)) {
|
|
208
|
+
// Restrict revocation to the operator key as well.
|
|
209
|
+
if (getRequestAuth(req)?.kind !== "operator") {
|
|
210
|
+
jsonError(res, "Unauthorized", 401);
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
const parsed = await revokeSessionTokenRoute.parse(req, res, pathSegments, queryParams);
|
|
214
|
+
if (!parsed) return true;
|
|
215
|
+
await revokeSessionToken(parsed.params.tokenId);
|
|
216
|
+
res.writeHead(204);
|
|
217
|
+
res.end();
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
139
221
|
if (listSessions.match(req.method, pathSegments)) {
|
|
140
222
|
const parsed = await listSessions.parse(req, res, pathSegments, queryParams);
|
|
141
223
|
if (!parsed) return true;
|
package/src/http/tasks.ts
CHANGED
|
@@ -515,7 +515,9 @@ const listPausedTasks = route({
|
|
|
515
515
|
responses: {
|
|
516
516
|
200: {
|
|
517
517
|
description: "Paused task list",
|
|
518
|
-
schema: z.object({
|
|
518
|
+
schema: z.object({
|
|
519
|
+
tasks: z.array(AgentTaskSchema.extend({ attachments: z.array(TaskAttachmentSchema) })),
|
|
520
|
+
}),
|
|
519
521
|
},
|
|
520
522
|
},
|
|
521
523
|
});
|
|
@@ -1449,7 +1451,13 @@ export async function handleTasks(
|
|
|
1449
1451
|
return true;
|
|
1450
1452
|
}
|
|
1451
1453
|
const pausedTasks = await getPausedTasksForAgent(myAgentId);
|
|
1452
|
-
|
|
1454
|
+
const tasks = await Promise.all(
|
|
1455
|
+
pausedTasks.map(async (task) => ({
|
|
1456
|
+
...task,
|
|
1457
|
+
attachments: await getTaskAttachments(task.id),
|
|
1458
|
+
})),
|
|
1459
|
+
);
|
|
1460
|
+
listPausedTasks.respond(res, 200, { tasks });
|
|
1453
1461
|
return true;
|
|
1454
1462
|
}
|
|
1455
1463
|
|
package/src/http/utils.ts
CHANGED
|
@@ -1,19 +1,97 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { getActiveTaskCount } from "../be/db";
|
|
3
3
|
import type { SwarmSpan } from "../otel";
|
|
4
|
+
import { isEnvFlagEnabled } from "../utils/env-flag";
|
|
4
5
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
5
6
|
|
|
7
|
+
const DEFAULT_ALLOWED_ORIGINS = [
|
|
8
|
+
"https://*.agent-swarm.dev",
|
|
9
|
+
"https://*.agent-swarm.cloud",
|
|
10
|
+
"http://localhost:5274",
|
|
11
|
+
"http://127.0.0.1:5274",
|
|
12
|
+
"http://[::1]:5274",
|
|
13
|
+
"https://ui.swarm.localhost:1355",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
let warnedAboutAllowAnyOrigin = false;
|
|
17
|
+
|
|
18
|
+
/** Warn at boot (after config injection), or on first use of the deployment opt-out. */
|
|
19
|
+
export function warnIfCorsAllowsAnyOrigin(): void {
|
|
20
|
+
if (isEnvFlagEnabled("CORS_ALLOW_ANY_ORIGIN", false) && !warnedAboutAllowAnyOrigin) {
|
|
21
|
+
warnedAboutAllowAnyOrigin = true;
|
|
22
|
+
console.warn(
|
|
23
|
+
"[CORS] CORS_ALLOW_ANY_ORIGIN=true: any request origin can receive non-credentialed responses; cookie-authenticated responses still require CORS_ALLOWED_ORIGINS. Set CORS_ALLOWED_ORIGINS and disable CORS_ALLOW_ANY_ORIGIN to restrict access.",
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Re-read env on each request so Configuration reloads take effect immediately. */
|
|
29
|
+
function getAllowedOrigins(): string[] {
|
|
30
|
+
const raw = process.env.CORS_ALLOWED_ORIGINS;
|
|
31
|
+
if (!raw || !raw.trim()) return DEFAULT_ALLOWED_ORIGINS;
|
|
32
|
+
return raw
|
|
33
|
+
.split(",")
|
|
34
|
+
.map((entry) => entry.trim())
|
|
35
|
+
.filter(Boolean);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Decide whether `origin` may receive credentialed CORS headers.
|
|
40
|
+
* Unset or blank uses the built-in hosted/dev allowlist. Exact entries retain
|
|
41
|
+
* their case-sensitive string comparison. A single leading `*.` matches one or
|
|
42
|
+
* more complete hostname labels, case-insensitively, but never the apex (list
|
|
43
|
+
* it separately). Wildcard schemes and explicit ports must match exactly.
|
|
44
|
+
* Bare `*`, `https://*`, other wildcard positions, and non-origin URLs are ignored.
|
|
45
|
+
*/
|
|
46
|
+
export function isOriginAllowedForCredentials(origin: string): boolean {
|
|
47
|
+
const allowed = getAllowedOrigins().some((entry) => {
|
|
48
|
+
if (!entry.includes("*")) return entry === origin;
|
|
49
|
+
|
|
50
|
+
// Fixed parsers, never a regex interpolated from operator-controlled text.
|
|
51
|
+
const pattern = /^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/\*\.([a-zA-Z0-9.-]+)(:\d+)?$/.exec(entry);
|
|
52
|
+
const candidate = /^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([a-zA-Z0-9.-]+)(:\d+)?$/.exec(origin);
|
|
53
|
+
if (!pattern || !candidate) return false;
|
|
54
|
+
if (pattern[1] !== candidate[1] || pattern[3] !== candidate[3]) return false;
|
|
55
|
+
|
|
56
|
+
const suffix = pattern[2]!.toLowerCase();
|
|
57
|
+
const hostname = candidate[2]!.toLowerCase();
|
|
58
|
+
// Reject empty or malformed labels instead of accepting suffix lookalikes.
|
|
59
|
+
const validLabel = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
60
|
+
if (![...suffix.split("."), ...hostname.split(".")].every((label) => validLabel.test(label))) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return hostname.endsWith(`.${suffix}`);
|
|
64
|
+
});
|
|
65
|
+
if (!allowed) {
|
|
66
|
+
console.warn(
|
|
67
|
+
`[CORS] Denied credentialed origin ${scrubSecrets(JSON.stringify(origin.slice(0, 512)))}; add the trusted SPA origin to CORS_ALLOWED_ORIGINS (custom values replace hosted/dev defaults).`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return allowed;
|
|
71
|
+
}
|
|
72
|
+
|
|
6
73
|
export function setCorsHeaders(req: IncomingMessage, res: ServerResponse) {
|
|
7
74
|
// Echo the request Origin (rather than emitting `*`) so credentialed fetches
|
|
8
75
|
// — e.g. the SPA's `credentials: 'include'` calls to `/p/:id.json` and the
|
|
9
76
|
// page-session cookie endpoints — pass the browser's CORS check. A wildcard
|
|
10
77
|
// would force the browser to reject any credentialed cross-origin response.
|
|
78
|
+
//
|
|
79
|
+
// Only allowlisted origins receive a credentialed CORS grant.
|
|
11
80
|
const rawOrigin = req.headers.origin;
|
|
12
81
|
const origin = Array.isArray(rawOrigin) ? rawOrigin[0] : rawOrigin;
|
|
82
|
+
const allowCredentials = origin ? isOriginAllowedForCredentials(origin) : false;
|
|
83
|
+
const allowAnyOrigin = isEnvFlagEnabled("CORS_ALLOW_ANY_ORIGIN", false);
|
|
84
|
+
if (allowAnyOrigin) warnIfCorsAllowsAnyOrigin();
|
|
85
|
+
if (origin && !allowCredentials && !allowAnyOrigin) {
|
|
86
|
+
res.setHeader("Vary", "Origin");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
13
89
|
if (origin) {
|
|
14
90
|
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
15
91
|
res.setHeader("Vary", "Origin");
|
|
16
|
-
|
|
92
|
+
// The compatibility flag never grants access to browser-supplied cookies.
|
|
93
|
+
// Bearer clients from unlisted origins must use credentials: "omit".
|
|
94
|
+
if (allowCredentials) res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
17
95
|
// When credentials are involved the spec disallows wildcards in
|
|
18
96
|
// Allow-Headers / Allow-Methods / Expose-Headers — they must be
|
|
19
97
|
// explicit. Echo whatever the preflight asked for (defensive default
|
|
@@ -74,7 +74,8 @@ Choose the path by the shape of the work:
|
|
|
74
74
|
Store progress with \`store-progress\` at each milestone. A milestone is a result the lead could act on.
|
|
75
75
|
The task is done when \`store-progress\` carries status \`completed\` and an \`output\` that names the result and every artifact link. On failure, status \`failed\` and a \`failureReason\` that names what you tried.
|
|
76
76
|
When the task carries an \`outputSchema\`, \`output\` is JSON that matches it.
|
|
77
|
-
When you are blocked after real effort, store the blocker with \`store-progress\` and keep working on what you can.
|
|
77
|
+
When you are blocked after real effort, store the blocker with \`store-progress\` and keep working on what you can.
|
|
78
|
+
The task has four endings. When you are done: \`completed\`. When the answer needs time (a build, a deploy, a reply): \`defer-task\` with a summary of what you did, when to wake up, and what to check. It completes the task now; the wake-up task continues it. When a person must decide: \`request-human-input\`. When nothing else is possible: \`failed\` with the blocker.
|
|
78
79
|
`,
|
|
79
80
|
variables: [],
|
|
80
81
|
category: "system",
|
|
@@ -89,9 +90,10 @@ registerTemplate({
|
|
|
89
90
|
Your output is delegation and review. Workers implement, research, analyze, and write. Data gathering, even a quick query, goes to a worker. You answer simple factual questions yourself.
|
|
90
91
|
|
|
91
92
|
\`get-swarm\` is the roster. Route by capability and load.
|
|
93
|
+
Delegate with \`send-task\`. Read a child's result with \`get-task-details\`.
|
|
92
94
|
A task states the goal, the repo URL when there is one, and the constraints. Workers know git, the skills, and \`store-progress\`.
|
|
93
95
|
Delegate by the shape of the work: a workflow for multi-step or fan-out work, a schedule for recurring work, a script for bulk data, an inline \`script-run\` for a one-off bulk job you can run yourself. The \`workflow-iterate\`, \`scheduling\`, and \`swarm-scripts\` skills build them.
|
|
94
|
-
Research or exploration: tell the worker to use
|
|
96
|
+
Research or exploration: tell the worker to use the \`researching\` skill. A large feature: a task for the \`planning\` skill first, then a task for the \`implementing\` skill with \`parentTaskId\`. A small fix: direct implementation.
|
|
95
97
|
A follow-up that continues earlier work carries \`parentTaskId\`. The worker receives the prior context.
|
|
96
98
|
A task whose result depends on the workers' output: wait for the children with the \`wait-for-task\` script, then merge and complete the task yourself. A turn that ends with children still running leaves the task unfinished.
|
|
97
99
|
|
|
@@ -191,10 +193,11 @@ registerTemplate({
|
|
|
191
193
|
defaultBody: `
|
|
192
194
|
## Outputs
|
|
193
195
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
+
Short answers: inline in the task output.
|
|
197
|
+
agent-fs is the shared drive between agents and people. Documents (reports, digests, summaries, specs, research, drafts): Markdown files via its CLI; see \`agent-fs\` skill.
|
|
198
|
+
Pages: only for polished artifacts shared with a wider audience or explicitly requested; see \`pages\` skill.
|
|
196
199
|
A tool a person will use, with data and actions: build an app. See the \`apps\` skill.
|
|
197
|
-
|
|
200
|
+
Link env: \`APP_URL\` for pages, \`MCP_BASE_URL\` for the API, \`AGENT_FS_LIVE_URL\` for files. Report missing variables.
|
|
198
201
|
`,
|
|
199
202
|
variables: [],
|
|
200
203
|
category: "system",
|
|
@@ -206,10 +209,11 @@ registerTemplate({
|
|
|
206
209
|
defaultBody: `
|
|
207
210
|
## Outputs
|
|
208
211
|
|
|
209
|
-
|
|
210
|
-
|
|
212
|
+
Short answers: inline in the task output.
|
|
213
|
+
Documents: Markdown task attachments; agent-fs is not configured here.
|
|
214
|
+
Pages: only for polished artifacts shared with a wider audience or explicitly requested; see \`pages\` skill.
|
|
211
215
|
A tool a person will use, with data and actions: build an app. See the \`apps\` skill.
|
|
212
|
-
|
|
216
|
+
Link env: \`APP_URL\` for pages, \`MCP_BASE_URL\` for the API. Report missing variables.
|
|
213
217
|
`,
|
|
214
218
|
variables: [],
|
|
215
219
|
category: "system",
|
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
type RequestPermissionResponse,
|
|
9
9
|
type SessionConfigOption,
|
|
10
10
|
type SessionNotification,
|
|
11
|
+
type Usage,
|
|
11
12
|
} from "@agentclientprotocol/sdk";
|
|
12
13
|
import pkg from "../../package.json";
|
|
13
14
|
import type { AcpSessionConfigOption } from "../types";
|
|
15
|
+
import { mintAcpSessionToken, revokeAcpSessionToken } from "../utils/acp-session-token";
|
|
14
16
|
import { fetchInstalledMcpServers } from "../utils/mcp-server-fetcher";
|
|
15
17
|
import {
|
|
16
18
|
detachedProcessGroup,
|
|
@@ -175,6 +177,10 @@ class ACPSession implements ProviderSession {
|
|
|
175
177
|
private output = "";
|
|
176
178
|
private completionPromise: Promise<ProviderResult>;
|
|
177
179
|
private completionResolve!: (result: ProviderResult) => void;
|
|
180
|
+
/** ID of the ephemeral aseph_ token minted for this session, or null when
|
|
181
|
+
* the fallback operator key is in use (e.g. during tests or if the API is
|
|
182
|
+
* not yet upgraded). Revoked in finish(). */
|
|
183
|
+
private ephemeralTokenId: string | null = null;
|
|
178
184
|
|
|
179
185
|
constructor(
|
|
180
186
|
private readonly connection: ClientSideConnection,
|
|
@@ -182,8 +188,10 @@ class ACPSession implements ProviderSession {
|
|
|
182
188
|
private readonly config: ProviderSessionConfig,
|
|
183
189
|
sessionId: string,
|
|
184
190
|
providerMeta?: Record<string, unknown>,
|
|
191
|
+
ephemeralTokenId?: string,
|
|
185
192
|
) {
|
|
186
193
|
this.sessionId = sessionId;
|
|
194
|
+
this.ephemeralTokenId = ephemeralTokenId ?? null;
|
|
187
195
|
this.completionPromise = new Promise((resolve) => {
|
|
188
196
|
this.completionResolve = resolve;
|
|
189
197
|
});
|
|
@@ -258,8 +266,21 @@ class ACPSession implements ProviderSession {
|
|
|
258
266
|
sessionId: this.sessionId,
|
|
259
267
|
prompt: [{ type: "text", text: this.config.prompt }],
|
|
260
268
|
});
|
|
269
|
+
this.emit({
|
|
270
|
+
type: "raw_log",
|
|
271
|
+
content: serializeAcpLog({
|
|
272
|
+
type: "custom",
|
|
273
|
+
name: "acp_prompt_response",
|
|
274
|
+
data: {
|
|
275
|
+
sessionId: this.sessionId,
|
|
276
|
+
stopReason: response.stopReason,
|
|
277
|
+
usage: response.usage ?? null,
|
|
278
|
+
_meta: response._meta,
|
|
279
|
+
},
|
|
280
|
+
}),
|
|
281
|
+
});
|
|
261
282
|
const isError = response.stopReason === "refusal" || response.stopReason === "cancelled";
|
|
262
|
-
const cost = this.buildCostData(isError);
|
|
283
|
+
const cost = this.buildCostData(isError, response.usage);
|
|
263
284
|
result = {
|
|
264
285
|
exitCode: isError ? 1 : 0,
|
|
265
286
|
sessionId: this.sessionId,
|
|
@@ -302,12 +323,16 @@ class ACPSession implements ProviderSession {
|
|
|
302
323
|
}
|
|
303
324
|
}
|
|
304
325
|
|
|
305
|
-
private buildCostData(isError: boolean): CostData {
|
|
326
|
+
private buildCostData(isError: boolean, usage?: Usage | null): CostData {
|
|
306
327
|
return {
|
|
307
328
|
sessionId: this.sessionId,
|
|
308
329
|
taskId: this.config.taskId,
|
|
309
330
|
agentId: this.config.agentId,
|
|
310
331
|
totalCostUsd: 0,
|
|
332
|
+
inputTokens: usage?.inputTokens,
|
|
333
|
+
outputTokens: usage?.outputTokens,
|
|
334
|
+
cacheReadTokens: usage?.cachedReadTokens ?? undefined,
|
|
335
|
+
cacheWriteTokens: usage?.cachedWriteTokens ?? undefined,
|
|
311
336
|
durationMs: Date.now() - this.startedAt,
|
|
312
337
|
numTurns: 1,
|
|
313
338
|
model: this.config.model,
|
|
@@ -320,6 +345,11 @@ class ACPSession implements ProviderSession {
|
|
|
320
345
|
if (this.completed) return;
|
|
321
346
|
this.completed = true;
|
|
322
347
|
this.completionResolve(result);
|
|
348
|
+
// Revoke the ephemeral token now that the session is done. Best-effort:
|
|
349
|
+
// the token expires on its own, so a failure here is not critical.
|
|
350
|
+
if (this.ephemeralTokenId) {
|
|
351
|
+
void revokeAcpSessionToken(this.config.apiUrl, this.config.apiKey, this.ephemeralTokenId);
|
|
352
|
+
}
|
|
323
353
|
}
|
|
324
354
|
}
|
|
325
355
|
|
|
@@ -355,12 +385,23 @@ export class ACPAdapter implements ProviderAdapter {
|
|
|
355
385
|
const stream = ndJsonStream(fileSinkWritableStream(proc.stdin), proc.stdout);
|
|
356
386
|
const connection = new ClientSideConnection(() => client, stream);
|
|
357
387
|
|
|
388
|
+
let ephemeralToken: { tokenId: string; plaintext: string } | null = null;
|
|
358
389
|
try {
|
|
359
390
|
await connection.initialize({
|
|
360
391
|
protocolVersion: PROTOCOL_VERSION,
|
|
361
392
|
clientInfo: { name: "agent-swarm", version: pkg.version },
|
|
362
393
|
clientCapabilities: { session: { configOptions: { boolean: {} } } },
|
|
363
394
|
});
|
|
395
|
+
// Mint a short-lived session-scoped bearer so the ACP target receives
|
|
396
|
+
// an aseph_ token rather than the full operator key. Throws on failure
|
|
397
|
+
// so we fail closed instead of falling back to the operator key.
|
|
398
|
+
ephemeralToken = await mintAcpSessionToken(
|
|
399
|
+
config.apiUrl,
|
|
400
|
+
config.apiKey,
|
|
401
|
+
config.agentId,
|
|
402
|
+
config.taskId,
|
|
403
|
+
);
|
|
404
|
+
|
|
364
405
|
const installedServers = await fetchInstalledMcpServers(
|
|
365
406
|
config.apiUrl,
|
|
366
407
|
config.apiKey,
|
|
@@ -370,7 +411,7 @@ export class ACPAdapter implements ProviderAdapter {
|
|
|
370
411
|
// The v2 draft (@agentclientprotocol/sdk/experimental/v2) adds a fourth
|
|
371
412
|
// `{ type: "acp", name, serverId }` variant where the client hosts the MCP
|
|
372
413
|
// server itself and the agent tunnels MCP over the existing ACP connection
|
|
373
|
-
// (mcp/connect, mcp/message, mcp/disconnect) instead of a network hop
|
|
414
|
+
// (mcp/connect, mcp/message, mcp/disconnect) instead of a network hop -- the
|
|
374
415
|
// shape for an ACP agent with no network route to the swarm API. Gated on
|
|
375
416
|
// `mcpCapabilities.acp` and UNSTABLE; not adopted here.
|
|
376
417
|
const newSession = await connection.newSession({
|
|
@@ -381,7 +422,7 @@ export class ACPAdapter implements ProviderAdapter {
|
|
|
381
422
|
name: "swarm",
|
|
382
423
|
url: `${config.apiUrl.replace(/\/+$/, "")}/mcp`,
|
|
383
424
|
headers: [
|
|
384
|
-
{ name: "Authorization", value: `Bearer ${
|
|
425
|
+
{ name: "Authorization", value: `Bearer ${ephemeralToken.plaintext}` },
|
|
385
426
|
{ name: "X-Agent-ID", value: config.agentId },
|
|
386
427
|
{ name: "X-Source-Task-Id", value: config.taskId },
|
|
387
428
|
],
|
|
@@ -395,13 +436,26 @@ export class ACPAdapter implements ProviderAdapter {
|
|
|
395
436
|
newSession.configOptions ?? [],
|
|
396
437
|
target.configuredOptions(config),
|
|
397
438
|
);
|
|
398
|
-
session = new ACPSession(
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
439
|
+
session = new ACPSession(
|
|
440
|
+
connection,
|
|
441
|
+
proc,
|
|
442
|
+
config,
|
|
443
|
+
newSession.sessionId,
|
|
444
|
+
{
|
|
445
|
+
target: target.target,
|
|
446
|
+
configOptions: sanitizeAcpConfigOptions(configOptions),
|
|
447
|
+
},
|
|
448
|
+
ephemeralToken.tokenId,
|
|
449
|
+
);
|
|
402
450
|
for (const event of preSessionEvents) session.emitFromAcp(event);
|
|
403
451
|
return session;
|
|
404
452
|
} catch (err) {
|
|
453
|
+
// Revoke the ephemeral token before re-throwing if ACPSession has not yet
|
|
454
|
+
// taken ownership of it (i.e. setup failed after mint but before the
|
|
455
|
+
// ACPSession constructor ran).
|
|
456
|
+
if (ephemeralToken && !session) {
|
|
457
|
+
void revokeAcpSessionToken(config.apiUrl, config.apiKey, ephemeralToken.tokenId);
|
|
458
|
+
}
|
|
405
459
|
await terminateProcessGroup(proc.pid);
|
|
406
460
|
throw new Error(`ACP target failed during startup: ${scrubSecrets(formatError(err))}`);
|
|
407
461
|
}
|
|
@@ -191,19 +191,36 @@ export async function checkPiMonoCredentials(
|
|
|
191
191
|
env: Record<string, string | undefined>,
|
|
192
192
|
opts: CredCheckOptions = {},
|
|
193
193
|
): Promise<CredStatus> {
|
|
194
|
-
// Determine Bedrock
|
|
195
|
-
// - Explicit: BEDROCK_AUTH_MODE=sdk
|
|
194
|
+
// Determine the Bedrock mode:
|
|
195
|
+
// - Explicit: BEDROCK_AUTH_MODE=sdk — the AWS SDK default credential chain
|
|
196
|
+
// - Explicit: BEDROCK_AUTH_MODE=bearer — an explicit Bedrock API key in
|
|
197
|
+
// AWS_BEARER_TOKEN_BEDROCK, which the AWS SDK picks up as the
|
|
198
|
+
// bearer identity for the Bedrock clients
|
|
196
199
|
// - Fallback: BEDROCK_AUTH_MODE absent AND MODEL_OVERRIDE starts with
|
|
197
200
|
// "amazon-bedrock/" (preserves today's prefix-inference semantics)
|
|
198
|
-
//
|
|
199
|
-
//
|
|
201
|
+
// Both explicit modes share the region check and the enumeration probe; they
|
|
202
|
+
// differ in what has to be present up front and in how readiness is reported.
|
|
200
203
|
const bedrockAuthMode = env.BEDROCK_AUTH_MODE?.toLowerCase();
|
|
204
|
+
const isBedrockBearer = bedrockAuthMode === "bearer";
|
|
201
205
|
const isBedrockSdk =
|
|
202
206
|
bedrockAuthMode === "sdk" ||
|
|
203
207
|
(bedrockAuthMode === undefined &&
|
|
204
208
|
env.MODEL_OVERRIDE?.toLowerCase().startsWith("amazon-bedrock/"));
|
|
205
209
|
|
|
206
|
-
if (
|
|
210
|
+
if (isBedrockBearer && !env.AWS_BEARER_TOKEN_BEDROCK) {
|
|
211
|
+
// The token is the whole point of this mode; without it there is nothing
|
|
212
|
+
// the probe could authenticate with, and the standard API keys do not
|
|
213
|
+
// apply to Bedrock, so do not fall through to them.
|
|
214
|
+
return {
|
|
215
|
+
ready: false,
|
|
216
|
+
missing: ["AWS_BEARER_TOKEN_BEDROCK"],
|
|
217
|
+
hint: "BEDROCK_AUTH_MODE=bearer requires AWS_BEARER_TOKEN_BEDROCK (a Bedrock API key); set it, or use BEDROCK_AUTH_MODE=sdk to authenticate through the AWS credential chain.",
|
|
218
|
+
bedrockModels: [],
|
|
219
|
+
bedrockRegion: env.AWS_REGION ?? "",
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (isBedrockSdk || isBedrockBearer) {
|
|
207
224
|
const region = env.AWS_REGION;
|
|
208
225
|
if (!region) {
|
|
209
226
|
// Do NOT fabricate a region. A guessed `us-east-1` can differ from where
|
|
@@ -231,7 +248,7 @@ export async function checkPiMonoCredentials(
|
|
|
231
248
|
return {
|
|
232
249
|
ready: true,
|
|
233
250
|
missing: [],
|
|
234
|
-
satisfiedBy: "sdk-delegated",
|
|
251
|
+
satisfiedBy: isBedrockBearer ? "env" : "sdk-delegated",
|
|
235
252
|
hint: `Bedrock models invocable in ${region} enumerated (${bedrockModels.length} usable; ListFoundationModels + ListInferenceProfiles).`,
|
|
236
253
|
bedrockModels,
|
|
237
254
|
bedrockRegion: region,
|
|
@@ -139,6 +139,17 @@ and heartbeat-classified roots are omitted, and the metrics remain separate.
|
|
|
139
139
|
(`webSearchRequests`, per-model `costUSD`) degrade per-field without
|
|
140
140
|
invalidating the entry.
|
|
141
141
|
|
|
142
|
+
- **ACP usage:** optional `session/prompt` response usage supplies `inputTokens`,
|
|
143
|
+
`outputTokens`, `cachedReadTokens`, and `cachedWriteTokens`. The adapter maps
|
|
144
|
+
those counters directly; absent counters remain `undefined` only at the adapter
|
|
145
|
+
boundary. The session-cost API and DB coalesce missing input/output/cache
|
|
146
|
+
counters to zero, so persisted costs and UI displays lose unknown-vs-zero. The scrubbed
|
|
147
|
+
`acp_prompt_response` session log preserves the payload and optional `_meta`.
|
|
148
|
+
Context-window `usage_update` totals are not billing token estimates.
|
|
149
|
+
ACP reports no USD amount through `CostData`; unresolved `(acp, model)`
|
|
150
|
+
pricing identity stays `unpriced`. A target may fall back after rejecting
|
|
151
|
+
the requested model, so that requested ID alone cannot justify a rate alias.
|
|
152
|
+
|
|
142
153
|
## When a model is missing
|
|
143
154
|
|
|
144
155
|
If `POST /api/session-costs` arrives with a `(provider, model)` pair that has
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import { unlinkSync } from "node:fs";
|
|
3
|
-
import { closeDb, createScheduledTask, initDb } from "../be/db";
|
|
4
|
-
import { calculateNextRun } from "./scheduler";
|
|
3
|
+
import { closeDb, createAgent, createScheduledTask, createTaskExtended, initDb } from "../be/db";
|
|
4
|
+
import { calculateNextRun, createStandaloneScheduleTask } from "./scheduler";
|
|
5
5
|
|
|
6
6
|
const TEST_DB_PATH = "./test-scheduler.sqlite";
|
|
7
7
|
|
|
@@ -151,3 +151,50 @@ describe("calculateNextRun", () => {
|
|
|
151
151
|
expect(nextRun).toBe("2026-01-19T09:00:00.000Z");
|
|
152
152
|
});
|
|
153
153
|
});
|
|
154
|
+
|
|
155
|
+
describe("createStandaloneScheduleTask", () => {
|
|
156
|
+
test("passes the schedule's parentTaskId through to the created task", async () => {
|
|
157
|
+
const agent = await createAgent({
|
|
158
|
+
name: "Deferred Wake-up Worker",
|
|
159
|
+
description: "Agent that owns the deferred task",
|
|
160
|
+
role: "worker",
|
|
161
|
+
isLead: false,
|
|
162
|
+
status: "idle",
|
|
163
|
+
maxTasks: 1,
|
|
164
|
+
capabilities: [],
|
|
165
|
+
});
|
|
166
|
+
const parent = await createTaskExtended("original work that needed time", {
|
|
167
|
+
agentId: agent.id,
|
|
168
|
+
source: "mcp",
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const schedule = await createScheduledTask({
|
|
172
|
+
name: "test-deferred-parent",
|
|
173
|
+
taskTemplate: `Resume task ${parent.id}: the deploy should be green now`,
|
|
174
|
+
scheduleType: "one_time",
|
|
175
|
+
nextRunAt: new Date(Date.now() + 60_000).toISOString(),
|
|
176
|
+
targetAgentId: agent.id,
|
|
177
|
+
createdByAgentId: agent.id,
|
|
178
|
+
taskType: "deferred",
|
|
179
|
+
tags: ["deferred"],
|
|
180
|
+
parentTaskId: parent.id,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const task = await createStandaloneScheduleTask(schedule);
|
|
184
|
+
expect(task.parentTaskId).toBe(parent.id);
|
|
185
|
+
expect(task.taskType).toBe("deferred");
|
|
186
|
+
expect(task.tags).toContain("deferred");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("a schedule without parentTaskId creates a parentless task", async () => {
|
|
190
|
+
const schedule = await createScheduledTask({
|
|
191
|
+
name: "test-no-parent",
|
|
192
|
+
taskTemplate: "Ordinary scheduled work",
|
|
193
|
+
cronExpression: "0 9 * * *",
|
|
194
|
+
timezone: "UTC",
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const task = await createStandaloneScheduleTask(schedule);
|
|
198
|
+
expect(task.parentTaskId).toBeFalsy();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -73,6 +73,10 @@ export async function createStandaloneScheduleTask(
|
|
|
73
73
|
source: "schedule",
|
|
74
74
|
requestedByUserId: schedule.createdBy,
|
|
75
75
|
contextKey: scheduleContextKey({ scheduleId: schedule.id }),
|
|
76
|
+
// Set only by `defer-task`. An explicit parent wins over the sibling-awareness
|
|
77
|
+
// auto-wiring (see withSiblingAwareness in src/tasks/sibling-awareness.ts), so
|
|
78
|
+
// the wake-up run continues the deferred task rather than a random sibling.
|
|
79
|
+
parentTaskId: schedule.parentTaskId,
|
|
76
80
|
});
|
|
77
81
|
}
|
|
78
82
|
|
|
@@ -12,6 +12,7 @@ export const SDK_TOOL_NAME_MAP = {
|
|
|
12
12
|
task_list: "get-tasks",
|
|
13
13
|
task_get: "get-task-details",
|
|
14
14
|
task_storeProgress: "store-progress",
|
|
15
|
+
task_defer: "defer-task",
|
|
15
16
|
task_poll: "poll-task",
|
|
16
17
|
task_send: "send-task",
|
|
17
18
|
task_cancel: "cancel-task", // destructive
|
|
@@ -143,6 +143,14 @@ declare module "swarm-sdk" {
|
|
|
143
143
|
task_list(args?: Record<string, unknown>): Promise<unknown>;
|
|
144
144
|
task_get(args: { taskId: string }): Promise<unknown>;
|
|
145
145
|
task_storeProgress(args: Record<string, unknown>): Promise<unknown>;
|
|
146
|
+
task_defer(args: {
|
|
147
|
+
taskId: string;
|
|
148
|
+
delayMs?: number;
|
|
149
|
+
runAt?: string;
|
|
150
|
+
summary: string;
|
|
151
|
+
note: string;
|
|
152
|
+
checks?: string[];
|
|
153
|
+
}): Promise<unknown>;
|
|
146
154
|
task_poll(args?: Record<string, unknown>): Promise<unknown>;
|
|
147
155
|
// --- kv ---
|
|
148
156
|
kv_get<T = unknown>(args: {
|
|
@@ -125,6 +125,14 @@ declare module "swarm-sdk" {
|
|
|
125
125
|
task_list(args?: Record<string, unknown>): Promise<unknown>;
|
|
126
126
|
task_get(args: { taskId: string }): Promise<unknown>;
|
|
127
127
|
task_storeProgress(args: Record<string, unknown>): Promise<unknown>;
|
|
128
|
+
task_defer(args: {
|
|
129
|
+
taskId: string;
|
|
130
|
+
delayMs?: number;
|
|
131
|
+
runAt?: string;
|
|
132
|
+
summary: string;
|
|
133
|
+
note: string;
|
|
134
|
+
checks?: string[];
|
|
135
|
+
}): Promise<unknown>;
|
|
128
136
|
task_poll(args?: Record<string, unknown>): Promise<unknown>;
|
|
129
137
|
// --- kv ---
|
|
130
138
|
kv_get<T = unknown>(args: {
|
package/src/server.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { registerCreateMetricTool } from "./tools/create-metric";
|
|
|
26
26
|
import { registerCreatePageTool } from "./tools/create-page";
|
|
27
27
|
import { registerCredentialBindingsTool } from "./tools/credential-bindings";
|
|
28
28
|
import { registerDbQueryTool } from "./tools/db-query";
|
|
29
|
+
import { registerDeferTaskTool } from "./tools/defer-task";
|
|
29
30
|
import { registerDeleteChannelTool } from "./tools/delete-channel";
|
|
30
31
|
import { registerDeletePageTool } from "./tools/delete-page";
|
|
31
32
|
import { registerGetMetricsTool } from "./tools/get-metrics";
|
|
@@ -421,6 +422,7 @@ export async function createServer(opts: { scriptsOnly?: boolean; fullSurface?:
|
|
|
421
422
|
if (hasCapability("scheduling")) {
|
|
422
423
|
registerListSchedulesTool(server);
|
|
423
424
|
registerCreateScheduleTool(server);
|
|
425
|
+
registerDeferTaskTool(server);
|
|
424
426
|
registerUpdateScheduleTool(server);
|
|
425
427
|
registerPatchScheduleTool(server);
|
|
426
428
|
registerDeleteScheduleTool(server);
|