@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IncomingMessage } from "node:http";
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { getSteeringMessageById } from "../be/db";
|
|
3
3
|
import { getActiveSessionById } from "../be/db/active-sessions";
|
|
4
4
|
import { getUserGrant } from "../be/rbac-roles";
|
|
@@ -10,10 +10,12 @@ import {
|
|
|
10
10
|
sessionAccessForRoot,
|
|
11
11
|
} from "../be/session-shares";
|
|
12
12
|
import { findUserById } from "../be/users";
|
|
13
|
-
import { isRbacEnabled } from "../rbac";
|
|
13
|
+
import { can, isRbacEnabled, type RbacPrincipal } from "../rbac";
|
|
14
|
+
import type { AgentTask } from "../types";
|
|
14
15
|
import { getRequestAuth } from "../utils/request-auth-context";
|
|
15
16
|
import { dashboardUserIdHeader } from "./favorite-owner";
|
|
16
17
|
import { type RouteDef, routePathParams } from "./route-def";
|
|
18
|
+
import { jsonError } from "./utils";
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Who a request acts for when it touches a session.
|
|
@@ -76,6 +78,53 @@ export function readableByFor(actor: SessionActor): { userId: string | null } |
|
|
|
76
78
|
export const SESSION_WRITE_DENIED =
|
|
77
79
|
"You can't write in this session. Ask the owner to share it with edit access.";
|
|
78
80
|
|
|
81
|
+
/** True unless the request acts for a user who lacks write access to the task's session. */
|
|
82
|
+
export async function hasSessionWrite(
|
|
83
|
+
req: IncomingMessage,
|
|
84
|
+
myAgentId: string | undefined,
|
|
85
|
+
taskId: string,
|
|
86
|
+
): Promise<boolean> {
|
|
87
|
+
const access = await sessionAccessFor(await resolveSessionActor(req, myAgentId), taskId);
|
|
88
|
+
return hasSessionAccess(access, "write");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The parent gate shared by `POST /api/tasks` and `POST /api/apps/{id}/actions/{name}`.
|
|
93
|
+
* The child inherits the parent's thread context, so an agent caller must own or
|
|
94
|
+
* have created the parent (`task.parent.attach`; operator and user callers keep
|
|
95
|
+
* their access), and a follow-up in someone else's session needs a write share.
|
|
96
|
+
* Writes the 403 and returns true when denied.
|
|
97
|
+
*/
|
|
98
|
+
export async function denyParentAttach(
|
|
99
|
+
req: IncomingMessage,
|
|
100
|
+
res: ServerResponse,
|
|
101
|
+
myAgentId: string | undefined,
|
|
102
|
+
parentTask: Pick<AgentTask, "id" | "requestedByUserId" | "creatorAgentId" | "agentId">,
|
|
103
|
+
principal: RbacPrincipal,
|
|
104
|
+
): Promise<boolean> {
|
|
105
|
+
const attach = can({
|
|
106
|
+
principal,
|
|
107
|
+
verb: "task.parent.attach",
|
|
108
|
+
resource: {
|
|
109
|
+
kind: "task",
|
|
110
|
+
taskId: parentTask.id,
|
|
111
|
+
requestedByUserId: parentTask.requestedByUserId,
|
|
112
|
+
creatorAgentId: parentTask.creatorAgentId,
|
|
113
|
+
agentId: parentTask.agentId,
|
|
114
|
+
},
|
|
115
|
+
source: "http",
|
|
116
|
+
});
|
|
117
|
+
if (!attach.allow) {
|
|
118
|
+
jsonError(res, `You don't have context for parent task ${parentTask.id}.`, 403);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (!(await hasSessionWrite(req, myAgentId, parentTask.id))) {
|
|
122
|
+
jsonError(res, SESSION_WRITE_DENIED, 403);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
79
128
|
/**
|
|
80
129
|
* The denial for an actor lacking `needed` access to the session holding
|
|
81
130
|
* `taskId`, or null. An unknown task passes, so the handler still answers 404.
|
package/src/http/sessions.ts
CHANGED
|
@@ -2,11 +2,14 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import {
|
|
4
4
|
countSessions,
|
|
5
|
+
createAgent,
|
|
6
|
+
getAgentById,
|
|
5
7
|
getDbClient,
|
|
6
8
|
getRootTaskChain,
|
|
7
9
|
getTaskById,
|
|
8
10
|
listRecentSessions,
|
|
9
11
|
} from "../be/db";
|
|
12
|
+
import { getUserGrant } from "../be/rbac-roles";
|
|
10
13
|
import {
|
|
11
14
|
getSessionRoot,
|
|
12
15
|
listActiveSessionShares,
|
|
@@ -18,13 +21,17 @@ import {
|
|
|
18
21
|
import { getTaskSteeringFieldsForTasks } from "../be/steering";
|
|
19
22
|
import { getTaskCitationsForTasks, TaskCitationSchema } from "../be/task-citations";
|
|
20
23
|
import {
|
|
24
|
+
AgentTokenExistsError,
|
|
21
25
|
findUserById,
|
|
22
26
|
InvalidRuntimeTokenError,
|
|
27
|
+
mintAgentToken,
|
|
23
28
|
mintRuntimeToken,
|
|
24
29
|
mintSessionToken,
|
|
30
|
+
revokeAgentTokens,
|
|
25
31
|
revokeSessionToken,
|
|
32
|
+
rotateAgentToken,
|
|
26
33
|
} from "../be/users";
|
|
27
|
-
import { can } from "../rbac";
|
|
34
|
+
import { can, isRbacEnabled } from "../rbac";
|
|
28
35
|
import { sessionAccessTopic } from "../realtime/auth";
|
|
29
36
|
import { realtimeBus } from "../realtime/bus";
|
|
30
37
|
import {
|
|
@@ -39,6 +46,7 @@ import {
|
|
|
39
46
|
} from "../types";
|
|
40
47
|
import { getRequestAuth } from "../utils/request-auth-context";
|
|
41
48
|
import { isRuntimePoolsEnabled } from "../utils/runtime-pools-flag";
|
|
49
|
+
import { agentFirstPrincipal } from "./request-principal";
|
|
42
50
|
import { route } from "./route-def";
|
|
43
51
|
import { resolveSessionActor, type SessionActor } from "./session-access";
|
|
44
52
|
import { jsonError } from "./utils";
|
|
@@ -118,7 +126,7 @@ const mintSessionTokenRoute = route({
|
|
|
118
126
|
},
|
|
119
127
|
summary: "Mint an ephemeral task or runtime token",
|
|
120
128
|
description:
|
|
121
|
-
"Operators can mint task or runtime tokens. Runtime tokens can mint task tokens for their own assigned tasks.
|
|
129
|
+
"Operators can mint task or runtime tokens. Runtime and agent tokens can mint task tokens for their own assigned tasks. A child token never outlives its parent and is revoked with it.",
|
|
122
130
|
tags: ["Sessions"],
|
|
123
131
|
body: z
|
|
124
132
|
.object({
|
|
@@ -147,7 +155,8 @@ const mintSessionTokenRoute = route({
|
|
|
147
155
|
},
|
|
148
156
|
auth: { apiKey: true },
|
|
149
157
|
rbac: {
|
|
150
|
-
ungated:
|
|
158
|
+
ungated:
|
|
159
|
+
"operator, or a runtime or agent token minting for its own assigned task, enforced in the handler",
|
|
151
160
|
},
|
|
152
161
|
});
|
|
153
162
|
|
|
@@ -157,7 +166,7 @@ const revokeSessionTokenRoute = route({
|
|
|
157
166
|
pattern: ["api", "sessions", "tokens", null],
|
|
158
167
|
summary: "Revoke an ephemeral session token",
|
|
159
168
|
description:
|
|
160
|
-
"Operators can revoke any ephemeral token.
|
|
169
|
+
"Operators can revoke any ephemeral token. Agent tokens, and runtime tokens when runtime pools are enabled, can revoke their own child task tokens. Repeated revocation has no effect.",
|
|
161
170
|
tags: ["Sessions"],
|
|
162
171
|
params: z.object({ tokenId: z.string() }),
|
|
163
172
|
responses: {
|
|
@@ -167,10 +176,234 @@ const revokeSessionTokenRoute = route({
|
|
|
167
176
|
},
|
|
168
177
|
auth: { apiKey: true },
|
|
169
178
|
rbac: {
|
|
170
|
-
ungated:
|
|
179
|
+
ungated:
|
|
180
|
+
"operator, or a runtime or agent token revoking its own child, enforced in the handler",
|
|
171
181
|
},
|
|
172
182
|
});
|
|
173
183
|
|
|
184
|
+
/** Default rotation grace: the old worker token keeps working this long. */
|
|
185
|
+
const AGENT_TOKEN_ROTATE_GRACE_SEC = 300;
|
|
186
|
+
|
|
187
|
+
const AgentTokenSchema = z.object({
|
|
188
|
+
agentId: z.string(),
|
|
189
|
+
tokenId: z.string(),
|
|
190
|
+
plaintext: z.string(),
|
|
191
|
+
expiresAt: z.string().nullable().describe("null when the token does not expire"),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const agentTokenTtl = z
|
|
195
|
+
.number()
|
|
196
|
+
.int()
|
|
197
|
+
.positive()
|
|
198
|
+
.optional()
|
|
199
|
+
.describe("Token lifetime in ms. Omit for the default (no expiry).");
|
|
200
|
+
|
|
201
|
+
const mintAgentTokenRoute = route({
|
|
202
|
+
method: "post",
|
|
203
|
+
path: "/api/agents/{id}/token",
|
|
204
|
+
pattern: ["api", "agents", null, "token"],
|
|
205
|
+
operationId: "agents_mintWorkerToken",
|
|
206
|
+
summary: "Mint an agent-bound worker token",
|
|
207
|
+
description:
|
|
208
|
+
"Returns the token once. A worker runs on it in place of the swarm API key and acts only as this agent. Creates the agent when it does not exist and `name` is given. Fails with 409 when the agent already holds an active token.",
|
|
209
|
+
tags: ["Sessions"],
|
|
210
|
+
params: z.object({ id: z.string().min(1) }),
|
|
211
|
+
body: z
|
|
212
|
+
.object({ name: z.string().min(1).optional(), ttlMs: agentTokenTtl })
|
|
213
|
+
.strict()
|
|
214
|
+
.optional(),
|
|
215
|
+
responses: {
|
|
216
|
+
200: {
|
|
217
|
+
description: "Minted worker token",
|
|
218
|
+
schema: AgentTokenSchema.extend({ agentCreated: z.boolean() }),
|
|
219
|
+
},
|
|
220
|
+
403: { description: "Operator or user credentials required" },
|
|
221
|
+
404: { description: "Agent not found and no name given" },
|
|
222
|
+
409: { description: "Agent already holds an active token" },
|
|
223
|
+
},
|
|
224
|
+
rbac: { permission: "agent.token.manage" },
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const rotateAgentTokenRoute = route({
|
|
228
|
+
method: "post",
|
|
229
|
+
path: "/api/agents/{id}/token/rotate",
|
|
230
|
+
pattern: ["api", "agents", null, "token", "rotate"],
|
|
231
|
+
operationId: "agents_rotateWorkerToken",
|
|
232
|
+
summary: "Rotate an agent-bound worker token",
|
|
233
|
+
description:
|
|
234
|
+
"Returns a new token once. The previous token keeps working for `graceSec` (default 300), then expires. Use revoke for a token that must stop now.",
|
|
235
|
+
tags: ["Sessions"],
|
|
236
|
+
params: z.object({ id: z.string().min(1) }),
|
|
237
|
+
body: z
|
|
238
|
+
.object({ graceSec: z.number().int().min(0).max(86_400).optional(), ttlMs: agentTokenTtl })
|
|
239
|
+
.strict()
|
|
240
|
+
.optional(),
|
|
241
|
+
responses: {
|
|
242
|
+
200: {
|
|
243
|
+
description: "New worker token",
|
|
244
|
+
schema: AgentTokenSchema.extend({ previousExpiresAt: z.string() }),
|
|
245
|
+
},
|
|
246
|
+
403: { description: "Operator or user credentials required" },
|
|
247
|
+
404: { description: "Agent holds no active token" },
|
|
248
|
+
},
|
|
249
|
+
rbac: { permission: "agent.token.manage" },
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const revokeAgentTokenRoute = route({
|
|
253
|
+
method: "delete",
|
|
254
|
+
path: "/api/agents/{id}/token",
|
|
255
|
+
pattern: ["api", "agents", null, "token"],
|
|
256
|
+
operationId: "agents_revokeWorkerToken",
|
|
257
|
+
summary: "Revoke an agent-bound worker token",
|
|
258
|
+
description:
|
|
259
|
+
"Revokes every active worker token of the agent and the task tokens they minted. Open realtime connections on them close.",
|
|
260
|
+
tags: ["Sessions"],
|
|
261
|
+
params: z.object({ id: z.string().min(1) }),
|
|
262
|
+
responses: {
|
|
263
|
+
200: {
|
|
264
|
+
description: "Number of worker tokens revoked (0 when none was active)",
|
|
265
|
+
schema: z.object({ revoked: z.number().int() }),
|
|
266
|
+
},
|
|
267
|
+
403: { description: "Operator or user credentials required" },
|
|
268
|
+
},
|
|
269
|
+
rbac: { permission: "agent.token.manage" },
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const AGENT_TOKEN_ROUTES = [mintAgentTokenRoute, rotateAgentTokenRoute, revokeAgentTokenRoute];
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Gate the worker-token routes. Only an operator key or a user may manage
|
|
276
|
+
* them, never a session token. The principal is agent-first: an operator key
|
|
277
|
+
* sent with X-Agent-ID acts as that agent and is refused, as on the
|
|
278
|
+
* controller-token route.
|
|
279
|
+
*/
|
|
280
|
+
async function ensureAgentTokenManager(
|
|
281
|
+
req: IncomingMessage,
|
|
282
|
+
res: ServerResponse,
|
|
283
|
+
agentId: string,
|
|
284
|
+
): Promise<boolean> {
|
|
285
|
+
const auth = getRequestAuth(req);
|
|
286
|
+
if (auth?.kind !== "operator" && auth?.kind !== "user") {
|
|
287
|
+
jsonError(res, "Worker tokens require operator or user credentials", 403);
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
const header = req.headers["x-agent-id"];
|
|
291
|
+
const decision = can({
|
|
292
|
+
principal: await agentFirstPrincipal(req, Array.isArray(header) ? header[0] : header),
|
|
293
|
+
verb: "agent.token.manage",
|
|
294
|
+
resource: { kind: "agent", agentId },
|
|
295
|
+
source: "http",
|
|
296
|
+
});
|
|
297
|
+
if (!decision.allow) {
|
|
298
|
+
jsonError(res, decision.reason, 403);
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
// A worker token carries the target agent's privileges and reads that
|
|
302
|
+
// agent's resolved config, global secrets included. A user without a
|
|
303
|
+
// grant-all role must already hold both, and may not manage the token of an
|
|
304
|
+
// agent that may itself act on any agent (a lead).
|
|
305
|
+
if (auth.kind === "user" && isRbacEnabled()) {
|
|
306
|
+
const grant = await getUserGrant(auth.userId);
|
|
307
|
+
if (!grant.grantsAll) {
|
|
308
|
+
const target = await getAgentById(agentId);
|
|
309
|
+
const targetActsOnAny = can({
|
|
310
|
+
principal: { kind: "agent", agentId, isLead: target?.isLead ?? false },
|
|
311
|
+
verb: "agent.act.any",
|
|
312
|
+
resource: { kind: "agent", agentId },
|
|
313
|
+
source: "http",
|
|
314
|
+
}).allow;
|
|
315
|
+
if (targetActsOnAny) {
|
|
316
|
+
jsonError(res, "A lead agent's worker token requires a grant-all role", 403);
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
const missing = (["agent.act.any", "config.read.secrets"] as const).filter(
|
|
320
|
+
(verb) => !grant.verbs.has(verb),
|
|
321
|
+
);
|
|
322
|
+
if (missing.length > 0) {
|
|
323
|
+
jsonError(res, `Worker tokens also require: ${missing.join(", ")}`, 403);
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function handleAgentTokens(
|
|
332
|
+
req: IncomingMessage,
|
|
333
|
+
res: ServerResponse,
|
|
334
|
+
pathSegments: string[],
|
|
335
|
+
queryParams: URLSearchParams,
|
|
336
|
+
): Promise<boolean> {
|
|
337
|
+
const matched = AGENT_TOKEN_ROUTES.find((def) => def.match(req.method, pathSegments));
|
|
338
|
+
if (!matched) return false;
|
|
339
|
+
if (!(await ensureAgentTokenManager(req, res, pathSegments[2]!))) return true;
|
|
340
|
+
res.setHeader("Cache-Control", "no-store");
|
|
341
|
+
|
|
342
|
+
if (matched === mintAgentTokenRoute) {
|
|
343
|
+
const parsed = await mintAgentTokenRoute.parse(req, res, pathSegments, queryParams);
|
|
344
|
+
if (!parsed) return true;
|
|
345
|
+
const agentId = parsed.params.id;
|
|
346
|
+
const result = await getDbClient()
|
|
347
|
+
.transaction(async () => {
|
|
348
|
+
let agentCreated = false;
|
|
349
|
+
if (!(await getAgentById(agentId))) {
|
|
350
|
+
if (!parsed.body?.name) return { status: 404 as const };
|
|
351
|
+
// Same defaults as a worker's first registration; it is offline until it connects.
|
|
352
|
+
await createAgent({
|
|
353
|
+
id: agentId,
|
|
354
|
+
name: parsed.body.name,
|
|
355
|
+
isLead: false,
|
|
356
|
+
status: "offline",
|
|
357
|
+
capabilities: [],
|
|
358
|
+
maxTasks: 1,
|
|
359
|
+
});
|
|
360
|
+
agentCreated = true;
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
status: 200 as const,
|
|
364
|
+
agentCreated,
|
|
365
|
+
token: await mintAgentToken(agentId, parsed.body?.ttlMs),
|
|
366
|
+
};
|
|
367
|
+
})
|
|
368
|
+
.catch((error) => {
|
|
369
|
+
if (error instanceof AgentTokenExistsError) return { status: 409 as const };
|
|
370
|
+
throw error;
|
|
371
|
+
});
|
|
372
|
+
if (result.status === 404) {
|
|
373
|
+
jsonError(res, "Agent not found; pass name to create it", 404);
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
if (result.status === 409) {
|
|
377
|
+
jsonError(res, "Agent already holds an active worker token; rotate or revoke it", 409);
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
mintAgentTokenRoute.respond(res, 200, {
|
|
381
|
+
agentId,
|
|
382
|
+
...result.token,
|
|
383
|
+
agentCreated: result.agentCreated,
|
|
384
|
+
});
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (matched === rotateAgentTokenRoute) {
|
|
389
|
+
const parsed = await rotateAgentTokenRoute.parse(req, res, pathSegments, queryParams);
|
|
390
|
+
if (!parsed) return true;
|
|
391
|
+
const graceSec = parsed.body?.graceSec ?? AGENT_TOKEN_ROTATE_GRACE_SEC;
|
|
392
|
+
const token = await rotateAgentToken(parsed.params.id, graceSec * 1000, parsed.body?.ttlMs);
|
|
393
|
+
if (!token) {
|
|
394
|
+
jsonError(res, "Agent holds no active worker token", 404);
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
rotateAgentTokenRoute.respond(res, 200, { agentId: parsed.params.id, ...token });
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const parsed = await revokeAgentTokenRoute.parse(req, res, pathSegments, queryParams);
|
|
402
|
+
if (!parsed) return true;
|
|
403
|
+
revokeAgentTokenRoute.respond(res, 200, { revoked: await revokeAgentTokens(parsed.params.id) });
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
|
|
174
407
|
const listSessions = route({
|
|
175
408
|
method: "get",
|
|
176
409
|
path: "/api/sessions",
|
|
@@ -309,9 +542,11 @@ export async function handleSessions(
|
|
|
309
542
|
queryParams: URLSearchParams,
|
|
310
543
|
myAgentId?: string,
|
|
311
544
|
): Promise<boolean> {
|
|
545
|
+
if (await handleAgentTokens(req, res, pathSegments, queryParams)) return true;
|
|
546
|
+
|
|
312
547
|
if (mintSessionTokenRoute.match(req.method, pathSegments)) {
|
|
313
548
|
const auth = getRequestAuth(req);
|
|
314
|
-
if (auth?.kind !== "operator" && !(auth?.kind === "agent" && auth.
|
|
549
|
+
if (auth?.kind !== "operator" && !(auth?.kind === "agent" && auth.tokenKind !== "task")) {
|
|
315
550
|
jsonError(res, "Unauthorized", 401);
|
|
316
551
|
return true;
|
|
317
552
|
}
|
|
@@ -334,7 +569,7 @@ export async function handleSessions(
|
|
|
334
569
|
return {
|
|
335
570
|
token: await mintSessionToken(agentId, taskId!, ttlMs!, {
|
|
336
571
|
tokenId: auth.tokenId,
|
|
337
|
-
runtimeId: auth.runtimeId
|
|
572
|
+
runtimeId: auth.runtimeId,
|
|
338
573
|
}),
|
|
339
574
|
};
|
|
340
575
|
}
|
|
@@ -365,7 +600,10 @@ export async function handleSessions(
|
|
|
365
600
|
const auth = getRequestAuth(req);
|
|
366
601
|
if (
|
|
367
602
|
auth?.kind !== "operator" &&
|
|
368
|
-
!(
|
|
603
|
+
!(
|
|
604
|
+
auth?.kind === "agent" &&
|
|
605
|
+
(auth.tokenKind === "agent" || (auth.tokenKind === "runtime" && isRuntimePoolsEnabled()))
|
|
606
|
+
)
|
|
369
607
|
) {
|
|
370
608
|
jsonError(res, "Unauthorized", 401);
|
|
371
609
|
return true;
|
|
@@ -375,8 +613,9 @@ export async function handleSessions(
|
|
|
375
613
|
const revoked = await getDbClient().transaction(async (client) => {
|
|
376
614
|
if (auth.kind === "agent") {
|
|
377
615
|
const child = await client.get(
|
|
378
|
-
|
|
379
|
-
|
|
616
|
+
`SELECT id FROM session_tokens WHERE id = ? AND agentId = ? AND taskId IS NOT NULL
|
|
617
|
+
AND ${auth.runtimeId ? "parentRuntimeId" : "parentTokenId"} = ?`,
|
|
618
|
+
[parsed.params.tokenId, auth.agentId, auth.runtimeId ?? auth.tokenId],
|
|
380
619
|
);
|
|
381
620
|
if (!child) return false;
|
|
382
621
|
}
|
package/src/http/tasks.ts
CHANGED
|
@@ -87,6 +87,7 @@ import { buildTaskRequestedBy, TaskRequestedBySchema } from "./requested-by";
|
|
|
87
87
|
import { bodyTask, route } from "./route-def";
|
|
88
88
|
import {
|
|
89
89
|
canReadSession,
|
|
90
|
+
denyParentAttach,
|
|
90
91
|
readableByFor,
|
|
91
92
|
resolveSessionActor,
|
|
92
93
|
SESSION_READ_DENIED,
|
|
@@ -779,16 +780,6 @@ const updateTaskTitleRoute = route({
|
|
|
779
780
|
},
|
|
780
781
|
});
|
|
781
782
|
|
|
782
|
-
/** True unless the request acts for a user who lacks write access to the task's session. */
|
|
783
|
-
async function hasSessionWrite(
|
|
784
|
-
req: IncomingMessage,
|
|
785
|
-
myAgentId: string | undefined,
|
|
786
|
-
taskId: string,
|
|
787
|
-
): Promise<boolean> {
|
|
788
|
-
const access = await sessionAccessFor(await resolveSessionActor(req, myAgentId), taskId);
|
|
789
|
-
return hasSessionAccess(access, "write");
|
|
790
|
-
}
|
|
791
|
-
|
|
792
783
|
async function canSteerTask(
|
|
793
784
|
req: IncomingMessage,
|
|
794
785
|
myAgentId: string | undefined,
|
|
@@ -1069,29 +1060,10 @@ export async function handleTasks(
|
|
|
1069
1060
|
? await getTaskById(parsed.body.parentTaskId)
|
|
1070
1061
|
: null;
|
|
1071
1062
|
// The child inherits the parent's thread context, so an agent caller must own or
|
|
1072
|
-
// have created the parent
|
|
1063
|
+
// have created the parent, and a follow-up in someone else's session needs a
|
|
1064
|
+
// write share. The same gate guards app actions that chain under a task.
|
|
1073
1065
|
const principal = await resolveTaskWritePrincipal(req, myAgentId);
|
|
1074
|
-
if (
|
|
1075
|
-
parentTask &&
|
|
1076
|
-
!can({
|
|
1077
|
-
principal,
|
|
1078
|
-
verb: "task.parent.attach",
|
|
1079
|
-
resource: {
|
|
1080
|
-
kind: "task",
|
|
1081
|
-
taskId: parentTask.id,
|
|
1082
|
-
requestedByUserId: parentTask.requestedByUserId,
|
|
1083
|
-
creatorAgentId: parentTask.creatorAgentId,
|
|
1084
|
-
agentId: parentTask.agentId,
|
|
1085
|
-
},
|
|
1086
|
-
source: "http",
|
|
1087
|
-
}).allow
|
|
1088
|
-
) {
|
|
1089
|
-
jsonError(res, `You don't have context for parent task ${parentTask.id}.`, 403);
|
|
1090
|
-
return true;
|
|
1091
|
-
}
|
|
1092
|
-
// A follow-up in someone else's session needs a write share.
|
|
1093
|
-
if (parentTask && !(await hasSessionWrite(req, myAgentId, parentTask.id))) {
|
|
1094
|
-
jsonError(res, SESSION_WRITE_DENIED, 403);
|
|
1066
|
+
if (parentTask && (await denyParentAttach(req, res, myAgentId, parentTask, principal))) {
|
|
1095
1067
|
return true;
|
|
1096
1068
|
}
|
|
1097
1069
|
// A chat-sdk contextKey binds the task to a chat thread that its assignee and creator
|
package/src/http/users.ts
CHANGED
|
@@ -550,7 +550,18 @@ const deleteIdentityRoute = route({
|
|
|
550
550
|
|
|
551
551
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
552
552
|
|
|
553
|
-
const UNMAPPED_KINDS = [
|
|
553
|
+
const UNMAPPED_KINDS = [
|
|
554
|
+
"slack",
|
|
555
|
+
"github",
|
|
556
|
+
"gitlab",
|
|
557
|
+
"azure-devops",
|
|
558
|
+
"linear",
|
|
559
|
+
"kapso",
|
|
560
|
+
// Chat SDK adapters record unknown senders under the adapter name.
|
|
561
|
+
"telegram",
|
|
562
|
+
"discord",
|
|
563
|
+
"teams",
|
|
564
|
+
] as const;
|
|
554
565
|
|
|
555
566
|
/**
|
|
556
567
|
* Group the two-key-per-identity kv entries (`<externalId>:meta` json +
|
|
@@ -63,9 +63,13 @@ export async function loadAdapter(
|
|
|
63
63
|
case "telegram": {
|
|
64
64
|
const { createTelegramAdapter } = await import("@chat-adapter/telegram");
|
|
65
65
|
// Explicit `false` blocks the TELEGRAM_ALLOW_UNVERIFIED_WEBHOOKS env fallback.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
// Explicit `true` makes a reply to the bot a mention whatever
|
|
67
|
+
// TELEGRAM_MENTION_ON_REPLY says.
|
|
68
|
+
return createTelegramAdapter({
|
|
69
|
+
...base,
|
|
70
|
+
allowUnverifiedWebhooks: false,
|
|
71
|
+
mentionOnReply: true,
|
|
72
|
+
} as Parameters<typeof createTelegramAdapter>[0]);
|
|
69
73
|
}
|
|
70
74
|
case "slack": {
|
|
71
75
|
const { createSlackAdapter } = await import("@chat-adapter/slack");
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* row. The task is created in `draft` while its uploads run, with its lease
|
|
10
10
|
* renewed, so no worker can claim it before its attachments exist. Every file
|
|
11
11
|
* also stays in the task text as a `[File: ...]` line. Only a fetch failure is
|
|
12
|
-
* flagged there; upload and record failures are logged.
|
|
12
|
+
* flagged there; upload and record failures are logged. Audio files (voice
|
|
13
|
+
* notes) get their transcript in the task text when transcription is
|
|
14
|
+
* configured, and keep the audio attached.
|
|
13
15
|
*/
|
|
14
16
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
15
17
|
import { tmpdir } from "node:os";
|
|
@@ -21,12 +23,28 @@ import { providerPath } from "@/fs/provider";
|
|
|
21
23
|
import { getFileStorageProvider } from "@/fs/registry";
|
|
22
24
|
import { createTaskWithSiblingAwareness } from "@/tasks/sibling-awareness";
|
|
23
25
|
import type { AgentTask, CreateTaskOptions } from "@/types";
|
|
26
|
+
import { transcribeAudioNotes } from "@/utils/transcription";
|
|
24
27
|
import { errorMessage } from "./logger";
|
|
25
28
|
|
|
26
29
|
/** How often a draft's lease is renewed while its uploads run; the sweep waits 5 minutes. */
|
|
27
30
|
const DRAFT_LEASE_REFRESH_MS = 60_000;
|
|
28
31
|
/** A file a user sent the bot, like one uploaded from the dashboard composer. */
|
|
29
|
-
const SHARED_FILE_INTENT = "user-upload";
|
|
32
|
+
export const SHARED_FILE_INTENT = "user-upload";
|
|
33
|
+
|
|
34
|
+
const MB = 1024 * 1024;
|
|
35
|
+
/**
|
|
36
|
+
* Per-file inbound cap by adapter. A Telegram bot can download at most 20 MB
|
|
37
|
+
* through `getFile`; a larger file fails at Telegram before our own cap.
|
|
38
|
+
* Other adapters use the task attachment cap.
|
|
39
|
+
*/
|
|
40
|
+
const CHAT_INBOUND_FILE_LIMITS: Record<string, number> = { telegram: 20 * MB };
|
|
41
|
+
|
|
42
|
+
export function chatInboundFileLimit(adapter: string): number {
|
|
43
|
+
return Math.min(
|
|
44
|
+
CHAT_INBOUND_FILE_LIMITS[adapter] ?? MAX_TASK_ATTACHMENT_BYTES,
|
|
45
|
+
MAX_TASK_ATTACHMENT_BYTES,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
30
48
|
|
|
31
49
|
/**
|
|
32
50
|
* One attachment of a buffered chat message. `id` is `<messageId>-<n>` with
|
|
@@ -43,7 +61,8 @@ export interface ChatFile {
|
|
|
43
61
|
attachment: Attachment;
|
|
44
62
|
}
|
|
45
63
|
|
|
46
|
-
|
|
64
|
+
/** `tooLarge` marks a file over the inbound cap; the user gets a reply naming it. */
|
|
65
|
+
export type ChatFileFailure = { file: ChatFile; reason: string; tooLarge?: boolean };
|
|
47
66
|
|
|
48
67
|
export type FetchedChatFile = { file: ChatFile; path: string; sizeBytes: number; sha256: string };
|
|
49
68
|
|
|
@@ -55,7 +74,7 @@ export type InboundChatFiles = {
|
|
|
55
74
|
[Symbol.asyncDispose](): Promise<void>;
|
|
56
75
|
};
|
|
57
76
|
|
|
58
|
-
function formatFileSize(bytes: number): string {
|
|
77
|
+
export function formatFileSize(bytes: number): string {
|
|
59
78
|
if (bytes < 1024) return `${bytes} B`;
|
|
60
79
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
61
80
|
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
@@ -81,20 +100,71 @@ export function toChatFiles(
|
|
|
81
100
|
|
|
82
101
|
/**
|
|
83
102
|
* `[File: <name> (<mime>, <size>) id=<id>]` per file, followed by
|
|
84
|
-
* `(not attached: <reason>)` when the file never reached the task
|
|
103
|
+
* `(not attached: <reason>)` when the file never reached the task, and by the
|
|
104
|
+
* file's note (a voice-note transcript) on the next line.
|
|
85
105
|
*/
|
|
86
|
-
export function buildChatAttachmentText(
|
|
106
|
+
export function buildChatAttachmentText(
|
|
107
|
+
files: ChatFile[],
|
|
108
|
+
failed: ChatFileFailure[] = [],
|
|
109
|
+
notes: Map<string, string> = new Map(),
|
|
110
|
+
): string {
|
|
87
111
|
const reasons = new Map(failed.map((f) => [f.file.id, f.reason]));
|
|
88
112
|
return files
|
|
89
113
|
.map((f) => {
|
|
90
114
|
const size = typeof f.size === "number" ? formatFileSize(f.size) : "unknown size";
|
|
91
115
|
const line = `[File: ${f.name} (${f.mimeType ?? "unknown type"}, ${size}) id=${f.id}]`;
|
|
92
116
|
const reason = reasons.get(f.id);
|
|
93
|
-
|
|
117
|
+
const note = notes.get(f.id);
|
|
118
|
+
return [reason ? `${line} (not attached: ${reason})` : line, note].filter(Boolean).join("\n");
|
|
94
119
|
})
|
|
95
120
|
.join("\n");
|
|
96
121
|
}
|
|
97
122
|
|
|
123
|
+
/**
|
|
124
|
+
* A sender-chosen file name as an inline code span, so the bot's reply shows it
|
|
125
|
+
* as literal text and never renders a link, image or heading from it. Backslash
|
|
126
|
+
* escapes are not enough: the Chat SDK parser still autolinks bare URLs.
|
|
127
|
+
*/
|
|
128
|
+
function markdownLiteral(value: string): string {
|
|
129
|
+
const text = value.replace(/[\r\n]+/g, " ");
|
|
130
|
+
const longestRun = Math.max(0, ...(text.match(/`+/g) ?? []).map((run) => run.length));
|
|
131
|
+
const fence = "`".repeat(longestRun + 1);
|
|
132
|
+
// CommonMark strips one space from each end of a padded span.
|
|
133
|
+
const pad = /^[` ]|[` ]$/.test(text) ? " " : "";
|
|
134
|
+
return `${fence}${pad}${text}${pad}${fence}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The reply posted to the thread when files were over the inbound cap. */
|
|
138
|
+
export function formatTooLargeReply(failed: ChatFileFailure[]): string {
|
|
139
|
+
const lines = failed.map((f) => `- ${markdownLiteral(f.file.name)}: ${f.reason}`);
|
|
140
|
+
return [
|
|
141
|
+
`Too large to read, so the swarm did not get ${failed.length === 1 ? "this file" : "these files"}:`,
|
|
142
|
+
...lines,
|
|
143
|
+
"Send a smaller file or a link.",
|
|
144
|
+
].join("\n");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Transcribe every fetched audio file. Returns one task-text note per file:
|
|
149
|
+
* the transcript, the failure, or (when no provider is configured) a line
|
|
150
|
+
* saying so. Never throws.
|
|
151
|
+
*/
|
|
152
|
+
export async function transcribeChatAudio(
|
|
153
|
+
fetched: FetchedChatFile[],
|
|
154
|
+
): Promise<Map<string, string>> {
|
|
155
|
+
return transcribeAudioNotes(
|
|
156
|
+
fetched
|
|
157
|
+
.filter(({ file }) => file.attachment.type === "audio")
|
|
158
|
+
.map(({ file, path }) => ({
|
|
159
|
+
key: file.id,
|
|
160
|
+
name: file.name,
|
|
161
|
+
data: Bun.file(path),
|
|
162
|
+
mimeType: file.mimeType,
|
|
163
|
+
})),
|
|
164
|
+
"[chat-sdk]",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
98
168
|
async function readAttachment(attachment: Attachment): Promise<Uint8Array> {
|
|
99
169
|
if (attachment.data instanceof Blob) return new Uint8Array(await attachment.data.arrayBuffer());
|
|
100
170
|
if (attachment.data) return new Uint8Array(attachment.data);
|
|
@@ -119,17 +189,25 @@ export async function fetchChatFiles(files: ChatFile[]): Promise<InboundChatFile
|
|
|
119
189
|
};
|
|
120
190
|
if (files.length === 0) return result;
|
|
121
191
|
|
|
122
|
-
const
|
|
192
|
+
const tooLarge = (file: ChatFile, size: number): ChatFileFailure => {
|
|
193
|
+
const limit = chatInboundFileLimit(file.adapter);
|
|
194
|
+
const platform = file.adapter.charAt(0).toUpperCase() + file.adapter.slice(1);
|
|
195
|
+
return {
|
|
196
|
+
file,
|
|
197
|
+
reason: `too large (${formatFileSize(size)}, the limit for files sent to the ${platform} bot is ${formatFileSize(limit)})`,
|
|
198
|
+
tooLarge: true,
|
|
199
|
+
};
|
|
200
|
+
};
|
|
123
201
|
dir = await mkdtemp(join(tmpdir(), "chat-files-"));
|
|
124
202
|
for (const [index, file] of files.entries()) {
|
|
125
|
-
if (typeof file.size === "number" && file.size >
|
|
126
|
-
result.failed.push(
|
|
203
|
+
if (typeof file.size === "number" && file.size > chatInboundFileLimit(file.adapter)) {
|
|
204
|
+
result.failed.push(tooLarge(file, file.size));
|
|
127
205
|
continue;
|
|
128
206
|
}
|
|
129
207
|
try {
|
|
130
208
|
const bytes = await readAttachment(file.attachment);
|
|
131
|
-
if (bytes.byteLength >
|
|
132
|
-
result.failed.push(
|
|
209
|
+
if (bytes.byteLength > chatInboundFileLimit(file.adapter)) {
|
|
210
|
+
result.failed.push(tooLarge(file, bytes.byteLength));
|
|
133
211
|
continue;
|
|
134
212
|
}
|
|
135
213
|
const path = join(dir, String(index));
|