@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/http/kv.ts
CHANGED
|
@@ -12,7 +12,11 @@ import {
|
|
|
12
12
|
upsertKv,
|
|
13
13
|
} from "../be/db";
|
|
14
14
|
import { mcpOverflowAuthError } from "../kv-overflow";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
privateNamespaceError,
|
|
17
|
+
reservedNamespaceError,
|
|
18
|
+
reservedRoomKeyError,
|
|
19
|
+
} from "../kv-reserved-namespaces";
|
|
16
20
|
import { hasKvViewArgs, resolveKvView } from "../kv-view";
|
|
17
21
|
import { can } from "../rbac";
|
|
18
22
|
import { agentContextKey, pageContextKey } from "../tasks/context-key";
|
|
@@ -405,8 +409,9 @@ function authorizeRead(
|
|
|
405
409
|
namespace: string,
|
|
406
410
|
ctx: AuthCtx,
|
|
407
411
|
): { status: number; message: string } | null {
|
|
408
|
-
const
|
|
409
|
-
|
|
412
|
+
const readErr =
|
|
413
|
+
privateNamespaceError(namespace) ?? mcpOverflowAuthError(namespace, ctx.callerAgentId);
|
|
414
|
+
return readErr ? { status: 403, message: readErr } : null;
|
|
410
415
|
}
|
|
411
416
|
|
|
412
417
|
function encodeValueOrError(
|
package/src/http/onboarding.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from "../be/onboarding";
|
|
22
22
|
import { assertUrlSafe, publicEndpointSsrfOptions } from "../oauth/mcp-wrapper";
|
|
23
23
|
import { ProviderNameSchema } from "../types";
|
|
24
|
+
import { splitCredentialPool } from "../utils/credentials";
|
|
24
25
|
import { getOpenRouterAttributionHeaders } from "../utils/openrouter-base-url";
|
|
25
26
|
import { ensureConfigAdmin } from "./config";
|
|
26
27
|
import { scheduleIntegrationsReload } from "./core";
|
|
@@ -36,11 +37,14 @@ async function buildSignals(state: OnboardingState): Promise<OnboardingSignals>
|
|
|
36
37
|
const provider = ProviderNameSchema.safeParse(entry.provider);
|
|
37
38
|
if (!provider.success) continue;
|
|
38
39
|
const rollup = await rollupCredStatusForProvider(provider.data);
|
|
40
|
+
const live = rollup.latestLiveTest;
|
|
39
41
|
providers.push({
|
|
40
42
|
provider: provider.data,
|
|
41
43
|
state: rollup.state,
|
|
42
44
|
workers: rollup.workers,
|
|
43
45
|
verifiedWorkers: rollup.verifiedWorkers,
|
|
46
|
+
error: live && !live.ok ? (live.error ?? null) : null,
|
|
47
|
+
warning: live?.ok ? (live.warning ?? null) : null,
|
|
44
48
|
});
|
|
45
49
|
}
|
|
46
50
|
|
|
@@ -241,7 +245,8 @@ async function handleMemoryProbe(body: OnboardingMemoryRequest): Promise<Onboard
|
|
|
241
245
|
body.preset === "existing"
|
|
242
246
|
? (process.env.EMBEDDING_API_KEY ?? process.env.OPENAI_API_KEY)
|
|
243
247
|
: undefined;
|
|
244
|
-
|
|
248
|
+
// Test and save the same pool member.
|
|
249
|
+
const apiKey = splitCredentialPool(explicitKey ?? reusedKey ?? implicitKey ?? "")[0];
|
|
245
250
|
|
|
246
251
|
if (!apiKey) {
|
|
247
252
|
return {
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { baseUrlEnvVar, providerForKeyVar } from "@desplega/model-catalog";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import {
|
|
5
|
+
deleteSwarmConfigByKey,
|
|
6
|
+
getDbClient,
|
|
7
|
+
getKeyStatuses,
|
|
8
|
+
getSwarmConfigs,
|
|
9
|
+
recordKeyValidation,
|
|
10
|
+
} from "../be/db";
|
|
11
|
+
import { retirePoolKeyStatus } from "../be/db/api-keys";
|
|
12
|
+
import { upsertSwarmConfigWithPolicyMirror } from "../be/multi-runtime";
|
|
13
|
+
import { validateConfigEntry } from "../be/swarm-config-guard";
|
|
14
|
+
import type { SwarmConfig } from "../types";
|
|
15
|
+
import { CREDENTIAL_POOL_VARS, splitCredentialPool } from "../utils/credentials";
|
|
16
|
+
import { checkProviderPoolKey } from "../utils/provider-key-probes";
|
|
17
|
+
import { registerEnvSecrets, scrubSecrets } from "../utils/secret-scrubber";
|
|
18
|
+
import { ensureConfigAdmin, requestSecretReadScope, runConfigWriteSideEffects } from "./config";
|
|
19
|
+
import { getDeploymentEnvValue } from "./core";
|
|
20
|
+
import { route } from "./route-def";
|
|
21
|
+
import { jsonError } from "./utils";
|
|
22
|
+
|
|
23
|
+
export const PoolKeyStatusSchema = z.enum(["ok", "rejected", "rate_limited", "unchecked"]);
|
|
24
|
+
export type PoolKeyStatus = z.infer<typeof PoolKeyStatusSchema>;
|
|
25
|
+
export const PoolKeySchema = z.object({
|
|
26
|
+
index: z.number().int().min(0),
|
|
27
|
+
suffix: z.string(),
|
|
28
|
+
status: PoolKeyStatusSchema,
|
|
29
|
+
error: z.string().nullable(),
|
|
30
|
+
checkedAt: z.string().nullable(),
|
|
31
|
+
rateLimitedUntil: z.string().nullable(),
|
|
32
|
+
});
|
|
33
|
+
export type PoolKey = z.infer<typeof PoolKeySchema>;
|
|
34
|
+
export const PoolKeysResponseSchema = z.object({
|
|
35
|
+
key: z.string(),
|
|
36
|
+
source: z.enum(["stored", "env", "none"]),
|
|
37
|
+
readOnly: z.boolean(),
|
|
38
|
+
keys: z.array(PoolKeySchema),
|
|
39
|
+
});
|
|
40
|
+
export type PoolKeysResponse = z.infer<typeof PoolKeysResponseSchema>;
|
|
41
|
+
export const PoolKeyCheckResultSchema = z.enum(["verified", "rejected", "not_tested"]);
|
|
42
|
+
export type PoolKeyCheckResult = z.infer<typeof PoolKeyCheckResultSchema>;
|
|
43
|
+
export const PoolKeyCheckSchema = z.object({
|
|
44
|
+
suffix: z.string(),
|
|
45
|
+
result: PoolKeyCheckResultSchema,
|
|
46
|
+
httpStatus: z.number().nullable(),
|
|
47
|
+
error: z.string().nullable(),
|
|
48
|
+
});
|
|
49
|
+
export type PoolKeyCheck = z.infer<typeof PoolKeyCheckSchema>;
|
|
50
|
+
|
|
51
|
+
const poolVar = z
|
|
52
|
+
.string()
|
|
53
|
+
.refine(
|
|
54
|
+
(key) => key !== "CODEX_OAUTH" && CREDENTIAL_POOL_VARS.includes(key),
|
|
55
|
+
"Choose a credential pool variable.",
|
|
56
|
+
);
|
|
57
|
+
const getPoolKeys = route({
|
|
58
|
+
method: "get",
|
|
59
|
+
path: "/api/config/pool-keys",
|
|
60
|
+
pattern: ["api", "config", "pool-keys"],
|
|
61
|
+
summary: "List credential pool suffixes and status",
|
|
62
|
+
tags: ["Config"],
|
|
63
|
+
query: z.object({ key: poolVar }),
|
|
64
|
+
responses: {
|
|
65
|
+
200: { description: "Credential pool", schema: PoolKeysResponseSchema },
|
|
66
|
+
400: { description: "Invalid pool variable" },
|
|
67
|
+
403: { description: "Secret read permission required" },
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
const addPoolKeys = route({
|
|
71
|
+
method: "post",
|
|
72
|
+
path: "/api/config/pool-keys",
|
|
73
|
+
pattern: ["api", "config", "pool-keys"],
|
|
74
|
+
summary: "Test and append keys to a credential pool",
|
|
75
|
+
tags: ["Config"],
|
|
76
|
+
rbac: { permission: "config.write.any" },
|
|
77
|
+
body: z.object({
|
|
78
|
+
key: poolVar,
|
|
79
|
+
value: z.string(),
|
|
80
|
+
// The OpenRouter card can switch away from a stored gateway before the new key is saved.
|
|
81
|
+
baseUrl: z
|
|
82
|
+
.string()
|
|
83
|
+
.nullable()
|
|
84
|
+
.optional()
|
|
85
|
+
.describe(
|
|
86
|
+
"Base URL for the check only. Null tests the provider default. Omit to use the stored URL.",
|
|
87
|
+
),
|
|
88
|
+
}),
|
|
89
|
+
responses: {
|
|
90
|
+
200: {
|
|
91
|
+
description: "Updated credential pool and checks",
|
|
92
|
+
schema: PoolKeysResponseSchema.extend({ checks: z.array(PoolKeyCheckSchema) }),
|
|
93
|
+
},
|
|
94
|
+
400: { description: "Invalid key" },
|
|
95
|
+
403: { description: "Config administration and permission to read all secrets required" },
|
|
96
|
+
409: { description: "Duplicate key or deployment env pool" },
|
|
97
|
+
422: {
|
|
98
|
+
description: "Provider rejected a key. Nothing was saved.",
|
|
99
|
+
schema: z.object({ error: z.string(), checks: z.array(PoolKeyCheckSchema) }),
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
const deletePoolKey = route({
|
|
104
|
+
method: "delete",
|
|
105
|
+
path: "/api/config/pool-keys",
|
|
106
|
+
pattern: ["api", "config", "pool-keys"],
|
|
107
|
+
summary: "Remove one credential pool member",
|
|
108
|
+
tags: ["Config"],
|
|
109
|
+
rbac: { permission: "config.delete.any" },
|
|
110
|
+
query: z.object({
|
|
111
|
+
key: poolVar,
|
|
112
|
+
index: z.coerce.number().int().min(0),
|
|
113
|
+
suffix: z.string().min(1).max(5),
|
|
114
|
+
}),
|
|
115
|
+
responses: {
|
|
116
|
+
200: { description: "Updated credential pool", schema: PoolKeysResponseSchema },
|
|
117
|
+
400: { description: "Invalid pool member" },
|
|
118
|
+
403: { description: "Config administration and permission to read all secrets required" },
|
|
119
|
+
409: { description: "The pool changed or comes from the deployment env" },
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const ENV_POOL_ERROR = "These keys come from the deployment env. Change them there.";
|
|
124
|
+
|
|
125
|
+
async function readPool(key: string) {
|
|
126
|
+
const [row] = await getSwarmConfigs({ scope: "global", key });
|
|
127
|
+
const envValue = getDeploymentEnvValue(key);
|
|
128
|
+
const source = row ? "stored" : envValue ? "env" : "none";
|
|
129
|
+
const value = row?.value ?? envValue ?? "";
|
|
130
|
+
registerEnvSecrets({ [key]: value });
|
|
131
|
+
return { row, source, members: splitCredentialPool(value) } as const;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function poolResponse(key: string): Promise<PoolKeysResponse> {
|
|
135
|
+
const pool = await readPool(key);
|
|
136
|
+
const statuses = await getKeyStatuses(key, "global", null);
|
|
137
|
+
return {
|
|
138
|
+
key,
|
|
139
|
+
source: pool.source,
|
|
140
|
+
readOnly: pool.source === "env",
|
|
141
|
+
keys: pool.members.map((member, index) => {
|
|
142
|
+
const suffix = member.slice(-5);
|
|
143
|
+
const row = statuses.find((status) => status.keySuffix === suffix);
|
|
144
|
+
const rejected = !!row?.lastValidationFailedAt;
|
|
145
|
+
const limited =
|
|
146
|
+
row?.status === "rate_limited" &&
|
|
147
|
+
!!row.rateLimitedUntil &&
|
|
148
|
+
Date.parse(row.rateLimitedUntil) > Date.now();
|
|
149
|
+
return {
|
|
150
|
+
index,
|
|
151
|
+
suffix,
|
|
152
|
+
status: rejected ? "rejected" : limited ? "rate_limited" : row ? "ok" : "unchecked",
|
|
153
|
+
error: rejected ? scrubSecrets(row?.lastValidationError) || null : null,
|
|
154
|
+
checkedAt: rejected ? row!.lastValidationFailedAt : (row?.updatedAt ?? null),
|
|
155
|
+
rateLimitedUntil: row?.rateLimitedUntil ?? null,
|
|
156
|
+
};
|
|
157
|
+
}),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function duplicateError(members: readonly string[], additions: string[]): string | null {
|
|
162
|
+
const seen = new Set(members);
|
|
163
|
+
for (const member of additions) {
|
|
164
|
+
if (seen.has(member)) return `The key ending ${member.slice(-5)} is already in the pool.`;
|
|
165
|
+
seen.add(member);
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function savePool(key: string, members: string[], row: SwarmConfig | undefined) {
|
|
171
|
+
const value = members.join(",");
|
|
172
|
+
const validationError = validateConfigEntry({
|
|
173
|
+
scope: "global",
|
|
174
|
+
key,
|
|
175
|
+
value,
|
|
176
|
+
exportToEnv: row?.exportToEnv,
|
|
177
|
+
});
|
|
178
|
+
if (validationError) return validationError;
|
|
179
|
+
await upsertSwarmConfigWithPolicyMirror({
|
|
180
|
+
scope: "global",
|
|
181
|
+
key,
|
|
182
|
+
value,
|
|
183
|
+
isSecret: true,
|
|
184
|
+
description: row?.description ?? null,
|
|
185
|
+
envPath: row?.envPath ?? null,
|
|
186
|
+
exportToEnv: row?.exportToEnv,
|
|
187
|
+
});
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Every response lists the pool's key suffixes and statuses, so each route needs this gate. */
|
|
192
|
+
async function ensureSecretRead(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
|
|
193
|
+
if ((await requestSecretReadScope(req)) === "all") return true;
|
|
194
|
+
jsonError(res, "Reading credential pools requires permission to read all secrets.", 403);
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function handlePoolKeys(
|
|
199
|
+
req: IncomingMessage,
|
|
200
|
+
res: ServerResponse,
|
|
201
|
+
pathSegments: string[],
|
|
202
|
+
queryParams: URLSearchParams,
|
|
203
|
+
): Promise<boolean> {
|
|
204
|
+
if (getPoolKeys.match(req.method, pathSegments)) {
|
|
205
|
+
const parsed = await getPoolKeys.parse(req, res, pathSegments, queryParams);
|
|
206
|
+
if (!parsed) return true;
|
|
207
|
+
if (!(await ensureSecretRead(req, res))) return true;
|
|
208
|
+
getPoolKeys.respond(res, 200, await poolResponse(parsed.query.key));
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
if (addPoolKeys.match(req.method, pathSegments)) {
|
|
212
|
+
const parsed = await addPoolKeys.parse(req, res, pathSegments, queryParams);
|
|
213
|
+
if (!parsed) return true;
|
|
214
|
+
if (!(await ensureConfigAdmin(req, res, "config.write.any"))) return true;
|
|
215
|
+
if (!(await ensureSecretRead(req, res))) return true;
|
|
216
|
+
const { key, value, baseUrl } = parsed.body;
|
|
217
|
+
const additions = value
|
|
218
|
+
.split(",")
|
|
219
|
+
.map((part) => part.trim())
|
|
220
|
+
.filter(Boolean);
|
|
221
|
+
if (!additions.length) {
|
|
222
|
+
jsonError(res, "Enter a key.", 400);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
if (additions.some((part) => /\s/.test(part))) {
|
|
226
|
+
jsonError(res, "A key cannot contain spaces or line breaks.", 400);
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
registerEnvSecrets({ [key]: additions.join(",") });
|
|
230
|
+
const initial = await readPool(key);
|
|
231
|
+
if (initial.source === "env") {
|
|
232
|
+
jsonError(res, ENV_POOL_ERROR, 409);
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
const duplicate = duplicateError(initial.members, additions);
|
|
236
|
+
if (duplicate) {
|
|
237
|
+
jsonError(res, scrubSecrets(duplicate), 409);
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
const env = { ...process.env };
|
|
241
|
+
const provider = providerForKeyVar(key);
|
|
242
|
+
const baseUrlKey =
|
|
243
|
+
key === "OPENROUTER_API_KEY"
|
|
244
|
+
? "OPENROUTER_BASE_URL"
|
|
245
|
+
: provider
|
|
246
|
+
? baseUrlEnvVar(provider)
|
|
247
|
+
: undefined;
|
|
248
|
+
if (baseUrlKey && baseUrl !== undefined) {
|
|
249
|
+
env[baseUrlKey] = baseUrl ?? "";
|
|
250
|
+
} else if (baseUrlKey) {
|
|
251
|
+
const [row] = await getSwarmConfigs({ scope: "global", key: baseUrlKey });
|
|
252
|
+
if (row) env[baseUrlKey] = row.value;
|
|
253
|
+
}
|
|
254
|
+
const checks = await Promise.all(
|
|
255
|
+
additions.map(async (member): Promise<PoolKeyCheck> => {
|
|
256
|
+
try {
|
|
257
|
+
const probe = await checkProviderPoolKey(key, member, env);
|
|
258
|
+
return {
|
|
259
|
+
suffix: member.slice(-5),
|
|
260
|
+
result: probe.skipped
|
|
261
|
+
? "not_tested"
|
|
262
|
+
: probe.ok
|
|
263
|
+
? "verified"
|
|
264
|
+
: probe.httpStatus === 401
|
|
265
|
+
? "rejected"
|
|
266
|
+
: "not_tested",
|
|
267
|
+
httpStatus: probe.httpStatus ?? null,
|
|
268
|
+
error: scrubSecrets(probe.error?.replaceAll(member, "[REDACTED]")) || null,
|
|
269
|
+
};
|
|
270
|
+
} catch {
|
|
271
|
+
return {
|
|
272
|
+
suffix: member.slice(-5),
|
|
273
|
+
result: "not_tested",
|
|
274
|
+
httpStatus: null,
|
|
275
|
+
error: "The provider check did not complete.",
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}),
|
|
279
|
+
);
|
|
280
|
+
const rejected = checks.find((check) => check.result === "rejected");
|
|
281
|
+
if (rejected) {
|
|
282
|
+
addPoolKeys.respond(res, 422, {
|
|
283
|
+
error: scrubSecrets(
|
|
284
|
+
`The key ending ${rejected.suffix} was rejected (HTTP 401). Nothing was saved.`,
|
|
285
|
+
),
|
|
286
|
+
checks,
|
|
287
|
+
});
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
const result = await getDbClient().transaction(async () => {
|
|
291
|
+
const current = await readPool(key);
|
|
292
|
+
if (current.source === "env") return { error: ENV_POOL_ERROR, status: 409 };
|
|
293
|
+
const duplicate = duplicateError(current.members, additions);
|
|
294
|
+
if (duplicate) return { error: duplicate, status: 409 };
|
|
295
|
+
const members = [...current.members, ...additions];
|
|
296
|
+
const error = await savePool(key, members, current.row);
|
|
297
|
+
if (error) return { error, status: 400 };
|
|
298
|
+
await recordKeyValidation(
|
|
299
|
+
key,
|
|
300
|
+
checks.flatMap((check, index) =>
|
|
301
|
+
check.result === "verified"
|
|
302
|
+
? [{ keyIndex: current.members.length + index, keySuffix: check.suffix, ok: true }]
|
|
303
|
+
: [],
|
|
304
|
+
),
|
|
305
|
+
"global",
|
|
306
|
+
null,
|
|
307
|
+
{ retireAbsent: false },
|
|
308
|
+
);
|
|
309
|
+
return { value: members.join(",") };
|
|
310
|
+
});
|
|
311
|
+
if ("error" in result) {
|
|
312
|
+
jsonError(res, scrubSecrets(result.error), result.status);
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
await runConfigWriteSideEffects("global", key, result.value);
|
|
316
|
+
addPoolKeys.respond(res, 200, { ...(await poolResponse(key)), checks });
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
if (deletePoolKey.match(req.method, pathSegments)) {
|
|
320
|
+
const parsed = await deletePoolKey.parse(req, res, pathSegments, queryParams);
|
|
321
|
+
if (!parsed) return true;
|
|
322
|
+
if (!(await ensureConfigAdmin(req, res, "config.delete.any"))) return true;
|
|
323
|
+
if (!(await ensureSecretRead(req, res))) return true;
|
|
324
|
+
const { key, index, suffix } = parsed.query;
|
|
325
|
+
const result = await getDbClient().transaction(async () => {
|
|
326
|
+
const current = await readPool(key);
|
|
327
|
+
if (current.source === "env") return { error: ENV_POOL_ERROR, status: 409 };
|
|
328
|
+
if (!current.members[index] || current.members[index].slice(-5) !== suffix) {
|
|
329
|
+
return { error: "The key list changed. Reload it and try again.", status: 409 };
|
|
330
|
+
}
|
|
331
|
+
const members = current.members.filter((_, position) => position !== index);
|
|
332
|
+
if (members.length) {
|
|
333
|
+
const error = await savePool(key, members, current.row);
|
|
334
|
+
if (error) return { error, status: 400 };
|
|
335
|
+
} else {
|
|
336
|
+
await deleteSwarmConfigByKey("global", null, key);
|
|
337
|
+
}
|
|
338
|
+
await retirePoolKeyStatus(
|
|
339
|
+
key,
|
|
340
|
+
suffix,
|
|
341
|
+
members.map((member, keyIndex) => ({ keySuffix: member.slice(-5), keyIndex })),
|
|
342
|
+
);
|
|
343
|
+
return { value: members.join(",") };
|
|
344
|
+
});
|
|
345
|
+
if ("error" in result) {
|
|
346
|
+
jsonError(res, scrubSecrets(result.error), result.status);
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
await runConfigWriteSideEffects("global", key, result.value);
|
|
350
|
+
deletePoolKey.respond(res, 200, await poolResponse(key));
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
return false;
|
|
354
|
+
}
|
package/src/http/sessions.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
normalizeAssetKey,
|
|
7
7
|
parseAssetKey,
|
|
8
8
|
} from "../assets/key";
|
|
9
|
+
import { SOURCE_GROUPS_READ_ONLY, sourceMoveError, sourceOfKey } from "../assets/source-key";
|
|
9
10
|
import { AssetKeyAuthorizationError, authorizeAssetKeyWrite } from "../be/asset-key-auth";
|
|
10
11
|
import { resolveHttpAuditUserId } from "../be/audit-user";
|
|
11
12
|
import {
|
|
@@ -20,13 +21,19 @@ import {
|
|
|
20
21
|
getTaskById,
|
|
21
22
|
listRecentSessions,
|
|
22
23
|
} from "../be/db";
|
|
24
|
+
import { getSessionLabels } from "../be/db/session-labels";
|
|
23
25
|
import {
|
|
24
26
|
listPrefixRoots,
|
|
25
27
|
listSharedPrefixRoots,
|
|
26
28
|
moveSessionChains,
|
|
27
29
|
type SessionKeyMove,
|
|
28
30
|
} from "../be/db/session-moves";
|
|
29
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
getSessionGroupKey,
|
|
33
|
+
getSessionReadState,
|
|
34
|
+
getSessionTree,
|
|
35
|
+
triageSessionList,
|
|
36
|
+
} from "../be/db/session-tree";
|
|
30
37
|
import { getUserGrant } from "../be/rbac-roles";
|
|
31
38
|
import {
|
|
32
39
|
getSessionRoot,
|
|
@@ -53,6 +60,7 @@ import {
|
|
|
53
60
|
import { can, isRbacEnabled } from "../rbac";
|
|
54
61
|
import { sessionAccessTopic } from "../realtime/auth";
|
|
55
62
|
import { realtimeBus } from "../realtime/bus";
|
|
63
|
+
import { warmSlackChannelLabels } from "../slack/channel-labels";
|
|
56
64
|
import {
|
|
57
65
|
type AgentTask,
|
|
58
66
|
AgentTaskSchema,
|
|
@@ -152,6 +160,7 @@ const SessionTreeGroupSchema = SessionSectionCountsSchema.extend({
|
|
|
152
160
|
lastActivityAt: z.string(),
|
|
153
161
|
});
|
|
154
162
|
const SessionTreeResponseSchema = z.object({
|
|
163
|
+
labels: z.record(z.string(), z.string()).optional(),
|
|
155
164
|
groups: z.array(SessionTreeGroupSchema),
|
|
156
165
|
inbox: SessionSectionCountsSchema,
|
|
157
166
|
later: SessionSectionCountsSchema,
|
|
@@ -738,7 +747,7 @@ async function checkSessionMove(
|
|
|
738
747
|
actor: SessionActor,
|
|
739
748
|
req: IncomingMessage,
|
|
740
749
|
myAgentId: string | undefined,
|
|
741
|
-
options: { ownerOnly?: boolean } = {},
|
|
750
|
+
options: { ownerOnly?: boolean; destination?: string | null } = {},
|
|
742
751
|
): Promise<{ reason: string } | { taskIds: string[] }> {
|
|
743
752
|
const root = await getTaskById(rootTaskId);
|
|
744
753
|
if (!root) return { reason: SESSION_MOVE_UNAVAILABLE };
|
|
@@ -752,6 +761,8 @@ async function checkSessionMove(
|
|
|
752
761
|
if (!hasSessionAccess(access, "write")) return { reason: "Session write access required" };
|
|
753
762
|
}
|
|
754
763
|
if (root.parentTaskId) return { reason: "Task is not a session root" };
|
|
764
|
+
const sourceError = sourceMoveError(options.destination ?? null, root);
|
|
765
|
+
if (sourceError) return { reason: sourceError };
|
|
755
766
|
const tasks = await getRootTaskChain(root.id);
|
|
756
767
|
for (const task of tasks) {
|
|
757
768
|
if (!(await canMutateTaskNamespace(task, myAgentId, req))) {
|
|
@@ -778,6 +789,8 @@ const getSession = route({
|
|
|
778
789
|
chain: z.array(TaskWithSteeringSchema),
|
|
779
790
|
access: SessionAccessSchema,
|
|
780
791
|
owner: SessionOwnerSchema.nullable(),
|
|
792
|
+
/** The group of `root` with the list's rule: a source thread with two or more roots is its own group. */
|
|
793
|
+
groupKey: z.string(),
|
|
781
794
|
approvals: z.array(SessionApprovalSchema).optional(),
|
|
782
795
|
/** The viewer's read state, with the list's rule. Omitted for callers that act for no user. */
|
|
783
796
|
readState: z.enum(["unread", "read", "later", "done"]).optional(),
|
|
@@ -1012,15 +1025,18 @@ export async function handleSessions(
|
|
|
1012
1025
|
const parsed = await sessionTree.parse(req, res, pathSegments, queryParams);
|
|
1013
1026
|
if (!parsed) return true;
|
|
1014
1027
|
const actor = await resolveSessionActor(req, myAgentId);
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1028
|
+
const tree = await getSessionTree(actor, undefined, {
|
|
1029
|
+
mine: parsed.query.show !== "all",
|
|
1030
|
+
hideSource: parsed.query.hideSource,
|
|
1031
|
+
showSource: parsed.query.showSource,
|
|
1032
|
+
});
|
|
1033
|
+
// Labels cover hidden sources too: a pane or tab of a hidden source still names its group.
|
|
1034
|
+
const { sourceGroups, slackChannels, threadLabels, ...counts } = tree;
|
|
1035
|
+
const labels = { ...threadLabels, ...(await getSessionLabels(sourceGroups)) };
|
|
1036
|
+
warmSlackChannelLabels(
|
|
1037
|
+
slackChannels.map((id) => `shared/slack/${id}/`).filter((key) => labels[key] === undefined),
|
|
1023
1038
|
);
|
|
1039
|
+
sessionTree.respond(res, 200, { ...counts, labels });
|
|
1024
1040
|
return true;
|
|
1025
1041
|
}
|
|
1026
1042
|
|
|
@@ -1054,7 +1070,9 @@ export async function handleSessions(
|
|
|
1054
1070
|
const skipped: { rootTaskId: string; reason: string }[] = [];
|
|
1055
1071
|
const moves: SessionKeyMove[] = [];
|
|
1056
1072
|
for (const rootId of new Set(parsed.body.rootTaskIds)) {
|
|
1057
|
-
const checked = await checkSessionMove(rootId, actor, req, myAgentId
|
|
1073
|
+
const checked = await checkSessionMove(rootId, actor, req, myAgentId, {
|
|
1074
|
+
destination: key,
|
|
1075
|
+
});
|
|
1058
1076
|
if ("reason" in checked) {
|
|
1059
1077
|
skipped.push({ rootTaskId: rootId, reason: checked.reason });
|
|
1060
1078
|
} else {
|
|
@@ -1076,6 +1094,10 @@ export async function handleSessions(
|
|
|
1076
1094
|
const parsed = await moveSessionPrefix.parse(req, res, pathSegments, queryParams);
|
|
1077
1095
|
if (!parsed) return true;
|
|
1078
1096
|
const from = normalizeAssetKey(parsed.body.from);
|
|
1097
|
+
if (sourceOfKey(from) || (parsed.body.to && sourceOfKey(normalizeAssetKey(parsed.body.to)))) {
|
|
1098
|
+
jsonError(res, SOURCE_GROUPS_READ_ONLY, 400);
|
|
1099
|
+
return true;
|
|
1100
|
+
}
|
|
1079
1101
|
if (parseAssetKey(from).relativeSegments.length === 0) {
|
|
1080
1102
|
jsonError(res, "Cannot move a namespace root", 400);
|
|
1081
1103
|
return true;
|
|
@@ -1126,6 +1148,7 @@ export async function handleSessions(
|
|
|
1126
1148
|
}
|
|
1127
1149
|
const checked = await checkSessionMove(root.id, actor, req, myAgentId, {
|
|
1128
1150
|
ownerOnly: true,
|
|
1151
|
+
destination: nextKey,
|
|
1129
1152
|
});
|
|
1130
1153
|
if ("reason" in checked) {
|
|
1131
1154
|
skipped.push({ rootTaskId: root.id, reason: checked.reason });
|
|
@@ -1175,7 +1198,7 @@ export async function handleSessions(
|
|
|
1175
1198
|
const chain = await getRootTaskChain(parsed.params.rootTaskId);
|
|
1176
1199
|
// Load steering, citations and pending approvals for the already loaded chain.
|
|
1177
1200
|
const tasks = [root, ...chain];
|
|
1178
|
-
const [steering, citations, approvals, read] = await Promise.all([
|
|
1201
|
+
const [steering, citations, approvals, read, groupKey] = await Promise.all([
|
|
1179
1202
|
getTaskSteeringFieldsForTasks(tasks),
|
|
1180
1203
|
getTaskCitationsForTasks(tasks.map((task) => task.id)),
|
|
1181
1204
|
getPendingApprovalsForTasks(tasks.map((task) => task.id)),
|
|
@@ -1184,6 +1207,7 @@ export async function handleSessions(
|
|
|
1184
1207
|
actor.kind === "user" && actor.userId
|
|
1185
1208
|
? getSessionReadState(sessionRoot.id, actor.userId)
|
|
1186
1209
|
: Promise.resolve(null),
|
|
1210
|
+
getSessionGroupKey(root),
|
|
1187
1211
|
]);
|
|
1188
1212
|
const decorate = (task: AgentTask) => ({
|
|
1189
1213
|
...task,
|
|
@@ -1195,6 +1219,7 @@ export async function handleSessions(
|
|
|
1195
1219
|
chain: chain.map(decorate),
|
|
1196
1220
|
access,
|
|
1197
1221
|
owner,
|
|
1222
|
+
groupKey,
|
|
1198
1223
|
...(approvals.length ? { approvals } : {}),
|
|
1199
1224
|
...(read ?? {}),
|
|
1200
1225
|
});
|
package/src/http/stats.ts
CHANGED
|
@@ -86,6 +86,7 @@ const SwarmMetricsSchema = z.object({
|
|
|
86
86
|
agentLog: RetentionTableStatsSchema.optional(),
|
|
87
87
|
events: RetentionTableStatsSchema.optional(),
|
|
88
88
|
contextVersions: RetentionTableStatsSchema.optional(),
|
|
89
|
+
scriptRuns: RetentionTableStatsSchema.optional(),
|
|
89
90
|
}),
|
|
90
91
|
});
|
|
91
92
|
|
package/src/http/status.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
getDbClient,
|
|
30
30
|
getInstanceActivity,
|
|
31
31
|
getLiveAgentCounts,
|
|
32
|
+
getReadyAgentCounts,
|
|
32
33
|
hasFirstCompletedTask,
|
|
33
34
|
listAgentsWithCredStatusByProvider,
|
|
34
35
|
NOT_EXTENSION_AGENT_SQL,
|
|
@@ -121,6 +122,12 @@ export type StatusIdentity = z.infer<typeof StatusIdentitySchema>;
|
|
|
121
122
|
export const StatusActivitySchema = z.object({
|
|
122
123
|
agents_online: z.number().int().nonnegative(),
|
|
123
124
|
leads_online: z.number().int().nonnegative(),
|
|
125
|
+
/**
|
|
126
|
+
* Leads that can take work now (status `idle` or `busy`). Unlike
|
|
127
|
+
* `leads_online`, this does not depend on a recent heartbeat: an idle lead
|
|
128
|
+
* stops updating its activity time.
|
|
129
|
+
*/
|
|
130
|
+
leads_ready: z.number().int().nonnegative(),
|
|
124
131
|
recent_tasks_count: z.number().int().nonnegative(),
|
|
125
132
|
});
|
|
126
133
|
export type StatusActivity = z.infer<typeof StatusActivitySchema>;
|
|
@@ -194,12 +201,13 @@ export type AutomationStatus = z.infer<typeof AutomationStatusSchema>;
|
|
|
194
201
|
/**
|
|
195
202
|
* Phase 2: Aggregate health derived from setup milestones.
|
|
196
203
|
*
|
|
197
|
-
* - `broken
|
|
198
|
-
* - `degraded
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
204
|
+
* - `broken`: harness or workers blocking (creds missing, no live workers ever).
|
|
205
|
+
* - `degraded`: no lead can take work (none is `idle` or `busy`), OR at
|
|
206
|
+
* least one optional integration `unverified`/`configured` while another
|
|
207
|
+
* is configured, OR harness creds present but the latest live test failed
|
|
208
|
+
* or never ran (`configured`).
|
|
209
|
+
* - `ok`: harness + workers `verified`, a lead can take work, and no
|
|
210
|
+
* integration left in `configured` state.
|
|
203
211
|
*/
|
|
204
212
|
export const StatusHealthSchema = z.enum(["ok", "degraded", "broken"]);
|
|
205
213
|
export type StatusHealth = z.infer<typeof StatusHealthSchema>;
|
|
@@ -692,19 +700,23 @@ export async function buildSetup(
|
|
|
692
700
|
* Decision matrix:
|
|
693
701
|
* - Harness `unverified` (no creds at all) → `broken` — the swarm cannot run a task.
|
|
694
702
|
* - Workers `unverified` (no agents ever) → `broken` — same reason.
|
|
703
|
+
* - No lead can take work (`readyLeads` is 0) → `degraded`. Tasks wait until a lead is online.
|
|
695
704
|
* - Harness `configured` (creds present, never live-tested) → `degraded`.
|
|
696
705
|
* - Any of {slack, github, linear, jira} `configured` (i.e. half-set-up) → `degraded`.
|
|
697
706
|
* - Otherwise → `ok`.
|
|
698
707
|
*
|
|
699
708
|
* Notes:
|
|
709
|
+
* - `readyLeads` counts leads with status `idle` or `busy`
|
|
710
|
+
* (`getReadyAgentCounts`). It does not use the heartbeat time: an agent
|
|
711
|
+
* updates `lastActivityAt` only while a session runs, so an idle, healthy
|
|
712
|
+
* lead looks stale.
|
|
700
713
|
* - Worker `configured` (fleet exists but missed recent heartbeats) does NOT
|
|
701
|
-
* degrade the rollup
|
|
702
|
-
* /agents page and the dashboard canvas — not a setup health signal.
|
|
714
|
+
* degrade the rollup on its own, for the same reason.
|
|
703
715
|
* - Integrations in `unverified` are NOT degrading on their own — most
|
|
704
716
|
* deployments don't connect every integration. They only nudge the rollup if
|
|
705
717
|
* paired with another integration in `configured` (the brainstorm contract).
|
|
706
718
|
*/
|
|
707
|
-
export function computeHealth(setup: SetupMilestone[]): StatusHealth {
|
|
719
|
+
export function computeHealth(setup: SetupMilestone[], readyLeads: number): StatusHealth {
|
|
708
720
|
const byId = new Map(setup.map((m) => [m.id, m] as const));
|
|
709
721
|
const harness = byId.get("harness");
|
|
710
722
|
const workers = byId.get("workers");
|
|
@@ -714,6 +726,7 @@ export function computeHealth(setup: SetupMilestone[]): StatusHealth {
|
|
|
714
726
|
if (!workers || workers.state === "unverified") return "broken";
|
|
715
727
|
|
|
716
728
|
// `degraded` rules.
|
|
729
|
+
if (readyLeads === 0) return "degraded";
|
|
717
730
|
if (harness.state === "configured") return "degraded";
|
|
718
731
|
|
|
719
732
|
for (const id of ["slack", "github", "linear", "jira"] as const) {
|
|
@@ -740,12 +753,13 @@ export async function buildStatusPayload(): Promise<StatusResponse> {
|
|
|
740
753
|
.map((automation) => toStatus(preflightAutomation(automation, automationSetup)))
|
|
741
754
|
.sort((a, b) => a.name.localeCompare(b.name) || a.kind.localeCompare(b.kind));
|
|
742
755
|
const activity = await getInstanceActivity();
|
|
756
|
+
const readyLeads = (await getReadyAgentCounts()).leads;
|
|
743
757
|
const instance = await getInstanceFeedbackSignals();
|
|
744
758
|
return {
|
|
745
759
|
runtime_pools: { enabled: isRuntimePoolsEnabled(), supervisor: runtimeSupervisorStatus() },
|
|
746
760
|
identity: buildIdentity(),
|
|
747
761
|
setup,
|
|
748
|
-
activity,
|
|
762
|
+
activity: { ...activity, leads_ready: readyLeads },
|
|
749
763
|
agent_fs: {
|
|
750
764
|
configured: !!process.env.AGENT_FS_API_URL,
|
|
751
765
|
base_url: process.env.AGENT_FS_API_URL ?? null,
|
|
@@ -753,7 +767,7 @@ export async function buildStatusPayload(): Promise<StatusResponse> {
|
|
|
753
767
|
comb: getCombStatus(await combServiceUserId),
|
|
754
768
|
},
|
|
755
769
|
automations,
|
|
756
|
-
health: computeHealth(setup),
|
|
770
|
+
health: computeHealth(setup, readyLeads),
|
|
757
771
|
telemetry: { enabled: isTelemetryEnabled() },
|
|
758
772
|
instance,
|
|
759
773
|
};
|
|
@@ -797,7 +811,7 @@ const getStatus = route({
|
|
|
797
811
|
pattern: ["status"],
|
|
798
812
|
summary: "Identity + setup readiness + live activity for the swarm dashboard",
|
|
799
813
|
description:
|
|
800
|
-
"Single source of truth consumed by the UI home page. Identity comes from SWARM_* envs; setup milestones each emit `unverified | configured | verified`; automations report `running | needs_setup` from the same runtime preflight used at dispatch; activity counts agents alive in the last 5 min and tasks created in the last 24h; agent_fs reports whether AGENT_FS_API_URL is set, plus the Comb settings (COMB_ENABLED, the browser-facing agent-fs URL, the shared org and drive ids, and the agent-fs user id of the swarm service account); telemetry reports the effective ANONYMIZED_TELEMETRY opt-out state.",
|
|
814
|
+
"Single source of truth consumed by the UI home page. Identity comes from SWARM_* envs; setup milestones each emit `unverified | configured | verified`; automations report `running | needs_setup` from the same runtime preflight used at dispatch; activity counts agents alive in the last 5 min, leads that can take work (status idle or busy), and tasks created in the last 24h; agent_fs reports whether AGENT_FS_API_URL is set, plus the Comb settings (COMB_ENABLED, the browser-facing agent-fs URL, the shared org and drive ids, and the agent-fs user id of the swarm service account); telemetry reports the effective ANONYMIZED_TELEMETRY opt-out state.",
|
|
801
815
|
tags: ["Status"],
|
|
802
816
|
responses: {
|
|
803
817
|
200: { description: "Status payload", schema: StatusResponseSchema },
|