@desplega.ai/agent-swarm 2.2.0 → 2.4.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 +1 -0
- package/dist/{acp-adapter-k0vwfyvn.js → acp-adapter-kedstxx6.js} +6 -6
- package/dist/{actions-w4pnb8vz.js → actions-vget6r1k.js} +18 -16
- package/dist/{amp-adapter-0cmxb2t5.js → amp-adapter-xeb5mktv.js} +2 -2
- package/dist/{app-zneem7g9.js → app-mqap8t8m.js} +3 -3
- package/dist/{assistant-wbzd2rnr.js → assistant-h0gv97mv.js} +20 -18
- package/dist/{boot-reembed-4kx69j3g.js → boot-reembed-qr8a894z.js} +1 -1
- package/dist/{boot-reembed-db0wwgy1.js → boot-reembed-rj85jjhw.js} +1 -1
- package/dist/{boot-scrub-sweep-tfv4d3mm.js → boot-scrub-sweep-eyfvn6v9.js} +3 -3
- package/dist/{boot-seeds-w2hmkxn5.js → boot-seeds-xs9a8dx1.js} +4 -4
- package/dist/{claude-adapter-5qrc2paq.js → claude-adapter-bm23cye7.js} +5 -5
- package/dist/{claude-managed-adapter-n7ahjyx6.js → claude-managed-adapter-1mwrsm9d.js} +2 -2
- package/dist/{claude-managed-setup-7ra32em4.js → claude-managed-setup-gkfgks4j.js} +1 -1
- package/dist/{claude-sdk-session-cd02gc76.js → claude-sdk-session-apb4ypz3.js} +5 -5
- package/dist/cli-02vhhssk.js +848 -0
- package/dist/{cli-4ync7m60.js → cli-0ebey9hg.js} +1 -1
- package/dist/{cli-665j3feb.js → cli-0xk505e8.js} +1 -1
- package/dist/{cli-8c1m9mhy.js → cli-1cevz46c.js} +5 -5
- package/dist/{cli-gzvb3d4a.js → cli-1jx0srjt.js} +3 -3
- package/dist/{cli-zj4d6afs.js → cli-1pa3rz69.js} +21 -6
- package/dist/{cli-vmbakbq0.js → cli-2rwet3r7.js} +1 -1
- package/dist/{cli-azs9bhc5.js → cli-3wjs22x3.js} +2 -2
- package/dist/{cli-tba8fxf9.js → cli-4fhyq711.js} +45 -36
- package/dist/{cli-ak4c12mh.js → cli-5ff2sjyt.js} +1 -1
- package/dist/{cli-6q7pyd3k.js → cli-5s5hx57y.js} +5 -3
- package/dist/{cli-h23mr2s6.js → cli-6ftrrwm1.js} +9 -371
- package/dist/{cli-9ac0mkg5.js → cli-8b9wvjps.js} +1 -1
- package/dist/cli-8cxkw1f5.js +382 -0
- package/dist/{cli-xhgtay7w.js → cli-8rwvksbc.js} +2 -2
- package/dist/{cli-e09hj50q.js → cli-8wkb09q5.js} +11 -11
- package/dist/{cli-ksrphveb.js → cli-a1esmmee.js} +14 -2
- package/dist/{cli-xnp2vkx7.js → cli-avmqhatn.js} +7 -7
- package/dist/{cli-xx13gqf3.js → cli-b80a1100.js} +1 -1
- package/dist/{cli-z1hh99j0.js → cli-bfc3k1x5.js} +3 -3
- package/dist/{cli-0k9rcwfy.js → cli-bhwa8rzc.js} +33 -15
- package/dist/{cli-8cvvytad.js → cli-bn26xbcs.js} +2 -2
- package/dist/{cli-k28p6xh7.js → cli-c8fwpgqb.js} +1 -1
- package/dist/{cli-rc3r0bnh.js → cli-ccn0eybf.js} +1 -1
- package/dist/{cli-9td5ss65.js → cli-dsgj3e7a.js} +2 -2
- package/dist/{cli-jrk8mj0t.js → cli-ecshf504.js} +2 -2
- package/dist/{cli-raejy0mp.js → cli-eqgfs202.js} +1 -1
- package/dist/{cli-ppapy34w.js → cli-f3nx1h8z.js} +2 -2
- package/dist/{cli-c9vrwcpb.js → cli-ffbrkh1r.js} +74 -35
- package/dist/{cli-mnry16kh.js → cli-kk2r80n5.js} +4 -4
- package/dist/{cli-c0dpzwnx.js → cli-kz116kvw.js} +3 -3
- package/dist/{cli-e5y285rg.js → cli-mvg2hf8p.js} +4 -4
- package/dist/{cli-2jm992rm.js → cli-mz1ezzzh.js} +2 -2
- package/dist/{cli-dw0njd9q.js → cli-nme7wery.js} +6 -6
- package/dist/{cli-mvsfbbgq.js → cli-nrrg0fvy.js} +18 -4
- package/dist/{cli-8jtrekpy.js → cli-p1m2b6gc.js} +7 -7
- package/dist/{cli-4c7k75rq.js → cli-rg7ntypm.js} +378 -84
- package/dist/{cli-z3dk189k.js → cli-rsd7ga7v.js} +10 -8
- package/dist/{cli-etvn5ej2.js → cli-sppc0cmw.js} +1 -1
- package/dist/{cli-8w65vwee.js → cli-tg9ksbn8.js} +1 -1
- package/dist/{cli-th8krqbf.js → cli-tgvpmnwz.js} +2 -2
- package/dist/{cli-hwc4a9xm.js → cli-tj4j2ts4.js} +6 -6
- package/dist/{cli-jhhb2v9n.js → cli-ttaz6c7m.js} +2 -2
- package/dist/{cli-w7z94e6c.js → cli-tx00c0h6.js} +4 -2
- package/dist/{cli-4g629sgr.js → cli-wrv09v7q.js} +20 -1
- package/dist/{cli-cby329ny.js → cli-wyrr38xm.js} +2 -2
- package/dist/{cli-5ks6x36q.js → cli-x1p5dhq2.js} +3 -3
- package/dist/{cli-bddxamg7.js → cli-x719j3vs.js} +220 -629
- package/dist/{cli-ztcxv5nv.js → cli-xfe63zx3.js} +1 -1
- package/dist/{cli-q6gw45wj.js → cli-ywyrffff.js} +1 -1
- package/dist/{cli-p46xdr1m.js → cli-zvze9rbm.js} +1 -1
- package/dist/cli.js +15 -15
- package/dist/{codex-adapter-m0fx3v3s.js → codex-adapter-pp2gq8gr.js} +3 -3
- package/dist/{codex-hook-x6tm6y5m.js → codex-hook-qwzccd64.js} +1 -1
- package/dist/{codex-session-runner-j9m2gfyy.js → codex-session-runner-0vzn09t8.js} +3 -3
- package/dist/{commands-ktnqwrv4.js → commands-psqhkttb.js} +2 -2
- package/dist/{controller-nex2he8v.js → controller-t20vz2k8.js} +2 -2
- package/dist/{controllers-connect-6yhwqv9v.js → controllers-connect-gkxmqw9d.js} +24 -20
- package/dist/{cursor-adapter-02s5ekva.js → cursor-adapter-3v542qe5.js} +1 -1
- package/dist/{db-jvr86v7f.js → db-2xbyenjr.js} +2 -2
- package/dist/{db-19tw9r5s.js → db-nc1rd869.js} +3 -2
- package/dist/{dispatcher-d3mnsg2y.js → dispatcher-ewceb8ts.js} +17 -15
- package/dist/{dsh-adapter-63rvej88.js → dsh-adapter-75xwh78t.js} +1 -1
- package/dist/{easter-egg-3d-6hg2r942.js → easter-egg-3d-xsc47jwf.js} +1 -1
- package/dist/{execute-bcqc69a5.js → execute-gzmw1mnw.js} +1 -1
- package/dist/{external-channel-44cwer32.js → external-channel-a5dsmxn4.js} +2 -2
- package/dist/{google-generative-ai-phpmwxvg.js → google-generative-ai-bmq8gkf3.js} +1 -1
- package/dist/{google-vertex-es9ybgdy.js → google-vertex-b1rvp52s.js} +1 -1
- package/dist/{grok-adapter-ysp907f6.js → grok-adapter-vj88xr23.js} +7 -7
- package/dist/handlers-zbs90bny.js +39 -0
- package/dist/{heartbeat-2etacsqq.js → heartbeat-t2m9r3wa.js} +20 -18
- package/dist/{hook-mzxxs5n2.js → hook-8femx1bg.js} +4 -4
- package/dist/{hook-3kvmax6d.js → hook-xa677f8r.js} +5 -5
- package/dist/{http-fn978m0g.js → http-xpmmq028.js} +940 -269
- package/dist/{index-e4tsa1h7.js → index-8pwc1jc8.js} +17 -15
- package/dist/{index-w2wr89xd.js → index-cdzyfrsp.js} +1 -1
- package/dist/{index-1r8hw4vd.js → index-dg2tmj99.js} +17 -15
- package/dist/index-nxa6me0q.js +41 -0
- package/dist/{index-039x2sag.js → index-s7ckdajq.js} +42 -14
- package/dist/{index-wgsw9xcz.js → index-syztqdt6.js} +1 -1
- package/dist/{index-x0q2a4f9.js → index-ysd45kgc.js} +1 -1
- package/dist/{index-g909y089.js → index-zgvqzjsw.js} +8 -8
- package/dist/{keepalive-d4nzs76x.js → keepalive-3nr4st3r.js} +3 -3
- package/dist/{lead-8h5n9k5f.js → lead-td8h2sfn.js} +33 -31
- package/dist/{maintenance-ea6jfw42.js → maintenance-verwpe2c.js} +1 -1
- package/dist/{oauth-refresh-sweep-3f25djq3.js → oauth-refresh-sweep-9q2t0y3q.js} +2 -2
- package/dist/{onboard-pvk7hdb2.js → onboard-aytqedmq.js} +2 -2
- package/dist/{opencode-adapter-eeesgfqk.js → opencode-adapter-f4hryjny.js} +4 -4
- package/dist/{otel-impl-qb7nhgdp.js → otel-impl-hckw4wt1.js} +1 -1
- package/dist/{pi-mono-adapter-zasp3ze3.js → pi-mono-adapter-hxby0ce1.js} +6 -6
- package/dist/{pricing-refresh-6ehwkcmp.js → pricing-refresh-5nvme3sm.js} +3 -3
- package/dist/{rbac-roles-km0f9cmm.js → rbac-roles-pd8c26m1.js} +5 -3
- package/dist/{rbac-roles-9648nh00.js → rbac-roles-vbpy21mg.js} +2 -2
- package/dist/{render-v2-xkze1wef.js → render-v2-7nqgvk2q.js} +3 -3
- package/dist/{resume-tq0bbvt5.js → resume-b6djcj3c.js} +6 -6
- package/dist/{runtime-ar8310ba.js → runtime-txrxc9za.js} +1 -1
- package/dist/session-access-fc3f9y12.js +25 -0
- package/dist/{setup-rnrazxvd.js → setup-n39szm6b.js} +1 -1
- package/dist/{virtual-modules-wzk6jf3a.js → virtual-modules-wyp2k6z3.js} +2 -2
- package/dist/{work-objects-e75q2rqs.js → work-objects-jem2fre0.js} +20 -18
- package/dist/{worker-4ww4hjef.js → worker-wr6xgfr2.js} +33 -31
- package/openapi.json +1011 -115
- package/package.json +2 -2
- package/src/apps/action-caller.ts +6 -0
- package/src/apps/script-types.ts +16 -3
- package/src/be/asset-key-audit.ts +4 -13
- package/src/be/db/active-sessions.ts +5 -0
- package/src/be/db/session-visibility.ts +29 -0
- package/src/be/db/tasks/read.ts +61 -3
- package/src/be/db/tasks/write.ts +15 -6
- package/src/be/db.ts +149 -66
- package/src/be/events.ts +11 -0
- package/src/be/migrations/227_inbox_item_state_last_read_at.sql +1 -0
- package/src/be/migrations/230_session_shares.sql +23 -0
- package/src/be/rbac-roles.ts +2 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +22 -4
- package/src/be/session-shares.ts +244 -0
- package/src/be/swarm-config-guard.ts +23 -2
- package/src/commands/runner.ts +11 -0
- package/src/heartbeat/heartbeat.ts +25 -7
- package/src/http/active-sessions.ts +13 -2
- package/src/http/agents.ts +10 -3
- package/src/http/all-routes.ts +1 -0
- package/src/http/api-keys.ts +3 -1
- package/src/http/approval-requests.ts +2 -1
- package/src/http/apps.ts +36 -11
- package/src/http/assets.ts +26 -4
- package/src/http/config.ts +3 -0
- package/src/http/context.ts +2 -0
- package/src/http/core.ts +39 -2
- package/src/http/events.ts +6 -2
- package/src/http/favorite-owner.ts +1 -1
- package/src/http/fs.ts +38 -3
- package/src/http/inbox-state.ts +58 -8
- package/src/http/index.ts +3 -1
- package/src/http/memory.ts +3 -1
- package/src/http/ranking.ts +367 -0
- package/src/http/realtime.ts +26 -21
- package/src/http/route-def.ts +77 -11
- package/src/http/script-runs.ts +2 -1
- package/src/http/scripts.ts +8 -0
- package/src/http/session-access.ts +179 -0
- package/src/http/session-data.ts +10 -2
- package/src/http/sessions.ts +212 -12
- package/src/http/stats.ts +7 -3
- package/src/http/tasks.ts +83 -7
- package/src/http/used-by.ts +11 -3
- package/src/kv-reserved-namespaces.ts +1 -0
- package/src/prompts/session-templates.ts +3 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/ranking/telemetry-ranking.ts +147 -0
- package/src/ranking/usage.ts +57 -0
- package/src/rbac/admission.ts +22 -7
- package/src/rbac/legacy-policy.ts +2 -0
- package/src/rbac/permissions.ts +9 -0
- package/src/realtime/auth.ts +69 -0
- package/src/realtime/tickets.ts +4 -1
- package/src/realtime/transport.ts +77 -9
- package/src/slack/blocks.ts +18 -0
- package/src/slack/dm.ts +29 -0
- package/src/telemetry-contract/EVENTS.md +3 -0
- package/src/telemetry-contract/SOURCE.json +37 -4
- package/src/telemetry-contract/catalog.json +69 -0
- package/src/telemetry-contract/ranking/fixtures/expected.json +30 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/bad-category.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/bad-claim.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/blank-description.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/blank-name.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/description-too-long.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/domain-uppercase.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-scheme.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-www.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/http-source-url.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/missing-description.json +10 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/missing-version.json +10 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/name-not-string.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/name-too-long.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/not-object.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/null.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/other-with-subcategory.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/oversize.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/short-claim.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/subcategory-other-category.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/trailing-data.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/unknown-field.json +12 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/unsupported-version.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-not-date.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-offset.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/wrong-case-field.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/category-without-subcategory.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/full.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/minimal.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/padded-text.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/unicode-name.json +11 -0
- package/src/telemetry-contract/ranking/listing.v1.schema.json +61 -0
- package/src/telemetry-contract/ranking/taxonomy.json +8 -0
- package/src/telemetry-contract/ranking-taxonomy.gen.ts +11 -0
- package/src/telemetry-contract/types.gen.ts +708 -1
- package/src/telemetry.ts +26 -2
- package/src/tests/apps-rbac.test.ts +189 -2
- package/src/tests/apps-script-types.test.ts +16 -3
- package/src/tests/asset-key-api.test.ts +128 -0
- package/src/tests/asset-key-invariants.test.ts +179 -3
- package/src/tests/asset-key-migration.test.ts +35 -0
- package/src/tests/check-pr-body.test.ts +23 -0
- package/src/tests/check-ranking-entry.test.ts +146 -0
- package/src/tests/db-extraction-contract.test.ts +1 -0
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/heartbeat-checklist.test.ts +88 -0
- package/src/tests/heartbeat-jev-gate.test.ts +394 -10
- package/src/tests/inbox-state.test.ts +167 -0
- package/src/tests/kv-tool.test.ts +15 -0
- package/src/tests/org-identity.test.ts +44 -0
- package/src/tests/ranking-listing.test.ts +112 -0
- package/src/tests/ranking-route.test.ts +319 -0
- package/src/tests/ranking-usage.test.ts +50 -0
- package/src/tests/rbac-admission-e2e.test.ts +8 -0
- package/src/tests/rbac-admission.test.ts +38 -0
- package/src/tests/rbac-charact-http.test.ts +4 -3
- package/src/tests/rbac-engine.test.ts +2 -0
- package/src/tests/rbac-roles.test.ts +2 -0
- package/src/tests/realtime-room-surfaces.test.ts +3 -1
- package/src/tests/realtime-session-join-race.test.ts +142 -0
- package/src/tests/realtime-tickets.test.ts +24 -17
- package/src/tests/realtime-transport.test.ts +91 -0
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/session-route-posture.test.ts +639 -0
- package/src/tests/session-sharing-routes.test.ts +282 -0
- package/src/tests/session-sharing.test.ts +526 -0
- package/src/tests/sessions-detail-batch.test.ts +7 -1
- package/src/tests/slack-tool-blocks.test.ts +101 -0
- package/src/tests/slack-update.test.ts +55 -0
- package/src/tests/slack-user-dm.test.ts +177 -0
- package/src/tests/used-by-route.test.ts +10 -0
- package/src/tools/slack-post.ts +18 -12
- package/src/tools/slack-read.ts +13 -1
- package/src/tools/slack-reply.ts +7 -10
- package/src/tools/slack-start-thread.ts +18 -11
- package/src/tools/slack-update.ts +2 -9
- package/src/types.ts +37 -0
- package/src/utils/org-identity.ts +33 -0
- package/templates/skills/apps/SKILL.md +17 -0
- package/templates/skills/apps/content.md +17 -0
- package/templates/skills/code-quality/content.md +2 -2
- package/templates/skills/pages/content.md +4 -0
- package/templates/skills/swarm-scripts/SKILL.md +6 -0
- package/templates/skills/swarm-scripts/content.md +6 -0
- package/dist/handlers-t5pbabwf.js +0 -37
- package/dist/index-qyjbb7br.js +0 -39
- package/dist/{cli-n5mrsg0f.js → cli-60cgh1rv.js} +3 -3
- /package/dist/{cli-j0v75y13.js → cli-y83zk5sb.js} +0 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getDbClient, upsertSwarmConfig } from "../be/db";
|
|
4
|
+
import {
|
|
5
|
+
buildListing,
|
|
6
|
+
mintRankingClaim,
|
|
7
|
+
PUBLIC_RANKING_ENABLED_KEY,
|
|
8
|
+
RANKING_CATEGORIES,
|
|
9
|
+
rankingEntryFile,
|
|
10
|
+
rankingFormSchema,
|
|
11
|
+
rankingLeaveUrl,
|
|
12
|
+
rankingPrUrl,
|
|
13
|
+
readTelemetryRanking,
|
|
14
|
+
TELEMETRY_RANKING_KEY,
|
|
15
|
+
type TelemetryRanking,
|
|
16
|
+
} from "../ranking/telemetry-ranking";
|
|
17
|
+
import { isTelemetryEnabled, telemetry } from "../telemetry";
|
|
18
|
+
import { isEnvFlagEnabled } from "../utils/env-flag";
|
|
19
|
+
import {
|
|
20
|
+
ORG_IDENTITY_KEYS,
|
|
21
|
+
type OrgIdentityStatus,
|
|
22
|
+
orgIdentityStatus,
|
|
23
|
+
} from "../utils/org-identity";
|
|
24
|
+
import { ensureConfigAdmin } from "./config";
|
|
25
|
+
import { scheduleIntegrationsReload } from "./core";
|
|
26
|
+
import { route } from "./route-def";
|
|
27
|
+
import { globalConfig, telemetryProxyEndpoint } from "./used-by";
|
|
28
|
+
|
|
29
|
+
// ─── Schemas ─────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const ListingSchema = z.object({
|
|
32
|
+
v: z.literal(1),
|
|
33
|
+
claim: z.string(),
|
|
34
|
+
name: z.string(),
|
|
35
|
+
domain: z.string(),
|
|
36
|
+
category: z.enum(RANKING_CATEGORIES),
|
|
37
|
+
subcategory: z.string().nullable(),
|
|
38
|
+
description: z.string().nullable(),
|
|
39
|
+
sourceCodeUrl: z.string().nullable(),
|
|
40
|
+
updatedAt: z.string(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const RankingStateSchema = z.object({
|
|
44
|
+
eligible: z.boolean(),
|
|
45
|
+
missing: z.array(z.enum(ORG_IDENTITY_KEYS)),
|
|
46
|
+
telemetryOn: z.boolean(),
|
|
47
|
+
enabled: z.boolean(),
|
|
48
|
+
/** The org identity the listing would copy. */
|
|
49
|
+
name: z.string().nullable(),
|
|
50
|
+
domain: z.string().nullable(),
|
|
51
|
+
listing: ListingSchema.nullable(),
|
|
52
|
+
/** False when the stored listing was built for another website: a new PR is needed. */
|
|
53
|
+
listingMatchesDomain: z.boolean(),
|
|
54
|
+
fileJson: z.string().nullable(),
|
|
55
|
+
prUrl: z.string().nullable(),
|
|
56
|
+
verifyUrl: z.string().nullable(),
|
|
57
|
+
});
|
|
58
|
+
export type RankingState = z.infer<typeof RankingStateSchema>;
|
|
59
|
+
|
|
60
|
+
const OptInResponseSchema = z.object({
|
|
61
|
+
domain: z.string(),
|
|
62
|
+
claim: z.string(),
|
|
63
|
+
listing: ListingSchema,
|
|
64
|
+
fileJson: z.string(),
|
|
65
|
+
prUrl: z.string(),
|
|
66
|
+
verifyUrl: z.string(),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const OptOutResponseSchema = z.object({
|
|
70
|
+
enabled: z.literal(false),
|
|
71
|
+
leaveUrl: z.string().nullable(),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
const getRanking = route({
|
|
77
|
+
method: "get",
|
|
78
|
+
path: "/api/ranking",
|
|
79
|
+
pattern: ["api", "ranking"],
|
|
80
|
+
summary: "Public ranking state",
|
|
81
|
+
description:
|
|
82
|
+
"Whether this install can join the public agent-swarm.dev ranking (telemetry on, organization name and website set), whether it is opted in, and its stored listing. The listing carries the private ranking claim, so only a config admin can read it.",
|
|
83
|
+
tags: ["Telemetry"],
|
|
84
|
+
responses: {
|
|
85
|
+
200: { description: "Ranking state", schema: RankingStateSchema },
|
|
86
|
+
401: { description: "Unauthorized" },
|
|
87
|
+
403: { description: "Only a config admin can read the ranking state" },
|
|
88
|
+
},
|
|
89
|
+
rbac: { permission: "config.write.any" },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const optIn = route({
|
|
93
|
+
method: "post",
|
|
94
|
+
path: "/api/ranking/opt-in",
|
|
95
|
+
pattern: ["api", "ranking", "opt-in"],
|
|
96
|
+
summary: "Opt this install into the public ranking",
|
|
97
|
+
description:
|
|
98
|
+
"Builds the telemetry_ranking listing from the organization name and website plus the chosen category, stores it, turns PUBLIC_RANKING_ENABLED on and sends ranking.opted_in. The install is ranked only after a ranking PR with the returned file is merged into desplega-ai/agent-swarm.",
|
|
99
|
+
tags: ["Telemetry"],
|
|
100
|
+
body: rankingFormSchema,
|
|
101
|
+
responses: {
|
|
102
|
+
200: { description: "Opted in", schema: OptInResponseSchema },
|
|
103
|
+
400: { description: "A listing field is invalid" },
|
|
104
|
+
401: { description: "Unauthorized" },
|
|
105
|
+
403: { description: "Only a config admin can opt in" },
|
|
106
|
+
409: {
|
|
107
|
+
description:
|
|
108
|
+
"telemetry_off: telemetry is off or the install has no ID. org_identity_missing: the organization name or website is not set (`missing` lists which).",
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
rbac: { permission: "config.write.any" },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const optOut = route({
|
|
115
|
+
method: "post",
|
|
116
|
+
path: "/api/ranking/opt-out",
|
|
117
|
+
pattern: ["api", "ranking", "opt-out"],
|
|
118
|
+
summary: "Leave the public ranking",
|
|
119
|
+
description:
|
|
120
|
+
"Turns PUBLIC_RANKING_ENABLED off and sends ranking.opted_out, so workers stop sending ranking.usage. Returns the GitHub page that deletes the ranking entry.",
|
|
121
|
+
tags: ["Telemetry"],
|
|
122
|
+
responses: {
|
|
123
|
+
200: { description: "Opted out", schema: OptOutResponseSchema },
|
|
124
|
+
401: { description: "Unauthorized" },
|
|
125
|
+
403: { description: "Only a config admin can opt out" },
|
|
126
|
+
},
|
|
127
|
+
rbac: { permission: "config.write.any" },
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
const RANKING_CONFIG_KEYS = new Set<string>([...ORG_IDENTITY_KEYS, PUBLIC_RANKING_ENABLED_KEY]);
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The org identity from global config. `changed` carries a value just written
|
|
136
|
+
* or deleted, since `globalConfig` falls back to `process.env`, which the
|
|
137
|
+
* debounced reload has not updated yet.
|
|
138
|
+
*/
|
|
139
|
+
async function currentIdentity(changed?: {
|
|
140
|
+
key: string;
|
|
141
|
+
value: string | null;
|
|
142
|
+
}): Promise<OrgIdentityStatus> {
|
|
143
|
+
const read = async (key: string) =>
|
|
144
|
+
changed?.key === key ? changed.value : await globalConfig(key);
|
|
145
|
+
return orgIdentityStatus({
|
|
146
|
+
SWARM_ORG_NAME: await read("SWARM_ORG_NAME"),
|
|
147
|
+
SWARM_ORG_WEBSITE: await read("SWARM_ORG_WEBSITE"),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function storedListing(): Promise<TelemetryRanking | null> {
|
|
152
|
+
return readTelemetryRanking(await globalConfig(TELEMETRY_RANKING_KEY));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function rankingEnabled(): Promise<boolean> {
|
|
156
|
+
const raw = await globalConfig(PUBLIC_RANKING_ENABLED_KEY);
|
|
157
|
+
return isEnvFlagEnabled(PUBLIC_RANKING_ENABLED_KEY, false, {
|
|
158
|
+
[PUBLIC_RANKING_ENABLED_KEY]: raw,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function telemetryReady(): Promise<boolean> {
|
|
163
|
+
return isTelemetryEnabled() && Boolean(await globalConfig("telemetry_installation_id"));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function verifyUrl(listing: Pick<TelemetryRanking, "domain" | "claim">): Promise<string> {
|
|
167
|
+
const params = new URLSearchParams({ domain: listing.domain, claim: listing.claim });
|
|
168
|
+
return `${await telemetryProxyEndpoint("/v1/ranking/verify")}?${params.toString()}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function setEnabled(enabled: boolean): Promise<void> {
|
|
172
|
+
await upsertSwarmConfig({
|
|
173
|
+
scope: "global",
|
|
174
|
+
key: PUBLIC_RANKING_ENABLED_KEY,
|
|
175
|
+
value: String(enabled),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function emitOptedIn(listing: TelemetryRanking): void {
|
|
180
|
+
telemetry.ranking(
|
|
181
|
+
"opted_in",
|
|
182
|
+
{ listing: JSON.stringify(listing) },
|
|
183
|
+
{ orgName: listing.name, orgWebsite: listing.domain },
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function emitOptedOut(domain: string): void {
|
|
188
|
+
telemetry.ranking("opted_out", { domain });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function buildRankingState(): Promise<RankingState> {
|
|
192
|
+
const [identity, listing, enabled, telemetryOn] = await Promise.all([
|
|
193
|
+
currentIdentity(),
|
|
194
|
+
storedListing(),
|
|
195
|
+
rankingEnabled(),
|
|
196
|
+
telemetryReady(),
|
|
197
|
+
]);
|
|
198
|
+
const listingMatchesDomain = Boolean(listing && listing.domain === identity.website);
|
|
199
|
+
const current = listingMatchesDomain ? listing : null;
|
|
200
|
+
return {
|
|
201
|
+
eligible: telemetryOn && identity.ok,
|
|
202
|
+
missing: identity.missing,
|
|
203
|
+
telemetryOn,
|
|
204
|
+
enabled: enabled && listingMatchesDomain,
|
|
205
|
+
name: identity.name,
|
|
206
|
+
domain: identity.website,
|
|
207
|
+
listing,
|
|
208
|
+
listingMatchesDomain,
|
|
209
|
+
fileJson: current ? rankingEntryFile(current) : null,
|
|
210
|
+
prUrl: current ? rankingPrUrl(current) : null,
|
|
211
|
+
verifyUrl: current ? await verifyUrl(current) : null,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Keep the listing in step with the org identity after a global config write.
|
|
217
|
+
* A renamed org rebuilds the listing with the same claim and re-sends it; a
|
|
218
|
+
* cleared or placeholder name, a changed or cleared website, or the setting
|
|
219
|
+
* turned off by hand opts the install out.
|
|
220
|
+
*/
|
|
221
|
+
export async function syncRankingAfterConfigChange(
|
|
222
|
+
key: string,
|
|
223
|
+
value: string | null,
|
|
224
|
+
): Promise<void> {
|
|
225
|
+
if (!RANKING_CONFIG_KEYS.has(key)) return;
|
|
226
|
+
const listing = await storedListing();
|
|
227
|
+
if (!listing) return;
|
|
228
|
+
if (key === PUBLIC_RANKING_ENABLED_KEY) {
|
|
229
|
+
if (!isEnvFlagEnabled(key, false, { [key]: value ?? undefined })) {
|
|
230
|
+
emitOptedOut(listing.domain);
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (!(await rankingEnabled())) return;
|
|
235
|
+
|
|
236
|
+
const identity = await currentIdentity({ key, value });
|
|
237
|
+
let rebuilt: TelemetryRanking | null = null;
|
|
238
|
+
if (identity.ok && identity.website === listing.domain) {
|
|
239
|
+
if (identity.name === listing.name) return;
|
|
240
|
+
try {
|
|
241
|
+
rebuilt = buildListing(identity, listing, listing.claim);
|
|
242
|
+
} catch {
|
|
243
|
+
// A name the listing cannot carry (over 60 characters) leaves the ranking.
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (!rebuilt) {
|
|
247
|
+
await setEnabled(false);
|
|
248
|
+
emitOptedOut(listing.domain);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
await upsertSwarmConfig({
|
|
252
|
+
scope: "global",
|
|
253
|
+
key: TELEMETRY_RANKING_KEY,
|
|
254
|
+
value: JSON.stringify(rebuilt),
|
|
255
|
+
});
|
|
256
|
+
emitOptedIn(rebuilt);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function conflict(res: ServerResponse, body: Record<string, unknown>): void {
|
|
260
|
+
res.writeHead(409, { "Content-Type": "application/json" });
|
|
261
|
+
res.end(JSON.stringify(body));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
export async function handleRanking(
|
|
267
|
+
req: IncomingMessage,
|
|
268
|
+
res: ServerResponse,
|
|
269
|
+
pathSegments: string[],
|
|
270
|
+
queryParams: URLSearchParams,
|
|
271
|
+
): Promise<boolean> {
|
|
272
|
+
if (getRanking.match(req.method, pathSegments)) {
|
|
273
|
+
const parsed = await getRanking.parse(req, res, pathSegments, queryParams);
|
|
274
|
+
if (!parsed) return true;
|
|
275
|
+
// The state carries the claim and the PR that proves ownership of the listing.
|
|
276
|
+
if (!(await ensureConfigAdmin(req, res, "config.write.any"))) return true;
|
|
277
|
+
getRanking.respond(res, 200, await buildRankingState());
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (optIn.match(req.method, pathSegments)) {
|
|
282
|
+
const parsed = await optIn.parse(req, res, pathSegments, queryParams);
|
|
283
|
+
if (!parsed) return true;
|
|
284
|
+
if (!(await ensureConfigAdmin(req, res, "config.write.any"))) return true;
|
|
285
|
+
|
|
286
|
+
if (!(await telemetryReady())) {
|
|
287
|
+
conflict(res, {
|
|
288
|
+
error: "telemetry_off",
|
|
289
|
+
message: "Turn telemetry on first: the ranking counts tokens from telemetry.",
|
|
290
|
+
});
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
const identity = await currentIdentity();
|
|
294
|
+
if (!identity.ok || !identity.website) {
|
|
295
|
+
conflict(res, {
|
|
296
|
+
error: "org_identity_missing",
|
|
297
|
+
message: "Set the organization name and website first",
|
|
298
|
+
missing: identity.missing,
|
|
299
|
+
});
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const website = identity.website;
|
|
304
|
+
const db = getDbClient();
|
|
305
|
+
// Read the stored claim under the write lock: two concurrent first opt-ins
|
|
306
|
+
// must return the same claim, or the one merged into community/ranking is lost.
|
|
307
|
+
const built = await db.transaction(async () => {
|
|
308
|
+
const stored = await storedListing();
|
|
309
|
+
const claim = stored?.domain === website ? stored.claim : mintRankingClaim();
|
|
310
|
+
let listing: TelemetryRanking;
|
|
311
|
+
try {
|
|
312
|
+
listing = buildListing(identity, parsed.body, claim);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
return { ok: false as const, error };
|
|
315
|
+
}
|
|
316
|
+
await upsertSwarmConfig({
|
|
317
|
+
scope: "global",
|
|
318
|
+
key: TELEMETRY_RANKING_KEY,
|
|
319
|
+
value: JSON.stringify(listing),
|
|
320
|
+
});
|
|
321
|
+
await setEnabled(true);
|
|
322
|
+
db.afterCommit(() => emitOptedIn(listing));
|
|
323
|
+
return { ok: true as const, listing };
|
|
324
|
+
});
|
|
325
|
+
if (!built.ok) {
|
|
326
|
+
const issue = built.error instanceof z.ZodError ? built.error.issues[0] : undefined;
|
|
327
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
328
|
+
res.end(
|
|
329
|
+
JSON.stringify({
|
|
330
|
+
error: issue
|
|
331
|
+
? `Invalid ${issue.path.join(".") || "listing"}: ${issue.message}`
|
|
332
|
+
: "Invalid listing",
|
|
333
|
+
}),
|
|
334
|
+
);
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
const { listing } = built;
|
|
338
|
+
scheduleIntegrationsReload();
|
|
339
|
+
|
|
340
|
+
optIn.respond(res, 200, {
|
|
341
|
+
domain: listing.domain,
|
|
342
|
+
claim: listing.claim,
|
|
343
|
+
listing,
|
|
344
|
+
fileJson: rankingEntryFile(listing),
|
|
345
|
+
prUrl: rankingPrUrl(listing),
|
|
346
|
+
verifyUrl: await verifyUrl(listing),
|
|
347
|
+
});
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (optOut.match(req.method, pathSegments)) {
|
|
352
|
+
const parsed = await optOut.parse(req, res, pathSegments, queryParams);
|
|
353
|
+
if (!parsed) return true;
|
|
354
|
+
if (!(await ensureConfigAdmin(req, res, "config.write.any"))) return true;
|
|
355
|
+
const listing = await storedListing();
|
|
356
|
+
await setEnabled(false);
|
|
357
|
+
if (listing) emitOptedOut(listing.domain);
|
|
358
|
+
scheduleIntegrationsReload();
|
|
359
|
+
optOut.respond(res, 200, {
|
|
360
|
+
enabled: false,
|
|
361
|
+
leaveUrl: listing ? rankingLeaveUrl(listing.domain) : null,
|
|
362
|
+
});
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return false;
|
|
367
|
+
}
|
package/src/http/realtime.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import { findUserById } from "../be/users";
|
|
4
4
|
import { can } from "../rbac";
|
|
5
5
|
import bundle from "../realtime/browser.generated.txt" with { type: "text" };
|
|
6
6
|
import { issueRealtimeTicket } from "../realtime/tickets";
|
|
7
7
|
import { getRequestAuth } from "../utils/request-auth-context";
|
|
8
|
+
import { dashboardUserIdHeader } from "./favorite-owner";
|
|
8
9
|
import { route } from "./route-def";
|
|
9
10
|
import { jsonError } from "./utils";
|
|
10
11
|
|
|
@@ -14,14 +15,16 @@ const realtimeTicketRoute = route({
|
|
|
14
15
|
pattern: ["api", "realtime", "ticket"],
|
|
15
16
|
summary: "Issue a one-shot dashboard realtime ticket",
|
|
16
17
|
tags: ["Rooms"],
|
|
17
|
-
|
|
18
|
+
description:
|
|
19
|
+
"The ticket authorizes one dashboard socket for presence rooms: Comb drives (`comb.presence`) and sessions (`session.presence`). An operator-key tab's picked user (`X-Swarm-User-Id`) rides on the ticket so session presence can check that user's access.",
|
|
20
|
+
// Comb presence came first; a role granting only `comb.presence` keeps its ticket.
|
|
21
|
+
rbac: { permission: "session.presence", alsoAdmits: ["comb.presence"] },
|
|
18
22
|
responses: {
|
|
19
23
|
200: {
|
|
20
24
|
description: "One-shot realtime ticket",
|
|
21
25
|
schema: z.object({ ticket: z.string(), expiresAt: z.number().int() }),
|
|
22
26
|
},
|
|
23
|
-
403: { description: "
|
|
24
|
-
404: { description: "Comb is not enabled" },
|
|
27
|
+
403: { description: "Presence is not allowed" },
|
|
25
28
|
},
|
|
26
29
|
});
|
|
27
30
|
|
|
@@ -37,7 +40,7 @@ route({
|
|
|
37
40
|
},
|
|
38
41
|
});
|
|
39
42
|
|
|
40
|
-
/** `POST /api/realtime/ticket`: a one-shot ticket for a dashboard realtime socket (
|
|
43
|
+
/** `POST /api/realtime/ticket`: a one-shot ticket for a dashboard realtime socket (presence rooms). */
|
|
41
44
|
export async function handleRealtimeTicket(
|
|
42
45
|
req: IncomingMessage,
|
|
43
46
|
res: ServerResponse,
|
|
@@ -45,34 +48,36 @@ export async function handleRealtimeTicket(
|
|
|
45
48
|
queryParams: URLSearchParams,
|
|
46
49
|
): Promise<boolean> {
|
|
47
50
|
if (!realtimeTicketRoute.match(req.method, pathSegments)) return false;
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
jsonError(res, "Comb is not enabled", 404);
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
51
|
+
// Session presence needs the ticket with Comb off too; each room still
|
|
52
|
+
// checks its own verb and access when the socket joins it.
|
|
53
53
|
const parsed = await realtimeTicketRoute.parse(req, res, pathSegments, queryParams);
|
|
54
54
|
if (!parsed) return true;
|
|
55
55
|
const auth = getRequestAuth(req);
|
|
56
56
|
if (!auth || auth.kind === "agent" || auth.kind === "guest" || auth.kind === "controller") {
|
|
57
|
-
jsonError(res, "
|
|
57
|
+
jsonError(res, "Presence requires dashboard authentication", 403);
|
|
58
58
|
return true;
|
|
59
59
|
}
|
|
60
60
|
const principal =
|
|
61
61
|
auth.kind === "operator"
|
|
62
62
|
? ({ kind: "operator" } as const)
|
|
63
63
|
: ({ kind: "user", userId: auth.userId } as const);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
resource: { kind: "none" },
|
|
69
|
-
source: "http",
|
|
70
|
-
}).allow
|
|
71
|
-
) {
|
|
72
|
-
jsonError(res, "Comb presence requires the comb.presence permission", 403);
|
|
64
|
+
const allowed = (verb: "comb.presence" | "session.presence") =>
|
|
65
|
+
can({ principal, verb, resource: { kind: "none" }, source: "http" }).allow;
|
|
66
|
+
if (!allowed("comb.presence") && !allowed("session.presence")) {
|
|
67
|
+
jsonError(res, "Presence requires the comb.presence or session.presence permission", 403);
|
|
73
68
|
return true;
|
|
74
69
|
}
|
|
75
|
-
|
|
70
|
+
let dashboardUserId: string | undefined;
|
|
71
|
+
if (auth.kind === "operator" && !auth.page) {
|
|
72
|
+
const picked = dashboardUserIdHeader(req);
|
|
73
|
+
const user = picked ? await findUserById(picked) : null;
|
|
74
|
+
if (user?.status === "active") dashboardUserId = user.id;
|
|
75
|
+
}
|
|
76
|
+
realtimeTicketRoute.respond(
|
|
77
|
+
res,
|
|
78
|
+
200,
|
|
79
|
+
issueRealtimeTicket(dashboardUserId ? { ...auth, dashboardUserId } : auth),
|
|
80
|
+
);
|
|
76
81
|
return true;
|
|
77
82
|
}
|
|
78
83
|
|
package/src/http/route-def.ts
CHANGED
|
@@ -76,7 +76,20 @@ export interface RouteDef<
|
|
|
76
76
|
auth?: {
|
|
77
77
|
apiKey?: boolean; // default true
|
|
78
78
|
agentId?: boolean; // requires X-Agent-ID
|
|
79
|
+
/**
|
|
80
|
+
* Why a user token may name an agent in X-Agent-ID here. Everywhere else
|
|
81
|
+
* `handleCore` refuses that pair. The user still needs `agent.act.any`.
|
|
82
|
+
*/
|
|
83
|
+
userNamesAgent?: string;
|
|
79
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* Session posture: how a user caller's session access gates this route.
|
|
87
|
+
* Required on every route keyed by a task, steering message or attachment,
|
|
88
|
+
* on every write whose body names a task, and on every active-session write
|
|
89
|
+
* (`session-route-posture.test.ts`).
|
|
90
|
+
* `handleCore` enforces the path forms; `parse()` enforces `bodyTaskIds`.
|
|
91
|
+
*/
|
|
92
|
+
session?: SessionPosture;
|
|
80
93
|
/**
|
|
81
94
|
* RBAC posture (DES-445). Required on every non-GET route — enforced by
|
|
82
95
|
* `bun run check:rbac-coverage` (CI). Either name the `PermissionVerb` the
|
|
@@ -84,7 +97,56 @@ export interface RouteDef<
|
|
|
84
97
|
* Declarative in slice 1: the handler still calls `can()` itself; the role
|
|
85
98
|
* engine (increment 3) may enforce the declared verb centrally.
|
|
86
99
|
*/
|
|
87
|
-
rbac?:
|
|
100
|
+
rbac?:
|
|
101
|
+
| { permission: PermissionVerb; alsoAdmits?: readonly PermissionVerb[] }
|
|
102
|
+
| { ungated: string };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* - `taskParam` / `steeringParam`: the path param naming a task, or a steering
|
|
107
|
+
* message whose task counts. A GET needs read access, any other method write.
|
|
108
|
+
* - `activeSessionParam`: the path param naming an active-session row; its task
|
|
109
|
+
* counts the same way. A row with no task admits no user.
|
|
110
|
+
* - `bodyTaskIds`: the task ids a write's body names; each needs write access.
|
|
111
|
+
* - `agentWide`: why the route changes sessions across a whole agent. It
|
|
112
|
+
* refuses every user actor; agents and the operator key keep access.
|
|
113
|
+
* - `none`: why the session does not gate this route.
|
|
114
|
+
*/
|
|
115
|
+
export type SessionPosture =
|
|
116
|
+
| { taskParam: string }
|
|
117
|
+
| { steeringParam: string }
|
|
118
|
+
| { activeSessionParam: string }
|
|
119
|
+
| { agentWide: string }
|
|
120
|
+
| { bodyTaskIds: (body: unknown) => readonly unknown[] }
|
|
121
|
+
| { none: string };
|
|
122
|
+
|
|
123
|
+
/** A `bodyTaskIds` posture reading one top-level body key. */
|
|
124
|
+
export function bodyTask(key: string): SessionPosture {
|
|
125
|
+
return { bodyTaskIds: (body) => [(body as Record<string, unknown> | null | undefined)?.[key]] };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A `bodyTaskIds` posture reading `key` off every item of the body's `listKey` array. */
|
|
129
|
+
export function bodyListTask(listKey: string, key: string): SessionPosture {
|
|
130
|
+
return {
|
|
131
|
+
bodyTaskIds: (body) => {
|
|
132
|
+
const items = (body as Record<string, unknown> | null | undefined)?.[listKey];
|
|
133
|
+
return Array.isArray(items)
|
|
134
|
+
? items.map((item) => (item as Record<string, unknown> | null)?.[key])
|
|
135
|
+
: [];
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The raw `{name}` path params of `def` read off the request's segments. */
|
|
141
|
+
export function routePathParams(def: RouteDef, pathSegments: string[]): Record<string, string> {
|
|
142
|
+
const params: Record<string, string> = {};
|
|
143
|
+
const paramNames = def.path.match(/\{(\w+)\}/g)?.map((p) => p.slice(1, -1)) ?? [];
|
|
144
|
+
for (let i = 0; i < def.pattern.length; i++) {
|
|
145
|
+
if (def.pattern[i] === null && paramNames.length > 0) {
|
|
146
|
+
params[paramNames.shift()!] = pathSegments[i] ?? "";
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return params;
|
|
88
150
|
}
|
|
89
151
|
|
|
90
152
|
interface ParsedRequest<TParams, TQuery, TBody> {
|
|
@@ -367,21 +429,25 @@ export function route<
|
|
|
367
429
|
|
|
368
430
|
async parse(req, res, pathSegments, queryParams) {
|
|
369
431
|
try {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
432
|
+
const rawBody = def.body ? await parseBody(req, def.maxBodyBytes) : undefined;
|
|
433
|
+
// Session access first, so a malformed body cannot tell a reader
|
|
434
|
+
// anything about a session they cannot write in.
|
|
435
|
+
if (def.session && "bodyTaskIds" in def.session) {
|
|
436
|
+
// Lazy: session-access reaches the DB layer, which imports route files.
|
|
437
|
+
const { bodySessionDenial } = await import("./session-access");
|
|
438
|
+
const denial = await bodySessionDenial(req, def.session.bodyTaskIds(rawBody));
|
|
439
|
+
if (denial) {
|
|
440
|
+
jsonError(res, denial, 403);
|
|
441
|
+
return null;
|
|
378
442
|
}
|
|
379
443
|
}
|
|
380
444
|
|
|
381
445
|
// Parse + validate each part
|
|
382
|
-
const params = def.params
|
|
446
|
+
const params = def.params
|
|
447
|
+
? def.params.parse(routePathParams(def, pathSegments))
|
|
448
|
+
: undefined;
|
|
383
449
|
const query = def.query ? def.query.parse(Object.fromEntries(queryParams)) : undefined;
|
|
384
|
-
const body = def.body ? def.body.parse(
|
|
450
|
+
const body = def.body ? def.body.parse(rawBody) : undefined;
|
|
385
451
|
|
|
386
452
|
return { params, query, body } as ParsedRequest<
|
|
387
453
|
z.infer<TParams>,
|
package/src/http/script-runs.ts
CHANGED
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
RawLlmConfigSchema,
|
|
48
48
|
RawLlmOutputSchema,
|
|
49
49
|
} from "../workflows/executors/raw-llm";
|
|
50
|
-
import { route } from "./route-def";
|
|
50
|
+
import { bodyTask, route } from "./route-def";
|
|
51
51
|
import { json, jsonError } from "./utils";
|
|
52
52
|
|
|
53
53
|
const DEFAULT_SCRIPT_RUN_CONCURRENCY_CAP = 10;
|
|
@@ -282,6 +282,7 @@ const agentTaskRoute = route({
|
|
|
282
282
|
method: "post",
|
|
283
283
|
path: "/api/internal/script-runs/{runId}/agent-task",
|
|
284
284
|
pattern: ["api", "internal", "script-runs", null, "agent-task"],
|
|
285
|
+
session: bodyTask("parentTaskId"),
|
|
285
286
|
operationId: "script_runs_internal_agent_task",
|
|
286
287
|
summary: "Create or wait for a script workflow agent task step",
|
|
287
288
|
tags: ["Script Runs"],
|
package/src/http/scripts.ts
CHANGED
|
@@ -225,6 +225,10 @@ const upsertRoute = route({
|
|
|
225
225
|
method: "post",
|
|
226
226
|
path: "/api/scripts/upsert",
|
|
227
227
|
pattern: ["api", "scripts", "upsert"],
|
|
228
|
+
auth: {
|
|
229
|
+
userNamesAgent:
|
|
230
|
+
"dashboard script playground: a user runs or saves a script in a chosen agent's scope",
|
|
231
|
+
},
|
|
228
232
|
operationId: "scripts_upsert",
|
|
229
233
|
summary: "Create or update a reusable script",
|
|
230
234
|
description: "Explicit script upserts run a TypeScript typecheck before writing.",
|
|
@@ -253,6 +257,10 @@ const runRoute = route({
|
|
|
253
257
|
method: "post",
|
|
254
258
|
path: "/api/scripts/run",
|
|
255
259
|
pattern: ["api", "scripts", "run"],
|
|
260
|
+
auth: {
|
|
261
|
+
userNamesAgent:
|
|
262
|
+
"dashboard script playground: a user runs or saves a script in a chosen agent's scope",
|
|
263
|
+
},
|
|
256
264
|
operationId: "scripts_run",
|
|
257
265
|
summary: "Run a reusable or inline script",
|
|
258
266
|
description:
|