@desplega.ai/agent-swarm 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/{acp-adapter-v85a2bqy.js → acp-adapter-kedstxx6.js} +6 -6
- package/dist/{actions-zgcxfspj.js → actions-vget6r1k.js} +18 -16
- package/dist/{amp-adapter-jw9xfz42.js → amp-adapter-xeb5mktv.js} +2 -2
- package/dist/{app-4e1s4675.js → app-mqap8t8m.js} +3 -3
- package/dist/{assistant-v238n9dk.js → assistant-h0gv97mv.js} +20 -18
- package/dist/{boot-reembed-p666cw5n.js → boot-reembed-qr8a894z.js} +1 -1
- package/dist/{boot-reembed-w6pm64j4.js → boot-reembed-rj85jjhw.js} +1 -1
- package/dist/{boot-scrub-sweep-jcyjagn3.js → boot-scrub-sweep-eyfvn6v9.js} +3 -3
- package/dist/{boot-seeds-gbedmamp.js → boot-seeds-xs9a8dx1.js} +4 -4
- package/dist/{claude-adapter-q7wspzt8.js → claude-adapter-bm23cye7.js} +5 -5
- package/dist/{claude-managed-adapter-n7ahjyx6.js → claude-managed-adapter-1mwrsm9d.js} +2 -2
- package/dist/{claude-managed-setup-wza784fb.js → claude-managed-setup-gkfgks4j.js} +1 -1
- package/dist/{claude-sdk-session-yf400ptr.js → claude-sdk-session-apb4ypz3.js} +5 -5
- package/dist/cli-02vhhssk.js +848 -0
- package/dist/{cli-4ync7m60.js → cli-0ebey9hg.js} +1 -1
- package/dist/{cli-665j3feb.js → cli-0xk505e8.js} +1 -1
- package/dist/{cli-30m5q8mc.js → cli-1cevz46c.js} +5 -5
- package/dist/{cli-gzvb3d4a.js → cli-1jx0srjt.js} +3 -3
- package/dist/{cli-zj4d6afs.js → cli-1pa3rz69.js} +21 -6
- package/dist/{cli-fhvqt2fp.js → cli-2rwet3r7.js} +1 -1
- package/dist/{cli-azs9bhc5.js → cli-3wjs22x3.js} +2 -2
- package/dist/{cli-dxwh4j32.js → cli-4fhyq711.js} +45 -36
- package/dist/{cli-yv3n83x8.js → cli-5ff2sjyt.js} +1 -1
- package/dist/{cli-6vcyw6v7.js → cli-5s5hx57y.js} +5 -3
- package/dist/{cli-hz64bcnj.js → cli-6ftrrwm1.js} +9 -371
- package/dist/{cli-zes4tsbb.js → cli-8b9wvjps.js} +1 -1
- package/dist/cli-8cxkw1f5.js +382 -0
- package/dist/{cli-4s1kqrqg.js → cli-8rwvksbc.js} +2 -2
- package/dist/{cli-e09hj50q.js → cli-8wkb09q5.js} +11 -11
- package/dist/{cli-yv0vgscc.js → cli-a1esmmee.js} +13 -2
- package/dist/{cli-4hmaheb6.js → cli-avmqhatn.js} +7 -7
- package/dist/{cli-g6p7c2my.js → cli-b80a1100.js} +1 -1
- package/dist/{cli-tr24eeny.js → cli-bfc3k1x5.js} +3 -3
- package/dist/{cli-ka01m5rb.js → cli-bhwa8rzc.js} +13 -11
- package/dist/{cli-8cvvytad.js → cli-bn26xbcs.js} +2 -2
- package/dist/{cli-k28p6xh7.js → cli-c8fwpgqb.js} +1 -1
- package/dist/{cli-rc3r0bnh.js → cli-ccn0eybf.js} +1 -1
- package/dist/{cli-9td5ss65.js → cli-dsgj3e7a.js} +2 -2
- package/dist/{cli-a17tzmn0.js → cli-ecshf504.js} +2 -2
- package/dist/{cli-d43xqzbe.js → cli-eqgfs202.js} +1 -1
- package/dist/{cli-ppapy34w.js → cli-f3nx1h8z.js} +2 -2
- package/dist/{cli-ea7chd26.js → cli-ffbrkh1r.js} +74 -35
- package/dist/{cli-xgn4rwyv.js → cli-kk2r80n5.js} +4 -4
- package/dist/{cli-hn3pftj2.js → cli-kz116kvw.js} +3 -3
- package/dist/{cli-1v8w782f.js → cli-mvg2hf8p.js} +4 -4
- package/dist/{cli-5d1m1qca.js → cli-mz1ezzzh.js} +1 -1
- package/dist/{cli-jhmjwqcg.js → cli-nme7wery.js} +6 -6
- package/dist/{cli-65q70v2v.js → cli-nrrg0fvy.js} +18 -4
- package/dist/{cli-8jtrekpy.js → cli-p1m2b6gc.js} +7 -7
- package/dist/{cli-re4wkbzx.js → cli-rg7ntypm.js} +328 -47
- package/dist/{cli-fp4xbw07.js → cli-rsd7ga7v.js} +10 -8
- package/dist/{cli-etvn5ej2.js → cli-sppc0cmw.js} +1 -1
- package/dist/{cli-qrwp40st.js → cli-tg9ksbn8.js} +1 -1
- package/dist/{cli-6p7ddg72.js → cli-tgvpmnwz.js} +2 -2
- package/dist/{cli-q1812jbw.js → cli-tj4j2ts4.js} +6 -6
- package/dist/{cli-kqypty34.js → cli-ttaz6c7m.js} +2 -2
- package/dist/{cli-w7z94e6c.js → cli-tx00c0h6.js} +4 -2
- package/dist/{cli-4g629sgr.js → cli-wrv09v7q.js} +20 -1
- package/dist/{cli-cby329ny.js → cli-wyrr38xm.js} +2 -2
- package/dist/{cli-y3dvz6ge.js → cli-x1p5dhq2.js} +3 -3
- package/dist/{cli-8qsx817b.js → cli-x719j3vs.js} +219 -629
- package/dist/{cli-9e9kjqsj.js → cli-xfe63zx3.js} +1 -1
- package/dist/{cli-wtg6edvd.js → cli-ywyrffff.js} +1 -1
- package/dist/{cli-0rh20bp3.js → cli-zvze9rbm.js} +1 -1
- package/dist/cli.js +14 -14
- package/dist/{codex-adapter-nhjj2veb.js → codex-adapter-pp2gq8gr.js} +3 -3
- package/dist/{codex-hook-x6tm6y5m.js → codex-hook-qwzccd64.js} +1 -1
- package/dist/{codex-session-runner-6hggrjbx.js → codex-session-runner-0vzn09t8.js} +3 -3
- package/dist/{commands-mrw1ph2w.js → commands-psqhkttb.js} +2 -2
- package/dist/{controller-pt1zam5w.js → controller-t20vz2k8.js} +2 -2
- package/dist/{controllers-connect-ymw5541d.js → controllers-connect-gkxmqw9d.js} +24 -20
- package/dist/{cursor-adapter-cf4zh5x0.js → cursor-adapter-3v542qe5.js} +1 -1
- package/dist/{db-hy80219q.js → db-2xbyenjr.js} +2 -2
- package/dist/{db-hhpy6m4x.js → db-nc1rd869.js} +3 -2
- package/dist/{dispatcher-6pdjtkr1.js → dispatcher-ewceb8ts.js} +17 -15
- package/dist/{dsh-adapter-fbx06re2.js → dsh-adapter-75xwh78t.js} +1 -1
- package/dist/{easter-egg-3d-6hg2r942.js → easter-egg-3d-xsc47jwf.js} +1 -1
- package/dist/{execute-bcqc69a5.js → execute-gzmw1mnw.js} +1 -1
- package/dist/{external-channel-j6vb6ya1.js → external-channel-a5dsmxn4.js} +2 -2
- package/dist/{google-generative-ai-phpmwxvg.js → google-generative-ai-bmq8gkf3.js} +1 -1
- package/dist/{google-vertex-es9ybgdy.js → google-vertex-b1rvp52s.js} +1 -1
- package/dist/{grok-adapter-1193sprp.js → grok-adapter-vj88xr23.js} +7 -7
- package/dist/handlers-zbs90bny.js +39 -0
- package/dist/{heartbeat-h1w1ktce.js → heartbeat-t2m9r3wa.js} +20 -18
- package/dist/{hook-skrbja18.js → hook-8femx1bg.js} +4 -4
- package/dist/{hook-ejja4bpe.js → hook-xa677f8r.js} +5 -5
- package/dist/{http-mzh41mfj.js → http-xpmmq028.js} +868 -253
- package/dist/{index-vjn3wbzf.js → index-8pwc1jc8.js} +17 -15
- package/dist/{index-epr2gnbw.js → index-cdzyfrsp.js} +1 -1
- package/dist/{index-xmmkyx82.js → index-dg2tmj99.js} +17 -15
- package/dist/index-nxa6me0q.js +41 -0
- package/dist/{index-6rmnn89d.js → index-s7ckdajq.js} +9 -9
- package/dist/{index-wgsw9xcz.js → index-syztqdt6.js} +1 -1
- package/dist/{index-x0q2a4f9.js → index-ysd45kgc.js} +1 -1
- package/dist/{index-g909y089.js → index-zgvqzjsw.js} +8 -8
- package/dist/{keepalive-4djvbm2j.js → keepalive-3nr4st3r.js} +3 -3
- package/dist/{lead-61kqp604.js → lead-td8h2sfn.js} +33 -31
- package/dist/{maintenance-f3xhnjsm.js → maintenance-verwpe2c.js} +1 -1
- package/dist/{oauth-refresh-sweep-fpd6x6rw.js → oauth-refresh-sweep-9q2t0y3q.js} +2 -2
- package/dist/{onboard-196tjgyn.js → onboard-aytqedmq.js} +2 -2
- package/dist/{opencode-adapter-eeesgfqk.js → opencode-adapter-f4hryjny.js} +4 -4
- package/dist/{otel-impl-g8fh4c21.js → otel-impl-hckw4wt1.js} +1 -1
- package/dist/{pi-mono-adapter-zasp3ze3.js → pi-mono-adapter-hxby0ce1.js} +6 -6
- package/dist/{pricing-refresh-sdcf5yaz.js → pricing-refresh-5nvme3sm.js} +3 -3
- package/dist/{rbac-roles-bsp5657v.js → rbac-roles-pd8c26m1.js} +5 -3
- package/dist/{rbac-roles-2x4g1wb5.js → rbac-roles-vbpy21mg.js} +2 -2
- package/dist/{render-v2-0pyb39bb.js → render-v2-7nqgvk2q.js} +3 -3
- package/dist/{resume-a86tkj6r.js → resume-b6djcj3c.js} +6 -6
- package/dist/{runtime-ar8310ba.js → runtime-txrxc9za.js} +1 -1
- package/dist/session-access-fc3f9y12.js +25 -0
- package/dist/{setup-ypbzcapd.js → setup-n39szm6b.js} +1 -1
- package/dist/{virtual-modules-wzk6jf3a.js → virtual-modules-wyp2k6z3.js} +2 -2
- package/dist/{work-objects-yjp8de12.js → work-objects-jem2fre0.js} +20 -18
- package/dist/{worker-4ptnn1ge.js → worker-wr6xgfr2.js} +33 -31
- package/openapi.json +844 -110
- package/package.json +1 -1
- package/src/apps/action-caller.ts +6 -0
- package/src/apps/script-types.ts +16 -3
- package/src/be/db/active-sessions.ts +5 -0
- package/src/be/db/session-visibility.ts +29 -0
- package/src/be/db/tasks/read.ts +61 -3
- package/src/be/db/tasks/write.ts +15 -6
- package/src/be/db.ts +92 -33
- package/src/be/events.ts +11 -0
- package/src/be/migrations/230_session_shares.sql +23 -0
- package/src/be/rbac-roles.ts +2 -0
- package/src/be/session-shares.ts +244 -0
- package/src/be/swarm-config-guard.ts +23 -2
- package/src/commands/runner.ts +11 -0
- package/src/http/active-sessions.ts +13 -2
- package/src/http/agents.ts +10 -3
- package/src/http/all-routes.ts +1 -0
- package/src/http/api-keys.ts +3 -1
- package/src/http/approval-requests.ts +2 -1
- package/src/http/apps.ts +36 -11
- package/src/http/config.ts +3 -0
- package/src/http/context.ts +2 -0
- package/src/http/core.ts +39 -2
- package/src/http/events.ts +6 -2
- package/src/http/favorite-owner.ts +1 -1
- package/src/http/fs.ts +38 -3
- package/src/http/index.ts +3 -1
- package/src/http/memory.ts +3 -1
- package/src/http/ranking.ts +367 -0
- package/src/http/realtime.ts +26 -21
- package/src/http/route-def.ts +77 -11
- package/src/http/script-runs.ts +2 -1
- package/src/http/scripts.ts +8 -0
- package/src/http/session-access.ts +179 -0
- package/src/http/session-data.ts +10 -2
- package/src/http/sessions.ts +212 -12
- package/src/http/stats.ts +7 -3
- package/src/http/tasks.ts +83 -7
- package/src/http/used-by.ts +11 -3
- package/src/prompts/session-templates.ts +3 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/ranking/telemetry-ranking.ts +147 -0
- package/src/ranking/usage.ts +57 -0
- package/src/rbac/admission.ts +22 -7
- package/src/rbac/legacy-policy.ts +2 -0
- package/src/rbac/permissions.ts +9 -0
- package/src/realtime/auth.ts +69 -0
- package/src/realtime/tickets.ts +4 -1
- package/src/realtime/transport.ts +77 -9
- package/src/slack/blocks.ts +18 -0
- package/src/slack/dm.ts +29 -0
- package/src/telemetry-contract/EVENTS.md +3 -0
- package/src/telemetry-contract/SOURCE.json +37 -4
- package/src/telemetry-contract/catalog.json +69 -0
- package/src/telemetry-contract/ranking/fixtures/expected.json +30 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/bad-category.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/bad-claim.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/blank-description.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/blank-name.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/description-too-long.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/domain-uppercase.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-scheme.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-www.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/http-source-url.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/missing-description.json +10 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/missing-version.json +10 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/name-not-string.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/name-too-long.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/not-object.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/null.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/other-with-subcategory.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/oversize.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/short-claim.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/subcategory-other-category.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/trailing-data.json +1 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/unknown-field.json +12 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/unsupported-version.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-not-date.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-offset.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/invalid/wrong-case-field.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/category-without-subcategory.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/full.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/minimal.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/padded-text.json +11 -0
- package/src/telemetry-contract/ranking/fixtures/valid/unicode-name.json +11 -0
- package/src/telemetry-contract/ranking/listing.v1.schema.json +61 -0
- package/src/telemetry-contract/ranking/taxonomy.json +8 -0
- package/src/telemetry-contract/ranking-taxonomy.gen.ts +11 -0
- package/src/telemetry-contract/types.gen.ts +708 -1
- package/src/telemetry.ts +26 -2
- package/src/tests/apps-rbac.test.ts +189 -2
- package/src/tests/apps-script-types.test.ts +16 -3
- package/src/tests/check-pr-body.test.ts +23 -0
- package/src/tests/check-ranking-entry.test.ts +146 -0
- package/src/tests/db-extraction-contract.test.ts +1 -0
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/org-identity.test.ts +44 -0
- package/src/tests/ranking-listing.test.ts +112 -0
- package/src/tests/ranking-route.test.ts +319 -0
- package/src/tests/ranking-usage.test.ts +50 -0
- package/src/tests/rbac-admission-e2e.test.ts +8 -0
- package/src/tests/rbac-admission.test.ts +38 -0
- package/src/tests/rbac-charact-http.test.ts +4 -3
- package/src/tests/rbac-engine.test.ts +2 -0
- package/src/tests/rbac-roles.test.ts +2 -0
- package/src/tests/realtime-room-surfaces.test.ts +3 -1
- package/src/tests/realtime-session-join-race.test.ts +142 -0
- package/src/tests/realtime-tickets.test.ts +24 -17
- package/src/tests/realtime-transport.test.ts +91 -0
- package/src/tests/session-route-posture.test.ts +639 -0
- package/src/tests/session-sharing-routes.test.ts +282 -0
- package/src/tests/session-sharing.test.ts +526 -0
- package/src/tests/sessions-detail-batch.test.ts +7 -1
- package/src/tests/slack-tool-blocks.test.ts +101 -0
- package/src/tests/slack-update.test.ts +55 -0
- package/src/tests/slack-user-dm.test.ts +177 -0
- package/src/tests/used-by-route.test.ts +10 -0
- package/src/tools/slack-post.ts +18 -12
- package/src/tools/slack-read.ts +13 -1
- package/src/tools/slack-reply.ts +7 -10
- package/src/tools/slack-start-thread.ts +18 -11
- package/src/tools/slack-update.ts +2 -9
- package/src/types.ts +35 -0
- package/src/utils/org-identity.ts +33 -0
- package/templates/skills/apps/SKILL.md +17 -0
- package/templates/skills/apps/content.md +17 -0
- package/dist/handlers-tb3xgjnm.js +0 -37
- package/dist/index-nyfcdpky.js +0 -39
- package/dist/{cli-n5mrsg0f.js → cli-60cgh1rv.js} +3 -3
- /package/dist/{cli-j0v75y13.js → cli-y83zk5sb.js} +0 -0
package/src/http/sessions.ts
CHANGED
|
@@ -7,18 +7,40 @@ import {
|
|
|
7
7
|
getTaskById,
|
|
8
8
|
listRecentSessions,
|
|
9
9
|
} from "../be/db";
|
|
10
|
+
import {
|
|
11
|
+
getSessionRoot,
|
|
12
|
+
listActiveSessionShares,
|
|
13
|
+
type SessionAccess,
|
|
14
|
+
sessionAccessForRoot,
|
|
15
|
+
setSessionShares,
|
|
16
|
+
toSessionSharesState,
|
|
17
|
+
} from "../be/session-shares";
|
|
10
18
|
import { getTaskSteeringFieldsForTasks } from "../be/steering";
|
|
11
19
|
import { getTaskCitationsForTasks, TaskCitationSchema } from "../be/task-citations";
|
|
12
20
|
import {
|
|
21
|
+
findUserById,
|
|
13
22
|
InvalidRuntimeTokenError,
|
|
14
23
|
mintRuntimeToken,
|
|
15
24
|
mintSessionToken,
|
|
16
25
|
revokeSessionToken,
|
|
17
26
|
} from "../be/users";
|
|
18
|
-
import {
|
|
27
|
+
import { can } from "../rbac";
|
|
28
|
+
import { sessionAccessTopic } from "../realtime/auth";
|
|
29
|
+
import { realtimeBus } from "../realtime/bus";
|
|
30
|
+
import {
|
|
31
|
+
type AgentTask,
|
|
32
|
+
AgentTaskSchema,
|
|
33
|
+
AgentTaskStatusSchema,
|
|
34
|
+
SessionAccessSchema,
|
|
35
|
+
SessionOwnerSchema,
|
|
36
|
+
SessionSharesSchema,
|
|
37
|
+
SessionSharesUpdateSchema,
|
|
38
|
+
SteerModeSchema,
|
|
39
|
+
} from "../types";
|
|
19
40
|
import { getRequestAuth } from "../utils/request-auth-context";
|
|
20
41
|
import { isRuntimePoolsEnabled } from "../utils/runtime-pools-flag";
|
|
21
42
|
import { route } from "./route-def";
|
|
43
|
+
import { resolveSessionActor, type SessionActor } from "./session-access";
|
|
22
44
|
import { jsonError } from "./utils";
|
|
23
45
|
|
|
24
46
|
// ─── Response Schemas ────────────────────────────────────────────────────────
|
|
@@ -91,6 +113,9 @@ const mintSessionTokenRoute = route({
|
|
|
91
113
|
method: "post",
|
|
92
114
|
path: "/api/sessions/tokens",
|
|
93
115
|
pattern: ["api", "sessions", "tokens"],
|
|
116
|
+
session: {
|
|
117
|
+
none: "user tokens cannot mint: the handler admits only the operator key or the task's own runtime",
|
|
118
|
+
},
|
|
94
119
|
summary: "Mint an ephemeral task or runtime token",
|
|
95
120
|
description:
|
|
96
121
|
"Operators can mint task or runtime tokens. Runtime tokens can mint task tokens for their own assigned tasks. Child tokens expire with their parent runtime.",
|
|
@@ -175,6 +200,14 @@ const listSessions = route({
|
|
|
175
200
|
contextKeyPrefix: z.string().min(1).optional(),
|
|
176
201
|
/** `full` restores the legacy shape (full root `AgentTask`); default is slim. */
|
|
177
202
|
fields: z.enum(["full", "slim"]).optional(),
|
|
203
|
+
/**
|
|
204
|
+
* `mine`: sessions the caller owns. `shared`: sessions someone else owns
|
|
205
|
+
* with an active share to the caller or to everyone. Omit for every
|
|
206
|
+
* session the caller can open. Callers that act for no user (agents, the
|
|
207
|
+
* operator key with no picked dashboard user) see every session, and
|
|
208
|
+
* an empty list for either scope.
|
|
209
|
+
*/
|
|
210
|
+
scope: z.enum(["mine", "shared"]).optional(),
|
|
178
211
|
}),
|
|
179
212
|
responses: {
|
|
180
213
|
200: {
|
|
@@ -195,23 +228,78 @@ const getSession = route({
|
|
|
195
228
|
method: "get",
|
|
196
229
|
path: "/api/sessions/{rootTaskId}",
|
|
197
230
|
pattern: ["api", "sessions", null],
|
|
231
|
+
session: { taskParam: "rootTaskId" },
|
|
198
232
|
summary: "Get a session — root task + the entire descendant chain",
|
|
199
233
|
tags: ["Sessions"],
|
|
200
234
|
params: z.object({ rootTaskId: z.string() }),
|
|
201
235
|
responses: {
|
|
202
236
|
200: {
|
|
203
|
-
description:
|
|
237
|
+
description:
|
|
238
|
+
"Root task + chain (ordered by createdAt), plus the caller's `access` and the session `owner`",
|
|
204
239
|
schema: z.object({
|
|
205
240
|
root: TaskWithSteeringSchema,
|
|
206
241
|
chain: z.array(TaskWithSteeringSchema),
|
|
242
|
+
access: SessionAccessSchema,
|
|
243
|
+
owner: SessionOwnerSchema.nullable(),
|
|
207
244
|
}),
|
|
208
245
|
},
|
|
209
246
|
401: { description: "Unauthorized" },
|
|
247
|
+
403: { description: "The caller has no access to this session" },
|
|
248
|
+
404: { description: "Root task not found" },
|
|
249
|
+
},
|
|
250
|
+
auth: { apiKey: true },
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const getSessionShares = route({
|
|
254
|
+
method: "get",
|
|
255
|
+
path: "/api/sessions/{rootTaskId}/shares",
|
|
256
|
+
pattern: ["api", "sessions", null, "shares"],
|
|
257
|
+
session: { taskParam: "rootTaskId" },
|
|
258
|
+
summary: "List a session's active shares",
|
|
259
|
+
description:
|
|
260
|
+
"Active shares only. `everyone` is the access every user has; `users` are per-person shares. A session with no shares is private to its owner.",
|
|
261
|
+
tags: ["Sessions"],
|
|
262
|
+
params: z.object({ rootTaskId: z.string() }),
|
|
263
|
+
responses: {
|
|
264
|
+
200: { description: "Active shares", schema: SessionSharesSchema },
|
|
265
|
+
401: { description: "Unauthorized" },
|
|
266
|
+
403: { description: "The caller has no access to this session" },
|
|
267
|
+
404: { description: "Root task not found" },
|
|
268
|
+
},
|
|
269
|
+
auth: { apiKey: true },
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const putSessionShares = route({
|
|
273
|
+
method: "put",
|
|
274
|
+
path: "/api/sessions/{rootTaskId}/shares",
|
|
275
|
+
pattern: ["api", "sessions", null, "shares"],
|
|
276
|
+
session: { taskParam: "rootTaskId" },
|
|
277
|
+
summary: "Set who can view or edit a session",
|
|
278
|
+
description:
|
|
279
|
+
"Owner only. The body is the full desired state: users left out lose access. Changed targets have their active share revoked and a new one inserted in one transaction; shares are never deleted.",
|
|
280
|
+
tags: ["Sessions"],
|
|
281
|
+
params: z.object({ rootTaskId: z.string() }),
|
|
282
|
+
body: SessionSharesUpdateSchema,
|
|
283
|
+
responses: {
|
|
284
|
+
200: { description: "Active shares after the change", schema: SessionSharesSchema },
|
|
285
|
+
400: { description: "Validation error or unknown user" },
|
|
286
|
+
401: { description: "Unauthorized" },
|
|
287
|
+
403: { description: "Only the session owner can share it" },
|
|
210
288
|
404: { description: "Root task not found" },
|
|
289
|
+
409: { description: "The session has no owner, so it is already open to everyone" },
|
|
211
290
|
},
|
|
212
291
|
auth: { apiKey: true },
|
|
292
|
+
rbac: { permission: "session.share" },
|
|
213
293
|
});
|
|
214
294
|
|
|
295
|
+
/** The actor's access to the session rooted at `root`; `owner` when the actor bypasses checks. */
|
|
296
|
+
async function accessForRoot(
|
|
297
|
+
actor: SessionActor,
|
|
298
|
+
root: { id: string; requestedByUserId: string | null },
|
|
299
|
+
): Promise<SessionAccess> {
|
|
300
|
+
return actor.kind === "bypass" ? "owner" : sessionAccessForRoot(actor.userId, root);
|
|
301
|
+
}
|
|
302
|
+
|
|
215
303
|
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
216
304
|
|
|
217
305
|
export async function handleSessions(
|
|
@@ -219,6 +307,7 @@ export async function handleSessions(
|
|
|
219
307
|
res: ServerResponse,
|
|
220
308
|
pathSegments: string[],
|
|
221
309
|
queryParams: URLSearchParams,
|
|
310
|
+
myAgentId?: string,
|
|
222
311
|
): Promise<boolean> {
|
|
223
312
|
if (mintSessionTokenRoute.match(req.method, pathSegments)) {
|
|
224
313
|
const auth = getRequestAuth(req);
|
|
@@ -312,13 +401,30 @@ export async function handleSessions(
|
|
|
312
401
|
.map((s) => s.trim())
|
|
313
402
|
.filter(Boolean)
|
|
314
403
|
: undefined;
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
404
|
+
const actor = await resolveSessionActor(req, myAgentId);
|
|
405
|
+
if (actor.kind === "bypass" && parsed.query.scope) {
|
|
406
|
+
// No user to own or receive a share.
|
|
407
|
+
listSessions.respond(res, 200, {
|
|
408
|
+
sessions: [],
|
|
409
|
+
total: 0,
|
|
410
|
+
limit: parsed.query.limit ?? 25,
|
|
411
|
+
offset: parsed.query.offset ?? 0,
|
|
412
|
+
});
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
const filterOpts = {
|
|
318
416
|
source: sources,
|
|
319
417
|
q: parsed.query.q,
|
|
320
418
|
requestedByUserId: parsed.query.requestedByUserId,
|
|
321
419
|
contextKeyPrefix: parsed.query.contextKeyPrefix,
|
|
420
|
+
...(actor.kind === "user"
|
|
421
|
+
? { viewer: { userId: actor.userId, scope: parsed.query.scope ?? "all" } as const }
|
|
422
|
+
: {}),
|
|
423
|
+
};
|
|
424
|
+
const baseOpts = {
|
|
425
|
+
...filterOpts,
|
|
426
|
+
limit: parsed.query.limit,
|
|
427
|
+
offset: parsed.query.offset,
|
|
322
428
|
};
|
|
323
429
|
// List responses default to slim (root is a task summary); `?fields=full` restores it.
|
|
324
430
|
const sessions =
|
|
@@ -327,12 +433,7 @@ export async function handleSessions(
|
|
|
327
433
|
: await listRecentSessions({ ...baseOpts, slim: true });
|
|
328
434
|
// Filter-aware total: same WHERE as the list query, so the UI pager
|
|
329
435
|
// reflects the filtered result set.
|
|
330
|
-
const total = await countSessions(
|
|
331
|
-
source: sources,
|
|
332
|
-
q: parsed.query.q,
|
|
333
|
-
requestedByUserId: parsed.query.requestedByUserId,
|
|
334
|
-
contextKeyPrefix: parsed.query.contextKeyPrefix,
|
|
335
|
-
});
|
|
436
|
+
const total = await countSessions(filterOpts);
|
|
336
437
|
listSessions.respond(res, 200, {
|
|
337
438
|
sessions,
|
|
338
439
|
total,
|
|
@@ -350,6 +451,25 @@ export async function handleSessions(
|
|
|
350
451
|
jsonError(res, "Root task not found", 404);
|
|
351
452
|
return true;
|
|
352
453
|
}
|
|
454
|
+
// The route accepts any task id; access follows the topmost root.
|
|
455
|
+
const sessionRoot = (await getSessionRoot(root.id)) ?? {
|
|
456
|
+
id: root.id,
|
|
457
|
+
requestedByUserId: root.requestedByUserId ?? null,
|
|
458
|
+
};
|
|
459
|
+
const access = await accessForRoot(await resolveSessionActor(req, myAgentId), sessionRoot);
|
|
460
|
+
if (access === "none") {
|
|
461
|
+
jsonError(res, "You don't have access to this session. Ask the owner to share it.", 403);
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
const ownerUser = sessionRoot.requestedByUserId
|
|
465
|
+
? await findUserById(sessionRoot.requestedByUserId)
|
|
466
|
+
: null;
|
|
467
|
+
const owner = sessionRoot.requestedByUserId
|
|
468
|
+
? {
|
|
469
|
+
id: sessionRoot.requestedByUserId,
|
|
470
|
+
name: ownerUser?.name ?? sessionRoot.requestedByUserId,
|
|
471
|
+
}
|
|
472
|
+
: null;
|
|
353
473
|
const chain = await getRootTaskChain(parsed.params.rootTaskId);
|
|
354
474
|
// Fixed query count regardless of chain length: one agents lookup and one
|
|
355
475
|
// citations lookup for the whole chain (chunked past 500 ids).
|
|
@@ -363,7 +483,87 @@ export async function handleSessions(
|
|
|
363
483
|
...steering.get(task.id)!,
|
|
364
484
|
citations: citations.get(task.id)!,
|
|
365
485
|
});
|
|
366
|
-
getSession.respond(res, 200, {
|
|
486
|
+
getSession.respond(res, 200, {
|
|
487
|
+
root: decorate(root),
|
|
488
|
+
chain: chain.map(decorate),
|
|
489
|
+
access,
|
|
490
|
+
owner,
|
|
491
|
+
});
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (getSessionShares.match(req.method, pathSegments)) {
|
|
496
|
+
const parsed = await getSessionShares.parse(req, res, pathSegments, queryParams);
|
|
497
|
+
if (!parsed) return true;
|
|
498
|
+
const root = await getSessionRoot(parsed.params.rootTaskId);
|
|
499
|
+
if (!root) {
|
|
500
|
+
jsonError(res, "Root task not found", 404);
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
const access = await accessForRoot(await resolveSessionActor(req, myAgentId), root);
|
|
504
|
+
if (access === "none") {
|
|
505
|
+
jsonError(res, "You don't have access to this session. Ask the owner to share it.", 403);
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
getSessionShares.respond(
|
|
509
|
+
res,
|
|
510
|
+
200,
|
|
511
|
+
toSessionSharesState(await listActiveSessionShares(root.id)),
|
|
512
|
+
);
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (putSessionShares.match(req.method, pathSegments)) {
|
|
517
|
+
const parsed = await putSessionShares.parse(req, res, pathSegments, queryParams);
|
|
518
|
+
if (!parsed) return true;
|
|
519
|
+
const auth = getRequestAuth(req);
|
|
520
|
+
const actor = await resolveSessionActor(req, myAgentId);
|
|
521
|
+
// Sharing is a dashboard action: a person, or the operator key itself.
|
|
522
|
+
const principal =
|
|
523
|
+
auth?.kind === "user"
|
|
524
|
+
? ({ kind: "user", userId: auth.userId } as const)
|
|
525
|
+
: auth?.kind === "operator" && !myAgentId
|
|
526
|
+
? ({ kind: "operator" } as const)
|
|
527
|
+
: null;
|
|
528
|
+
if (
|
|
529
|
+
!principal ||
|
|
530
|
+
!can({ principal, verb: "session.share", resource: { kind: "none" }, source: "http" }).allow
|
|
531
|
+
) {
|
|
532
|
+
jsonError(res, "Only the session owner can share it", 403);
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
const root = await getSessionRoot(parsed.params.rootTaskId);
|
|
536
|
+
if (!root) {
|
|
537
|
+
jsonError(res, "Root task not found", 404);
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
if (!root.requestedByUserId) {
|
|
541
|
+
jsonError(res, "This session has no owner, so it is already open to everyone", 409);
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
if (actor.kind === "user" && actor.userId !== root.requestedByUserId) {
|
|
545
|
+
jsonError(res, "Only the session owner can share it", 403);
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
const userIds = [...new Set(parsed.body.users.map((user) => user.userId))];
|
|
549
|
+
if (userIds.length !== parsed.body.users.length) {
|
|
550
|
+
jsonError(res, "Each user may appear only once", 400);
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
for (const userId of userIds) {
|
|
554
|
+
if (!(await findUserById(userId))) {
|
|
555
|
+
jsonError(res, `Unknown user ${userId}`, 400);
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const state = await setSessionShares(
|
|
560
|
+
root,
|
|
561
|
+
parsed.body,
|
|
562
|
+
actor.kind === "user" ? actor.userId : null,
|
|
563
|
+
);
|
|
564
|
+
// Committed: presence sockets re-check access, so a revoked viewer leaves.
|
|
565
|
+
realtimeBus.publish(sessionAccessTopic(root.id), null);
|
|
566
|
+
putSessionShares.respond(res, 200, state);
|
|
367
567
|
return true;
|
|
368
568
|
}
|
|
369
569
|
|
package/src/http/stats.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { isMultiRuntimeEnabled } from "../utils/multi-runtime";
|
|
|
19
19
|
import { resolveHttpFavoriteOwner } from "./favorite-owner";
|
|
20
20
|
import { route } from "./route-def";
|
|
21
21
|
import { scheduleSummaryWithFavoriteSchema } from "./schedules";
|
|
22
|
+
import { readableByFor, resolveSessionActor } from "./session-access";
|
|
22
23
|
|
|
23
24
|
// ─── Response schemas ────────────────────────────────────────────────────────
|
|
24
25
|
|
|
@@ -222,11 +223,12 @@ export async function handleStats(
|
|
|
222
223
|
if (!parsed) return true;
|
|
223
224
|
const limit = parsed.query.limit ?? 100;
|
|
224
225
|
const agentId = parsed.query.agentId;
|
|
226
|
+
const readableBy = readableByFor(await resolveSessionActor(req, myAgentId));
|
|
225
227
|
let logs: AgentLog[] = [];
|
|
226
228
|
if (agentId) {
|
|
227
|
-
logs = (await getLogsByAgentId(agentId)).slice(0, limit);
|
|
229
|
+
logs = (await getLogsByAgentId(agentId, readableBy)).slice(0, limit);
|
|
228
230
|
} else {
|
|
229
|
-
logs = await getAllLogs(limit);
|
|
231
|
+
logs = await getAllLogs(limit, readableBy);
|
|
230
232
|
}
|
|
231
233
|
listLogs.respond(res, 200, { logs });
|
|
232
234
|
return true;
|
|
@@ -306,7 +308,9 @@ export async function handleStats(
|
|
|
306
308
|
}
|
|
307
309
|
|
|
308
310
|
if (getConcurrentContextRoute.match(req.method, pathSegments)) {
|
|
309
|
-
const context = await getConcurrentContext(
|
|
311
|
+
const context = await getConcurrentContext(
|
|
312
|
+
readableByFor(await resolveSessionActor(req, myAgentId)),
|
|
313
|
+
);
|
|
310
314
|
getConcurrentContextRoute.respond(res, 200, context);
|
|
311
315
|
return true;
|
|
312
316
|
}
|
package/src/http/tasks.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
} from "../be/db";
|
|
38
38
|
import { modelProfileRefError, taskProfileError } from "../be/db/model-profiles";
|
|
39
39
|
import { explicitModelErrorForAgent } from "../be/model-validation";
|
|
40
|
+
import { hasSessionAccess, type SessionAccess } from "../be/session-shares";
|
|
40
41
|
import {
|
|
41
42
|
getTaskSteeringFields,
|
|
42
43
|
markSteeringUndeliverable,
|
|
@@ -80,7 +81,15 @@ import { getRequestAuth } from "../utils/request-auth-context";
|
|
|
80
81
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
81
82
|
import { rejectGuest } from "./request-principal";
|
|
82
83
|
import { buildTaskRequestedBy, TaskRequestedBySchema } from "./requested-by";
|
|
83
|
-
import { route } from "./route-def";
|
|
84
|
+
import { bodyTask, route } from "./route-def";
|
|
85
|
+
import {
|
|
86
|
+
canReadSession,
|
|
87
|
+
readableByFor,
|
|
88
|
+
resolveSessionActor,
|
|
89
|
+
SESSION_READ_DENIED,
|
|
90
|
+
SESSION_WRITE_DENIED,
|
|
91
|
+
sessionAccessFor,
|
|
92
|
+
} from "./session-access";
|
|
84
93
|
import { jsonError, parseBody } from "./utils";
|
|
85
94
|
|
|
86
95
|
// ─── Response Schemas ────────────────────────────────────────────────────────
|
|
@@ -207,7 +216,7 @@ const listTasks = route({
|
|
|
207
216
|
pattern: ["api", "tasks"],
|
|
208
217
|
summary: "List tasks with filters",
|
|
209
218
|
description:
|
|
210
|
-
"Returns tasks with the full `task` text replaced by a bounded `taskPreview` and completion/integration blobs dropped by default — list views only need the preview. Pass `fields=full` to restore the full `AgentTask`, or `fields=timeline` for the narrow shape the dashboard timeline draws. Fetch a single task in full via `GET /api/tasks/{id}`. `total` (the filtered row count, ignoring limit/offset) is computed only with `includeTotal=true`.",
|
|
219
|
+
"Returns tasks with the full `task` text replaced by a bounded `taskPreview` and completion/integration blobs dropped by default — list views only need the preview. Pass `fields=full` to restore the full `AgentTask`, or `fields=timeline` for the narrow shape the dashboard timeline draws. Every shape gives a user caller only tasks of sessions they can read. Fetch a single task in full via `GET /api/tasks/{id}`. `total` (the filtered row count, ignoring limit/offset) is computed only with `includeTotal=true`.",
|
|
211
220
|
tags: ["Tasks"],
|
|
212
221
|
query: z.object({
|
|
213
222
|
/** Single status, or comma-separated list (e.g. "failed,cancelled"). */
|
|
@@ -263,6 +272,7 @@ const createTask = route({
|
|
|
263
272
|
method: "post",
|
|
264
273
|
path: "/api/tasks",
|
|
265
274
|
pattern: ["api", "tasks"],
|
|
275
|
+
session: bodyTask("parentTaskId"),
|
|
266
276
|
summary: "Create a new task",
|
|
267
277
|
tags: ["Tasks"],
|
|
268
278
|
body: z
|
|
@@ -340,6 +350,7 @@ const promoteDraftTaskRoute = route({
|
|
|
340
350
|
method: "post",
|
|
341
351
|
path: "/api/tasks/{id}/promote-draft",
|
|
342
352
|
pattern: ["api", "tasks", null, "promote-draft"],
|
|
353
|
+
session: { taskParam: "id" },
|
|
343
354
|
summary: "Promote a draft task out of the pre-dispatch draft state",
|
|
344
355
|
description:
|
|
345
356
|
"Transitions a `draft` task (#1240 — created with attachments still uploading) to its normal dispatch-eligible status: `offered` if it was offered to an agent, `pending` if it has an owning agent (the common case — UI-composer tasks default to Lead), otherwise `unassigned`. Called by the UI composer once its attachment upload batch settles, whether every file uploaded, some failed, or all failed — a draft must never be stranded by an upload error. Idempotent: calling it on a task that already left `draft` returns the current task unchanged rather than erroring, so a retried request is safe.",
|
|
@@ -357,6 +368,7 @@ const updateSession = route({
|
|
|
357
368
|
method: "put",
|
|
358
369
|
path: "/api/tasks/{id}/session",
|
|
359
370
|
pattern: ["api", "tasks", null, "session"],
|
|
371
|
+
session: { taskParam: "id" },
|
|
360
372
|
summary: "Update provider session ID and harness metadata for a task",
|
|
361
373
|
tags: ["Tasks"],
|
|
362
374
|
params: z.object({ id: z.string() }),
|
|
@@ -392,6 +404,7 @@ const cancelTaskRoute = route({
|
|
|
392
404
|
method: "post",
|
|
393
405
|
path: "/api/tasks/{id}/cancel",
|
|
394
406
|
pattern: ["api", "tasks", null, "cancel"],
|
|
407
|
+
session: { taskParam: "id" },
|
|
395
408
|
summary: "Cancel a pending or in-progress task",
|
|
396
409
|
tags: ["Tasks"],
|
|
397
410
|
params: z.object({ id: z.string() }),
|
|
@@ -407,6 +420,7 @@ const steerTaskRoute = route({
|
|
|
407
420
|
method: "post",
|
|
408
421
|
path: "/api/tasks/{id}/steer",
|
|
409
422
|
pattern: ["api", "tasks", null, "steer"],
|
|
423
|
+
session: { taskParam: "id" },
|
|
410
424
|
summary: "Deliver a steering message to a running task",
|
|
411
425
|
tags: ["Tasks"],
|
|
412
426
|
params: z.object({ id: z.string() }),
|
|
@@ -439,6 +453,7 @@ const getTaskSteeringMessagesRoute = route({
|
|
|
439
453
|
method: "get",
|
|
440
454
|
path: "/api/tasks/{id}/steering-messages",
|
|
441
455
|
pattern: ["api", "tasks", null, "steering-messages"],
|
|
456
|
+
session: { taskParam: "id" },
|
|
442
457
|
summary: "List steering messages for a task",
|
|
443
458
|
tags: ["Tasks"],
|
|
444
459
|
params: z.object({ id: z.string() }),
|
|
@@ -467,6 +482,7 @@ const markSteeringDeliveredRoute = route({
|
|
|
467
482
|
method: "post",
|
|
468
483
|
path: "/api/steering-messages/{id}/delivered",
|
|
469
484
|
pattern: ["api", "steering-messages", null, "delivered"],
|
|
485
|
+
session: { steeringParam: "id" },
|
|
470
486
|
summary: "Mark a steering message delivered",
|
|
471
487
|
tags: ["Tasks"],
|
|
472
488
|
params: z.object({ id: z.string() }),
|
|
@@ -492,6 +508,7 @@ const markSteeringHandledRoute = route({
|
|
|
492
508
|
method: "post",
|
|
493
509
|
path: "/api/steering-messages/{id}/handled",
|
|
494
510
|
pattern: ["api", "steering-messages", null, "handled"],
|
|
511
|
+
session: { steeringParam: "id" },
|
|
495
512
|
summary: "Mark a steering message handled",
|
|
496
513
|
description:
|
|
497
514
|
"Optionally accepts a JSON body `{ note?: string }` — a short acceptance note describing how the steering was incorporated, persisted as `handledNote`.",
|
|
@@ -517,6 +534,7 @@ const markSteeringUndeliverableRoute = route({
|
|
|
517
534
|
method: "post",
|
|
518
535
|
path: "/api/steering-messages/{id}/undeliverable",
|
|
519
536
|
pattern: ["api", "steering-messages", null, "undeliverable"],
|
|
537
|
+
session: { steeringParam: "id" },
|
|
520
538
|
summary: "Promote an undeliverable steering message",
|
|
521
539
|
tags: ["Tasks"],
|
|
522
540
|
params: z.object({ id: z.string() }),
|
|
@@ -542,6 +560,7 @@ const getTask = route({
|
|
|
542
560
|
method: "get",
|
|
543
561
|
path: "/api/tasks/{id}",
|
|
544
562
|
pattern: ["api", "tasks", null],
|
|
563
|
+
session: { taskParam: "id" },
|
|
545
564
|
summary: "Get task details with logs and attachments",
|
|
546
565
|
description:
|
|
547
566
|
"Returns the full `AgentTask` row decorated with `logs` (capped by `logsLimit`) and `attachments` (pointer-based artifacts stored on the task, ordered by `created_at`).",
|
|
@@ -553,6 +572,7 @@ const getTask = route({
|
|
|
553
572
|
}),
|
|
554
573
|
responses: {
|
|
555
574
|
200: { description: "Task with logs and attachments", schema: GetTaskResponseSchema },
|
|
575
|
+
403: { description: "A user caller cannot read the task's session" },
|
|
556
576
|
404: { description: "Task not found" },
|
|
557
577
|
},
|
|
558
578
|
});
|
|
@@ -561,6 +581,7 @@ const updateTaskProgressRoute = route({
|
|
|
561
581
|
method: "post",
|
|
562
582
|
path: "/api/tasks/{id}/progress",
|
|
563
583
|
pattern: ["api", "tasks", null, "progress"],
|
|
584
|
+
session: { taskParam: "id" },
|
|
564
585
|
summary: "Update task progress text",
|
|
565
586
|
tags: ["Tasks"],
|
|
566
587
|
params: z.object({ id: z.string() }),
|
|
@@ -579,6 +600,7 @@ const finishTask = route({
|
|
|
579
600
|
method: "post",
|
|
580
601
|
path: "/api/tasks/{id}/finish",
|
|
581
602
|
pattern: ["api", "tasks", null, "finish"],
|
|
603
|
+
session: { taskParam: "id" },
|
|
582
604
|
summary: "Mark task as completed or failed (runner endpoint)",
|
|
583
605
|
tags: ["Tasks"],
|
|
584
606
|
params: z.object({ id: z.string() }),
|
|
@@ -631,6 +653,7 @@ const pauseTaskRoute = route({
|
|
|
631
653
|
method: "post",
|
|
632
654
|
path: "/api/tasks/{id}/pause",
|
|
633
655
|
pattern: ["api", "tasks", null, "pause"],
|
|
656
|
+
session: { taskParam: "id" },
|
|
634
657
|
summary: "Pause an in-progress task",
|
|
635
658
|
tags: ["Tasks"],
|
|
636
659
|
params: z.object({ id: z.string() }),
|
|
@@ -646,6 +669,7 @@ const resumeTaskRoute = route({
|
|
|
646
669
|
method: "post",
|
|
647
670
|
path: "/api/tasks/{id}/resume",
|
|
648
671
|
pattern: ["api", "tasks", null, "resume"],
|
|
672
|
+
session: { taskParam: "id" },
|
|
649
673
|
summary: "Resume a paused task",
|
|
650
674
|
tags: ["Tasks"],
|
|
651
675
|
params: z.object({ id: z.string() }),
|
|
@@ -661,6 +685,7 @@ const supersedeTaskRoute = route({
|
|
|
661
685
|
method: "post",
|
|
662
686
|
path: "/api/tasks/{id}/supersede",
|
|
663
687
|
pattern: ["api", "tasks", null, "supersede"],
|
|
688
|
+
session: { taskParam: "id" },
|
|
664
689
|
summary: "Supersede an in-progress task (terminate + spawn resume follow-up)",
|
|
665
690
|
description:
|
|
666
691
|
'Marks the original task `superseded` (terminal) and creates a fresh `taskType="resume"` follow-up so a worker can pick up the work in a new provider session. Workflow-step tasks (those with `workflowRunStepId`) are carved out: the original is marked `failed` with reason `superseded_workflow_task` and no follow-up is created — the workflow engine\'s retry/failure policy applies.',
|
|
@@ -683,6 +708,7 @@ const updateTaskVcsRoute = route({
|
|
|
683
708
|
method: "patch",
|
|
684
709
|
path: "/api/tasks/{id}/vcs",
|
|
685
710
|
pattern: ["api", "tasks", null, "vcs"],
|
|
711
|
+
session: { taskParam: "id" },
|
|
686
712
|
summary: "Update VCS (PR/MR) info for a task",
|
|
687
713
|
tags: ["Tasks"],
|
|
688
714
|
params: z.object({ id: z.string() }),
|
|
@@ -704,6 +730,7 @@ const updateTaskTitleRoute = route({
|
|
|
704
730
|
method: "patch",
|
|
705
731
|
path: "/api/tasks/{id}/title",
|
|
706
732
|
pattern: ["api", "tasks", null, "title"],
|
|
733
|
+
session: { taskParam: "id" },
|
|
707
734
|
summary: "Set or clear a task's display title (session rename)",
|
|
708
735
|
description:
|
|
709
736
|
"Sets a human-facing display title override on a task. The sessions UI only reads this from root tasks (session list items), but titles on child tasks are harmless. Pass `title: null` (or an empty string) to clear the override and fall back to the task prompt.",
|
|
@@ -722,22 +749,37 @@ const updateTaskTitleRoute = route({
|
|
|
722
749
|
},
|
|
723
750
|
});
|
|
724
751
|
|
|
752
|
+
/** True unless the request acts for a user who lacks write access to the task's session. */
|
|
753
|
+
async function hasSessionWrite(
|
|
754
|
+
req: IncomingMessage,
|
|
755
|
+
myAgentId: string | undefined,
|
|
756
|
+
taskId: string,
|
|
757
|
+
): Promise<boolean> {
|
|
758
|
+
const access = await sessionAccessFor(await resolveSessionActor(req, myAgentId), taskId);
|
|
759
|
+
return hasSessionAccess(access, "write");
|
|
760
|
+
}
|
|
761
|
+
|
|
725
762
|
async function canSteerTask(
|
|
726
763
|
req: IncomingMessage,
|
|
727
764
|
myAgentId: string | undefined,
|
|
728
765
|
task: AgentTask,
|
|
766
|
+
sessionAccess: SessionAccess = "none",
|
|
729
767
|
): Promise<boolean> {
|
|
768
|
+
const auth = getRequestAuth(req);
|
|
730
769
|
const resource: RbacResource = {
|
|
731
770
|
kind: "task",
|
|
732
771
|
taskId: task.id,
|
|
733
|
-
|
|
772
|
+
// A user with a write share steers as if the session were their own; the
|
|
773
|
+
// verb gate (and any role grant) still applies. Only an actual share
|
|
774
|
+
// counts: `owner` from an ownerless root must not widen steering.
|
|
775
|
+
requestedByUserId:
|
|
776
|
+
auth?.kind === "user" && sessionAccess === "write" ? auth.userId : task.requestedByUserId,
|
|
734
777
|
creatorAgentId: task.creatorAgentId,
|
|
735
778
|
agentId: task.agentId,
|
|
736
779
|
};
|
|
737
780
|
let principal: RbacPrincipal;
|
|
738
781
|
let verb: "task.steer.any" | "task.steer.own";
|
|
739
782
|
|
|
740
|
-
const auth = getRequestAuth(req);
|
|
741
783
|
if (auth?.kind === "operator") {
|
|
742
784
|
principal = { kind: "operator" };
|
|
743
785
|
verb = "task.steer.own";
|
|
@@ -906,6 +948,9 @@ export async function handleTasks(
|
|
|
906
948
|
if (validated.length > 0) source = validated;
|
|
907
949
|
}
|
|
908
950
|
|
|
951
|
+
// Every shape carries task text, so a user sees only sessions they can
|
|
952
|
+
// read, and `total` counts the same rows.
|
|
953
|
+
const actor = await resolveSessionActor(req, myAgentId);
|
|
909
954
|
const filters = {
|
|
910
955
|
status,
|
|
911
956
|
agentId: parsed.query.agentId || undefined,
|
|
@@ -925,6 +970,7 @@ export async function handleTasks(
|
|
|
925
970
|
orderBy: parsed.query.orderBy,
|
|
926
971
|
limit: parsed.query.limit,
|
|
927
972
|
offset: parsed.query.offset,
|
|
973
|
+
readableBy: actor.kind === "user" ? { userId: actor.userId } : undefined,
|
|
928
974
|
};
|
|
929
975
|
// List responses default to slim (full `task` text → bounded `taskPreview`,
|
|
930
976
|
// heavy blobs dropped); `?fields=full` restores the full `AgentTask`.
|
|
@@ -1013,6 +1059,11 @@ export async function handleTasks(
|
|
|
1013
1059
|
jsonError(res, `You don't have context for parent task ${parentTask.id}.`, 403);
|
|
1014
1060
|
return true;
|
|
1015
1061
|
}
|
|
1062
|
+
// A follow-up in someone else's session needs a write share.
|
|
1063
|
+
if (parentTask && !(await hasSessionWrite(req, myAgentId, parentTask.id))) {
|
|
1064
|
+
jsonError(res, SESSION_WRITE_DENIED, 403);
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1016
1067
|
// A chat-sdk contextKey binds the task to a chat thread that its assignee and creator
|
|
1017
1068
|
// may then read and post to. An agent may only name a thread other than the parent's
|
|
1018
1069
|
// with the lead's chat post permission.
|
|
@@ -1285,7 +1336,13 @@ export async function handleTasks(
|
|
|
1285
1336
|
jsonError(res, "Task not found", 404);
|
|
1286
1337
|
return true;
|
|
1287
1338
|
}
|
|
1288
|
-
|
|
1339
|
+
const sessionActor = await resolveSessionActor(req, myAgentId);
|
|
1340
|
+
const steerAccess = await sessionAccessFor(sessionActor, task.id);
|
|
1341
|
+
if (!hasSessionAccess(steerAccess, "write")) {
|
|
1342
|
+
jsonError(res, SESSION_WRITE_DENIED, 403);
|
|
1343
|
+
return true;
|
|
1344
|
+
}
|
|
1345
|
+
if (!(await canSteerTask(req, myAgentId, task, steerAccess))) {
|
|
1289
1346
|
jsonError(res, "Forbidden: caller cannot steer this task", 403);
|
|
1290
1347
|
return true;
|
|
1291
1348
|
}
|
|
@@ -1338,6 +1395,10 @@ export async function handleTasks(
|
|
|
1338
1395
|
jsonError(res, "Task not found", 404);
|
|
1339
1396
|
return true;
|
|
1340
1397
|
}
|
|
1398
|
+
if (!(await canReadSession(req, myAgentId, parsed.params.id))) {
|
|
1399
|
+
jsonError(res, SESSION_READ_DENIED, 403);
|
|
1400
|
+
return true;
|
|
1401
|
+
}
|
|
1341
1402
|
getTaskSteeringMessagesRoute.respond(res, 200, {
|
|
1342
1403
|
messages: await getSteeringMessagesForTask(parsed.params.id),
|
|
1343
1404
|
});
|
|
@@ -1357,7 +1418,12 @@ export async function handleTasks(
|
|
|
1357
1418
|
return true;
|
|
1358
1419
|
}
|
|
1359
1420
|
|
|
1421
|
+
// A user token naming an agent in X-Agent-ID still reads only its sessions.
|
|
1360
1422
|
if (parsed.query.taskId) {
|
|
1423
|
+
if (!(await canReadSession(req, myAgentId, parsed.query.taskId))) {
|
|
1424
|
+
jsonError(res, SESSION_READ_DENIED, 403);
|
|
1425
|
+
return true;
|
|
1426
|
+
}
|
|
1361
1427
|
const task = await getTaskById(parsed.query.taskId);
|
|
1362
1428
|
const messages =
|
|
1363
1429
|
task?.agentId === myAgentId ? await getPendingSteeringForTask(parsed.query.taskId) : [];
|
|
@@ -1365,8 +1431,9 @@ export async function handleTasks(
|
|
|
1365
1431
|
return true;
|
|
1366
1432
|
}
|
|
1367
1433
|
|
|
1434
|
+
const actor = await resolveSessionActor(req, myAgentId);
|
|
1368
1435
|
getPendingSteeringMessagesRoute.respond(res, 200, {
|
|
1369
|
-
messages: await getPendingSteeringForAgent(myAgentId),
|
|
1436
|
+
messages: await getPendingSteeringForAgent(myAgentId, readableByFor(actor)),
|
|
1370
1437
|
});
|
|
1371
1438
|
return true;
|
|
1372
1439
|
}
|
|
@@ -1529,6 +1596,11 @@ export async function handleTasks(
|
|
|
1529
1596
|
jsonError(res, "Task not found", 404);
|
|
1530
1597
|
return true;
|
|
1531
1598
|
}
|
|
1599
|
+
const readAccess = await sessionAccessFor(await resolveSessionActor(req, myAgentId), task.id);
|
|
1600
|
+
if (!hasSessionAccess(readAccess, "read")) {
|
|
1601
|
+
jsonError(res, "You don't have access to this session. Ask the owner to share it.", 403);
|
|
1602
|
+
return true;
|
|
1603
|
+
}
|
|
1532
1604
|
|
|
1533
1605
|
const logs = await getLogsByTaskId(parsed.params.id, parsed.query.logsLimit ?? 200);
|
|
1534
1606
|
const attachments = await getTaskAttachments(parsed.params.id);
|
|
@@ -1758,7 +1830,11 @@ export async function handleTasks(
|
|
|
1758
1830
|
jsonError(res, "Missing X-Agent-ID header", 400);
|
|
1759
1831
|
return true;
|
|
1760
1832
|
}
|
|
1761
|
-
|
|
1833
|
+
// A user token naming an agent in X-Agent-ID still reads only its sessions.
|
|
1834
|
+
const pausedTasks = await getPausedTasksForAgent(
|
|
1835
|
+
myAgentId,
|
|
1836
|
+
readableByFor(await resolveSessionActor(req, myAgentId)),
|
|
1837
|
+
);
|
|
1762
1838
|
const tasks = await Promise.all(
|
|
1763
1839
|
pausedTasks.map(async (task) => {
|
|
1764
1840
|
const requestedBy = await buildTaskRequestedBy(task);
|