@desplega.ai/agent-swarm 2.7.4 → 2.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/acp-adapter-g3ads55s.js +15 -0
- package/dist/{actions-gfrr9vb7.js → actions-zwfykqqk.js} +18 -18
- package/dist/{amp-adapter-0s58gm46.js → amp-adapter-z3s0ssdt.js} +3 -3
- package/dist/{app-stvq692g.js → app-vmp4fgep.js} +3 -3
- package/dist/{assistant-5s37fr9n.js → assistant-vwde653f.js} +28 -21
- package/dist/{boot-reembed-yzy1841n.js → boot-reembed-375t7tbk.js} +1 -1
- package/dist/{boot-reembed-cv82gz7y.js → boot-reembed-gq9rcpnj.js} +1 -1
- package/dist/{boot-scrub-sweep-8k4q95ap.js → boot-scrub-sweep-nd938kyf.js} +4 -4
- package/dist/{boot-seeds-ftt9htah.js → boot-seeds-aykx51v3.js} +4 -4
- package/dist/{boxd-515v1t8z.js → boxd-k1j26g3h.js} +1 -1
- package/dist/{claude-adapter-t00wrzeh.js → claude-adapter-8j4hm099.js} +7 -7
- package/dist/{claude-managed-adapter-g6vmjccb.js → claude-managed-adapter-dvs2xw5n.js} +2 -2
- package/dist/{claude-managed-setup-afpa82f7.js → claude-managed-setup-0wyynpn1.js} +3 -3
- package/dist/{claude-sdk-session-vj0c2azx.js → claude-sdk-session-q6wj5hmy.js} +9 -9
- package/dist/{cli-wndv9p7g.js → cli-0731hvbw.js} +6 -6
- package/dist/{cli-d2pzqej1.js → cli-0bxqnvq9.js} +719 -7
- package/dist/{cli-jkt52mgt.js → cli-0gey5fzy.js} +2 -2
- package/dist/{cli-1yekbyqm.js → cli-1338796d.js} +205 -48
- package/dist/{cli-1s1dqn1j.js → cli-1ayk1eas.js} +171 -123
- package/dist/{cli-a4btqqft.js → cli-1b7btd38.js} +1 -1
- package/dist/{cli-a2jw6ee5.js → cli-1p89xny0.js} +1 -1
- package/dist/{cli-v726afdm.js → cli-2pnswgh8.js} +1 -1
- package/dist/{cli-kvtc7aez.js → cli-3h4nhjn8.js} +42 -14
- package/dist/{cli-655xm9ms.js → cli-583frbyr.js} +1 -1
- package/dist/{cli-4r12t6w6.js → cli-5c4hjzne.js} +101 -4
- package/dist/{cli-az0bpf8p.js → cli-5pag6rmn.js} +3 -3
- package/dist/{cli-36g23z21.js → cli-6s7vdcta.js} +4 -4
- package/dist/{cli-rtk9g7av.js → cli-73jzwksr.js} +6 -2
- package/dist/{cli-jg0nfda6.js → cli-75gkpjvw.js} +52 -21
- package/dist/{cli-a81qevyg.js → cli-7a42nfy0.js} +20 -36
- package/dist/{cli-0sbkss7h.js → cli-7wcyk2s5.js} +2 -2
- package/dist/{cli-41z8xwj0.js → cli-7wspm5gm.js} +14 -4
- package/dist/{cli-byqfbnjw.js → cli-82ae98pz.js} +9 -9
- package/dist/{cli-bq2pcphn.js → cli-8hs41zy2.js} +1 -1
- package/dist/{cli-vqgnyh76.js → cli-98rw5dt5.js} +15 -3
- package/dist/{cli-9cctx1tn.js → cli-a22h291q.js} +1 -1
- package/dist/{cli-j4kt1mpb.js → cli-a47xy8eq.js} +3 -3
- package/dist/{cli-ycm6180s.js → cli-a7v854rf.js} +5 -3
- package/dist/{cli-98pke4vk.js → cli-ae7mrdf9.js} +2 -2
- package/dist/{cli-ahd90bwt.js → cli-afrrppsw.js} +6 -6
- package/dist/{cli-8yy525ad.js → cli-bhjt02p8.js} +2 -2
- package/dist/{cli-v1yw3wha.js → cli-bnz7a7qf.js} +4 -4
- package/dist/{cli-j4kw3rsx.js → cli-cm5yqn35.js} +18 -0
- package/dist/{cli-h55y2bjc.js → cli-dhar9qay.js} +2 -2
- package/dist/{cli-5gr1g711.js → cli-fqzx7gwa.js} +2 -2
- package/dist/{cli-tzrpv5sc.js → cli-ftyxyspd.js} +1 -1
- package/dist/cli-fw2w0kva.js +47 -0
- package/dist/{cli-2dfbv8qr.js → cli-fy6x31ps.js} +1 -0
- package/dist/{cli-mvckssyq.js → cli-g61qxmge.js} +5 -5
- package/dist/{cli-aq58fk5m.js → cli-g83s9ewh.js} +7 -1
- package/dist/{cli-224epwwf.js → cli-hn9hxmz9.js} +3 -3
- package/dist/{cli-5hjced2v.js → cli-j3jvnpe8.js} +6 -6
- package/dist/{cli-dae4sbgy.js → cli-jm2vfpfd.js} +2 -2
- package/dist/{cli-zn15c46z.js → cli-mjzynrse.js} +1 -1
- package/dist/{cli-a1fhj4n9.js → cli-mmwm7zbf.js} +2 -2
- package/dist/{cli-cq1pf336.js → cli-mry0qzym.js} +5 -5
- package/dist/{cli-bwty35gx.js → cli-mthwvjre.js} +1 -1
- package/dist/{cli-s0exq74r.js → cli-nmbmy64z.js} +3 -3
- package/dist/{cli-wcmnan2g.js → cli-nn4d8dne.js} +5 -5
- package/dist/{cli-jg4p5t36.js → cli-nzxr556s.js} +121 -51
- package/dist/{cli-bcz074zx.js → cli-p599f1qm.js} +1 -1
- package/dist/{cli-k76tgq03.js → cli-pywf3eh5.js} +2 -2
- package/dist/{cli-wk9md5px.js → cli-qxnf8z2z.js} +2 -2
- package/dist/{cli-je8yeyc5.js → cli-rhw40jgb.js} +5 -5
- package/dist/{cli-avgcz4ra.js → cli-rm6hqj3z.js} +406 -35
- package/dist/{cli-0fgr1w9b.js → cli-rpm2sgv0.js} +2 -2
- package/dist/{cli-t282jgab.js → cli-rqkxrmgw.js} +12 -12
- package/dist/{cli-2qpvmmkw.js → cli-s0qnd4wv.js} +1 -1
- package/dist/{cli-pwyevs29.js → cli-spknvxbq.js} +22 -21
- package/dist/{cli-9y354g3j.js → cli-srye3vdt.js} +1 -1
- package/dist/{cli-pe09p6dv.js → cli-tqgg40ek.js} +1 -1
- package/dist/{cli-f255yaqn.js → cli-vbyfwh92.js} +3 -3
- package/dist/{cli-gcbdjt47.js → cli-vfym7wmz.js} +6 -6
- package/dist/{cli-edmm9d9y.js → cli-vqypxzh4.js} +7 -7
- package/dist/{cli-1gm8jcyr.js → cli-wvaawacd.js} +13 -13
- package/dist/{cli-eny54typ.js → cli-wx3rhnm0.js} +1 -1
- package/dist/{cli-b1afgtj3.js → cli-xbkdm710.js} +3 -3
- package/dist/{cli-mc97xy2s.js → cli-xtjhfmpc.js} +16 -4
- package/dist/{cli-eqx6k993.js → cli-y24sc29k.js} +25 -13
- package/dist/{cli-ktmvb20s.js → cli-z6jkx23q.js} +12 -12
- package/dist/{cli-7n5sbw0z.js → cli-z8eh3ex8.js} +5 -5
- package/dist/cli-zh3fp8v2.js +151 -0
- package/dist/{cli-13jvmew3.js → cli-zppwg9p9.js} +7 -7
- package/dist/cli.js +18 -18
- package/dist/{codex-adapter-g0gctw29.js → codex-adapter-q6k3gnyg.js} +5 -5
- package/dist/{codex-hook-vrzz2xhr.js → codex-hook-9efm2x1m.js} +1 -1
- package/dist/{codex-login-0gg46ygn.js → codex-login-y0pmff1c.js} +1 -1
- package/dist/{codex-session-runner-dr2cxe1c.js → codex-session-runner-3mwmk3d9.js} +6 -6
- package/dist/{commands-1p07cvff.js → commands-evrenjn9.js} +2 -2
- package/dist/{controller-z0s7sv6r.js → controller-xjp8vswh.js} +4 -4
- package/dist/{controllers-connect-x9kvvhyk.js → controllers-connect-pp5jnxvc.js} +20 -20
- package/dist/{cursor-adapter-m5b98pvk.js → cursor-adapter-yf47w4t0.js} +2 -2
- package/dist/{db-rsh04kh3.js → db-rgfsh91f.js} +2 -2
- package/dist/{db-capk2raj.js → db-zxpw6aht.js} +3 -3
- package/dist/{devin-adapter-yxm87fe9.js → devin-adapter-r9phbnc9.js} +1 -1
- package/dist/{dispatcher-304q6fwa.js → dispatcher-smasgws7.js} +17 -17
- package/dist/{docker-7rxt0rry.js → docker-rrjhaqt9.js} +2 -2
- package/dist/{dsh-adapter-btewem0y.js → dsh-adapter-ftyy8dnp.js} +2 -2
- package/dist/{e2b-renc0k8q.js → e2b-qcaj675y.js} +2 -2
- package/dist/{e2b-7fc9b9bd.js → e2b-w1p0bnpm.js} +2 -2
- package/dist/{easter-egg-3d-fm1p3n4d.js → easter-egg-3d-yyts7p16.js} +1 -1
- package/dist/{embedding-space-s68v49ct.js → embedding-space-1z0mbg95.js} +6 -6
- package/dist/{embeddings-4ms3604w.js → embeddings-ygeg7tm7.js} +1 -1
- package/dist/{event-bus-dfjx2qdh.js → event-bus-g0ka3pbw.js} +1 -1
- package/dist/{execute-mcdgccjs.js → execute-14qhqp4d.js} +1 -1
- package/dist/{external-channel-9dxwfztn.js → external-channel-ttgrs3sx.js} +2 -2
- package/dist/fx-adapter-gt6pncj2.js +18 -0
- package/dist/{grok-adapter-05ecv4h0.js → grok-adapter-sm4m5k4g.js} +8 -8
- package/dist/handlers-vwmba1m2.js +31 -0
- package/dist/{heartbeat-3n94heh3.js → heartbeat-qahz4p62.js} +20 -20
- package/dist/{hook-zxwje86c.js → hook-4fd5wq5w.js} +4 -4
- package/dist/{hook-aadrn69x.js → hook-kj3d398m.js} +6 -6
- package/dist/{http-xzxk9mst.js → http-1ted0j1k.js} +100 -64
- package/dist/index-5r71rf98.js +32 -0
- package/dist/{index-d34f6r87.js → index-6rq3r5zb.js} +1 -1
- package/dist/{index-9j4zwcht.js → index-7v51awbn.js} +158 -672
- package/dist/index-anmwxs87.js +29 -0
- package/dist/{index-qeb82z37.js → index-q4b917zs.js} +1 -1
- package/dist/index-spp0ra5c.js +36 -0
- package/dist/{keepalive-w4aakbyk.js → keepalive-6cf311t5.js} +4 -4
- package/dist/lead-fjy418cw.js +69 -0
- package/dist/{maintenance-hbptpk6p.js → maintenance-a4bm1bmt.js} +1 -1
- package/dist/{oauth-refresh-sweep-217semrv.js → oauth-refresh-sweep-3h0dfw1k.js} +3 -3
- package/dist/{onboard-vz2s5df3.js → onboard-myn5hraa.js} +3 -3
- package/dist/{opencode-adapter-gtfd7d7v.js → opencode-adapter-p2nkb5am.js} +5 -5
- package/dist/{otel-impl-aefave37.js → otel-impl-a1a0ef6b.js} +2 -2
- package/dist/{pi-mono-adapter-f8h5yseh.js → pi-mono-adapter-rzmpb72a.js} +117 -23
- package/dist/{pricing-refresh-m1yhh9ya.js → pricing-refresh-83drrnxj.js} +3 -3
- package/dist/{rbac-roles-svj72cdf.js → rbac-roles-0fb6e7pj.js} +2 -2
- package/dist/{rbac-roles-rrw8twdz.js → rbac-roles-s8y6ecb5.js} +1 -1
- package/dist/{render-v2-amwbnfxh.js → render-v2-twmbgrz4.js} +3 -3
- package/dist/{resume-9cd6wf6z.js → resume-zfqc36xj.js} +7 -7
- package/dist/{runtime-fp4vrx7m.js → runtime-qwggdpqr.js} +1 -1
- package/dist/{session-access-21t6yjyg.js → session-access-sc9pn9da.js} +4 -4
- package/dist/{session-source-backfill-pxbzcsc6.js → session-source-backfill-qh07xr8g.js} +3 -3
- package/dist/{setup-4592tqz4.js → setup-4fr0m7a1.js} +1 -1
- package/dist/{virtual-modules-gzfnpmnz.js → virtual-modules-fh4ha15b.js} +2 -2
- package/dist/{work-objects-t9tcf174.js → work-objects-0wr8zxct.js} +20 -20
- package/dist/worker-0c5tyzqe.js +69 -0
- package/dist/{worker-follow-up-q32jh4yk.js → worker-follow-up-ktyyazyp.js} +17 -17
- package/dist/{x-b6wxntdt.js → x-vmmye1kb.js} +2 -2
- package/openapi.json +97 -32
- package/package.json +1 -1
- package/src/be/db/session-summaries.ts +8 -2
- package/src/be/db/session-tree.ts +174 -61
- package/src/be/db/session-visibility.ts +5 -4
- package/src/be/db.ts +8 -4
- package/src/be/migrations/259_session_summaries_open_tasks.sql +111 -0
- package/src/be/model-validation.ts +5 -1
- package/src/be/pricing-normalize.ts +7 -2
- package/src/be/scripts/typecheck.ts +1 -0
- package/src/be/seed/schedules-seeder.ts +135 -9
- package/src/be/seed/workflows-seeder.ts +65 -1
- package/src/be/seed-pricing.ts +23 -0
- package/src/be/steering.ts +50 -3
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/commands/provider-credentials.ts +29 -2
- package/src/commands/runner.ts +86 -5
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +27 -1
- package/src/extensions/dispatcher.ts +4 -0
- package/src/http/agents.ts +1 -0
- package/src/http/mcp-bridge.ts +4 -0
- package/src/integrations/chat-sdk/inbound.ts +15 -5
- package/src/integrations/chat-sdk/steering.ts +3 -1
- package/src/kv-reserved-namespaces.ts +2 -0
- package/src/prompts/base-prompt.ts +4 -1
- package/src/prompts/session-templates.ts +26 -0
- package/src/providers/acp-adapter.ts +190 -26
- package/src/providers/acp-targets.ts +306 -6
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +9 -44
- package/src/providers/codex-oauth/flow.ts +24 -3
- package/src/providers/codex-oauth/storage.ts +56 -17
- package/src/providers/codex-oauth/subscription.ts +67 -0
- package/src/providers/fx-adapter.ts +211 -0
- package/src/providers/index.ts +5 -1
- package/src/providers/mcp-loopback-proxy.ts +94 -0
- package/src/providers/pi-mono-adapter.ts +98 -13
- package/src/providers/pricing-sources.md +8 -0
- package/src/providers/reasoning-effort.ts +17 -1
- package/src/providers/types.ts +2 -1
- package/src/scheduler/scheduler.ts +36 -3
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +5 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +33 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +5 -0
- package/src/server.ts +7 -0
- package/src/slack/assistant.ts +17 -1
- package/src/slack/handlers.ts +18 -5
- package/src/slack/left-threads.ts +65 -0
- package/src/slack/steering.ts +3 -0
- package/src/slack/thread-buffer.ts +40 -16
- package/src/tests/acp-adapter.test.ts +79 -0
- package/src/tests/base-prompt.test.ts +12 -0
- package/src/tests/chat-sdk-inbound.test.ts +30 -0
- package/src/tests/chatgpt-subscription-model-groups.test.ts +62 -0
- package/src/tests/chatgpt-subscription-pi.test.ts +170 -0
- package/src/tests/chatgpt-subscription-session-bench.test.ts +198 -0
- package/src/tests/codex-oauth-credential-info.test.ts +40 -0
- package/src/tests/codex-oauth-refresh-egress.test.ts +194 -0
- package/src/tests/context-window.test.ts +7 -0
- package/src/tests/explicit-model-validation.test.ts +99 -0
- package/src/tests/extensions-run-context.test.ts +11 -0
- package/src/tests/extensions-steering.test.ts +353 -0
- package/src/tests/extensions-validate.test.ts +5 -0
- package/src/tests/fixtures/extensions/steering-policy/hooks.ts +39 -0
- package/src/tests/fixtures/extensions/steering-policy/manifest.json +7 -0
- package/src/tests/fixtures/fake-fx.ts +167 -0
- package/src/tests/fixtures/fx/acp-responses-recorded.json +157 -0
- package/src/tests/fixtures/fx/acp-updates-recorded.jsonl +15 -0
- package/src/tests/fx-adapter.test.ts +771 -0
- package/src/tests/grok-adapter.test.ts +36 -0
- package/src/tests/mcp-loopback-proxy.test.ts +193 -0
- package/src/tests/model-tier-resolution.test.ts +3 -3
- package/src/tests/pricing-refresh.test.ts +5 -5
- package/src/tests/prompt-template-session.test.ts +5 -3
- package/src/tests/provider-registration.test.ts +2 -1
- package/src/tests/query-audit-plans.test.ts +3 -2
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/seed-model-profiles.test.ts +2 -2
- package/src/tests/seed-schedules.test.ts +138 -0
- package/src/tests/session-cost-recompute-unit.test.ts +39 -0
- package/src/tests/session-costs-golden.test.ts +26 -0
- package/src/tests/session-summaries-plans.test.ts +120 -0
- package/src/tests/session-summaries.test.ts +72 -12
- package/src/tests/sessions-tree.test.ts +11 -3
- package/src/tests/slack-leave-thread.test.ts +431 -0
- package/src/tests/tool-annotations.test.ts +2 -1
- package/src/tests/workflow-engine-v2.test.ts +42 -0
- package/src/tests/workflow-script-principal.test.ts +207 -7
- package/src/tools/slack-leave-thread.ts +104 -0
- package/src/tools/steer-task.ts +3 -2
- package/src/tools/tool-config.ts +2 -1
- package/src/types.ts +19 -0
- package/src/utils/codex-auth-failure.ts +30 -0
- package/src/utils/context-window.ts +4 -1
- package/src/utils/credentials.ts +7 -0
- package/src/utils/fx-models.ts +43 -0
- package/src/workflows/cooldown.ts +3 -2
- package/src/workflows/engine.ts +24 -9
- package/templates/schedules/dreaming-daily/config.json +1 -1
- package/templates/schedules/dreaming-daily/content.md +6 -14
- package/templates/skills/swarm-docs/content.md +1 -0
- package/templates/workflows/dreaming/content.md +5 -1
- package/dist/acp-adapter-96dc64m2.js +0 -15
- package/dist/handlers-jg7v74ty.js +0 -31
- package/dist/index-8c1zbz59.js +0 -29
- package/dist/index-8k8tmfxd.js +0 -32
- package/dist/index-hq617xdj.js +0 -36
- package/dist/lead-567kw7x3.js +0 -68
- package/dist/worker-fjdqra6q.js +0 -68
|
@@ -1,13 +1,24 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
1
|
+
import { chmod, mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import type {
|
|
5
|
+
ContentBlock,
|
|
6
|
+
InitializeResponse,
|
|
7
|
+
PromptRequest,
|
|
8
|
+
PromptResponse,
|
|
9
|
+
SessionUpdate,
|
|
10
|
+
} from "@agentclientprotocol/sdk/unstable";
|
|
4
11
|
import {
|
|
5
12
|
baseUrlOverride,
|
|
6
13
|
normalizeGrokModel,
|
|
7
14
|
providerBaseUrl,
|
|
8
15
|
providerForModel,
|
|
9
16
|
} from "@desplega/model-catalog";
|
|
17
|
+
import { resolveTemplateAsync, resolveTemplateFromCode } from "../prompts/resolver";
|
|
18
|
+
import "../prompts/session-templates";
|
|
10
19
|
import { clampContextPercent, getContextWindowSize } from "../utils/context-window";
|
|
20
|
+
import { requireFxRoute } from "../utils/fx-models";
|
|
21
|
+
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
11
22
|
import {
|
|
12
23
|
ACP_TARGET_IDS,
|
|
13
24
|
type AcpTarget,
|
|
@@ -20,11 +31,44 @@ import {
|
|
|
20
31
|
type OpencodeConfig,
|
|
21
32
|
} from "./opencode-adapter";
|
|
22
33
|
import { applyReasoningEffort } from "./reasoning-effort";
|
|
23
|
-
import type {
|
|
34
|
+
import type {
|
|
35
|
+
CostData,
|
|
36
|
+
CostModelUsage,
|
|
37
|
+
ProviderEvent,
|
|
38
|
+
ProviderSessionConfig,
|
|
39
|
+
SteerDelivery,
|
|
40
|
+
SteerDeliveryResult,
|
|
41
|
+
} from "./types";
|
|
42
|
+
|
|
43
|
+
/** What a target's `sessionSystemPrompt` hook sends as `session/new` `systemPrompt`. */
|
|
44
|
+
export interface AcpSessionSystemPrompt {
|
|
45
|
+
/** Text content blocks, sent as the `systemPrompt` array (append mode). */
|
|
46
|
+
blocks: ContentBlock[];
|
|
47
|
+
/** Set when the blocks carry only part of the prompt and the whole of it is in this file. */
|
|
48
|
+
artifactPath?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** What the adapter hands a target's `steer` hook for one steering message. */
|
|
52
|
+
export interface AcpSteerContext {
|
|
53
|
+
sessionId: string;
|
|
54
|
+
delivery: SteerDelivery;
|
|
55
|
+
/** The `initialize` result, for capability gating. */
|
|
56
|
+
initResult: InitializeResponse;
|
|
57
|
+
/** Sends `session/prompt` on the session's ACP connection. */
|
|
58
|
+
prompt(params: PromptRequest): Promise<PromptResponse>;
|
|
59
|
+
/** Observes raw session updates until the returned function is called. */
|
|
60
|
+
onUpdate(listener: (update: SessionUpdate) => void): () => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Native session resume over `session/resume`. */
|
|
64
|
+
export interface AcpResumeHook {
|
|
65
|
+
readonly method: "session/resume";
|
|
66
|
+
canResume(sessionId: string): Promise<boolean>;
|
|
67
|
+
}
|
|
24
68
|
|
|
25
69
|
export interface AcpTargetProfile {
|
|
26
70
|
/** An operator-selectable ACP target, or a first-class harness built on the ACP client. */
|
|
27
|
-
readonly target: AcpTarget | "grok";
|
|
71
|
+
readonly target: AcpTarget | "grok" | "fx";
|
|
28
72
|
command(config: ProviderSessionConfig): string[];
|
|
29
73
|
env(config: ProviderSessionConfig): Record<string, string>;
|
|
30
74
|
configuredOptions(config: ProviderSessionConfig): Record<string, string | boolean>;
|
|
@@ -35,8 +79,11 @@ export interface AcpTargetProfile {
|
|
|
35
79
|
sessionMeta?(config: ProviderSessionConfig): Record<string, unknown> | undefined;
|
|
36
80
|
/** A clearer message for a target error whose own text misleads, else undefined. */
|
|
37
81
|
describeError?(message: string): string | undefined;
|
|
38
|
-
/**
|
|
39
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Rewrites a translated event for display, e.g. unwrapping a tool proxy.
|
|
84
|
+
* `update` is the ACP session update the event was translated from.
|
|
85
|
+
*/
|
|
86
|
+
rewriteEvent?(event: ProviderEvent, update?: SessionUpdate): ProviderEvent;
|
|
40
87
|
/** Usage, cost and the model that ran, from the `session/prompt` response `_meta`. */
|
|
41
88
|
promptCost?(
|
|
42
89
|
meta: Record<string, unknown> | null | undefined,
|
|
@@ -47,6 +94,23 @@ export interface AcpTargetProfile {
|
|
|
47
94
|
meta: Record<string, unknown> | null | undefined,
|
|
48
95
|
model: string,
|
|
49
96
|
): Extract<ProviderEvent, { type: "context_usage" }> | undefined;
|
|
97
|
+
// Opt-in hooks. A target without them behaves exactly as before they existed.
|
|
98
|
+
/**
|
|
99
|
+
* The standard `systemPrompt` for `session/new`. Sent only when the agent
|
|
100
|
+
* advertises `sessionCapabilities.systemPrompt`.
|
|
101
|
+
*/
|
|
102
|
+
sessionSystemPrompt?(config: ProviderSessionConfig): Promise<AcpSessionSystemPrompt | undefined>;
|
|
103
|
+
/**
|
|
104
|
+
* The target accepts MCP URLs only over HTTPS or explicit loopback HTTP. A
|
|
105
|
+
* plain-HTTP, non-loopback server URL (the swarm API at `http://api:3013`)
|
|
106
|
+
* goes through a per-session 127.0.0.1 forwarder, and SSE servers are dropped
|
|
107
|
+
* unless the agent advertises `mcpCapabilities.sse`.
|
|
108
|
+
*/
|
|
109
|
+
readonly requiresSecureMcpUrl?: boolean;
|
|
110
|
+
/** Live steering. Absent means the session has no `deliverSteering`. */
|
|
111
|
+
steer?(context: AcpSteerContext): Promise<SteerDeliveryResult>;
|
|
112
|
+
/** Native resume: `config.resumeSessionId` is resumed instead of creating a session. */
|
|
113
|
+
readonly resume?: AcpResumeHook;
|
|
50
114
|
}
|
|
51
115
|
|
|
52
116
|
export class AcpTargetResolutionError extends Error {
|
|
@@ -359,7 +423,7 @@ const copilotTargetProfile: AcpTargetProfile = {
|
|
|
359
423
|
},
|
|
360
424
|
};
|
|
361
425
|
|
|
362
|
-
/** Qwen Code's `--auth-type` choices (0.26.
|
|
426
|
+
/** Qwen Code's `--auth-type` choices (0.26.4). `qwen-oauth` needs a browser, so it is left out. */
|
|
363
427
|
const QWEN_AUTH_TYPES = ["openai", "openai-responses", "anthropic", "gemini", "vertex-ai"] as const;
|
|
364
428
|
|
|
365
429
|
// Qwen Code is a Gemini CLI fork: QWEN_SYSTEM_MD replaces its built-in prompt
|
|
@@ -696,6 +760,242 @@ export const grokTargetProfile: AcpTargetProfile = {
|
|
|
696
760
|
},
|
|
697
761
|
};
|
|
698
762
|
|
|
763
|
+
// ---------------------------------------------------------------------------
|
|
764
|
+
// fx (Vercel Labs' `fx acp`), measured against fx 0.0.15 on 2026-10-29.
|
|
765
|
+
// ---------------------------------------------------------------------------
|
|
766
|
+
|
|
767
|
+
/** Above this many UTF-8 bytes the prompt goes to a file; fx caps `systemPrompt` at 64 KiB. */
|
|
768
|
+
export const FX_SYSTEM_PROMPT_INLINE_MAX_BYTES = 60 * 1024;
|
|
769
|
+
/** How much of an oversized prompt is still sent inline, ahead of the pointer line. */
|
|
770
|
+
export const FX_SYSTEM_PROMPT_HEAD_BYTES = 56 * 1024;
|
|
771
|
+
const FX_SYSTEM_PROMPT_FILE_TEMPLATE = "system.agent.fx.system_prompt_file";
|
|
772
|
+
/** fx streams this notice as an assistant message chunk; it is not model output. */
|
|
773
|
+
const FX_CONTEXT_NOTICE_PREFIX = "[context] ";
|
|
774
|
+
const FX_AUTH_ERROR_RE =
|
|
775
|
+
/authentication is unavailable|credential is unavailable|MissingApiKey|unauthorized|\b401\b/i;
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Per-task scratch dir for fx: `acp.log` (`--log-file`) and, for an oversized
|
|
779
|
+
* prompt, `SYSTEM.md`. Outside the task cwd, removed when the session ends.
|
|
780
|
+
*/
|
|
781
|
+
export function fxRunDir(config: Pick<ProviderSessionConfig, "taskId">): string {
|
|
782
|
+
const safe = config.taskId.replace(/[^A-Za-z0-9._-]/g, "_") || "session";
|
|
783
|
+
return join(tmpdir(), `fx-${safe}`);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/** The first `max` bytes of `bytes` as text, never ending inside a UTF-8 sequence. */
|
|
787
|
+
function utf8Head(bytes: Uint8Array, max: number): string {
|
|
788
|
+
let end = Math.min(max, bytes.length);
|
|
789
|
+
// Back off continuation bytes (10xxxxxx) so a multi-byte character is not split.
|
|
790
|
+
while (end > 0 && end < bytes.length && ((bytes[end] ?? 0) & 0xc0) === 0x80) end -= 1;
|
|
791
|
+
return new TextDecoder().decode(bytes.subarray(0, end));
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
async function fxSystemPromptPointer(path: string): Promise<string> {
|
|
795
|
+
try {
|
|
796
|
+
const result = await resolveTemplateAsync(FX_SYSTEM_PROMPT_FILE_TEMPLATE, { path });
|
|
797
|
+
if (!result.skipped && result.text.trim()) return result.text;
|
|
798
|
+
} catch {
|
|
799
|
+
// Fall through to the code default.
|
|
800
|
+
}
|
|
801
|
+
// A skipped or blank override must not drop the pointer to the full prompt.
|
|
802
|
+
return resolveTemplateFromCode(FX_SYSTEM_PROMPT_FILE_TEMPLATE, { path }).text;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* The `systemPrompt` fx gets. Up to 60 KiB it is the prompt itself. A larger
|
|
807
|
+
* one is written whole to `<dir>/SYSTEM.md`; fx then gets its first 56 KiB plus
|
|
808
|
+
* a line telling the model to read that file first (fx returns `-32602
|
|
809
|
+
* systemPrompt exceeds the 64 KiB limit` above 65,536 bytes).
|
|
810
|
+
*/
|
|
811
|
+
export async function fxSystemPrompt(
|
|
812
|
+
systemPrompt: string,
|
|
813
|
+
dir: string,
|
|
814
|
+
): Promise<AcpSessionSystemPrompt | undefined> {
|
|
815
|
+
if (!systemPrompt.trim()) return undefined;
|
|
816
|
+
const bytes = new TextEncoder().encode(systemPrompt);
|
|
817
|
+
if (bytes.length <= FX_SYSTEM_PROMPT_INLINE_MAX_BYTES) {
|
|
818
|
+
return { blocks: [{ type: "text", text: systemPrompt }] };
|
|
819
|
+
}
|
|
820
|
+
const artifactPath = join(dir, "SYSTEM.md");
|
|
821
|
+
await mkdir(dir, { recursive: true });
|
|
822
|
+
await Bun.write(artifactPath, systemPrompt);
|
|
823
|
+
await chmod(artifactPath, 0o600);
|
|
824
|
+
const head = utf8Head(bytes, FX_SYSTEM_PROMPT_HEAD_BYTES);
|
|
825
|
+
const pointer = await fxSystemPromptPointer(artifactPath);
|
|
826
|
+
return { blocks: [{ type: "text", text: `${head}\n\n${pointer}` }], artifactPath };
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/** `~/.fx/sessions/v2/<id>/log.jsonl`: where fx 0.0.15 keeps a saved session. */
|
|
830
|
+
export async function fxSessionExists(
|
|
831
|
+
sessionId: string,
|
|
832
|
+
home: string | undefined,
|
|
833
|
+
): Promise<boolean> {
|
|
834
|
+
if (!home || !/^[A-Za-z0-9_-]+$/.test(sessionId)) return false;
|
|
835
|
+
return Bun.file(join(home, ".fx", "sessions", "v2", sessionId, "log.jsonl")).exists();
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
type FxToolCallMeta = { fx?: { toolCall?: { mcp?: { server?: unknown; tool?: unknown } } } };
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* fx's display adjustments:
|
|
842
|
+
* - The `[context] … capability_search` notice arrives as its own assistant
|
|
843
|
+
* chunk. It becomes an `fx_context_notice` custom event, so it never reaches
|
|
844
|
+
* the task output.
|
|
845
|
+
* - A `tool_call` titles the action ("Running", "Get the agent swarm") and
|
|
846
|
+
* names the tool in a non-standard `name` field (`shell`, `skill`,
|
|
847
|
+
* `mcp_swarm_get-swarm`), with the MCP server and tool in
|
|
848
|
+
* `_meta.fx.toolCall.mcp`. The tool start carries that name, MCP calls as
|
|
849
|
+
* `mcp__<server>__<tool>` like every other harness.
|
|
850
|
+
*/
|
|
851
|
+
export function rewriteFxEvent(event: ProviderEvent, update?: SessionUpdate): ProviderEvent {
|
|
852
|
+
if (
|
|
853
|
+
event.type === "message" &&
|
|
854
|
+
event.role === "assistant" &&
|
|
855
|
+
event.content.startsWith(FX_CONTEXT_NOTICE_PREFIX)
|
|
856
|
+
) {
|
|
857
|
+
return {
|
|
858
|
+
type: "custom",
|
|
859
|
+
name: "fx_context_notice",
|
|
860
|
+
data: { text: event.content, messageId: event.messageId ?? null },
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
if (event.type === "tool_start" && update?.sessionUpdate === "tool_call") {
|
|
864
|
+
const mcp = (update._meta as FxToolCallMeta | null | undefined)?.fx?.toolCall?.mcp;
|
|
865
|
+
if (typeof mcp?.server === "string" && typeof mcp.tool === "string" && mcp.server && mcp.tool) {
|
|
866
|
+
return { ...event, toolName: `mcp__${mcp.server}__${mcp.tool}` };
|
|
867
|
+
}
|
|
868
|
+
const name = (update as { name?: unknown }).name;
|
|
869
|
+
if (typeof name === "string" && name.trim()) return { ...event, toolName: name.trim() };
|
|
870
|
+
}
|
|
871
|
+
return event;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* fx's native steer: a second `session/prompt` with `_meta.fx.steer` during the
|
|
876
|
+
* running turn. fx applies it at the next tool boundary without cancelling and
|
|
877
|
+
* echoes it as a `user_message_chunk` carrying `_meta.fx.steering`; the steer
|
|
878
|
+
* request itself resolves with the turn, `_meta.fx.steering: "absorbed"`.
|
|
879
|
+
* Either one counts as delivered. The echo arrives first, so the row is
|
|
880
|
+
* `delivered` while the turn still runs and `accept-steer` can mark it handled.
|
|
881
|
+
* Any other answer is undeliverable and the runner promotes the message.
|
|
882
|
+
*/
|
|
883
|
+
export async function steerFx({
|
|
884
|
+
sessionId,
|
|
885
|
+
delivery,
|
|
886
|
+
initResult,
|
|
887
|
+
prompt,
|
|
888
|
+
onUpdate,
|
|
889
|
+
}: AcpSteerContext): Promise<SteerDeliveryResult> {
|
|
890
|
+
if (delivery.mode !== "steer") {
|
|
891
|
+
return { delivered: false, reason: `fx supports steer only, not ${delivery.mode}` };
|
|
892
|
+
}
|
|
893
|
+
const meta = initResult.agentCapabilities?._meta as { fx?: { steering?: unknown } } | undefined;
|
|
894
|
+
if (meta?.fx?.steering !== true) {
|
|
895
|
+
return { delivered: false, reason: "fx did not advertise _meta.fx.steering" };
|
|
896
|
+
}
|
|
897
|
+
return new Promise<SteerDeliveryResult>((resolve) => {
|
|
898
|
+
let settled = false;
|
|
899
|
+
let stopListening = () => {};
|
|
900
|
+
const settle = (result: SteerDeliveryResult) => {
|
|
901
|
+
if (settled) return;
|
|
902
|
+
settled = true;
|
|
903
|
+
stopListening();
|
|
904
|
+
resolve(result);
|
|
905
|
+
};
|
|
906
|
+
stopListening = onUpdate((update) => {
|
|
907
|
+
if (update.sessionUpdate !== "user_message_chunk") return;
|
|
908
|
+
const steering = (update._meta as { fx?: { steering?: unknown } } | null | undefined)?.fx
|
|
909
|
+
?.steering;
|
|
910
|
+
if (!steering || update.content.type !== "text" || update.content.text !== delivery.text) {
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
settle({ delivered: true, mode: "steer" });
|
|
914
|
+
});
|
|
915
|
+
prompt({
|
|
916
|
+
sessionId,
|
|
917
|
+
prompt: [{ type: "text", text: delivery.text }],
|
|
918
|
+
_meta: { fx: { steer: true } },
|
|
919
|
+
}).then(
|
|
920
|
+
(response) => {
|
|
921
|
+
const state = (response._meta as { fx?: { steering?: unknown } } | null | undefined)?.fx
|
|
922
|
+
?.steering;
|
|
923
|
+
settle(
|
|
924
|
+
state === "absorbed"
|
|
925
|
+
? { delivered: true, mode: "steer" }
|
|
926
|
+
: {
|
|
927
|
+
delivered: false,
|
|
928
|
+
reason: `fx did not absorb the steer (stopReason ${response.stopReason}, steering ${JSON.stringify(state ?? null)})`,
|
|
929
|
+
},
|
|
930
|
+
);
|
|
931
|
+
},
|
|
932
|
+
(err: unknown) => {
|
|
933
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
934
|
+
settle({ delivered: false, reason: scrubSecrets(`fx steer failed: ${message}`) });
|
|
935
|
+
},
|
|
936
|
+
);
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
export const fxTargetProfile: AcpTargetProfile = {
|
|
941
|
+
target: "fx",
|
|
942
|
+
command(config) {
|
|
943
|
+
const binary = readEnv(config, "FX_BINARY")?.trim() || "fx";
|
|
944
|
+
const route = requireFxRoute(config.model);
|
|
945
|
+
return [
|
|
946
|
+
binary,
|
|
947
|
+
"acp",
|
|
948
|
+
"--model",
|
|
949
|
+
route.fxModel,
|
|
950
|
+
"--log-file",
|
|
951
|
+
join(fxRunDir(config), "acp.log"),
|
|
952
|
+
];
|
|
953
|
+
},
|
|
954
|
+
env(config) {
|
|
955
|
+
const route = requireFxRoute(config.model);
|
|
956
|
+
const env = baseTargetEnv(config);
|
|
957
|
+
// Only the route's own key: an OpenRouter session never sees OPENAI_API_KEY.
|
|
958
|
+
copyEnvKeys(config, env, [route.keyVar]);
|
|
959
|
+
// Process-local selection and permissions. `session/set_mode` and the
|
|
960
|
+
// `mode` / `provider` config options persist into ~/.fx/settings.json, so
|
|
961
|
+
// the adapter never sends them. Auto mode's reviewer holds swarm MCP calls
|
|
962
|
+
// headless, so the session runs full-access.
|
|
963
|
+
env.FX_PROVIDER = route.fxProvider;
|
|
964
|
+
env.FX_PERMISSION_MODE = "full-access";
|
|
965
|
+
env.FX_AUTO_UPGRADE = "0";
|
|
966
|
+
env.FX_NO_OPEN_BROWSER = "1";
|
|
967
|
+
return env;
|
|
968
|
+
},
|
|
969
|
+
// The model rides on `--model`. Only fx's AI Gateway provider has an
|
|
970
|
+
// `effort` option; custom connections send no reasoning control.
|
|
971
|
+
configuredOptions(config): Record<string, string | boolean> {
|
|
972
|
+
if (!config.reasoningEffort) return {};
|
|
973
|
+
const effort = applyReasoningEffort("fx", config.model.trim(), config.reasoningEffort);
|
|
974
|
+
return effort.kind === "fx-effort" ? { effort: effort.reasoningEffort } : {};
|
|
975
|
+
},
|
|
976
|
+
async writeSystemPromptArtifact(config) {
|
|
977
|
+
// acp.log can hold session/new params, including the ephemeral MCP bearer.
|
|
978
|
+
await mkdir(fxRunDir(config), { recursive: true, mode: 0o700 });
|
|
979
|
+
},
|
|
980
|
+
async cleanupSystemPromptArtifact(config) {
|
|
981
|
+
await rm(fxRunDir(config), { recursive: true, force: true });
|
|
982
|
+
},
|
|
983
|
+
sessionSystemPrompt(config) {
|
|
984
|
+
return fxSystemPrompt(config.systemPrompt ?? "", fxRunDir(config));
|
|
985
|
+
},
|
|
986
|
+
requiresSecureMcpUrl: true,
|
|
987
|
+
steer: steerFx,
|
|
988
|
+
resume: {
|
|
989
|
+
method: "session/resume",
|
|
990
|
+
canResume: (sessionId) => fxSessionExists(sessionId, process.env.HOME),
|
|
991
|
+
},
|
|
992
|
+
rewriteEvent: rewriteFxEvent,
|
|
993
|
+
describeError(message) {
|
|
994
|
+
if (!FX_AUTH_ERROR_RE.test(message)) return undefined;
|
|
995
|
+
return `fx could not authenticate the model route (OPENROUTER_API_KEY, OPENAI_API_KEY or AI_GATEWAY_API_KEY invalid or missing): ${message}`;
|
|
996
|
+
},
|
|
997
|
+
};
|
|
998
|
+
|
|
699
999
|
export function resolveAcpTarget(config: ProviderSessionConfig): AcpTargetProfile {
|
|
700
1000
|
const target = readEnv(config, "ACP_TARGET") ?? "custom";
|
|
701
1001
|
if (!isAcpTarget(target)) {
|
|
@@ -45,7 +45,7 @@ import type {
|
|
|
45
45
|
* default; the image installs exactly this version and the per-task settings
|
|
46
46
|
* set `amp.updates.mode: "disabled"`. Bump it deliberately, with a live run.
|
|
47
47
|
*/
|
|
48
|
-
export const AMP_PACKAGE = "@ampcode/cli@0.0.
|
|
48
|
+
export const AMP_PACKAGE = "@ampcode/cli@0.0.1793275344-g17bf52";
|
|
49
49
|
|
|
50
50
|
/** MCP server name. Amp exposes its tools as `agent_swarm` / `store_progress` (dashes become underscores). */
|
|
51
51
|
const AMP_MCP_SERVER_NAME = "agent-swarm";
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
import { buildRatingsFromLlm, fetchRetrievalsForTask, postRatings } from "../be/memory/raters/llm";
|
|
41
41
|
import { contextBoundNoteThreshold, createContextBoundNote } from "../hooks/context-bound-note";
|
|
42
42
|
import { getApiKey } from "../utils/api-key";
|
|
43
|
+
import { isCodexRejectedAccessError } from "../utils/codex-auth-failure";
|
|
43
44
|
import {
|
|
44
45
|
CONTEXT_FORMULA,
|
|
45
46
|
clampContextPercent,
|
|
@@ -67,7 +68,7 @@ import { computeCodexCostUsd, getCodexContextWindow, resolveCodexModel } from ".
|
|
|
67
68
|
import { credentialsToAuthJson } from "./codex-oauth/auth-json.js";
|
|
68
69
|
import { hasCodexOAuthPoolSlots } from "./codex-oauth/env-keys.js";
|
|
69
70
|
import { codexUserHome } from "./codex-oauth/home.js";
|
|
70
|
-
import {
|
|
71
|
+
import { buildPoolRevalidationFailureReason, getValidCodexOAuth } from "./codex-oauth/storage.js";
|
|
71
72
|
import { pruneCodexRollouts } from "./codex-rollouts";
|
|
72
73
|
import { resolveCodexPrompt } from "./codex-skill-resolver";
|
|
73
74
|
import { createCodexSwarmEventHandler } from "./codex-swarm-events";
|
|
@@ -89,6 +90,8 @@ import type {
|
|
|
89
90
|
SteerDeliveryResult,
|
|
90
91
|
} from "./types";
|
|
91
92
|
|
|
93
|
+
export { buildPoolRevalidationFailureReason } from "./codex-oauth/storage.js";
|
|
94
|
+
|
|
92
95
|
type CodexConfig = Record<string, CodexConfigValue | undefined>;
|
|
93
96
|
|
|
94
97
|
/** `model_provider` id of the generated gateway block. */
|
|
@@ -442,29 +445,6 @@ export async function resolveCodexAuthMode(
|
|
|
442
445
|
return currentMode;
|
|
443
446
|
}
|
|
444
447
|
|
|
445
|
-
/**
|
|
446
|
-
* Build the `failureReason` for a pool-slot OAuth revalidation failure
|
|
447
|
-
* (see `resolveCodexAuthMode` above).
|
|
448
|
-
*
|
|
449
|
-
* Wording matters: every variant carries `[auth-error]`, but the runner's
|
|
450
|
-
* `isCodexAuthFailureReason` counts only a confirmed refresh rejection toward
|
|
451
|
-
* the auth-failure bench (2 in a row). A lock-wait timeout ("waiting for the
|
|
452
|
-
* refresh lock") and any other thrown error (refresh-lock HTTP or network
|
|
453
|
-
* failure, config-store persistence failure: "not a confirmed auth
|
|
454
|
-
* rejection") are excluded as transient, so an outage does NOT bench a
|
|
455
|
-
* healthy login.
|
|
456
|
-
*/
|
|
457
|
-
export function buildPoolRevalidationFailureReason(err: unknown, slot: number): string {
|
|
458
|
-
if (err instanceof CodexOAuthRefreshError) {
|
|
459
|
-
if (err.reason === "lock_timeout") {
|
|
460
|
-
return `[auth-error] Codex pool slot ${slot} [...${err.keySuffix}] revalidation failed: timed out waiting for the refresh lock — transient, will retry on next task.`;
|
|
461
|
-
}
|
|
462
|
-
return `[auth-error] Codex pool slot ${slot} [...${err.keySuffix}] revalidation failed: refresh rejected (${err.status ?? "unknown status"} ${err.body ?? ""}) — credential likely revoked; re-run codex-login for this slot.`;
|
|
463
|
-
}
|
|
464
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
465
|
-
return `[auth-error] Codex pool slot ${slot} revalidation failed (not a confirmed auth rejection, transient): ${message}`;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
448
|
/**
|
|
469
449
|
* Build the configuration supplied through the `thread/start` request.
|
|
470
450
|
* It overrides the baseline `~/.codex/config.toml` in the worker image.
|
|
@@ -1594,26 +1574,11 @@ export class CodexSession implements ProviderSession {
|
|
|
1594
1574
|
};
|
|
1595
1575
|
}
|
|
1596
1576
|
|
|
1597
|
-
// Bad / missing / invalid API key — codexErrorInfo: "Unauthorized"
|
|
1598
|
-
//
|
|
1599
|
-
//
|
|
1600
|
-
//
|
|
1601
|
-
|
|
1602
|
-
// Codex CLI self-refreshing straight from the blanked-refresh-token
|
|
1603
|
-
// auth.json and surfacing this turn-level error.
|
|
1604
|
-
const authPatterns = [
|
|
1605
|
-
"unauthorized",
|
|
1606
|
-
"http 401",
|
|
1607
|
-
" 401 ",
|
|
1608
|
-
"invalid api key",
|
|
1609
|
-
"invalid_api_key",
|
|
1610
|
-
"missing api key",
|
|
1611
|
-
"no api key",
|
|
1612
|
-
"authentication failed",
|
|
1613
|
-
"failed to refresh token",
|
|
1614
|
-
"invalid 'refresh_token'",
|
|
1615
|
-
];
|
|
1616
|
-
if (authPatterns.some((p) => normalized.includes(p))) {
|
|
1577
|
+
// Bad / missing / invalid API key — codexErrorInfo: "Unauthorized" — or
|
|
1578
|
+
// a pool login revoked while its access token is unexpired: the spawned
|
|
1579
|
+
// Codex CLI self-refreshes from the blanked-refresh-token auth.json and
|
|
1580
|
+
// surfaces this turn-level error.
|
|
1581
|
+
if (isCodexRejectedAccessError(normalized)) {
|
|
1617
1582
|
const slot = this.config.codexSlot;
|
|
1618
1583
|
const guidance =
|
|
1619
1584
|
slot === undefined
|
|
@@ -150,14 +150,19 @@ export async function refreshAccessToken(refreshToken: string): Promise<TokenRes
|
|
|
150
150
|
});
|
|
151
151
|
|
|
152
152
|
if (!response.ok) {
|
|
153
|
-
|
|
153
|
+
// The endpoint may echo the submitted refresh token: redact before the
|
|
154
|
+
// text reaches a log, the thrown error or a task's failure reason.
|
|
155
|
+
const text = redactTokens(await response.text().catch(() => ""), [refreshToken]);
|
|
154
156
|
console.error("[codex-oauth] Token refresh failed:", response.status, text);
|
|
155
157
|
return { type: "failed", status: response.status, error: text.slice(0, 300) };
|
|
156
158
|
}
|
|
157
159
|
|
|
158
160
|
const json = (await response.json()) as Record<string, unknown>;
|
|
159
161
|
if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
|
|
160
|
-
console.error(
|
|
162
|
+
console.error(
|
|
163
|
+
"[codex-oauth] Token refresh response missing fields; got keys:",
|
|
164
|
+
Object.keys(json).join(", "),
|
|
165
|
+
);
|
|
161
166
|
return { type: "failed" };
|
|
162
167
|
}
|
|
163
168
|
|
|
@@ -168,11 +173,27 @@ export async function refreshAccessToken(refreshToken: string): Promise<TokenRes
|
|
|
168
173
|
expires: Date.now() + (json.expires_in as number) * 1000,
|
|
169
174
|
};
|
|
170
175
|
} catch (error) {
|
|
171
|
-
|
|
176
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
177
|
+
console.error("[codex-oauth] Token refresh error:", redactTokens(message, [refreshToken]));
|
|
172
178
|
return { type: "failed" };
|
|
173
179
|
}
|
|
174
180
|
}
|
|
175
181
|
|
|
182
|
+
/**
|
|
183
|
+
* `text` with every given token, raw or URL/JSON-encoded, replaced by a
|
|
184
|
+
* marker, then run through `scrubSecrets`. For error text from an endpoint
|
|
185
|
+
* that was sent those tokens and may echo them back.
|
|
186
|
+
*/
|
|
187
|
+
export function redactTokens(text: string, tokens: readonly (string | undefined)[]): string {
|
|
188
|
+
let out = text;
|
|
189
|
+
for (const token of tokens) {
|
|
190
|
+
if (!token) continue;
|
|
191
|
+
const forms = new Set([token, encodeURIComponent(token), JSON.stringify(token).slice(1, -1)]);
|
|
192
|
+
for (const form of forms) out = out.split(form).join("[REDACTED:codex_oauth_token]");
|
|
193
|
+
}
|
|
194
|
+
return scrubSecrets(out);
|
|
195
|
+
}
|
|
196
|
+
|
|
176
197
|
export function getAccountId(accessToken: string): string | null {
|
|
177
198
|
const payload = decodeJwt(accessToken);
|
|
178
199
|
const auth = payload?.[JWT_CLAIM_PATH] as Record<string, unknown> | undefined;
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* until the 071 migration renames it.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { scrubSecrets } from "../../utils/secret-scrubber.js";
|
|
12
13
|
import { deriveCodexKeySuffix } from "./auth-json.js";
|
|
13
|
-
import { refreshAccessToken } from "./flow.js";
|
|
14
|
+
import { redactTokens, refreshAccessToken } from "./flow.js";
|
|
14
15
|
import type { CodexOAuthCredentials } from "./types.js";
|
|
15
16
|
|
|
16
17
|
/** Legacy single-credential key — kept for backwards-compat fallback reads. */
|
|
@@ -102,6 +103,30 @@ export class CodexOAuthRefreshError extends Error {
|
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Build the `failureReason` for a pool-slot OAuth revalidation failure
|
|
108
|
+
* (`resolveCodexAuthMode` in codex-adapter.ts, and the pi ChatGPT
|
|
109
|
+
* subscription path in `subscription.ts`).
|
|
110
|
+
*
|
|
111
|
+
* Wording matters: every variant carries `[auth-error]`, but the runner's
|
|
112
|
+
* `isCodexAuthFailureReason` counts only a confirmed refresh rejection toward
|
|
113
|
+
* the auth-failure bench (2 in a row). A lock-wait timeout ("waiting for the
|
|
114
|
+
* refresh lock") and any other thrown error (refresh-lock HTTP or network
|
|
115
|
+
* failure, config-store persistence failure: "not a confirmed auth
|
|
116
|
+
* rejection") are excluded as transient, so an outage does NOT bench a
|
|
117
|
+
* healthy login.
|
|
118
|
+
*/
|
|
119
|
+
export function buildPoolRevalidationFailureReason(err: unknown, slot: number): string {
|
|
120
|
+
if (err instanceof CodexOAuthRefreshError) {
|
|
121
|
+
if (err.reason === "lock_timeout") {
|
|
122
|
+
return `[auth-error] Codex pool slot ${slot} [...${err.keySuffix}] revalidation failed: timed out waiting for the refresh lock — transient, will retry on next task.`;
|
|
123
|
+
}
|
|
124
|
+
return `[auth-error] Codex pool slot ${slot} [...${err.keySuffix}] revalidation failed: refresh rejected (${err.status ?? "unknown status"} ${scrubSecrets(err.body ?? "")}) — credential likely revoked; re-run codex-login for this slot.`;
|
|
125
|
+
}
|
|
126
|
+
const message = scrubSecrets(err instanceof Error ? err.message : String(err));
|
|
127
|
+
return `[auth-error] Codex pool slot ${slot} revalidation failed (not a confirmed auth rejection, transient): ${message}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
105
130
|
/**
|
|
106
131
|
* Acquire the cross-process refresh lock for a Codex OAuth slot via the API
|
|
107
132
|
* server's `oauth_refresh_locks` table (migration 077). Worker-side code
|
|
@@ -246,23 +271,33 @@ export async function storeCodexOAuth(
|
|
|
246
271
|
slot = 0,
|
|
247
272
|
): Promise<void> {
|
|
248
273
|
const key = codexOAuthKeyForSlot(slot);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
274
|
+
// The request carries the tokens, so a transport error or the store's reply
|
|
275
|
+
// may echo them. Every error leaving here is rebuilt from redacted text,
|
|
276
|
+
// with no `cause` pointing back at the raw one.
|
|
277
|
+
const redact = (text: string) => redactTokens(text, [creds.access, creds.refresh]);
|
|
278
|
+
let res: Response;
|
|
279
|
+
try {
|
|
280
|
+
res = await fetch(`${apiUrl}/api/config`, {
|
|
281
|
+
method: "PUT",
|
|
282
|
+
headers: {
|
|
283
|
+
"Content-Type": "application/json",
|
|
284
|
+
Authorization: `Bearer ${apiKey}`,
|
|
285
|
+
},
|
|
286
|
+
body: JSON.stringify({
|
|
287
|
+
scope: "global",
|
|
288
|
+
key,
|
|
289
|
+
value: JSON.stringify(creds),
|
|
290
|
+
isSecret: true,
|
|
291
|
+
description: `Codex ChatGPT OAuth credentials slot ${slot} (stored by codex-login)`,
|
|
292
|
+
}),
|
|
293
|
+
});
|
|
294
|
+
} catch (error) {
|
|
295
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
296
|
+
throw new Error(`Failed to store ${key} config: ${redact(message)}`);
|
|
297
|
+
}
|
|
263
298
|
|
|
264
299
|
if (!res.ok) {
|
|
265
|
-
const text = await res.text().catch(() => "");
|
|
300
|
+
const text = redact(await res.text().catch(() => ""));
|
|
266
301
|
throw new Error(`Failed to store ${key} config: HTTP ${res.status} ${text}`);
|
|
267
302
|
}
|
|
268
303
|
}
|
|
@@ -343,7 +378,11 @@ export async function persistCodexOAuth(
|
|
|
343
378
|
try {
|
|
344
379
|
await storeCodexOAuth(apiUrl, apiKey, creds, slot);
|
|
345
380
|
} catch (err) {
|
|
346
|
-
|
|
381
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
382
|
+
console.error(
|
|
383
|
+
"[codex-oauth] persistCodexOAuth failed (non-fatal):",
|
|
384
|
+
redactTokens(message, [creds.access, creds.refresh]),
|
|
385
|
+
);
|
|
347
386
|
}
|
|
348
387
|
}
|
|
349
388
|
|