@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/apps.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import * as z from "zod";
|
|
3
|
+
import type { AppActionCaller } from "../apps/action-caller";
|
|
3
4
|
import {
|
|
4
5
|
type AppDefinition,
|
|
5
6
|
AppNameSchema,
|
|
@@ -660,45 +661,64 @@ const syncAppRoute = route({
|
|
|
660
661
|
});
|
|
661
662
|
|
|
662
663
|
/**
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
* (after writing the 403) when the write is denied.
|
|
664
|
+
* Check app permissions and resolve server-verified identity for action input
|
|
665
|
+
* and row provenance. Return null after a 403 response when access is denied.
|
|
666
666
|
*/
|
|
667
|
-
async function
|
|
667
|
+
async function authorizeAppCaller(
|
|
668
668
|
req: IncomingMessage,
|
|
669
669
|
res: ServerResponse,
|
|
670
670
|
myAgentId: string | undefined,
|
|
671
671
|
verb: "app.manage" | "app.use",
|
|
672
672
|
resource: RbacResource,
|
|
673
|
-
): Promise<string | null> {
|
|
673
|
+
): Promise<{ actor: string; caller: AppActionCaller } | null> {
|
|
674
674
|
const auth = getRequestAuth(req);
|
|
675
675
|
let principal: RbacPrincipal;
|
|
676
676
|
let actor: string;
|
|
677
|
+
let caller: AppActionCaller;
|
|
677
678
|
if (auth?.kind === "operator") {
|
|
678
679
|
principal = { kind: "operator" };
|
|
679
|
-
|
|
680
|
+
// Page sessions never act as an agent, even if a header names one.
|
|
681
|
+
const agent = myAgentId && !auth.page ? await getAgentById(myAgentId) : null;
|
|
682
|
+
actor = agent ? `agent:${agent.id}` : "operator";
|
|
683
|
+
caller = agent
|
|
684
|
+
? { kind: "agent", id: agent.id, name: agent.name }
|
|
685
|
+
: { kind: "operator", id: null };
|
|
680
686
|
} else if (auth?.kind === "user") {
|
|
681
687
|
principal = { kind: "user", userId: auth.userId };
|
|
682
688
|
actor = `user:${auth.userId}`;
|
|
683
|
-
|
|
689
|
+
caller = { kind: "user", id: auth.userId, name: auth.user.name };
|
|
690
|
+
} else if (auth?.kind === "guest" || auth?.kind === "controller") {
|
|
684
691
|
principal = { kind: "guest" };
|
|
685
692
|
actor = "guest";
|
|
693
|
+
caller = { kind: "guest", id: null };
|
|
686
694
|
} else {
|
|
687
695
|
const agent = myAgentId ? await getAgentById(myAgentId) : null;
|
|
688
696
|
principal = { kind: "agent", agentId: myAgentId ?? "", isLead: agent?.isLead ?? false };
|
|
689
697
|
actor = myAgentId ? `agent:${myAgentId}` : "agent";
|
|
698
|
+
caller = { kind: "agent", id: myAgentId ?? null, name: agent?.name };
|
|
690
699
|
}
|
|
700
|
+
if (auth?.page) caller.pageId = auth.page.id;
|
|
691
701
|
const decision = can({
|
|
692
702
|
principal,
|
|
693
703
|
verb,
|
|
694
704
|
resource,
|
|
695
705
|
source: "http",
|
|
696
706
|
});
|
|
697
|
-
if (decision.allow) return actor;
|
|
707
|
+
if (decision.allow) return { actor, caller };
|
|
698
708
|
jsonError(res, decision.reason, 403);
|
|
699
709
|
return null;
|
|
700
710
|
}
|
|
701
711
|
|
|
712
|
+
async function authorizeApp(
|
|
713
|
+
req: IncomingMessage,
|
|
714
|
+
res: ServerResponse,
|
|
715
|
+
myAgentId: string | undefined,
|
|
716
|
+
verb: "app.manage" | "app.use",
|
|
717
|
+
resource: RbacResource,
|
|
718
|
+
): Promise<string | null> {
|
|
719
|
+
return (await authorizeAppCaller(req, res, myAgentId, verb, resource))?.actor ?? null;
|
|
720
|
+
}
|
|
721
|
+
|
|
702
722
|
async function authorizeAppManage(
|
|
703
723
|
req: IncomingMessage,
|
|
704
724
|
res: ServerResponse,
|
|
@@ -1326,8 +1346,12 @@ export async function handleApps(
|
|
|
1326
1346
|
if (enforceContentLengthCap(req, res, MAX_APP_ROW_BODY_BYTES) === BODY_TOO_LARGE) return true;
|
|
1327
1347
|
const parsed = await runActionRoute.parse(req, res, pathSegments, queryParams);
|
|
1328
1348
|
if (!parsed) return true;
|
|
1329
|
-
const
|
|
1330
|
-
|
|
1349
|
+
const identity = await authorizeAppCaller(req, res, myAgentId, "app.use", {
|
|
1350
|
+
kind: "app",
|
|
1351
|
+
appId: parsed.params.id,
|
|
1352
|
+
});
|
|
1353
|
+
if (!identity) return true;
|
|
1354
|
+
const { actor, caller } = identity;
|
|
1331
1355
|
|
|
1332
1356
|
const app = await getApp(parsed.params.id);
|
|
1333
1357
|
if (!app) {
|
|
@@ -1375,7 +1399,7 @@ export async function handleApps(
|
|
|
1375
1399
|
|
|
1376
1400
|
const output = await runSavedScriptAsAgent({
|
|
1377
1401
|
script,
|
|
1378
|
-
input: { ...action.args, ...input, app: { id: app.id } },
|
|
1402
|
+
input: { ...action.args, ...input, app: { id: app.id }, caller },
|
|
1379
1403
|
agentId: runAsAgentId,
|
|
1380
1404
|
});
|
|
1381
1405
|
const ok = output.exitCode === 0 && !output.error && !output.runtimeError;
|
|
@@ -1434,6 +1458,7 @@ export async function handleApps(
|
|
|
1434
1458
|
app_id: app.id,
|
|
1435
1459
|
action_name: parsed.params.name,
|
|
1436
1460
|
input_json: JSON.stringify(input),
|
|
1461
|
+
caller_json: JSON.stringify(caller),
|
|
1437
1462
|
});
|
|
1438
1463
|
const task = await createTaskWithSiblingAwareness(
|
|
1439
1464
|
taskPrompt.text,
|
package/src/http/assets.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { resolveHttpAuditUserId } from "../be/audit-user";
|
|
|
7
7
|
import {
|
|
8
8
|
getDb,
|
|
9
9
|
getDbClient,
|
|
10
|
+
getRootTaskChain,
|
|
10
11
|
getTaskById,
|
|
11
12
|
listAssetSummaries,
|
|
12
13
|
moveAssetKey,
|
|
@@ -120,10 +121,17 @@ function ensureOperator(req: IncomingMessage, res: ServerResponse): boolean {
|
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
const moveAssetBodySchema = z.object({ key: AssetKeySchema });
|
|
124
|
+
const moveGenericAssetBodySchema = moveAssetBodySchema.extend({
|
|
125
|
+
cascade: z
|
|
126
|
+
.boolean()
|
|
127
|
+
.optional()
|
|
128
|
+
.describe("For tasks only, also move every descendant task recursively through parentTaskId."),
|
|
129
|
+
});
|
|
123
130
|
const moveAssetSuccessSchema = z.object({
|
|
124
131
|
entityType: AssetEntityTypeSchema,
|
|
125
132
|
id: z.string(),
|
|
126
133
|
key: AssetKeySchema,
|
|
134
|
+
movedTaskIds: z.array(z.string()).optional(),
|
|
127
135
|
});
|
|
128
136
|
const moveAssetResponses = {
|
|
129
137
|
200: { description: "Asset namespace updated", schema: moveAssetSuccessSchema },
|
|
@@ -168,7 +176,7 @@ const moveAssetRoute = route({
|
|
|
168
176
|
"Updates namespace metadata only. Provider-backed files keep the same provider key, org, and drive; no remote move occurs. Personal keys are labels, not a privacy guarantee.",
|
|
169
177
|
tags: ["Assets"],
|
|
170
178
|
params: z.object({ entityType: AssetEntityTypeSchema, id: z.string().min(1) }),
|
|
171
|
-
body:
|
|
179
|
+
body: moveGenericAssetBodySchema,
|
|
172
180
|
responses: moveAssetResponses,
|
|
173
181
|
rbac: {
|
|
174
182
|
ungated:
|
|
@@ -302,17 +310,29 @@ export async function handleAssets(
|
|
|
302
310
|
) {
|
|
303
311
|
const parsed = await moveAssetRoute.parse(req, res, pathSegments, queryParams);
|
|
304
312
|
if (!parsed) return true;
|
|
313
|
+
if (parsed.body.cascade && parsed.params.entityType !== "task") {
|
|
314
|
+
jsonError(res, "Cascade is only supported for task namespace moves", 400);
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
305
317
|
if (parsed.params.entityType === "file" && !ensureOperator(req, res)) return true;
|
|
318
|
+
let descendantTaskIds: string[] | undefined;
|
|
306
319
|
if (parsed.params.entityType === "task") {
|
|
307
320
|
const task = await getTaskById(parsed.params.id);
|
|
308
321
|
if (!task) {
|
|
309
322
|
jsonError(res, "Asset not found", 404);
|
|
310
323
|
return true;
|
|
311
324
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
325
|
+
// Only the tasks checked here move: a subtask created after this read stays put.
|
|
326
|
+
const tasks = parsed.body.cascade ? await getRootTaskChain(task.id) : [task];
|
|
327
|
+
for (const target of tasks) {
|
|
328
|
+
if (!(await canMutateTaskNamespace(target, myAgentId, req))) {
|
|
329
|
+
jsonError(res, "Not authorized to move this task namespace", 403);
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
315
332
|
}
|
|
333
|
+
descendantTaskIds = tasks
|
|
334
|
+
.filter((target) => target.id !== task.id)
|
|
335
|
+
.map((target) => target.id);
|
|
316
336
|
}
|
|
317
337
|
if (parsed.params.entityType === "app") {
|
|
318
338
|
if (!(await getApp(parsed.params.id))) {
|
|
@@ -344,6 +364,7 @@ export async function handleAssets(
|
|
|
344
364
|
id: parsed.params.id,
|
|
345
365
|
key,
|
|
346
366
|
changedBy: actor ?? undefined,
|
|
367
|
+
descendantTaskIds,
|
|
347
368
|
});
|
|
348
369
|
if (!moved) {
|
|
349
370
|
jsonError(res, "Asset not found", 404);
|
|
@@ -353,6 +374,7 @@ export async function handleAssets(
|
|
|
353
374
|
entityType: parsed.params.entityType,
|
|
354
375
|
id: parsed.params.id,
|
|
355
376
|
key,
|
|
377
|
+
...moved,
|
|
356
378
|
});
|
|
357
379
|
} catch (error) {
|
|
358
380
|
if (error instanceof AssetKeyAuthorizationError) {
|
package/src/http/config.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { SwarmConfigSchema } from "../types";
|
|
|
31
31
|
import { getRequestAuth, type HttpRequestAuth } from "../utils/request-auth-context";
|
|
32
32
|
import { registerVolatileSecret } from "../utils/secret-scrubber";
|
|
33
33
|
import { reloadGlobalConfigsAndIntegrations, scheduleIntegrationsReload } from "./core";
|
|
34
|
+
import { syncRankingAfterConfigChange } from "./ranking";
|
|
34
35
|
import { route } from "./route-def";
|
|
35
36
|
import { duplicateGuardedQueryKey, jsonError } from "./utils";
|
|
36
37
|
|
|
@@ -527,6 +528,7 @@ export async function handleConfig(
|
|
|
527
528
|
if (scope === "global" && key === "SWARM_ORG_WEBSITE") {
|
|
528
529
|
await fillOrgLogoFromWebsite(String(value));
|
|
529
530
|
}
|
|
531
|
+
if (scope === "global") await syncRankingAfterConfigChange(key, String(value));
|
|
530
532
|
if (scope === "global") {
|
|
531
533
|
scheduleIntegrationsReload();
|
|
532
534
|
}
|
|
@@ -561,6 +563,7 @@ export async function handleConfig(
|
|
|
561
563
|
return true;
|
|
562
564
|
}
|
|
563
565
|
if (existing.scope === "global") {
|
|
566
|
+
await syncRankingAfterConfigChange(existing.key, null);
|
|
564
567
|
scheduleIntegrationsReload();
|
|
565
568
|
}
|
|
566
569
|
if (
|
package/src/http/context.ts
CHANGED
|
@@ -43,6 +43,7 @@ const postContext = route({
|
|
|
43
43
|
method: "post",
|
|
44
44
|
path: "/api/tasks/{id}/context",
|
|
45
45
|
pattern: ["api", "tasks", null, "context"],
|
|
46
|
+
session: { taskParam: "id" },
|
|
46
47
|
summary: "Record a context usage snapshot for a task",
|
|
47
48
|
tags: ["Tasks"],
|
|
48
49
|
params: z.object({ id: z.string() }),
|
|
@@ -72,6 +73,7 @@ const getContext = route({
|
|
|
72
73
|
method: "get",
|
|
73
74
|
path: "/api/tasks/{id}/context",
|
|
74
75
|
pattern: ["api", "tasks", null, "context"],
|
|
76
|
+
session: { taskParam: "id" },
|
|
75
77
|
summary: "Get context usage history for a task",
|
|
76
78
|
tags: ["Tasks"],
|
|
77
79
|
params: z.object({ id: z.string() }),
|
package/src/http/core.ts
CHANGED
|
@@ -47,6 +47,14 @@ import { refreshSecretScrubberCache, registerSensitiveKeyName } from "../utils/s
|
|
|
47
47
|
import { resolveHttpRequestAuth } from "./auth";
|
|
48
48
|
import { generateOpenApiSpec, SCALAR_HTML } from "./openapi";
|
|
49
49
|
import { findRoute, isPublicRoute, route, runtimeInstanceHeader } from "./route-def";
|
|
50
|
+
import {
|
|
51
|
+
canReadSession,
|
|
52
|
+
pathSessionDenial,
|
|
53
|
+
readableByFor,
|
|
54
|
+
resolveSessionActor,
|
|
55
|
+
SESSION_READ_DENIED,
|
|
56
|
+
userAgentIdentityDenial,
|
|
57
|
+
} from "./session-access";
|
|
50
58
|
import {
|
|
51
59
|
agentWithCapacity,
|
|
52
60
|
duplicateGuardedQueryKey,
|
|
@@ -503,6 +511,15 @@ export async function handleCore(
|
|
|
503
511
|
setRequestAuth(req, auth);
|
|
504
512
|
}
|
|
505
513
|
|
|
514
|
+
// A user token never gains agent identity from X-Agent-ID, on /mcp, /api/poll
|
|
515
|
+
// or any worker-header route.
|
|
516
|
+
const routeDef = findRoute(req.method, pathSegments);
|
|
517
|
+
const agentIdentityDenial = await userAgentIdentityDenial(req, routeDef, pathSegments);
|
|
518
|
+
if (agentIdentityDenial) {
|
|
519
|
+
jsonError(res, agentIdentityDenial, 403);
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
|
|
506
523
|
if (auth?.kind === "agent") {
|
|
507
524
|
const query = parseQueryParams(req.url || "");
|
|
508
525
|
const duplicate = duplicateGuardedQueryKey(query);
|
|
@@ -565,7 +582,7 @@ export async function handleCore(
|
|
|
565
582
|
if (auth?.kind === "user" && isRbacEnabled()) {
|
|
566
583
|
const grant = await getUserGrant(auth.userId);
|
|
567
584
|
if (!grant.grantsAll) {
|
|
568
|
-
const def =
|
|
585
|
+
const def = routeDef;
|
|
569
586
|
const decision = decideAdmission({
|
|
570
587
|
method: req.method ?? "",
|
|
571
588
|
rbac: def?.rbac,
|
|
@@ -585,6 +602,16 @@ export async function handleCore(
|
|
|
585
602
|
}
|
|
586
603
|
}
|
|
587
604
|
|
|
605
|
+
// Session access for every route keyed by a task or steering message, before
|
|
606
|
+
// its handler reads or changes anything (the route's `session` posture).
|
|
607
|
+
if (routeDef?.session) {
|
|
608
|
+
const sessionDenial = await pathSessionDenial(req, myAgentId, routeDef, pathSegments);
|
|
609
|
+
if (sessionDenial) {
|
|
610
|
+
jsonError(res, sessionDenial, 403);
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
588
615
|
// POST /internal/reload-config — re-read swarm_config into process.env and re-init integrations
|
|
589
616
|
if (req.method === "POST" && req.url === "/internal/reload-config") {
|
|
590
617
|
try {
|
|
@@ -662,7 +689,14 @@ export async function handleCore(
|
|
|
662
689
|
const queryParams = parseQueryParams(req.url || "");
|
|
663
690
|
const taskId = queryParams.get("taskId");
|
|
664
691
|
|
|
692
|
+
// A user token naming an agent in X-Agent-ID still reads only its sessions.
|
|
693
|
+
const sessionActor = await resolveSessionActor(req, myAgentId);
|
|
665
694
|
if (taskId) {
|
|
695
|
+
if (!(await canReadSession(req, myAgentId, taskId))) {
|
|
696
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
697
|
+
res.end(JSON.stringify({ error: SESSION_READ_DENIED }));
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
666
700
|
// Check if specific task is cancelled
|
|
667
701
|
const task = await getTaskById(taskId);
|
|
668
702
|
if (task && task.status === "cancelled") {
|
|
@@ -687,7 +721,10 @@ export async function handleCore(
|
|
|
687
721
|
}
|
|
688
722
|
|
|
689
723
|
// No taskId - return all recently cancelled tasks for this agent
|
|
690
|
-
const cancelledTasks = await getRecentlyCancelledTasksForAgent(
|
|
724
|
+
const cancelledTasks = await getRecentlyCancelledTasksForAgent(
|
|
725
|
+
myAgentId,
|
|
726
|
+
readableByFor(sessionActor),
|
|
727
|
+
);
|
|
691
728
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
692
729
|
res.end(JSON.stringify({ cancelled: cancelledTasks }));
|
|
693
730
|
return true;
|
package/src/http/events.ts
CHANGED
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
EventStatusSchema,
|
|
14
14
|
SwarmEventSchema,
|
|
15
15
|
} from "../types";
|
|
16
|
-
import { route } from "./route-def";
|
|
16
|
+
import { bodyListTask, bodyTask, route } from "./route-def";
|
|
17
|
+
import { readableByFor, resolveSessionActor } from "./session-access";
|
|
17
18
|
import { jsonError } from "./utils";
|
|
18
19
|
|
|
19
20
|
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
@@ -36,6 +37,7 @@ const createEventRoute = route({
|
|
|
36
37
|
method: "post",
|
|
37
38
|
path: "/api/events",
|
|
38
39
|
pattern: ["api", "events"],
|
|
40
|
+
session: bodyTask("taskId"),
|
|
39
41
|
summary: "Store a single event",
|
|
40
42
|
tags: ["Events"],
|
|
41
43
|
body: eventBodySchema,
|
|
@@ -53,6 +55,7 @@ const createEventsBatchRoute = route({
|
|
|
53
55
|
method: "post",
|
|
54
56
|
path: "/api/events/batch",
|
|
55
57
|
pattern: ["api", "events", "batch"],
|
|
58
|
+
session: bodyListTask("events", "taskId"),
|
|
56
59
|
summary: "Store multiple events in a batch",
|
|
57
60
|
tags: ["Events"],
|
|
58
61
|
body: z.object({
|
|
@@ -153,7 +156,7 @@ export async function handleEvents(
|
|
|
153
156
|
res: ServerResponse,
|
|
154
157
|
pathSegments: string[],
|
|
155
158
|
queryParams: URLSearchParams,
|
|
156
|
-
|
|
159
|
+
myAgentId: string | undefined,
|
|
157
160
|
): Promise<boolean> {
|
|
158
161
|
// Match batch BEFORE generic /api/events (POST)
|
|
159
162
|
if (createEventsBatchRoute.match(req.method, pathSegments)) {
|
|
@@ -252,6 +255,7 @@ export async function handleEvents(
|
|
|
252
255
|
limit: parsed.query.limit ?? 100,
|
|
253
256
|
events: eventFilter,
|
|
254
257
|
latestPerDataField: latestPerDataField ? { events: latestEvents, dataFields } : undefined,
|
|
258
|
+
readableBy: readableByFor(await resolveSessionActor(req, myAgentId)),
|
|
255
259
|
});
|
|
256
260
|
getEventsRoute.respond(
|
|
257
261
|
res,
|
|
@@ -16,7 +16,7 @@ export type FavoriteOwner = {
|
|
|
16
16
|
*/
|
|
17
17
|
export const DASHBOARD_USER_HEADER = "x-swarm-user-id";
|
|
18
18
|
|
|
19
|
-
function dashboardUserIdHeader(req: IncomingMessage): string | null {
|
|
19
|
+
export function dashboardUserIdHeader(req: IncomingMessage): string | null {
|
|
20
20
|
const raw = req.headers[DASHBOARD_USER_HEADER];
|
|
21
21
|
const value = (Array.isArray(raw) ? raw[0] : raw)?.trim();
|
|
22
22
|
return value ? value : null;
|
package/src/http/fs.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { getCurrentRequestAuth } from "../utils/request-auth-context";
|
|
|
15
15
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
16
16
|
import { requestPrincipal } from "./request-principal";
|
|
17
17
|
import { route } from "./route-def";
|
|
18
|
+
import { canReadSession, SESSION_READ_DENIED } from "./session-access";
|
|
18
19
|
import { BODY_TOO_LARGE, enforceContentLengthCap, jsonError } from "./utils";
|
|
19
20
|
|
|
20
21
|
// Upload wall-clock past which the provider round-trip is worth a log line.
|
|
@@ -126,6 +127,7 @@ const listTaskFilesRoute = route({
|
|
|
126
127
|
method: "get",
|
|
127
128
|
path: "/api/fs/tasks/{taskId}/files",
|
|
128
129
|
pattern: ["api", "fs", "tasks", null, "files"],
|
|
130
|
+
session: { taskParam: "taskId" },
|
|
129
131
|
summary: "List task file attachments",
|
|
130
132
|
tags: ["FS"],
|
|
131
133
|
params: taskParams,
|
|
@@ -142,6 +144,7 @@ const uploadTaskFileRoute = route({
|
|
|
142
144
|
method: "post",
|
|
143
145
|
path: "/api/fs/tasks/{taskId}/files",
|
|
144
146
|
pattern: ["api", "fs", "tasks", null, "files"],
|
|
147
|
+
session: { taskParam: "taskId" },
|
|
145
148
|
summary: "Upload a binary task file attachment",
|
|
146
149
|
description:
|
|
147
150
|
"Accepts a raw binary request body. Pass the display/path name as the `name` query parameter.",
|
|
@@ -163,6 +166,7 @@ const getTaskFileRoute = route({
|
|
|
163
166
|
method: "get",
|
|
164
167
|
path: "/api/fs/tasks/{taskId}/files/{attachmentId}",
|
|
165
168
|
pattern: ["api", "fs", "tasks", null, "files", null],
|
|
169
|
+
session: { taskParam: "taskId" },
|
|
166
170
|
summary: "Get task file attachment metadata",
|
|
167
171
|
tags: ["FS"],
|
|
168
172
|
params: attachmentParams,
|
|
@@ -176,6 +180,7 @@ const downloadTaskFileRoute = route({
|
|
|
176
180
|
method: "get",
|
|
177
181
|
path: "/api/fs/tasks/{taskId}/files/{attachmentId}/raw",
|
|
178
182
|
pattern: ["api", "fs", "tasks", null, "files", null, "raw"],
|
|
183
|
+
session: { taskParam: "taskId" },
|
|
179
184
|
summary: "Download raw task file bytes",
|
|
180
185
|
description: "Streams raw bytes. File content is not secret-scrubbed.",
|
|
181
186
|
tags: ["FS"],
|
|
@@ -193,6 +198,7 @@ const signedUrlTaskFileRoute = route({
|
|
|
193
198
|
method: "get",
|
|
194
199
|
path: "/api/fs/tasks/{taskId}/files/{attachmentId}/signed-url",
|
|
195
200
|
pattern: ["api", "fs", "tasks", null, "files", null, "signed-url"],
|
|
201
|
+
session: { taskParam: "taskId" },
|
|
196
202
|
summary: "Create a provider signed GET URL for a task file",
|
|
197
203
|
tags: ["FS"],
|
|
198
204
|
params: attachmentParams,
|
|
@@ -208,6 +214,7 @@ const deleteTaskFileRoute = route({
|
|
|
208
214
|
method: "delete",
|
|
209
215
|
path: "/api/fs/tasks/{taskId}/files/{attachmentId}",
|
|
210
216
|
pattern: ["api", "fs", "tasks", null, "files", null],
|
|
217
|
+
session: { taskParam: "taskId" },
|
|
211
218
|
summary: "Delete a task file attachment",
|
|
212
219
|
tags: ["FS"],
|
|
213
220
|
params: attachmentParams,
|
|
@@ -263,7 +270,13 @@ export async function handleFs(
|
|
|
263
270
|
if (downloadTaskFileRoute.match(req.method, pathSegments)) {
|
|
264
271
|
const parsed = await downloadTaskFileRoute.parse(req, res, pathSegments, queryParams);
|
|
265
272
|
if (!parsed) return true;
|
|
266
|
-
const attachment = await findAttachment(
|
|
273
|
+
const attachment = await findAttachment(
|
|
274
|
+
req,
|
|
275
|
+
myAgentId,
|
|
276
|
+
parsed.params.taskId,
|
|
277
|
+
parsed.params.attachmentId,
|
|
278
|
+
res,
|
|
279
|
+
);
|
|
267
280
|
if (!attachment) return true;
|
|
268
281
|
return sendDownload(res, attachment);
|
|
269
282
|
}
|
|
@@ -271,7 +284,13 @@ export async function handleFs(
|
|
|
271
284
|
if (signedUrlTaskFileRoute.match(req.method, pathSegments)) {
|
|
272
285
|
const parsed = await signedUrlTaskFileRoute.parse(req, res, pathSegments, queryParams);
|
|
273
286
|
if (!parsed) return true;
|
|
274
|
-
const attachment = await findAttachment(
|
|
287
|
+
const attachment = await findAttachment(
|
|
288
|
+
req,
|
|
289
|
+
myAgentId,
|
|
290
|
+
parsed.params.taskId,
|
|
291
|
+
parsed.params.attachmentId,
|
|
292
|
+
res,
|
|
293
|
+
);
|
|
275
294
|
if (!attachment) return true;
|
|
276
295
|
return sendSignedUrl(res, attachment, parsed.query.expiresIn);
|
|
277
296
|
}
|
|
@@ -279,7 +298,13 @@ export async function handleFs(
|
|
|
279
298
|
if (getTaskFileRoute.match(req.method, pathSegments)) {
|
|
280
299
|
const parsed = await getTaskFileRoute.parse(req, res, pathSegments, queryParams);
|
|
281
300
|
if (!parsed) return true;
|
|
282
|
-
const attachment = await findAttachment(
|
|
301
|
+
const attachment = await findAttachment(
|
|
302
|
+
req,
|
|
303
|
+
myAgentId,
|
|
304
|
+
parsed.params.taskId,
|
|
305
|
+
parsed.params.attachmentId,
|
|
306
|
+
res,
|
|
307
|
+
);
|
|
283
308
|
if (!attachment) return true;
|
|
284
309
|
getTaskFileRoute.respond(res, 200, attachment);
|
|
285
310
|
return true;
|
|
@@ -313,6 +338,10 @@ export async function handleFs(
|
|
|
313
338
|
const parsed = await listTaskFilesRoute.parse(req, res, pathSegments, queryParams);
|
|
314
339
|
if (!parsed) return true;
|
|
315
340
|
if (!(await getTaskById(parsed.params.taskId))) return notFound(res, "Task not found");
|
|
341
|
+
if (!(await canReadSession(req, myAgentId, parsed.params.taskId))) {
|
|
342
|
+
jsonError(res, SESSION_READ_DENIED, 403);
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
316
345
|
listTaskFilesRoute.respond(res, 200, {
|
|
317
346
|
attachments: await getTaskAttachments(parsed.params.taskId),
|
|
318
347
|
});
|
|
@@ -493,6 +522,8 @@ function backingProviderId(attachment: TaskAttachment): string | null {
|
|
|
493
522
|
}
|
|
494
523
|
|
|
495
524
|
async function findAttachment(
|
|
525
|
+
req: IncomingMessage,
|
|
526
|
+
myAgentId: string | undefined,
|
|
496
527
|
taskId: string,
|
|
497
528
|
attachmentId: string,
|
|
498
529
|
res: ServerResponse,
|
|
@@ -501,6 +532,10 @@ async function findAttachment(
|
|
|
501
532
|
notFound(res, "Task not found");
|
|
502
533
|
return null;
|
|
503
534
|
}
|
|
535
|
+
if (!(await canReadSession(req, myAgentId, taskId))) {
|
|
536
|
+
jsonError(res, SESSION_READ_DENIED, 403);
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
504
539
|
const attachment = (await getTaskAttachments(taskId)).find((item) => item.id === attachmentId);
|
|
505
540
|
if (!attachment) {
|
|
506
541
|
notFound(res, "Attachment not found");
|
package/src/http/inbox-state.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { listInboxState, upsertInboxState } from "../be/db";
|
|
3
|
+
import { getDbClient, listInboxState, upsertInboxState } from "../be/db";
|
|
4
4
|
import { InboxItemStateSchema, InboxItemStatusSchema, InboxItemTypeSchema } from "../types";
|
|
5
5
|
import { route } from "./route-def";
|
|
6
6
|
import { jsonError } from "./utils";
|
|
@@ -29,29 +29,60 @@ const listState = route({
|
|
|
29
29
|
auth: { apiKey: true },
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
+
const inboxStateItemSchema = z.object({
|
|
33
|
+
itemType: InboxItemTypeSchema,
|
|
34
|
+
itemId: z.string().min(1),
|
|
35
|
+
status: InboxItemStatusSchema,
|
|
36
|
+
snoozeUntil: z.string().datetime().optional(),
|
|
37
|
+
readAt: z.string().datetime().optional(),
|
|
38
|
+
lastReadAt: z
|
|
39
|
+
.string()
|
|
40
|
+
.datetime()
|
|
41
|
+
.nullable()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe(
|
|
44
|
+
"Omit to preserve the read cursor, send null to clear it, or send a timestamp to set it, including an earlier time.",
|
|
45
|
+
),
|
|
46
|
+
});
|
|
47
|
+
|
|
32
48
|
const upsertState = route({
|
|
33
49
|
method: "patch",
|
|
34
50
|
path: "/api/inbox-state",
|
|
35
51
|
pattern: ["api", "inbox-state"],
|
|
36
52
|
summary: "Upsert per-user dismiss/snooze/done state for an inbox item",
|
|
37
53
|
tags: ["Inbox State"],
|
|
54
|
+
body: inboxStateItemSchema.extend({ userId: z.string() }),
|
|
55
|
+
responses: {
|
|
56
|
+
200: {
|
|
57
|
+
description: "Upserted inbox state row",
|
|
58
|
+
schema: z.object({ item: InboxItemStateSchema }),
|
|
59
|
+
},
|
|
60
|
+
400: { description: "Validation error" },
|
|
61
|
+
401: { description: "Unauthorized" },
|
|
62
|
+
},
|
|
63
|
+
auth: { apiKey: true },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const bulkUpsertState = route({
|
|
67
|
+
method: "patch",
|
|
68
|
+
path: "/api/inbox-state/bulk",
|
|
69
|
+
pattern: ["api", "inbox-state", "bulk"],
|
|
70
|
+
summary: "Upsert inbox-item state in one transaction",
|
|
71
|
+
tags: ["Inbox State"],
|
|
38
72
|
body: z.object({
|
|
39
73
|
userId: z.string(),
|
|
40
|
-
|
|
41
|
-
itemId: z.string().min(1),
|
|
42
|
-
status: InboxItemStatusSchema,
|
|
43
|
-
snoozeUntil: z.string().datetime().optional(),
|
|
44
|
-
readAt: z.string().datetime().optional(),
|
|
74
|
+
items: z.array(inboxStateItemSchema).min(1).max(200),
|
|
45
75
|
}),
|
|
46
76
|
responses: {
|
|
47
77
|
200: {
|
|
48
|
-
description: "Upserted inbox state
|
|
49
|
-
schema: z.object({
|
|
78
|
+
description: "Upserted inbox state rows in request order",
|
|
79
|
+
schema: z.object({ items: z.array(InboxItemStateSchema) }),
|
|
50
80
|
},
|
|
51
81
|
400: { description: "Validation error" },
|
|
52
82
|
401: { description: "Unauthorized" },
|
|
53
83
|
},
|
|
54
84
|
auth: { apiKey: true },
|
|
85
|
+
rbac: { ungated: "per-user inbox UI state, same posture as PATCH /api/inbox-state" },
|
|
55
86
|
});
|
|
56
87
|
|
|
57
88
|
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
@@ -74,6 +105,24 @@ export async function handleInboxState(
|
|
|
74
105
|
return true;
|
|
75
106
|
}
|
|
76
107
|
|
|
108
|
+
if (bulkUpsertState.match(req.method, pathSegments)) {
|
|
109
|
+
const parsed = await bulkUpsertState.parse(req, res, pathSegments, queryParams);
|
|
110
|
+
if (!parsed) return true;
|
|
111
|
+
try {
|
|
112
|
+
const items = await getDbClient().transaction(async () => {
|
|
113
|
+
const rows = [];
|
|
114
|
+
for (const item of parsed.body.items) {
|
|
115
|
+
rows.push(await upsertInboxState({ ...item, userId: parsed.body.userId }));
|
|
116
|
+
}
|
|
117
|
+
return rows;
|
|
118
|
+
});
|
|
119
|
+
bulkUpsertState.respond(res, 200, { items });
|
|
120
|
+
} catch (err) {
|
|
121
|
+
jsonError(res, err instanceof Error ? err.message : "Failed to upsert inbox state", 500);
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
77
126
|
if (upsertState.match(req.method, pathSegments)) {
|
|
78
127
|
const parsed = await upsertState.parse(req, res, pathSegments, queryParams);
|
|
79
128
|
if (!parsed) return true;
|
|
@@ -85,6 +134,7 @@ export async function handleInboxState(
|
|
|
85
134
|
status: parsed.body.status,
|
|
86
135
|
snoozeUntil: parsed.body.snoozeUntil,
|
|
87
136
|
readAt: parsed.body.readAt,
|
|
137
|
+
lastReadAt: parsed.body.lastReadAt,
|
|
88
138
|
});
|
|
89
139
|
upsertState.respond(res, 200, { item });
|
|
90
140
|
} catch (err) {
|
package/src/http/index.ts
CHANGED
|
@@ -116,6 +116,7 @@ import { handlePagesPublic } from "./pages-public";
|
|
|
116
116
|
import { handlePoll } from "./poll";
|
|
117
117
|
import { handlePricing } from "./pricing";
|
|
118
118
|
import { handlePromptTemplates } from "./prompt-templates";
|
|
119
|
+
import { handleRanking } from "./ranking";
|
|
119
120
|
import { handleRealtimeAsset, handleRealtimeTicket } from "./realtime";
|
|
120
121
|
import { handleRepos } from "./repos";
|
|
121
122
|
import { handleRooms } from "./rooms";
|
|
@@ -403,10 +404,11 @@ const httpServer = createHttpServer(async (req, res) => {
|
|
|
403
404
|
() => handleExtensions(req, res, pathSegments, queryParams, myAgentId),
|
|
404
405
|
() => handleFavorites(req, res, pathSegments, queryParams, myAgentId),
|
|
405
406
|
() => handleUsers(req, res, pathSegments, queryParams),
|
|
406
|
-
() => handleSessions(req, res, pathSegments, queryParams),
|
|
407
|
+
() => handleSessions(req, res, pathSegments, queryParams, myAgentId),
|
|
407
408
|
() => handleInboxState(req, res, pathSegments, queryParams),
|
|
408
409
|
() => handleTelemetryUi(req, res, pathSegments, queryParams, myAgentId),
|
|
409
410
|
() => handleUsedBy(req, res, pathSegments, queryParams, myAgentId),
|
|
411
|
+
() => handleRanking(req, res, pathSegments, queryParams),
|
|
410
412
|
() => handleFleetBenchmark(req, res, pathSegments, queryParams),
|
|
411
413
|
() => handleTaskTemplates(req, res, pathSegments, queryParams),
|
|
412
414
|
() => handleMcp(req, res, transports, transportActivity, mcpSessionAgents),
|
package/src/http/memory.ts
CHANGED
|
@@ -44,7 +44,7 @@ import { AgentMemorySchema, AgentMemoryScopeSchema, AgentMemorySourceSchema } fr
|
|
|
44
44
|
import { getRequestAuth } from "../utils/request-auth-context";
|
|
45
45
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
46
46
|
import { rejectGuest } from "./request-principal";
|
|
47
|
-
import { route } from "./route-def";
|
|
47
|
+
import { bodyTask, route } from "./route-def";
|
|
48
48
|
import { jsonError, parseQueryParams } from "./utils";
|
|
49
49
|
|
|
50
50
|
// ─── Response Schemas ────────────────────────────────────────────────────────
|
|
@@ -68,6 +68,7 @@ const indexMemory = route({
|
|
|
68
68
|
method: "post",
|
|
69
69
|
path: "/api/memory/index",
|
|
70
70
|
pattern: ["api", "memory", "index"],
|
|
71
|
+
session: bodyTask("sourceTaskId"),
|
|
71
72
|
summary: "Ingest content into memory system (async embedding)",
|
|
72
73
|
tags: ["Memory"],
|
|
73
74
|
// The gate only bites when the key that lands on the rows (here, `sourcePath`) sits under a
|
|
@@ -687,6 +688,7 @@ const rateMemory = route({
|
|
|
687
688
|
method: "post",
|
|
688
689
|
path: "/api/memory/rate",
|
|
689
690
|
pattern: ["api", "memory", "rate"],
|
|
691
|
+
session: { none: "worker-only: it needs X-Agent-ID, which a user token cannot send" },
|
|
690
692
|
summary: "Submit RatingEvents to update memory usefulness posteriors",
|
|
691
693
|
tags: ["Memory"],
|
|
692
694
|
auth: { apiKey: true, agentId: true },
|