@desplega.ai/agent-swarm 2.4.2 → 2.4.4
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/dist/{acp-adapter-dv7hmx4r.js → acp-adapter-thq04adh.js} +4 -5
- package/dist/{actions-51nc1gny.js → actions-j6ywh6x5.js} +18 -18
- package/dist/{amp-adapter-abp5tdx8.js → amp-adapter-k8b5xmj8.js} +3 -3
- package/dist/{app-9h14qvr6.js → app-rb7vc02s.js} +3 -3
- package/dist/{assistant-wv1gz3mt.js → assistant-ftta1zjf.js} +20 -20
- package/dist/{boot-reembed-6yn53ms9.js → boot-reembed-srp6v6af.js} +1 -1
- package/dist/{boot-reembed-7fcka4z5.js → boot-reembed-xns7h5h9.js} +1 -1
- package/dist/{boot-scrub-sweep-pm1580ax.js → boot-scrub-sweep-3qqrh2mz.js} +3 -3
- package/dist/{boot-seeds-g5hfc1x8.js → boot-seeds-9ks8hce7.js} +4 -4
- package/dist/{claude-adapter-gf3kxhf0.js → claude-adapter-k5ksyres.js} +4 -4
- package/dist/{claude-managed-setup-h00hsgtz.js → claude-managed-setup-n5a5eqks.js} +1 -1
- package/dist/{claude-sdk-session-s2k1qd8y.js → claude-sdk-session-2j3j4kwn.js} +9822 -9749
- package/dist/{cli-pkht5hap.js → cli-1d70crsy.js} +2 -2
- package/dist/{cli-r99kc9yq.js → cli-1ephgwnq.js} +3 -3
- package/dist/{cli-7225gwj7.js → cli-1m17dc9d.js} +1 -1
- package/dist/{cli-bcj753sq.js → cli-2v045xyf.js} +3 -3
- package/dist/{cli-h38rjv87.js → cli-2y91wnbh.js} +12 -7
- package/dist/{cli-sv5gb0yb.js → cli-2zqpkvtt.js} +1 -1
- package/dist/{cli-xbzvet96.js → cli-36ckw0wx.js} +5 -4
- package/dist/{cli-8sxqsbbq.js → cli-37yb7307.js} +1 -1
- package/dist/{cli-hyfw2j1e.js → cli-3cgngyd8.js} +11 -11
- package/dist/{cli-a4gvwttf.js → cli-44159k28.js} +2 -2
- package/dist/{cli-7awf1fp8.js → cli-4dg55wsk.js} +3 -3
- package/dist/{cli-mhywgrrz.js → cli-4sydckyj.js} +1 -1
- package/dist/{cli-y2x52gs2.js → cli-51y09c6p.js} +20 -17
- package/dist/{cli-ahtpy9kf.js → cli-55qmbp6z.js} +10 -5
- package/dist/{cli-pw7v69vp.js → cli-625tb7aq.js} +1 -1
- package/dist/{cli-x5mq1fyq.js → cli-74c1wpm0.js} +3 -3
- package/dist/{cli-b1dfr1m4.js → cli-7a6p4e65.js} +1 -1
- package/dist/{cli-hqhzg1qq.js → cli-82d3nkc3.js} +1 -1
- package/dist/{cli-nmqdc2t3.js → cli-9fhkkpj5.js} +78 -12
- package/dist/{cli-h93c9vb3.js → cli-amyhm3gj.js} +1 -1
- package/dist/{cli-0qvtffe9.js → cli-b8k7epj1.js} +6 -6
- package/dist/{cli-1pa3rz69.js → cli-ba8ne1es.js} +6 -1
- package/dist/{cli-g0hgtz4p.js → cli-bnz0r3kc.js} +3 -3
- package/dist/{cli-0hyhdkdk.js → cli-cebvbc9t.js} +4 -1
- package/dist/{cli-efaqv1c7.js → cli-cynvc9fj.js} +35 -7
- package/dist/{cli-8zwe9mpk.js → cli-dqb4n629.js} +10 -10
- package/dist/{cli-7a91w22y.js → cli-e8xyjvfc.js} +1 -1
- package/dist/{cli-s6vr7r2d.js → cli-esxerzg7.js} +1 -1
- package/dist/{cli-w9g4sz81.js → cli-gf2850q7.js} +1 -1
- package/dist/{cli-10hhg0km.js → cli-gqvh8d8a.js} +7 -7
- package/dist/{cli-t30yj95q.js → cli-hj3debgw.js} +1 -1
- package/dist/{cli-fpmsqq59.js → cli-nc35pxb7.js} +3 -3
- package/dist/{cli-d3qqdy4c.js → cli-p20r8w42.js} +16 -14
- package/dist/{cli-shvfedvm.js → cli-pccfmtb5.js} +5 -5
- package/dist/{cli-02qm7src.js → cli-q6r7t299.js} +1037 -299
- package/dist/{cli-2wag8973.js → cli-qeaq3j6t.js} +1 -1
- package/dist/{cli-nn47d7e8.js → cli-qkhc5aqy.js} +15 -15
- package/dist/{cli-faypxz7q.js → cli-t7cyef5g.js} +4 -4
- package/dist/{cli-z4wd2ftv.js → cli-te6b5wvg.js} +149 -184
- package/dist/{cli-5rk36sjf.js → cli-v3yr2gbg.js} +119 -44
- package/dist/{cli-239yjk27.js → cli-vdm20wej.js} +5 -5
- package/dist/{cli-zfg9enh9.js → cli-vfha21c9.js} +6 -1
- package/dist/{cli-rdyw3mct.js → cli-vq97g1jq.js} +6 -5
- package/dist/{cli-n85dqg6d.js → cli-w6nadkh4.js} +4 -4
- package/dist/{cli-99jez72r.js → cli-w9etnnxk.js} +6 -1
- package/dist/{cli-saw9a6f6.js → cli-xc27fwzg.js} +2 -2
- package/dist/{cli-ftyypbg5.js → cli-xj0re5tm.js} +1 -1
- package/dist/{cli-fyzjmseb.js → cli-zrk6jxv8.js} +1 -1
- package/dist/{cli-y8n5k515.js → cli-zrwpctp4.js} +28 -28
- package/dist/cli.js +44 -19
- package/dist/{codex-adapter-ttygw382.js → codex-adapter-968xwfy7.js} +2 -2
- package/dist/{codex-hook-x7nejyrr.js → codex-hook-e2he7aca.js} +2 -2
- package/dist/{codex-session-runner-60b58vv3.js → codex-session-runner-ht4z99fa.js} +2 -2
- package/dist/{commands-edmdwa4x.js → commands-pcymaeg9.js} +3 -3
- package/dist/{controller-m263fg43.js → controller-fpjyk46r.js} +3 -3
- package/dist/{controllers-connect-5fym8sw1.js → controllers-connect-mwzx1fh6.js} +19 -19
- package/dist/{cursor-adapter-cz6nm6mj.js → cursor-adapter-cp4s2e2f.js} +2 -2
- package/dist/{db-2y0mgstc.js → db-sgd12bt9.js} +3 -3
- package/dist/{db-nn0hcmx5.js → db-xsjg9jqq.js} +2 -2
- package/dist/{dispatcher-0a9wwjcx.js → dispatcher-x0jfw2c9.js} +17 -17
- package/dist/{dsh-adapter-3n3net33.js → dsh-adapter-jrq7ma7h.js} +2 -2
- package/dist/{e2b-thdcxtjd.js → e2b-16d6ch39.js} +1 -1
- package/dist/{e2b-7am6tmq6.js → e2b-zn0b4w5w.js} +1 -1
- package/dist/{external-channel-zhvt95n3.js → external-channel-8njj4h3b.js} +2 -2
- package/dist/{grok-adapter-qnr1hp2y.js → grok-adapter-kkr97v2f.js} +5 -6
- package/dist/handlers-0pvee9nw.js +39 -0
- package/dist/{heartbeat-gz97r7ac.js → heartbeat-wd03zhn0.js} +20 -20
- package/dist/{hook-1mmdgjqs.js → hook-f0vghj42.js} +10 -5
- package/dist/{hook-x8rthca4.js → hook-xzj2ts2p.js} +2 -2
- package/dist/{http-r8f85xr9.js → http-4hwxv62p.js} +261 -123
- package/dist/{index-va3y63es.js → index-6srwvd5b.js} +1 -2
- package/dist/{index-y7rbcrc1.js → index-97q4xn03.js} +20 -11
- package/dist/{index-s98037v0.js → index-cbxkmfc5.js} +1 -2
- package/dist/{index-vzvfqzfj.js → index-gdh8jrdm.js} +17 -17
- package/dist/{index-aex0eba9.js → index-hg0x2cdv.js} +17 -17
- package/dist/index-thm0y6a8.js +41 -0
- package/dist/{index-smxyk04s.js → index-ww2vznkp.js} +1 -1
- package/dist/{keepalive-64c0f34c.js → keepalive-hrh7xnr9.js} +3 -3
- package/dist/{lead-b1da0f97.js → lead-ee8vazz2.js} +30 -31
- package/dist/{maintenance-mnxpar12.js → maintenance-e8wnwnqx.js} +1 -1
- package/dist/{oauth-refresh-sweep-36hs2v4h.js → oauth-refresh-sweep-zm0yj4sj.js} +2 -2
- package/dist/{onboard-xes8de61.js → onboard-dtwcq603.js} +3 -3
- package/dist/{opencode-adapter-3s7ax02e.js → opencode-adapter-x4tqzhkh.js} +3 -3
- package/dist/{otel-impl-fzc57kcq.js → otel-impl-w3fbhpn0.js} +1 -1
- package/dist/{pi-mono-adapter-ytp7gz8b.js → pi-mono-adapter-mcfkn9vf.js} +3 -3
- package/dist/{pricing-refresh-1nek2w7b.js → pricing-refresh-j38dehfs.js} +3 -3
- package/dist/{rbac-roles-ttyfaep2.js → rbac-roles-ae8yttq4.js} +3 -3
- package/dist/{rbac-roles-sqefb5hw.js → rbac-roles-ejxx9b35.js} +2 -2
- package/dist/{render-v2-vb527jwn.js → render-v2-n16n0phe.js} +3 -3
- package/dist/{resume-jcdzg1bn.js → resume-z0fgjc38.js} +5 -5
- package/dist/{session-access-zsp4wsp0.js → session-access-cv3mq6mc.js} +6 -4
- package/dist/{setup-1nb2zqd3.js → setup-mafhj3yt.js} +1 -1
- package/dist/{work-objects-61w08fwr.js → work-objects-8b86mgjp.js} +21 -21
- package/dist/{worker-c1e68hpa.js → worker-16s75hm5.js} +30 -31
- package/dist/worker-token-xf11yyds.js +136 -0
- package/openapi.json +295 -12
- package/package.json +5 -5
- package/src/apps/action-runner.ts +4 -0
- package/src/apps/catalog.generated.json +3 -0
- package/src/apps/page-validator.ts +8 -2
- package/src/be/db.ts +7 -4
- package/src/be/migrations/231_slack_tasks_last_updated_index.sql +5 -0
- package/src/be/migrations/232_agent_session_tokens.sql +41 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +10 -1
- package/src/be/swarm-config-guard.ts +16 -0
- package/src/be/users.ts +169 -12
- package/src/cli.tsx +24 -0
- package/src/commands/worker-token.ts +164 -0
- package/src/github/handlers.ts +6 -5
- package/src/hooks/hook.ts +2 -1
- package/src/http/apps.ts +43 -5
- package/src/http/codex-oauth-keep-warm.ts +27 -9
- package/src/http/config.ts +5 -3
- package/src/http/core.ts +2 -0
- package/src/http/session-access.ts +51 -2
- package/src/http/sessions.ts +249 -10
- package/src/http/tasks.ts +4 -32
- package/src/providers/acp-adapter.ts +33 -13
- package/src/providers/acp-swarm-events.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +4 -4
- package/src/providers/codex-oauth/storage.ts +1 -1
- package/src/providers/opencode-adapter.ts +2 -2
- package/src/providers/pi-mono-extension.ts +2 -1
- package/src/rbac/legacy-policy.ts +3 -1
- package/src/rbac/permissions.ts +4 -0
- package/src/tests/acp-adapter.test.ts +10 -23
- package/src/tests/acp-fake-agent.ts +36 -0
- package/src/tests/acp-swarm-events.test.ts +1 -1
- package/src/tests/agent-session-tokens.test.ts +452 -0
- package/src/tests/apps-action-parent-task.test.ts +295 -0
- package/src/tests/claude-stop-hook.test.ts +47 -0
- package/src/tests/codex-adapter.test.ts +42 -0
- package/src/tests/codex-oauth-keep-warm.test.ts +85 -1
- package/src/tests/config-session-auth.test.ts +14 -0
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/env-flag.test.ts +15 -1
- package/src/tests/github-handlers-cancel-config.test.ts +29 -8
- package/src/tests/github-handlers-inline-comments.test.ts +9 -2
- package/src/tests/grok-adapter.test.ts +3 -3
- package/src/tests/heartbeat-jev-gate.test.ts +23 -0
- package/src/tests/migration-232-agent-session-tokens.test.ts +67 -0
- package/src/tests/rbac-engine.test.ts +1 -0
- package/src/tests/runtime-child-token-cleanup.test.ts +3 -3
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/session-route-posture.test.ts +2 -0
- package/src/tests/skill-script-refs.test.ts +63 -0
- package/src/tests/slack-render-v2-query-plans.test.ts +147 -0
- package/src/tests/worker-token-cli.test.ts +76 -0
- package/src/tests/workflow-executors.test.ts +4 -2
- package/src/tests/workflow-system-one-extraction.test.ts +805 -0
- package/src/types.ts +2 -1
- package/src/utils/env-flag.ts +9 -0
- package/src/utils/request-auth-context.ts +2 -1
- package/src/workflows/definition.ts +7 -1
- package/src/workflows/engine.ts +18 -12
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/system-one-decision.ts +56 -255
- package/src/workflows/executors/system-one-extract-wire.ts +57 -0
- package/src/workflows/executors/system-one-extraction.ts +830 -0
- package/src/workflows/executors/system-one-providers.ts +277 -110
- package/src/workflows/executors/system-one-transport.ts +264 -0
- package/templates/official/lead/HEARTBEAT.md +1 -1
- package/templates/skills/apps/SKILL.md +4 -1
- package/templates/skills/apps/content.md +4 -1
- package/templates/skills/code-quality/content.md +9 -14
- package/templates/skills/heartbeat-runbook/content.md +3 -3
- package/dist/cli-v1sjhn52.js +0 -7
- package/dist/handlers-dkcarkd0.js +0 -39
- package/dist/index-5jecgp3n.js +0 -41
package/src/http/apps.ts
CHANGED
|
@@ -62,14 +62,16 @@ import {
|
|
|
62
62
|
rollbackApp,
|
|
63
63
|
snapshotApp,
|
|
64
64
|
} from "../apps/version";
|
|
65
|
-
import { getAgentById, getAppVersion, getAppVersions } from "../be/db";
|
|
65
|
+
import { getAgentById, getAppVersion, getAppVersions, getTaskById } from "../be/db";
|
|
66
66
|
import type { RbacPrincipal, RbacResource } from "../rbac";
|
|
67
67
|
import { can } from "../rbac";
|
|
68
68
|
import { AgentTaskStatusSchema } from "../types";
|
|
69
69
|
import { getRequestAuth } from "../utils/request-auth-context";
|
|
70
70
|
import { scrubObject } from "../utils/secret-scrubber";
|
|
71
71
|
import { resolveHttpFavoriteOwner } from "./favorite-owner";
|
|
72
|
-
import {
|
|
72
|
+
import { agentFirstPrincipal } from "./request-principal";
|
|
73
|
+
import { bodyTask, route } from "./route-def";
|
|
74
|
+
import { denyParentAttach } from "./session-access";
|
|
73
75
|
import { BODY_TOO_LARGE, enforceContentLengthCap, json, jsonError } from "./utils";
|
|
74
76
|
|
|
75
77
|
const MAX_APP_BODY_BYTES = 5 * 1024 * 1024;
|
|
@@ -618,17 +620,27 @@ const runActionRoute = route({
|
|
|
618
620
|
method: "post",
|
|
619
621
|
path: "/api/apps/{id}/actions/{name}",
|
|
620
622
|
pattern: ["api", "apps", null, "actions", null],
|
|
623
|
+
// A task-kind action may chain under `body.parentTaskId`: a user caller needs
|
|
624
|
+
// write access to that session, as for POST /api/tasks.
|
|
625
|
+
session: bodyTask("parentTaskId"),
|
|
621
626
|
summary: "Run a custom app action",
|
|
622
627
|
description: "Runs the saved script, or creates the agent task, named by the app definition.",
|
|
623
628
|
tags: ["Apps"],
|
|
624
629
|
params: z.object({ id: z.string().min(1), name: AppNameSchema }),
|
|
625
|
-
body: z.object({
|
|
630
|
+
body: z.object({
|
|
631
|
+
input: z.record(z.string(), z.unknown()).optional(),
|
|
632
|
+
/** Chains the new task off an existing one. Task-kind actions only: a script or sync action rejects it (400). */
|
|
633
|
+
parentTaskId: z.string().min(1).optional(),
|
|
634
|
+
}),
|
|
626
635
|
responses: {
|
|
627
636
|
200: { description: "Action invoked", schema: runActionResultSchema },
|
|
628
|
-
400: {
|
|
637
|
+
400: {
|
|
638
|
+
description:
|
|
639
|
+
"Invalid action input, stale script reference, or parentTaskId on a non-task action",
|
|
640
|
+
},
|
|
629
641
|
403: { description: "Permission denied" },
|
|
630
642
|
409: { description: "App definition needs repair" },
|
|
631
|
-
404: { description: "App or
|
|
643
|
+
404: { description: "App, action, or parent task not found" },
|
|
632
644
|
},
|
|
633
645
|
rbac: { permission: "app.use" },
|
|
634
646
|
});
|
|
@@ -1382,10 +1394,36 @@ export async function handleApps(
|
|
|
1382
1394
|
return true;
|
|
1383
1395
|
}
|
|
1384
1396
|
if (definitionNeedsRepair(res, app)) return true;
|
|
1397
|
+
if (parsed.body.parentTaskId) {
|
|
1398
|
+
// `parentTaskId` only means something to a task-kind action: a script or
|
|
1399
|
+
// sync action rejects it rather than silently dropping it. An unknown
|
|
1400
|
+
// action keeps `runAppAction`'s 404.
|
|
1401
|
+
const actions = app.definition.actions;
|
|
1402
|
+
const action =
|
|
1403
|
+
actions && Object.hasOwn(actions, parsed.params.name)
|
|
1404
|
+
? actions[parsed.params.name]
|
|
1405
|
+
: undefined;
|
|
1406
|
+
if (action && action.kind !== "task") {
|
|
1407
|
+
jsonError(res, "parentTaskId is only supported for task-kind actions", 400);
|
|
1408
|
+
return true;
|
|
1409
|
+
}
|
|
1410
|
+
if (action) {
|
|
1411
|
+
const parentTask = await getTaskById(parsed.body.parentTaskId);
|
|
1412
|
+
if (!parentTask) {
|
|
1413
|
+
jsonError(res, "parent task not found", 404);
|
|
1414
|
+
return true;
|
|
1415
|
+
}
|
|
1416
|
+
// The same gate as POST /api/tasks: without it an app action would
|
|
1417
|
+
// chain under any task past `task.parent.attach`.
|
|
1418
|
+
const principal = await agentFirstPrincipal(req, myAgentId);
|
|
1419
|
+
if (await denyParentAttach(req, res, myAgentId, parentTask, principal)) return true;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1385
1422
|
const output = await runAppAction({
|
|
1386
1423
|
app,
|
|
1387
1424
|
name: parsed.params.name,
|
|
1388
1425
|
input: parsed.body.input,
|
|
1426
|
+
parentTaskId: parsed.body.parentTaskId,
|
|
1389
1427
|
caller,
|
|
1390
1428
|
actor,
|
|
1391
1429
|
});
|
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
* when a task happens to draw it within `REFRESH_SKEW_MS` of expiry — which
|
|
13
13
|
* for an idle/rarely-drawn slot can be never, letting its refresh token sit
|
|
14
14
|
* unexercised well past OpenAI's ~8-day session-staleness guidance. This
|
|
15
|
-
* sweep calls the SAME `getValidCodexOAuth` with `opts.maxAgeMs`
|
|
16
|
-
* every slot refreshes on a
|
|
17
|
-
* frequency — zero logic
|
|
15
|
+
* sweep calls the SAME `getValidCodexOAuth` with `opts.maxAgeMs` (default 5
|
|
16
|
+
* days, `CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS`) so every slot refreshes on a
|
|
17
|
+
* fixed cadence regardless of task draw frequency — zero logic
|
|
18
|
+
* duplication, all lock/persist/quarantine behavior
|
|
18
19
|
* reused verbatim (see storage.ts doc comment; hand-rolling a refresher here
|
|
19
20
|
* would re-create the 2026-06-19 race).
|
|
20
21
|
*
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
26
27
|
import { z } from "zod";
|
|
27
28
|
import { getKv } from "../be/db";
|
|
29
|
+
import { CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS_BOUNDS } from "../be/swarm-config-guard";
|
|
28
30
|
import { deriveCodexKeySuffix } from "../providers/codex-oauth/auth-json.js";
|
|
29
31
|
import { getValidCodexOAuth, loadAllCodexOAuthSlots } from "../providers/codex-oauth/storage.js";
|
|
30
32
|
import { getApiKey } from "../utils/api-key";
|
|
@@ -32,8 +34,25 @@ import { CODEX_AUTH_WATCH_NAMESPACE, codexAuthBenchMarkerKey } from "../utils/co
|
|
|
32
34
|
import { getMcpBaseUrl } from "../utils/constants";
|
|
33
35
|
import { route } from "./route-def";
|
|
34
36
|
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Default refresh age. The old 7-day value lost a slot whose ChatGPT session
|
|
39
|
+
* OpenAI ended at about 6 days 20 hours, before the first attempt. 5 days
|
|
40
|
+
* leaves 44 hours of margin, so even a daily trigger refreshes by 6 days.
|
|
41
|
+
*/
|
|
42
|
+
const DEFAULT_KEEP_WARM_MAX_AGE_HOURS = 120;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read on every sweep, never at module load, so a stored override applies on
|
|
46
|
+
* config reload without a restart. A non-integer or out-of-range value falls
|
|
47
|
+
* back to the default; the config API rejects writes outside the same bounds.
|
|
48
|
+
*/
|
|
49
|
+
export function keepWarmMaxAgeMs(env: NodeJS.ProcessEnv = process.env): number {
|
|
50
|
+
const raw = env.CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS?.trim();
|
|
51
|
+
const { min, max } = CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS_BOUNDS;
|
|
52
|
+
const value = raw && /^\d+$/.test(raw) ? Number(raw) : Number.NaN;
|
|
53
|
+
const hours = value >= min && value <= max ? value : DEFAULT_KEEP_WARM_MAX_AGE_HOURS;
|
|
54
|
+
return hours * 60 * 60 * 1000;
|
|
55
|
+
}
|
|
37
56
|
|
|
38
57
|
type SlotOutcome =
|
|
39
58
|
| { slot: number; keySuffix: string; outcome: "warm" | "refreshed" }
|
|
@@ -61,7 +80,7 @@ const keepWarmRoute = route({
|
|
|
61
80
|
pattern: ["api", "oauth", "keep-warm", "codex"],
|
|
62
81
|
summary: "Locked keep-warm refresh sweep across all Codex OAuth pool slots",
|
|
63
82
|
description:
|
|
64
|
-
"Enumerates codex_oauth_* slots and refreshes any older than
|
|
83
|
+
"Enumerates codex_oauth_* slots and refreshes any older than CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS (default 120 hours) through the same locked getValidCodexOAuth path used at task time. Skips slots with a codex-auth-watch bench marker (auth-failure bench or codex-auth-expiry-watch).",
|
|
65
84
|
tags: ["OAuth"],
|
|
66
85
|
responses: {
|
|
67
86
|
200: {
|
|
@@ -82,6 +101,7 @@ export async function handleCodexOAuthKeepWarm(
|
|
|
82
101
|
|
|
83
102
|
const apiUrl = getMcpBaseUrl();
|
|
84
103
|
const apiKey = getApiKey();
|
|
104
|
+
const maxAgeMs = keepWarmMaxAgeMs();
|
|
85
105
|
const slots = await loadAllCodexOAuthSlots(apiUrl, apiKey);
|
|
86
106
|
const results: SlotOutcome[] = [];
|
|
87
107
|
|
|
@@ -95,9 +115,7 @@ export async function handleCodexOAuthKeepWarm(
|
|
|
95
115
|
|
|
96
116
|
try {
|
|
97
117
|
const beforeAccess = creds.access;
|
|
98
|
-
const refreshed = await getValidCodexOAuth(apiUrl, apiKey, slot, {
|
|
99
|
-
maxAgeMs: KEEP_WARM_MAX_AGE_MS,
|
|
100
|
-
});
|
|
118
|
+
const refreshed = await getValidCodexOAuth(apiUrl, apiKey, slot, { maxAgeMs });
|
|
101
119
|
if (!refreshed) {
|
|
102
120
|
results.push({ slot, outcome: "no-credentials" });
|
|
103
121
|
} else {
|
package/src/http/config.ts
CHANGED
|
@@ -86,7 +86,9 @@ async function requestSecretReadScope(
|
|
|
86
86
|
}).allow
|
|
87
87
|
)
|
|
88
88
|
return "all";
|
|
89
|
-
|
|
89
|
+
// Runtime and agent tokens stand in for a worker and read their own agent's
|
|
90
|
+
// scope. Task tokens are ACP harness credentials and read no secrets.
|
|
91
|
+
if (auth.tokenKind === "task" || resolvedScope?.agentId !== auth.agentId) return "none";
|
|
90
92
|
return "self";
|
|
91
93
|
}
|
|
92
94
|
if (auth?.kind === "guest") return "none";
|
|
@@ -107,7 +109,7 @@ async function resolveSecretsRead(
|
|
|
107
109
|
const auth = getRequestAuth(req);
|
|
108
110
|
return {
|
|
109
111
|
effectiveIncludeSecrets: includeSecrets && scope !== "none",
|
|
110
|
-
includeRepoSecrets: !(auth?.kind === "agent" && auth.
|
|
112
|
+
includeRepoSecrets: !(auth?.kind === "agent" && auth.tokenKind !== "task" && scope !== "all"),
|
|
111
113
|
secretsNote: includeSecrets && scope === "none" ? SECRETS_FORCE_MASK_NOTE : "",
|
|
112
114
|
};
|
|
113
115
|
}
|
|
@@ -363,7 +365,7 @@ export async function handleConfig(
|
|
|
363
365
|
const resolvedScope = {
|
|
364
366
|
agentId:
|
|
365
367
|
parsed.query.agentId ||
|
|
366
|
-
(auth?.kind === "agent" && auth.
|
|
368
|
+
(auth?.kind === "agent" && auth.tokenKind !== "task" ? auth.agentId : undefined),
|
|
367
369
|
repoId: parsed.query.repoId || undefined,
|
|
368
370
|
};
|
|
369
371
|
const { effectiveIncludeSecrets, includeRepoSecrets, secretsNote } = await resolveSecretsRead(
|
package/src/http/core.ts
CHANGED
|
@@ -547,6 +547,8 @@ export async function handleCore(
|
|
|
547
547
|
return true;
|
|
548
548
|
}
|
|
549
549
|
req.headers["x-runtime-instance-id"] = auth.runtimeId;
|
|
550
|
+
}
|
|
551
|
+
if (auth.tokenKind !== "task") {
|
|
550
552
|
const sourceHeader = req.headers["x-source-task-id"];
|
|
551
553
|
const taskId = Array.isArray(sourceHeader) ? sourceHeader[0] : sourceHeader;
|
|
552
554
|
if (taskId !== undefined && (await getTaskById(taskId))?.agentId !== auth.agentId) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IncomingMessage } from "node:http";
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import { getSteeringMessageById } from "../be/db";
|
|
3
3
|
import { getActiveSessionById } from "../be/db/active-sessions";
|
|
4
4
|
import { getUserGrant } from "../be/rbac-roles";
|
|
@@ -10,10 +10,12 @@ import {
|
|
|
10
10
|
sessionAccessForRoot,
|
|
11
11
|
} from "../be/session-shares";
|
|
12
12
|
import { findUserById } from "../be/users";
|
|
13
|
-
import { isRbacEnabled } from "../rbac";
|
|
13
|
+
import { can, isRbacEnabled, type RbacPrincipal } from "../rbac";
|
|
14
|
+
import type { AgentTask } from "../types";
|
|
14
15
|
import { getRequestAuth } from "../utils/request-auth-context";
|
|
15
16
|
import { dashboardUserIdHeader } from "./favorite-owner";
|
|
16
17
|
import { type RouteDef, routePathParams } from "./route-def";
|
|
18
|
+
import { jsonError } from "./utils";
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Who a request acts for when it touches a session.
|
|
@@ -76,6 +78,53 @@ export function readableByFor(actor: SessionActor): { userId: string | null } |
|
|
|
76
78
|
export const SESSION_WRITE_DENIED =
|
|
77
79
|
"You can't write in this session. Ask the owner to share it with edit access.";
|
|
78
80
|
|
|
81
|
+
/** True unless the request acts for a user who lacks write access to the task's session. */
|
|
82
|
+
export async function hasSessionWrite(
|
|
83
|
+
req: IncomingMessage,
|
|
84
|
+
myAgentId: string | undefined,
|
|
85
|
+
taskId: string,
|
|
86
|
+
): Promise<boolean> {
|
|
87
|
+
const access = await sessionAccessFor(await resolveSessionActor(req, myAgentId), taskId);
|
|
88
|
+
return hasSessionAccess(access, "write");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The parent gate shared by `POST /api/tasks` and `POST /api/apps/{id}/actions/{name}`.
|
|
93
|
+
* The child inherits the parent's thread context, so an agent caller must own or
|
|
94
|
+
* have created the parent (`task.parent.attach`; operator and user callers keep
|
|
95
|
+
* their access), and a follow-up in someone else's session needs a write share.
|
|
96
|
+
* Writes the 403 and returns true when denied.
|
|
97
|
+
*/
|
|
98
|
+
export async function denyParentAttach(
|
|
99
|
+
req: IncomingMessage,
|
|
100
|
+
res: ServerResponse,
|
|
101
|
+
myAgentId: string | undefined,
|
|
102
|
+
parentTask: Pick<AgentTask, "id" | "requestedByUserId" | "creatorAgentId" | "agentId">,
|
|
103
|
+
principal: RbacPrincipal,
|
|
104
|
+
): Promise<boolean> {
|
|
105
|
+
const attach = can({
|
|
106
|
+
principal,
|
|
107
|
+
verb: "task.parent.attach",
|
|
108
|
+
resource: {
|
|
109
|
+
kind: "task",
|
|
110
|
+
taskId: parentTask.id,
|
|
111
|
+
requestedByUserId: parentTask.requestedByUserId,
|
|
112
|
+
creatorAgentId: parentTask.creatorAgentId,
|
|
113
|
+
agentId: parentTask.agentId,
|
|
114
|
+
},
|
|
115
|
+
source: "http",
|
|
116
|
+
});
|
|
117
|
+
if (!attach.allow) {
|
|
118
|
+
jsonError(res, `You don't have context for parent task ${parentTask.id}.`, 403);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (!(await hasSessionWrite(req, myAgentId, parentTask.id))) {
|
|
122
|
+
jsonError(res, SESSION_WRITE_DENIED, 403);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
79
128
|
/**
|
|
80
129
|
* The denial for an actor lacking `needed` access to the session holding
|
|
81
130
|
* `taskId`, or null. An unknown task passes, so the handler still answers 404.
|
package/src/http/sessions.ts
CHANGED
|
@@ -2,11 +2,14 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import {
|
|
4
4
|
countSessions,
|
|
5
|
+
createAgent,
|
|
6
|
+
getAgentById,
|
|
5
7
|
getDbClient,
|
|
6
8
|
getRootTaskChain,
|
|
7
9
|
getTaskById,
|
|
8
10
|
listRecentSessions,
|
|
9
11
|
} from "../be/db";
|
|
12
|
+
import { getUserGrant } from "../be/rbac-roles";
|
|
10
13
|
import {
|
|
11
14
|
getSessionRoot,
|
|
12
15
|
listActiveSessionShares,
|
|
@@ -18,13 +21,17 @@ import {
|
|
|
18
21
|
import { getTaskSteeringFieldsForTasks } from "../be/steering";
|
|
19
22
|
import { getTaskCitationsForTasks, TaskCitationSchema } from "../be/task-citations";
|
|
20
23
|
import {
|
|
24
|
+
AgentTokenExistsError,
|
|
21
25
|
findUserById,
|
|
22
26
|
InvalidRuntimeTokenError,
|
|
27
|
+
mintAgentToken,
|
|
23
28
|
mintRuntimeToken,
|
|
24
29
|
mintSessionToken,
|
|
30
|
+
revokeAgentTokens,
|
|
25
31
|
revokeSessionToken,
|
|
32
|
+
rotateAgentToken,
|
|
26
33
|
} from "../be/users";
|
|
27
|
-
import { can } from "../rbac";
|
|
34
|
+
import { can, isRbacEnabled } from "../rbac";
|
|
28
35
|
import { sessionAccessTopic } from "../realtime/auth";
|
|
29
36
|
import { realtimeBus } from "../realtime/bus";
|
|
30
37
|
import {
|
|
@@ -39,6 +46,7 @@ import {
|
|
|
39
46
|
} from "../types";
|
|
40
47
|
import { getRequestAuth } from "../utils/request-auth-context";
|
|
41
48
|
import { isRuntimePoolsEnabled } from "../utils/runtime-pools-flag";
|
|
49
|
+
import { agentFirstPrincipal } from "./request-principal";
|
|
42
50
|
import { route } from "./route-def";
|
|
43
51
|
import { resolveSessionActor, type SessionActor } from "./session-access";
|
|
44
52
|
import { jsonError } from "./utils";
|
|
@@ -118,7 +126,7 @@ const mintSessionTokenRoute = route({
|
|
|
118
126
|
},
|
|
119
127
|
summary: "Mint an ephemeral task or runtime token",
|
|
120
128
|
description:
|
|
121
|
-
"Operators can mint task or runtime tokens. Runtime tokens can mint task tokens for their own assigned tasks.
|
|
129
|
+
"Operators can mint task or runtime tokens. Runtime and agent tokens can mint task tokens for their own assigned tasks. A child token never outlives its parent and is revoked with it.",
|
|
122
130
|
tags: ["Sessions"],
|
|
123
131
|
body: z
|
|
124
132
|
.object({
|
|
@@ -147,7 +155,8 @@ const mintSessionTokenRoute = route({
|
|
|
147
155
|
},
|
|
148
156
|
auth: { apiKey: true },
|
|
149
157
|
rbac: {
|
|
150
|
-
ungated:
|
|
158
|
+
ungated:
|
|
159
|
+
"operator, or a runtime or agent token minting for its own assigned task, enforced in the handler",
|
|
151
160
|
},
|
|
152
161
|
});
|
|
153
162
|
|
|
@@ -157,7 +166,7 @@ const revokeSessionTokenRoute = route({
|
|
|
157
166
|
pattern: ["api", "sessions", "tokens", null],
|
|
158
167
|
summary: "Revoke an ephemeral session token",
|
|
159
168
|
description:
|
|
160
|
-
"Operators can revoke any ephemeral token.
|
|
169
|
+
"Operators can revoke any ephemeral token. Agent tokens, and runtime tokens when runtime pools are enabled, can revoke their own child task tokens. Repeated revocation has no effect.",
|
|
161
170
|
tags: ["Sessions"],
|
|
162
171
|
params: z.object({ tokenId: z.string() }),
|
|
163
172
|
responses: {
|
|
@@ -167,10 +176,234 @@ const revokeSessionTokenRoute = route({
|
|
|
167
176
|
},
|
|
168
177
|
auth: { apiKey: true },
|
|
169
178
|
rbac: {
|
|
170
|
-
ungated:
|
|
179
|
+
ungated:
|
|
180
|
+
"operator, or a runtime or agent token revoking its own child, enforced in the handler",
|
|
171
181
|
},
|
|
172
182
|
});
|
|
173
183
|
|
|
184
|
+
/** Default rotation grace: the old worker token keeps working this long. */
|
|
185
|
+
const AGENT_TOKEN_ROTATE_GRACE_SEC = 300;
|
|
186
|
+
|
|
187
|
+
const AgentTokenSchema = z.object({
|
|
188
|
+
agentId: z.string(),
|
|
189
|
+
tokenId: z.string(),
|
|
190
|
+
plaintext: z.string(),
|
|
191
|
+
expiresAt: z.string().nullable().describe("null when the token does not expire"),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const agentTokenTtl = z
|
|
195
|
+
.number()
|
|
196
|
+
.int()
|
|
197
|
+
.positive()
|
|
198
|
+
.optional()
|
|
199
|
+
.describe("Token lifetime in ms. Omit for the default (no expiry).");
|
|
200
|
+
|
|
201
|
+
const mintAgentTokenRoute = route({
|
|
202
|
+
method: "post",
|
|
203
|
+
path: "/api/agents/{id}/token",
|
|
204
|
+
pattern: ["api", "agents", null, "token"],
|
|
205
|
+
operationId: "agents_mintWorkerToken",
|
|
206
|
+
summary: "Mint an agent-bound worker token",
|
|
207
|
+
description:
|
|
208
|
+
"Returns the token once. A worker runs on it in place of the swarm API key and acts only as this agent. Creates the agent when it does not exist and `name` is given. Fails with 409 when the agent already holds an active token.",
|
|
209
|
+
tags: ["Sessions"],
|
|
210
|
+
params: z.object({ id: z.string().min(1) }),
|
|
211
|
+
body: z
|
|
212
|
+
.object({ name: z.string().min(1).optional(), ttlMs: agentTokenTtl })
|
|
213
|
+
.strict()
|
|
214
|
+
.optional(),
|
|
215
|
+
responses: {
|
|
216
|
+
200: {
|
|
217
|
+
description: "Minted worker token",
|
|
218
|
+
schema: AgentTokenSchema.extend({ agentCreated: z.boolean() }),
|
|
219
|
+
},
|
|
220
|
+
403: { description: "Operator or user credentials required" },
|
|
221
|
+
404: { description: "Agent not found and no name given" },
|
|
222
|
+
409: { description: "Agent already holds an active token" },
|
|
223
|
+
},
|
|
224
|
+
rbac: { permission: "agent.token.manage" },
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const rotateAgentTokenRoute = route({
|
|
228
|
+
method: "post",
|
|
229
|
+
path: "/api/agents/{id}/token/rotate",
|
|
230
|
+
pattern: ["api", "agents", null, "token", "rotate"],
|
|
231
|
+
operationId: "agents_rotateWorkerToken",
|
|
232
|
+
summary: "Rotate an agent-bound worker token",
|
|
233
|
+
description:
|
|
234
|
+
"Returns a new token once. The previous token keeps working for `graceSec` (default 300), then expires. Use revoke for a token that must stop now.",
|
|
235
|
+
tags: ["Sessions"],
|
|
236
|
+
params: z.object({ id: z.string().min(1) }),
|
|
237
|
+
body: z
|
|
238
|
+
.object({ graceSec: z.number().int().min(0).max(86_400).optional(), ttlMs: agentTokenTtl })
|
|
239
|
+
.strict()
|
|
240
|
+
.optional(),
|
|
241
|
+
responses: {
|
|
242
|
+
200: {
|
|
243
|
+
description: "New worker token",
|
|
244
|
+
schema: AgentTokenSchema.extend({ previousExpiresAt: z.string() }),
|
|
245
|
+
},
|
|
246
|
+
403: { description: "Operator or user credentials required" },
|
|
247
|
+
404: { description: "Agent holds no active token" },
|
|
248
|
+
},
|
|
249
|
+
rbac: { permission: "agent.token.manage" },
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const revokeAgentTokenRoute = route({
|
|
253
|
+
method: "delete",
|
|
254
|
+
path: "/api/agents/{id}/token",
|
|
255
|
+
pattern: ["api", "agents", null, "token"],
|
|
256
|
+
operationId: "agents_revokeWorkerToken",
|
|
257
|
+
summary: "Revoke an agent-bound worker token",
|
|
258
|
+
description:
|
|
259
|
+
"Revokes every active worker token of the agent and the task tokens they minted. Open realtime connections on them close.",
|
|
260
|
+
tags: ["Sessions"],
|
|
261
|
+
params: z.object({ id: z.string().min(1) }),
|
|
262
|
+
responses: {
|
|
263
|
+
200: {
|
|
264
|
+
description: "Number of worker tokens revoked (0 when none was active)",
|
|
265
|
+
schema: z.object({ revoked: z.number().int() }),
|
|
266
|
+
},
|
|
267
|
+
403: { description: "Operator or user credentials required" },
|
|
268
|
+
},
|
|
269
|
+
rbac: { permission: "agent.token.manage" },
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const AGENT_TOKEN_ROUTES = [mintAgentTokenRoute, rotateAgentTokenRoute, revokeAgentTokenRoute];
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Gate the worker-token routes. Only an operator key or a user may manage
|
|
276
|
+
* them, never a session token. The principal is agent-first: an operator key
|
|
277
|
+
* sent with X-Agent-ID acts as that agent and is refused, as on the
|
|
278
|
+
* controller-token route.
|
|
279
|
+
*/
|
|
280
|
+
async function ensureAgentTokenManager(
|
|
281
|
+
req: IncomingMessage,
|
|
282
|
+
res: ServerResponse,
|
|
283
|
+
agentId: string,
|
|
284
|
+
): Promise<boolean> {
|
|
285
|
+
const auth = getRequestAuth(req);
|
|
286
|
+
if (auth?.kind !== "operator" && auth?.kind !== "user") {
|
|
287
|
+
jsonError(res, "Worker tokens require operator or user credentials", 403);
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
const header = req.headers["x-agent-id"];
|
|
291
|
+
const decision = can({
|
|
292
|
+
principal: await agentFirstPrincipal(req, Array.isArray(header) ? header[0] : header),
|
|
293
|
+
verb: "agent.token.manage",
|
|
294
|
+
resource: { kind: "agent", agentId },
|
|
295
|
+
source: "http",
|
|
296
|
+
});
|
|
297
|
+
if (!decision.allow) {
|
|
298
|
+
jsonError(res, decision.reason, 403);
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
// A worker token carries the target agent's privileges and reads that
|
|
302
|
+
// agent's resolved config, global secrets included. A user without a
|
|
303
|
+
// grant-all role must already hold both, and may not manage the token of an
|
|
304
|
+
// agent that may itself act on any agent (a lead).
|
|
305
|
+
if (auth.kind === "user" && isRbacEnabled()) {
|
|
306
|
+
const grant = await getUserGrant(auth.userId);
|
|
307
|
+
if (!grant.grantsAll) {
|
|
308
|
+
const target = await getAgentById(agentId);
|
|
309
|
+
const targetActsOnAny = can({
|
|
310
|
+
principal: { kind: "agent", agentId, isLead: target?.isLead ?? false },
|
|
311
|
+
verb: "agent.act.any",
|
|
312
|
+
resource: { kind: "agent", agentId },
|
|
313
|
+
source: "http",
|
|
314
|
+
}).allow;
|
|
315
|
+
if (targetActsOnAny) {
|
|
316
|
+
jsonError(res, "A lead agent's worker token requires a grant-all role", 403);
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
const missing = (["agent.act.any", "config.read.secrets"] as const).filter(
|
|
320
|
+
(verb) => !grant.verbs.has(verb),
|
|
321
|
+
);
|
|
322
|
+
if (missing.length > 0) {
|
|
323
|
+
jsonError(res, `Worker tokens also require: ${missing.join(", ")}`, 403);
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function handleAgentTokens(
|
|
332
|
+
req: IncomingMessage,
|
|
333
|
+
res: ServerResponse,
|
|
334
|
+
pathSegments: string[],
|
|
335
|
+
queryParams: URLSearchParams,
|
|
336
|
+
): Promise<boolean> {
|
|
337
|
+
const matched = AGENT_TOKEN_ROUTES.find((def) => def.match(req.method, pathSegments));
|
|
338
|
+
if (!matched) return false;
|
|
339
|
+
if (!(await ensureAgentTokenManager(req, res, pathSegments[2]!))) return true;
|
|
340
|
+
res.setHeader("Cache-Control", "no-store");
|
|
341
|
+
|
|
342
|
+
if (matched === mintAgentTokenRoute) {
|
|
343
|
+
const parsed = await mintAgentTokenRoute.parse(req, res, pathSegments, queryParams);
|
|
344
|
+
if (!parsed) return true;
|
|
345
|
+
const agentId = parsed.params.id;
|
|
346
|
+
const result = await getDbClient()
|
|
347
|
+
.transaction(async () => {
|
|
348
|
+
let agentCreated = false;
|
|
349
|
+
if (!(await getAgentById(agentId))) {
|
|
350
|
+
if (!parsed.body?.name) return { status: 404 as const };
|
|
351
|
+
// Same defaults as a worker's first registration; it is offline until it connects.
|
|
352
|
+
await createAgent({
|
|
353
|
+
id: agentId,
|
|
354
|
+
name: parsed.body.name,
|
|
355
|
+
isLead: false,
|
|
356
|
+
status: "offline",
|
|
357
|
+
capabilities: [],
|
|
358
|
+
maxTasks: 1,
|
|
359
|
+
});
|
|
360
|
+
agentCreated = true;
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
status: 200 as const,
|
|
364
|
+
agentCreated,
|
|
365
|
+
token: await mintAgentToken(agentId, parsed.body?.ttlMs),
|
|
366
|
+
};
|
|
367
|
+
})
|
|
368
|
+
.catch((error) => {
|
|
369
|
+
if (error instanceof AgentTokenExistsError) return { status: 409 as const };
|
|
370
|
+
throw error;
|
|
371
|
+
});
|
|
372
|
+
if (result.status === 404) {
|
|
373
|
+
jsonError(res, "Agent not found; pass name to create it", 404);
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
if (result.status === 409) {
|
|
377
|
+
jsonError(res, "Agent already holds an active worker token; rotate or revoke it", 409);
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
mintAgentTokenRoute.respond(res, 200, {
|
|
381
|
+
agentId,
|
|
382
|
+
...result.token,
|
|
383
|
+
agentCreated: result.agentCreated,
|
|
384
|
+
});
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (matched === rotateAgentTokenRoute) {
|
|
389
|
+
const parsed = await rotateAgentTokenRoute.parse(req, res, pathSegments, queryParams);
|
|
390
|
+
if (!parsed) return true;
|
|
391
|
+
const graceSec = parsed.body?.graceSec ?? AGENT_TOKEN_ROTATE_GRACE_SEC;
|
|
392
|
+
const token = await rotateAgentToken(parsed.params.id, graceSec * 1000, parsed.body?.ttlMs);
|
|
393
|
+
if (!token) {
|
|
394
|
+
jsonError(res, "Agent holds no active worker token", 404);
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
rotateAgentTokenRoute.respond(res, 200, { agentId: parsed.params.id, ...token });
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const parsed = await revokeAgentTokenRoute.parse(req, res, pathSegments, queryParams);
|
|
402
|
+
if (!parsed) return true;
|
|
403
|
+
revokeAgentTokenRoute.respond(res, 200, { revoked: await revokeAgentTokens(parsed.params.id) });
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
|
|
174
407
|
const listSessions = route({
|
|
175
408
|
method: "get",
|
|
176
409
|
path: "/api/sessions",
|
|
@@ -309,9 +542,11 @@ export async function handleSessions(
|
|
|
309
542
|
queryParams: URLSearchParams,
|
|
310
543
|
myAgentId?: string,
|
|
311
544
|
): Promise<boolean> {
|
|
545
|
+
if (await handleAgentTokens(req, res, pathSegments, queryParams)) return true;
|
|
546
|
+
|
|
312
547
|
if (mintSessionTokenRoute.match(req.method, pathSegments)) {
|
|
313
548
|
const auth = getRequestAuth(req);
|
|
314
|
-
if (auth?.kind !== "operator" && !(auth?.kind === "agent" && auth.
|
|
549
|
+
if (auth?.kind !== "operator" && !(auth?.kind === "agent" && auth.tokenKind !== "task")) {
|
|
315
550
|
jsonError(res, "Unauthorized", 401);
|
|
316
551
|
return true;
|
|
317
552
|
}
|
|
@@ -334,7 +569,7 @@ export async function handleSessions(
|
|
|
334
569
|
return {
|
|
335
570
|
token: await mintSessionToken(agentId, taskId!, ttlMs!, {
|
|
336
571
|
tokenId: auth.tokenId,
|
|
337
|
-
runtimeId: auth.runtimeId
|
|
572
|
+
runtimeId: auth.runtimeId,
|
|
338
573
|
}),
|
|
339
574
|
};
|
|
340
575
|
}
|
|
@@ -365,7 +600,10 @@ export async function handleSessions(
|
|
|
365
600
|
const auth = getRequestAuth(req);
|
|
366
601
|
if (
|
|
367
602
|
auth?.kind !== "operator" &&
|
|
368
|
-
!(
|
|
603
|
+
!(
|
|
604
|
+
auth?.kind === "agent" &&
|
|
605
|
+
(auth.tokenKind === "agent" || (auth.tokenKind === "runtime" && isRuntimePoolsEnabled()))
|
|
606
|
+
)
|
|
369
607
|
) {
|
|
370
608
|
jsonError(res, "Unauthorized", 401);
|
|
371
609
|
return true;
|
|
@@ -375,8 +613,9 @@ export async function handleSessions(
|
|
|
375
613
|
const revoked = await getDbClient().transaction(async (client) => {
|
|
376
614
|
if (auth.kind === "agent") {
|
|
377
615
|
const child = await client.get(
|
|
378
|
-
|
|
379
|
-
|
|
616
|
+
`SELECT id FROM session_tokens WHERE id = ? AND agentId = ? AND taskId IS NOT NULL
|
|
617
|
+
AND ${auth.runtimeId ? "parentRuntimeId" : "parentTokenId"} = ?`,
|
|
618
|
+
[parsed.params.tokenId, auth.agentId, auth.runtimeId ?? auth.tokenId],
|
|
380
619
|
);
|
|
381
620
|
if (!child) return false;
|
|
382
621
|
}
|