@desplega.ai/agent-swarm 2.4.3 → 2.4.5
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-1885dpr7.js → acp-adapter-2wdkktv6.js} +1 -2
- package/dist/{actions-pn76n32q.js → actions-c40ebcm4.js} +18 -18
- package/dist/{amp-adapter-af84rx1d.js → amp-adapter-wb67m95z.js} +2 -2
- package/dist/{app-6sv8mhzm.js → app-353vre8g.js} +3 -3
- package/dist/{assistant-sc2tf825.js → assistant-sj1njh23.js} +21 -21
- package/dist/{boot-reembed-y3m0ydzj.js → boot-reembed-b9c3az3c.js} +1 -1
- package/dist/{boot-reembed-gvxjs2py.js → boot-reembed-qdkvxeem.js} +1 -1
- package/dist/{boot-scrub-sweep-1j9mk05f.js → boot-scrub-sweep-m3wh8eks.js} +2 -2
- package/dist/{boot-seeds-37sp626b.js → boot-seeds-j2273s54.js} +4 -4
- package/dist/{claude-adapter-rp0jtnwr.js → claude-adapter-m7q0a085.js} +2 -2
- package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-5ptb6tzs.js} +2 -2
- package/dist/{claude-managed-setup-rdt248k7.js → claude-managed-setup-smrqk5j5.js} +3 -3
- package/dist/{claude-sdk-session-53rcw5we.js → claude-sdk-session-38xnz21e.js} +8284 -8267
- package/dist/{cli-6eyncg0y.js → cli-018r4jq6.js} +34 -6
- package/dist/{cli-sds5zzs8.js → cli-1f3qzw6y.js} +78 -12
- package/dist/{cli-f0p0f6ja.js → cli-1sp861s4.js} +7 -7
- package/dist/{cli-dm8p2dpf.js → cli-1syn2xy1.js} +10 -10
- package/dist/{cli-tkf4x6ha.js → cli-37j04k05.js} +11 -11
- package/dist/{cli-v87dr6e3.js → cli-3a45zcgm.js} +1 -1
- package/dist/{cli-1n9gdg56.js → cli-43x64hv3.js} +4 -4
- package/dist/{cli-p0qerd2r.js → cli-4zfqgjc5.js} +2 -2
- package/dist/{cli-aa3tb29r.js → cli-7c9qs530.js} +1 -1
- package/dist/{cli-9qtjfd60.js → cli-848hxdwj.js} +1 -1
- package/dist/{cli-1xsraagq.js → cli-94gwyg7m.js} +8 -8
- package/dist/{cli-0dxxwfkt.js → cli-9hstj09h.js} +17 -14
- package/dist/{cli-1012xb7j.js → cli-9yqgm9nd.js} +22 -22
- package/dist/{cli-pygdsgh8.js → cli-a92r1127.js} +1 -1
- package/dist/{cli-8vverzf7.js → cli-ba8ne1es.js} +5 -0
- package/dist/{cli-bxrqvpg5.js → cli-be2161h1.js} +7 -7
- package/dist/{cli-npfe9t1s.js → cli-bnes4z6b.js} +2 -2
- package/dist/{cli-aypbg9r2.js → cli-cxxe61f6.js} +1 -1
- package/dist/{cli-vq9gqarq.js → cli-dk44dxec.js} +1 -1
- package/dist/{cli-836ehxmt.js → cli-eww120g8.js} +3 -2
- package/dist/{cli-a0kcty51.js → cli-f01h25yt.js} +1 -1
- package/dist/{cli-mzkwkbdy.js → cli-f0g3pgvf.js} +1 -1
- package/dist/{cli-1ma335fz.js → cli-ftc1g5tj.js} +2 -2
- package/dist/{cli-4yef8ghv.js → cli-fv5hepyy.js} +3 -3
- package/dist/{cli-58vwn100.js → cli-g35m2xwm.js} +4 -4
- package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
- package/dist/{cli-gx0j2khs.js → cli-j99azg3g.js} +3 -3
- package/dist/{cli-er83cjbn.js → cli-k8g16gz3.js} +416 -116
- package/dist/{cli-qp2m4btv.js → cli-kpw04va0.js} +2 -2
- package/dist/{cli-8y99kfey.js → cli-ms85309d.js} +33 -15
- package/dist/{cli-kgnkyabk.js → cli-n72rg53p.js} +1555 -645
- package/dist/{cli-e4d78xsv.js → cli-p14m2cnp.js} +1 -1
- package/dist/{cli-1eszr343.js → cli-qfxc84gh.js} +1 -1
- package/dist/{cli-wk28hzqn.js → cli-qwhy2qma.js} +1 -1
- package/dist/{cli-73ezfhha.js → cli-st8aw1m6.js} +228 -11
- package/dist/{cli-zvbb4n0e.js → cli-tw4p90jb.js} +92 -192
- package/dist/{cli-038fga5k.js → cli-v0yrfmdh.js} +148 -183
- package/dist/{cli-6aen3ezb.js → cli-v8fjakwf.js} +1 -1
- package/dist/{cli-99jez72r.js → cli-w9etnnxk.js} +6 -1
- package/dist/{cli-vcgscaan.js → cli-wt0jf9hm.js} +217 -80
- package/dist/{cli-s9rr4gr2.js → cli-xpa64rek.js} +1065 -305
- package/dist/{cli-h0mgxann.js → cli-z7d29rw9.js} +2 -2
- package/dist/cli.js +45 -20
- package/dist/{codex-adapter-2zrmab9b.js → codex-adapter-tn6k5mkg.js} +1 -1
- package/dist/{codex-session-runner-kbkz62vc.js → codex-session-runner-7vjt7jbw.js} +1 -1
- package/dist/{commands-hd574th6.js → commands-t7rz9146.js} +2 -2
- package/dist/{controller-pz5mcfg4.js → controller-m2jasma5.js} +2 -2
- package/dist/{controllers-connect-5rgq2hfk.js → controllers-connect-9ftpex99.js} +19 -19
- package/dist/{cursor-adapter-mgaprq0j.js → cursor-adapter-mjv79e4z.js} +1 -1
- package/dist/{db-6nf1207q.js → db-6sfrn9g3.js} +1 -1
- package/dist/{db-h06dxp05.js → db-rn673r06.js} +3 -3
- package/dist/{dispatcher-zthpy4m1.js → dispatcher-c155xz0p.js} +17 -17
- package/dist/{dsh-adapter-64dhw0j7.js → dsh-adapter-3mmszc1x.js} +1 -1
- package/dist/{external-channel-x01t7ke6.js → external-channel-35k6tkv8.js} +2 -2
- package/dist/{grok-adapter-h21jv594.js → grok-adapter-tfrq7tg1.js} +2 -3
- package/dist/handlers-7gw64dxw.js +39 -0
- package/dist/{heartbeat-zzfksckz.js → heartbeat-dyg4npkp.js} +20 -20
- package/dist/{hook-z4zhzj1f.js → hook-drspbwpe.js} +1 -1
- package/dist/{hook-3xfqhh2s.js → hook-x26b31bg.js} +1 -1
- package/dist/{http-q813tmya.js → http-pk972xyw.js} +571 -294
- package/dist/{index-va3y63es.js → index-6srwvd5b.js} +1 -2
- package/dist/{index-x8xbyn1t.js → index-arytgj5q.js} +1 -1
- package/dist/{index-wbbs14c5.js → index-bgdyvcnv.js} +17 -17
- package/dist/{index-s98037v0.js → index-cbxkmfc5.js} +1 -2
- package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
- package/dist/{index-m0a4ydqj.js → index-gabbcdn2.js} +17 -17
- package/dist/{index-220tk9bw.js → index-jwxkkex2.js} +139 -76
- package/dist/index-tfb26zhy.js +41 -0
- package/dist/{keepalive-y5t1my7m.js → keepalive-7yrng7px.js} +3 -3
- package/dist/{lead-2xwtq66b.js → lead-7e02z839.js} +29 -30
- package/dist/{maintenance-ybfsaedv.js → maintenance-e6cemdqt.js} +1 -1
- package/dist/{node-3v3f08f7.js → node-de6akczr.js} +10 -9
- package/dist/{oauth-refresh-sweep-909jmn12.js → oauth-refresh-sweep-yw3xdbcj.js} +2 -2
- package/dist/{onboard-xn6fkkys.js → onboard-0a9y5yrc.js} +2 -2
- package/dist/{otel-impl-e6mn0z13.js → otel-impl-trjpkfb7.js} +1 -1
- package/dist/{pricing-refresh-ygd362ep.js → pricing-refresh-d2b5my0y.js} +3 -3
- package/dist/{rbac-roles-mb470p4e.js → rbac-roles-hey3yv9d.js} +2 -2
- package/dist/{rbac-roles-c9axvfnz.js → rbac-roles-nxh5ahwk.js} +2 -2
- package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
- package/dist/{render-v2-g0306nh1.js → render-v2-rr3p2atg.js} +3 -3
- package/dist/{resume-v315hnq6.js → resume-2efkf1s1.js} +5 -5
- package/dist/{session-access-2zg285a1.js → session-access-s2mttxpw.js} +6 -4
- package/dist/{setup-a4mtf02k.js → setup-vypma5yt.js} +1 -1
- package/dist/{work-objects-9fr0fgtn.js → work-objects-rvhckdkp.js} +20 -20
- package/dist/{worker-qae22jeb.js → worker-5snytpbd.js} +29 -30
- package/dist/worker-token-xf11yyds.js +136 -0
- package/openapi.json +303 -13
- package/package.json +8 -8
- package/src/apps/action-runner.ts +4 -0
- package/src/apps/catalog.generated.json +209 -4
- package/src/apps/page-validator.ts +21 -3
- package/src/be/chunking.ts +42 -9
- package/src/be/db/tasks/draft-text.ts +15 -0
- package/src/be/db/tasks/read.ts +14 -1
- package/src/be/db.ts +5 -1
- package/src/be/memory/index-content.ts +36 -17
- package/src/be/memory/providers/openai-embedding.ts +115 -61
- package/src/be/memory/providers/sqlite-store.ts +69 -23
- package/src/be/memory/reranker.ts +31 -1
- package/src/be/memory/search-candidates.ts +47 -0
- package/src/be/memory/types.ts +14 -0
- package/src/be/migrations/232_agent_session_tokens.sql +41 -0
- package/src/be/scripts/typecheck.ts +2 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +116 -55
- package/src/be/seed-scripts/index.ts +2 -2
- package/src/be/swarm-config-guard.ts +40 -1
- package/src/be/task-attachment-store.ts +30 -0
- package/src/be/users.ts +169 -12
- package/src/cli.tsx +24 -0
- package/src/commands/worker-token.ts +164 -0
- package/src/extensions/catalog.generated.json +4 -4
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +7 -0
- package/src/fs/provider.ts +13 -1
- package/src/heartbeat/heartbeat.ts +31 -11
- package/src/http/apps.ts +43 -5
- package/src/http/codex-oauth-keep-warm.ts +27 -9
- package/src/http/config.ts +5 -3
- package/src/http/core.ts +2 -0
- package/src/http/fs.ts +54 -34
- package/src/http/mcp-body.ts +37 -0
- package/src/http/mcp-user.ts +3 -5
- package/src/http/mcp.ts +6 -7
- package/src/http/memory.ts +26 -15
- package/src/http/page-proxy.ts +61 -5
- package/src/http/pages-public.ts +27 -44
- package/src/http/pages.ts +4 -4
- package/src/http/script-runs.ts +2 -0
- package/src/http/session-access.ts +51 -2
- package/src/http/sessions.ts +249 -10
- package/src/http/tasks.ts +4 -32
- package/src/http/users.ts +12 -1
- package/src/integrations/chat-sdk/adapters.ts +7 -3
- package/src/integrations/chat-sdk/attachments.ts +90 -12
- package/src/integrations/chat-sdk/facts.ts +120 -0
- package/src/integrations/chat-sdk/inbound.ts +156 -8
- package/src/integrations/chat-sdk/limits.ts +24 -0
- package/src/integrations/chat-sdk/outbound-files.ts +186 -0
- package/src/integrations/chat-sdk/outbound.ts +234 -4
- package/src/integrations/chat-sdk/relay.ts +4 -1
- package/src/integrations/chat-sdk/router.ts +28 -2
- package/src/integrations/chat-sdk/steering.ts +9 -0
- package/src/integrations/chat-sdk/templates.ts +14 -0
- package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
- package/src/mcp-limits.ts +20 -0
- package/src/pages/browser-sdk.ts +8 -3
- package/src/pages/feedback-overlay.ts +1 -1
- package/src/providers/acp-adapter.ts +33 -13
- package/src/providers/acp-swarm-events.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +106 -119
- package/src/providers/codex-agents-md.ts +35 -229
- package/src/providers/codex-oauth/storage.ts +1 -1
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/rbac/legacy-policy.ts +3 -1
- package/src/rbac/permissions.ts +4 -0
- package/src/realtime/transport.ts +1 -2
- package/src/script-workflows/workflow-ctx.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +2 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +8 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
- package/src/server-user.ts +2 -0
- package/src/server.ts +2 -0
- package/src/slack/assistant.ts +2 -2
- package/src/slack/files.ts +2 -0
- package/src/slack/handlers.ts +4 -4
- package/src/slack/inbound-files.ts +39 -6
- package/src/slack/thread-buffer.ts +2 -1
- package/src/tasks/task-terminal-effects.ts +21 -17
- package/src/tests/acp-adapter.test.ts +10 -23
- package/src/tests/acp-fake-agent.ts +36 -0
- package/src/tests/acp-swarm-events.test.ts +1 -1
- package/src/tests/agent-session-tokens.test.ts +452 -0
- package/src/tests/apps-action-parent-task.test.ts +295 -0
- package/src/tests/apps-card-grid.test.ts +197 -0
- package/src/tests/apps-catalog-additions.test.ts +120 -0
- package/src/tests/apps-tree.test.ts +129 -0
- package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
- package/src/tests/chat-sdk-config.test.ts +11 -0
- package/src/tests/chat-sdk-facts.test.ts +23 -0
- package/src/tests/chat-sdk-inbound.test.ts +558 -4
- package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
- package/src/tests/codex-adapter.test.ts +167 -242
- package/src/tests/codex-oauth-keep-warm.test.ts +85 -1
- package/src/tests/config-api-only-keys.test.ts +9 -0
- package/src/tests/config-session-auth.test.ts +14 -0
- package/src/tests/cors-allowlist.test.ts +1 -1
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
- package/src/tests/grok-adapter.test.ts +3 -3
- package/src/tests/heartbeat-checklist.test.ts +33 -0
- package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
- package/src/tests/heartbeat-jev-gate.test.ts +265 -1
- package/src/tests/kv-page-proxy.test.ts +91 -5
- package/src/tests/launch-password-rejection.test.ts +2 -2
- package/src/tests/mcp-input-caps.test.ts +294 -0
- package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
- package/src/tests/memory-embedding-input-cap.test.ts +114 -0
- package/src/tests/memory.test.ts +14 -0
- package/src/tests/migration-232-agent-session-tokens.test.ts +67 -0
- package/src/tests/page-guest-session.test.ts +19 -8
- package/src/tests/page-proxy-authed.test.ts +1 -1
- package/src/tests/page-proxy.test.ts +17 -10
- package/src/tests/page-session.test.ts +102 -1
- package/src/tests/pages-authed-mode.test.ts +82 -12
- package/src/tests/pages-password-mode.test.ts +5 -5
- package/src/tests/rbac-engine.test.ts +1 -0
- package/src/tests/realtime-transport.test.ts +13 -7
- package/src/tests/runtime-child-token-cleanup.test.ts +3 -3
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/script-runs-http.test.ts +84 -0
- package/src/tests/scripts-http.test.ts +10 -1
- package/src/tests/session-route-posture.test.ts +2 -0
- package/src/tests/swarm-config-export-to-env.test.ts +22 -3
- package/src/tests/task-audio-transcription.test.ts +299 -0
- package/src/tests/task-completion-memory-chunking.test.ts +434 -0
- package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
- package/src/tests/transcription.test.ts +299 -0
- package/src/tests/worker-token-cli.test.ts +76 -0
- package/src/tests/workflow-executors.test.ts +4 -2
- package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
- package/src/tests/workflow-system-one-decision.test.ts +2 -1
- package/src/tests/workflow-system-one-extraction.test.ts +805 -0
- package/src/tools/chat-access.ts +10 -0
- package/src/tools/chat-post.ts +31 -11
- package/src/tools/chat-reply.ts +32 -10
- package/src/tools/memory-search.ts +3 -1
- package/src/tools/system-one-decide.ts +17 -4
- package/src/types.ts +8 -1
- package/src/utils/config-env-export.ts +3 -1
- package/src/utils/page-session.ts +133 -20
- package/src/utils/request-auth-context.ts +2 -1
- package/src/utils/transcription.ts +246 -0
- package/src/workflows/definition.ts +7 -1
- package/src/workflows/engine.ts +18 -12
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/system-one-decision.ts +84 -262
- package/src/workflows/executors/system-one-extract-wire.ts +57 -0
- package/src/workflows/executors/system-one-extraction.ts +830 -0
- package/src/workflows/executors/system-one-providers.ts +316 -114
- package/src/workflows/executors/system-one-transport.ts +264 -0
- package/templates/extensions/slack-reply-gate/README.md +24 -22
- package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
- package/templates/extensions/slack-reply-gate/manifest.json +2 -2
- package/templates/official/lead/HEARTBEAT.md +1 -1
- package/templates/skills/apps/SKILL.md +31 -7
- package/templates/skills/apps/content.md +31 -7
- package/templates/skills/chat-sdk/content.md +6 -2
- package/templates/skills/code-quality/content.md +2 -1
- package/templates/skills/heartbeat-runbook/content.md +8 -8
- package/templates/skills/pages/content.md +6 -4
- package/templates/skills/swarm-scripts/SKILL.md +2 -2
- package/templates/skills/swarm-scripts/content.md +2 -2
- package/dist/cli-v1sjhn52.js +0 -7
- package/dist/handlers-2qjw434w.js +0 -39
- package/dist/index-d0g18bax.js +0 -41
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
4
|
+
import {
|
|
5
|
+
closeDb,
|
|
6
|
+
createAgent,
|
|
7
|
+
createTaskExtended,
|
|
8
|
+
createUser,
|
|
9
|
+
getAgentById,
|
|
10
|
+
getDbClient,
|
|
11
|
+
initDb,
|
|
12
|
+
upsertSwarmConfig,
|
|
13
|
+
} from "../be/db";
|
|
14
|
+
import { attachRole } from "../be/rbac-roles";
|
|
15
|
+
import { createRuntimeController } from "../be/runtime-pools/db";
|
|
16
|
+
import { mintControllerToken } from "../be/runtime-pools/tokens";
|
|
17
|
+
import {
|
|
18
|
+
AGENT_TOKEN_NO_EXPIRY,
|
|
19
|
+
type IdentityActor,
|
|
20
|
+
mintAgentToken,
|
|
21
|
+
mintRuntimeToken,
|
|
22
|
+
mintSessionToken,
|
|
23
|
+
mintToken,
|
|
24
|
+
resolveBySessionToken,
|
|
25
|
+
resolveSessionToken,
|
|
26
|
+
revokeAgentTokens,
|
|
27
|
+
} from "../be/users";
|
|
28
|
+
import { handleAgentRegister, handleAgentsRest } from "../http/agents";
|
|
29
|
+
import { handleConfig } from "../http/config";
|
|
30
|
+
import { handleCore } from "../http/core";
|
|
31
|
+
import { handlePoll } from "../http/poll";
|
|
32
|
+
import { handleSessions } from "../http/sessions";
|
|
33
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
34
|
+
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
35
|
+
import { registerSessionTokenConnection } from "../utils/session-token-connections";
|
|
36
|
+
import { listenOnFreePort } from "./test-net";
|
|
37
|
+
|
|
38
|
+
// Synthetic credentials, built at runtime so no literal looks like a real secret.
|
|
39
|
+
const key = `agent-token-test-${randomBytes(8).toString("hex")}`;
|
|
40
|
+
const synthetic = (label: string) => `synthetic-${label}-${randomBytes(12).toString("hex")}`;
|
|
41
|
+
const USER_ACTOR: IdentityActor = { kind: "operator", id: "agent-session-tokens-test" };
|
|
42
|
+
let server: Server;
|
|
43
|
+
let baseUrl: string;
|
|
44
|
+
let workerId: string;
|
|
45
|
+
let otherId: string;
|
|
46
|
+
let leadId: string;
|
|
47
|
+
let ownTaskId: string;
|
|
48
|
+
let otherTaskId: string;
|
|
49
|
+
|
|
50
|
+
async function request(
|
|
51
|
+
path: string,
|
|
52
|
+
bearer: string,
|
|
53
|
+
options: { method?: string; body?: unknown; headers?: Record<string, string> } = {},
|
|
54
|
+
) {
|
|
55
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
56
|
+
method: options.method ?? (options.body === undefined ? "GET" : "POST"),
|
|
57
|
+
headers: {
|
|
58
|
+
Authorization: `Bearer ${bearer}`,
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
...options.headers,
|
|
61
|
+
},
|
|
62
|
+
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
63
|
+
});
|
|
64
|
+
const text = await res.text();
|
|
65
|
+
let body: any = text;
|
|
66
|
+
try {
|
|
67
|
+
body = JSON.parse(text);
|
|
68
|
+
} catch {}
|
|
69
|
+
return { status: res.status, text, body };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function dispatch(req: IncomingMessage, res: ServerResponse) {
|
|
73
|
+
if (await handleCore(req, res, req.headers["x-agent-id"] as string | undefined, key)) return;
|
|
74
|
+
const segments = getPathSegments(req.url ?? "");
|
|
75
|
+
const query = parseQueryParams(req.url ?? "");
|
|
76
|
+
const id = req.headers["x-agent-id"] as string | undefined;
|
|
77
|
+
if (await handleAgentRegister(req, res, segments, id)) return;
|
|
78
|
+
if (await handlePoll(req, res, segments, query, id)) return;
|
|
79
|
+
if (await handleAgentsRest(req, res, segments, query, id)) return;
|
|
80
|
+
if (await handleSessions(req, res, segments, query)) return;
|
|
81
|
+
if (await handleConfig(req, res, segments, query)) return;
|
|
82
|
+
res.writeHead(404).end();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
beforeAll(async () => {
|
|
86
|
+
initDb(":memory:");
|
|
87
|
+
workerId = (await createAgent({ name: "token-worker", isLead: false, status: "idle" })).id;
|
|
88
|
+
otherId = (await createAgent({ name: "token-other", isLead: false, status: "idle" })).id;
|
|
89
|
+
leadId = (await createAgent({ name: "token-lead", isLead: true, status: "idle" })).id;
|
|
90
|
+
ownTaskId = (await createTaskExtended("Own task", { agentId: workerId, source: "system" })).id;
|
|
91
|
+
otherTaskId = (await createTaskExtended("Other task", { agentId: otherId, source: "system" })).id;
|
|
92
|
+
server = createServer((req, res) => {
|
|
93
|
+
void dispatch(req, res).catch(() => res.writeHead(500).end());
|
|
94
|
+
});
|
|
95
|
+
baseUrl = `http://127.0.0.1:${await listenOnFreePort(server)}`;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
afterAll(async () => {
|
|
99
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
100
|
+
closeDb();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("worker token management routes", () => {
|
|
104
|
+
test("operator mints a non-expiring agent token, once per agent", async () => {
|
|
105
|
+
const newId = crypto.randomUUID();
|
|
106
|
+
expect((await request(`/api/agents/${newId}/token`, key, { body: {} })).status).toBe(404);
|
|
107
|
+
|
|
108
|
+
const minted = await request(`/api/agents/${newId}/token`, key, {
|
|
109
|
+
body: { name: "minted-worker" },
|
|
110
|
+
});
|
|
111
|
+
expect(minted.status).toBe(200);
|
|
112
|
+
expect(minted.body).toMatchObject({ agentId: newId, expiresAt: null, agentCreated: true });
|
|
113
|
+
expect(await getAgentById(newId)).toMatchObject({
|
|
114
|
+
name: "minted-worker",
|
|
115
|
+
isLead: false,
|
|
116
|
+
status: "offline",
|
|
117
|
+
});
|
|
118
|
+
expect(await resolveBySessionToken(minted.body.plaintext)).toMatchObject({
|
|
119
|
+
tokenId: minted.body.tokenId,
|
|
120
|
+
tokenKind: "agent",
|
|
121
|
+
agentId: newId,
|
|
122
|
+
taskId: null,
|
|
123
|
+
runtimeId: null,
|
|
124
|
+
expiresAt: AGENT_TOKEN_NO_EXPIRY,
|
|
125
|
+
});
|
|
126
|
+
// The plaintext is registered with the scrubber at mint time.
|
|
127
|
+
expect(scrubSecrets(`token=${minted.body.plaintext}`)).not.toContain(minted.body.plaintext);
|
|
128
|
+
|
|
129
|
+
const again = await request(`/api/agents/${newId}/token`, key, { body: {} });
|
|
130
|
+
expect(again.status).toBe(409);
|
|
131
|
+
|
|
132
|
+
// The stored expiry is exact on every mint, so the active-token lookup
|
|
133
|
+
// (a string comparison) always finds the token the resolver accepts.
|
|
134
|
+
for (let i = 0; i < 25; i++) {
|
|
135
|
+
await revokeAgentTokens(newId);
|
|
136
|
+
const token = await mintAgentToken(newId);
|
|
137
|
+
expect((await resolveBySessionToken(token.plaintext))?.expiresAt).toBe(AGENT_TOKEN_NO_EXPIRY);
|
|
138
|
+
}
|
|
139
|
+
expect(await revokeAgentTokens(newId)).toBe(1);
|
|
140
|
+
|
|
141
|
+
const ttl = await request(`/api/agents/${otherId}/token`, key, { body: { ttlMs: 60_000 } });
|
|
142
|
+
expect(ttl.status).toBe(200);
|
|
143
|
+
expect(ttl.body.agentCreated).toBe(false);
|
|
144
|
+
expect(Date.parse(ttl.body.expiresAt)).toBeLessThanOrEqual(Date.now() + 60_000);
|
|
145
|
+
expect((await request(`/api/agents/${otherId}/token`, key, { method: "DELETE" })).body).toEqual(
|
|
146
|
+
{ revoked: 1 },
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("only an operator key without X-Agent-ID or a user manages worker tokens", async () => {
|
|
151
|
+
const own = await mintAgentToken(leadId);
|
|
152
|
+
const lead = await mintRuntimeToken(leadId, crypto.randomUUID(), 60_000);
|
|
153
|
+
const task = await mintSessionToken(workerId, ownTaskId, 60_000);
|
|
154
|
+
const path = `/api/agents/${workerId}/token`;
|
|
155
|
+
// Agent-first: the operator key acting as an agent, even the lead, is that agent.
|
|
156
|
+
for (const agentId of [workerId, leadId]) {
|
|
157
|
+
const res = await request(path, key, { body: {}, headers: { "X-Agent-ID": agentId } });
|
|
158
|
+
expect(res.status).toBe(403);
|
|
159
|
+
}
|
|
160
|
+
for (const bearer of [own.plaintext, lead.plaintext, task.plaintext]) {
|
|
161
|
+
for (const [method, suffix] of [
|
|
162
|
+
["POST", ""],
|
|
163
|
+
["POST", "/rotate"],
|
|
164
|
+
["DELETE", ""],
|
|
165
|
+
] as const) {
|
|
166
|
+
const res = await request(`${path}${suffix}`, bearer, {
|
|
167
|
+
method,
|
|
168
|
+
body: method === "POST" ? {} : undefined,
|
|
169
|
+
});
|
|
170
|
+
expect(res.status).toBe(403);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
expect((await request(`/api/agents/${leadId}/token`, key, { method: "DELETE" })).body).toEqual({
|
|
174
|
+
revoked: 1,
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("rotation keeps the old token for the grace window, then it expires", async () => {
|
|
179
|
+
expect((await request(`/api/agents/${workerId}/token/rotate`, key, { body: {} })).status).toBe(
|
|
180
|
+
404,
|
|
181
|
+
);
|
|
182
|
+
const first = await mintAgentToken(workerId);
|
|
183
|
+
const firstChild = await mintSessionToken(workerId, ownTaskId, 7 * 86_400_000, {
|
|
184
|
+
tokenId: first.tokenId,
|
|
185
|
+
runtimeId: null,
|
|
186
|
+
});
|
|
187
|
+
const rotated = await request(`/api/agents/${workerId}/token/rotate`, key, {
|
|
188
|
+
body: { graceSec: 60 },
|
|
189
|
+
});
|
|
190
|
+
expect(rotated.status).toBe(200);
|
|
191
|
+
expect(rotated.body.expiresAt).toBeNull();
|
|
192
|
+
expect(Date.parse(rotated.body.previousExpiresAt)).toBeLessThanOrEqual(Date.now() + 60_000);
|
|
193
|
+
expect((await request("/api/poll", first.plaintext)).status).toBe(200);
|
|
194
|
+
expect((await request("/api/poll", rotated.body.plaintext)).status).toBe(200);
|
|
195
|
+
// A child of the rotated token expires with it, not at its own longer TTL.
|
|
196
|
+
expect((await resolveBySessionToken(firstChild.plaintext))?.expiresAt).toBe(
|
|
197
|
+
rotated.body.previousExpiresAt,
|
|
198
|
+
);
|
|
199
|
+
const rotatedChild = await mintSessionToken(workerId, ownTaskId, 7 * 86_400_000, {
|
|
200
|
+
tokenId: rotated.body.tokenId,
|
|
201
|
+
runtimeId: null,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const again = await request(`/api/agents/${workerId}/token/rotate`, key, {
|
|
205
|
+
body: { graceSec: 0 },
|
|
206
|
+
});
|
|
207
|
+
expect(again.status).toBe(200);
|
|
208
|
+
expect((await request("/api/poll", rotated.body.plaintext)).status).toBe(401);
|
|
209
|
+
expect((await request("/api/poll", rotatedChild.plaintext)).status).toBe(401);
|
|
210
|
+
expect((await request("/api/poll", again.body.plaintext)).status).toBe(200);
|
|
211
|
+
expect(
|
|
212
|
+
(await request(`/api/agents/${workerId}/token`, key, { method: "DELETE" })).body,
|
|
213
|
+
).toEqual({ revoked: 1 });
|
|
214
|
+
expect((await request("/api/poll", again.body.plaintext)).status).toBe(401);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("a user without a grant-all role needs the agent's privileges and never mints a lead's", async () => {
|
|
218
|
+
const roleId = `token-manager-${crypto.randomUUID()}`;
|
|
219
|
+
await getDbClient().run(
|
|
220
|
+
"INSERT INTO roles (id, name, description, isBuiltin, grantsAll) VALUES (?, ?, '', 0, 0)",
|
|
221
|
+
[roleId, roleId],
|
|
222
|
+
);
|
|
223
|
+
const grant = (verb: string) =>
|
|
224
|
+
getDbClient().run("INSERT INTO role_permissions (roleId, verb) VALUES (?, ?)", [
|
|
225
|
+
roleId,
|
|
226
|
+
verb,
|
|
227
|
+
]);
|
|
228
|
+
await grant("agent.token.manage");
|
|
229
|
+
const limited = await createUser({ name: "Token manager" });
|
|
230
|
+
// New users start as admin (rbac-roles insert trigger); narrow this one.
|
|
231
|
+
await getDbClient().run("DELETE FROM principal_roles WHERE principalId = ?", [limited.id]);
|
|
232
|
+
await getDbClient().run(
|
|
233
|
+
"INSERT INTO principal_roles (principalType, principalId, roleId) VALUES ('user', ?, ?)",
|
|
234
|
+
[limited.id, roleId],
|
|
235
|
+
);
|
|
236
|
+
const limitedToken = (await mintToken(limited.id, "limited", USER_ACTOR)).plaintext;
|
|
237
|
+
const admin = await createUser({ name: "Token admin" });
|
|
238
|
+
await attachRole(admin.id, "admin");
|
|
239
|
+
const adminToken = (await mintToken(admin.id, "admin", USER_ACTOR)).plaintext;
|
|
240
|
+
|
|
241
|
+
// agent.token.manage alone would hand out the agent's privileges and secret reads.
|
|
242
|
+
for (const agentId of [workerId, leadId]) {
|
|
243
|
+
const res = await request(`/api/agents/${agentId}/token`, limitedToken, { body: {} });
|
|
244
|
+
expect(res.status).toBe(403);
|
|
245
|
+
}
|
|
246
|
+
await grant("agent.act.any");
|
|
247
|
+
await grant("config.read.secrets");
|
|
248
|
+
const minted = await request(`/api/agents/${workerId}/token`, limitedToken, { body: {} });
|
|
249
|
+
expect(minted.status).toBe(200);
|
|
250
|
+
expect(minted.body.agentId).toBe(workerId);
|
|
251
|
+
for (const [method, suffix] of [
|
|
252
|
+
["POST", ""],
|
|
253
|
+
["POST", "/rotate"],
|
|
254
|
+
["DELETE", ""],
|
|
255
|
+
] as const) {
|
|
256
|
+
const res = await request(`/api/agents/${leadId}/token${suffix}`, limitedToken, {
|
|
257
|
+
method,
|
|
258
|
+
body: method === "POST" ? {} : undefined,
|
|
259
|
+
});
|
|
260
|
+
expect(res.status).toBe(403);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const lead = await request(`/api/agents/${leadId}/token`, adminToken, { body: {} });
|
|
264
|
+
expect(lead.status).toBe(200);
|
|
265
|
+
for (const agentId of [workerId, leadId]) {
|
|
266
|
+
expect(
|
|
267
|
+
(await request(`/api/agents/${agentId}/token`, adminToken, { method: "DELETE" })).body,
|
|
268
|
+
).toEqual({ revoked: 1 });
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("worker token as an agent principal", () => {
|
|
274
|
+
test("binds X-Agent-ID and X-Source-Task-Id to the token agent", async () => {
|
|
275
|
+
const token = await mintAgentToken(workerId);
|
|
276
|
+
try {
|
|
277
|
+
expect((await request("/api/poll", token.plaintext)).status).toBe(200);
|
|
278
|
+
const spoofed = await request("/api/poll", token.plaintext, {
|
|
279
|
+
headers: { "X-Agent-ID": otherId },
|
|
280
|
+
});
|
|
281
|
+
expect(spoofed.status).toBe(403);
|
|
282
|
+
const foreignTask = await request("/api/config/resolved", token.plaintext, {
|
|
283
|
+
headers: { "X-Source-Task-Id": otherTaskId },
|
|
284
|
+
});
|
|
285
|
+
expect(foreignTask.status).toBe(403);
|
|
286
|
+
const ownTask = await request("/api/config/resolved", token.plaintext, {
|
|
287
|
+
headers: { "X-Source-Task-Id": ownTaskId },
|
|
288
|
+
});
|
|
289
|
+
expect(ownTask.status).toBe(200);
|
|
290
|
+
// Registration accepts the token for its own agent and cannot promote it.
|
|
291
|
+
const registered = await request("/api/agents", token.plaintext, {
|
|
292
|
+
body: { name: "token-worker", isLead: true },
|
|
293
|
+
});
|
|
294
|
+
expect(registered.status).toBeLessThan(300);
|
|
295
|
+
expect((await getAgentById(workerId))?.isLead).toBe(false);
|
|
296
|
+
} finally {
|
|
297
|
+
await request(`/api/agents/${workerId}/token`, key, { method: "DELETE" });
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("mints and revokes child task tokens for its own assigned task only", async () => {
|
|
302
|
+
const token = await mintAgentToken(workerId);
|
|
303
|
+
const child = await request("/api/sessions/tokens", token.plaintext, {
|
|
304
|
+
body: { agentId: workerId, taskId: ownTaskId, ttlMs: 60_000 },
|
|
305
|
+
});
|
|
306
|
+
expect(child.status).toBe(200);
|
|
307
|
+
expect(await resolveBySessionToken(child.body.plaintext)).toMatchObject({
|
|
308
|
+
tokenKind: "task",
|
|
309
|
+
taskId: ownTaskId,
|
|
310
|
+
});
|
|
311
|
+
for (const body of [
|
|
312
|
+
{ agentId: workerId, taskId: otherTaskId, ttlMs: 60_000 },
|
|
313
|
+
{ agentId: otherId, taskId: otherTaskId, ttlMs: 60_000 },
|
|
314
|
+
{ agentId: workerId, runtimeId: crypto.randomUUID(), ttlMs: 60_000 },
|
|
315
|
+
]) {
|
|
316
|
+
expect((await request("/api/sessions/tokens", token.plaintext, { body })).status).toBe(403);
|
|
317
|
+
}
|
|
318
|
+
// A child task token cannot mint further tokens.
|
|
319
|
+
const grandchild = await request("/api/sessions/tokens", child.body.plaintext, {
|
|
320
|
+
body: { agentId: workerId, taskId: ownTaskId, ttlMs: 60_000 },
|
|
321
|
+
});
|
|
322
|
+
expect(grandchild.status).toBe(401);
|
|
323
|
+
|
|
324
|
+
const foreign = await mintSessionToken(workerId, ownTaskId, 60_000);
|
|
325
|
+
const revokeForeign = await request(
|
|
326
|
+
`/api/sessions/tokens/${foreign.tokenId}`,
|
|
327
|
+
token.plaintext,
|
|
328
|
+
{ method: "DELETE" },
|
|
329
|
+
);
|
|
330
|
+
expect(revokeForeign.status).toBe(403);
|
|
331
|
+
const revokeOwn = await request(`/api/sessions/tokens/${child.body.tokenId}`, token.plaintext, {
|
|
332
|
+
method: "DELETE",
|
|
333
|
+
});
|
|
334
|
+
expect(revokeOwn.status).toBe(204);
|
|
335
|
+
expect(await resolveBySessionToken(child.body.plaintext)).toBeNull();
|
|
336
|
+
await request(`/api/agents/${workerId}/token`, key, { method: "DELETE" });
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("revoking the agent token revokes its children and closes their connections", async () => {
|
|
340
|
+
for (const revoke of ["agent-route", "session-route"] as const) {
|
|
341
|
+
const token = await mintAgentToken(workerId);
|
|
342
|
+
const child = await request("/api/sessions/tokens", token.plaintext, {
|
|
343
|
+
body: { agentId: workerId, taskId: ownTaskId, ttlMs: 60_000 },
|
|
344
|
+
});
|
|
345
|
+
expect(child.status).toBe(200);
|
|
346
|
+
const closed: string[] = [];
|
|
347
|
+
const unregister = [token.tokenId, child.body.tokenId].map((id) =>
|
|
348
|
+
registerSessionTokenConnection(id, () => closed.push(id)),
|
|
349
|
+
);
|
|
350
|
+
try {
|
|
351
|
+
const res =
|
|
352
|
+
revoke === "agent-route"
|
|
353
|
+
? await request(`/api/agents/${workerId}/token`, key, { method: "DELETE" })
|
|
354
|
+
: await request(`/api/sessions/tokens/${token.tokenId}`, key, { method: "DELETE" });
|
|
355
|
+
expect(res.status).toBeLessThan(300);
|
|
356
|
+
expect((await request("/api/poll", token.plaintext)).status).toBe(401);
|
|
357
|
+
expect((await request("/api/poll", child.body.plaintext)).status).toBe(401);
|
|
358
|
+
expect(closed.sort()).toEqual([token.tokenId, child.body.tokenId].sort());
|
|
359
|
+
} finally {
|
|
360
|
+
for (const fn of unregister) fn();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe("secret read scope by session token kind", () => {
|
|
367
|
+
const globalSecret = synthetic("global");
|
|
368
|
+
const ownSecret = synthetic("own");
|
|
369
|
+
const otherSecret = synthetic("other");
|
|
370
|
+
|
|
371
|
+
beforeAll(async () => {
|
|
372
|
+
await upsertSwarmConfig({
|
|
373
|
+
scope: "global",
|
|
374
|
+
key: "AGENT_TOKEN_TEST_GLOBAL",
|
|
375
|
+
value: globalSecret,
|
|
376
|
+
isSecret: true,
|
|
377
|
+
});
|
|
378
|
+
for (const [scopeId, value] of [
|
|
379
|
+
[workerId, ownSecret],
|
|
380
|
+
[otherId, otherSecret],
|
|
381
|
+
] as const) {
|
|
382
|
+
await upsertSwarmConfig({
|
|
383
|
+
scope: "agent",
|
|
384
|
+
scopeId,
|
|
385
|
+
key: "AGENT_TOKEN_TEST_AGENT",
|
|
386
|
+
value,
|
|
387
|
+
isSecret: true,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const resolved = (bearer: string, agentId?: string) =>
|
|
393
|
+
request(
|
|
394
|
+
`/api/config/resolved?includeSecrets=true${agentId ? `&agentId=${agentId}` : ""}`,
|
|
395
|
+
bearer,
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
test("agent and runtime tokens read their own scope, defaulting agentId to the token", async () => {
|
|
399
|
+
const agent = await mintAgentToken(workerId);
|
|
400
|
+
const runtime = await mintRuntimeToken(workerId, crypto.randomUUID(), 60_000);
|
|
401
|
+
try {
|
|
402
|
+
for (const bearer of [agent.plaintext, runtime.plaintext]) {
|
|
403
|
+
for (const agentId of [undefined, workerId]) {
|
|
404
|
+
const res = await resolved(bearer, agentId);
|
|
405
|
+
expect(res.status).toBe(200);
|
|
406
|
+
expect(res.text).toContain(globalSecret);
|
|
407
|
+
expect(res.text).toContain(ownSecret);
|
|
408
|
+
expect(res.text).not.toContain(otherSecret);
|
|
409
|
+
}
|
|
410
|
+
const foreign = await resolved(bearer, otherId);
|
|
411
|
+
expect(foreign.status).toBe(200);
|
|
412
|
+
expect(foreign.text).not.toContain(otherSecret);
|
|
413
|
+
expect(foreign.text).not.toContain(globalSecret);
|
|
414
|
+
}
|
|
415
|
+
} finally {
|
|
416
|
+
await request(`/api/agents/${workerId}/token`, key, { method: "DELETE" });
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("task tokens read none, controller tokens are refused, the operator reads all", async () => {
|
|
421
|
+
const task = await mintSessionToken(workerId, ownTaskId, 60_000);
|
|
422
|
+
for (const agentId of [undefined, workerId]) {
|
|
423
|
+
const res = await resolved(task.plaintext, agentId);
|
|
424
|
+
expect(res.status).toBe(200);
|
|
425
|
+
expect(res.text).not.toContain(globalSecret);
|
|
426
|
+
expect(res.text).not.toContain(ownSecret);
|
|
427
|
+
expect(res.text).toContain("secret values masked");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const controllerId = (
|
|
431
|
+
await createRuntimeController({ name: "remote", kind: "docker", placement: "remote" })
|
|
432
|
+
).id;
|
|
433
|
+
const controller = await mintControllerToken(controllerId);
|
|
434
|
+
expect((await resolveSessionToken(controller.plaintext))?.kind).toBe("controller");
|
|
435
|
+
expect((await resolved(controller.plaintext, workerId)).status).toBe(403);
|
|
436
|
+
|
|
437
|
+
// Positive control: the same request with the operator key returns plaintext.
|
|
438
|
+
const operator = await resolved(key, otherId);
|
|
439
|
+
expect(operator.status).toBe(200);
|
|
440
|
+
expect(operator.text).toContain(otherSecret);
|
|
441
|
+
expect(operator.text).toContain(globalSecret);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("an unknown aseph_ token is unauthenticated", async () => {
|
|
445
|
+
const random = `aseph_${randomBytes(24).toString("base64url")}`;
|
|
446
|
+
expect(await resolveSessionToken(random)).toBeNull();
|
|
447
|
+
for (const path of ["/api/config/resolved?includeSecrets=true", "/api/poll"]) {
|
|
448
|
+
expect((await request(path, random)).status).toBe(401);
|
|
449
|
+
}
|
|
450
|
+
expect((await request(`/api/agents/${workerId}/token`, random, { body: {} })).status).toBe(401);
|
|
451
|
+
});
|
|
452
|
+
});
|