@desplega.ai/agent-swarm 2.5.4 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/{acp-adapter-ek38cwsv.js → acp-adapter-dgfakfpw.js} +9 -9
- package/dist/{actions-92mepq03.js → actions-fbqma8aa.js} +21 -21
- package/dist/{amp-adapter-9gaxg015.js → amp-adapter-en0jy6hs.js} +4 -4
- package/dist/{app-ctn8gsgx.js → app-k8a5cgpp.js} +3 -3
- package/dist/{assistant-zyr5kz47.js → assistant-a16433gr.js} +29 -25
- package/dist/{boot-reembed-dwzfttdr.js → boot-reembed-6c1sjfnr.js} +1 -1
- package/dist/{boot-reembed-w75maga2.js → boot-reembed-f4nn561n.js} +1 -1
- package/dist/{boot-scrub-sweep-e9kq43kf.js → boot-scrub-sweep-d4w9gajf.js} +4 -4
- package/dist/{boot-seeds-x28v3fhb.js → boot-seeds-9bmkspyw.js} +4 -4
- package/dist/{claude-adapter-f0afqnne.js → claude-adapter-nys1kjxa.js} +9 -9
- package/dist/{claude-managed-adapter-x81a7r65.js → claude-managed-adapter-55207973.js} +2 -2
- package/dist/{claude-managed-setup-bqzwjd9x.js → claude-managed-setup-3cm35gey.js} +10 -3
- package/dist/{claude-sdk-session-7b7whkaq.js → claude-sdk-session-nw5yd8cp.js} +11 -11
- package/dist/{cli-c61mbdds.js → cli-03gg9pd1.js} +4 -4
- package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
- package/dist/cli-1tem6ns9.js +894 -0
- package/dist/{cli-jbnn4vxb.js → cli-24j1jeeb.js} +3 -3
- package/dist/{cli-pzatvw94.js → cli-2sjhpsns.js} +2 -2
- package/dist/{cli-dew6x255.js → cli-39aet655.js} +2 -2
- package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
- package/dist/{cli-x0f2v69c.js → cli-3aqd4jaa.js} +12 -7
- package/dist/{cli-gbfxp48k.js → cli-3fwqy71j.js} +15 -15
- package/dist/{cli-7p4jxgtt.js → cli-3gfvmzht.js} +6 -6
- package/dist/{cli-03vpq45j.js → cli-3ndmpxxv.js} +2 -2
- package/dist/{cli-asq5ydm4.js → cli-4hxrf0p4.js} +131 -50
- package/dist/{cli-mz9afxxf.js → cli-4rf0f5vg.js} +15 -3
- package/dist/{cli-k4tjsdx9.js → cli-5rj83bam.js} +272 -195
- package/dist/{cli-sbwsxvtd.js → cli-5sckpqcx.js} +2 -2
- package/dist/{cli-cz5nmdxt.js → cli-683y0b3a.js} +2 -2
- package/dist/{cli-ethgy7ha.js → cli-6y51ryva.js} +2 -2
- package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
- package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
- package/dist/{cli-gx4rm0qx.js → cli-7enjwya9.js} +1 -1
- package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
- package/dist/{cli-hh5we352.js → cli-9abdgn5t.js} +7 -7
- package/dist/{cli-ff1tm9n9.js → cli-a2rr0fmg.js} +1 -1
- package/dist/{cli-c952c7ac.js → cli-a34pr3w1.js} +2 -2
- package/dist/{cli-s682zgqz.js → cli-abkm71vr.js} +1 -1
- package/dist/{cli-mmhj8vd7.js → cli-ag2yrnt5.js} +11 -2
- package/dist/{cli-bvpgr9ny.js → cli-aj8djyqt.js} +5 -5
- package/dist/{cli-gwhjmh8g.js → cli-art7c1zh.js} +5 -5
- package/dist/{cli-5bnsc0e1.js → cli-az25fn5f.js} +4 -4
- package/dist/{cli-9s05422w.js → cli-bqztfyve.js} +1 -1
- package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
- package/dist/{cli-1r5ad7j5.js → cli-c2habvsf.js} +12 -12
- package/dist/{cli-64mh9mf3.js → cli-ca6banwz.js} +69 -17
- package/dist/{cli-25n3mv5d.js → cli-cz37jqss.js} +5 -5
- package/dist/{cli-6n8axjv1.js → cli-d2dpf95n.js} +2 -2
- package/dist/{cli-gn5af0x5.js → cli-emhjcj2y.js} +11 -11
- package/dist/{cli-6a1xg1p7.js → cli-esakngh8.js} +2 -2
- package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
- package/dist/{cli-4b2z6n0m.js → cli-ffvsj2qw.js} +17 -8
- package/dist/{cli-abs8ndtr.js → cli-g20jjwt4.js} +26 -26
- package/dist/{cli-jb30dzvk.js → cli-g8ka5j4j.js} +446 -164
- package/dist/{cli-19202kew.js → cli-gf95q3bm.js} +5 -5
- package/dist/{cli-eftzyh9y.js → cli-gnsb1zxa.js} +2 -2
- package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
- package/dist/{cli-e42vss7b.js → cli-hfeb68z4.js} +5 -5
- package/dist/{cli-g9vd9ece.js → cli-k21gx5ht.js} +14 -16
- package/dist/{cli-tsma7p8v.js → cli-kf4jree9.js} +2 -2
- package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
- package/dist/{cli-wx2nj19m.js → cli-m5a9m0ak.js} +6 -6
- package/dist/{cli-621ce7kw.js → cli-m70bajzr.js} +3 -3
- package/dist/{cli-9j129brq.js → cli-mtnj4fwa.js} +6 -6
- package/dist/{cli-xzfq4at3.js → cli-mvjz5an3.js} +15 -11
- package/dist/{cli-277c31dw.js → cli-n7asmce6.js} +225 -3
- package/dist/{cli-xtrz6bzs.js → cli-nn6vx2mq.js} +1 -1
- package/dist/{cli-2655k16q.js → cli-nq677csy.js} +1 -1
- package/dist/{cli-0shx9j93.js → cli-pfk92eqz.js} +2 -2
- package/dist/{cli-wghjkr0e.js → cli-sqma6vw1.js} +10 -7
- package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
- package/dist/{cli-qqzmjxkf.js → cli-v49fkghp.js} +11 -6
- package/dist/{cli-x77gen2p.js → cli-w2656ka1.js} +7 -7
- package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
- package/dist/{cli-m4ecdxvz.js → cli-zqne1qcj.js} +7 -7
- package/dist/cli.js +17 -17
- package/dist/{codex-adapter-wch12q99.js → codex-adapter-d95q4qsn.js} +7 -7
- package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
- package/dist/{codex-session-runner-41pfcr95.js → codex-session-runner-gdwd8h0c.js} +8 -8
- package/dist/{commands-9849dkj6.js → commands-jqmgk54b.js} +2 -2
- package/dist/{controller-ja0sd95p.js → controller-z3jq589f.js} +4 -4
- package/dist/{controllers-connect-xp05dj6x.js → controllers-connect-r2za39ef.js} +23 -23
- package/dist/{cursor-adapter-623qxqew.js → cursor-adapter-4mr4jgkw.js} +2 -2
- package/dist/{db-47vj40dy.js → db-ppv2tze7.js} +3 -3
- package/dist/{db-w759qmx0.js → db-w36wwb5g.js} +4 -2
- package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
- package/dist/{dispatcher-7b8e4z2j.js → dispatcher-pv5h77vf.js} +20 -20
- package/dist/{docker-s84hjs91.js → docker-fdzyewpz.js} +2 -2
- package/dist/{dsh-adapter-2ezw3xea.js → dsh-adapter-d4pdpb3r.js} +2 -2
- package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
- package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
- package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-4sp4dvmc.js} +1 -1
- package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
- package/dist/{execute-0xpz8950.js → execute-jev839y1.js} +1 -1
- package/dist/{external-channel-e2s028da.js → external-channel-y6nkzhnn.js} +2 -2
- package/dist/{grok-adapter-ys7gaqee.js → grok-adapter-t08b9ntm.js} +10 -10
- package/dist/handlers-eqsnmv95.js +41 -0
- package/dist/{heartbeat-w3mntghj.js → heartbeat-vn31xwdk.js} +23 -23
- package/dist/{hook-g7kvv6x9.js → hook-6d0zbvvt.js} +6 -6
- package/dist/{hook-3jjywnpb.js → hook-cpjs8q51.js} +8 -8
- package/dist/{http-tkp7d4kb.js → http-9c6medda.js} +856 -99
- package/dist/{index-xdaahthb.js → index-38t35h0n.js} +20 -20
- package/dist/{index-3n8m9ea2.js → index-g6k9tctj.js} +1 -1
- package/dist/{index-1ef1myq2.js → index-k0yt1tvq.js} +20 -20
- package/dist/{index-31ceyf42.js → index-pkma1yf8.js} +1 -1
- package/dist/{index-q7m26kvs.js → index-qhjf2htm.js} +13 -10
- package/dist/index-yqgbfgaj.js +43 -0
- package/dist/{keepalive-v4ta8wgg.js → keepalive-em0117bp.js} +4 -4
- package/dist/lead-23e7z59t.js +79 -0
- package/dist/{maintenance-84n1ex21.js → maintenance-kbme3qy9.js} +1 -1
- package/dist/{oauth-refresh-sweep-vjnnandw.js → oauth-refresh-sweep-xysj4yse.js} +3 -3
- package/dist/{onboard-sfmpe4np.js → onboard-p6q0vz97.js} +3 -3
- package/dist/{opencode-adapter-jmpst4q4.js → opencode-adapter-esv3k9yn.js} +7 -7
- package/dist/{otel-impl-v6cc0zve.js → otel-impl-54qhqh1z.js} +2 -2
- package/dist/{pi-mono-adapter-fk8fkcf6.js → pi-mono-adapter-mymbwhj0.js} +10 -10
- package/dist/{pricing-refresh-fsk1e1zc.js → pricing-refresh-yb75wv33.js} +3 -3
- package/dist/{rbac-roles-prmkdnkw.js → rbac-roles-1vp2mw4p.js} +2 -2
- package/dist/{rbac-roles-zxmxqa39.js → rbac-roles-dexhc9x8.js} +1 -1
- package/dist/render-v2-tjay5hhf.js +16 -0
- package/dist/{resume-frdtgfa0.js → resume-8ayf37g5.js} +9 -9
- package/dist/{runtime-bt5pkpm1.js → runtime-hqe5cqcd.js} +1 -1
- package/dist/{session-access-fpez4y5q.js → session-access-fdhp987s.js} +4 -4
- package/dist/session-source-backfill-qgtc86z1.js +110 -0
- package/dist/{setup-mq6m3m8q.js → setup-4rrmqcq3.js} +1 -1
- package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-52km00d6.js} +2 -2
- package/dist/{work-objects-kfahhjsq.js → work-objects-p28fgd70.js} +23 -23
- package/dist/worker-e3w7vye1.js +79 -0
- package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
- package/openapi.json +1274 -32
- package/package.json +1 -1
- package/src/assets/source-key.ts +95 -0
- package/src/be/db/api-keys.ts +80 -0
- package/src/be/db/session-labels.ts +28 -0
- package/src/be/db/session-moves.ts +5 -6
- package/src/be/db/session-tree.ts +153 -14
- package/src/be/db/tasks/read.ts +11 -0
- package/src/be/db/tasks/write.ts +6 -1
- package/src/be/db/workflow-runs.ts +183 -1
- package/src/be/db-retention.ts +61 -6
- package/src/be/db.ts +59 -114
- package/src/be/memory/providers/openai-embedding.ts +5 -1
- package/src/be/migrations/244_api_key_validation.sql +4 -0
- package/src/be/onboarding.ts +4 -0
- package/src/be/scripts/typecheck.ts +1 -1
- package/src/be/session-source-backfill.ts +163 -0
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/comb/review-batch.ts +28 -1
- package/src/comb/templates.ts +7 -2
- package/src/commands/claude-managed-setup.ts +7 -1
- package/src/commands/credential-refresh.ts +76 -1
- package/src/commands/credential-wait.ts +2 -2
- package/src/commands/provider-credentials.ts +210 -170
- package/src/commands/runner.ts +25 -4
- package/src/extensions/apply-task-create.ts +8 -1
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +5 -0
- package/src/http/all-routes.ts +2 -0
- package/src/http/api-keys.ts +53 -0
- package/src/http/assets.ts +15 -0
- package/src/http/codex-oauth-accounts.ts +191 -0
- package/src/http/comb.ts +15 -2
- package/src/http/config.ts +1 -1
- package/src/http/core.ts +6 -0
- package/src/http/fleet-benchmark.ts +5 -2
- package/src/http/index.ts +11 -0
- package/src/http/integrations.ts +5 -1
- package/src/http/kv.ts +8 -3
- package/src/http/onboarding.ts +6 -1
- package/src/http/pool-keys.ts +354 -0
- package/src/http/sessions.ts +37 -12
- package/src/http/stats.ts +1 -0
- package/src/http/status.ts +26 -12
- package/src/http/tasks.ts +3 -0
- package/src/http/used-by.ts +7 -2
- package/src/http/workflows.ts +78 -9
- package/src/integrations/chat-sdk/inbound.ts +27 -0
- package/src/kv-reserved-namespaces.ts +19 -0
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +5 -1
- package/src/providers/claude-managed-adapter.ts +16 -9
- package/src/providers/codex-adapter.ts +71 -11
- package/src/scheduler/scheduler.ts +3 -2
- package/src/scripts-runtime/swarm-sdk.ts +7 -1
- package/src/scripts-runtime/types/stdlib.d.ts +1 -1
- package/src/scripts-runtime/types/swarm-extension.d.ts +17 -8
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -1
- package/src/slack/assistant.ts +7 -0
- package/src/slack/channel-labels.ts +74 -0
- package/src/slack/handlers.ts +8 -0
- package/src/slack/inbound-files.ts +5 -2
- package/src/slack/thread-buffer.ts +4 -0
- package/src/tasks/sibling-awareness.ts +12 -3
- package/src/telemetry-context.ts +5 -6
- package/src/telemetry-identity.ts +3 -3
- package/src/tests/api-keys-validation.test.ts +170 -0
- package/src/tests/claude-managed-adapter.test.ts +46 -0
- package/src/tests/claude-routes-e2e.test.ts +7 -2
- package/src/tests/codex-credential-pools.test.ts +120 -0
- package/src/tests/codex-gateway-route.test.ts +165 -0
- package/src/tests/codex-oauth-accounts.test.ts +357 -0
- package/src/tests/comb-review-batch.test.ts +63 -2
- package/src/tests/config-pool-keys.test.ts +533 -0
- package/src/tests/credential-check.test.ts +290 -2
- package/src/tests/credential-refresh.test.ts +172 -1
- package/src/tests/db-retention.test.ts +9 -3
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +1 -3
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/extensions-pre-task.test.ts +53 -1
- package/src/tests/fixtures/extensions/record-task-create/hooks.ts +9 -0
- package/src/tests/fixtures/extensions/record-task-create/manifest.json +7 -0
- package/src/tests/fleet-benchmark.test.ts +8 -0
- package/src/tests/get-workflow-run-tool.test.ts +98 -0
- package/src/tests/integrations-http.test.ts +32 -2
- package/src/tests/internal-ai/complete-structured.test.ts +37 -1
- package/src/tests/internal-ai/credentials.test.ts +23 -0
- package/src/tests/kv-http.test.ts +32 -0
- package/src/tests/kv-tool.test.ts +25 -0
- package/src/tests/multi-runtime-registration.test.ts +12 -12
- package/src/tests/onboarding.test.ts +108 -1
- package/src/tests/openai-embedding-credentials.test.ts +35 -0
- package/src/tests/openrouter-attribution-call-sites.test.ts +2 -2
- package/src/tests/org-website.test.ts +36 -0
- package/src/tests/provider-key-probes.test.ts +181 -0
- package/src/tests/rbac-e2e-helpers.test.ts +88 -1
- package/src/tests/rbac-e2e-helpers.ts +36 -11
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/script-runs-retention.test.ts +212 -0
- package/src/tests/sdk-allowlist.test.ts +39 -0
- package/src/tests/session-source-backfill.test.ts +429 -0
- package/src/tests/session-source-keys.test.ts +236 -0
- package/src/tests/session-source-labels.test.ts +419 -0
- package/src/tests/sessions-detail-batch.test.ts +2 -0
- package/src/tests/sessions-move.test.ts +131 -0
- package/src/tests/sessions-tree.test.ts +299 -2
- package/src/tests/slack-inbound-files.test.ts +103 -23
- package/src/tests/status.test.ts +101 -18
- package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
- package/src/tests/test-net.ts +31 -0
- package/src/tests/transcription.test.ts +8 -0
- package/src/tests/used-by-route.test.ts +11 -0
- package/src/tests/workflow-cascade-cancel.test.ts +172 -0
- package/src/tests/workflow-http-v2.test.ts +131 -0
- package/src/tests/workflow-step-order.test.ts +92 -0
- package/src/tests/workflow-sub-workflow.test.ts +30 -0
- package/src/tools/get-tasks.ts +8 -0
- package/src/tools/kv/kv-read-auth.ts +4 -2
- package/src/tools/task-action.ts +1 -1
- package/src/tools/workflows/get-workflow-run.ts +14 -4
- package/src/types.ts +24 -0
- package/src/utils/company-profile.ts +51 -1
- package/src/utils/credentials.ts +5 -2
- package/src/utils/internal-ai/complete-structured.ts +5 -0
- package/src/utils/internal-ai/credentials.ts +4 -3
- package/src/utils/provider-key-probes.ts +328 -0
- package/src/utils/transcription.ts +4 -1
- package/src/workflows/executors/sub-workflow.ts +8 -1
- package/templates/ai-toolbox.manifest.json +2 -2
- package/templates/skills/researching/content.md +2 -0
- package/templates/skills/swarm-scripts/SKILL.md +1 -0
- package/templates/skills/swarm-scripts/content.md +1 -0
- package/dist/cli-7421jzsw.js +0 -84
- package/dist/cli-e45e4m6b.js +0 -413
- package/dist/cli-zw88zjba.js +0 -307
- package/dist/handlers-wy1dr6ja.js +0 -41
- package/dist/index-nmm79qgr.js +0 -43
- package/dist/lead-sawnyqxk.js +0 -79
- package/dist/render-v2-pjwxvnkw.js +0 -12
- package/dist/worker-v5e5rxj3.js +0 -79
package/src/slack/assistant.ts
CHANGED
|
@@ -213,6 +213,10 @@ export function createAssistant(): Assistant {
|
|
|
213
213
|
buildEffectiveText(messageText, inbound.files, inbound.failed, inbound.notes),
|
|
214
214
|
cachedBotUserId ?? undefined,
|
|
215
215
|
);
|
|
216
|
+
const slack = {
|
|
217
|
+
trigger: "mention" as const,
|
|
218
|
+
messages: [{ ts: message.ts, userId, text: messageText }],
|
|
219
|
+
};
|
|
216
220
|
|
|
217
221
|
if (workingAgent && workingAgent.status !== "offline") {
|
|
218
222
|
// Otherwise, create a follow-up task for the working agent
|
|
@@ -236,6 +240,7 @@ export function createAssistant(): Assistant {
|
|
|
236
240
|
contextKey: slackContextKey({ channelId, threadTs }),
|
|
237
241
|
},
|
|
238
242
|
inbound,
|
|
243
|
+
slack,
|
|
239
244
|
);
|
|
240
245
|
await ackSlackMessage(
|
|
241
246
|
client,
|
|
@@ -285,6 +290,7 @@ export function createAssistant(): Assistant {
|
|
|
285
290
|
contextKey: slackContextKey({ channelId, threadTs }),
|
|
286
291
|
},
|
|
287
292
|
inbound,
|
|
293
|
+
slack,
|
|
288
294
|
);
|
|
289
295
|
await ackSlackMessage(
|
|
290
296
|
client,
|
|
@@ -318,6 +324,7 @@ export function createAssistant(): Assistant {
|
|
|
318
324
|
contextKey: slackContextKey({ channelId, threadTs }),
|
|
319
325
|
},
|
|
320
326
|
inbound,
|
|
327
|
+
slack,
|
|
321
328
|
);
|
|
322
329
|
await ackSlackMessage(client, channelId, message.ts, reactionName("accepted"), "accepted");
|
|
323
330
|
await notifySlackFileFailures(client, channelId, threadTs, unattached);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { WebClient } from "@slack/web-api";
|
|
2
|
+
import { rememberSessionLabel } from "../be/db/session-labels";
|
|
3
|
+
import { getSlackApp } from "./app";
|
|
4
|
+
import { callSlackWithRetry, slackErrorCode } from "./render-v2";
|
|
5
|
+
|
|
6
|
+
const LABEL_TTL_MS = 24 * 60 * 60 * 1000;
|
|
7
|
+
const FAILURE_TTL_MS = 10 * 60 * 1000;
|
|
8
|
+
const FAILURE_CACHE_MAX = 500;
|
|
9
|
+
const LOOKUP_QUEUE_MAX = 50;
|
|
10
|
+
const inFlight = new Set<string>();
|
|
11
|
+
const failedUntil = new Map<string, number>();
|
|
12
|
+
let pending = Promise.resolve();
|
|
13
|
+
|
|
14
|
+
async function channelLabel(client: WebClient, id: string): Promise<string> {
|
|
15
|
+
const result = await callSlackWithRetry(client, "conversations.info", {
|
|
16
|
+
channel: id.toUpperCase(),
|
|
17
|
+
});
|
|
18
|
+
const channel = result.channel as
|
|
19
|
+
| { name?: string; is_im?: boolean; is_mpim?: boolean; user?: string }
|
|
20
|
+
| undefined;
|
|
21
|
+
if (result.ok === false || !channel) throw new Error("Channel lookup failed");
|
|
22
|
+
if (channel.is_mpim) return "Group DM";
|
|
23
|
+
if (channel.is_im && channel.user) {
|
|
24
|
+
const result = await callSlackWithRetry(client, "users.info", { user: channel.user });
|
|
25
|
+
const user = result.user as
|
|
26
|
+
| { profile?: { display_name?: string; real_name?: string }; real_name?: string }
|
|
27
|
+
| undefined;
|
|
28
|
+
const name = user?.profile?.display_name || user?.real_name || user?.profile?.real_name;
|
|
29
|
+
if (result.ok !== false && name) return `DM · ${name}`;
|
|
30
|
+
} else if (channel.name) {
|
|
31
|
+
return `#${channel.name}`;
|
|
32
|
+
}
|
|
33
|
+
throw new Error("Channel label unavailable");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const CHANNEL_GROUP_KEY = /^shared\/slack\/([^/]+)\/$/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Queue a Slack lookup for each channel group key (`shared/slack/<channel>/`)
|
|
40
|
+
* without delaying the tree response. Other keys are ignored.
|
|
41
|
+
*/
|
|
42
|
+
export function warmSlackChannelLabels(groupKeys: string[]): void {
|
|
43
|
+
const client = getSlackApp()?.client;
|
|
44
|
+
if (!client) return;
|
|
45
|
+
for (const groupKey of groupKeys) {
|
|
46
|
+
const id = CHANNEL_GROUP_KEY.exec(groupKey)?.[1];
|
|
47
|
+
if (!id) continue;
|
|
48
|
+
if (inFlight.has(id) || (failedUntil.get(id) ?? 0) > Date.now()) continue;
|
|
49
|
+
failedUntil.delete(id);
|
|
50
|
+
if (inFlight.size >= LOOKUP_QUEUE_MAX) continue;
|
|
51
|
+
// Reserve failure capacity for queued work. Live failures must retain their TTL.
|
|
52
|
+
if (failedUntil.size + inFlight.size >= FAILURE_CACHE_MAX) {
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
for (const [failedId, until] of failedUntil) {
|
|
55
|
+
if (until <= now) failedUntil.delete(failedId);
|
|
56
|
+
}
|
|
57
|
+
if (failedUntil.size + inFlight.size >= FAILURE_CACHE_MAX) continue;
|
|
58
|
+
}
|
|
59
|
+
inFlight.add(id);
|
|
60
|
+
pending = pending.then(async () => {
|
|
61
|
+
try {
|
|
62
|
+
const label = await channelLabel(client, id);
|
|
63
|
+
await rememberSessionLabel(groupKey, label, LABEL_TTL_MS);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
failedUntil.set(id, Date.now() + FAILURE_TTL_MS);
|
|
66
|
+
console.warn(
|
|
67
|
+
`[Slack] Could not cache a session channel label: ${slackErrorCode(error) ?? "unknown error"}`,
|
|
68
|
+
);
|
|
69
|
+
} finally {
|
|
70
|
+
inFlight.delete(id);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/slack/handlers.ts
CHANGED
|
@@ -679,6 +679,11 @@ export function registerMessageHandler(app: App): void {
|
|
|
679
679
|
routingThreadContext,
|
|
680
680
|
);
|
|
681
681
|
|
|
682
|
+
const slack = {
|
|
683
|
+
trigger: "mention" as const,
|
|
684
|
+
messages: [{ ts: msg.ts, userId: msg.user, text: msg.text ?? "" }],
|
|
685
|
+
};
|
|
686
|
+
|
|
682
687
|
if (matches.length === 0) {
|
|
683
688
|
if (!botMentioned && !isImplicitMention) return ignoreSlackInbound("not_addressed");
|
|
684
689
|
|
|
@@ -742,6 +747,7 @@ export function registerMessageHandler(app: App): void {
|
|
|
742
747
|
contextKey: slackContextKey({ channelId: msg.channel, threadTs }),
|
|
743
748
|
},
|
|
744
749
|
inbound,
|
|
750
|
+
slack,
|
|
745
751
|
);
|
|
746
752
|
await ackSlackMessage(client, msg.channel, msg.ts, reactionName("accepted"), "accepted");
|
|
747
753
|
await notifySlackFileFailures(client, msg.channel, threadTs, unattached);
|
|
@@ -864,6 +870,7 @@ export function registerMessageHandler(app: App): void {
|
|
|
864
870
|
contextKey: slackContextKey({ channelId: msg.channel, threadTs }),
|
|
865
871
|
},
|
|
866
872
|
inbound,
|
|
873
|
+
slack,
|
|
867
874
|
);
|
|
868
875
|
const { task } = created;
|
|
869
876
|
unattached.push(...created.unattached);
|
|
@@ -888,6 +895,7 @@ export function registerMessageHandler(app: App): void {
|
|
|
888
895
|
contextKey: slackContextKey({ channelId: msg.channel, threadTs }),
|
|
889
896
|
},
|
|
890
897
|
inbound,
|
|
898
|
+
slack,
|
|
891
899
|
);
|
|
892
900
|
const { task } = created;
|
|
893
901
|
unattached.push(...created.unattached);
|
|
@@ -22,6 +22,7 @@ import { join } from "node:path";
|
|
|
22
22
|
import type { WebClient } from "@slack/web-api";
|
|
23
23
|
import { getTaskAttachments, getTaskById, promoteDraftTask, refreshDraftTaskLease } from "../be/db";
|
|
24
24
|
import { MAX_TASK_ATTACHMENT_BYTES, recordTaskAttachmentUpload } from "../be/task-attachment-store";
|
|
25
|
+
import type { TaskCreateEvent } from "../extensions/contract";
|
|
25
26
|
import { providerPath } from "../fs/provider";
|
|
26
27
|
import { getFileStorageProvider } from "../fs/registry";
|
|
27
28
|
import { resolveTemplate } from "../prompts/resolver";
|
|
@@ -320,17 +321,19 @@ export async function createSlackTaskWithFiles(
|
|
|
320
321
|
description: string,
|
|
321
322
|
options: CreateTaskOptions,
|
|
322
323
|
inbound: InboundSlackFiles,
|
|
324
|
+
slack?: TaskCreateEvent["slack"],
|
|
323
325
|
): Promise<{ task: AgentTask; unattached: SlackFileFailure[] }> {
|
|
324
326
|
noteSlackInboundSideEffect("task_created");
|
|
327
|
+
const context = { origin: "slack" as const, slack };
|
|
325
328
|
if (inbound.fetched.length === 0) {
|
|
326
|
-
const task = await createTaskWithSiblingAwareness(description, options,
|
|
329
|
+
const task = await createTaskWithSiblingAwareness(description, options, context);
|
|
327
330
|
return { task, unattached: inbound.failed };
|
|
328
331
|
}
|
|
329
332
|
|
|
330
333
|
const task = await createTaskWithSiblingAwareness(
|
|
331
334
|
description,
|
|
332
335
|
{ ...options, status: "draft" },
|
|
333
|
-
|
|
336
|
+
context,
|
|
334
337
|
);
|
|
335
338
|
const unattached = [...inbound.failed];
|
|
336
339
|
// Uploads run one after another and can outlast the abandoned-draft sweep's
|
|
@@ -313,6 +313,10 @@ async function slackFlush(
|
|
|
313
313
|
contextKey: slackContextKey({ channelId, threadTs }),
|
|
314
314
|
},
|
|
315
315
|
inbound,
|
|
316
|
+
{
|
|
317
|
+
trigger: "thread-buffer",
|
|
318
|
+
messages: items.map(({ ts, userId, text }) => ({ ts, userId, text })),
|
|
319
|
+
},
|
|
316
320
|
);
|
|
317
321
|
} catch (error) {
|
|
318
322
|
if (!(error instanceof TaskCreationBlockedError)) throw error;
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
getInProgressTasksByContextKey,
|
|
19
19
|
} from "../be/db";
|
|
20
20
|
import { applyPreTaskCreate } from "../extensions/apply-task-create";
|
|
21
|
-
import type { TaskCreateOrigin } from "../extensions/contract";
|
|
21
|
+
import type { TaskCreateEvent, TaskCreateOrigin } from "../extensions/contract";
|
|
22
22
|
import type { AgentTask } from "../types";
|
|
23
23
|
import { TaskCreationBlockedError } from "./errors";
|
|
24
24
|
import {
|
|
@@ -151,7 +151,11 @@ export type PreparedTaskCreate = { description: string; options: CreateTaskOptio
|
|
|
151
151
|
export async function prepareTaskWithSiblingAwareness(
|
|
152
152
|
description: string,
|
|
153
153
|
options: CreateTaskOptions,
|
|
154
|
-
args: {
|
|
154
|
+
args: {
|
|
155
|
+
origin?: TaskCreateOrigin;
|
|
156
|
+
allowCustomModel?: boolean;
|
|
157
|
+
slack?: TaskCreateEvent["slack"];
|
|
158
|
+
} = {},
|
|
155
159
|
): Promise<PreparedTaskCreate> {
|
|
156
160
|
const preCreate = await applyPreTaskCreate({
|
|
157
161
|
description,
|
|
@@ -159,6 +163,7 @@ export async function prepareTaskWithSiblingAwareness(
|
|
|
159
163
|
// Preserve the plan's REST default for callers without an explicit ingress origin.
|
|
160
164
|
origin: args.origin ?? "rest",
|
|
161
165
|
allowCustomModel: args.allowCustomModel,
|
|
166
|
+
slack: args.slack,
|
|
162
167
|
});
|
|
163
168
|
if (preCreate.kind === "blocked") {
|
|
164
169
|
throw new TaskCreationBlockedError(preCreate.reason, preCreate.extension);
|
|
@@ -169,7 +174,11 @@ export async function prepareTaskWithSiblingAwareness(
|
|
|
169
174
|
export async function createTaskWithSiblingAwareness(
|
|
170
175
|
description: string,
|
|
171
176
|
options: CreateTaskOptions,
|
|
172
|
-
args: {
|
|
177
|
+
args: {
|
|
178
|
+
origin?: TaskCreateOrigin;
|
|
179
|
+
allowCustomModel?: boolean;
|
|
180
|
+
slack?: TaskCreateEvent["slack"];
|
|
181
|
+
} = {},
|
|
173
182
|
): Promise<AgentTask> {
|
|
174
183
|
const { description: d, options: o } = await prepareTaskWithSiblingAwareness(
|
|
175
184
|
description,
|
package/src/telemetry-context.ts
CHANGED
|
@@ -12,8 +12,8 @@ import catalog from "./telemetry-contract/catalog.json";
|
|
|
12
12
|
import {
|
|
13
13
|
COMPANY_ACQUISITION_SOURCES,
|
|
14
14
|
COMPANY_TEAM_SIZES,
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
reportedUseCase,
|
|
16
|
+
reportedUserFunction,
|
|
17
17
|
} from "./utils/company-profile";
|
|
18
18
|
|
|
19
19
|
/** `org_` + 16 lowercase hex (self-host, minted here) or 27 alphanumerics (cloud). Same pattern the proxy enforces. */
|
|
@@ -115,10 +115,9 @@ export function buildContext(input: ContextInput): EventContext {
|
|
|
115
115
|
const orgDomain = validOrgDomain(input.orgDomain);
|
|
116
116
|
const orgWebsite = validOrgDomain(input.orgWebsite);
|
|
117
117
|
const teamSize = COMPANY_TEAM_SIZES.find((value) => value === input.teamSize);
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
: undefined;
|
|
118
|
+
// A value the catalog does not list yet is left out, as if unset.
|
|
119
|
+
const useCase = reportedUseCase(input.useCase);
|
|
120
|
+
const userFunction = userId ? reportedUserFunction(input.userFunction) : undefined;
|
|
122
121
|
return {
|
|
123
122
|
org_id: input.orgId,
|
|
124
123
|
org_name: orgName ? orgName : null,
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { getDbClient } from "./be/db/runtime";
|
|
14
14
|
import { setTelemetryOrgDomain } from "./telemetry";
|
|
15
15
|
import { emailDomain, type TelemetryUserRole } from "./telemetry-context";
|
|
16
|
-
import {
|
|
16
|
+
import { reportedUserFunction } from "./utils/company-profile";
|
|
17
17
|
|
|
18
18
|
const ROLE_TTL_MS = 60 * 60_000;
|
|
19
19
|
const ROLE_CACHE_LIMIT = 1_000;
|
|
@@ -21,7 +21,7 @@ const DOMAIN_RECOMPUTE_DEBOUNCE_MS = 2_000;
|
|
|
21
21
|
|
|
22
22
|
interface UserRoleEntry {
|
|
23
23
|
role: TelemetryUserRole;
|
|
24
|
-
userFunction?:
|
|
24
|
+
userFunction?: ReturnType<typeof reportedUserFunction>;
|
|
25
25
|
at: number;
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -40,7 +40,7 @@ async function loadUserRole(userId: string): Promise<UserRoleEntry | undefined>
|
|
|
40
40
|
);
|
|
41
41
|
return {
|
|
42
42
|
role,
|
|
43
|
-
userFunction:
|
|
43
|
+
userFunction: reportedUserFunction(user?.jobFunction),
|
|
44
44
|
at: Date.now(),
|
|
45
45
|
};
|
|
46
46
|
} catch {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { Readable } from "node:stream";
|
|
3
|
+
import {
|
|
4
|
+
closeDb,
|
|
5
|
+
getAvailableKeyIndices,
|
|
6
|
+
getDbClient,
|
|
7
|
+
getKeyStatuses,
|
|
8
|
+
initDb,
|
|
9
|
+
recordKeyValidation,
|
|
10
|
+
} from "../be/db";
|
|
11
|
+
import { handleApiKeys } from "../http/api-keys";
|
|
12
|
+
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
13
|
+
|
|
14
|
+
const TEST_DB = "./test-api-keys-validation.sqlite";
|
|
15
|
+
const keyType = "ANTHROPIC_API_KEY";
|
|
16
|
+
const rejected = { keyIndex: 0, keySuffix: "aaa11", ok: false, httpStatus: 401 };
|
|
17
|
+
|
|
18
|
+
async function call(method: string, url: string, body?: unknown) {
|
|
19
|
+
const req = Readable.from(body === undefined ? [] : [Buffer.from(JSON.stringify(body))]);
|
|
20
|
+
Object.assign(req, { method, url, headers: { "content-type": "application/json" } });
|
|
21
|
+
let statusCode = 200;
|
|
22
|
+
let responseBody = "";
|
|
23
|
+
const res = {
|
|
24
|
+
setHeader: () => {},
|
|
25
|
+
writeHead: (code: number) => {
|
|
26
|
+
statusCode = code;
|
|
27
|
+
},
|
|
28
|
+
end: (chunk?: string) => {
|
|
29
|
+
responseBody += chunk ?? "";
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
const handled = await handleApiKeys(
|
|
33
|
+
req as never,
|
|
34
|
+
res as never,
|
|
35
|
+
getPathSegments(url),
|
|
36
|
+
parseQueryParams(url),
|
|
37
|
+
);
|
|
38
|
+
return { handled, statusCode, body: JSON.parse(responseBody) };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("pooled key validation", () => {
|
|
42
|
+
beforeAll(() => {
|
|
43
|
+
initDb(TEST_DB);
|
|
44
|
+
});
|
|
45
|
+
beforeEach(async () => {
|
|
46
|
+
await getDbClient().run("DELETE FROM api_key_status");
|
|
47
|
+
});
|
|
48
|
+
afterAll(async () => {
|
|
49
|
+
closeDb();
|
|
50
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
51
|
+
await Bun.file(TEST_DB + suffix)
|
|
52
|
+
.delete()
|
|
53
|
+
.catch(() => {});
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("HTTP 401 marks the key as rejected", async () => {
|
|
58
|
+
await recordKeyValidation(keyType, [{ ...rejected, httpStatus: 401 }]);
|
|
59
|
+
const [row] = await getKeyStatuses(keyType);
|
|
60
|
+
expect(row).toMatchObject({
|
|
61
|
+
keyIndex: 0,
|
|
62
|
+
keySuffix: "aaa11",
|
|
63
|
+
provider: "claude",
|
|
64
|
+
lastValidationError: "HTTP 401",
|
|
65
|
+
});
|
|
66
|
+
expect(row!.lastValidationFailedAt).not.toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("a pass clears rejection and updates the index", async () => {
|
|
70
|
+
await recordKeyValidation(keyType, [rejected]);
|
|
71
|
+
await recordKeyValidation(keyType, [{ ...rejected, keyIndex: 2, ok: true }]);
|
|
72
|
+
expect((await getKeyStatuses(keyType))[0]).toMatchObject({
|
|
73
|
+
keyIndex: 2,
|
|
74
|
+
lastValidationError: null,
|
|
75
|
+
lastValidationFailedAt: null,
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// 403 included: a valid key can lack the scope of the list endpoint.
|
|
80
|
+
test.each([
|
|
81
|
+
403,
|
|
82
|
+
500,
|
|
83
|
+
429,
|
|
84
|
+
undefined,
|
|
85
|
+
])("failure with status %s preserves existing state", async (httpStatus) => {
|
|
86
|
+
await recordKeyValidation(keyType, [rejected]);
|
|
87
|
+
const before = await getKeyStatuses(keyType);
|
|
88
|
+
await recordKeyValidation(keyType, [{ ...rejected, keyIndex: 3, httpStatus }]);
|
|
89
|
+
expect(await getKeyStatuses(keyType)).toEqual(before);
|
|
90
|
+
await recordKeyValidation("OPENAI_API_KEY", [{ ...rejected, httpStatus }]);
|
|
91
|
+
expect(await getKeyStatuses("OPENAI_API_KEY")).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("a removed suffix cannot block a replacement at the same index", async () => {
|
|
95
|
+
await recordKeyValidation(keyType, [rejected]);
|
|
96
|
+
await recordKeyValidation(keyType, [{ keyIndex: 0, keySuffix: "bbb22", ok: true }]);
|
|
97
|
+
const rows = await getKeyStatuses(keyType);
|
|
98
|
+
expect(rows.find((row) => row.keySuffix === "aaa11")).toMatchObject({
|
|
99
|
+
lastValidationError: null,
|
|
100
|
+
lastValidationFailedAt: null,
|
|
101
|
+
});
|
|
102
|
+
expect((await getAvailableKeyIndices(keyType, 1)).availableIndices).toEqual([0]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("rejections and removed-suffix cleanup stay within their scope and key type", async () => {
|
|
106
|
+
await recordKeyValidation(keyType, [rejected], "agent", "agent-a");
|
|
107
|
+
await recordKeyValidation(keyType, [rejected], "agent", "agent-b");
|
|
108
|
+
await recordKeyValidation("OPENAI_API_KEY", [rejected], "agent", "agent-a");
|
|
109
|
+
await recordKeyValidation(
|
|
110
|
+
keyType,
|
|
111
|
+
[{ keyIndex: 0, keySuffix: "bbb22", ok: true }],
|
|
112
|
+
"agent",
|
|
113
|
+
"agent-a",
|
|
114
|
+
);
|
|
115
|
+
expect((await getAvailableKeyIndices(keyType, 1, "agent", "agent-a")).availableIndices).toEqual(
|
|
116
|
+
[0],
|
|
117
|
+
);
|
|
118
|
+
expect((await getAvailableKeyIndices(keyType, 1, "agent", "agent-b")).availableIndices).toEqual(
|
|
119
|
+
[],
|
|
120
|
+
);
|
|
121
|
+
expect(
|
|
122
|
+
(await getAvailableKeyIndices("OPENAI_API_KEY", 1, "agent", "agent-a")).availableIndices,
|
|
123
|
+
).toEqual([]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("availability excludes rejected keys and reports their indices", async () => {
|
|
127
|
+
await recordKeyValidation(keyType, [rejected, { keyIndex: 1, keySuffix: "bbb22", ok: true }]);
|
|
128
|
+
expect(await getAvailableKeyIndices(keyType, 3)).toMatchObject({
|
|
129
|
+
availableIndices: [1, 2],
|
|
130
|
+
rejectedIndices: [0],
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("availability is empty when every key is rejected", async () => {
|
|
135
|
+
await recordKeyValidation(keyType, [
|
|
136
|
+
rejected,
|
|
137
|
+
{ ...rejected, keyIndex: 1, keySuffix: "bbb22" },
|
|
138
|
+
]);
|
|
139
|
+
expect(await getAvailableKeyIndices(keyType, 2)).toMatchObject({
|
|
140
|
+
availableIndices: [],
|
|
141
|
+
rejectedIndices: [0, 1],
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("the report handler records validation and exposes it in reads", async () => {
|
|
146
|
+
expect(
|
|
147
|
+
await call("POST", "/api/keys/report-validation", { keyType, keys: [rejected] }),
|
|
148
|
+
).toEqual({
|
|
149
|
+
handled: true,
|
|
150
|
+
statusCode: 200,
|
|
151
|
+
body: { success: true, message: "Key validation recorded" },
|
|
152
|
+
});
|
|
153
|
+
expect(
|
|
154
|
+
(await call("GET", `/api/keys/available?keyType=${keyType}&totalKeys=2`)).body,
|
|
155
|
+
).toMatchObject({
|
|
156
|
+
availableIndices: [1],
|
|
157
|
+
rejectedIndices: [0],
|
|
158
|
+
});
|
|
159
|
+
const status = await call("GET", `/api/keys/status?keyType=${keyType}`);
|
|
160
|
+
expect(status.body.keys[0]).toMatchObject({ lastValidationError: "HTTP 401" });
|
|
161
|
+
expect(status.body.keys[0].lastValidationFailedAt).not.toBeNull();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("the report handler rejects an empty pool", async () => {
|
|
165
|
+
expect(
|
|
166
|
+
(await call("POST", "/api/keys/report-validation", { keyType, keys: [] })).statusCode,
|
|
167
|
+
).toBe(400);
|
|
168
|
+
expect(await getKeyStatuses(keyType)).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -59,6 +59,52 @@ describe("ClaudeManagedAdapter (Phase 1 skeleton)", () => {
|
|
|
59
59
|
expect(adapter.formatCommand("plan")).toBe("/plan");
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
+
for (const source of ["container", "task", "task-pool"] as const) {
|
|
63
|
+
test(`uses one Anthropic key from ${source}`, async () => {
|
|
64
|
+
const originalFetch = globalThis.fetch;
|
|
65
|
+
const originalKey = process.env.ANTHROPIC_API_KEY;
|
|
66
|
+
let apiKey: string | null = null;
|
|
67
|
+
process.env.ANTHROPIC_API_KEY = "example-container-first,example-container-second";
|
|
68
|
+
globalThis.fetch = (async (input, init) => {
|
|
69
|
+
apiKey =
|
|
70
|
+
new Request(input, init).headers.get("authorization")?.slice("Bearer ".length) ?? null;
|
|
71
|
+
return Response.json(
|
|
72
|
+
{ error: { type: "authentication_error", message: "test rejection" } },
|
|
73
|
+
{ status: 401 },
|
|
74
|
+
);
|
|
75
|
+
}) as typeof fetch;
|
|
76
|
+
try {
|
|
77
|
+
const adapter = new ClaudeManagedAdapter();
|
|
78
|
+
await expect(
|
|
79
|
+
adapter.createSession(
|
|
80
|
+
tConfig({
|
|
81
|
+
env:
|
|
82
|
+
source === "container"
|
|
83
|
+
? {}
|
|
84
|
+
: {
|
|
85
|
+
ANTHROPIC_API_KEY:
|
|
86
|
+
source === "task"
|
|
87
|
+
? "example-task"
|
|
88
|
+
: "example-task-first,example-task-second",
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
),
|
|
92
|
+
).rejects.toThrow("test rejection");
|
|
93
|
+
expect(
|
|
94
|
+
source === "container"
|
|
95
|
+
? ["example-container-first", "example-container-second"]
|
|
96
|
+
: source === "task"
|
|
97
|
+
? ["example-task"]
|
|
98
|
+
: ["example-task-first", "example-task-second"],
|
|
99
|
+
).toContain(apiKey);
|
|
100
|
+
} finally {
|
|
101
|
+
globalThis.fetch = originalFetch;
|
|
102
|
+
if (originalKey === undefined) delete process.env.ANTHROPIC_API_KEY;
|
|
103
|
+
else process.env.ANTHROPIC_API_KEY = originalKey;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
62
108
|
test("ctor throws when MANAGED_AGENT_ID is missing", () => {
|
|
63
109
|
const saved = process.env.MANAGED_AGENT_ID;
|
|
64
110
|
delete process.env.MANAGED_AGENT_ID;
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
* auth redacted.
|
|
11
11
|
*/
|
|
12
12
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
13
|
-
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
13
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
14
14
|
import { createRequire } from "node:module";
|
|
15
15
|
import { tmpdir } from "node:os";
|
|
16
|
-
import { dirname, join } from "node:path";
|
|
16
|
+
import { basename, dirname, join } from "node:path";
|
|
17
17
|
import { deriveDefaultRoute } from "@desplega/model-routing";
|
|
18
18
|
import { closeDb, createAgent, getAgentById, initDb, updateAgentCredentialState } from "../be/db";
|
|
19
19
|
import { awaitCredentials } from "../commands/credential-wait";
|
|
@@ -240,6 +240,11 @@ describe.skipIf(!CLAUDE_BINARY)("claude routes e2e (real CLI, fake endpoints)",
|
|
|
240
240
|
|
|
241
241
|
expect(result).toMatchObject({ isError: false, exitCode: 0 });
|
|
242
242
|
expect(result.output).toContain(REPLY);
|
|
243
|
+
// Trust lands in the session's HOME, never in the host's ~/.claude.json.
|
|
244
|
+
const claudeJson = JSON.parse(readFileSync(join(env.HOME ?? "", ".claude.json"), "utf8"));
|
|
245
|
+
expect(
|
|
246
|
+
Object.keys(claudeJson.projects ?? {}).some((dir) => dir.endsWith(basename(cwd))),
|
|
247
|
+
).toBe(true);
|
|
243
248
|
|
|
244
249
|
const turn = fake.requests.find(
|
|
245
250
|
(req) => req.method === "POST" && route.turnPath.test(req.path),
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { CodexAdapter } from "../providers/codex-adapter";
|
|
6
|
+
import type { ProviderSessionConfig } from "../providers/types";
|
|
7
|
+
|
|
8
|
+
const keyVars = ["OPENAI_API_KEY", "OPENROUTER_API_KEY", "ANTHROPIC_API_KEY"];
|
|
9
|
+
let savedEnv: Record<string, string | undefined>;
|
|
10
|
+
let home: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
savedEnv = Object.fromEntries(["HOME", ...keyVars].map((key) => [key, process.env[key]]));
|
|
14
|
+
home = await mkdtemp(join(tmpdir(), "credential-validation-codex-"));
|
|
15
|
+
process.env.HOME = home;
|
|
16
|
+
for (const key of keyVars) process.env[key] = "example-first, example-second";
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
for (const [key, value] of Object.entries(savedEnv)) {
|
|
21
|
+
if (value === undefined) delete process.env[key];
|
|
22
|
+
else process.env[key] = value;
|
|
23
|
+
}
|
|
24
|
+
await rm(home, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function config(env?: Record<string, string>): ProviderSessionConfig {
|
|
28
|
+
return {
|
|
29
|
+
agentId: crypto.randomUUID(),
|
|
30
|
+
prompt: "test",
|
|
31
|
+
systemPrompt: "",
|
|
32
|
+
cwd: home,
|
|
33
|
+
logFile: "/dev/null",
|
|
34
|
+
env,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("Codex credential pools", () => {
|
|
39
|
+
for (const source of ["container", "task", "task-pool"] as const) {
|
|
40
|
+
test(`app-server uses one OpenAI key from ${source}`, async () => {
|
|
41
|
+
let capturedEnv: Record<string, string> | undefined;
|
|
42
|
+
const adapter = new CodexAdapter({
|
|
43
|
+
bypassSubprocess: true,
|
|
44
|
+
appServerFactory: ({ env }) => {
|
|
45
|
+
capturedEnv = env;
|
|
46
|
+
throw new Error("test stops before app-server spawn");
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
const env =
|
|
50
|
+
source === "container"
|
|
51
|
+
? {}
|
|
52
|
+
: {
|
|
53
|
+
OPENAI_API_KEY:
|
|
54
|
+
source === "task" ? "example-task" : "example-task-first, example-task-second",
|
|
55
|
+
};
|
|
56
|
+
await expect(adapter.createSession(config(env))).rejects.toThrow(
|
|
57
|
+
"test stops before app-server spawn",
|
|
58
|
+
);
|
|
59
|
+
expect(
|
|
60
|
+
source === "container"
|
|
61
|
+
? ["example-first", "example-second"]
|
|
62
|
+
: source === "task"
|
|
63
|
+
? ["example-task"]
|
|
64
|
+
: ["example-task-first", "example-task-second"],
|
|
65
|
+
).toContain(capturedEnv?.OPENAI_API_KEY);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test(`subprocess forwards one key per provider from ${source}`, async () => {
|
|
69
|
+
let capturedEnv: Record<string, string | undefined> | undefined;
|
|
70
|
+
const spawn = spyOn(Bun, "spawn").mockImplementation(((
|
|
71
|
+
...args: Parameters<typeof Bun.spawn>
|
|
72
|
+
) => {
|
|
73
|
+
capturedEnv = args[1]?.env;
|
|
74
|
+
throw new Error("test stops before subprocess spawn");
|
|
75
|
+
}) as typeof Bun.spawn);
|
|
76
|
+
try {
|
|
77
|
+
const env =
|
|
78
|
+
source === "container"
|
|
79
|
+
? {}
|
|
80
|
+
: Object.fromEntries(
|
|
81
|
+
keyVars.map((key) => [
|
|
82
|
+
key,
|
|
83
|
+
source === "task" ? "example-task" : "example-task-first, example-task-second",
|
|
84
|
+
]),
|
|
85
|
+
);
|
|
86
|
+
await expect(new CodexAdapter().createSession(config(env))).rejects.toThrow(
|
|
87
|
+
"test stops before subprocess spawn",
|
|
88
|
+
);
|
|
89
|
+
for (const key of keyVars) {
|
|
90
|
+
expect(
|
|
91
|
+
source === "container"
|
|
92
|
+
? ["example-first", "example-second"]
|
|
93
|
+
: source === "task"
|
|
94
|
+
? ["example-task"]
|
|
95
|
+
: ["example-task-first", "example-task-second"],
|
|
96
|
+
).toContain(capturedEnv?.[key]);
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
spawn.mockRestore();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
test("chatgpt mode still removes the OpenAI API key", async () => {
|
|
105
|
+
await Bun.write(join(home, ".codex", "auth.json"), JSON.stringify({ auth_mode: "chatgpt" }));
|
|
106
|
+
let capturedEnv: Record<string, string> | undefined;
|
|
107
|
+
const adapter = new CodexAdapter({
|
|
108
|
+
bypassSubprocess: true,
|
|
109
|
+
appServerFactory: ({ env }) => {
|
|
110
|
+
capturedEnv = env;
|
|
111
|
+
throw new Error("test stops before app-server spawn");
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
await expect(adapter.createSession(config({ OPENAI_API_KEY: "example-task" }))).rejects.toThrow(
|
|
115
|
+
"test stops before app-server spawn",
|
|
116
|
+
);
|
|
117
|
+
expect(capturedEnv).toBeDefined();
|
|
118
|
+
expect(capturedEnv?.OPENAI_API_KEY).toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
});
|