@desplega.ai/agent-swarm 2.4.3 → 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-1885dpr7.js → acp-adapter-thq04adh.js} +1 -2
- package/dist/{actions-pn76n32q.js → actions-j6ywh6x5.js} +17 -17
- package/dist/{amp-adapter-af84rx1d.js → amp-adapter-k8b5xmj8.js} +2 -2
- package/dist/{app-6sv8mhzm.js → app-rb7vc02s.js} +3 -3
- package/dist/{assistant-sc2tf825.js → assistant-ftta1zjf.js} +18 -18
- package/dist/{boot-reembed-y3m0ydzj.js → boot-reembed-srp6v6af.js} +1 -1
- package/dist/{boot-reembed-gvxjs2py.js → boot-reembed-xns7h5h9.js} +1 -1
- package/dist/{boot-scrub-sweep-1j9mk05f.js → boot-scrub-sweep-3qqrh2mz.js} +2 -2
- package/dist/{boot-seeds-37sp626b.js → boot-seeds-9ks8hce7.js} +4 -4
- package/dist/{claude-adapter-rp0jtnwr.js → claude-adapter-k5ksyres.js} +2 -2
- package/dist/{claude-managed-setup-rdt248k7.js → claude-managed-setup-n5a5eqks.js} +1 -1
- package/dist/{claude-sdk-session-53rcw5we.js → claude-sdk-session-2j3j4kwn.js} +2 -2
- package/dist/{cli-v87dr6e3.js → cli-1d70crsy.js} +1 -1
- package/dist/{cli-1ma335fz.js → cli-1ephgwnq.js} +2 -2
- package/dist/{cli-4yef8ghv.js → cli-2v045xyf.js} +3 -3
- package/dist/{cli-73ezfhha.js → cli-2y91wnbh.js} +10 -5
- package/dist/{cli-1eszr343.js → cli-2zqpkvtt.js} +1 -1
- package/dist/{cli-e4d78xsv.js → cli-37yb7307.js} +1 -1
- package/dist/{cli-8y99kfey.js → cli-3cgngyd8.js} +9 -9
- package/dist/{cli-wk28hzqn.js → cli-44159k28.js} +1 -1
- package/dist/{cli-qp2m4btv.js → cli-4dg55wsk.js} +2 -2
- package/dist/{cli-0dxxwfkt.js → cli-51y09c6p.js} +17 -14
- package/dist/{cli-aa3tb29r.js → cli-55qmbp6z.js} +1 -1
- package/dist/{cli-mzkwkbdy.js → cli-82d3nkc3.js} +1 -1
- package/dist/{cli-sds5zzs8.js → cli-9fhkkpj5.js} +78 -12
- package/dist/{cli-vq9gqarq.js → cli-amyhm3gj.js} +1 -1
- package/dist/{cli-bxrqvpg5.js → cli-b8k7epj1.js} +5 -5
- package/dist/{cli-8vverzf7.js → cli-ba8ne1es.js} +5 -0
- package/dist/{cli-58vwn100.js → cli-bnz0r3kc.js} +1 -1
- package/dist/{cli-6eyncg0y.js → cli-cynvc9fj.js} +34 -6
- package/dist/{cli-f0p0f6ja.js → cli-dqb4n629.js} +7 -7
- package/dist/{cli-pygdsgh8.js → cli-esxerzg7.js} +1 -1
- package/dist/{cli-dm8p2dpf.js → cli-gqvh8d8a.js} +6 -6
- package/dist/{cli-p0qerd2r.js → cli-hj3debgw.js} +1 -1
- package/dist/{cli-836ehxmt.js → cli-nc35pxb7.js} +2 -2
- package/dist/{cli-vcgscaan.js → cli-p20r8w42.js} +8 -6
- package/dist/{cli-1xsraagq.js → cli-pccfmtb5.js} +4 -4
- package/dist/{cli-s9rr4gr2.js → cli-q6r7t299.js} +1036 -298
- package/dist/{cli-tkf4x6ha.js → cli-qkhc5aqy.js} +11 -11
- package/dist/{cli-1n9gdg56.js → cli-t7cyef5g.js} +4 -4
- package/dist/{cli-038fga5k.js → cli-te6b5wvg.js} +148 -183
- package/dist/{cli-kgnkyabk.js → cli-v3yr2gbg.js} +109 -35
- package/dist/{cli-h0mgxann.js → cli-vdm20wej.js} +2 -2
- package/dist/{cli-zvbb4n0e.js → cli-vq97g1jq.js} +2 -2
- package/dist/{cli-gx0j2khs.js → cli-w6nadkh4.js} +3 -3
- package/dist/{cli-99jez72r.js → cli-w9etnnxk.js} +6 -1
- package/dist/{cli-npfe9t1s.js → cli-xc27fwzg.js} +2 -2
- package/dist/{cli-6aen3ezb.js → cli-xj0re5tm.js} +1 -1
- package/dist/{cli-aypbg9r2.js → cli-zrk6jxv8.js} +1 -1
- package/dist/{cli-1012xb7j.js → cli-zrwpctp4.js} +20 -20
- package/dist/cli.js +41 -16
- package/dist/{codex-adapter-2zrmab9b.js → codex-adapter-968xwfy7.js} +1 -1
- package/dist/{codex-session-runner-kbkz62vc.js → codex-session-runner-ht4z99fa.js} +1 -1
- package/dist/{commands-hd574th6.js → commands-pcymaeg9.js} +2 -2
- package/dist/{controller-pz5mcfg4.js → controller-fpjyk46r.js} +2 -2
- package/dist/{controllers-connect-5rgq2hfk.js → controllers-connect-mwzx1fh6.js} +18 -18
- package/dist/{cursor-adapter-mgaprq0j.js → cursor-adapter-cp4s2e2f.js} +1 -1
- package/dist/{db-h06dxp05.js → db-sgd12bt9.js} +3 -3
- package/dist/{db-6nf1207q.js → db-xsjg9jqq.js} +1 -1
- package/dist/{dispatcher-zthpy4m1.js → dispatcher-x0jfw2c9.js} +16 -16
- package/dist/{dsh-adapter-64dhw0j7.js → dsh-adapter-jrq7ma7h.js} +1 -1
- package/dist/{external-channel-x01t7ke6.js → external-channel-8njj4h3b.js} +2 -2
- package/dist/{grok-adapter-h21jv594.js → grok-adapter-kkr97v2f.js} +2 -3
- package/dist/handlers-0pvee9nw.js +39 -0
- package/dist/{heartbeat-zzfksckz.js → heartbeat-wd03zhn0.js} +19 -19
- package/dist/{hook-z4zhzj1f.js → hook-f0vghj42.js} +1 -1
- package/dist/{hook-3xfqhh2s.js → hook-xzj2ts2p.js} +1 -1
- package/dist/{http-q813tmya.js → http-4hwxv62p.js} +255 -117
- package/dist/{index-va3y63es.js → index-6srwvd5b.js} +1 -2
- package/dist/{index-220tk9bw.js → index-97q4xn03.js} +19 -10
- package/dist/{index-s98037v0.js → index-cbxkmfc5.js} +1 -2
- package/dist/{index-m0a4ydqj.js → index-gdh8jrdm.js} +16 -16
- package/dist/{index-wbbs14c5.js → index-hg0x2cdv.js} +16 -16
- package/dist/{index-d0g18bax.js → index-thm0y6a8.js} +19 -19
- package/dist/{index-x8xbyn1t.js → index-ww2vznkp.js} +1 -1
- package/dist/{keepalive-y5t1my7m.js → keepalive-hrh7xnr9.js} +3 -3
- package/dist/{lead-2xwtq66b.js → lead-ee8vazz2.js} +26 -27
- package/dist/{maintenance-ybfsaedv.js → maintenance-e8wnwnqx.js} +1 -1
- package/dist/{oauth-refresh-sweep-909jmn12.js → oauth-refresh-sweep-zm0yj4sj.js} +2 -2
- package/dist/{onboard-xn6fkkys.js → onboard-dtwcq603.js} +2 -2
- package/dist/{otel-impl-e6mn0z13.js → otel-impl-w3fbhpn0.js} +1 -1
- package/dist/{pricing-refresh-ygd362ep.js → pricing-refresh-j38dehfs.js} +3 -3
- package/dist/{rbac-roles-c9axvfnz.js → rbac-roles-ae8yttq4.js} +2 -2
- package/dist/{rbac-roles-mb470p4e.js → rbac-roles-ejxx9b35.js} +2 -2
- package/dist/{render-v2-g0306nh1.js → render-v2-n16n0phe.js} +3 -3
- package/dist/{resume-v315hnq6.js → resume-z0fgjc38.js} +5 -5
- package/dist/{session-access-2zg285a1.js → session-access-cv3mq6mc.js} +6 -4
- package/dist/{setup-a4mtf02k.js → setup-mafhj3yt.js} +1 -1
- package/dist/{work-objects-9fr0fgtn.js → work-objects-8b86mgjp.js} +19 -19
- package/dist/{worker-qae22jeb.js → worker-16s75hm5.js} +26 -27
- package/dist/worker-token-xf11yyds.js +136 -0
- package/openapi.json +295 -12
- package/package.json +4 -4
- 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/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/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-oauth/storage.ts +1 -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/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/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 +2 -0
- package/src/tests/session-route-posture.test.ts +2 -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/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 +3 -1
- package/templates/skills/apps/content.md +3 -1
- package/templates/skills/heartbeat-runbook/content.md +3 -3
- package/dist/cli-v1sjhn52.js +0 -7
- package/dist/handlers-2qjw434w.js +0 -39
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
|
}
|
package/src/http/tasks.ts
CHANGED
|
@@ -87,6 +87,7 @@ import { buildTaskRequestedBy, TaskRequestedBySchema } from "./requested-by";
|
|
|
87
87
|
import { bodyTask, route } from "./route-def";
|
|
88
88
|
import {
|
|
89
89
|
canReadSession,
|
|
90
|
+
denyParentAttach,
|
|
90
91
|
readableByFor,
|
|
91
92
|
resolveSessionActor,
|
|
92
93
|
SESSION_READ_DENIED,
|
|
@@ -779,16 +780,6 @@ const updateTaskTitleRoute = route({
|
|
|
779
780
|
},
|
|
780
781
|
});
|
|
781
782
|
|
|
782
|
-
/** True unless the request acts for a user who lacks write access to the task's session. */
|
|
783
|
-
async function hasSessionWrite(
|
|
784
|
-
req: IncomingMessage,
|
|
785
|
-
myAgentId: string | undefined,
|
|
786
|
-
taskId: string,
|
|
787
|
-
): Promise<boolean> {
|
|
788
|
-
const access = await sessionAccessFor(await resolveSessionActor(req, myAgentId), taskId);
|
|
789
|
-
return hasSessionAccess(access, "write");
|
|
790
|
-
}
|
|
791
|
-
|
|
792
783
|
async function canSteerTask(
|
|
793
784
|
req: IncomingMessage,
|
|
794
785
|
myAgentId: string | undefined,
|
|
@@ -1069,29 +1060,10 @@ export async function handleTasks(
|
|
|
1069
1060
|
? await getTaskById(parsed.body.parentTaskId)
|
|
1070
1061
|
: null;
|
|
1071
1062
|
// The child inherits the parent's thread context, so an agent caller must own or
|
|
1072
|
-
// have created the parent
|
|
1063
|
+
// have created the parent, and a follow-up in someone else's session needs a
|
|
1064
|
+
// write share. The same gate guards app actions that chain under a task.
|
|
1073
1065
|
const principal = await resolveTaskWritePrincipal(req, myAgentId);
|
|
1074
|
-
if (
|
|
1075
|
-
parentTask &&
|
|
1076
|
-
!can({
|
|
1077
|
-
principal,
|
|
1078
|
-
verb: "task.parent.attach",
|
|
1079
|
-
resource: {
|
|
1080
|
-
kind: "task",
|
|
1081
|
-
taskId: parentTask.id,
|
|
1082
|
-
requestedByUserId: parentTask.requestedByUserId,
|
|
1083
|
-
creatorAgentId: parentTask.creatorAgentId,
|
|
1084
|
-
agentId: parentTask.agentId,
|
|
1085
|
-
},
|
|
1086
|
-
source: "http",
|
|
1087
|
-
}).allow
|
|
1088
|
-
) {
|
|
1089
|
-
jsonError(res, `You don't have context for parent task ${parentTask.id}.`, 403);
|
|
1090
|
-
return true;
|
|
1091
|
-
}
|
|
1092
|
-
// A follow-up in someone else's session needs a write share.
|
|
1093
|
-
if (parentTask && !(await hasSessionWrite(req, myAgentId, parentTask.id))) {
|
|
1094
|
-
jsonError(res, SESSION_WRITE_DENIED, 403);
|
|
1066
|
+
if (parentTask && (await denyParentAttach(req, res, myAgentId, parentTask, principal))) {
|
|
1095
1067
|
return true;
|
|
1096
1068
|
}
|
|
1097
1069
|
// A chat-sdk contextKey binds the task to a chat thread that its assignee and creator
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
+
// `/unstable` keeps the full ACP v1 surface the 1.x default import carried:
|
|
2
|
+
// turn `usage` on the prompt response is unstable, and the stable entrypoint
|
|
3
|
+
// does not type it. Both entrypoints speak ACP v1 on the wire.
|
|
1
4
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
5
|
+
client as acpClient,
|
|
6
|
+
type ClientContext,
|
|
4
7
|
type McpServer,
|
|
8
|
+
methods,
|
|
5
9
|
ndJsonStream,
|
|
6
10
|
PROTOCOL_VERSION,
|
|
7
11
|
type RequestPermissionRequest,
|
|
8
12
|
type RequestPermissionResponse,
|
|
9
13
|
type SessionConfigOption,
|
|
10
14
|
type SessionNotification,
|
|
15
|
+
type SetSessionConfigOptionRequest,
|
|
16
|
+
type SetSessionConfigOptionResponse,
|
|
11
17
|
type Usage,
|
|
12
|
-
} from "@agentclientprotocol/sdk";
|
|
18
|
+
} from "@agentclientprotocol/sdk/unstable";
|
|
13
19
|
import pkg from "../../package.json";
|
|
14
20
|
import type { AcpSessionConfigOption } from "../types";
|
|
15
21
|
import { mintAcpSessionToken, revokeAcpSessionToken } from "../utils/acp-session-token";
|
|
@@ -154,7 +160,7 @@ function boundedAcpLogScalar(value: unknown): unknown {
|
|
|
154
160
|
return typeof value === "string" ? value.slice(0, 500) : value;
|
|
155
161
|
}
|
|
156
162
|
|
|
157
|
-
class SwarmAcpClient
|
|
163
|
+
class SwarmAcpClient {
|
|
158
164
|
/** Latest cumulative USD cost the target reported for the session. */
|
|
159
165
|
reportedCostUsd: number | null = null;
|
|
160
166
|
|
|
@@ -199,7 +205,7 @@ class ACPSession implements ProviderSession {
|
|
|
199
205
|
private ephemeralTokenId: string | null = null;
|
|
200
206
|
|
|
201
207
|
constructor(
|
|
202
|
-
private readonly
|
|
208
|
+
private readonly agent: ClientContext,
|
|
203
209
|
private readonly client: SwarmAcpClient,
|
|
204
210
|
private readonly process: Bun.Subprocess<"pipe", "pipe", "pipe">,
|
|
205
211
|
private readonly config: ProviderSessionConfig,
|
|
@@ -234,7 +240,7 @@ class ACPSession implements ProviderSession {
|
|
|
234
240
|
if (this.aborted) return;
|
|
235
241
|
this.aborted = true;
|
|
236
242
|
try {
|
|
237
|
-
await this.
|
|
243
|
+
await this.agent.notify(methods.agent.session.cancel, { sessionId: this.sessionId });
|
|
238
244
|
await Promise.race([this.completionPromise, Bun.sleep(ACP_CANCEL_SETTLE_MS)]);
|
|
239
245
|
} catch (err) {
|
|
240
246
|
this.emit({
|
|
@@ -282,7 +288,7 @@ class ACPSession implements ProviderSession {
|
|
|
282
288
|
private async runPrompt(): Promise<void> {
|
|
283
289
|
let result: ProviderResult;
|
|
284
290
|
try {
|
|
285
|
-
const response = await this.
|
|
291
|
+
const response = await this.agent.request(methods.agent.session.prompt, {
|
|
286
292
|
sessionId: this.sessionId,
|
|
287
293
|
prompt: [{ type: "text", text: this.config.prompt }],
|
|
288
294
|
});
|
|
@@ -441,11 +447,16 @@ export class ACPAdapter implements ProviderAdapter {
|
|
|
441
447
|
else preSessionEvents.push(event);
|
|
442
448
|
}, target);
|
|
443
449
|
const stream = ndJsonStream(fileSinkWritableStream(proc.stdin), proc.stdout);
|
|
444
|
-
const
|
|
450
|
+
const agent = acpClient()
|
|
451
|
+
.onRequest(methods.client.session.requestPermission, ({ params }) =>
|
|
452
|
+
client.requestPermission(params),
|
|
453
|
+
)
|
|
454
|
+
.onNotification(methods.client.session.update, ({ params }) => client.sessionUpdate(params))
|
|
455
|
+
.connect(stream).agent;
|
|
445
456
|
|
|
446
457
|
let ephemeralToken: { tokenId: string; plaintext: string } | null = null;
|
|
447
458
|
try {
|
|
448
|
-
await
|
|
459
|
+
await agent.request(methods.agent.initialize, {
|
|
449
460
|
protocolVersion: PROTOCOL_VERSION,
|
|
450
461
|
clientInfo: { name: "agent-swarm", version: pkg.version },
|
|
451
462
|
clientCapabilities: { session: { configOptions: { boolean: {} } } },
|
|
@@ -473,7 +484,7 @@ export class ACPAdapter implements ProviderAdapter {
|
|
|
473
484
|
// shape for an ACP agent with no network route to the swarm API. Gated on
|
|
474
485
|
// `mcpCapabilities.acp` and UNSTABLE; not adopted here.
|
|
475
486
|
const sessionMeta = target.sessionMeta?.(config);
|
|
476
|
-
const newSession = await
|
|
487
|
+
const newSession = await agent.request(methods.agent.session.new, {
|
|
477
488
|
...(sessionMeta ? { _meta: sessionMeta } : {}),
|
|
478
489
|
cwd: config.cwd,
|
|
479
490
|
mcpServers: [
|
|
@@ -491,13 +502,16 @@ export class ACPAdapter implements ProviderAdapter {
|
|
|
491
502
|
],
|
|
492
503
|
});
|
|
493
504
|
const configOptions = await applyConfiguredOptions(
|
|
494
|
-
|
|
505
|
+
{
|
|
506
|
+
setSessionConfigOption: (params) =>
|
|
507
|
+
agent.request(methods.agent.session.setConfigOption, params),
|
|
508
|
+
},
|
|
495
509
|
newSession.sessionId,
|
|
496
510
|
newSession.configOptions ?? [],
|
|
497
511
|
target.configuredOptions(config),
|
|
498
512
|
);
|
|
499
513
|
session = new ACPSession(
|
|
500
|
-
|
|
514
|
+
agent,
|
|
501
515
|
client,
|
|
502
516
|
proc,
|
|
503
517
|
config,
|
|
@@ -537,8 +551,14 @@ export class ACPAdapter implements ProviderAdapter {
|
|
|
537
551
|
}
|
|
538
552
|
}
|
|
539
553
|
|
|
554
|
+
export interface AcpConfigOptionSetter {
|
|
555
|
+
setSessionConfigOption(
|
|
556
|
+
params: SetSessionConfigOptionRequest,
|
|
557
|
+
): Promise<SetSessionConfigOptionResponse>;
|
|
558
|
+
}
|
|
559
|
+
|
|
540
560
|
export async function applyConfiguredOptions(
|
|
541
|
-
connection:
|
|
561
|
+
connection: AcpConfigOptionSetter,
|
|
542
562
|
sessionId: string,
|
|
543
563
|
advertised: SessionConfigOption[],
|
|
544
564
|
configured: Record<string, string | boolean>,
|
|
@@ -44,7 +44,7 @@ import type {
|
|
|
44
44
|
* default; the image installs exactly this version and the per-task settings
|
|
45
45
|
* set `amp.updates.mode: "disabled"`. Bump it deliberately, with a live run.
|
|
46
46
|
*/
|
|
47
|
-
export const AMP_PACKAGE = "@ampcode/cli@0.0.
|
|
47
|
+
export const AMP_PACKAGE = "@ampcode/cli@0.0.1792506219-g025e7e";
|
|
48
48
|
|
|
49
49
|
/** MCP server name. Amp exposes its tools as `agent_swarm` / `store_progress` (dashes become underscores). */
|
|
50
50
|
const AMP_MCP_SERVER_NAME = "agent-swarm";
|
|
@@ -364,7 +364,7 @@ export async function persistCodexOAuth(
|
|
|
364
364
|
* once it was issued more than that long ago (see {@link isStaleByAge}) — on
|
|
365
365
|
* top of the near-expiry skew above, not instead of it. This is how the
|
|
366
366
|
* locked keep-warm sweep (`POST /api/oauth/keep-warm/codex`) reuses this same
|
|
367
|
-
* function to refresh slots proactively (
|
|
367
|
+
* function to refresh slots proactively (`maxAgeMs` default 5 days) rather
|
|
368
368
|
* than waiting for a task to draw a slot within `REFRESH_SKEW_MS` of expiry.
|
|
369
369
|
* Default behavior (opts omitted) is unchanged.
|
|
370
370
|
*
|
|
@@ -318,7 +318,7 @@ export const LEGACY_POLICY = {
|
|
|
318
318
|
"config.write.any": leadOnly,
|
|
319
319
|
"config.delete.any": leadOnly,
|
|
320
320
|
"config.read.secrets": leadOnly,
|
|
321
|
-
// HTTP runtime tokens can read their own resolved config in src/http/config.ts.
|
|
321
|
+
// HTTP runtime and agent tokens can read their own resolved config in src/http/config.ts.
|
|
322
322
|
// Lead tokens retain config.read.secrets. This exception does not grant config administration.
|
|
323
323
|
"models.catalog.write": leadOrOperator,
|
|
324
324
|
"models.harness-support.write": agentOrOperator,
|
|
@@ -356,6 +356,8 @@ export const LEGACY_POLICY = {
|
|
|
356
356
|
"runtime_pool.write": operatorOrUser,
|
|
357
357
|
"runtime_pool.bind": leadOrOperatorOrUser,
|
|
358
358
|
"runtime_controller.write": operatorOrUser,
|
|
359
|
+
// A worker token stands in for the swarm API key on a worker, so only humans mint it.
|
|
360
|
+
"agent.token.manage": operatorOrUser,
|
|
359
361
|
"extension.write": extensionWrite,
|
|
360
362
|
"extension.activate": extensionActivation,
|
|
361
363
|
"extension.install.inline": leadOrOperatorOrUser,
|
package/src/rbac/permissions.ts
CHANGED
|
@@ -33,6 +33,10 @@ export const PERMISSIONS = {
|
|
|
33
33
|
description: "View or diff another agent's context history.",
|
|
34
34
|
namespace: "agent",
|
|
35
35
|
},
|
|
36
|
+
"agent.token.manage": {
|
|
37
|
+
description: "Mint, rotate, or revoke an agent-bound worker token.",
|
|
38
|
+
namespace: "agent",
|
|
39
|
+
},
|
|
36
40
|
"task.cancel.any": {
|
|
37
41
|
description: "Cancel any task (beyond tasks the caller created).",
|
|
38
42
|
namespace: "task",
|