@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
package/src/tools/chat-access.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
1
2
|
import { getAgentById, getAgentCurrentTask, getTaskById } from "@/be/db";
|
|
2
3
|
import { can } from "@/rbac";
|
|
3
4
|
import { chatThreadOfContextKey } from "@/tasks/context-key";
|
|
4
5
|
import type { RequestInfo } from "@/tools/utils";
|
|
5
6
|
|
|
7
|
+
/** The `attachments` input of `chat-reply` and `chat-post`. */
|
|
8
|
+
export const chatAttachmentsSchema = z
|
|
9
|
+
.array(z.string().min(1))
|
|
10
|
+
.max(10)
|
|
11
|
+
.optional()
|
|
12
|
+
.describe(
|
|
13
|
+
"Files to send after the text, up to 10: attachment ids of your current task (from get-task-details) or file paths in your agent-fs drive, such as thoughts/<agent-id>/report.pdf, read with your own agent-fs credential. Telegram shows JPEG and PNG as photos, MP4 as video, MP3 and M4A as audio, and anything else as a document. A file over the platform upload limit (Telegram 50 MB, Discord 10 MB) is not sent, and the thread gets a note naming it.",
|
|
14
|
+
);
|
|
15
|
+
|
|
6
16
|
/** Chat SDK verbs; the legacy policy grants them to the lead only. */
|
|
7
17
|
export type ChatVerb = "integration.chat.post" | "integration.chat.read" | "integration.chat.dm";
|
|
8
18
|
|
package/src/tools/chat-post.ts
CHANGED
|
@@ -2,8 +2,13 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import { CHAT_SDK_ADAPTER_NAMES } from "@/integrations/chat-sdk/config";
|
|
4
4
|
import { errorMessage } from "@/integrations/chat-sdk/logger";
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
adapterOfChatId,
|
|
7
|
+
postChatMessage,
|
|
8
|
+
unsentFilesSummary,
|
|
9
|
+
} from "@/integrations/chat-sdk/outbound";
|
|
10
|
+
import { resolveChatAttachmentRefs } from "@/integrations/chat-sdk/outbound-files";
|
|
11
|
+
import { canUseChatVerb, chatAttachmentsSchema } from "@/tools/chat-access";
|
|
7
12
|
import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
|
|
8
13
|
|
|
9
14
|
export const registerChatPostTool = (server: McpServer) => {
|
|
@@ -27,6 +32,7 @@ export const registerChatPostTool = (server: McpServer) => {
|
|
|
27
32
|
.optional()
|
|
28
33
|
.describe("Full SDK channel id, such as slack:C123. Ignored when threadId is set."),
|
|
29
34
|
text: z.string().min(1).max(20_000).describe("Markdown text to post."),
|
|
35
|
+
attachments: chatAttachmentsSchema,
|
|
30
36
|
}),
|
|
31
37
|
outputSchema: swarmToolOutputSchema({
|
|
32
38
|
yourAgentId: z.string().optional(),
|
|
@@ -34,9 +40,12 @@ export const registerChatPostTool = (server: McpServer) => {
|
|
|
34
40
|
adapter: z.string().optional(),
|
|
35
41
|
threadId: z.string().optional(),
|
|
36
42
|
channelId: z.string().optional(),
|
|
43
|
+
unsentFiles: z
|
|
44
|
+
.array(z.looseObject({ name: z.string().optional(), reason: z.string().optional() }))
|
|
45
|
+
.optional(),
|
|
37
46
|
}),
|
|
38
47
|
},
|
|
39
|
-
async ({ adapter, threadId, channelId, text }, requestInfo) => {
|
|
48
|
+
async ({ adapter, threadId, channelId, text, attachments }, requestInfo) => {
|
|
40
49
|
const data = { yourAgentId: requestInfo.agentId };
|
|
41
50
|
const target = threadId ?? channelId;
|
|
42
51
|
if (!target) return toolErr("Give a threadId or a channelId.", { data });
|
|
@@ -49,22 +58,33 @@ export const registerChatPostTool = (server: McpServer) => {
|
|
|
49
58
|
{ data },
|
|
50
59
|
);
|
|
51
60
|
}
|
|
61
|
+
const resolved = await resolveChatAttachmentRefs(
|
|
62
|
+
attachments ?? [],
|
|
63
|
+
requestInfo.sourceTaskId,
|
|
64
|
+
requestInfo.agentId,
|
|
65
|
+
);
|
|
66
|
+
if ("error" in resolved) return toolErr(resolved.error, { data });
|
|
52
67
|
try {
|
|
53
68
|
const sent = await postChatMessage({
|
|
54
69
|
adapter,
|
|
55
70
|
...(threadId ? { threadId } : { channelId }),
|
|
56
71
|
text,
|
|
57
72
|
taskId: requestInfo.sourceTaskId,
|
|
73
|
+
files: resolved.files,
|
|
58
74
|
});
|
|
59
|
-
return toolOk(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
return toolOk(
|
|
76
|
+
`Posted to ${adapter} ${threadId ? "thread" : "channel"} ${target}.${unsentFilesSummary(sent.unsentFiles)}`,
|
|
77
|
+
{
|
|
78
|
+
data: {
|
|
79
|
+
...data,
|
|
80
|
+
messageId: sent.messageIds.at(-1),
|
|
81
|
+
adapter: sent.adapter,
|
|
82
|
+
threadId: sent.threadId,
|
|
83
|
+
channelId: sent.channelId,
|
|
84
|
+
unsentFiles: sent.unsentFiles,
|
|
85
|
+
},
|
|
66
86
|
},
|
|
67
|
-
|
|
87
|
+
);
|
|
68
88
|
} catch (error) {
|
|
69
89
|
const message = errorMessage(error);
|
|
70
90
|
return toolErr(`Could not post: ${message}`, { data });
|
package/src/tools/chat-reply.ts
CHANGED
|
@@ -2,8 +2,14 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import { getAgentCurrentTask, getTaskById } from "@/be/db";
|
|
4
4
|
import { errorMessage } from "@/integrations/chat-sdk/logger";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
ChatAdapterUnavailableError,
|
|
7
|
+
postChatMessage,
|
|
8
|
+
unsentFilesSummary,
|
|
9
|
+
} from "@/integrations/chat-sdk/outbound";
|
|
10
|
+
import { resolveChatAttachmentRefs } from "@/integrations/chat-sdk/outbound-files";
|
|
6
11
|
import { chatThreadOfContextKey } from "@/tasks/context-key";
|
|
12
|
+
import { chatAttachmentsSchema } from "@/tools/chat-access";
|
|
7
13
|
import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
|
|
8
14
|
|
|
9
15
|
export const registerChatReplyTool = (server: McpServer) => {
|
|
@@ -12,19 +18,23 @@ export const registerChatReplyTool = (server: McpServer) => {
|
|
|
12
18
|
{
|
|
13
19
|
title: "Reply in the chat thread of this task",
|
|
14
20
|
description:
|
|
15
|
-
"Post a message to the Telegram, Slack, Discord or Teams thread your current task came from. Works only on a task whose contextKey starts with task:chat-sdk:. The task's final output is posted to the thread automatically when the task ends, so use this only for a question to the user
|
|
21
|
+
"Post a message to the Telegram, Slack, Discord or Teams thread your current task came from. Works only on a task whose contextKey starts with task:chat-sdk:. The task's final output is posted to the thread automatically when the task ends, so use this only for a question to the user, a short progress note on long work, or to send files (attachments). Markdown is supported; long text is split into several messages. When the final output of a task is posted, the files attached to that task go with it.",
|
|
16
22
|
annotations: { openWorldHint: true },
|
|
17
23
|
inputSchema: z.object({
|
|
18
24
|
text: z.string().min(1).max(20_000).describe("Markdown text to post to the thread."),
|
|
25
|
+
attachments: chatAttachmentsSchema,
|
|
19
26
|
}),
|
|
20
27
|
outputSchema: swarmToolOutputSchema({
|
|
21
28
|
yourAgentId: z.string().optional(),
|
|
22
29
|
messageId: z.string().optional(),
|
|
23
30
|
adapter: z.string().optional(),
|
|
24
31
|
threadId: z.string().optional(),
|
|
32
|
+
unsentFiles: z
|
|
33
|
+
.array(z.looseObject({ name: z.string().optional(), reason: z.string().optional() }))
|
|
34
|
+
.optional(),
|
|
25
35
|
}),
|
|
26
36
|
},
|
|
27
|
-
async ({ text }, requestInfo) => {
|
|
37
|
+
async ({ text, attachments }, requestInfo) => {
|
|
28
38
|
if (!requestInfo.agentId) return toolErr("Agent ID not found.");
|
|
29
39
|
const data = { yourAgentId: requestInfo.agentId };
|
|
30
40
|
|
|
@@ -46,21 +56,33 @@ export const registerChatReplyTool = (server: McpServer) => {
|
|
|
46
56
|
);
|
|
47
57
|
}
|
|
48
58
|
|
|
59
|
+
const resolved = await resolveChatAttachmentRefs(
|
|
60
|
+
attachments ?? [],
|
|
61
|
+
task.id,
|
|
62
|
+
requestInfo.agentId,
|
|
63
|
+
);
|
|
64
|
+
if ("error" in resolved) return toolErr(resolved.error, { data });
|
|
65
|
+
|
|
49
66
|
try {
|
|
50
67
|
const sent = await postChatMessage({
|
|
51
68
|
adapter: thread.adapter,
|
|
52
69
|
threadId: thread.threadId,
|
|
53
70
|
text,
|
|
54
71
|
taskId: task.id,
|
|
72
|
+
files: resolved.files,
|
|
55
73
|
});
|
|
56
|
-
return toolOk(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
74
|
+
return toolOk(
|
|
75
|
+
`Posted to ${thread.adapter} thread ${sent.threadId}.${unsentFilesSummary(sent.unsentFiles)}`,
|
|
76
|
+
{
|
|
77
|
+
data: {
|
|
78
|
+
...data,
|
|
79
|
+
messageId: sent.messageIds.at(-1),
|
|
80
|
+
adapter: sent.adapter,
|
|
81
|
+
threadId: sent.threadId,
|
|
82
|
+
unsentFiles: sent.unsentFiles,
|
|
83
|
+
},
|
|
62
84
|
},
|
|
63
|
-
|
|
85
|
+
);
|
|
64
86
|
} catch (error) {
|
|
65
87
|
const message = errorMessage(error);
|
|
66
88
|
return toolErr(
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
recordRetrievals,
|
|
12
12
|
} from "@/be/memory/raters/retrieval";
|
|
13
13
|
import { rerank } from "@/be/memory/reranker";
|
|
14
|
+
import { searchMemoryCandidates } from "@/be/memory/search-candidates";
|
|
14
15
|
import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
|
|
15
16
|
import type { AgentMemorySource } from "@/types";
|
|
16
17
|
import { AgentMemoryScopeSchema, AgentMemorySourceSchema } from "@/types";
|
|
@@ -122,7 +123,8 @@ export const registerMemorySearchTool = (server: McpServer) => {
|
|
|
122
123
|
const queryEmbedding = await provider.embed(query);
|
|
123
124
|
|
|
124
125
|
const candidateLimit = limit * CANDIDATE_SET_MULTIPLIER;
|
|
125
|
-
const candidates = await
|
|
126
|
+
const candidates = await searchMemoryCandidates(
|
|
127
|
+
store,
|
|
126
128
|
queryEmbedding ?? new Float32Array(0),
|
|
127
129
|
requestInfo.agentId,
|
|
128
130
|
{
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { SYSTEM_ONE_PROVIDER_IDS } from "@/workflows/executors/system-one-providers";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* One typed decision (Jev and the other SystemOne hosts) made in the API
|
|
13
|
+
* One typed decision (Jev, Clef and the other SystemOne hosts) made in the API
|
|
14
14
|
* process, the same way a `system-one-decision` workflow node makes it. The
|
|
15
15
|
* provider credential is read and sent here and never reaches the caller, so a
|
|
16
16
|
* script can ask for a decision without a credential binding in its own realm.
|
|
@@ -22,14 +22,16 @@ export const registerSystemOneDecideTool = (server: McpServer) => {
|
|
|
22
22
|
title: "Ask a SystemOne decision model",
|
|
23
23
|
annotations: { destructiveHint: false, openWorldHint: true },
|
|
24
24
|
description:
|
|
25
|
-
"Send a state and typed questions (noul, choice, score) to a SystemOne decision host such as Jev, and return the validated answers. " +
|
|
25
|
+
"Send a state and typed questions (noul, choice, score) to a SystemOne decision host such as Jev or Clef, and return the validated answers. " +
|
|
26
26
|
"Same request, provider list, credentials and validation as the `system-one-decision` workflow node, without its human review band. " +
|
|
27
27
|
"The provider credential is a global secret that stays on the server; this tool never returns it.",
|
|
28
28
|
inputSchema: z.object({
|
|
29
29
|
provider: z
|
|
30
30
|
.enum(SYSTEM_ONE_PROVIDER_IDS)
|
|
31
31
|
.optional()
|
|
32
|
-
.describe(
|
|
32
|
+
.describe(
|
|
33
|
+
"Decision host (default: the SYSTEM_ONE_DEFAULT_PROVIDER global config, typesafe when unset)",
|
|
34
|
+
),
|
|
33
35
|
model: z.string().min(1).optional().describe("Provider model id (default: the provider's)"),
|
|
34
36
|
state: z
|
|
35
37
|
.union([z.string(), z.array(z.string()), z.record(z.string(), z.unknown())])
|
|
@@ -55,6 +57,12 @@ export const registerSystemOneDecideTool = (server: McpServer) => {
|
|
|
55
57
|
answers: z.record(z.string(), z.unknown()).optional(),
|
|
56
58
|
usage: z.record(z.string(), z.unknown()).optional(),
|
|
57
59
|
requestId: z.string().optional(),
|
|
60
|
+
providerUnavailable: z
|
|
61
|
+
.boolean()
|
|
62
|
+
.optional()
|
|
63
|
+
.describe(
|
|
64
|
+
"On an error: true when this deployment refuses the provider (e.g. OpenRouter behind a gateway) and no request was sent, so a caller choosing automatically can try its next provider",
|
|
65
|
+
),
|
|
58
66
|
}),
|
|
59
67
|
},
|
|
60
68
|
async ({ provider, model, state, questions, timeoutMs, maxRetries }, requestInfo) => {
|
|
@@ -88,7 +96,12 @@ export const registerSystemOneDecideTool = (server: McpServer) => {
|
|
|
88
96
|
return toolErr("The system-one-decision executor is not available");
|
|
89
97
|
}
|
|
90
98
|
const decided = await executor.decide(parsed.data);
|
|
91
|
-
if (!decided.ok)
|
|
99
|
+
if (!decided.ok) {
|
|
100
|
+
return toolErr(
|
|
101
|
+
decided.error,
|
|
102
|
+
decided.deploymentRefused ? { data: { providerUnavailable: true } } : {},
|
|
103
|
+
);
|
|
104
|
+
}
|
|
92
105
|
|
|
93
106
|
const { output } = decided;
|
|
94
107
|
return toolOk(`Decision from ${output.model}`, {
|
package/src/types.ts
CHANGED
|
@@ -905,6 +905,12 @@ export const CreateTaskOptionsSchema = z.object({
|
|
|
905
905
|
* contract on completion (which would block the control task — DES-523).
|
|
906
906
|
*/
|
|
907
907
|
inheritParentOutputSchema: z.boolean().optional(),
|
|
908
|
+
/**
|
|
909
|
+
* When a `parentTaskId` is set and `requestedByUserId` is not, the child
|
|
910
|
+
* inherits the parent's requester by default. Set this to `false` when the
|
|
911
|
+
* child answers a different person (a chat message whose sender is unknown).
|
|
912
|
+
*/
|
|
913
|
+
inheritParentRequester: z.boolean().optional(),
|
|
908
914
|
/**
|
|
909
915
|
* Skip parent routing requirements only for a child with its own explicit
|
|
910
916
|
* Lead-only control-plane authorization.
|
|
@@ -2287,7 +2293,7 @@ export const WorkflowNodeSchema = z
|
|
|
2287
2293
|
type: z
|
|
2288
2294
|
.string()
|
|
2289
2295
|
.describe(
|
|
2290
|
-
"Executor type: 'agent-task', 'script', 'swarm-script', 'raw-llm', 'system-one-decision', 'validate', 'property-match'",
|
|
2296
|
+
"Executor type: 'agent-task', 'script', 'swarm-script', 'raw-llm', 'system-one-decision', 'system-one-extraction', 'validate', 'property-match'",
|
|
2291
2297
|
),
|
|
2292
2298
|
label: z.string().optional().describe("Human-readable label for UI display"),
|
|
2293
2299
|
config: z
|
|
@@ -2297,6 +2303,7 @@ export const WorkflowNodeSchema = z
|
|
|
2297
2303
|
"For script: { runtime, script, args?, timeout? }. " +
|
|
2298
2304
|
"For swarm-script: { scriptName, scope?, pinHash?, args?, fsMode?, timeoutMs? (1000-300000) }. " +
|
|
2299
2305
|
"For system-one-decision (typed decisions, Jev by default): { provider? ('typesafe' default | 'openrouter' | 'laya'; a literal, never a {{token}}), state, questions: { <id>: { type: 'noul'|'choice'|'score', instructions, criteria? } }, returns: { <id>: { type } }, model? (provider-specific; unset = the provider's default, and laya has none so it sends no model and picks its own checkpoint), timeoutMs?, maxRetries? (0-3), humanReview? { band: { min, max } (0-1, inclusive), approvers: { users?, roles?, policy }, title?, timeout?, notifications? } }; each provider needs its own global secret (TYPESAFE_API_KEY, OPENROUTER_API_KEY, or LAYA_API_KEY; laya also needs the global config LAYA_URL), and a save warns and a run fails before any node executes when one is missing; system-one-decision nodes must not set retry or validation.retry. With humanReview, an answer whose confidence is inside the band waits for a person (human-in-the-loop approval), and next must map ports { approved, rejected?, timeout? }. " +
|
|
2306
|
+
"For system-one-extraction (typed extraction, Cloudflare Clairic by default): { provider? ('cloudflare'; a literal), state (text to read; may be empty when images are sent), fields: { <id>: { type: 'text'|'number'|'date'|'time'|'datetime'|'duration'|'list'|'estimate'|'forecast', instructions?, ...type options (number: unit?, integer?, decimals?; duration: unit? (seconds..years), integer?, decimals?; list: items ('text'|'number'|'date'); estimate: min, max, step?, scale?, unit?; forecast: min, max, horizon?, interval?, scale?, unit?; text/number/date/time/datetime/duration: null_threshold?) } }, images? (max 4 base64 data: URLs or { content_type, base64 }), referenceDate? (YYYY-MM-DD), model?, timeoutMs?, maxRetries? (0-3) }; output answers.<id> carries value (null when the input does not say), guess, p_null, confidence, span/spans, and for estimate/forecast median/p10/p90. Needs the global secret CLOUDFLARE_API_TOKEN (or CLOUDFLARE_API_KEY) and the global config CLOUDFLARE_ACCOUNT_ID; must not set retry or validation.retry. " +
|
|
2300
2307
|
"Agent-task templates and ordinary config values support {{interpolation}} from the node's inputs context, including trigger and declared upstream aliases. " +
|
|
2301
2308
|
"SECURITY: executable source for script/swarm-script nodes does not interpolate trigger.* or upstream node outputs; only input/workflow/swarm/run values are allowed in inline script source, and named swarm-script source is not workflow-interpolated. " +
|
|
2302
2309
|
'Pass dynamic values through config.args instead (inline script receives them as argv, with runtime: "bash" running bash -c <script> ...args so args[0] is $0 and args[1] is $1; swarm-script receives its args object). ' +
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* binding instead.
|
|
9
9
|
*
|
|
10
10
|
* Keys below are read only by the API server (integration signing secrets, app
|
|
11
|
-
* private keys, OAuth client secrets, the API's own agent-fs
|
|
11
|
+
* private keys, OAuth client secrets, the API's own agent-fs and
|
|
12
|
+
* speech-to-text keys). They can
|
|
12
13
|
* never be exported: the config API rejects `exportToEnv: true` for them and
|
|
13
14
|
* every reader treats a stored true as false.
|
|
14
15
|
*/
|
|
@@ -25,6 +26,7 @@ export const ENV_EXPORT_RESTRICTED_KEYS: ReadonlySet<string> = new Set([
|
|
|
25
26
|
"AZURE_DEVOPS_WEBHOOK_SECRET",
|
|
26
27
|
"AGENTMAIL_WEBHOOK_SECRET",
|
|
27
28
|
"API_AGENT_FS_API_KEY",
|
|
29
|
+
"TRANSCRIPTION_API_KEY",
|
|
28
30
|
]);
|
|
29
31
|
|
|
30
32
|
/**
|
|
@@ -246,23 +246,83 @@ export function parseCookieHeader(
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
/**
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
249
|
+
* The single shared cookie name used before sessions became per-page. Every
|
|
250
|
+
* launch overwrote it, so only the newest page in a browser had a session.
|
|
251
|
+
* Still read for back-compat, but only for the page its payload names.
|
|
252
|
+
*/
|
|
253
|
+
export const LEGACY_PAGE_SESSION_COOKIE = "page_session";
|
|
254
|
+
|
|
255
|
+
/** Page ids are 32 hex chars today; anything outside the cookie-name-safe set is refused. */
|
|
256
|
+
const COOKIE_SAFE_PAGE_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
257
|
+
|
|
258
|
+
export function isCookieSafePageId(pageId: string): boolean {
|
|
259
|
+
return COOKIE_SAFE_PAGE_ID.test(pageId);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Per-page cookie name, so sessions for different pages coexist in one browser. */
|
|
263
|
+
export function pageSessionCookieName(pageId: string): string {
|
|
264
|
+
return `${LEGACY_PAGE_SESSION_COOKIE}_${pageId}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface VerifiedPageSession {
|
|
268
|
+
/** The raw signed token, forwarded upstream by the page proxy. */
|
|
269
|
+
token: string;
|
|
270
|
+
payload: PageSessionPayload;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
type CookieRequest = { headers: { cookie?: string | string[] | undefined } };
|
|
274
|
+
|
|
275
|
+
async function verifyNamedCookie(
|
|
276
|
+
req: CookieRequest,
|
|
277
|
+
name: string,
|
|
278
|
+
): Promise<VerifiedPageSession | null> {
|
|
279
|
+
const token = parseCookieHeader(req.headers.cookie, name);
|
|
280
|
+
const payload = await verifyPageSession(token);
|
|
281
|
+
return payload && token ? { token, payload } : null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Find and verify the session for `pageId`: `page_session_<pageId>` first,
|
|
286
|
+
* then the legacy `page_session`. A cookie whose signed payload names a
|
|
287
|
+
* different page is ignored, so the result always satisfies
|
|
288
|
+
* `payload.pageId === pageId`. Returns `null` on any failure (no cookie,
|
|
289
|
+
* malformed, bad signature, expired, wrong page).
|
|
290
|
+
*/
|
|
291
|
+
export async function extractPageSession(
|
|
292
|
+
req: CookieRequest,
|
|
293
|
+
pageId: string,
|
|
294
|
+
): Promise<VerifiedPageSession | null> {
|
|
295
|
+
if (!isCookieSafePageId(pageId)) return null;
|
|
296
|
+
for (const name of [pageSessionCookieName(pageId), LEGACY_PAGE_SESSION_COOKIE]) {
|
|
297
|
+
const session = await verifyNamedCookie(req, name);
|
|
298
|
+
if (session?.payload.pageId === pageId) return session;
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Verify the legacy `page_session` cookie for whatever page it names. Only for
|
|
305
|
+
* callers that send no page id (clients predating per-page cookies).
|
|
306
|
+
*/
|
|
307
|
+
export async function extractLegacyPageSession(
|
|
308
|
+
req: CookieRequest,
|
|
309
|
+
): Promise<VerifiedPageSession | null> {
|
|
310
|
+
return verifyNamedCookie(req, LEGACY_PAGE_SESSION_COOKIE);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Payload-only form of `extractPageSession`. Shared by the `/p/:id` handler
|
|
315
|
+
* (`src/http/pages-public.ts`) and the realtime upgrade
|
|
316
|
+
* (`src/realtime/transport.ts`) so they converge on the same semantics.
|
|
256
317
|
*
|
|
257
318
|
* Accepts an object with a `headers.cookie` field — duck-typed to keep this
|
|
258
319
|
* helper test-friendly without dragging `node:http` types into the utility.
|
|
259
320
|
*/
|
|
260
|
-
export async function extractAndVerifyCookie(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return verifyPageSession(token);
|
|
321
|
+
export async function extractAndVerifyCookie(
|
|
322
|
+
req: CookieRequest,
|
|
323
|
+
pageId: string,
|
|
324
|
+
): Promise<PageSessionPayload | null> {
|
|
325
|
+
return (await extractPageSession(req, pageId))?.payload ?? null;
|
|
266
326
|
}
|
|
267
327
|
|
|
268
328
|
// ───────────────────────────────────────────────────────────────────────────
|
|
@@ -284,6 +344,10 @@ const PAGE_SESSION_TTL_SECONDS = 3600;
|
|
|
284
344
|
* - `dev=true` → `SameSite=Lax` without `Secure` (works on http://localhost).
|
|
285
345
|
* - `dev=false` → `SameSite=None; Secure` (cross-site iframe embedding in prod).
|
|
286
346
|
*
|
|
347
|
+
* The cookie is named `page_session_<pageId>`, so launching page B leaves
|
|
348
|
+
* page A's session intact. Responses should go through `pageSessionSetCookies`,
|
|
349
|
+
* which also bounds how many of these a browser keeps.
|
|
350
|
+
*
|
|
287
351
|
* The caller is responsible for setting `Set-Cookie` on the response — this
|
|
288
352
|
* helper only builds the string. TTL is 1 hour; renewed on every issuance.
|
|
289
353
|
*/
|
|
@@ -299,13 +363,12 @@ export async function issuePageSessionCookie(
|
|
|
299
363
|
...(opts.name ? { name: opts.name } : {}),
|
|
300
364
|
...(opts.op ? { op: true as const } : {}),
|
|
301
365
|
});
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (opts.dev) {
|
|
366
|
+
return buildCookie(pageSessionCookieName(pageId), token, PAGE_SESSION_TTL_SECONDS, opts.dev);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function buildCookie(name: string, value: string, maxAge: number, dev: boolean): string {
|
|
370
|
+
const attrs = [`${name}=${value}`, "HttpOnly", "Path=/", `Max-Age=${maxAge}`];
|
|
371
|
+
if (dev) {
|
|
309
372
|
attrs.push("SameSite=Lax");
|
|
310
373
|
} else {
|
|
311
374
|
attrs.push("SameSite=None");
|
|
@@ -313,3 +376,53 @@ export async function issuePageSessionCookie(
|
|
|
313
376
|
}
|
|
314
377
|
return attrs.join("; ");
|
|
315
378
|
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Most per-page session cookies one browser keeps, counting the one being
|
|
382
|
+
* issued. Each is ~220 bytes on the wire, and `Path=/` sends all of them on
|
|
383
|
+
* every request to this host, so an unbounded jar grows past the server's
|
|
384
|
+
* 16 KB header limit (431 on every route, `/health` included) after ~75
|
|
385
|
+
* launches within the hour. 20 keeps the jar near 4.5 KB.
|
|
386
|
+
*/
|
|
387
|
+
export const MAX_PAGE_SESSION_COOKIES = 20;
|
|
388
|
+
|
|
389
|
+
const PAGE_SESSION_COOKIE_RE = new RegExp(
|
|
390
|
+
`(?:^|;\\s*)(${LEGACY_PAGE_SESSION_COOKIE}_[A-Za-z0-9_-]{1,64})=([^;]*)`,
|
|
391
|
+
"g",
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* `Set-Cookie` values for a launch of `pageId`: the new session, plus an
|
|
396
|
+
* expiring cookie for every other per-page session in the request's jar that
|
|
397
|
+
* is invalid or expired, or falls outside the `MAX_PAGE_SESSION_COOKIES - 1`
|
|
398
|
+
* newest. The browser then holds at most `MAX_PAGE_SESSION_COOKIES` of them.
|
|
399
|
+
* An evicted page's open tab loses its session, the same as after expiry.
|
|
400
|
+
*/
|
|
401
|
+
export async function pageSessionSetCookies(
|
|
402
|
+
req: CookieRequest,
|
|
403
|
+
pageId: string,
|
|
404
|
+
opts: { dev: boolean; uid?: string; name?: string; op?: true },
|
|
405
|
+
): Promise<string[]> {
|
|
406
|
+
const issued = await issuePageSessionCookie(pageId, opts);
|
|
407
|
+
const header = Array.isArray(req.headers.cookie)
|
|
408
|
+
? req.headers.cookie.join("; ")
|
|
409
|
+
: (req.headers.cookie ?? "");
|
|
410
|
+
const keep: { name: string; exp: number; order: number }[] = [];
|
|
411
|
+
const evict = new Set<string>();
|
|
412
|
+
const seen = new Set([pageSessionCookieName(pageId)]);
|
|
413
|
+
for (const [, name, token] of header.matchAll(PAGE_SESSION_COOKIE_RE)) {
|
|
414
|
+
if (!name || seen.has(name)) continue;
|
|
415
|
+
seen.add(name);
|
|
416
|
+
const payload = await verifyPageSession(token);
|
|
417
|
+
if (payload && name === pageSessionCookieName(payload.pageId)) {
|
|
418
|
+
keep.push({ name, exp: payload.exp, order: keep.length });
|
|
419
|
+
} else {
|
|
420
|
+
evict.add(name);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// `exp` has one-second granularity. Browsers list older cookies first, so a
|
|
424
|
+
// later position breaks ties toward the more recent launch.
|
|
425
|
+
keep.sort((a, b) => b.exp - a.exp || b.order - a.order);
|
|
426
|
+
for (const { name } of keep.slice(MAX_PAGE_SESSION_COOKIES - 1)) evict.add(name);
|
|
427
|
+
return [issued, ...[...evict].map((name) => buildCookie(name, "", 0, opts.dev))];
|
|
428
|
+
}
|
|
@@ -8,9 +8,10 @@ export type HttpRequestAuth = (
|
|
|
8
8
|
/** Password-page viewer with no user identity. Least privilege: every gate denies it. */
|
|
9
9
|
| { kind: "guest" }
|
|
10
10
|
| { kind: "controller"; controllerId: string; tokenId: string; expiresAt: string }
|
|
11
|
-
/**
|
|
11
|
+
/** Session token bound to an ACP task, a worker runtime, or only its agent (aseph_ prefix). */
|
|
12
12
|
| {
|
|
13
13
|
kind: "agent";
|
|
14
|
+
tokenKind: "task" | "runtime" | "agent";
|
|
14
15
|
agentId: string;
|
|
15
16
|
taskId: string | null;
|
|
16
17
|
runtimeId: string | null;
|