@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/be/db.ts
CHANGED
|
@@ -813,6 +813,9 @@ export async function getSlackOutcomeMessage(taskId: string): Promise<SlackMessa
|
|
|
813
813
|
* subsequent re-defers to the latest wake, keeping the original card open.
|
|
814
814
|
* `deferredAt` (not the `deferred` tag) identifies the deferring task,
|
|
815
815
|
* because the wake-up task inherits the tag.
|
|
816
|
+
*
|
|
817
|
+
* CROSS JOIN and `+wake.taskType` pin the join order to the small chain. Without
|
|
818
|
+
* them the planner drives the outer join from every terminal task (~1s at 150k tasks).
|
|
816
819
|
*/
|
|
817
820
|
export async function getResolvableDeferralOutcomes(
|
|
818
821
|
limit = 50,
|
|
@@ -826,13 +829,13 @@ export async function getResolvableDeferralOutcomes(
|
|
|
826
829
|
AND card.ts NOT LIKE 'pending:%'
|
|
827
830
|
UNION
|
|
828
831
|
SELECT chain.card_id, wake.id FROM chain
|
|
829
|
-
JOIN agent_tasks parent ON parent.id = chain.task_id AND parent.deferredAt IS NOT NULL
|
|
830
|
-
JOIN agent_tasks wake ON wake.parentTaskId = parent.id AND wake.taskType = 'deferred'
|
|
832
|
+
CROSS JOIN agent_tasks parent ON parent.id = chain.task_id AND parent.deferredAt IS NOT NULL
|
|
833
|
+
CROSS JOIN agent_tasks wake ON wake.parentTaskId = parent.id AND +wake.taskType = 'deferred'
|
|
831
834
|
)
|
|
832
835
|
SELECT card.*, wake.id AS wake_task_id
|
|
833
836
|
FROM chain
|
|
834
|
-
JOIN slack_messages card ON card.id = chain.card_id
|
|
835
|
-
JOIN agent_tasks wake ON wake.id = chain.task_id AND wake.id != card.task_id
|
|
837
|
+
CROSS JOIN slack_messages card ON card.id = chain.card_id
|
|
838
|
+
CROSS JOIN agent_tasks wake ON wake.id = chain.task_id AND wake.id != card.task_id
|
|
836
839
|
WHERE card.kind = 'outcome'
|
|
837
840
|
AND card.finalized_at IS NOT NULL
|
|
838
841
|
AND card.deferral_resolved_at IS NULL
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
-- getSlackTasksMissingTree runs on every Slack watcher tick. Without this index it walks
|
|
2
|
+
-- idx_agent_tasks_last_updated across every task to find the few Slack ones.
|
|
3
|
+
CREATE INDEX IF NOT EXISTS idx_agent_tasks_slack_last_updated
|
|
4
|
+
ON agent_tasks(lastUpdatedAt DESC)
|
|
5
|
+
WHERE source = 'slack' AND slackChannelId IS NOT NULL AND slackThreadTs IS NOT NULL;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
PRAGMA defer_foreign_keys = ON;
|
|
2
|
+
|
|
3
|
+
-- Add agent-bound worker tokens: a fourth binding that names only the agent.
|
|
4
|
+
-- parentTokenId links a child task token to the agent token that minted it,
|
|
5
|
+
-- so revoking the agent token revokes its children.
|
|
6
|
+
CREATE TABLE session_tokens_new (
|
|
7
|
+
id TEXT PRIMARY KEY,
|
|
8
|
+
tokenHash TEXT NOT NULL UNIQUE,
|
|
9
|
+
tokenPreview TEXT NOT NULL,
|
|
10
|
+
agentId TEXT NOT NULL,
|
|
11
|
+
taskId TEXT,
|
|
12
|
+
runtimeId TEXT,
|
|
13
|
+
parentRuntimeId TEXT,
|
|
14
|
+
controllerId TEXT,
|
|
15
|
+
parentTokenId TEXT,
|
|
16
|
+
expiresAt TEXT NOT NULL,
|
|
17
|
+
revokedAt TEXT,
|
|
18
|
+
createdAt TEXT NOT NULL DEFAULT (datetime('now')),
|
|
19
|
+
lastUsedAt TEXT,
|
|
20
|
+
CHECK (
|
|
21
|
+
(taskId IS NOT NULL AND runtimeId IS NULL AND controllerId IS NULL
|
|
22
|
+
AND (parentRuntimeId IS NULL OR parentTokenId IS NULL))
|
|
23
|
+
OR (taskId IS NULL AND runtimeId IS NOT NULL AND parentRuntimeId IS NULL AND controllerId IS NULL AND parentTokenId IS NULL)
|
|
24
|
+
OR (controllerId IS NOT NULL AND taskId IS NULL AND runtimeId IS NULL AND parentRuntimeId IS NULL AND parentTokenId IS NULL)
|
|
25
|
+
OR (taskId IS NULL AND runtimeId IS NULL AND controllerId IS NULL AND parentRuntimeId IS NULL AND parentTokenId IS NULL)
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
INSERT INTO session_tokens_new
|
|
30
|
+
(id, tokenHash, tokenPreview, agentId, taskId, runtimeId, parentRuntimeId, controllerId,
|
|
31
|
+
expiresAt, revokedAt, createdAt, lastUsedAt)
|
|
32
|
+
SELECT id, tokenHash, tokenPreview, agentId, taskId, runtimeId, parentRuntimeId, controllerId,
|
|
33
|
+
expiresAt, revokedAt, createdAt, lastUsedAt
|
|
34
|
+
FROM session_tokens;
|
|
35
|
+
|
|
36
|
+
DROP TABLE session_tokens;
|
|
37
|
+
ALTER TABLE session_tokens_new RENAME TO session_tokens;
|
|
38
|
+
CREATE INDEX idx_session_tokens_agentId ON session_tokens(agentId);
|
|
39
|
+
CREATE INDEX idx_session_tokens_runtimeId ON session_tokens(runtimeId);
|
|
40
|
+
CREATE INDEX idx_session_tokens_parentRuntimeId ON session_tokens(parentRuntimeId);
|
|
41
|
+
CREATE INDEX idx_session_tokens_parentTokenId ON session_tokens(parentTokenId);
|
|
@@ -189,11 +189,20 @@ async function buildState(ctx: any, leadId: string, nowMs: number, timeZone: str
|
|
|
189
189
|
const mmdd = new Date(nowMs).toISOString().slice(5, 10);
|
|
190
190
|
const mmddLocal = stamp.slice(5, 10);
|
|
191
191
|
const due: string[] = [];
|
|
192
|
+
// An MM-DD counts only as a bare token: "release on 10-20" is due, while a
|
|
193
|
+
// re-verification stamp ("2026-10-20 ~10:45Z", a UUID piece) and a suffixed
|
|
194
|
+
// token ("10-20s") are not.
|
|
195
|
+
const mentionsMmdd = (s: string, tok: string) =>
|
|
196
|
+
new RegExp(`(?<![\\w-])${tok}(?![\\w-])`).test(s);
|
|
192
197
|
for (const line of String(lead[0]?.heartbeatMd ?? "").split("\n")) {
|
|
193
198
|
const l = line.trim();
|
|
194
199
|
if (!l) continue;
|
|
195
200
|
const del = /DELETE <?(\d\d-\d\d)>?/.exec(l);
|
|
196
|
-
if (
|
|
201
|
+
if (
|
|
202
|
+
mentionsMmdd(l, mmdd) ||
|
|
203
|
+
mentionsMmdd(l, mmddLocal) ||
|
|
204
|
+
(del?.[1] && del[1] <= mmddLocal)
|
|
205
|
+
) {
|
|
197
206
|
due.push(oneLine(l).slice(0, 200));
|
|
198
207
|
}
|
|
199
208
|
if (due.length >= 6) break;
|
|
@@ -149,6 +149,17 @@ export const DB_RETENTION_TUNING_BOUNDS = {
|
|
|
149
149
|
*/
|
|
150
150
|
export const APPS_JSON_COLUMN_MAX_BYTES_BOUNDS = { min: 1_024, max: 16_777_216 } as const;
|
|
151
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Bounds for the Codex OAuth keep-warm age threshold
|
|
154
|
+
* (`CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS`).
|
|
155
|
+
*
|
|
156
|
+
* `src/http/codex-oauth-keep-warm.ts` reads the knob against exactly these at
|
|
157
|
+
* call time and falls back to its default outside them. The 216 h (9 day)
|
|
158
|
+
* ceiling stays under the 9.5 day point where task-time near-expiry refresh
|
|
159
|
+
* takes over, so every accepted value still changes when a slot refreshes.
|
|
160
|
+
*/
|
|
161
|
+
export const CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS_BOUNDS = { min: 1, max: 216 } as const;
|
|
162
|
+
|
|
152
163
|
/** Build `{ KEY: validator }` entries accepting only boolean literals. */
|
|
153
164
|
function booleanValidators(keys: string[]): Record<string, ConfigValidator> {
|
|
154
165
|
const message = (key: string) =>
|
|
@@ -542,6 +553,11 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
|
|
|
542
553
|
APPS_JSON_COLUMN_MAX_BYTES_BOUNDS.min,
|
|
543
554
|
APPS_JSON_COLUMN_MAX_BYTES_BOUNDS.max,
|
|
544
555
|
),
|
|
556
|
+
...boundedIntegerValidators(
|
|
557
|
+
["CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS"],
|
|
558
|
+
CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS_BOUNDS.min,
|
|
559
|
+
CODEX_OAUTH_KEEP_WARM_MAX_AGE_HOURS_BOUNDS.max,
|
|
560
|
+
),
|
|
545
561
|
...boundedIntegerValidators(
|
|
546
562
|
[
|
|
547
563
|
"SESSION_LOG_RETENTION_DAYS",
|
package/src/be/users.ts
CHANGED
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
23
23
|
import { scheduleOrgDomainRecompute } from "../telemetry-identity";
|
|
24
24
|
import type { User } from "../types";
|
|
25
|
-
import { unregisterVolatileSecret } from "../utils/secret-scrubber";
|
|
25
|
+
import { registerVolatileSecret, unregisterVolatileSecret } from "../utils/secret-scrubber";
|
|
26
26
|
import { closeSessionTokenConnections } from "../utils/session-token-connections";
|
|
27
27
|
import { getDbClient } from "./db";
|
|
28
|
+
import type { DbExecutor } from "./db-client";
|
|
28
29
|
import { validateRuntimeTokenTtlHours } from "./swarm-config-guard";
|
|
29
30
|
|
|
30
31
|
// ---------------------------------------------------------------------------
|
|
@@ -547,6 +548,12 @@ export async function resolveUserByToken(plaintext: string): Promise<User | null
|
|
|
547
548
|
|
|
548
549
|
const SESSION_TOKEN_PREFIX = "aseph_"; // agent-swarm-ephemeral
|
|
549
550
|
|
|
551
|
+
/**
|
|
552
|
+
* What an agent-principal session token is bound to: one task (ACP), one
|
|
553
|
+
* runtime instance (runtime pools), or only its agent (a worker token).
|
|
554
|
+
*/
|
|
555
|
+
export type SessionTokenKind = "task" | "runtime" | "agent";
|
|
556
|
+
|
|
550
557
|
export class InvalidRuntimeTokenError extends Error {
|
|
551
558
|
constructor() {
|
|
552
559
|
super("Runtime token is no longer valid");
|
|
@@ -563,14 +570,21 @@ export async function mintSessionToken(
|
|
|
563
570
|
agentId: string,
|
|
564
571
|
taskId: string,
|
|
565
572
|
ttlMs: number,
|
|
566
|
-
parent?: { tokenId: string; runtimeId: string },
|
|
573
|
+
parent?: { tokenId: string; runtimeId: string | null },
|
|
567
574
|
): Promise<{ tokenId: string; plaintext: string }> {
|
|
568
575
|
if (parent) {
|
|
569
576
|
return await getDbClient().transaction(async (client) => {
|
|
577
|
+
// A runtime parent is matched by its runtime; an agent parent has no
|
|
578
|
+
// binding but its agent, so it is matched by the empty binding columns.
|
|
570
579
|
const row = await client.get<{ expiresAt: string }>(
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
580
|
+
parent.runtimeId
|
|
581
|
+
? `SELECT expiresAt FROM session_tokens WHERE id = ? AND agentId = ? AND runtimeId = ?
|
|
582
|
+
AND revokedAt IS NULL AND expiresAt > ?`
|
|
583
|
+
: `SELECT expiresAt FROM session_tokens WHERE id = ? AND agentId = ? AND ${AGENT_TOKEN_BINDING}
|
|
584
|
+
AND revokedAt IS NULL AND expiresAt > ?`,
|
|
585
|
+
parent.runtimeId
|
|
586
|
+
? [parent.tokenId, agentId, parent.runtimeId, new Date().toISOString()]
|
|
587
|
+
: [parent.tokenId, agentId, new Date().toISOString()],
|
|
574
588
|
);
|
|
575
589
|
if (!row) throw new InvalidRuntimeTokenError();
|
|
576
590
|
return await insertSessionToken(
|
|
@@ -580,6 +594,8 @@ export async function mintSessionToken(
|
|
|
580
594
|
ttlMs,
|
|
581
595
|
parent.runtimeId,
|
|
582
596
|
row.expiresAt,
|
|
597
|
+
null,
|
|
598
|
+
parent.runtimeId ? null : parent.tokenId,
|
|
583
599
|
);
|
|
584
600
|
});
|
|
585
601
|
}
|
|
@@ -607,6 +623,7 @@ export async function insertSessionToken(
|
|
|
607
623
|
parentRuntimeId: string | null = null,
|
|
608
624
|
expiresAtCeiling?: string,
|
|
609
625
|
controllerId: string | null = null,
|
|
626
|
+
parentTokenId: string | null = null,
|
|
610
627
|
): Promise<{ tokenId: string; plaintext: string }> {
|
|
611
628
|
const plaintext = `${SESSION_TOKEN_PREFIX}${base62(randomBytes(24))}`;
|
|
612
629
|
const tokenId = randomUUID().replace(/-/g, "");
|
|
@@ -619,8 +636,8 @@ export async function insertSessionToken(
|
|
|
619
636
|
|
|
620
637
|
await getDbClient().run(
|
|
621
638
|
`INSERT INTO session_tokens
|
|
622
|
-
(id, tokenHash, tokenPreview, agentId, taskId, runtimeId, parentRuntimeId, expiresAt, createdAt, controllerId)
|
|
623
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
639
|
+
(id, tokenHash, tokenPreview, agentId, taskId, runtimeId, parentRuntimeId, expiresAt, createdAt, controllerId, parentTokenId)
|
|
640
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
624
641
|
[
|
|
625
642
|
tokenId,
|
|
626
643
|
hash,
|
|
@@ -632,6 +649,7 @@ export async function insertSessionToken(
|
|
|
632
649
|
expiresAt,
|
|
633
650
|
now.toISOString(),
|
|
634
651
|
controllerId,
|
|
652
|
+
parentTokenId,
|
|
635
653
|
],
|
|
636
654
|
);
|
|
637
655
|
|
|
@@ -648,15 +666,31 @@ export async function revokeSessionToken(tokenId: string): Promise<void> {
|
|
|
648
666
|
if (row?.runtimeId) {
|
|
649
667
|
await revokeRuntimeTokensForRuntime(row.runtimeId);
|
|
650
668
|
} else {
|
|
651
|
-
await client
|
|
652
|
-
"UPDATE session_tokens SET revokedAt = ? WHERE id = ? AND revokedAt IS NULL",
|
|
653
|
-
[new Date().toISOString(), tokenId],
|
|
654
|
-
);
|
|
655
|
-
getDbClient().afterCommit(() => closeSessionTokenConnections([tokenId]));
|
|
669
|
+
await revokeTokensWithChildren(client, [tokenId]);
|
|
656
670
|
}
|
|
657
671
|
});
|
|
658
672
|
}
|
|
659
673
|
|
|
674
|
+
/** Revoke tokens and the child task tokens an agent token minted. */
|
|
675
|
+
async function revokeTokensWithChildren(client: DbExecutor, tokenIds: string[]) {
|
|
676
|
+
if (tokenIds.length === 0) return;
|
|
677
|
+
const placeholders = tokenIds.map(() => "?").join(", ");
|
|
678
|
+
const children = await client.query<{ id: string }>(
|
|
679
|
+
`SELECT id FROM session_tokens WHERE parentTokenId IN (${placeholders})`,
|
|
680
|
+
tokenIds,
|
|
681
|
+
);
|
|
682
|
+
const ids = [...tokenIds, ...children.map((row) => row.id)];
|
|
683
|
+
await client.run(
|
|
684
|
+
`UPDATE session_tokens SET revokedAt = ?
|
|
685
|
+
WHERE id IN (${ids.map(() => "?").join(", ")}) AND revokedAt IS NULL`,
|
|
686
|
+
[new Date().toISOString(), ...ids],
|
|
687
|
+
);
|
|
688
|
+
getDbClient().afterCommit(() => {
|
|
689
|
+
for (const id of tokenIds) unregisterVolatileSecret(`agent-token:${id}`);
|
|
690
|
+
closeSessionTokenConnections(ids);
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
660
694
|
export async function revokeRuntimeTokensForRuntime(runtimeId: string): Promise<void> {
|
|
661
695
|
await getDbClient().transaction(async (client) => {
|
|
662
696
|
const rows = await client.query<{ id: string }>(
|
|
@@ -675,6 +709,126 @@ export async function revokeRuntimeTokensForRuntime(runtimeId: string): Promise<
|
|
|
675
709
|
});
|
|
676
710
|
}
|
|
677
711
|
|
|
712
|
+
// ---------------------------------------------------------------------------
|
|
713
|
+
// Agent tokens (long-lived, one agent, no runtime)
|
|
714
|
+
// ---------------------------------------------------------------------------
|
|
715
|
+
|
|
716
|
+
/** SQL predicate for an agent token: bound to its agent and nothing else. */
|
|
717
|
+
const AGENT_TOKEN_BINDING =
|
|
718
|
+
"taskId IS NULL AND runtimeId IS NULL AND controllerId IS NULL AND parentRuntimeId IS NULL";
|
|
719
|
+
|
|
720
|
+
/** Stored expiry of a token that does not expire. `expiresAt` is NOT NULL. */
|
|
721
|
+
export const AGENT_TOKEN_NO_EXPIRY = "9999-12-31T23:59:59.999Z";
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Default agent-token lifetime in ms. `null` means the token does not expire
|
|
725
|
+
* and stays valid until it is rotated or revoked. A fixed lifetime (for
|
|
726
|
+
* example 90 days) is a one-line change here.
|
|
727
|
+
*/
|
|
728
|
+
export const AGENT_TOKEN_DEFAULT_TTL_MS: number | null = null;
|
|
729
|
+
|
|
730
|
+
export type AgentTokenCredential = {
|
|
731
|
+
tokenId: string;
|
|
732
|
+
plaintext: string;
|
|
733
|
+
/** null when the token does not expire. */
|
|
734
|
+
expiresAt: string | null;
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
export class AgentTokenExistsError extends Error {
|
|
738
|
+
constructor() {
|
|
739
|
+
super("Agent already has an active token; rotate or revoke it");
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function storedExpiry(ttlMs: number | null): string {
|
|
744
|
+
return ttlMs === null ? AGENT_TOKEN_NO_EXPIRY : new Date(Date.now() + ttlMs).toISOString();
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function publicExpiry(expiresAt: string): string | null {
|
|
748
|
+
return expiresAt === AGENT_TOKEN_NO_EXPIRY ? null : expiresAt;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async function activeAgentTokenIds(client: DbExecutor, agentId: string) {
|
|
752
|
+
const rows = await client.query<{ id: string }>(
|
|
753
|
+
`SELECT id FROM session_tokens WHERE agentId = ? AND ${AGENT_TOKEN_BINDING}
|
|
754
|
+
AND revokedAt IS NULL AND expiresAt > ?`,
|
|
755
|
+
[agentId, new Date().toISOString()],
|
|
756
|
+
);
|
|
757
|
+
return rows.map((row) => row.id);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async function insertAgentToken(
|
|
761
|
+
agentId: string,
|
|
762
|
+
ttlMs: number | null,
|
|
763
|
+
): Promise<AgentTokenCredential> {
|
|
764
|
+
const expiresAt = storedExpiry(ttlMs);
|
|
765
|
+
// Pass the expiry as the ceiling so it is stored exactly; a TTL recomputed
|
|
766
|
+
// from a later clock read could step past AGENT_TOKEN_NO_EXPIRY into year 10000.
|
|
767
|
+
const token = await insertSessionToken(
|
|
768
|
+
agentId,
|
|
769
|
+
null,
|
|
770
|
+
null,
|
|
771
|
+
Number.MAX_SAFE_INTEGER,
|
|
772
|
+
null,
|
|
773
|
+
expiresAt,
|
|
774
|
+
);
|
|
775
|
+
getDbClient().afterCommit(() =>
|
|
776
|
+
registerVolatileSecret(token.plaintext, `agent-token:${token.tokenId}`),
|
|
777
|
+
);
|
|
778
|
+
return { ...token, expiresAt: publicExpiry(expiresAt) };
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Mint the agent-bound worker token for `agentId`. A worker runs on it in
|
|
783
|
+
* place of the swarm API key. Throws `AgentTokenExistsError` when the agent
|
|
784
|
+
* already holds an active one, so tokens do not accumulate silently.
|
|
785
|
+
*/
|
|
786
|
+
export async function mintAgentToken(
|
|
787
|
+
agentId: string,
|
|
788
|
+
ttlMs: number | null = AGENT_TOKEN_DEFAULT_TTL_MS,
|
|
789
|
+
): Promise<AgentTokenCredential> {
|
|
790
|
+
return await getDbClient().transaction(async (client) => {
|
|
791
|
+
if ((await activeAgentTokenIds(client, agentId)).length > 0) {
|
|
792
|
+
throw new AgentTokenExistsError();
|
|
793
|
+
}
|
|
794
|
+
return await insertAgentToken(agentId, ttlMs);
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Replace the agent's active token. The old token keeps working for
|
|
800
|
+
* `graceMs` so a running worker can pick up the new one, then expires. Its
|
|
801
|
+
* child task tokens expire with it, so no credential it delegated outlives
|
|
802
|
+
* it. Returns null when the agent holds no active token.
|
|
803
|
+
*/
|
|
804
|
+
export async function rotateAgentToken(
|
|
805
|
+
agentId: string,
|
|
806
|
+
graceMs: number,
|
|
807
|
+
ttlMs: number | null = AGENT_TOKEN_DEFAULT_TTL_MS,
|
|
808
|
+
): Promise<(AgentTokenCredential & { previousExpiresAt: string }) | null> {
|
|
809
|
+
return await getDbClient().transaction(async (client) => {
|
|
810
|
+
const previous = await activeAgentTokenIds(client, agentId);
|
|
811
|
+
if (previous.length === 0) return null;
|
|
812
|
+
const previousExpiresAt = new Date(Date.now() + graceMs).toISOString();
|
|
813
|
+
const placeholders = previous.map(() => "?").join(", ");
|
|
814
|
+
await client.run(
|
|
815
|
+
`UPDATE session_tokens SET expiresAt = ?
|
|
816
|
+
WHERE (id IN (${placeholders}) OR parentTokenId IN (${placeholders})) AND expiresAt > ?`,
|
|
817
|
+
[previousExpiresAt, ...previous, ...previous, previousExpiresAt],
|
|
818
|
+
);
|
|
819
|
+
return { ...(await insertAgentToken(agentId, ttlMs)), previousExpiresAt };
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/** Revoke every active agent token of `agentId` and their child task tokens. */
|
|
824
|
+
export async function revokeAgentTokens(agentId: string): Promise<number> {
|
|
825
|
+
return await getDbClient().transaction(async (client) => {
|
|
826
|
+
const ids = await activeAgentTokenIds(client, agentId);
|
|
827
|
+
await revokeTokensWithChildren(client, ids);
|
|
828
|
+
return ids.length;
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
|
|
678
832
|
/**
|
|
679
833
|
* Resolve a plaintext `aseph_` token to its principal. Returns null when the
|
|
680
834
|
* token is unknown, revoked, or past its expiry. On a hit, touches `lastUsedAt`
|
|
@@ -685,6 +839,7 @@ export async function resolveBySessionToken(
|
|
|
685
839
|
options: { touch?: boolean } = {},
|
|
686
840
|
): Promise<{
|
|
687
841
|
tokenId: string;
|
|
842
|
+
tokenKind: SessionTokenKind;
|
|
688
843
|
agentId: string;
|
|
689
844
|
taskId: string | null;
|
|
690
845
|
runtimeId: string | null;
|
|
@@ -704,6 +859,7 @@ export async function resolveSessionToken(
|
|
|
704
859
|
| {
|
|
705
860
|
kind: "agent";
|
|
706
861
|
tokenId: string;
|
|
862
|
+
tokenKind: SessionTokenKind;
|
|
707
863
|
agentId: string;
|
|
708
864
|
taskId: string | null;
|
|
709
865
|
runtimeId: string | null;
|
|
@@ -758,6 +914,7 @@ export async function resolveSessionToken(
|
|
|
758
914
|
return {
|
|
759
915
|
kind: "agent",
|
|
760
916
|
tokenId: row.id,
|
|
917
|
+
tokenKind: row.taskId !== null ? "task" : row.runtimeId !== null ? "runtime" : "agent",
|
|
761
918
|
agentId: row.agentId,
|
|
762
919
|
taskId: row.taskId,
|
|
763
920
|
runtimeId: row.runtimeId,
|
package/src/cli.tsx
CHANGED
|
@@ -360,6 +360,25 @@ const COMMAND_HELP: Record<
|
|
|
360
360
|
` ${binName} claude-managed-setup --api-url https://swarm.example.com`,
|
|
361
361
|
].join("\n"),
|
|
362
362
|
},
|
|
363
|
+
"worker-token": {
|
|
364
|
+
usage: `${binName} worker-token <create|rotate|revoke> (--agent-id <id> | --name <name>) [options]`,
|
|
365
|
+
description:
|
|
366
|
+
"Manage the agent-bound token a worker runs on in place of the swarm API key. The token acts only as its agent. create and rotate print the new token to stdout once; messages go to stderr. Uses your operator swarm API key.",
|
|
367
|
+
options: [
|
|
368
|
+
" --agent-id <id> Agent the token is bound to",
|
|
369
|
+
" --name <name> Agent name; create makes the agent when no agent has this name",
|
|
370
|
+
" --ttl-days <n> Token lifetime (create, rotate). Default: no expiry",
|
|
371
|
+
" --grace-sec <n> How long the old token keeps working after rotate (default 300)",
|
|
372
|
+
" --api-url <url> Swarm API URL (default: MCP_BASE_URL or http://localhost:3013)",
|
|
373
|
+
" --api-key <key> Operator swarm API key (default: AGENT_SWARM_API_KEY or API_KEY, falling back to 123123)",
|
|
374
|
+
" -h, --help Show this help",
|
|
375
|
+
].join("\n"),
|
|
376
|
+
examples: [
|
|
377
|
+
` ${binName} worker-token create --name worker-1`,
|
|
378
|
+
` ${binName} worker-token rotate --agent-id <agent-id> --grace-sec 600`,
|
|
379
|
+
` ${binName} worker-token revoke --agent-id <agent-id>`,
|
|
380
|
+
].join("\n"),
|
|
381
|
+
},
|
|
363
382
|
e2b: {
|
|
364
383
|
usage: `${binName} e2b <subcommand> [options]`,
|
|
365
384
|
description:
|
|
@@ -445,6 +464,7 @@ function printHelp(command?: string) {
|
|
|
445
464
|
["docs", "Open documentation (--open to launch in browser)"],
|
|
446
465
|
["codex-login", "Authenticate Codex via ChatGPT OAuth"],
|
|
447
466
|
["claude-managed-setup", "Bootstrap Anthropic Managed Agents (agent + env + skills)"],
|
|
467
|
+
["worker-token", "Mint, rotate, or revoke an agent-bound worker token"],
|
|
448
468
|
["e2b", "Build templates and start E2B API/worker sandboxes"],
|
|
449
469
|
["version", "Show version number"],
|
|
450
470
|
["help", "Show this help message"],
|
|
@@ -803,6 +823,10 @@ if (args.showHelp || args.command === "help" || args.command === undefined) {
|
|
|
803
823
|
const { runClaudeManagedSetup } = await import("./commands/claude-managed-setup");
|
|
804
824
|
const setupArgs = process.argv.slice(process.argv.indexOf("claude-managed-setup") + 1);
|
|
805
825
|
await runClaudeManagedSetup(setupArgs);
|
|
826
|
+
} else if (args.command === "worker-token") {
|
|
827
|
+
const { runWorkerToken } = await import("./commands/worker-token");
|
|
828
|
+
const tokenArgs = process.argv.slice(process.argv.indexOf("worker-token") + 1);
|
|
829
|
+
process.exit(await runWorkerToken(tokenArgs));
|
|
806
830
|
} else if (args.command === "e2b") {
|
|
807
831
|
const { runE2BCommand } = await import("./commands/e2b");
|
|
808
832
|
const e2bArgs = process.argv.slice(process.argv.indexOf("e2b") + 1);
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agent-swarm worker-token create|rotate|revoke`: manage the agent-bound
|
|
3
|
+
* token a worker runs on in place of the swarm API key.
|
|
4
|
+
*
|
|
5
|
+
* Operator-run. It authenticates with the operator's own swarm API key and
|
|
6
|
+
* talks to the API over HTTP only. The new token goes to stdout, once, so it
|
|
7
|
+
* can be captured (`API_KEY=$(agent-swarm worker-token create --name w1)`);
|
|
8
|
+
* everything else goes to stderr.
|
|
9
|
+
*/
|
|
10
|
+
import { getApiKey } from "../utils/api-key";
|
|
11
|
+
|
|
12
|
+
type Action = "create" | "rotate" | "revoke";
|
|
13
|
+
|
|
14
|
+
export type WorkerTokenArgs = {
|
|
15
|
+
action?: Action;
|
|
16
|
+
agentId?: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
apiUrl?: string;
|
|
19
|
+
apiKey?: string;
|
|
20
|
+
ttlDays?: number;
|
|
21
|
+
graceSec?: number;
|
|
22
|
+
showHelp: boolean;
|
|
23
|
+
error?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const WORKER_TOKEN_USAGE = `Usage:
|
|
27
|
+
agent-swarm worker-token <create|rotate|revoke> (--agent-id <id> | --name <name>) [options]
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
--agent-id <id> Agent the token is bound to
|
|
31
|
+
--name <name> Agent name. create makes the agent when no agent has this name
|
|
32
|
+
--ttl-days <n> Token lifetime (create, rotate). Default: no expiry
|
|
33
|
+
--grace-sec <n> How long the old token keeps working after rotate (default 300)
|
|
34
|
+
--api-url <url> Swarm API URL (default: MCP_BASE_URL or http://localhost:3013)
|
|
35
|
+
--api-key <key> Operator swarm API key (default: AGENT_SWARM_API_KEY or API_KEY, falling back to 123123)
|
|
36
|
+
-h, --help Show this help`;
|
|
37
|
+
|
|
38
|
+
const ACTIONS = new Set<string>(["create", "rotate", "revoke"]);
|
|
39
|
+
|
|
40
|
+
export function parseWorkerTokenArgs(args: string[]): WorkerTokenArgs {
|
|
41
|
+
const parsed: WorkerTokenArgs = { showHelp: false };
|
|
42
|
+
const number = (flag: string, value: string | undefined, min: number) => {
|
|
43
|
+
const n = Number(value);
|
|
44
|
+
if (!Number.isInteger(n) || n < min) parsed.error = `${flag} must be an integer >= ${min}`;
|
|
45
|
+
return n;
|
|
46
|
+
};
|
|
47
|
+
for (let i = 0; i < args.length; i++) {
|
|
48
|
+
const arg = args[i]!;
|
|
49
|
+
if (arg === "--help" || arg === "-h") parsed.showHelp = true;
|
|
50
|
+
else if (arg === "--agent-id") parsed.agentId = args[++i];
|
|
51
|
+
else if (arg === "--name") parsed.name = args[++i];
|
|
52
|
+
else if (arg === "--api-url") parsed.apiUrl = args[++i];
|
|
53
|
+
else if (arg === "--api-key") parsed.apiKey = args[++i];
|
|
54
|
+
else if (arg === "--ttl-days") parsed.ttlDays = number(arg, args[++i], 1);
|
|
55
|
+
else if (arg === "--grace-sec") parsed.graceSec = number(arg, args[++i], 0);
|
|
56
|
+
else if (!parsed.action && ACTIONS.has(arg)) parsed.action = arg as Action;
|
|
57
|
+
else parsed.error = `Unknown argument: ${arg}`;
|
|
58
|
+
}
|
|
59
|
+
if (parsed.error || parsed.showHelp) return parsed;
|
|
60
|
+
if (!parsed.action) parsed.error = "Missing action: create, rotate, or revoke";
|
|
61
|
+
else if (!parsed.agentId === !parsed.name)
|
|
62
|
+
parsed.error = "Pass exactly one of --agent-id or --name";
|
|
63
|
+
return parsed;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type Deps = {
|
|
67
|
+
env?: Record<string, string | undefined>;
|
|
68
|
+
fetch?: typeof fetch;
|
|
69
|
+
stdout?: (line: string) => void;
|
|
70
|
+
stderr?: (line: string) => void;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
async function readError(res: Response): Promise<string> {
|
|
74
|
+
const text = await res.text().catch(() => "");
|
|
75
|
+
try {
|
|
76
|
+
return (JSON.parse(text) as { error?: string }).error ?? text;
|
|
77
|
+
} catch {
|
|
78
|
+
return text;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Runs the command. Returns the process exit code. */
|
|
83
|
+
export async function runWorkerToken(args: string[], deps: Deps = {}): Promise<number> {
|
|
84
|
+
const env = deps.env ?? process.env;
|
|
85
|
+
const doFetch = deps.fetch ?? fetch;
|
|
86
|
+
const stdout = deps.stdout ?? ((line: string) => console.log(line));
|
|
87
|
+
const stderr = deps.stderr ?? ((line: string) => console.error(line));
|
|
88
|
+
const parsed = parseWorkerTokenArgs(args);
|
|
89
|
+
if (parsed.showHelp) {
|
|
90
|
+
stdout(WORKER_TOKEN_USAGE);
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
if (parsed.error) {
|
|
94
|
+
stderr(`${parsed.error}\n\n${WORKER_TOKEN_USAGE}`);
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const apiUrl = (parsed.apiUrl ?? env.MCP_BASE_URL ?? "http://localhost:3013").replace(/\/+$/, "");
|
|
99
|
+
const apiKey = parsed.apiKey ?? (getApiKey(env) || "123123");
|
|
100
|
+
const headers = { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" };
|
|
101
|
+
|
|
102
|
+
let agentId = parsed.agentId;
|
|
103
|
+
let name: string | undefined;
|
|
104
|
+
if (!agentId) {
|
|
105
|
+
const res = await doFetch(`${apiUrl}/api/agents`, { headers });
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
stderr(`Listing agents failed (${res.status}): ${await readError(res)}`);
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
const { agents } = (await res.json()) as { agents: { id: string; name: string }[] };
|
|
111
|
+
const matches = agents.filter((agent) => agent.name === parsed.name);
|
|
112
|
+
if (matches.length > 1) {
|
|
113
|
+
stderr(`Several agents are named "${parsed.name}"; pass --agent-id instead`);
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
agentId = matches[0]?.id;
|
|
117
|
+
if (!agentId) {
|
|
118
|
+
if (parsed.action !== "create") {
|
|
119
|
+
stderr(`No agent is named "${parsed.name}"`);
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
agentId = crypto.randomUUID();
|
|
123
|
+
name = parsed.name;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const base = `${apiUrl}/api/agents/${encodeURIComponent(agentId)}/token`;
|
|
128
|
+
const ttlMs = parsed.ttlDays === undefined ? undefined : parsed.ttlDays * 86_400_000;
|
|
129
|
+
const res =
|
|
130
|
+
parsed.action === "revoke"
|
|
131
|
+
? await doFetch(base, { method: "DELETE", headers })
|
|
132
|
+
: await doFetch(parsed.action === "rotate" ? `${base}/rotate` : base, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers,
|
|
135
|
+
body: JSON.stringify(
|
|
136
|
+
parsed.action === "rotate" ? { graceSec: parsed.graceSec, ttlMs } : { name, ttlMs },
|
|
137
|
+
),
|
|
138
|
+
});
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
stderr(`worker-token ${parsed.action} failed (${res.status}): ${await readError(res)}`);
|
|
141
|
+
return 1;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (parsed.action === "revoke") {
|
|
145
|
+
const { revoked } = (await res.json()) as { revoked: number };
|
|
146
|
+
stderr(`Revoked ${revoked} worker token(s) of agent ${agentId}.`);
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
const token = (await res.json()) as {
|
|
150
|
+
plaintext: string;
|
|
151
|
+
expiresAt: string | null;
|
|
152
|
+
agentCreated?: boolean;
|
|
153
|
+
previousExpiresAt?: string;
|
|
154
|
+
};
|
|
155
|
+
if (token.agentCreated) stderr(`Created agent ${agentId} (${name}).`);
|
|
156
|
+
stderr(
|
|
157
|
+
`Worker token for agent ${agentId}, ${token.expiresAt ? `expires ${token.expiresAt}` : "no expiry"}.`,
|
|
158
|
+
);
|
|
159
|
+
if (token.previousExpiresAt)
|
|
160
|
+
stderr(`The previous token stops working at ${token.previousExpiresAt}.`);
|
|
161
|
+
stderr(`Set it as the worker's API_KEY with AGENT_ID=${agentId}. It is shown only once.`);
|
|
162
|
+
stdout(token.plaintext);
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
package/src/github/handlers.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { findUserByExternalId } from "../be/users";
|
|
|
4
4
|
import { resolveTemplate } from "../prompts/resolver";
|
|
5
5
|
import { githubContextKey } from "../tasks/context-key";
|
|
6
6
|
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
7
|
+
import { parseEnvFlag } from "../utils/env-flag";
|
|
7
8
|
import { getInstallationToken } from "./app";
|
|
8
9
|
import {
|
|
9
10
|
detectMention,
|
|
@@ -50,12 +51,12 @@ function buildGithubContextKey(
|
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
53
|
* Runtime-config guards for cancel-on-unassign and cancel-on-review-request-removed.
|
|
53
|
-
*
|
|
54
|
-
*
|
|
54
|
+
* Values "false" / "0" (case-insensitive, trimmed) leave the task untouched.
|
|
55
|
+
* Absent, empty, or unrecognized values default to true (cancel).
|
|
55
56
|
*/
|
|
56
57
|
async function cancelFlagEnabled(key: string): Promise<boolean> {
|
|
57
58
|
const row = (await getSwarmConfigs({ scope: "global", key }))[0];
|
|
58
|
-
return row?.value
|
|
59
|
+
return parseEnvFlag(row?.value, true);
|
|
59
60
|
}
|
|
60
61
|
const cancelOnUnassignEnabled = () => cancelFlagEnabled("github.cancelOnUnassign");
|
|
61
62
|
const cancelOnReviewRequestRemovedEnabled = () =>
|
|
@@ -1186,9 +1187,9 @@ const PR_REVIEW_ACTION_ITEMS_QUERY = `query($owner: String!, $name: String!, $nu
|
|
|
1186
1187
|
}
|
|
1187
1188
|
}`;
|
|
1188
1189
|
|
|
1189
|
-
/** Approved reviews with nothing to act on skip the lead task
|
|
1190
|
+
/** Approved reviews with nothing to act on skip the lead task by default. */
|
|
1190
1191
|
function skipNoopApprovalsEnabled(): boolean {
|
|
1191
|
-
return process.env.GITHUB_SKIP_NOOP_APPROVALS
|
|
1192
|
+
return parseEnvFlag(process.env.GITHUB_SKIP_NOOP_APPROVALS, true);
|
|
1192
1193
|
}
|
|
1193
1194
|
|
|
1194
1195
|
/**
|
package/src/hooks/hook.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import type { Agent } from "../types";
|
|
22
22
|
import { getApiKey } from "../utils/api-key";
|
|
23
23
|
import { getMcpBaseUrl, MAX_PROFILE_FILE_LENGTH } from "../utils/constants";
|
|
24
|
+
import { isSessionSummarySkipped } from "../utils/env-flag";
|
|
24
25
|
import { summarizeSession as runSummarize } from "../utils/internal-ai";
|
|
25
26
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
26
27
|
import { guardGhPrBody } from "./pr-body-guard";
|
|
@@ -315,7 +316,7 @@ export async function runStopHookSessionSummary(
|
|
|
315
316
|
deps: RunStopHookSessionSummaryDeps = {},
|
|
316
317
|
): Promise<void> {
|
|
317
318
|
const env = opts.env ?? process.env;
|
|
318
|
-
if (env
|
|
319
|
+
if (isSessionSummarySkipped(env)) {
|
|
319
320
|
console.debug("session_summary skipped (claude): SKIP_SESSION_SUMMARY is set");
|
|
320
321
|
return;
|
|
321
322
|
}
|