@desplega.ai/agent-swarm 2.7.6 → 2.7.7
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-bp91d37f.js → acp-adapter-7b4vk3h5.js} +5 -4
- package/dist/{actions-yj0eeaqh.js → actions-3tt153af.js} +18 -18
- package/dist/{amp-adapter-4pg9h34q.js → amp-adapter-njpts0yr.js} +1 -1
- package/dist/{anthropic-messages-x4q6rfq5.js → anthropic-messages-xey2skwx.js} +7 -7
- package/dist/{app-909e97cr.js → app-nky4378f.js} +3 -3
- package/dist/{assistant-qdyb5fyy.js → assistant-jnb669pa.js} +19 -19
- package/dist/{azure-openai-responses-s1w5n70z.js → azure-openai-responses-26t7j9ra.js} +5 -5
- package/dist/{boot-reembed-fk0s9z18.js → boot-reembed-9mgvaztt.js} +1 -1
- package/dist/{boot-reembed-gs1rzrnh.js → boot-reembed-qs2djdej.js} +1 -1
- package/dist/{boot-scrub-sweep-4qfd4kfx.js → boot-scrub-sweep-yjpdj7s1.js} +3 -3
- package/dist/{boot-seeds-yr00k13d.js → boot-seeds-5cm6ambd.js} +4 -4
- package/dist/{boxd-vdmfssd9.js → boxd-nv507va8.js} +1 -1
- package/dist/{claude-adapter-qta1f1xn.js → claude-adapter-fn287zmm.js} +5 -4
- package/dist/{claude-managed-setup-sc6j4dm0.js → claude-managed-setup-wtdz2mj2.js} +1 -1
- package/dist/{claude-sdk-session-zgrz6yvc.js → claude-sdk-session-x91kqyhm.js} +8288 -8224
- package/dist/{cli-wp9sp1qb.js → cli-08bmz9z9.js} +1 -1
- package/dist/{cli-yy3rhsdw.js → cli-1c6tzt8g.js} +7 -7
- package/dist/{cli-q0k1w5zd.js → cli-35rsp8ye.js} +1 -1
- package/dist/{cli-xnrwazav.js → cli-3fg0bdcv.js} +1 -1
- package/dist/{cli-89knwags.js → cli-5b456bpf.js} +2 -2
- package/dist/{cli-drr5v6av.js → cli-5jnhv72d.js} +1 -1
- package/dist/{cli-dxae2yg7.js → cli-5zx699j9.js} +2 -2
- package/dist/{cli-0at8w34w.js → cli-6g0hb4cp.js} +8 -8
- package/dist/{cli-e3r2neck.js → cli-6pfd54vx.js} +5 -5
- package/dist/{cli-bsn8nhrq.js → cli-6vcp76qk.js} +3 -3
- package/dist/{cli-ekwks2vs.js → cli-72v799xn.js} +1 -1
- package/dist/cli-82j36f2m.js +45 -0
- package/dist/{cli-yx4kfngt.js → cli-8305gbq0.js} +29 -16
- package/dist/{cli-4necwtsv.js → cli-8sckt74b.js} +8 -8
- package/dist/{cli-vvpx3gsx.js → cli-95jzxcp3.js} +1 -1
- package/dist/{cli-hr2e2fft.js → cli-adeegh7d.js} +7 -7
- package/dist/{cli-m48g6p95.js → cli-afkqkrtw.js} +3 -3
- package/dist/{cli-0qatpy8q.js → cli-ayayvfg4.js} +24 -4
- package/dist/{cli-bqt1mptt.js → cli-az48n0nr.js} +4 -4
- package/dist/{cli-zm72anhg.js → cli-bfv7vdg6.js} +9 -9
- package/dist/{cli-hgaazk69.js → cli-caj33vcq.js} +5 -5
- package/dist/{cli-3ypk9vk1.js → cli-cb2a8pna.js} +1 -1
- package/dist/{cli-waqbxg08.js → cli-cppkf404.js} +17 -17
- package/dist/{cli-yn4kxs06.js → cli-ddaanqkd.js} +4 -4
- package/dist/{cli-386tyxxg.js → cli-e0kmphd6.js} +2 -2
- package/dist/{cli-54t00gt8.js → cli-e46hg0xw.js} +1 -1
- package/dist/{cli-jm2vfpfd.js → cli-e5bzrn01.js} +1 -1
- package/dist/{cli-436532ck.js → cli-excnnjg2.js} +165 -153
- package/dist/{cli-71ptav7x.js → cli-ferkkrg9.js} +1 -1
- package/dist/{cli-vpf89f34.js → cli-fmjd0t1y.js} +3 -3
- package/dist/{cli-pd7vagab.js → cli-fyt2g4wd.js} +3 -3
- package/dist/{cli-gdhpcbhf.js → cli-g08zdaec.js} +1 -1
- package/dist/{cli-ehq736cn.js → cli-g7s47cc5.js} +4 -4
- package/dist/{cli-svj5090z.js → cli-gz6q4eve.js} +3 -3
- package/dist/{cli-8snvgs5r.js → cli-je518txm.js} +10 -10
- package/dist/{cli-rmmmrjkd.js → cli-jtjtabxe.js} +1 -1
- package/dist/{cli-611a1294.js → cli-k0wwfqbg.js} +18 -1
- package/dist/{cli-es6va8jk.js → cli-k1nje3v3.js} +38 -43
- package/dist/{cli-p5gp8pcx.js → cli-k8196sh2.js} +6 -6
- package/dist/{cli-nwj0ee2w.js → cli-k899ehvh.js} +2 -2
- package/dist/{cli-decfb6v8.js → cli-m0da8j35.js} +23 -9
- package/dist/{cli-he9wchme.js → cli-mkk9rnzz.js} +4 -4
- package/dist/{cli-nhrvpaj7.js → cli-n6bv12jn.js} +510 -4
- package/dist/{cli-8bc1w0tw.js → cli-nmzhp1b5.js} +14 -12
- package/dist/{cli-0yv5z2pg.js → cli-p82cnnjq.js} +1 -1
- package/dist/{cli-c60vetmm.js → cli-pc6n7fre.js} +1 -1
- package/dist/{cli-xds5f77d.js → cli-pjctyea3.js} +2 -2
- package/dist/{cli-xbbndvjy.js → cli-pq73h5mz.js} +3 -3
- package/dist/{cli-aa6yxc7m.js → cli-pz4rk021.js} +1 -1
- package/dist/{cli-xnqgpshy.js → cli-q3tkegby.js} +1 -1
- package/dist/cli-qghe0h66.js +67 -0
- package/dist/{cli-zbbwez0r.js → cli-qh4d4tvk.js} +4 -4
- package/dist/{cli-w6dt9ged.js → cli-rtt8g4v9.js} +1 -1
- package/dist/{cli-1h3wcpbw.js → cli-snpj5zbj.js} +2 -2
- package/dist/{cli-a7v854rf.js → cli-tgrv9cjx.js} +263 -12
- package/dist/{cli-faxe6thg.js → cli-vz22fyqv.js} +186 -54
- package/dist/{cli-k2gzwsj2.js → cli-wk5yrfwd.js} +160 -26
- package/dist/{cli-shpbx5ee.js → cli-ws19768n.js} +1641 -49
- package/dist/{cli-70r77n04.js → cli-xae4g51r.js} +1 -1
- package/dist/{cli-kqdg1bsb.js → cli-yndmfxb3.js} +2 -2
- package/dist/{cli-bjb3z51z.js → cli-yyj3j5m4.js} +2 -2
- package/dist/cli.js +21 -21
- package/dist/{cloudflare-workers-ai-system-one-bm60fjm0.js → cloudflare-workers-ai-system-one-ef2a980q.js} +2 -2
- package/dist/{codex-adapter-ayd1fb6h.js → codex-adapter-cf2yxe9m.js} +3 -2
- package/dist/{codex-hook-9efm2x1m.js → codex-hook-a0c6g6t6.js} +1 -1
- package/dist/{codex-session-runner-jesa32xc.js → codex-session-runner-6vfzdgbd.js} +3 -2
- package/dist/{commands-hanewgf2.js → commands-t3mfy7vn.js} +3 -3
- package/dist/{controller-dt1n3c1q.js → controller-q7xrbaz0.js} +3 -3
- package/dist/{controllers-connect-xx8x64n5.js → controllers-connect-c6nm70kg.js} +19 -19
- package/dist/{cursor-adapter-9s4dwwdw.js → cursor-adapter-ftd81ga8.js} +1 -1
- package/dist/{db-3vttcbv4.js → db-dk572x25.js} +3 -3
- package/dist/{db-9dv0g1ab.js → db-vemtpmcc.js} +4 -2
- package/dist/{dispatcher-tx9hd1sv.js → dispatcher-kh7ecxh9.js} +17 -17
- package/dist/{dsh-adapter-007y4ejs.js → dsh-adapter-v8628r89.js} +1 -1
- package/dist/{e2b-we6hs6q5.js → e2b-fr12yccx.js} +1 -1
- package/dist/{e2b-a19d28rz.js → e2b-x6ywzak3.js} +1 -1
- package/dist/{easter-egg-3d-rgbrqb8s.js → easter-egg-3d-nenw4x63.js} +1 -1
- package/dist/{embedding-space-ny1e2940.js → embedding-space-0jepb239.js} +6 -6
- package/dist/{embeddings-2t386t13.js → embeddings-xxt49yxg.js} +1 -1
- package/dist/{execute-pksh0ceh.js → execute-1mhw6e4x.js} +1 -1
- package/dist/{external-channel-phgmhmd7.js → external-channel-55v12e60.js} +2 -2
- package/dist/{fx-adapter-fy8rr921.js → fx-adapter-bs8wqrk5.js} +6 -5
- package/dist/{google-generative-ai-8y3m3k4b.js → google-generative-ai-v94hr7h0.js} +5 -5
- package/dist/{google-vertex-w3v98n7b.js → google-vertex-g7vr6cf8.js} +5 -5
- package/dist/{grok-adapter-q0abvpaj.js → grok-adapter-gpjqmkcq.js} +6 -5
- package/dist/handlers-zxmp1ta4.js +32 -0
- package/dist/{heartbeat-kadyek8p.js → heartbeat-k42svnr0.js} +20 -20
- package/dist/{hook-9k551xnj.js → hook-114m7hap.js} +4 -3
- package/dist/{hook-5zs3msf6.js → hook-6nphweh0.js} +24 -9
- package/dist/{http-mhz9vw1f.js → http-r8cc4wz7.js} +1025 -173
- package/dist/index-a8phxqnk.js +37 -0
- package/dist/{index-vdygbq10.js → index-e3s8wmxy.js} +1 -1
- package/dist/{index-q361cg2f.js → index-fpk4bcd9.js} +1 -1
- package/dist/index-k6h00t4c.js +30 -0
- package/dist/{index-rvr00sg7.js → index-nh3wyqdt.js} +1017 -574
- package/dist/index-xrz7te8h.js +33 -0
- package/dist/{keepalive-w56f32cg.js → keepalive-x060nt9p.js} +3 -3
- package/dist/lead-956sd2yc.js +71 -0
- package/dist/{maintenance-7mdgxcea.js → maintenance-2xxfexhf.js} +1 -1
- package/dist/{mistral-conversations-4pmwj1c0.js → mistral-conversations-3gjpdb4b.js} +4 -4
- package/dist/{oauth-refresh-sweep-29txy9vk.js → oauth-refresh-sweep-fckc4ar5.js} +2 -2
- package/dist/{onboard-kpsrkarj.js → onboard-zc48j237.js} +3 -3
- package/dist/{openai-codex-responses-44j573nm.js → openai-codex-responses-8pbxq9h5.js} +5 -5
- package/dist/{openai-completions-a4gee2a8.js → openai-completions-rm00s912.js} +4 -4
- package/dist/{openai-decisions-w4qkrcar.js → openai-decisions-8v957r2r.js} +1 -1
- package/dist/{openai-responses-rbp6h5kr.js → openai-responses-agxpf96k.js} +5 -5
- package/dist/{opencode-adapter-j25vm3rr.js → opencode-adapter-r690h1cr.js} +4 -3
- package/dist/{otel-impl-6kkct7a8.js → otel-impl-r5xbh81q.js} +4 -4
- package/dist/{pi-mono-adapter-exhkzhn2.js → pi-mono-adapter-c7v8sdec.js} +7 -6
- package/dist/{pricing-refresh-n2x7jh6k.js → pricing-refresh-3pmre11v.js} +3 -3
- package/dist/{rbac-roles-81ypx6es.js → rbac-roles-3vbshwsb.js} +1 -1
- package/dist/{rbac-roles-b5htkjby.js → rbac-roles-910mmkgz.js} +2 -2
- package/dist/{render-v2-qn6jmpk4.js → render-v2-pxykkbab.js} +3 -3
- package/dist/{resume-wd30wx23.js → resume-adda0bwy.js} +6 -6
- package/dist/{runtime-4hsg7h60.js → runtime-fnbm7stk.js} +1 -1
- package/dist/{session-access-5qvfevhk.js → session-access-2fvamrdc.js} +4 -4
- package/dist/{session-source-backfill-n1czavbr.js → session-source-backfill-08xvv0sm.js} +2 -2
- package/dist/{setup-dnb56qy5.js → setup-1aqmvmh1.js} +1 -1
- package/dist/{typesafe-system-one-5mavm259.js → typesafe-system-one-0bmqcxfg.js} +2 -2
- package/dist/{virtual-modules-76xxv9fs.js → virtual-modules-d2pjk9j1.js} +5 -5
- package/dist/{work-objects-4n21tfkm.js → work-objects-9s0yrm5j.js} +20 -20
- package/dist/{worker-follow-up-77hvzxww.js → worker-follow-up-3q78fqmr.js} +17 -17
- package/dist/worker-y15hy08h.js +71 -0
- package/openapi.json +197 -4
- package/package.json +5 -5
- package/src/apps/definition.ts +6 -5
- package/src/be/db/page-theme-tokens.ts +12 -0
- package/src/be/db/tasks/read.ts +9 -1
- package/src/be/db/tasks/write.ts +19 -0
- package/src/be/db-client.ts +107 -7
- package/src/be/db.ts +34 -2
- package/src/be/migrations/269_pages_design_system.sql +23 -0
- package/src/be/seed/schedules-seeder.ts +0 -10
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +2 -0
- package/src/be/seed-scripts/catalog/catalog-report.ts +2 -0
- package/src/be/seed-scripts/catalog/memory-eval.ts +2 -0
- package/src/be/seed-scripts/catalog/swarm-update-check.ts +150 -0
- package/src/be/seed-scripts/index.ts +9 -0
- package/src/be/seed-skills/bundled-files.generated.json +3 -3
- package/src/be/swarm-config-guard.ts +32 -0
- package/src/be/task-auto-title-listener.ts +127 -0
- package/src/commands/onboard/agent-fs-version.ts +1 -1
- package/src/commands/profile-sync.ts +151 -16
- package/src/commands/runner.ts +15 -7
- package/src/hooks/hook.ts +20 -9
- package/src/http/index.ts +3 -0
- package/src/http/pages-public.ts +93 -129
- package/src/http/pages.ts +94 -21
- package/src/pages/browser-sdk.ts +48 -21
- package/src/pages/color-literals.ts +115 -0
- package/src/pages/page-head.ts +766 -0
- package/src/pages/version.ts +3 -0
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +1 -0
- package/src/providers/codex-app-server.ts +27 -1
- package/src/providers/pi-mono-extension.ts +30 -15
- package/src/tasks/follow-up-gate.ts +318 -0
- package/src/tasks/worker-follow-up.ts +21 -1
- package/src/tests/codex-app-server.test.ts +59 -0
- package/src/tests/codex-gateway-route.test.ts +50 -1
- package/src/tests/create-page-tool.test.ts +501 -0
- package/src/tests/db-client-busy-retry.test.ts +81 -0
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +2 -0
- package/src/tests/db-tasks-read.test.ts +21 -0
- package/src/tests/fixtures/pages/head-v1.html +56 -0
- package/src/tests/follow-up-gate-shadow.test.ts +513 -0
- package/src/tests/identity-field-budget.test.ts +1 -1
- package/src/tests/internal-ai/credentials.test.ts +62 -0
- package/src/tests/internal-ai/task-title.test.ts +283 -0
- package/src/tests/merge-gate-detect-changes.test.ts +145 -0
- package/src/tests/page-proxy.test.ts +1 -1
- package/src/tests/page-session-dev-cookie.test.ts +188 -0
- package/src/tests/pages-frame-ancestors.test.ts +28 -1
- package/src/tests/pages-head-v2.test.ts +534 -0
- package/src/tests/pages-http.test.ts +330 -0
- package/src/tests/pages-public-html.test.ts +43 -0
- package/src/tests/pages-storage.test.ts +182 -0
- package/src/tests/pages-versioning.test.ts +72 -0
- package/src/tests/profile-sync-stale-writeback.test.ts +334 -0
- package/src/tests/profile-sync.test.ts +11 -15
- package/src/tests/scheduled-task-required-params-migration.test.ts +3 -1
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/seed-registry.test.ts +28 -6
- package/src/tests/seed-schedules.test.ts +61 -8
- package/src/tests/seed-scripts.test.ts +142 -2
- package/src/tests/swarm-diff.test.ts +121 -2
- package/src/tests/task-auto-title-config-guard.test.ts +37 -0
- package/src/tests/task-auto-title-listener.test.ts +165 -0
- package/src/tests/theme-import.test.ts +758 -0
- package/src/tests/themes-page-theme.test.ts +155 -0
- package/src/tests/themes-presets.test.ts +172 -0
- package/src/tests/workflow-script-principal.test.ts +2 -1
- package/src/themes/classics.ts +949 -0
- package/src/themes/import.ts +420 -0
- package/src/themes/page-fields.ts +40 -0
- package/src/themes/page-theme.ts +102 -0
- package/src/themes/presets.ts +561 -0
- package/src/themes/tokens.ts +218 -0
- package/src/tools/app-upsert.ts +1 -1
- package/src/tools/create-page.ts +118 -12
- package/src/tools/utils.ts +33 -0
- package/src/types.ts +45 -3
- package/src/utils/constants.ts +19 -0
- package/src/utils/follow-up-gate-schedule-ids.ts +13 -0
- package/src/utils/internal-ai/credentials.ts +146 -93
- package/src/utils/internal-ai/index.ts +10 -0
- package/src/utils/internal-ai/models.ts +18 -0
- package/src/utils/internal-ai/task-title.ts +282 -0
- package/src/utils/page-session.ts +37 -0
- package/templates/schedules/daily-status-report/content.md +88 -2
- package/templates/schedules/daily-swarm-update-check/content.md +16 -40
- package/templates/skills/apps/SKILL.md +1 -1
- package/templates/skills/apps/content.md +1 -1
- package/templates/skills/heartbeat-runbook/content.md +1 -1
- package/templates/skills/pages/config.json +1 -1
- package/templates/skills/pages/content.md +539 -86
- package/templates/skills/pages/files/examples/annotated-pr.html +21 -12
- package/templates/skills/pages/files/examples/multiplayer-board.html +41 -41
- package/templates/skills/pages/files/examples/report-page.html +125 -124
- package/dist/cli-2pnswgh8.js +0 -189
- package/dist/handlers-by3sfxyx.js +0 -32
- package/dist/index-4f2pvxae.js +0 -37
- package/dist/index-98gz7tmp.js +0 -30
- package/dist/index-k08z2cak.js +0 -33
- package/dist/lead-enq35nqp.js +0 -70
- package/dist/worker-gg1576j3.js +0 -70
- package/templates/schedules/daily-workflow-health-audit/config.json +0 -19
- package/templates/schedules/daily-workflow-health-audit/content.md +0 -191
|
@@ -108,6 +108,30 @@ export interface ResolveCredentialOptions {
|
|
|
108
108
|
_persistCodexOAuth?: typeof persistCodexOAuth;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Credential kinds in precedence order. `resolveCredentials` walks this list;
|
|
113
|
+
* `resolveCredential` returns the first hit.
|
|
114
|
+
*/
|
|
115
|
+
export const CREDENTIAL_KIND_PRECEDENCE: readonly CredentialKind[] = [
|
|
116
|
+
"openrouter",
|
|
117
|
+
"anthropic",
|
|
118
|
+
"openai",
|
|
119
|
+
"openai-codex",
|
|
120
|
+
"claude-cli",
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
export interface ResolveCredentialsOptions extends ResolveCredentialOptions {
|
|
124
|
+
/**
|
|
125
|
+
* Only resolve these kinds (in precedence order). Lets a caller probe one
|
|
126
|
+
* provider at a time, so the codex OAuth lookup (an HTTP call that can
|
|
127
|
+
* rotate a refresh token) runs only when that kind is actually needed.
|
|
128
|
+
* Default: every kind.
|
|
129
|
+
*/
|
|
130
|
+
kinds?: readonly CredentialKind[];
|
|
131
|
+
/** Stop after this many credentials. Default: no limit. */
|
|
132
|
+
limit?: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
111
135
|
/**
|
|
112
136
|
* Resolve a credential according to the documented precedence. Returns `null`
|
|
113
137
|
* if no credential could be resolved — callers MUST treat null as a graceful
|
|
@@ -125,112 +149,141 @@ export interface ResolveCredentialOptions {
|
|
|
125
149
|
export async function resolveCredential(
|
|
126
150
|
opts: ResolveCredentialOptions = {},
|
|
127
151
|
): Promise<ResolvedCredential | null> {
|
|
152
|
+
const [first] = await resolveCredentials({ ...opts, limit: 1 });
|
|
153
|
+
return first ?? null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Every credential the environment offers, in the precedence order documented
|
|
158
|
+
* on {@link resolveCredential}. Callers that can fall back across providers
|
|
159
|
+
* (task titles) walk the list; `resolveCredential` is the `limit: 1` case.
|
|
160
|
+
* The codex OAuth probe runs only when `openai-codex` is reached, so an env
|
|
161
|
+
* key ahead of it (or `kinds` that exclude it) never triggers the HTTP call.
|
|
162
|
+
*/
|
|
163
|
+
export async function resolveCredentials(
|
|
164
|
+
opts: ResolveCredentialsOptions = {},
|
|
165
|
+
): Promise<ResolvedCredential[]> {
|
|
128
166
|
const env = opts.env ?? process.env;
|
|
129
167
|
const getEnvKey = opts._getEnvApiKey ?? getEnvApiKey;
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
const
|
|
168
|
+
const wanted = opts.kinds ?? CREDENTIAL_KIND_PRECEDENCE;
|
|
169
|
+
const limit = opts.limit ?? Number.POSITIVE_INFINITY;
|
|
170
|
+
const found: ResolvedCredential[] = [];
|
|
133
171
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
apiKey: selectCredential(openrouterKey, undefined, "OPENROUTER_API_KEY").selected,
|
|
140
|
-
modelDefault: resolveRaterModelString("openrouter"),
|
|
141
|
-
};
|
|
172
|
+
for (const kind of CREDENTIAL_KIND_PRECEDENCE) {
|
|
173
|
+
if (found.length >= limit) break;
|
|
174
|
+
if (!wanted.includes(kind)) continue;
|
|
175
|
+
const cred = await resolveKind(kind, opts, env, getEnvKey);
|
|
176
|
+
if (cred) found.push(cred);
|
|
142
177
|
}
|
|
178
|
+
return found;
|
|
179
|
+
}
|
|
143
180
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
181
|
+
async function resolveKind(
|
|
182
|
+
kind: CredentialKind,
|
|
183
|
+
opts: ResolveCredentialOptions,
|
|
184
|
+
env: NodeJS.ProcessEnv,
|
|
185
|
+
getEnvKey: typeof getEnvApiKey,
|
|
186
|
+
): Promise<ResolvedCredential | null> {
|
|
187
|
+
switch (kind) {
|
|
188
|
+
case "openrouter": {
|
|
189
|
+
const key = env.OPENROUTER_API_KEY ?? getEnvKey("openrouter");
|
|
190
|
+
if (!key) return null;
|
|
191
|
+
return {
|
|
192
|
+
kind,
|
|
193
|
+
apiKey: selectCredential(key, undefined, "OPENROUTER_API_KEY").selected,
|
|
194
|
+
modelDefault: resolveRaterModelString(kind),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
case "anthropic": {
|
|
198
|
+
// pi-ai sends this key to api.anthropic.com, so skip it when
|
|
199
|
+
// ANTHROPIC_BASE_URL names a gateway: the key belongs to the gateway (#1800).
|
|
200
|
+
const anthropicBaseUrl = env.ANTHROPIC_BASE_URL?.trim();
|
|
201
|
+
const keyIsForGateway =
|
|
202
|
+
Boolean(anthropicBaseUrl) && !isFirstPartyAnthropicUrl(anthropicBaseUrl ?? "");
|
|
203
|
+
const key = keyIsForGateway ? undefined : (env.ANTHROPIC_API_KEY ?? getEnvKey("anthropic"));
|
|
204
|
+
if (!key) return null;
|
|
205
|
+
return {
|
|
206
|
+
kind,
|
|
207
|
+
apiKey: selectCredential(key, undefined, "ANTHROPIC_API_KEY").selected,
|
|
208
|
+
modelDefault: resolveRaterModelString(kind),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
case "openai": {
|
|
212
|
+
const key = env.OPENAI_API_KEY ?? getEnvKey("openai");
|
|
213
|
+
if (!key) return null;
|
|
214
|
+
return {
|
|
215
|
+
kind,
|
|
216
|
+
apiKey: selectCredential(key, undefined, "OPENAI_API_KEY").selected,
|
|
217
|
+
modelDefault: resolveRaterModelString(kind),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
case "openai-codex":
|
|
221
|
+
// Only if we have apiUrl + apiKey to probe the config store.
|
|
222
|
+
if (!opts.apiUrl || !opts.apiKey) return null;
|
|
223
|
+
return resolveCodexOAuthCredential(opts, opts.apiUrl, opts.apiKey);
|
|
224
|
+
case "claude-cli":
|
|
225
|
+
// `AGENT_SWARM_CLAUDE_OAUTH_TOKEN` is the mirror set by claude-adapter.ts
|
|
226
|
+
// before spawning `claude` — the CLI strips `CLAUDE_CODE_OAUTH_TOKEN` from
|
|
227
|
+
// hook subprocesses (security), so the hook reads the mirror instead.
|
|
228
|
+
if (!env.AGENT_SWARM_CLAUDE_OAUTH_TOKEN && !env.CLAUDE_CODE_OAUTH_TOKEN) return null;
|
|
229
|
+
return { kind, modelDefault: resolveRaterModelString(kind) };
|
|
158
230
|
}
|
|
231
|
+
}
|
|
159
232
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
233
|
+
async function resolveCodexOAuthCredential(
|
|
234
|
+
opts: ResolveCredentialOptions,
|
|
235
|
+
apiUrl: string,
|
|
236
|
+
apiKey: string,
|
|
237
|
+
): Promise<ResolvedCredential | null> {
|
|
238
|
+
const getCodex = opts._getValidCodexOAuth ?? getValidCodexOAuth;
|
|
239
|
+
const getOAuth = opts._getOAuthApiKey ?? resolveOAuthApiKey;
|
|
240
|
+
const persistCodex = opts._persistCodexOAuth ?? persistCodexOAuth;
|
|
241
|
+
try {
|
|
242
|
+
const codexCreds = await getCodex(apiUrl, apiKey);
|
|
243
|
+
if (!codexCreds) return null;
|
|
244
|
+
// pi-ai expects a Record<providerID, OAuthCredentials>.
|
|
245
|
+
const credMap: Record<string, OAuthCredentials> = {
|
|
246
|
+
"openai-codex": {
|
|
247
|
+
access: codexCreds.access,
|
|
248
|
+
refresh: codexCreds.refresh,
|
|
249
|
+
expires: codexCreds.expires,
|
|
250
|
+
},
|
|
167
251
|
};
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
// failure must never prevent the current call from succeeding.
|
|
189
|
-
if (oauthResult.newCredentials) {
|
|
190
|
-
const updated = oauthResult.newCredentials;
|
|
191
|
-
try {
|
|
192
|
-
await persistCodex(opts.apiUrl, opts.apiKey, {
|
|
193
|
-
access: String(updated.access),
|
|
194
|
-
refresh: String(updated.refresh),
|
|
195
|
-
expires: Number(updated.expires),
|
|
196
|
-
accountId: codexCreds.accountId,
|
|
197
|
-
});
|
|
198
|
-
} catch (err) {
|
|
199
|
-
console.error(
|
|
200
|
-
`internal-ai: persistCodexOAuth failed (callerTag=${opts.callerTag ?? "<unset>"}):`,
|
|
201
|
-
err,
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return {
|
|
206
|
-
kind: "openai-codex",
|
|
207
|
-
apiKey: oauthResult.apiKey,
|
|
208
|
-
modelDefault: resolveRaterModelString("openai-codex"),
|
|
209
|
-
};
|
|
210
|
-
}
|
|
252
|
+
const oauthResult = await getOAuth("openai-codex", credMap);
|
|
253
|
+
if (!oauthResult) return null;
|
|
254
|
+
// Persist any rotated refresh token — best-effort, must not block.
|
|
255
|
+
// Wrap defensively here even though the production helper already
|
|
256
|
+
// swallows; tests may inject a throwing hook, and persistence
|
|
257
|
+
// failure must never prevent the current call from succeeding.
|
|
258
|
+
if (oauthResult.newCredentials) {
|
|
259
|
+
const updated = oauthResult.newCredentials;
|
|
260
|
+
try {
|
|
261
|
+
await persistCodex(apiUrl, apiKey, {
|
|
262
|
+
access: String(updated.access),
|
|
263
|
+
refresh: String(updated.refresh),
|
|
264
|
+
expires: Number(updated.expires),
|
|
265
|
+
accountId: codexCreds.accountId,
|
|
266
|
+
});
|
|
267
|
+
} catch (err) {
|
|
268
|
+
console.error(
|
|
269
|
+
`internal-ai: persistCodexOAuth failed (callerTag=${opts.callerTag ?? "<unset>"}):`,
|
|
270
|
+
err,
|
|
271
|
+
);
|
|
211
272
|
}
|
|
212
|
-
} catch (err) {
|
|
213
|
-
console.error(
|
|
214
|
-
`internal-ai: codex OAuth probe failed (callerTag=${opts.callerTag ?? "<unset>"}):`,
|
|
215
|
-
err,
|
|
216
|
-
);
|
|
217
|
-
// Fall through to claude-cli fallback below.
|
|
218
273
|
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// 5. CLAUDE_CODE_OAUTH_TOKEN → claude-cli fallback.
|
|
222
|
-
// `AGENT_SWARM_CLAUDE_OAUTH_TOKEN` is the mirror set by claude-adapter.ts
|
|
223
|
-
// before spawning `claude` — the CLI strips `CLAUDE_CODE_OAUTH_TOKEN` from
|
|
224
|
-
// hook subprocesses (security), so the hook reads the mirror instead.
|
|
225
|
-
if (env.AGENT_SWARM_CLAUDE_OAUTH_TOKEN || env.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
226
274
|
return {
|
|
227
|
-
kind: "
|
|
228
|
-
|
|
275
|
+
kind: "openai-codex",
|
|
276
|
+
apiKey: oauthResult.apiKey,
|
|
277
|
+
modelDefault: resolveRaterModelString("openai-codex"),
|
|
229
278
|
};
|
|
279
|
+
} catch (err) {
|
|
280
|
+
console.error(
|
|
281
|
+
`internal-ai: codex OAuth probe failed (callerTag=${opts.callerTag ?? "<unset>"}):`,
|
|
282
|
+
err,
|
|
283
|
+
);
|
|
284
|
+
// The caller falls through to the claude-cli fallback.
|
|
285
|
+
return null;
|
|
230
286
|
}
|
|
231
|
-
|
|
232
|
-
// 6. No creds.
|
|
233
|
-
return null;
|
|
234
287
|
}
|
|
235
288
|
|
|
236
289
|
// Re-export for convenience to keep imports flat.
|
|
@@ -19,11 +19,14 @@ export {
|
|
|
19
19
|
completeStructuredWithModel,
|
|
20
20
|
} from "./complete-structured.js";
|
|
21
21
|
export {
|
|
22
|
+
CREDENTIAL_KIND_PRECEDENCE,
|
|
22
23
|
type CredentialKind,
|
|
23
24
|
DEFAULT_MODEL,
|
|
24
25
|
type ResolveCredentialOptions,
|
|
26
|
+
type ResolveCredentialsOptions,
|
|
25
27
|
type ResolvedCredential,
|
|
26
28
|
resolveCredential,
|
|
29
|
+
resolveCredentials,
|
|
27
30
|
} from "./credentials.js";
|
|
28
31
|
export {
|
|
29
32
|
MEMORY_RATER_DEFAULT_MODEL,
|
|
@@ -36,3 +39,10 @@ export {
|
|
|
36
39
|
summarizeSession,
|
|
37
40
|
summaryToolSchema,
|
|
38
41
|
} from "./summarize-session.js";
|
|
42
|
+
export {
|
|
43
|
+
type GenerateTaskTitleOptions,
|
|
44
|
+
type GenerateTaskTitleResult,
|
|
45
|
+
generateTaskTitle,
|
|
46
|
+
TASK_TITLE_MODEL_CANDIDATES,
|
|
47
|
+
TASK_TITLE_MODEL_ENV,
|
|
48
|
+
} from "./task-title.js";
|
|
@@ -59,6 +59,24 @@ export function resolveRaterModelString(kind: CredentialKind): string {
|
|
|
59
59
|
return process.env.MEMORY_RATER_MODEL ?? MEMORY_RATER_DEFAULT_MODEL[kind];
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
const PROVIDER_TO_KIND: Record<string, CredentialKind> = {
|
|
63
|
+
openrouter: "openrouter",
|
|
64
|
+
anthropic: "anthropic",
|
|
65
|
+
openai: "openai",
|
|
66
|
+
"openai-codex": "openai-codex",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Credential kind a model string belongs to. `provider/model-id` maps through
|
|
71
|
+
* the provider prefix; a bare alias (`haiku`) is a `claude -p` model. `null`
|
|
72
|
+
* for a provider the internal-ai layer cannot call.
|
|
73
|
+
*/
|
|
74
|
+
export function kindForModelString(modelStr: string): CredentialKind | null {
|
|
75
|
+
const slash = modelStr.indexOf("/");
|
|
76
|
+
if (slash < 0) return "claude-cli";
|
|
77
|
+
return PROVIDER_TO_KIND[modelStr.slice(0, slash)] ?? null;
|
|
78
|
+
}
|
|
79
|
+
|
|
62
80
|
/**
|
|
63
81
|
* Split a `provider/model-id` string on the FIRST `/` so that OpenRouter
|
|
64
82
|
* compound IDs like `openrouter/deepseek/deepseek-v4.1-flash` parse as
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task title generation: a short display title for a task prompt.
|
|
3
|
+
*
|
|
4
|
+
* Consumer of `completeStructuredWithModel`. Walks `TASK_TITLE_MODEL_CANDIDATES`
|
|
5
|
+
* (provider order, then model order) and returns the first title a configured
|
|
6
|
+
* credential produces. Every candidate is a cheap, fast model: one title costs
|
|
7
|
+
* well under a tenth of a cent, so thousands of weekly tasks stay in cents.
|
|
8
|
+
*
|
|
9
|
+
* Worker-safe: no bun:sqlite import. The API server wires it to task creation
|
|
10
|
+
* in `src/be/task-auto-title-listener.ts`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Type } from "typebox";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import { scrubSecrets } from "../secret-scrubber.js";
|
|
16
|
+
import { completeStructuredWithModel } from "./complete-structured.js";
|
|
17
|
+
import { type CredentialKind, type ResolvedCredential, resolveCredentials } from "./credentials.js";
|
|
18
|
+
import { kindForModelString } from "./models.js";
|
|
19
|
+
|
|
20
|
+
/** Longest generated title. The PATCH /api/tasks/{id}/title route allows 120. */
|
|
21
|
+
export const TASK_TITLE_MAX_CHARS = 100;
|
|
22
|
+
/** Prompts shorter than this keep the prompt itself as the display title. */
|
|
23
|
+
export const TASK_TITLE_MIN_TASK_CHARS = 48;
|
|
24
|
+
/** Prompt text sent to the model is cut here. Keeps every call small. */
|
|
25
|
+
export const TASK_TITLE_MAX_TASK_CHARS = 2000;
|
|
26
|
+
/** Whole generation, across every candidate. */
|
|
27
|
+
export const TASK_TITLE_TIMEOUT_MS = 20_000;
|
|
28
|
+
|
|
29
|
+
/** Env var that pins one model (`provider/model-id`, or a `claude -p` alias). */
|
|
30
|
+
export const TASK_TITLE_MODEL_ENV = "TASK_AUTO_TITLE_MODEL";
|
|
31
|
+
|
|
32
|
+
export interface TaskTitleCandidateGroup {
|
|
33
|
+
kind: CredentialKind;
|
|
34
|
+
/** Full model strings for this credential kind, best first. */
|
|
35
|
+
models: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Provider order, then model order within a provider. Cheapest and fastest
|
|
40
|
+
* first: pay-per-token API keys before subscription OAuth, and the `claude -p`
|
|
41
|
+
* shellout last (it spawns a full CLI process per call).
|
|
42
|
+
*
|
|
43
|
+
* Model prices (input / output, USD per million tokens, models.dev snapshot):
|
|
44
|
+
* deepseek-v4.1-flash 0.034 / 0.137 · claude-haiku-5.5 0.10 / 0.50 ·
|
|
45
|
+
* gpt-6-luna 0.10 / 0.50 · gpt-5-nano 0.05 / 0.40
|
|
46
|
+
*/
|
|
47
|
+
export const TASK_TITLE_MODEL_CANDIDATES: readonly TaskTitleCandidateGroup[] = [
|
|
48
|
+
{
|
|
49
|
+
kind: "openrouter",
|
|
50
|
+
models: [
|
|
51
|
+
"openrouter/deepseek/deepseek-v4.1-flash",
|
|
52
|
+
"openrouter/anthropic/claude-haiku-5.5",
|
|
53
|
+
"openrouter/openai/gpt-6-luna",
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{ kind: "anthropic", models: ["anthropic/claude-haiku-5-5"] },
|
|
57
|
+
{ kind: "openai", models: ["openai/gpt-6-luna", "openai/gpt-5-nano"] },
|
|
58
|
+
{ kind: "openai-codex", models: ["openai-codex/gpt-6-luna"] },
|
|
59
|
+
{ kind: "claude-cli", models: ["haiku"] },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
export { kindForModelString };
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The candidate list for one environment. `TASK_AUTO_TITLE_MODEL` moves its
|
|
66
|
+
* provider to the front and puts the pinned model first inside it; the rest of
|
|
67
|
+
* the list stays as fallback. An unknown provider is ignored with a warning.
|
|
68
|
+
*/
|
|
69
|
+
export function buildTaskTitlePlan(
|
|
70
|
+
env: Record<string, string | undefined> = process.env,
|
|
71
|
+
): readonly TaskTitleCandidateGroup[] {
|
|
72
|
+
const pinned = env[TASK_TITLE_MODEL_ENV]?.trim();
|
|
73
|
+
if (!pinned) return TASK_TITLE_MODEL_CANDIDATES;
|
|
74
|
+
const kind = kindForModelString(pinned);
|
|
75
|
+
if (!kind) {
|
|
76
|
+
console.warn(
|
|
77
|
+
`[task-title] ${TASK_TITLE_MODEL_ENV}=${pinned} names an unsupported provider; ignored`,
|
|
78
|
+
);
|
|
79
|
+
return TASK_TITLE_MODEL_CANDIDATES;
|
|
80
|
+
}
|
|
81
|
+
const own = TASK_TITLE_MODEL_CANDIDATES.find((group) => group.kind === kind);
|
|
82
|
+
const rest = TASK_TITLE_MODEL_CANDIDATES.filter((group) => group.kind !== kind);
|
|
83
|
+
const models = [pinned, ...(own?.models ?? []).filter((model) => model !== pinned)];
|
|
84
|
+
return [{ kind, models }, ...rest];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const SIBLING_OPEN = "<sibling_tasks_in_progress>";
|
|
88
|
+
const SIBLING_CLOSE = "</sibling_tasks_in_progress>";
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Task text as the model should see it: without the sibling-awareness block
|
|
92
|
+
* the API prepends to child tasks, with runs of blank lines collapsed, and cut
|
|
93
|
+
* to `TASK_TITLE_MAX_TASK_CHARS`.
|
|
94
|
+
*/
|
|
95
|
+
export function prepareTaskTextForTitle(task: string): string {
|
|
96
|
+
let text = task;
|
|
97
|
+
if (text.startsWith(SIBLING_OPEN)) {
|
|
98
|
+
const close = text.indexOf(SIBLING_CLOSE);
|
|
99
|
+
if (close !== -1) text = text.slice(close + SIBLING_CLOSE.length);
|
|
100
|
+
}
|
|
101
|
+
text = text
|
|
102
|
+
.replace(/\r\n?/g, "\n")
|
|
103
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
104
|
+
.trim();
|
|
105
|
+
if (text.length > TASK_TITLE_MAX_TASK_CHARS) {
|
|
106
|
+
text = `${text.slice(0, TASK_TITLE_MAX_TASK_CHARS).trimEnd()} …`;
|
|
107
|
+
}
|
|
108
|
+
return text;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const WRAPPING_QUOTES: ReadonlyArray<[string, string]> = [
|
|
112
|
+
['"', '"'],
|
|
113
|
+
["'", "'"],
|
|
114
|
+
["“", "”"],
|
|
115
|
+
["‘", "’"],
|
|
116
|
+
["`", "`"],
|
|
117
|
+
["«", "»"],
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Clean a model-produced title: one line, no wrapping quotes or `Title:`
|
|
122
|
+
* prefix, no markdown emphasis, no trailing sentence punctuation, at most
|
|
123
|
+
* `TASK_TITLE_MAX_CHARS` (cut at a word boundary). Empty string when nothing
|
|
124
|
+
* usable remains.
|
|
125
|
+
*/
|
|
126
|
+
export function normalizeTaskTitle(raw: string): string {
|
|
127
|
+
let title = raw.replace(/\s+/g, " ").trim();
|
|
128
|
+
title = title.replace(/^title\s*:\s*/i, "");
|
|
129
|
+
title = title.replace(/\*\*|__|^#+\s*/g, "");
|
|
130
|
+
const stripTrailingPunctuation = () => {
|
|
131
|
+
title = title.replace(/[.!…]+$/u, "").trim();
|
|
132
|
+
};
|
|
133
|
+
// Quotes can sit inside or outside the trailing punctuation: `"Title".` and `"Title…"`.
|
|
134
|
+
stripTrailingPunctuation();
|
|
135
|
+
for (const [open, close] of WRAPPING_QUOTES) {
|
|
136
|
+
if (title.length >= 2 && title.startsWith(open) && title.endsWith(close)) {
|
|
137
|
+
title = title.slice(open.length, title.length - close.length).trim();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
stripTrailingPunctuation();
|
|
141
|
+
if (title.length > TASK_TITLE_MAX_CHARS) {
|
|
142
|
+
const cut = title.slice(0, TASK_TITLE_MAX_CHARS);
|
|
143
|
+
const lastSpace = cut.lastIndexOf(" ");
|
|
144
|
+
title = (lastSpace >= TASK_TITLE_MAX_CHARS / 2 ? cut.slice(0, lastSpace) : cut).trimEnd();
|
|
145
|
+
title = title.replace(/[,;:\-–—]+$/u, "").trimEnd();
|
|
146
|
+
}
|
|
147
|
+
return title;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const TASK_TITLE_SYSTEM_PROMPT = [
|
|
151
|
+
"You write titles for tasks on a software team's dashboard.",
|
|
152
|
+
"Rules:",
|
|
153
|
+
"- 3 to 8 words, at most 80 characters.",
|
|
154
|
+
"- Sentence case. No trailing period. No quotes, markdown, emoji, or task ids.",
|
|
155
|
+
"- Name the concrete subject (feature, bug, repo, file, system, person), not a generic verb alone.",
|
|
156
|
+
"- Keep the language of the request.",
|
|
157
|
+
"- Ignore pasted logs, code, and context blocks; title the request itself.",
|
|
158
|
+
"Call the set_title tool with the title.",
|
|
159
|
+
].join("\n");
|
|
160
|
+
|
|
161
|
+
export const TASK_TITLE_TOOL_NAME = "set_title";
|
|
162
|
+
|
|
163
|
+
const TaskTitleZodSchema = z.object({ title: z.string().trim().min(1).max(200) });
|
|
164
|
+
const TaskTitleToolSchema = Type.Object({
|
|
165
|
+
title: Type.String({ minLength: 1, maxLength: 200, description: "The task title." }),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
export interface GenerateTaskTitleOptions {
|
|
169
|
+
/** Full task prompt as stored on the task. */
|
|
170
|
+
task: string;
|
|
171
|
+
/** Credential lookup env. Defaults to `process.env`. */
|
|
172
|
+
env?: Record<string, string | undefined>;
|
|
173
|
+
/** Optional loopback context for the codex OAuth probe (see `resolveCredentials`). */
|
|
174
|
+
apiUrl?: string;
|
|
175
|
+
apiKey?: string;
|
|
176
|
+
signal?: AbortSignal;
|
|
177
|
+
/** Diagnostic tag forwarded to `completeStructured` logs. */
|
|
178
|
+
callerTag?: string;
|
|
179
|
+
// Test injection points:
|
|
180
|
+
_resolveCredentials?: typeof resolveCredentials;
|
|
181
|
+
_completeStructured?: typeof completeStructuredWithModel;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export type GenerateTaskTitleResult =
|
|
185
|
+
| { ok: true; title: string; model: string; kind: CredentialKind }
|
|
186
|
+
| { ok: false; reason: "short-task" | "no-credentials" | "all-models-failed" };
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Generate a title for `task`. Never throws: a provider error moves on to the
|
|
190
|
+
* next candidate, and exhausting the list returns `{ ok: false }`.
|
|
191
|
+
*/
|
|
192
|
+
export async function generateTaskTitle(
|
|
193
|
+
opts: GenerateTaskTitleOptions,
|
|
194
|
+
): Promise<GenerateTaskTitleResult> {
|
|
195
|
+
const text = prepareTaskTextForTitle(opts.task);
|
|
196
|
+
if (text.length < TASK_TITLE_MIN_TASK_CHARS) return { ok: false, reason: "short-task" };
|
|
197
|
+
|
|
198
|
+
const env = opts.env ?? process.env;
|
|
199
|
+
const resolve = opts._resolveCredentials ?? resolveCredentials;
|
|
200
|
+
const complete = opts._completeStructured ?? completeStructuredWithModel;
|
|
201
|
+
const callerTag = opts.callerTag ?? "task-title";
|
|
202
|
+
const plan = buildTaskTitlePlan(env);
|
|
203
|
+
|
|
204
|
+
// One lookup per kind, on demand, so the codex OAuth probe (an HTTP call
|
|
205
|
+
// that can rotate a refresh token) runs only when its turn comes.
|
|
206
|
+
const credentialCache = new Map<CredentialKind, ResolvedCredential | null>();
|
|
207
|
+
const credentialFor = async (kind: CredentialKind): Promise<ResolvedCredential | null> => {
|
|
208
|
+
const cached = credentialCache.get(kind);
|
|
209
|
+
if (cached !== undefined) return cached;
|
|
210
|
+
const [cred] = await resolve({
|
|
211
|
+
env,
|
|
212
|
+
apiUrl: opts.apiUrl,
|
|
213
|
+
apiKey: opts.apiKey,
|
|
214
|
+
callerTag,
|
|
215
|
+
kinds: [kind],
|
|
216
|
+
limit: 1,
|
|
217
|
+
});
|
|
218
|
+
credentialCache.set(kind, cred ?? null);
|
|
219
|
+
return cred ?? null;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
let sawCredential = false;
|
|
223
|
+
for (const group of plan) {
|
|
224
|
+
if (opts.signal?.aborted) break;
|
|
225
|
+
const cred = await credentialFor(group.kind);
|
|
226
|
+
if (!cred) continue;
|
|
227
|
+
sawCredential = true;
|
|
228
|
+
for (const model of group.models) {
|
|
229
|
+
if (opts.signal?.aborted) break;
|
|
230
|
+
const title = await attemptTitle(complete, {
|
|
231
|
+
cred,
|
|
232
|
+
model,
|
|
233
|
+
text,
|
|
234
|
+
env,
|
|
235
|
+
callerTag,
|
|
236
|
+
signal: opts.signal,
|
|
237
|
+
});
|
|
238
|
+
if (title) return { ok: true, title, model, kind: cred.kind };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return { ok: false, reason: sawCredential ? "all-models-failed" : "no-credentials" };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
interface TitleAttempt {
|
|
245
|
+
cred: ResolvedCredential;
|
|
246
|
+
model: string;
|
|
247
|
+
text: string;
|
|
248
|
+
env: Record<string, string | undefined>;
|
|
249
|
+
callerTag: string;
|
|
250
|
+
signal?: AbortSignal;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** One model call. `completeStructuredWithModel` swallows provider errors; a throw here is still just a miss. */
|
|
254
|
+
async function attemptTitle(
|
|
255
|
+
complete: typeof completeStructuredWithModel,
|
|
256
|
+
attempt: TitleAttempt,
|
|
257
|
+
): Promise<string> {
|
|
258
|
+
try {
|
|
259
|
+
const result = await complete({
|
|
260
|
+
zodSchema: TaskTitleZodSchema,
|
|
261
|
+
toolSchema: TaskTitleToolSchema,
|
|
262
|
+
toolName: TASK_TITLE_TOOL_NAME,
|
|
263
|
+
toolDescription: "Record the title for this task.",
|
|
264
|
+
systemPrompt: TASK_TITLE_SYSTEM_PROMPT,
|
|
265
|
+
userPrompt: `Task request:\n\n${attempt.text}`,
|
|
266
|
+
env: attempt.env,
|
|
267
|
+
retries: 1,
|
|
268
|
+
signal: attempt.signal,
|
|
269
|
+
callerTag: `${attempt.callerTag}:${attempt.model}`,
|
|
270
|
+
_credentialOverride: { ...attempt.cred, modelDefault: attempt.model },
|
|
271
|
+
});
|
|
272
|
+
return result ? normalizeTaskTitle(result.data.title) : "";
|
|
273
|
+
} catch (error) {
|
|
274
|
+
// Provider errors can echo request fragments; scrub before logging.
|
|
275
|
+
console.error(
|
|
276
|
+
scrubSecrets(
|
|
277
|
+
`[task-title] ${attempt.model} threw: ${error instanceof Error ? error.message : String(error)}`,
|
|
278
|
+
),
|
|
279
|
+
);
|
|
280
|
+
return "";
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
29
29
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
30
|
+
import type { IncomingHttpHeaders } from "node:http";
|
|
30
31
|
import path from "node:path";
|
|
32
|
+
import { isDevOrTestNodeEnv } from "./node-env";
|
|
31
33
|
|
|
32
34
|
const ENV_SECRET = "PAGE_SESSION_SECRET";
|
|
33
35
|
const ENV_SECRET_FILE = "PAGE_SESSION_SECRET_FILE";
|
|
@@ -336,6 +338,41 @@ export async function extractAndVerifyCookie(
|
|
|
336
338
|
*/
|
|
337
339
|
const PAGE_SESSION_TTL_SECONDS = 3600;
|
|
338
340
|
|
|
341
|
+
const LOCAL_DEV_HOSTNAMES = new Set(["localhost", "127.0.0.1"]);
|
|
342
|
+
|
|
343
|
+
/** True only for a parsed `http:` URL whose hostname is exactly a loopback dev name. */
|
|
344
|
+
function isLocalHttpUrl(value: string): boolean {
|
|
345
|
+
try {
|
|
346
|
+
const url = new URL(value);
|
|
347
|
+
return url.protocol === "http:" && LOCAL_DEV_HOSTNAMES.has(url.hostname);
|
|
348
|
+
} catch {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Decide the `dev` flag for `issuePageSessionCookie`. Shared by the launch
|
|
355
|
+
* endpoint (`src/http/pages.ts`) and the password flow on `/p/:id`
|
|
356
|
+
* (`src/http/pages-public.ts`) so both issue the same cookie.
|
|
357
|
+
*
|
|
358
|
+
* Fails closed: only an explicit development or test NODE_ENV can soften the
|
|
359
|
+
* cookie. Deployed images leave NODE_ENV unset and get `SameSite=None; Secure`.
|
|
360
|
+
* In dev, the request must come from `http://localhost` or `http://127.0.0.1`
|
|
361
|
+
* (parsed, so `http://localhost.evil.example` does not match). Without an
|
|
362
|
+
* Origin, the Host header decides; a missing Host counts as local.
|
|
363
|
+
* Anything else, including portless `https://*.localhost`, keeps
|
|
364
|
+
* `SameSite=None; Secure` so the cookie travels on cross-site fetches.
|
|
365
|
+
* Chrome treats localhost as a secure context, so Secure also works over http.
|
|
366
|
+
*/
|
|
367
|
+
export function isLocalDevPageRequest(req: { headers: IncomingHttpHeaders }): boolean {
|
|
368
|
+
if (!isDevOrTestNodeEnv()) return false;
|
|
369
|
+
const origin = req.headers.origin ?? "";
|
|
370
|
+
if (origin !== "") return isLocalHttpUrl(origin);
|
|
371
|
+
const host = req.headers.host;
|
|
372
|
+
if (!host) return true;
|
|
373
|
+
return isLocalHttpUrl(`http://${host}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
339
376
|
/**
|
|
340
377
|
* Mint a signed page-session token + build the `Set-Cookie` header value for
|
|
341
378
|
* `pageId`. Shared by the bearer-authed launch endpoint (`src/http/pages.ts`)
|