@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
package/src/http/tasks.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
} from "../be/db";
|
|
38
38
|
import { modelProfileRefError, taskProfileError } from "../be/db/model-profiles";
|
|
39
39
|
import { explicitModelErrorForAgent } from "../be/model-validation";
|
|
40
|
+
import { hasSessionAccess, type SessionAccess } from "../be/session-shares";
|
|
40
41
|
import {
|
|
41
42
|
getTaskSteeringFields,
|
|
42
43
|
markSteeringUndeliverable,
|
|
@@ -80,7 +81,15 @@ import { getRequestAuth } from "../utils/request-auth-context";
|
|
|
80
81
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
81
82
|
import { rejectGuest } from "./request-principal";
|
|
82
83
|
import { buildTaskRequestedBy, TaskRequestedBySchema } from "./requested-by";
|
|
83
|
-
import { route } from "./route-def";
|
|
84
|
+
import { bodyTask, route } from "./route-def";
|
|
85
|
+
import {
|
|
86
|
+
canReadSession,
|
|
87
|
+
readableByFor,
|
|
88
|
+
resolveSessionActor,
|
|
89
|
+
SESSION_READ_DENIED,
|
|
90
|
+
SESSION_WRITE_DENIED,
|
|
91
|
+
sessionAccessFor,
|
|
92
|
+
} from "./session-access";
|
|
84
93
|
import { jsonError, parseBody } from "./utils";
|
|
85
94
|
|
|
86
95
|
// ─── Response Schemas ────────────────────────────────────────────────────────
|
|
@@ -207,7 +216,7 @@ const listTasks = route({
|
|
|
207
216
|
pattern: ["api", "tasks"],
|
|
208
217
|
summary: "List tasks with filters",
|
|
209
218
|
description:
|
|
210
|
-
"Returns tasks with the full `task` text replaced by a bounded `taskPreview` and completion/integration blobs dropped by default — list views only need the preview. Pass `fields=full` to restore the full `AgentTask`, or `fields=timeline` for the narrow shape the dashboard timeline draws. Fetch a single task in full via `GET /api/tasks/{id}`. `total` (the filtered row count, ignoring limit/offset) is computed only with `includeTotal=true`.",
|
|
219
|
+
"Returns tasks with the full `task` text replaced by a bounded `taskPreview` and completion/integration blobs dropped by default — list views only need the preview. Pass `fields=full` to restore the full `AgentTask`, or `fields=timeline` for the narrow shape the dashboard timeline draws. Every shape gives a user caller only tasks of sessions they can read. Fetch a single task in full via `GET /api/tasks/{id}`. `total` (the filtered row count, ignoring limit/offset) is computed only with `includeTotal=true`.",
|
|
211
220
|
tags: ["Tasks"],
|
|
212
221
|
query: z.object({
|
|
213
222
|
/** Single status, or comma-separated list (e.g. "failed,cancelled"). */
|
|
@@ -263,6 +272,7 @@ const createTask = route({
|
|
|
263
272
|
method: "post",
|
|
264
273
|
path: "/api/tasks",
|
|
265
274
|
pattern: ["api", "tasks"],
|
|
275
|
+
session: bodyTask("parentTaskId"),
|
|
266
276
|
summary: "Create a new task",
|
|
267
277
|
tags: ["Tasks"],
|
|
268
278
|
body: z
|
|
@@ -340,6 +350,7 @@ const promoteDraftTaskRoute = route({
|
|
|
340
350
|
method: "post",
|
|
341
351
|
path: "/api/tasks/{id}/promote-draft",
|
|
342
352
|
pattern: ["api", "tasks", null, "promote-draft"],
|
|
353
|
+
session: { taskParam: "id" },
|
|
343
354
|
summary: "Promote a draft task out of the pre-dispatch draft state",
|
|
344
355
|
description:
|
|
345
356
|
"Transitions a `draft` task (#1240 — created with attachments still uploading) to its normal dispatch-eligible status: `offered` if it was offered to an agent, `pending` if it has an owning agent (the common case — UI-composer tasks default to Lead), otherwise `unassigned`. Called by the UI composer once its attachment upload batch settles, whether every file uploaded, some failed, or all failed — a draft must never be stranded by an upload error. Idempotent: calling it on a task that already left `draft` returns the current task unchanged rather than erroring, so a retried request is safe.",
|
|
@@ -357,6 +368,7 @@ const updateSession = route({
|
|
|
357
368
|
method: "put",
|
|
358
369
|
path: "/api/tasks/{id}/session",
|
|
359
370
|
pattern: ["api", "tasks", null, "session"],
|
|
371
|
+
session: { taskParam: "id" },
|
|
360
372
|
summary: "Update provider session ID and harness metadata for a task",
|
|
361
373
|
tags: ["Tasks"],
|
|
362
374
|
params: z.object({ id: z.string() }),
|
|
@@ -392,6 +404,7 @@ const cancelTaskRoute = route({
|
|
|
392
404
|
method: "post",
|
|
393
405
|
path: "/api/tasks/{id}/cancel",
|
|
394
406
|
pattern: ["api", "tasks", null, "cancel"],
|
|
407
|
+
session: { taskParam: "id" },
|
|
395
408
|
summary: "Cancel a pending or in-progress task",
|
|
396
409
|
tags: ["Tasks"],
|
|
397
410
|
params: z.object({ id: z.string() }),
|
|
@@ -407,6 +420,7 @@ const steerTaskRoute = route({
|
|
|
407
420
|
method: "post",
|
|
408
421
|
path: "/api/tasks/{id}/steer",
|
|
409
422
|
pattern: ["api", "tasks", null, "steer"],
|
|
423
|
+
session: { taskParam: "id" },
|
|
410
424
|
summary: "Deliver a steering message to a running task",
|
|
411
425
|
tags: ["Tasks"],
|
|
412
426
|
params: z.object({ id: z.string() }),
|
|
@@ -439,6 +453,7 @@ const getTaskSteeringMessagesRoute = route({
|
|
|
439
453
|
method: "get",
|
|
440
454
|
path: "/api/tasks/{id}/steering-messages",
|
|
441
455
|
pattern: ["api", "tasks", null, "steering-messages"],
|
|
456
|
+
session: { taskParam: "id" },
|
|
442
457
|
summary: "List steering messages for a task",
|
|
443
458
|
tags: ["Tasks"],
|
|
444
459
|
params: z.object({ id: z.string() }),
|
|
@@ -467,6 +482,7 @@ const markSteeringDeliveredRoute = route({
|
|
|
467
482
|
method: "post",
|
|
468
483
|
path: "/api/steering-messages/{id}/delivered",
|
|
469
484
|
pattern: ["api", "steering-messages", null, "delivered"],
|
|
485
|
+
session: { steeringParam: "id" },
|
|
470
486
|
summary: "Mark a steering message delivered",
|
|
471
487
|
tags: ["Tasks"],
|
|
472
488
|
params: z.object({ id: z.string() }),
|
|
@@ -492,6 +508,7 @@ const markSteeringHandledRoute = route({
|
|
|
492
508
|
method: "post",
|
|
493
509
|
path: "/api/steering-messages/{id}/handled",
|
|
494
510
|
pattern: ["api", "steering-messages", null, "handled"],
|
|
511
|
+
session: { steeringParam: "id" },
|
|
495
512
|
summary: "Mark a steering message handled",
|
|
496
513
|
description:
|
|
497
514
|
"Optionally accepts a JSON body `{ note?: string }` — a short acceptance note describing how the steering was incorporated, persisted as `handledNote`.",
|
|
@@ -517,6 +534,7 @@ const markSteeringUndeliverableRoute = route({
|
|
|
517
534
|
method: "post",
|
|
518
535
|
path: "/api/steering-messages/{id}/undeliverable",
|
|
519
536
|
pattern: ["api", "steering-messages", null, "undeliverable"],
|
|
537
|
+
session: { steeringParam: "id" },
|
|
520
538
|
summary: "Promote an undeliverable steering message",
|
|
521
539
|
tags: ["Tasks"],
|
|
522
540
|
params: z.object({ id: z.string() }),
|
|
@@ -542,6 +560,7 @@ const getTask = route({
|
|
|
542
560
|
method: "get",
|
|
543
561
|
path: "/api/tasks/{id}",
|
|
544
562
|
pattern: ["api", "tasks", null],
|
|
563
|
+
session: { taskParam: "id" },
|
|
545
564
|
summary: "Get task details with logs and attachments",
|
|
546
565
|
description:
|
|
547
566
|
"Returns the full `AgentTask` row decorated with `logs` (capped by `logsLimit`) and `attachments` (pointer-based artifacts stored on the task, ordered by `created_at`).",
|
|
@@ -553,6 +572,7 @@ const getTask = route({
|
|
|
553
572
|
}),
|
|
554
573
|
responses: {
|
|
555
574
|
200: { description: "Task with logs and attachments", schema: GetTaskResponseSchema },
|
|
575
|
+
403: { description: "A user caller cannot read the task's session" },
|
|
556
576
|
404: { description: "Task not found" },
|
|
557
577
|
},
|
|
558
578
|
});
|
|
@@ -561,6 +581,7 @@ const updateTaskProgressRoute = route({
|
|
|
561
581
|
method: "post",
|
|
562
582
|
path: "/api/tasks/{id}/progress",
|
|
563
583
|
pattern: ["api", "tasks", null, "progress"],
|
|
584
|
+
session: { taskParam: "id" },
|
|
564
585
|
summary: "Update task progress text",
|
|
565
586
|
tags: ["Tasks"],
|
|
566
587
|
params: z.object({ id: z.string() }),
|
|
@@ -579,6 +600,7 @@ const finishTask = route({
|
|
|
579
600
|
method: "post",
|
|
580
601
|
path: "/api/tasks/{id}/finish",
|
|
581
602
|
pattern: ["api", "tasks", null, "finish"],
|
|
603
|
+
session: { taskParam: "id" },
|
|
582
604
|
summary: "Mark task as completed or failed (runner endpoint)",
|
|
583
605
|
tags: ["Tasks"],
|
|
584
606
|
params: z.object({ id: z.string() }),
|
|
@@ -631,6 +653,7 @@ const pauseTaskRoute = route({
|
|
|
631
653
|
method: "post",
|
|
632
654
|
path: "/api/tasks/{id}/pause",
|
|
633
655
|
pattern: ["api", "tasks", null, "pause"],
|
|
656
|
+
session: { taskParam: "id" },
|
|
634
657
|
summary: "Pause an in-progress task",
|
|
635
658
|
tags: ["Tasks"],
|
|
636
659
|
params: z.object({ id: z.string() }),
|
|
@@ -646,6 +669,7 @@ const resumeTaskRoute = route({
|
|
|
646
669
|
method: "post",
|
|
647
670
|
path: "/api/tasks/{id}/resume",
|
|
648
671
|
pattern: ["api", "tasks", null, "resume"],
|
|
672
|
+
session: { taskParam: "id" },
|
|
649
673
|
summary: "Resume a paused task",
|
|
650
674
|
tags: ["Tasks"],
|
|
651
675
|
params: z.object({ id: z.string() }),
|
|
@@ -661,6 +685,7 @@ const supersedeTaskRoute = route({
|
|
|
661
685
|
method: "post",
|
|
662
686
|
path: "/api/tasks/{id}/supersede",
|
|
663
687
|
pattern: ["api", "tasks", null, "supersede"],
|
|
688
|
+
session: { taskParam: "id" },
|
|
664
689
|
summary: "Supersede an in-progress task (terminate + spawn resume follow-up)",
|
|
665
690
|
description:
|
|
666
691
|
'Marks the original task `superseded` (terminal) and creates a fresh `taskType="resume"` follow-up so a worker can pick up the work in a new provider session. Workflow-step tasks (those with `workflowRunStepId`) are carved out: the original is marked `failed` with reason `superseded_workflow_task` and no follow-up is created — the workflow engine\'s retry/failure policy applies.',
|
|
@@ -683,6 +708,7 @@ const updateTaskVcsRoute = route({
|
|
|
683
708
|
method: "patch",
|
|
684
709
|
path: "/api/tasks/{id}/vcs",
|
|
685
710
|
pattern: ["api", "tasks", null, "vcs"],
|
|
711
|
+
session: { taskParam: "id" },
|
|
686
712
|
summary: "Update VCS (PR/MR) info for a task",
|
|
687
713
|
tags: ["Tasks"],
|
|
688
714
|
params: z.object({ id: z.string() }),
|
|
@@ -704,6 +730,7 @@ const updateTaskTitleRoute = route({
|
|
|
704
730
|
method: "patch",
|
|
705
731
|
path: "/api/tasks/{id}/title",
|
|
706
732
|
pattern: ["api", "tasks", null, "title"],
|
|
733
|
+
session: { taskParam: "id" },
|
|
707
734
|
summary: "Set or clear a task's display title (session rename)",
|
|
708
735
|
description:
|
|
709
736
|
"Sets a human-facing display title override on a task. The sessions UI only reads this from root tasks (session list items), but titles on child tasks are harmless. Pass `title: null` (or an empty string) to clear the override and fall back to the task prompt.",
|
|
@@ -722,22 +749,37 @@ const updateTaskTitleRoute = route({
|
|
|
722
749
|
},
|
|
723
750
|
});
|
|
724
751
|
|
|
752
|
+
/** True unless the request acts for a user who lacks write access to the task's session. */
|
|
753
|
+
async function hasSessionWrite(
|
|
754
|
+
req: IncomingMessage,
|
|
755
|
+
myAgentId: string | undefined,
|
|
756
|
+
taskId: string,
|
|
757
|
+
): Promise<boolean> {
|
|
758
|
+
const access = await sessionAccessFor(await resolveSessionActor(req, myAgentId), taskId);
|
|
759
|
+
return hasSessionAccess(access, "write");
|
|
760
|
+
}
|
|
761
|
+
|
|
725
762
|
async function canSteerTask(
|
|
726
763
|
req: IncomingMessage,
|
|
727
764
|
myAgentId: string | undefined,
|
|
728
765
|
task: AgentTask,
|
|
766
|
+
sessionAccess: SessionAccess = "none",
|
|
729
767
|
): Promise<boolean> {
|
|
768
|
+
const auth = getRequestAuth(req);
|
|
730
769
|
const resource: RbacResource = {
|
|
731
770
|
kind: "task",
|
|
732
771
|
taskId: task.id,
|
|
733
|
-
|
|
772
|
+
// A user with a write share steers as if the session were their own; the
|
|
773
|
+
// verb gate (and any role grant) still applies. Only an actual share
|
|
774
|
+
// counts: `owner` from an ownerless root must not widen steering.
|
|
775
|
+
requestedByUserId:
|
|
776
|
+
auth?.kind === "user" && sessionAccess === "write" ? auth.userId : task.requestedByUserId,
|
|
734
777
|
creatorAgentId: task.creatorAgentId,
|
|
735
778
|
agentId: task.agentId,
|
|
736
779
|
};
|
|
737
780
|
let principal: RbacPrincipal;
|
|
738
781
|
let verb: "task.steer.any" | "task.steer.own";
|
|
739
782
|
|
|
740
|
-
const auth = getRequestAuth(req);
|
|
741
783
|
if (auth?.kind === "operator") {
|
|
742
784
|
principal = { kind: "operator" };
|
|
743
785
|
verb = "task.steer.own";
|
|
@@ -906,6 +948,9 @@ export async function handleTasks(
|
|
|
906
948
|
if (validated.length > 0) source = validated;
|
|
907
949
|
}
|
|
908
950
|
|
|
951
|
+
// Every shape carries task text, so a user sees only sessions they can
|
|
952
|
+
// read, and `total` counts the same rows.
|
|
953
|
+
const actor = await resolveSessionActor(req, myAgentId);
|
|
909
954
|
const filters = {
|
|
910
955
|
status,
|
|
911
956
|
agentId: parsed.query.agentId || undefined,
|
|
@@ -925,6 +970,7 @@ export async function handleTasks(
|
|
|
925
970
|
orderBy: parsed.query.orderBy,
|
|
926
971
|
limit: parsed.query.limit,
|
|
927
972
|
offset: parsed.query.offset,
|
|
973
|
+
readableBy: actor.kind === "user" ? { userId: actor.userId } : undefined,
|
|
928
974
|
};
|
|
929
975
|
// List responses default to slim (full `task` text → bounded `taskPreview`,
|
|
930
976
|
// heavy blobs dropped); `?fields=full` restores the full `AgentTask`.
|
|
@@ -1013,6 +1059,11 @@ export async function handleTasks(
|
|
|
1013
1059
|
jsonError(res, `You don't have context for parent task ${parentTask.id}.`, 403);
|
|
1014
1060
|
return true;
|
|
1015
1061
|
}
|
|
1062
|
+
// A follow-up in someone else's session needs a write share.
|
|
1063
|
+
if (parentTask && !(await hasSessionWrite(req, myAgentId, parentTask.id))) {
|
|
1064
|
+
jsonError(res, SESSION_WRITE_DENIED, 403);
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1016
1067
|
// A chat-sdk contextKey binds the task to a chat thread that its assignee and creator
|
|
1017
1068
|
// may then read and post to. An agent may only name a thread other than the parent's
|
|
1018
1069
|
// with the lead's chat post permission.
|
|
@@ -1285,7 +1336,13 @@ export async function handleTasks(
|
|
|
1285
1336
|
jsonError(res, "Task not found", 404);
|
|
1286
1337
|
return true;
|
|
1287
1338
|
}
|
|
1288
|
-
|
|
1339
|
+
const sessionActor = await resolveSessionActor(req, myAgentId);
|
|
1340
|
+
const steerAccess = await sessionAccessFor(sessionActor, task.id);
|
|
1341
|
+
if (!hasSessionAccess(steerAccess, "write")) {
|
|
1342
|
+
jsonError(res, SESSION_WRITE_DENIED, 403);
|
|
1343
|
+
return true;
|
|
1344
|
+
}
|
|
1345
|
+
if (!(await canSteerTask(req, myAgentId, task, steerAccess))) {
|
|
1289
1346
|
jsonError(res, "Forbidden: caller cannot steer this task", 403);
|
|
1290
1347
|
return true;
|
|
1291
1348
|
}
|
|
@@ -1338,6 +1395,10 @@ export async function handleTasks(
|
|
|
1338
1395
|
jsonError(res, "Task not found", 404);
|
|
1339
1396
|
return true;
|
|
1340
1397
|
}
|
|
1398
|
+
if (!(await canReadSession(req, myAgentId, parsed.params.id))) {
|
|
1399
|
+
jsonError(res, SESSION_READ_DENIED, 403);
|
|
1400
|
+
return true;
|
|
1401
|
+
}
|
|
1341
1402
|
getTaskSteeringMessagesRoute.respond(res, 200, {
|
|
1342
1403
|
messages: await getSteeringMessagesForTask(parsed.params.id),
|
|
1343
1404
|
});
|
|
@@ -1357,7 +1418,12 @@ export async function handleTasks(
|
|
|
1357
1418
|
return true;
|
|
1358
1419
|
}
|
|
1359
1420
|
|
|
1421
|
+
// A user token naming an agent in X-Agent-ID still reads only its sessions.
|
|
1360
1422
|
if (parsed.query.taskId) {
|
|
1423
|
+
if (!(await canReadSession(req, myAgentId, parsed.query.taskId))) {
|
|
1424
|
+
jsonError(res, SESSION_READ_DENIED, 403);
|
|
1425
|
+
return true;
|
|
1426
|
+
}
|
|
1361
1427
|
const task = await getTaskById(parsed.query.taskId);
|
|
1362
1428
|
const messages =
|
|
1363
1429
|
task?.agentId === myAgentId ? await getPendingSteeringForTask(parsed.query.taskId) : [];
|
|
@@ -1365,8 +1431,9 @@ export async function handleTasks(
|
|
|
1365
1431
|
return true;
|
|
1366
1432
|
}
|
|
1367
1433
|
|
|
1434
|
+
const actor = await resolveSessionActor(req, myAgentId);
|
|
1368
1435
|
getPendingSteeringMessagesRoute.respond(res, 200, {
|
|
1369
|
-
messages: await getPendingSteeringForAgent(myAgentId),
|
|
1436
|
+
messages: await getPendingSteeringForAgent(myAgentId, readableByFor(actor)),
|
|
1370
1437
|
});
|
|
1371
1438
|
return true;
|
|
1372
1439
|
}
|
|
@@ -1529,6 +1596,11 @@ export async function handleTasks(
|
|
|
1529
1596
|
jsonError(res, "Task not found", 404);
|
|
1530
1597
|
return true;
|
|
1531
1598
|
}
|
|
1599
|
+
const readAccess = await sessionAccessFor(await resolveSessionActor(req, myAgentId), task.id);
|
|
1600
|
+
if (!hasSessionAccess(readAccess, "read")) {
|
|
1601
|
+
jsonError(res, "You don't have access to this session. Ask the owner to share it.", 403);
|
|
1602
|
+
return true;
|
|
1603
|
+
}
|
|
1532
1604
|
|
|
1533
1605
|
const logs = await getLogsByTaskId(parsed.params.id, parsed.query.logsLimit ?? 200);
|
|
1534
1606
|
const attachments = await getTaskAttachments(parsed.params.id);
|
|
@@ -1758,7 +1830,11 @@ export async function handleTasks(
|
|
|
1758
1830
|
jsonError(res, "Missing X-Agent-ID header", 400);
|
|
1759
1831
|
return true;
|
|
1760
1832
|
}
|
|
1761
|
-
|
|
1833
|
+
// A user token naming an agent in X-Agent-ID still reads only its sessions.
|
|
1834
|
+
const pausedTasks = await getPausedTasksForAgent(
|
|
1835
|
+
myAgentId,
|
|
1836
|
+
readableByFor(await resolveSessionActor(req, myAgentId)),
|
|
1837
|
+
);
|
|
1762
1838
|
const tasks = await Promise.all(
|
|
1763
1839
|
pausedTasks.map(async (task) => {
|
|
1764
1840
|
const requestedBy = await buildTaskRequestedBy(task);
|
package/src/http/used-by.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getSwarmConfigs, upsertSwarmConfig } from "../be/db";
|
|
4
4
|
import { isTelemetryEnabled, trackRelayed } from "../telemetry";
|
|
5
|
+
import { orgIdentityStatus } from "../utils/org-identity";
|
|
5
6
|
import { normalizeOrgWebsite, TELEMETRY_PROXY_ORIGIN } from "../utils/org-website";
|
|
6
7
|
import { ensureConfigAdmin } from "./config";
|
|
7
8
|
import { scheduleIntegrationsReload } from "./core";
|
|
@@ -46,7 +47,8 @@ const usedBy = route({
|
|
|
46
47
|
|
|
47
48
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
/** A global config value, falling back to the process env. */
|
|
51
|
+
export async function globalConfig(key: string): Promise<string | undefined> {
|
|
50
52
|
const stored = (await getSwarmConfigs({ scope: "global", key }))?.[0]?.value?.trim();
|
|
51
53
|
return stored || process.env[key]?.trim() || undefined;
|
|
52
54
|
}
|
|
@@ -79,8 +81,14 @@ export async function handleUsedBy(
|
|
|
79
81
|
if (!(await ensureConfigAdmin(req, res, "config.write.any"))) return true;
|
|
80
82
|
|
|
81
83
|
const { granted } = parsed.body;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
// The shared identity reader. The grant keeps its own rule (any non-empty
|
|
85
|
+
// name), so it does not apply the ranking's placeholder-name check.
|
|
86
|
+
const current = orgIdentityStatus({
|
|
87
|
+
SWARM_ORG_NAME: await globalConfig("SWARM_ORG_NAME"),
|
|
88
|
+
SWARM_ORG_WEBSITE: await globalConfig("SWARM_ORG_WEBSITE"),
|
|
89
|
+
});
|
|
90
|
+
const currentName = current.name ?? undefined;
|
|
91
|
+
const currentWebsite = current.website;
|
|
84
92
|
const grantedName = granted ? undefined : await globalConfig(GRANTED_NAME_KEY);
|
|
85
93
|
const grantedWebsite = granted
|
|
86
94
|
? null
|
|
@@ -8,6 +8,7 @@ const RESERVED_FAMILIES = new Map([
|
|
|
8
8
|
["apps", "namespace is reserved for swarm apps; use the app row endpoints"],
|
|
9
9
|
// step-9: Comb's "Send to swarm" claims (`comb:sent`).
|
|
10
10
|
["comb", "namespace is reserved for Comb; use POST /api/comb/review-batches"],
|
|
11
|
+
["heartbeat-checklist", "namespace is reserved for heartbeat checklist receipts"],
|
|
11
12
|
["worker", "namespace is reserved for worker nudges"],
|
|
12
13
|
// Durable dedupe for SLACK_BLOCK_EXTERNAL_CHANNELS alerts (src/slack/external-channel.ts).
|
|
13
14
|
["slack-external-alert", "namespace is reserved for Slack external channel alerts"],
|
|
@@ -555,12 +555,14 @@ registerTemplate({
|
|
|
555
555
|
header: "",
|
|
556
556
|
defaultBody: `{{prompt}}
|
|
557
557
|
|
|
558
|
-
[App action] app={{app_id}} action={{action_name}} input={{input_json}}
|
|
558
|
+
[App action] app={{app_id}} action={{action_name}} input={{input_json}}
|
|
559
|
+
Caller (server-verified): {{caller_json}}`,
|
|
559
560
|
variables: [
|
|
560
561
|
{ name: "prompt", description: "The task prompt configured on the app action" },
|
|
561
562
|
{ name: "app_id", description: "The app identifier" },
|
|
562
563
|
{ name: "action_name", description: "The invoked action name" },
|
|
563
564
|
{ name: "input_json", description: "The action input serialized as JSON" },
|
|
565
|
+
{ name: "caller_json", description: "The server-verified action caller serialized as JSON" },
|
|
564
566
|
],
|
|
565
567
|
category: "task_lifecycle",
|
|
566
568
|
});
|
|
@@ -44,7 +44,7 @@ import type {
|
|
|
44
44
|
* default; the image installs exactly this version and the per-task settings
|
|
45
45
|
* set `amp.updates.mode: "disabled"`. Bump it deliberately, with a live run.
|
|
46
46
|
*/
|
|
47
|
-
export const AMP_PACKAGE = "@ampcode/cli@0.0.
|
|
47
|
+
export const AMP_PACKAGE = "@ampcode/cli@0.0.1792324855-g9c86a5";
|
|
48
48
|
|
|
49
49
|
/** MCP server name. Amp exposes its tools as `agent_swarm` / `store_progress` (dashes become underscores). */
|
|
50
50
|
const AMP_MCP_SERVER_NAME = "agent-swarm";
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `telemetry_ranking` listing: what an install opted into the public ranking
|
|
3
|
+
* shows on agent-swarm.dev/ranking. Stored stringified in the global
|
|
4
|
+
* `swarm_config` key `telemetry_ranking` and sent as the `listing` property of
|
|
5
|
+
* `ranking.opted_in`.
|
|
6
|
+
*
|
|
7
|
+
* The proxy owns the contract (`src/telemetry-contract/ranking/`, vendored by
|
|
8
|
+
* `bun run sync:telemetry-contract`). This schema and the proxy's Go decoder are
|
|
9
|
+
* both held to its fixtures. Pure: imported by the API and the dashboard.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { RANKING_TAXONOMY } from "../telemetry-contract/ranking-taxonomy.gen";
|
|
13
|
+
import type { OrgIdentityStatus } from "../utils/org-identity";
|
|
14
|
+
import { normalizeOrgWebsite } from "../utils/org-website";
|
|
15
|
+
|
|
16
|
+
export { RANKING_TAXONOMY };
|
|
17
|
+
|
|
18
|
+
export const TELEMETRY_RANKING_KEY = "telemetry_ranking";
|
|
19
|
+
export const PUBLIC_RANKING_ENABLED_KEY = "PUBLIC_RANKING_ENABLED";
|
|
20
|
+
export const TELEMETRY_RANKING_MAX_BYTES = 2048;
|
|
21
|
+
|
|
22
|
+
export const RANKING_CATEGORIES = Object.keys(RANKING_TAXONOMY) as [
|
|
23
|
+
keyof typeof RANKING_TAXONOMY,
|
|
24
|
+
...(keyof typeof RANKING_TAXONOMY)[],
|
|
25
|
+
];
|
|
26
|
+
export const RANKING_CLAIM_PATTERN = /^rk_[0-9a-f]{32}$/;
|
|
27
|
+
|
|
28
|
+
const hostname = z.string().refine((v) => normalizeOrgWebsite(v) === v, "bare hostname");
|
|
29
|
+
const nullableText = (max: number) => z.string().trim().min(1).max(max).nullable();
|
|
30
|
+
|
|
31
|
+
function inCategory(category: string, subcategory: string | null): boolean {
|
|
32
|
+
if (subcategory === null) return true;
|
|
33
|
+
const subs: readonly string[] = RANKING_TAXONOMY[category as keyof typeof RANKING_TAXONOMY];
|
|
34
|
+
return subs?.includes(subcategory) ?? false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const telemetryRankingV1 = z
|
|
38
|
+
.strictObject({
|
|
39
|
+
v: z.literal(1),
|
|
40
|
+
claim: z.string().regex(RANKING_CLAIM_PATTERN),
|
|
41
|
+
name: z.string().trim().min(1).max(60),
|
|
42
|
+
domain: hostname,
|
|
43
|
+
category: z.enum(RANKING_CATEGORIES),
|
|
44
|
+
subcategory: z
|
|
45
|
+
.string()
|
|
46
|
+
.regex(/^[a-z0-9]+(-[a-z0-9]+)*$/)
|
|
47
|
+
.max(40)
|
|
48
|
+
.nullable(),
|
|
49
|
+
description: nullableText(160),
|
|
50
|
+
sourceCodeUrl: z
|
|
51
|
+
.url({ protocol: /^https$/ })
|
|
52
|
+
.max(200)
|
|
53
|
+
.nullable(),
|
|
54
|
+
updatedAt: z.iso.datetime(),
|
|
55
|
+
})
|
|
56
|
+
.refine((l) => inCategory(l.category, l.subcategory), {
|
|
57
|
+
path: ["subcategory"],
|
|
58
|
+
message: "not in this category",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/** Discriminated on `v`; a v2 is added here, never by editing v1. */
|
|
62
|
+
export const telemetryRankingSchema = z.discriminatedUnion("v", [telemetryRankingV1]);
|
|
63
|
+
export type TelemetryRanking = z.infer<typeof telemetryRankingSchema>;
|
|
64
|
+
|
|
65
|
+
export function parseTelemetryRanking(raw: string): TelemetryRanking {
|
|
66
|
+
if (new TextEncoder().encode(raw).length > TELEMETRY_RANKING_MAX_BYTES) {
|
|
67
|
+
throw new Error(`telemetry_ranking exceeds ${TELEMETRY_RANKING_MAX_BYTES} bytes`);
|
|
68
|
+
}
|
|
69
|
+
return telemetryRankingSchema.parse(JSON.parse(raw));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The stored listing, or null when it is missing or no longer valid. */
|
|
73
|
+
export function readTelemetryRanking(raw: string | null | undefined): TelemetryRanking | null {
|
|
74
|
+
if (!raw?.trim()) return null;
|
|
75
|
+
try {
|
|
76
|
+
return parseTelemetryRanking(raw);
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** What the admin picks in the ranking form. Name and domain are never typed here. */
|
|
83
|
+
export const rankingFormSchema = z.object({
|
|
84
|
+
category: z.enum(RANKING_CATEGORIES),
|
|
85
|
+
subcategory: z.string().nullable().optional(),
|
|
86
|
+
description: z.string().nullable().optional(),
|
|
87
|
+
sourceCodeUrl: z.string().nullable().optional(),
|
|
88
|
+
});
|
|
89
|
+
export type RankingForm = z.infer<typeof rankingFormSchema>;
|
|
90
|
+
|
|
91
|
+
/** `rk_` + 32 lowercase hex characters from the platform CSPRNG. */
|
|
92
|
+
export function mintRankingClaim(): string {
|
|
93
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
94
|
+
return `rk_${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const blankToNull = (value: string | null | undefined) => value?.trim() || null;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build and validate the listing. Name and domain are copied from the org
|
|
101
|
+
* identity (`SWARM_ORG_NAME`, `SWARM_ORG_WEBSITE`); throws when the identity is
|
|
102
|
+
* not set or a form field breaks the contract.
|
|
103
|
+
*/
|
|
104
|
+
export function buildListing(
|
|
105
|
+
identity: OrgIdentityStatus,
|
|
106
|
+
form: RankingForm,
|
|
107
|
+
claim: string,
|
|
108
|
+
now: Date = new Date(),
|
|
109
|
+
): TelemetryRanking {
|
|
110
|
+
if (!identity.ok || !identity.name || !identity.website) {
|
|
111
|
+
throw new Error(`Org identity is missing: ${identity.missing.join(", ")}`);
|
|
112
|
+
}
|
|
113
|
+
const listing = {
|
|
114
|
+
v: 1 as const,
|
|
115
|
+
claim,
|
|
116
|
+
name: identity.name,
|
|
117
|
+
domain: identity.website,
|
|
118
|
+
category: form.category,
|
|
119
|
+
subcategory: blankToNull(form.subcategory),
|
|
120
|
+
description: blankToNull(form.description),
|
|
121
|
+
sourceCodeUrl: blankToNull(form.sourceCodeUrl),
|
|
122
|
+
updatedAt: now.toISOString(),
|
|
123
|
+
};
|
|
124
|
+
return parseTelemetryRanking(JSON.stringify(listing));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The `community/ranking/<domain>.json` file that approves this install. */
|
|
128
|
+
export function rankingEntryFile(listing: Pick<TelemetryRanking, "domain" | "claim">): string {
|
|
129
|
+
return `${JSON.stringify({ $schema: "./schema.json", domain: listing.domain, claim: listing.claim }, null, 2)}\n`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const RANKING_REPO = "desplega-ai/agent-swarm";
|
|
133
|
+
|
|
134
|
+
/** GitHub's "new file" page, prefilled with the entry, as a PR from the admin's fork. */
|
|
135
|
+
export function rankingPrUrl(listing: Pick<TelemetryRanking, "domain" | "claim">): string {
|
|
136
|
+
const params = new URLSearchParams({
|
|
137
|
+
filename: `community/ranking/${listing.domain}.json`,
|
|
138
|
+
value: rankingEntryFile(listing),
|
|
139
|
+
message: `chore(ranking): add ${listing.domain}`,
|
|
140
|
+
});
|
|
141
|
+
return `https://github.com/${RANKING_REPO}/new/main?${params.toString()}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** GitHub's delete page for an entry, to leave the ranking for good. */
|
|
145
|
+
export function rankingLeaveUrl(domain: string): string {
|
|
146
|
+
return `https://github.com/${RANKING_REPO}/delete/main/community/ranking/${domain}.json`;
|
|
147
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker side of the public ranking: the `ranking.usage` properties for one
|
|
3
|
+
* session. Pure, so the runner and its test share the gate.
|
|
4
|
+
*/
|
|
5
|
+
import { isEnvFlagEnabled } from "../utils/env-flag";
|
|
6
|
+
import { orgIdentityStatus } from "../utils/org-identity";
|
|
7
|
+
|
|
8
|
+
export interface SessionTokens {
|
|
9
|
+
inputTokens?: number | null;
|
|
10
|
+
outputTokens?: number | null;
|
|
11
|
+
cacheReadTokens?: number | null;
|
|
12
|
+
cacheWriteTokens?: number | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RankingUsageProps {
|
|
16
|
+
/** The same model and provider `session.cost` reports for the session. */
|
|
17
|
+
model: string;
|
|
18
|
+
provider: string;
|
|
19
|
+
totalTokens: number;
|
|
20
|
+
inputTokens: number;
|
|
21
|
+
outputTokens: number;
|
|
22
|
+
cacheReadTokens?: number;
|
|
23
|
+
cacheWriteTokens?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const tokens = (value: number | null | undefined) =>
|
|
27
|
+
typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Null unless telemetry is on, `PUBLIC_RANKING_ENABLED` is set and both org
|
|
31
|
+
* identity envs are set. Carries token counts plus the model and provider
|
|
32
|
+
* (for the per-model chart), never the task, agent or cost.
|
|
33
|
+
*/
|
|
34
|
+
export function rankingUsageProps(
|
|
35
|
+
cost: SessionTokens,
|
|
36
|
+
session: { model: string; provider: string },
|
|
37
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
38
|
+
): RankingUsageProps | null {
|
|
39
|
+
if (!isEnvFlagEnabled("ANONYMIZED_TELEMETRY", true, env)) return null;
|
|
40
|
+
if (!isEnvFlagEnabled("PUBLIC_RANKING_ENABLED", false, env)) return null;
|
|
41
|
+
const identity = { SWARM_ORG_NAME: env.SWARM_ORG_NAME, SWARM_ORG_WEBSITE: env.SWARM_ORG_WEBSITE };
|
|
42
|
+
if (!orgIdentityStatus(identity).ok) return null;
|
|
43
|
+
const input = tokens(cost.inputTokens);
|
|
44
|
+
const output = tokens(cost.outputTokens);
|
|
45
|
+
const cacheRead = tokens(cost.cacheReadTokens);
|
|
46
|
+
const cacheWrite = tokens(cost.cacheWriteTokens);
|
|
47
|
+
return {
|
|
48
|
+
model: session.model,
|
|
49
|
+
provider: session.provider,
|
|
50
|
+
// The unified sum, as computeContextUsedUnified counts it.
|
|
51
|
+
totalTokens: input + cacheRead + cacheWrite + output,
|
|
52
|
+
inputTokens: input,
|
|
53
|
+
outputTokens: output,
|
|
54
|
+
...(cost.cacheReadTokens != null ? { cacheReadTokens: cacheRead } : {}),
|
|
55
|
+
...(cost.cacheWriteTokens != null ? { cacheWriteTokens: cacheWrite } : {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
package/src/rbac/admission.ts
CHANGED
|
@@ -8,7 +8,20 @@ export type AdmissionGrant = {
|
|
|
8
8
|
};
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* `alsoAdmits` names verbs that admit the route as well as `permission`, for a
|
|
13
|
+
* route that serves two features (each handler check still names its own verb).
|
|
14
|
+
*/
|
|
15
|
+
export type AdmissionRbac =
|
|
16
|
+
| { permission: PermissionVerb; alsoAdmits?: readonly PermissionVerb[] }
|
|
17
|
+
| { ungated: string };
|
|
18
|
+
|
|
19
|
+
function grantedVerb(
|
|
20
|
+
rbac: { permission: PermissionVerb; alsoAdmits?: readonly PermissionVerb[] },
|
|
21
|
+
grant: AdmissionGrant,
|
|
22
|
+
): PermissionVerb | undefined {
|
|
23
|
+
return [rbac.permission, ...(rbac.alsoAdmits ?? [])].find((verb) => grant.verbs.has(verb));
|
|
24
|
+
}
|
|
12
25
|
|
|
13
26
|
export type AdmissionDecision =
|
|
14
27
|
| { allow: true; verb?: PermissionVerb }
|
|
@@ -29,10 +42,11 @@ export function decideAdmission(input: {
|
|
|
29
42
|
}
|
|
30
43
|
|
|
31
44
|
if (input.rbac && "permission" in input.rbac) {
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
return { allow: true, verb };
|
|
45
|
+
const granted = grantedVerb(input.rbac, input.grant);
|
|
46
|
+
if (granted) {
|
|
47
|
+
return { allow: true, verb: granted };
|
|
35
48
|
}
|
|
49
|
+
const verb = input.rbac.permission;
|
|
36
50
|
return { allow: false, reason: `admission: missing permission '${verb}'`, verb };
|
|
37
51
|
}
|
|
38
52
|
|
|
@@ -54,10 +68,11 @@ export function decideToolAdmission(input: {
|
|
|
54
68
|
}
|
|
55
69
|
|
|
56
70
|
if (input.rbac && "permission" in input.rbac) {
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
59
|
-
return { allow: true, verb };
|
|
71
|
+
const granted = grantedVerb(input.rbac, input.grant);
|
|
72
|
+
if (granted) {
|
|
73
|
+
return { allow: true, verb: granted };
|
|
60
74
|
}
|
|
75
|
+
const verb = input.rbac.permission;
|
|
61
76
|
return { allow: false, reason: `admission: missing permission '${verb}'`, verb };
|
|
62
77
|
}
|
|
63
78
|
|
|
@@ -285,6 +285,8 @@ export const LEGACY_POLICY = {
|
|
|
285
285
|
"task.action.own": requesterOwnsTask,
|
|
286
286
|
"task.fs.mutate": taskFsMutate,
|
|
287
287
|
"task.parent.attach": taskFsMutate,
|
|
288
|
+
"session.share": operatorOrUser,
|
|
289
|
+
"session.presence": operatorOrUser,
|
|
288
290
|
"task.progress.write": taskProgressWrite,
|
|
289
291
|
"favorite.write.own": anyAuthenticated,
|
|
290
292
|
"memory.learning.inject": leadOnly,
|
package/src/rbac/permissions.ts
CHANGED
|
@@ -89,6 +89,15 @@ export const PERMISSIONS = {
|
|
|
89
89
|
"Write a task's progress, status, or output (its assignee, a lead, a human, or anyone while unassigned).",
|
|
90
90
|
namespace: "task",
|
|
91
91
|
},
|
|
92
|
+
"session.share": {
|
|
93
|
+
description:
|
|
94
|
+
"Change who can view or edit a session the principal owns (the owner check runs in the handler).",
|
|
95
|
+
namespace: "task",
|
|
96
|
+
},
|
|
97
|
+
"session.presence": {
|
|
98
|
+
description: "Join the presence room of a session the principal can view.",
|
|
99
|
+
namespace: "task",
|
|
100
|
+
},
|
|
92
101
|
"favorite.write.own": {
|
|
93
102
|
description: "Set a favorite the caller owns.",
|
|
94
103
|
namespace: "favorite",
|