@desplega.ai/agent-swarm 2.3.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-v85a2bqy.js → acp-adapter-kedstxx6.js} +6 -6
- package/dist/{actions-zgcxfspj.js → actions-vget6r1k.js} +18 -16
- package/dist/{amp-adapter-jw9xfz42.js → amp-adapter-xeb5mktv.js} +2 -2
- package/dist/{app-4e1s4675.js → app-mqap8t8m.js} +3 -3
- package/dist/{assistant-v238n9dk.js → assistant-h0gv97mv.js} +20 -18
- package/dist/{boot-reembed-p666cw5n.js → boot-reembed-qr8a894z.js} +1 -1
- package/dist/{boot-reembed-w6pm64j4.js → boot-reembed-rj85jjhw.js} +1 -1
- package/dist/{boot-scrub-sweep-jcyjagn3.js → boot-scrub-sweep-eyfvn6v9.js} +3 -3
- package/dist/{boot-seeds-gbedmamp.js → boot-seeds-xs9a8dx1.js} +4 -4
- package/dist/{claude-adapter-q7wspzt8.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-wza784fb.js → claude-managed-setup-gkfgks4j.js} +1 -1
- package/dist/{claude-sdk-session-yf400ptr.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-30m5q8mc.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-fhvqt2fp.js → cli-2rwet3r7.js} +1 -1
- package/dist/{cli-azs9bhc5.js → cli-3wjs22x3.js} +2 -2
- package/dist/{cli-dxwh4j32.js → cli-4fhyq711.js} +45 -36
- package/dist/{cli-yv3n83x8.js → cli-5ff2sjyt.js} +1 -1
- package/dist/{cli-6vcyw6v7.js → cli-5s5hx57y.js} +5 -3
- package/dist/{cli-hz64bcnj.js → cli-6ftrrwm1.js} +9 -371
- package/dist/{cli-zes4tsbb.js → cli-8b9wvjps.js} +1 -1
- package/dist/cli-8cxkw1f5.js +382 -0
- package/dist/{cli-4s1kqrqg.js → cli-8rwvksbc.js} +2 -2
- package/dist/{cli-e09hj50q.js → cli-8wkb09q5.js} +11 -11
- package/dist/{cli-yv0vgscc.js → cli-a1esmmee.js} +13 -2
- package/dist/{cli-4hmaheb6.js → cli-avmqhatn.js} +7 -7
- package/dist/{cli-g6p7c2my.js → cli-b80a1100.js} +1 -1
- package/dist/{cli-tr24eeny.js → cli-bfc3k1x5.js} +3 -3
- package/dist/{cli-ka01m5rb.js → cli-bhwa8rzc.js} +13 -11
- 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-a17tzmn0.js → cli-ecshf504.js} +2 -2
- package/dist/{cli-d43xqzbe.js → cli-eqgfs202.js} +1 -1
- package/dist/{cli-ppapy34w.js → cli-f3nx1h8z.js} +2 -2
- package/dist/{cli-ea7chd26.js → cli-ffbrkh1r.js} +74 -35
- package/dist/{cli-xgn4rwyv.js → cli-kk2r80n5.js} +4 -4
- package/dist/{cli-hn3pftj2.js → cli-kz116kvw.js} +3 -3
- package/dist/{cli-1v8w782f.js → cli-mvg2hf8p.js} +4 -4
- package/dist/{cli-5d1m1qca.js → cli-mz1ezzzh.js} +1 -1
- package/dist/{cli-jhmjwqcg.js → cli-nme7wery.js} +6 -6
- package/dist/{cli-65q70v2v.js → cli-nrrg0fvy.js} +18 -4
- package/dist/{cli-8jtrekpy.js → cli-p1m2b6gc.js} +7 -7
- package/dist/{cli-re4wkbzx.js → cli-rg7ntypm.js} +328 -47
- package/dist/{cli-fp4xbw07.js → cli-rsd7ga7v.js} +10 -8
- package/dist/{cli-etvn5ej2.js → cli-sppc0cmw.js} +1 -1
- package/dist/{cli-qrwp40st.js → cli-tg9ksbn8.js} +1 -1
- package/dist/{cli-6p7ddg72.js → cli-tgvpmnwz.js} +2 -2
- package/dist/{cli-q1812jbw.js → cli-tj4j2ts4.js} +6 -6
- package/dist/{cli-kqypty34.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-y3dvz6ge.js → cli-x1p5dhq2.js} +3 -3
- package/dist/{cli-8qsx817b.js → cli-x719j3vs.js} +219 -629
- package/dist/{cli-9e9kjqsj.js → cli-xfe63zx3.js} +1 -1
- package/dist/{cli-wtg6edvd.js → cli-ywyrffff.js} +1 -1
- package/dist/{cli-0rh20bp3.js → cli-zvze9rbm.js} +1 -1
- package/dist/cli.js +14 -14
- package/dist/{codex-adapter-nhjj2veb.js → codex-adapter-pp2gq8gr.js} +3 -3
- package/dist/{codex-hook-x6tm6y5m.js → codex-hook-qwzccd64.js} +1 -1
- package/dist/{codex-session-runner-6hggrjbx.js → codex-session-runner-0vzn09t8.js} +3 -3
- package/dist/{commands-mrw1ph2w.js → commands-psqhkttb.js} +2 -2
- package/dist/{controller-pt1zam5w.js → controller-t20vz2k8.js} +2 -2
- package/dist/{controllers-connect-ymw5541d.js → controllers-connect-gkxmqw9d.js} +24 -20
- package/dist/{cursor-adapter-cf4zh5x0.js → cursor-adapter-3v542qe5.js} +1 -1
- package/dist/{db-hy80219q.js → db-2xbyenjr.js} +2 -2
- package/dist/{db-hhpy6m4x.js → db-nc1rd869.js} +3 -2
- package/dist/{dispatcher-6pdjtkr1.js → dispatcher-ewceb8ts.js} +17 -15
- package/dist/{dsh-adapter-fbx06re2.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-j6vb6ya1.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-1193sprp.js → grok-adapter-vj88xr23.js} +7 -7
- package/dist/handlers-zbs90bny.js +39 -0
- package/dist/{heartbeat-h1w1ktce.js → heartbeat-t2m9r3wa.js} +20 -18
- package/dist/{hook-skrbja18.js → hook-8femx1bg.js} +4 -4
- package/dist/{hook-ejja4bpe.js → hook-xa677f8r.js} +5 -5
- package/dist/{http-mzh41mfj.js → http-xpmmq028.js} +868 -253
- package/dist/{index-vjn3wbzf.js → index-8pwc1jc8.js} +17 -15
- package/dist/{index-epr2gnbw.js → index-cdzyfrsp.js} +1 -1
- package/dist/{index-xmmkyx82.js → index-dg2tmj99.js} +17 -15
- package/dist/index-nxa6me0q.js +41 -0
- package/dist/{index-6rmnn89d.js → index-s7ckdajq.js} +9 -9
- 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-4djvbm2j.js → keepalive-3nr4st3r.js} +3 -3
- package/dist/{lead-61kqp604.js → lead-td8h2sfn.js} +33 -31
- package/dist/{maintenance-f3xhnjsm.js → maintenance-verwpe2c.js} +1 -1
- package/dist/{oauth-refresh-sweep-fpd6x6rw.js → oauth-refresh-sweep-9q2t0y3q.js} +2 -2
- package/dist/{onboard-196tjgyn.js → onboard-aytqedmq.js} +2 -2
- package/dist/{opencode-adapter-eeesgfqk.js → opencode-adapter-f4hryjny.js} +4 -4
- package/dist/{otel-impl-g8fh4c21.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-sdcf5yaz.js → pricing-refresh-5nvme3sm.js} +3 -3
- package/dist/{rbac-roles-bsp5657v.js → rbac-roles-pd8c26m1.js} +5 -3
- package/dist/{rbac-roles-2x4g1wb5.js → rbac-roles-vbpy21mg.js} +2 -2
- package/dist/{render-v2-0pyb39bb.js → render-v2-7nqgvk2q.js} +3 -3
- package/dist/{resume-a86tkj6r.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-ypbzcapd.js → setup-n39szm6b.js} +1 -1
- package/dist/{virtual-modules-wzk6jf3a.js → virtual-modules-wyp2k6z3.js} +2 -2
- package/dist/{work-objects-yjp8de12.js → work-objects-jem2fre0.js} +20 -18
- package/dist/{worker-4ptnn1ge.js → worker-wr6xgfr2.js} +33 -31
- package/openapi.json +844 -110
- package/package.json +1 -1
- package/src/apps/action-caller.ts +6 -0
- package/src/apps/script-types.ts +16 -3
- 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 +92 -33
- package/src/be/events.ts +11 -0
- package/src/be/migrations/230_session_shares.sql +23 -0
- package/src/be/rbac-roles.ts +2 -0
- 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/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/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/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/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/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/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/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 +35 -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/dist/handlers-tb3xgjnm.js +0 -37
- package/dist/index-nyfcdpky.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/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:
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { IncomingMessage } from "node:http";
|
|
2
|
+
import { getSteeringMessageById } from "../be/db";
|
|
3
|
+
import { getActiveSessionById } from "../be/db/active-sessions";
|
|
4
|
+
import { getUserGrant } from "../be/rbac-roles";
|
|
5
|
+
import {
|
|
6
|
+
getSessionRoot,
|
|
7
|
+
hasSessionAccess,
|
|
8
|
+
type SessionAccess,
|
|
9
|
+
sessionAccess,
|
|
10
|
+
sessionAccessForRoot,
|
|
11
|
+
} from "../be/session-shares";
|
|
12
|
+
import { findUserById } from "../be/users";
|
|
13
|
+
import { isRbacEnabled } from "../rbac";
|
|
14
|
+
import { getRequestAuth } from "../utils/request-auth-context";
|
|
15
|
+
import { dashboardUserIdHeader } from "./favorite-owner";
|
|
16
|
+
import { type RouteDef, routePathParams } from "./route-def";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Who a request acts for when it touches a session.
|
|
20
|
+
*
|
|
21
|
+
* - `bypass`: no session check. Agents (an `aseph_` token, or the operator key
|
|
22
|
+
* with `X-Agent-ID`) continue chains through `POST /api/tasks` all the time,
|
|
23
|
+
* and an operator-key call with no picked dashboard user is a script or the
|
|
24
|
+
* CLI. Both keep the access they had before sharing existed.
|
|
25
|
+
* - `user`: an `aswt_` user token, or an operator-key dashboard tab naming an
|
|
26
|
+
* active user in `X-Swarm-User-Id`. On the shared key that identity is
|
|
27
|
+
* honor-system, the same trust the dashboard already gives favorites.
|
|
28
|
+
* `userId: null` is a guest page session, which has no user at all.
|
|
29
|
+
*/
|
|
30
|
+
export type SessionActor = { kind: "bypass" } | { kind: "user"; userId: string | null };
|
|
31
|
+
|
|
32
|
+
const BYPASS: SessionActor = { kind: "bypass" };
|
|
33
|
+
|
|
34
|
+
export async function resolveSessionActor(
|
|
35
|
+
req: IncomingMessage,
|
|
36
|
+
myAgentId: string | undefined,
|
|
37
|
+
): Promise<SessionActor> {
|
|
38
|
+
const auth = getRequestAuth(req);
|
|
39
|
+
if (auth?.kind === "user") return { kind: "user", userId: auth.userId };
|
|
40
|
+
if (auth?.kind === "guest") return { kind: "user", userId: null };
|
|
41
|
+
if (auth?.kind === "operator" && !myAgentId && !auth.page) {
|
|
42
|
+
const picked = dashboardUserIdHeader(req);
|
|
43
|
+
if (!picked) return BYPASS;
|
|
44
|
+
const user = await findUserById(picked);
|
|
45
|
+
return user?.status === "active" ? { kind: "user", userId: user.id } : BYPASS;
|
|
46
|
+
}
|
|
47
|
+
return BYPASS;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The actor's access to the session holding `taskId`; `owner` for a bypass actor. */
|
|
51
|
+
export async function sessionAccessFor(
|
|
52
|
+
actor: SessionActor,
|
|
53
|
+
taskId: string,
|
|
54
|
+
): Promise<SessionAccess> {
|
|
55
|
+
return actor.kind === "bypass" ? "owner" : sessionAccess(actor.userId, taskId);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const SESSION_READ_DENIED =
|
|
59
|
+
"You don't have access to this session. Ask the owner to share it.";
|
|
60
|
+
|
|
61
|
+
/** True unless the request acts for a user who lacks read access to the task's session. */
|
|
62
|
+
export async function canReadSession(
|
|
63
|
+
req: IncomingMessage,
|
|
64
|
+
myAgentId: string | undefined,
|
|
65
|
+
taskId: string,
|
|
66
|
+
): Promise<boolean> {
|
|
67
|
+
const access = await sessionAccessFor(await resolveSessionActor(req, myAgentId), taskId);
|
|
68
|
+
return hasSessionAccess(access, "read");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The SQL list filter (`readableSessionCondition`) for an actor; none for a bypass actor. */
|
|
72
|
+
export function readableByFor(actor: SessionActor): { userId: string | null } | undefined {
|
|
73
|
+
return actor.kind === "user" ? { userId: actor.userId } : undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const SESSION_WRITE_DENIED =
|
|
77
|
+
"You can't write in this session. Ask the owner to share it with edit access.";
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The denial for an actor lacking `needed` access to the session holding
|
|
81
|
+
* `taskId`, or null. An unknown task passes, so the handler still answers 404.
|
|
82
|
+
*/
|
|
83
|
+
async function denialFor(
|
|
84
|
+
actor: SessionActor,
|
|
85
|
+
taskId: string,
|
|
86
|
+
needed: "read" | "write",
|
|
87
|
+
): Promise<string | null> {
|
|
88
|
+
if (actor.kind === "bypass") return null;
|
|
89
|
+
const root = await getSessionRoot(taskId);
|
|
90
|
+
if (!root) return null;
|
|
91
|
+
if (hasSessionAccess(await sessionAccessForRoot(actor.userId, root), needed)) return null;
|
|
92
|
+
return needed === "read" ? SESSION_READ_DENIED : SESSION_WRITE_DENIED;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const SESSION_AGENT_WIDE_DENIED =
|
|
96
|
+
"This changes sessions across a whole agent. Only the agent or the operator can call it.";
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Enforces a route's path session posture (`taskParam`, `steeringParam`,
|
|
100
|
+
* `activeSessionParam`, `agentWide`) before any handler runs: a GET needs read
|
|
101
|
+
* access, every other method write access.
|
|
102
|
+
*/
|
|
103
|
+
export async function pathSessionDenial(
|
|
104
|
+
req: IncomingMessage,
|
|
105
|
+
myAgentId: string | undefined,
|
|
106
|
+
def: RouteDef,
|
|
107
|
+
pathSegments: string[],
|
|
108
|
+
): Promise<string | null> {
|
|
109
|
+
const posture = def.session;
|
|
110
|
+
if (!posture || "bodyTaskIds" in posture || "none" in posture) return null;
|
|
111
|
+
const actor = await resolveSessionActor(req, myAgentId);
|
|
112
|
+
if (actor.kind === "bypass") return null;
|
|
113
|
+
if ("agentWide" in posture) return SESSION_AGENT_WIDE_DENIED;
|
|
114
|
+
const needed = def.method === "get" ? "read" : "write";
|
|
115
|
+
const params = routePathParams(def, pathSegments);
|
|
116
|
+
let taskId: string | undefined;
|
|
117
|
+
if ("taskParam" in posture) {
|
|
118
|
+
taskId = params[posture.taskParam];
|
|
119
|
+
} else if ("steeringParam" in posture) {
|
|
120
|
+
const message = await getSteeringMessageById(params[posture.steeringParam] ?? "");
|
|
121
|
+
taskId = message?.taskId;
|
|
122
|
+
} else {
|
|
123
|
+
const row = await getActiveSessionById(params[posture.activeSessionParam] ?? "");
|
|
124
|
+
// An unknown row passes so the handler answers; a row with no task is the agent's own.
|
|
125
|
+
if (!row) return null;
|
|
126
|
+
if (!row.taskId) return needed === "read" ? SESSION_READ_DENIED : SESSION_WRITE_DENIED;
|
|
127
|
+
taskId = row.taskId;
|
|
128
|
+
}
|
|
129
|
+
if (!taskId) return null;
|
|
130
|
+
return denialFor(actor, taskId, needed);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Enforces a `bodyTaskIds` posture: every task the body names needs write access. */
|
|
134
|
+
export async function bodySessionDenial(
|
|
135
|
+
req: IncomingMessage,
|
|
136
|
+
taskIds: readonly unknown[],
|
|
137
|
+
): Promise<string | null> {
|
|
138
|
+
const ids = [...new Set(taskIds.filter((id): id is string => typeof id === "string" && !!id))];
|
|
139
|
+
if (ids.length === 0) return null;
|
|
140
|
+
const actor = await resolveSessionActor(req, headerAgentId(req));
|
|
141
|
+
for (const id of ids) {
|
|
142
|
+
const denial = await denialFor(actor, id, "write");
|
|
143
|
+
if (denial) return denial;
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function headerAgentId(req: IncomingMessage): string | undefined {
|
|
149
|
+
const header = req.headers["x-agent-id"];
|
|
150
|
+
return Array.isArray(header) ? header[0] : header;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const USER_AGENT_HEADER_DENIED =
|
|
154
|
+
"A user token cannot act as an agent. Drop X-Agent-ID, or call with the agent's own credential.";
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Refuses a user or guest token that claims agent identity: any X-Agent-ID,
|
|
158
|
+
* and the agent MCP transport at all (`/mcp-user` is the user transport).
|
|
159
|
+
* Runs once in `handleCore`, before any handler reads or changes state. A
|
|
160
|
+
* route that sets `auth.userNamesAgent` (the dashboard script playground)
|
|
161
|
+
* admits the pair for a user holding `agent.act.any`, an authorization of the
|
|
162
|
+
* user, never of the named agent.
|
|
163
|
+
*/
|
|
164
|
+
export async function userAgentIdentityDenial(
|
|
165
|
+
req: IncomingMessage,
|
|
166
|
+
def: RouteDef | undefined,
|
|
167
|
+
pathSegments: string[],
|
|
168
|
+
): Promise<string | null> {
|
|
169
|
+
const auth = getRequestAuth(req);
|
|
170
|
+
if (auth?.kind !== "user" && auth?.kind !== "guest") return null;
|
|
171
|
+
if (pathSegments.length === 1 && pathSegments[0] === "mcp") return USER_AGENT_HEADER_DENIED;
|
|
172
|
+
if (req.headers["x-agent-id"] === undefined) return null;
|
|
173
|
+
if (auth.kind === "user" && def?.auth?.userNamesAgent) {
|
|
174
|
+
if (!isRbacEnabled()) return null;
|
|
175
|
+
const grant = await getUserGrant(auth.userId);
|
|
176
|
+
if (grant.grantsAll || grant.verbs.has("agent.act.any")) return null;
|
|
177
|
+
}
|
|
178
|
+
return USER_AGENT_HEADER_DENIED;
|
|
179
|
+
}
|
package/src/http/session-data.ts
CHANGED
|
@@ -24,7 +24,8 @@ import {
|
|
|
24
24
|
SessionCostSchema,
|
|
25
25
|
SessionLogSchema,
|
|
26
26
|
} from "../types";
|
|
27
|
-
import { route } from "./route-def";
|
|
27
|
+
import { bodyTask, route } from "./route-def";
|
|
28
|
+
import { canReadSession, SESSION_READ_DENIED } from "./session-access";
|
|
28
29
|
import { recomputeSessionCost } from "./session-cost-recompute";
|
|
29
30
|
import { cachedUsageReport } from "./usage-cache";
|
|
30
31
|
import { jsonError } from "./utils";
|
|
@@ -127,6 +128,7 @@ const createSessionLogsRoute = route({
|
|
|
127
128
|
method: "post",
|
|
128
129
|
path: "/api/session-logs",
|
|
129
130
|
pattern: ["api", "session-logs"],
|
|
131
|
+
session: bodyTask("taskId"),
|
|
130
132
|
summary: "Store session logs",
|
|
131
133
|
tags: ["Session Data"],
|
|
132
134
|
body: z.object({
|
|
@@ -149,6 +151,7 @@ const getSessionLogsByTask = route({
|
|
|
149
151
|
method: "get",
|
|
150
152
|
path: "/api/tasks/{taskId}/session-logs",
|
|
151
153
|
pattern: ["api", "tasks", null, "session-logs"],
|
|
154
|
+
session: { taskParam: "taskId" },
|
|
152
155
|
summary: "Get session logs for a task",
|
|
153
156
|
tags: ["Session Data"],
|
|
154
157
|
params: z.object({ taskId: z.string() }),
|
|
@@ -172,6 +175,7 @@ const createSessionCostRoute = route({
|
|
|
172
175
|
method: "post",
|
|
173
176
|
path: "/api/session-costs",
|
|
174
177
|
pattern: ["api", "session-costs"],
|
|
178
|
+
session: bodyTask("taskId"),
|
|
175
179
|
summary: "Store session cost record",
|
|
176
180
|
tags: ["Session Data"],
|
|
177
181
|
body: z.object({
|
|
@@ -310,7 +314,7 @@ export async function handleSessionData(
|
|
|
310
314
|
res: ServerResponse,
|
|
311
315
|
pathSegments: string[],
|
|
312
316
|
queryParams: URLSearchParams,
|
|
313
|
-
|
|
317
|
+
myAgentId: string | undefined,
|
|
314
318
|
): Promise<boolean> {
|
|
315
319
|
if (createSessionLogsRoute.match(req.method, pathSegments)) {
|
|
316
320
|
const parsed = await createSessionLogsRoute.parse(req, res, pathSegments, queryParams);
|
|
@@ -353,6 +357,10 @@ export async function handleSessionData(
|
|
|
353
357
|
jsonError(res, "Task not found", 404);
|
|
354
358
|
return true;
|
|
355
359
|
}
|
|
360
|
+
if (!(await canReadSession(req, myAgentId, task.id))) {
|
|
361
|
+
jsonError(res, SESSION_READ_DENIED, 403);
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
356
364
|
const logs = await getSessionLogsByTaskId(parsed.params.taskId, parsed.query?.limit);
|
|
357
365
|
getSessionLogsByTask.respond(res, 200, { logs });
|
|
358
366
|
return true;
|