@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
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { getDbClient } from "./db";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Session sharing store. A session is a root task (`parentTaskId IS NULL`) and
|
|
5
|
+
* its descendants; its owner is the root's `requestedByUserId`.
|
|
6
|
+
*
|
|
7
|
+
* Rows in `session_shares` are never deleted. Revoking sets `revokedAt`, and an
|
|
8
|
+
* access change revokes the active row and inserts a new one in the same
|
|
9
|
+
* transaction, so the rows for a root are its full share history.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type SessionShareAccess = "read" | "write";
|
|
13
|
+
export type SessionAccess = "owner" | SessionShareAccess | "none";
|
|
14
|
+
export type SessionEveryoneAccess = SessionShareAccess | "none";
|
|
15
|
+
|
|
16
|
+
export type SessionShareRow = {
|
|
17
|
+
id: string;
|
|
18
|
+
rootTaskId: string;
|
|
19
|
+
/** NULL = everyone. */
|
|
20
|
+
userId: string | null;
|
|
21
|
+
access: SessionShareAccess;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
revokedAt: string | null;
|
|
24
|
+
revokedBy: string | null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type SessionRoot = { id: string; requestedByUserId: string | null };
|
|
28
|
+
|
|
29
|
+
export type SessionSharesState = {
|
|
30
|
+
everyone: SessionEveryoneAccess;
|
|
31
|
+
users: { userId: string; access: SessionShareAccess; createdAt: string }[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type DesiredSessionShares = {
|
|
35
|
+
everyone: SessionEveryoneAccess;
|
|
36
|
+
users: { userId: string; access: SessionShareAccess }[];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const RANK: Record<SessionAccess, number> = { none: 0, read: 1, write: 2, owner: 3 };
|
|
40
|
+
|
|
41
|
+
/** True when `access` grants at least `needed`. */
|
|
42
|
+
export function hasSessionAccess(access: SessionAccess, needed: SessionAccess): boolean {
|
|
43
|
+
return RANK[access] >= RANK[needed];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The session root of any task: walk `parentTaskId` up to the topmost task
|
|
48
|
+
* that still exists. No depth cap, so a chain of any length resolves to its
|
|
49
|
+
* actual root; a corrupt parent cycle has no top and resolves to null.
|
|
50
|
+
*/
|
|
51
|
+
export async function getSessionRoot(taskId: string): Promise<SessionRoot | null> {
|
|
52
|
+
return (await getSessionRoots([taskId])).get(taskId) ?? null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const ROOT_LOOKUP_CHUNK = 500;
|
|
56
|
+
|
|
57
|
+
type AncestorRow = { id: string; parentTaskId: string | null; requestedByUserId: string | null };
|
|
58
|
+
|
|
59
|
+
/** `getSessionRoot` for many tasks; unknown tasks and tasks in a cycle are absent. */
|
|
60
|
+
export async function getSessionRoots(taskIds: string[]): Promise<Map<string, SessionRoot>> {
|
|
61
|
+
const ids = [...new Set(taskIds)];
|
|
62
|
+
// Every ancestor once (`UNION` drops revisits, which also ends a cycle),
|
|
63
|
+
// then resolve each chain in memory: linear in the tasks involved.
|
|
64
|
+
const tasks = new Map<string, AncestorRow>();
|
|
65
|
+
for (let start = 0; start < ids.length; start += ROOT_LOOKUP_CHUNK) {
|
|
66
|
+
const chunk = ids.slice(start, start + ROOT_LOOKUP_CHUNK).filter((id) => !tasks.has(id));
|
|
67
|
+
if (chunk.length === 0) continue;
|
|
68
|
+
const rows = await getDbClient().query<AncestorRow>(
|
|
69
|
+
`WITH RECURSIVE up(id) AS (
|
|
70
|
+
SELECT id FROM agent_tasks WHERE id IN (${chunk.map(() => "?").join(", ")})
|
|
71
|
+
UNION
|
|
72
|
+
SELECT t.parentTaskId FROM agent_tasks t JOIN up ON t.id = up.id
|
|
73
|
+
WHERE t.parentTaskId IS NOT NULL
|
|
74
|
+
)
|
|
75
|
+
SELECT t.id, t.parentTaskId, t.requestedByUserId
|
|
76
|
+
FROM up CROSS JOIN agent_tasks t ON t.id = up.id`,
|
|
77
|
+
chunk,
|
|
78
|
+
);
|
|
79
|
+
for (const row of rows) tasks.set(row.id, row);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const resolved = new Map<string, SessionRoot | null>();
|
|
83
|
+
const rootOf = (taskId: string): SessionRoot | null => {
|
|
84
|
+
const path: string[] = [];
|
|
85
|
+
const onPath = new Set<string>();
|
|
86
|
+
let current = tasks.get(taskId);
|
|
87
|
+
let root: SessionRoot | null = null;
|
|
88
|
+
while (current) {
|
|
89
|
+
const known = resolved.get(current.id);
|
|
90
|
+
if (known !== undefined) {
|
|
91
|
+
root = known;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
if (onPath.has(current.id)) break;
|
|
95
|
+
path.push(current.id);
|
|
96
|
+
onPath.add(current.id);
|
|
97
|
+
const parent = current.parentTaskId ? tasks.get(current.parentTaskId) : undefined;
|
|
98
|
+
if (!parent) {
|
|
99
|
+
root = { id: current.id, requestedByUserId: current.requestedByUserId };
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
current = parent;
|
|
103
|
+
}
|
|
104
|
+
for (const id of path) resolved.set(id, root);
|
|
105
|
+
return root;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const roots = new Map<string, SessionRoot>();
|
|
109
|
+
for (const id of ids) {
|
|
110
|
+
const root = rootOf(id);
|
|
111
|
+
if (root) roots.set(id, root);
|
|
112
|
+
}
|
|
113
|
+
return roots;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* What `actingUserId` may do in the session rooted at `root`.
|
|
118
|
+
*
|
|
119
|
+
* - `owner` for the root's requester, and for every user when the root has no
|
|
120
|
+
* owner (Slack and schedule sessions stay open, as before sharing existed).
|
|
121
|
+
* - Otherwise the best active share naming the user or everyone; `none` when
|
|
122
|
+
* there is none. A root with no rows is private to its owner.
|
|
123
|
+
*
|
|
124
|
+
* A null `actingUserId` is a viewer with no user (a guest page session): it
|
|
125
|
+
* gets ownerless roots and everyone shares only. Callers that act for no user
|
|
126
|
+
* (agents, operator-key calls with no picked dashboard user) skip this check
|
|
127
|
+
* entirely; see `resolveSessionActor`.
|
|
128
|
+
*/
|
|
129
|
+
export async function sessionAccessForRoot(
|
|
130
|
+
actingUserId: string | null,
|
|
131
|
+
root: SessionRoot,
|
|
132
|
+
): Promise<SessionAccess> {
|
|
133
|
+
if (!root.requestedByUserId) return "owner";
|
|
134
|
+
if (actingUserId && root.requestedByUserId === actingUserId) return "owner";
|
|
135
|
+
const rows = await getDbClient().query<{ access: SessionShareAccess }>(
|
|
136
|
+
`SELECT access FROM session_shares
|
|
137
|
+
WHERE rootTaskId = ? AND revokedAt IS NULL AND (userId IS NULL OR userId = ?)`,
|
|
138
|
+
[root.id, actingUserId],
|
|
139
|
+
);
|
|
140
|
+
if (rows.some((row) => row.access === "write")) return "write";
|
|
141
|
+
return rows.length > 0 ? "read" : "none";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** `sessionAccessForRoot` for any task in the session; `none` for an unknown task. */
|
|
145
|
+
export async function sessionAccess(
|
|
146
|
+
actingUserId: string | null,
|
|
147
|
+
taskId: string,
|
|
148
|
+
): Promise<SessionAccess> {
|
|
149
|
+
const root = await getSessionRoot(taskId);
|
|
150
|
+
return root ? sessionAccessForRoot(actingUserId, root) : "none";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function listActiveSessionShares(rootTaskId: string): Promise<SessionShareRow[]> {
|
|
154
|
+
return getDbClient().query<SessionShareRow>(
|
|
155
|
+
`SELECT id, rootTaskId, userId, access, createdAt, revokedAt, revokedBy
|
|
156
|
+
FROM session_shares
|
|
157
|
+
WHERE rootTaskId = ? AND revokedAt IS NULL
|
|
158
|
+
ORDER BY createdAt, rowid`,
|
|
159
|
+
[rootTaskId],
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Every row for a root, revoked ones included, oldest first. */
|
|
164
|
+
export async function listSessionShareHistory(rootTaskId: string): Promise<SessionShareRow[]> {
|
|
165
|
+
return getDbClient().query<SessionShareRow>(
|
|
166
|
+
`SELECT id, rootTaskId, userId, access, createdAt, revokedAt, revokedBy
|
|
167
|
+
FROM session_shares
|
|
168
|
+
WHERE rootTaskId = ?
|
|
169
|
+
ORDER BY createdAt, rowid`,
|
|
170
|
+
[rootTaskId],
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function toSessionSharesState(rows: SessionShareRow[]): SessionSharesState {
|
|
175
|
+
const everyone = rows.find((row) => row.userId === null);
|
|
176
|
+
return {
|
|
177
|
+
everyone: everyone?.access ?? "none",
|
|
178
|
+
users: rows
|
|
179
|
+
.filter((row) => row.userId !== null)
|
|
180
|
+
.map((row) => ({ userId: row.userId!, access: row.access, createdAt: row.createdAt })),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Make the active shares of `root` equal `desired`, in one write transaction.
|
|
186
|
+
* A target whose access changes has its active row revoked and a new row
|
|
187
|
+
* inserted; a target no longer listed is revoked; nothing is deleted or
|
|
188
|
+
* updated in place apart from revocation. The owner is never stored.
|
|
189
|
+
*/
|
|
190
|
+
export async function setSessionShares(
|
|
191
|
+
root: SessionRoot,
|
|
192
|
+
desired: DesiredSessionShares,
|
|
193
|
+
actorUserId: string | null,
|
|
194
|
+
): Promise<SessionSharesState> {
|
|
195
|
+
const want = new Map<string, SessionShareAccess>();
|
|
196
|
+
if (desired.everyone !== "none") want.set("*", desired.everyone);
|
|
197
|
+
for (const user of desired.users) {
|
|
198
|
+
if (user.userId !== root.requestedByUserId) want.set(user.userId, user.access);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return getDbClient().transaction(async (tx) => {
|
|
202
|
+
const now = new Date().toISOString();
|
|
203
|
+
const active = await tx.query<SessionShareRow>(
|
|
204
|
+
`SELECT id, rootTaskId, userId, access, createdAt, revokedAt, revokedBy
|
|
205
|
+
FROM session_shares WHERE rootTaskId = ? AND revokedAt IS NULL`,
|
|
206
|
+
[root.id],
|
|
207
|
+
);
|
|
208
|
+
const kept = new Set<string>();
|
|
209
|
+
for (const row of active) {
|
|
210
|
+
const target = row.userId ?? "*";
|
|
211
|
+
if (want.get(target) === row.access) {
|
|
212
|
+
kept.add(target);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
await tx.run(
|
|
216
|
+
"UPDATE session_shares SET revokedAt = ?, revokedBy = ?, updated_by = ? WHERE id = ?",
|
|
217
|
+
[now, actorUserId, actorUserId, row.id],
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
for (const [target, access] of want) {
|
|
221
|
+
if (kept.has(target)) continue;
|
|
222
|
+
await tx.run(
|
|
223
|
+
`INSERT INTO session_shares (id, rootTaskId, userId, access, createdAt, created_by, updated_by)
|
|
224
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
225
|
+
[
|
|
226
|
+
crypto.randomUUID(),
|
|
227
|
+
root.id,
|
|
228
|
+
target === "*" ? null : target,
|
|
229
|
+
access,
|
|
230
|
+
now,
|
|
231
|
+
actorUserId,
|
|
232
|
+
actorUserId,
|
|
233
|
+
],
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const rows = await tx.query<SessionShareRow>(
|
|
237
|
+
`SELECT id, rootTaskId, userId, access, createdAt, revokedAt, revokedBy
|
|
238
|
+
FROM session_shares WHERE rootTaskId = ? AND revokedAt IS NULL
|
|
239
|
+
ORDER BY createdAt, rowid`,
|
|
240
|
+
[root.id],
|
|
241
|
+
);
|
|
242
|
+
return toSessionSharesState(rows);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { validateChatSdkAdaptersValue } from "../integrations/chat-sdk/schema";
|
|
2
|
+
import { parseTelemetryRanking, TELEMETRY_RANKING_KEY } from "../ranking/telemetry-ranking";
|
|
2
3
|
import { validateQuickJSPoolSize } from "../scripts-runtime/executors/quickjs-pool-config";
|
|
3
4
|
import { normalizeSlackReactionShortcode } from "../slack/reaction-shortcode";
|
|
4
5
|
import { ProviderNameSchema } from "../types";
|
|
@@ -69,15 +70,24 @@ export function isApiOnlyConfigKey(key: string): boolean {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
/** Config rows owned by dedicated API surfaces, not the generic config API. */
|
|
72
|
-
export const INTERNAL_CONFIG_KEYS = new Set([
|
|
73
|
+
export const INTERNAL_CONFIG_KEYS = new Set([
|
|
74
|
+
"onboarding_state",
|
|
75
|
+
"runtime_owner_id",
|
|
76
|
+
TELEMETRY_RANKING_KEY,
|
|
77
|
+
]);
|
|
73
78
|
|
|
74
79
|
export function isInternalConfigKey(key: string): boolean {
|
|
75
80
|
return INTERNAL_CONFIG_KEYS.has(key.toLowerCase());
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
export function internalConfigKeyError(key: string): Error {
|
|
84
|
+
const lower = key.toLowerCase();
|
|
79
85
|
const owner =
|
|
80
|
-
|
|
86
|
+
lower === "runtime_owner_id"
|
|
87
|
+
? "the runtime supervisor"
|
|
88
|
+
: lower === TELEMETRY_RANKING_KEY
|
|
89
|
+
? "/api/ranking"
|
|
90
|
+
: "/api/onboarding";
|
|
81
91
|
return new Error(`Key '${key}' is managed by ${owner}`);
|
|
82
92
|
}
|
|
83
93
|
|
|
@@ -301,6 +311,16 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
|
|
|
301
311
|
return "Invalid TASK_TOOL_MANIFESTS (expected JSON with taskTypes and/or schedules maps, each selecting at most 16 known swarm tools)";
|
|
302
312
|
}
|
|
303
313
|
},
|
|
314
|
+
// Owned by /api/ranking (an internal key); parsed here as defense in depth.
|
|
315
|
+
[TELEMETRY_RANKING_KEY.toUpperCase()]: (value) => {
|
|
316
|
+
try {
|
|
317
|
+
if (typeof value !== "string") throw new Error("Expected JSON string");
|
|
318
|
+
parseTelemetryRanking(value);
|
|
319
|
+
return null;
|
|
320
|
+
} catch {
|
|
321
|
+
return "Invalid telemetry_ranking (expected the v1 ranking listing JSON)";
|
|
322
|
+
}
|
|
323
|
+
},
|
|
304
324
|
// MCP tool allowlist: comma-separated names or a JSON array of strings.
|
|
305
325
|
// Unknown names are accepted here and warned about at session init.
|
|
306
326
|
SWARM_ENABLED_TOOLS: (value) => {
|
|
@@ -405,6 +425,7 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
|
|
|
405
425
|
},
|
|
406
426
|
...booleanValidators([
|
|
407
427
|
"SWARM_USED_BY_CONSENT",
|
|
428
|
+
"PUBLIC_RANKING_ENABLED",
|
|
408
429
|
"SWARM_DEV_MODE",
|
|
409
430
|
"MULTI_RUNTIME_ENABLED",
|
|
410
431
|
"STEERING_ENABLED",
|
package/src/commands/runner.ts
CHANGED
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
skillFromToolArgs,
|
|
56
56
|
} from "../providers/skill-invoke.ts";
|
|
57
57
|
import type { SteerDeliveryResult } from "../providers/types.ts";
|
|
58
|
+
import { rankingUsageProps } from "../ranking/usage.ts";
|
|
58
59
|
import { WorkerRealtimeClient } from "../realtime/worker-client";
|
|
59
60
|
import { resumedTaskModelSelection, type TaskModelPinFields } from "../tasks/model-pin.ts";
|
|
60
61
|
import { initTelemetry, telemetry } from "../telemetry.ts";
|
|
@@ -1228,6 +1229,10 @@ export const RELOADABLE_ENV_KEYS: ReadonlySet<string> = new Set([
|
|
|
1228
1229
|
"TEMPLATE_REGISTRY_URL",
|
|
1229
1230
|
"SLACK_DISABLE",
|
|
1230
1231
|
"SWARM_ORG_NAME",
|
|
1232
|
+
// Gate the public-ranking `ranking.usage` emit, so clearing the website or
|
|
1233
|
+
// turning the setting off stops it on running workers.
|
|
1234
|
+
"SWARM_ORG_WEBSITE",
|
|
1235
|
+
"PUBLIC_RANKING_ENABLED",
|
|
1231
1236
|
// pi reads these from process.env for its traits and its session.
|
|
1232
1237
|
"PI_TOOL_DEFERRAL",
|
|
1233
1238
|
"PI_CODEMODE",
|
|
@@ -4923,6 +4928,12 @@ async function spawnProviderProcess(
|
|
|
4923
4928
|
},
|
|
4924
4929
|
{ userId: opts.requestedByUserId ?? null },
|
|
4925
4930
|
);
|
|
4931
|
+
// Public ranking: token counts only, while the install is opted in.
|
|
4932
|
+
const rankingUsage = rankingUsageProps(result.cost, {
|
|
4933
|
+
model: result.cost.model,
|
|
4934
|
+
provider: result.cost.provider ?? opts.harnessProvider,
|
|
4935
|
+
});
|
|
4936
|
+
if (rankingUsage) telemetry.ranking("usage", rankingUsage);
|
|
4926
4937
|
}
|
|
4927
4938
|
|
|
4928
4939
|
// Post completion context usage snapshot
|
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
} from "../be/db";
|
|
14
14
|
import { ActiveSessionSchema, AgentTaskSchema } from "../types";
|
|
15
15
|
import { isMultiRuntimeEnabled } from "../utils/multi-runtime";
|
|
16
|
-
import { route } from "./route-def";
|
|
16
|
+
import { bodyTask, route } from "./route-def";
|
|
17
|
+
import { readableByFor, resolveSessionActor } from "./session-access";
|
|
17
18
|
import { jsonError } from "./utils";
|
|
18
19
|
|
|
19
20
|
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
@@ -39,6 +40,7 @@ const createActiveSession = route({
|
|
|
39
40
|
method: "post",
|
|
40
41
|
path: "/api/active-sessions",
|
|
41
42
|
pattern: ["api", "active-sessions"],
|
|
43
|
+
session: bodyTask("taskId"),
|
|
42
44
|
summary: "Create a new active session",
|
|
43
45
|
tags: ["Active Sessions"],
|
|
44
46
|
body: z.object({
|
|
@@ -60,6 +62,7 @@ const deleteSessionByTask = route({
|
|
|
60
62
|
method: "delete",
|
|
61
63
|
path: "/api/active-sessions/by-task/{taskId}",
|
|
62
64
|
pattern: ["api", "active-sessions", "by-task", null],
|
|
65
|
+
session: { taskParam: "taskId" },
|
|
63
66
|
summary: "Delete active session by task ID",
|
|
64
67
|
tags: ["Active Sessions"],
|
|
65
68
|
params: z.object({ taskId: z.string() }),
|
|
@@ -72,6 +75,7 @@ const deleteSessionById = route({
|
|
|
72
75
|
method: "delete",
|
|
73
76
|
path: "/api/active-sessions/{id}",
|
|
74
77
|
pattern: ["api", "active-sessions", null],
|
|
78
|
+
session: { activeSessionParam: "id" },
|
|
75
79
|
summary: "Delete active session by ID",
|
|
76
80
|
tags: ["Active Sessions"],
|
|
77
81
|
params: z.object({ id: z.string() }),
|
|
@@ -84,6 +88,7 @@ const heartbeatSession = route({
|
|
|
84
88
|
method: "put",
|
|
85
89
|
path: "/api/active-sessions/heartbeat/{taskId}",
|
|
86
90
|
pattern: ["api", "active-sessions", "heartbeat", null],
|
|
91
|
+
session: { taskParam: "taskId" },
|
|
87
92
|
summary: "Update heartbeat for an active session",
|
|
88
93
|
tags: ["Active Sessions"],
|
|
89
94
|
params: z.object({ taskId: z.string() }),
|
|
@@ -96,6 +101,7 @@ const updateProviderSession = route({
|
|
|
96
101
|
method: "put",
|
|
97
102
|
path: "/api/active-sessions/provider-session/{taskId}",
|
|
98
103
|
pattern: ["api", "active-sessions", "provider-session", null],
|
|
104
|
+
session: { taskParam: "taskId" },
|
|
99
105
|
summary: "Update provider session ID on an active session",
|
|
100
106
|
tags: ["Active Sessions"],
|
|
101
107
|
params: z.object({ taskId: z.string() }),
|
|
@@ -109,6 +115,7 @@ const cleanupSessions = route({
|
|
|
109
115
|
method: "post",
|
|
110
116
|
path: "/api/active-sessions/cleanup",
|
|
111
117
|
pattern: ["api", "active-sessions", "cleanup"],
|
|
118
|
+
session: { agentWide: "Deletes every session of an agent, or every stale session." },
|
|
112
119
|
summary: "Clean up stale sessions",
|
|
113
120
|
tags: ["Active Sessions"],
|
|
114
121
|
body: z
|
|
@@ -126,6 +133,7 @@ const recoverOrphanedTasks = route({
|
|
|
126
133
|
method: "post",
|
|
127
134
|
path: "/api/active-sessions/recover-orphaned-tasks",
|
|
128
135
|
pattern: ["api", "active-sessions", "recover-orphaned-tasks"],
|
|
136
|
+
session: { none: "Its handler admits only the calling agent, never a user." },
|
|
129
137
|
summary: "Recover orphaned in-progress tasks for an agent",
|
|
130
138
|
tags: ["Active Sessions"],
|
|
131
139
|
body: z.object({
|
|
@@ -153,7 +161,10 @@ export async function handleActiveSessions(
|
|
|
153
161
|
if (listActiveSessions.match(req.method, pathSegments)) {
|
|
154
162
|
const parsed = await listActiveSessions.parse(req, res, pathSegments, queryParams);
|
|
155
163
|
if (!parsed) return true;
|
|
156
|
-
const sessions = await getActiveSessions(
|
|
164
|
+
const sessions = await getActiveSessions(
|
|
165
|
+
parsed.query.agentId || undefined,
|
|
166
|
+
readableByFor(await resolveSessionActor(req, myAgentId)),
|
|
167
|
+
);
|
|
157
168
|
listActiveSessions.respond(res, 200, { sessions });
|
|
158
169
|
return true;
|
|
159
170
|
}
|
package/src/http/agents.ts
CHANGED
|
@@ -90,6 +90,7 @@ import { getRequestAuth } from "../utils/request-auth-context";
|
|
|
90
90
|
import { isRuntimePoolsEnabled } from "../utils/runtime-pools-flag";
|
|
91
91
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
92
92
|
import { route, runtimeInstanceHeader } from "./route-def";
|
|
93
|
+
import { readableByFor, resolveSessionActor } from "./session-access";
|
|
93
94
|
import { agentWithCapacity, json, jsonError } from "./utils";
|
|
94
95
|
|
|
95
96
|
function singleHeaderValue(req: IncomingMessage, name: string): string | undefined {
|
|
@@ -844,7 +845,7 @@ export async function handleAgentsRest(
|
|
|
844
845
|
res: ServerResponse,
|
|
845
846
|
pathSegments: string[],
|
|
846
847
|
queryParams: URLSearchParams,
|
|
847
|
-
|
|
848
|
+
myAgentId: string | undefined,
|
|
848
849
|
): Promise<boolean> {
|
|
849
850
|
if (listAgents.match(req.method, pathSegments)) {
|
|
850
851
|
const parsed = await listAgents.parse(req, res, pathSegments, queryParams);
|
|
@@ -853,7 +854,10 @@ export async function handleAgentsRest(
|
|
|
853
854
|
// List responses default to slim (no identity markdown); `?fields=full` restores it.
|
|
854
855
|
const slim = parsed.query.fields !== "full";
|
|
855
856
|
const agents = includeTasks
|
|
856
|
-
? await getAllAgentsWithTasks({
|
|
857
|
+
? await getAllAgentsWithTasks({
|
|
858
|
+
slim,
|
|
859
|
+
readableBy: readableByFor(await resolveSessionActor(req, myAgentId)),
|
|
860
|
+
})
|
|
857
861
|
: await getAllAgents({ slim });
|
|
858
862
|
const [agentsWithCapacity, transports] = await Promise.all([
|
|
859
863
|
Promise.all(agents.map(agentWithCapacity)),
|
|
@@ -1537,7 +1541,10 @@ export async function handleAgentsRest(
|
|
|
1537
1541
|
if (!parsed) return true;
|
|
1538
1542
|
const includeTasks = parsed.query.include === "tasks";
|
|
1539
1543
|
const agent = includeTasks
|
|
1540
|
-
? await getAgentWithTasks(
|
|
1544
|
+
? await getAgentWithTasks(
|
|
1545
|
+
parsed.params.id,
|
|
1546
|
+
readableByFor(await resolveSessionActor(req, myAgentId)),
|
|
1547
|
+
)
|
|
1541
1548
|
: await getAgentById(parsed.params.id);
|
|
1542
1549
|
|
|
1543
1550
|
if (!agent) {
|
package/src/http/all-routes.ts
CHANGED
package/src/http/api-keys.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
SUBSCRIPTION_PLANS,
|
|
21
21
|
SUBSCRIPTION_PLANS_CHECKED_AT,
|
|
22
22
|
} from "../utils/subscription-plans";
|
|
23
|
-
import { route } from "./route-def";
|
|
23
|
+
import { bodyTask, route } from "./route-def";
|
|
24
24
|
import { clearUsageCache } from "./usage-cache";
|
|
25
25
|
import { jsonError } from "./utils";
|
|
26
26
|
|
|
@@ -36,6 +36,7 @@ const reportUsage = route({
|
|
|
36
36
|
method: "post",
|
|
37
37
|
path: "/api/keys/report-usage",
|
|
38
38
|
pattern: ["api", "keys", "report-usage"],
|
|
39
|
+
session: bodyTask("taskId"),
|
|
39
40
|
summary: "Record which API key was used for a task",
|
|
40
41
|
tags: ["API Keys"],
|
|
41
42
|
body: z.object({
|
|
@@ -82,6 +83,7 @@ const reportAuthFailure = route({
|
|
|
82
83
|
method: "post",
|
|
83
84
|
path: "/api/keys/report-auth-failure",
|
|
84
85
|
pattern: ["api", "keys", "report-auth-failure"],
|
|
86
|
+
session: bodyTask("taskId"),
|
|
85
87
|
summary: "Record an auth failure for a pooled key; bench it after 2 in a row",
|
|
86
88
|
tags: ["API Keys"],
|
|
87
89
|
body: z.object({
|
|
@@ -23,7 +23,7 @@ import { getRequestAuth } from "../utils/request-auth-context";
|
|
|
23
23
|
import { cancelApprovalRequest } from "../workflows/approval-cancel";
|
|
24
24
|
import { createApprovalFollowUpTask } from "../workflows/approval-notifications";
|
|
25
25
|
import { workflowEventBus } from "../workflows/event-bus";
|
|
26
|
-
import { route } from "./route-def";
|
|
26
|
+
import { bodyTask, route } from "./route-def";
|
|
27
27
|
import { jsonError } from "./utils";
|
|
28
28
|
|
|
29
29
|
// ─── Route Definitions ───────────────────────────────────────────────────────
|
|
@@ -255,6 +255,7 @@ const createRoute = route({
|
|
|
255
255
|
method: "post",
|
|
256
256
|
path: "/api/approval-requests",
|
|
257
257
|
pattern: ["api", "approval-requests"],
|
|
258
|
+
session: bodyTask("sourceTaskId"),
|
|
258
259
|
summary: "Create a new approval request",
|
|
259
260
|
tags: ["ApprovalRequests"],
|
|
260
261
|
body: z.object({
|
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/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() }),
|