@desplega.ai/agent-swarm 1.145.0 → 1.147.0
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 +2 -1
- package/dist/{acp-adapter-580t41c0.js → acp-adapter-9n319wqc.js} +61 -6
- package/dist/{actions-m662tjng.js → actions-2vxqvpr9.js} +9 -8
- package/dist/{app-zb3rndm8.js → app-w066xfy0.js} +6 -5
- package/dist/assistant-kt31dj2h.js +236 -0
- package/dist/{boot-reembed-072x00vn.js → boot-reembed-az5rassp.js} +7 -6
- package/dist/{boot-reembed-65zz2smt.js → boot-reembed-j3mm3rfz.js} +8 -7
- package/dist/{boot-scrub-logs-yz0ams04.js → boot-scrub-logs-b6h54817.js} +4 -4
- package/dist/{claude-adapter-paj270y2.js → claude-adapter-w1z04ann.js} +8 -6
- package/dist/{claude-managed-setup-w619jncn.js → claude-managed-setup-f5pmsyrp.js} +24 -3
- package/dist/{claude-sdk-session-swt80j9z.js → claude-sdk-session-f0cc1kec.js} +8 -6
- package/dist/{cli-dt553ktb.js → cli-0b6y7kxm.js} +1 -1
- package/dist/{cli-bxdhw615.js → cli-12fz972k.js} +4 -4
- package/dist/{cli-s2f20r5w.js → cli-1th1728g.js} +2 -2
- package/dist/{cli-rs6ec3b9.js → cli-3tgwnf43.js} +3 -3
- package/dist/{cli-gxnzc6wh.js → cli-4nr988h6.js} +1 -1
- package/dist/{cli-axs8e6d4.js → cli-53s590z8.js} +1 -1
- package/dist/{cli-ygbjp3z8.js → cli-54wwve05.js} +5 -4
- package/dist/{cli-v0asr5yv.js → cli-719k9j2c.js} +2 -2
- package/dist/{cli-anktweqr.js → cli-89xfd7f9.js} +21 -47
- package/dist/{cli-q34a0e10.js → cli-8v0g1yc8.js} +1 -1
- package/dist/cli-9d8ntetj.js +42 -0
- package/dist/{cli-z9fprcrf.js → cli-atcve9y8.js} +881 -738
- package/dist/{cli-jmb7g47h.js → cli-b5bq9crk.js} +14 -31
- package/dist/cli-d8brzxjb.js +190 -0
- package/dist/{cli-gzky0gkr.js → cli-de14znh8.js} +2 -2
- package/dist/{cli-xytwff7f.js → cli-dttph1bw.js} +2 -2
- package/dist/cli-ep19k3mb.js +19 -0
- package/dist/{cli-m1xa8be7.js → cli-f146mn5s.js} +7 -50
- package/dist/{cli-1fj0chdk.js → cli-f7dsy61y.js} +5 -3
- package/dist/{cli-f567d5c1.js → cli-ftht3zzj.js} +2782 -2589
- package/dist/{cli-a0g1gbt5.js → cli-hbv0kq6w.js} +2 -2
- package/dist/{cli-fhsz3twr.js → cli-jsp14b6c.js} +171 -1
- package/dist/{cli-7fnkj1hj.js → cli-kpv03zhj.js} +21 -17
- package/dist/{cli-m4j6trn4.js → cli-m8yrsg97.js} +2 -2
- package/dist/cli-m9sxbkhm.js +779 -0
- package/dist/cli-mmemxxdc.js +823 -0
- package/dist/{cli-tqtvj5pt.js → cli-n8508nre.js} +1 -1
- package/dist/{cli-z96t0thj.js → cli-p1d5b073.js} +37 -2
- package/dist/{cli-gh8y8f36.js → cli-rw51bq3j.js} +13 -10
- package/dist/{cli-59ej1c07.js → cli-smby5m17.js} +7 -6
- package/dist/{cli-65vqzz3v.js → cli-tzvk9haz.js} +94 -372
- package/dist/{cli-dc9k2ba9.js → cli-w9kb3bk1.js} +1 -1
- package/dist/{cli-kpev657q.js → cli-y6b6fb76.js} +25 -3
- package/dist/{cli-123q2get.js → cli-yz3djrm0.js} +9 -9
- package/dist/{cli-wvp6bvy4.js → cli-zhjmqxzh.js} +1 -1
- package/dist/cli.js +13 -13
- package/dist/{codex-adapter-k98vaw2r.js → codex-adapter-zw4dwsx7.js} +2 -2
- package/dist/{codex-hook-9pf19cks.js → codex-hook-bw8p5nh4.js} +2 -2
- package/dist/{codex-session-runner-tngtphx7.js → codex-session-runner-k981s65v.js} +2 -2
- package/dist/{commands-y8nbr3fv.js → commands-sykrxx7e.js} +4 -4
- package/dist/{db-30p4h2t1.js → db-sfbsc8wt.js} +6 -4
- package/dist/handlers-0aspcejm.js +603 -0
- package/dist/{hook-dcw4jfbj.js → hook-4nvqj2sb.js} +7 -5
- package/dist/{hook-21jexvfx.js → hook-hxccs7m5.js} +24 -49
- package/dist/{http-r4kq5g8d.js → http-y06z517d.js} +230 -94
- package/dist/{index-q3qja6ta.js → index-hssww7kj.js} +15 -14
- package/dist/{index-gay66b9w.js → index-k8znfx0z.js} +13 -12
- package/dist/{index-53zyht4z.js → index-tce1rss4.js} +371 -110
- package/dist/{index-nqvf26za.js → index-x5j894zf.js} +14 -13
- package/dist/{keepalive-xs8fa55y.js → keepalive-w733ax66.js} +6 -6
- package/dist/{lead-d9m2t4s0.js → lead-75bjqa9d.js} +29 -27
- package/dist/{maintenance-3zh82g6z.js → maintenance-w4f1zjck.js} +8 -7
- package/dist/{oauth-refresh-sweep-03snsye5.js → oauth-refresh-sweep-wdetky26.js} +5 -5
- package/dist/{onboard-0cg1925x.js → onboard-bb5net8s.js} +2 -2
- package/dist/{otel-impl-0ab95z27.js → otel-impl-0w7c14ap.js} +1 -1
- package/dist/{pi-mono-adapter-dtkz4e7j.js → pi-mono-adapter-2has4gnp.js} +12 -2
- package/dist/{pricing-refresh-e5jhzz4j.js → pricing-refresh-9nzsrgcq.js} +6 -6
- package/dist/{rbac-roles-p1mf40z3.js → rbac-roles-r3hqjp09.js} +5 -5
- package/dist/{rbac-roles-2w3cmrkc.js → rbac-roles-z1jrffbt.js} +4 -4
- package/dist/{registry-9kdck5ye.js → registry-v5wytg0s.js} +1 -2
- package/dist/{render-v2-xhawry3k.js → render-v2-vbr89fnb.js} +6 -5
- package/dist/{seed-pricing-t223sw91.js → seed-pricing-g62hy1pk.js} +7 -5
- package/dist/{setup-zcs0rce7.js → setup-gnfqnptp.js} +2 -2
- package/dist/{worker-qnp2ka62.js → worker-kznxp4qm.js} +29 -27
- package/openapi.json +263 -12
- package/package.json +1 -1
- package/plugin/commands/start-worker.md +4 -9
- package/plugin/pi-skills/start-worker/SKILL.md +4 -9
- package/src/be/db/agents.ts +556 -0
- package/src/be/db/context-versions.ts +117 -0
- package/src/be/db/runtime.ts +406 -0
- package/src/be/db/tasks/read.ts +1167 -0
- package/src/be/db/tasks/write.ts +953 -0
- package/src/be/db.ts +584 -3429
- package/src/be/migrations/147_scheduled_tasks_parent_task.sql +7 -0
- package/src/be/migrations/148_pages_svg.sql +123 -0
- package/src/be/migrations/149_session_tokens.sql +24 -0
- package/src/be/scripts/typecheck.ts +1 -0
- package/src/be/seed-pricing.ts +13 -8
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
- package/src/be/seed-scripts/catalog/star-history-refresh.ts +101 -0
- package/src/be/seed-scripts/catalog/star-history-renderer.ts +119 -0
- package/src/be/seed-scripts/index.ts +17 -0
- package/src/be/seed-skills/index.ts +14 -0
- package/src/be/swarm-config-guard.ts +5 -2
- package/src/be/task-attachment-store.ts +70 -0
- package/src/be/users.ts +77 -0
- package/src/commands/attachments-section.ts +37 -0
- package/src/commands/claude-managed-setup.ts +45 -3
- package/src/commands/claude-md-session.ts +408 -0
- package/src/commands/context-preamble.ts +15 -3
- package/src/commands/profile-sync.ts +205 -5
- package/src/commands/provider-credentials.ts +60 -27
- package/src/commands/runner.ts +63 -58
- package/src/commands/templates.ts +25 -2
- package/src/hooks/hook.ts +26 -67
- package/src/http/agents.ts +40 -1
- package/src/http/auth.ts +13 -1
- package/src/http/fs.ts +21 -51
- package/src/http/index.ts +2 -0
- package/src/http/mcp.ts +32 -0
- package/src/http/pages-public.ts +20 -4
- package/src/http/pages.ts +11 -3
- package/src/http/poll.ts +4 -0
- package/src/http/repos.ts +3 -3
- package/src/http/sessions.ts +82 -0
- package/src/http/tasks.ts +10 -2
- package/src/http/utils.ts +79 -1
- package/src/prompts/session-templates.ts +12 -8
- package/src/providers/acp-adapter.ts +62 -8
- package/src/providers/pi-mono-adapter.ts +23 -6
- package/src/providers/pricing-sources.md +11 -0
- package/src/scheduler/scheduler.test.ts +49 -2
- package/src/scheduler/scheduler.ts +4 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +8 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +8 -0
- package/src/server.ts +2 -0
- package/src/slack/assistant.ts +87 -46
- package/src/slack/handlers.test.ts +1 -3
- package/src/slack/handlers.ts +102 -87
- package/src/slack/inbound-files.ts +466 -0
- package/src/slack/templates.ts +14 -0
- package/src/tasks/task-terminal-effects.ts +158 -0
- package/src/tests/acp-adapter.test.ts +369 -117
- package/src/tests/budget-claim-gate.test.ts +1 -0
- package/src/tests/build-pi-skills.test.ts +5 -7
- package/src/tests/claude-managed-setup.test.ts +52 -4
- package/src/tests/claude-md-session.test.ts +603 -0
- package/src/tests/codex-skill-resolver.test.ts +19 -0
- package/src/tests/context-versioning.test.ts +107 -0
- package/src/tests/cors-allowlist.test.ts +309 -0
- package/src/tests/credential-check.test.ts +157 -14
- package/src/tests/db-agents.test.ts +91 -0
- package/src/tests/db-context-versions.test.ts +37 -0
- package/src/tests/db-extraction-contract.test.ts +45 -0
- package/src/tests/db-runtime.test.ts +70 -0
- package/src/tests/db-task-read-contract.test.ts +624 -0
- package/src/tests/db-task-write-contract.test.ts +653 -0
- package/src/tests/db-tasks-read.test.ts +183 -0
- package/src/tests/db-tasks-write.test.ts +230 -0
- package/src/tests/defer-task.test.ts +389 -0
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/e2e-visuals-stage.test.ts +151 -0
- package/src/tests/file-lock.test.ts +144 -0
- package/src/tests/fixtures/file-lock-holder.ts +13 -0
- package/src/tests/fixtures/hold-file-lock.ts +25 -0
- package/src/tests/heartbeat-expiry.test.ts +188 -0
- package/src/tests/http-api-integration.test.ts +78 -1
- package/src/tests/pages-public-html.test.ts +43 -0
- package/src/tests/pages-svg-migration.test.ts +42 -0
- package/src/tests/preload.ts +4 -1
- package/src/tests/profile-sync-warning.test.ts +16 -0
- package/src/tests/profile-sync.test.ts +126 -0
- package/src/tests/prompt-template-session.test.ts +20 -4
- package/src/tests/provider-registration.test.ts +93 -0
- package/src/tests/rbac-e2e-helpers.test.ts +53 -0
- package/src/tests/rbac-e2e-helpers.ts +19 -2
- package/src/tests/reload-config.test.ts +26 -0
- package/src/tests/routing-affinity-inventory.test.ts +1 -1
- package/src/tests/runner-attachments-section.test.ts +22 -2
- package/src/tests/runner-context-preamble.test.ts +30 -0
- package/src/tests/runner-deployment-resume.test.ts +107 -0
- package/src/tests/runner-repo-autostash.test.ts +12 -0
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/seed-scripts.test.ts +2 -2
- package/src/tests/session-tokens.test.ts +279 -0
- package/src/tests/slack-file-tools-attachments.test.ts +408 -0
- package/src/tests/slack-inbound-files.test.ts +847 -0
- package/src/tests/star-history-refresh.test.ts +102 -0
- package/src/tests/swarm-config-reserved-keys.test.ts +21 -0
- package/src/tests/swarm-repos.test.ts +20 -0
- package/src/tests/tool-annotations.test.ts +4 -3
- package/src/tests/ui-logs-parser.test.ts +176 -0
- package/src/tools/create-page.ts +4 -2
- package/src/tools/defer-task.ts +239 -0
- package/src/tools/repos/update-repo.ts +2 -2
- package/src/tools/schedules/create-schedule.ts +1 -0
- package/src/tools/schedules/list-schedules.ts +1 -0
- package/src/tools/schedules/patch-schedule.ts +1 -0
- package/src/tools/schedules/update-schedule.ts +1 -0
- package/src/tools/slack-download-file.ts +60 -29
- package/src/tools/slack-read.ts +73 -32
- package/src/tools/store-progress.ts +16 -146
- package/src/tools/tool-config.ts +1 -0
- package/src/tools/utils.ts +21 -0
- package/src/types.ts +37 -4
- package/src/utils/acp-session-token.ts +56 -0
- package/src/utils/file-lock.ts +111 -0
- package/src/utils/heartbeat-expiry.ts +78 -0
- package/src/utils/request-auth-context.ts +2 -0
- package/src/utils/task-attachment-links.ts +22 -0
- package/templates/skills/code-quality/content.md +8 -5
- package/templates/skills/download-task-attachment/content.md +2 -1
- package/{plugin/pi-skills → templates/skills}/review-offered-task/SKILL.md +5 -5
- package/templates/skills/review-offered-task/config.json +14 -0
- package/{plugin/commands/review-offered-task.md → templates/skills/review-offered-task/content.md} +3 -8
- package/templates/skills/scheduling/content.md +4 -0
- package/templates/skills/slack-interaction/content.md +2 -2
- package/templates/skills/work-on-task/SKILL.md +16 -0
- package/templates/skills/work-on-task/config.json +14 -0
- package/templates/skills/work-on-task/content.md +11 -0
- package/dist/assistant-0qt42nzg.js +0 -212
- package/dist/cli-fs1kkqgr.js +0 -317
- package/dist/cli-jnf5zghe.js +0 -269
- package/dist/cli-n1q8q14d.js +0 -177
- package/dist/handlers-yfp1qn81.js +0 -592
- package/plugin/commands/work-on-task.md +0 -39
- package/plugin/pi-skills/work-on-task/SKILL.md +0 -39
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
initDb,
|
|
11
11
|
updateAgentProfile,
|
|
12
12
|
} from "../be/db";
|
|
13
|
+
import type { ProfileSyncConflict } from "../types";
|
|
14
|
+
import { IDENTITY_FIELD_BUDGETS } from "../utils/identity-field-budget";
|
|
13
15
|
|
|
14
16
|
const TEST_DB_PATH = "./test-context-versioning.sqlite";
|
|
15
17
|
|
|
@@ -411,6 +413,111 @@ describe("Context Versioning", () => {
|
|
|
411
413
|
});
|
|
412
414
|
});
|
|
413
415
|
|
|
416
|
+
// ============================================================================
|
|
417
|
+
// Compare-and-set (expectedHashes)
|
|
418
|
+
// ============================================================================
|
|
419
|
+
|
|
420
|
+
describe("compare-and-set via expectedHashes", () => {
|
|
421
|
+
const casAgentId = "cccc0000-0000-4000-8000-000000000004";
|
|
422
|
+
const v1 = "# CLAUDE.md\n\nversion one";
|
|
423
|
+
const v2 = "# CLAUDE.md\n\nversion two — the Lead added a rule";
|
|
424
|
+
const v3 = "# CLAUDE.md\n\nversion three — edited in a session";
|
|
425
|
+
|
|
426
|
+
beforeAll(async () => {
|
|
427
|
+
await createAgent({ id: casAgentId, name: "CAS Agent", isLead: false, status: "idle" });
|
|
428
|
+
await updateAgentProfile(casAgentId, { claudeMd: v1 }, { changeSource: "self_edit" });
|
|
429
|
+
await updateAgentProfile(casAgentId, { claudeMd: v2 }, { changeSource: "self_edit" });
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("drops a copy based on a superseded version and reports the conflict", async () => {
|
|
433
|
+
// The 2026-09-11 incident: a session materialized v1, the Lead moved the
|
|
434
|
+
// DB to v2 meanwhile, and the session's end-of-run sync carried v1 back.
|
|
435
|
+
const conflicts: ProfileSyncConflict[] = [];
|
|
436
|
+
const agent = await updateAgentProfile(
|
|
437
|
+
casAgentId,
|
|
438
|
+
{ claudeMd: v3 },
|
|
439
|
+
{ changeSource: "session_sync" },
|
|
440
|
+
{ expectedHashes: { claudeMd: sha256(v1) }, onConflict: (c) => conflicts.push(c) },
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
expect(agent!.claudeMd).toBe(v2);
|
|
444
|
+
expect((await getLatestContextVersion(casAgentId, "claudeMd"))!.version).toBe(2);
|
|
445
|
+
expect(conflicts).toEqual([
|
|
446
|
+
{ field: "claudeMd", expectedHash: sha256(v1), currentHash: sha256(v2) },
|
|
447
|
+
]);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test("applies a deliberate session_sync revert to an earlier version", async () => {
|
|
451
|
+
// The session was based on the current value (v2) and restored v1 on
|
|
452
|
+
// purpose. Its hash is in the history, and that must not matter.
|
|
453
|
+
const agent = await updateAgentProfile(
|
|
454
|
+
casAgentId,
|
|
455
|
+
{ claudeMd: v1 },
|
|
456
|
+
{ changeSource: "session_sync" },
|
|
457
|
+
{ expectedHashes: { claudeMd: sha256(v2) } },
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
expect(agent!.claudeMd).toBe(v1);
|
|
461
|
+
const latest = await getLatestContextVersion(casAgentId, "claudeMd");
|
|
462
|
+
expect(latest!.version).toBe(3);
|
|
463
|
+
expect(latest!.content).toBe(v1);
|
|
464
|
+
expect(latest!.changeSource).toBe("session_sync");
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test("the drop is per field: other fields in the same update land", async () => {
|
|
468
|
+
const soul = "s".repeat(600);
|
|
469
|
+
const agent = await updateAgentProfile(
|
|
470
|
+
casAgentId,
|
|
471
|
+
{ claudeMd: v3, soulMd: soul },
|
|
472
|
+
{ changeSource: "session_sync" },
|
|
473
|
+
{ expectedHashes: { claudeMd: sha256(v2) } }, // stale: the DB is at v1 now
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
expect(agent!.claudeMd).toBe(v1);
|
|
477
|
+
expect(agent!.soulMd).toBe(soul);
|
|
478
|
+
expect((await getLatestContextVersion(casAgentId, "claudeMd"))!.version).toBe(3);
|
|
479
|
+
expect((await getLatestContextVersion(casAgentId, "soulMd"))!.version).toBe(1);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("without expectedHashes the write stays unconditional", async () => {
|
|
483
|
+
const agent = await updateAgentProfile(
|
|
484
|
+
casAgentId,
|
|
485
|
+
{ claudeMd: v2 },
|
|
486
|
+
{ changeSource: "session_sync" },
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
expect(agent!.claudeMd).toBe(v2);
|
|
490
|
+
expect((await getLatestContextVersion(casAgentId, "claudeMd"))!.version).toBe(4);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test("a stale copy over the budget is dropped, not rejected", async () => {
|
|
494
|
+
// The profile has since been shortened under the budget. A stale copy of
|
|
495
|
+
// the old, oversized value must be dropped by the compare-and-set — not
|
|
496
|
+
// throw IdentityFieldBudgetError and roll back the other fields too.
|
|
497
|
+
const longAgentId = "cccc0000-0000-4000-8000-000000000005";
|
|
498
|
+
const oversized = "x".repeat(IDENTITY_FIELD_BUDGETS.claudeMd + 500);
|
|
499
|
+
const shortened = "# CLAUDE.md\n\nshortened below the budget";
|
|
500
|
+
await createAgent({
|
|
501
|
+
id: longAgentId,
|
|
502
|
+
name: "Long CAS Agent",
|
|
503
|
+
isLead: false,
|
|
504
|
+
status: "idle",
|
|
505
|
+
});
|
|
506
|
+
await updateAgentProfile(longAgentId, { claudeMd: shortened }, { changeSource: "self_edit" });
|
|
507
|
+
|
|
508
|
+
const soul = "t".repeat(600);
|
|
509
|
+
const agent = await updateAgentProfile(
|
|
510
|
+
longAgentId,
|
|
511
|
+
{ claudeMd: oversized, soulMd: soul },
|
|
512
|
+
{ changeSource: "session_sync" },
|
|
513
|
+
{ expectedHashes: { claudeMd: sha256(oversized) } },
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
expect(agent!.claudeMd).toBe(shortened);
|
|
517
|
+
expect(agent!.soulMd).toBe(soul);
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
414
521
|
// ============================================================================
|
|
415
522
|
// Content hash consistency
|
|
416
523
|
// ============================================================================
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterAll,
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeAll,
|
|
5
|
+
beforeEach,
|
|
6
|
+
describe,
|
|
7
|
+
expect,
|
|
8
|
+
spyOn,
|
|
9
|
+
test,
|
|
10
|
+
} from "bun:test";
|
|
11
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
12
|
+
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { closeDb, initDb } from "../be/db";
|
|
16
|
+
import { handlePageProxy } from "../http/page-proxy";
|
|
17
|
+
import { handlePages } from "../http/pages";
|
|
18
|
+
import { handlePagesPublic } from "../http/pages-public";
|
|
19
|
+
import {
|
|
20
|
+
isOriginAllowedForCredentials,
|
|
21
|
+
setCorsHeaders,
|
|
22
|
+
warnIfCorsAllowsAnyOrigin,
|
|
23
|
+
} from "../http/utils";
|
|
24
|
+
import { listenOnFreePort } from "./test-net";
|
|
25
|
+
|
|
26
|
+
const ENV_KEY = "CORS_ALLOWED_ORIGINS";
|
|
27
|
+
const originalEnv = process.env[ENV_KEY];
|
|
28
|
+
const originalOptOut = process.env.CORS_ALLOW_ANY_ORIGIN;
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
delete process.env.CORS_ALLOW_ANY_ORIGIN;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
if (originalOptOut === undefined) delete process.env.CORS_ALLOW_ANY_ORIGIN;
|
|
35
|
+
else process.env.CORS_ALLOW_ANY_ORIGIN = originalOptOut;
|
|
36
|
+
if (originalEnv === undefined) delete process.env[ENV_KEY];
|
|
37
|
+
else process.env[ENV_KEY] = originalEnv;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function fakeReq(origin: string | undefined): IncomingMessage {
|
|
41
|
+
return { headers: origin ? { origin } : {} } as unknown as IncomingMessage;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function fakeRes(): { res: ServerResponse; headers: Map<string, string> } {
|
|
45
|
+
const headers = new Map<string, string>();
|
|
46
|
+
const res = {
|
|
47
|
+
setHeader: (name: string, value: string) => {
|
|
48
|
+
headers.set(name.toLowerCase(), value);
|
|
49
|
+
},
|
|
50
|
+
} as unknown as ServerResponse;
|
|
51
|
+
return { res, headers };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("isOriginAllowedForCredentials", () => {
|
|
55
|
+
test("denies arbitrary origins when CORS_ALLOWED_ORIGINS is unset", () => {
|
|
56
|
+
delete process.env[ENV_KEY];
|
|
57
|
+
expect(isOriginAllowedForCredentials("https://evil.example")).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("denies arbitrary origins when CORS_ALLOWED_ORIGINS is blank", () => {
|
|
61
|
+
process.env[ENV_KEY] = " ";
|
|
62
|
+
expect(isOriginAllowedForCredentials("https://evil.example")).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("allows only listed origins when set", () => {
|
|
66
|
+
process.env[ENV_KEY] = "https://app.example.com, https://dashboard.example.com";
|
|
67
|
+
expect(isOriginAllowedForCredentials("https://app.example.com")).toBe(true);
|
|
68
|
+
expect(isOriginAllowedForCredentials("https://dashboard.example.com")).toBe(true);
|
|
69
|
+
expect(isOriginAllowedForCredentials("https://evil.example")).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test.each([
|
|
73
|
+
["matches a subdomain", "https://app.agent-swarm.dev", true],
|
|
74
|
+
["matches multiple subdomain labels", "https://a.b.agent-swarm.dev", true],
|
|
75
|
+
["rejects suffix confusion", "https://evil-agent-swarm.dev", false],
|
|
76
|
+
["rejects trailing-domain attacks", "https://app.agent-swarm.dev.evil.com", false],
|
|
77
|
+
["rejects scheme mismatch", "http://app.agent-swarm.dev", false],
|
|
78
|
+
["compares scheme exactly", "HTTPS://app.agent-swarm.dev", false],
|
|
79
|
+
["rejects the apex", "https://agent-swarm.dev", false],
|
|
80
|
+
["compares host case-insensitively", "https://APP.AGENT-SWARM.DEV", true],
|
|
81
|
+
["rejects an unlisted port", "https://app.agent-swarm.dev:8443", false],
|
|
82
|
+
["rejects empty labels", "https://.agent-swarm.dev", false],
|
|
83
|
+
["rejects credentials", "https://evil.com@app.agent-swarm.dev", false],
|
|
84
|
+
["rejects paths", "https://app.agent-swarm.dev/evil", false],
|
|
85
|
+
])("wildcard %s", (_name, origin, allowed) => {
|
|
86
|
+
process.env[ENV_KEY] = "https://*.agent-swarm.dev";
|
|
87
|
+
expect(isOriginAllowedForCredentials(origin)).toBe(allowed);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test.each([
|
|
91
|
+
"*",
|
|
92
|
+
"https://*",
|
|
93
|
+
"https://*.*.agent-swarm.dev",
|
|
94
|
+
"https://app*.agent-swarm.dev",
|
|
95
|
+
])("ignores invalid wildcard entry %s", (entry) => {
|
|
96
|
+
process.env[ENV_KEY] = entry;
|
|
97
|
+
expect(isOriginAllowedForCredentials("https://app.agent-swarm.dev")).toBe(false);
|
|
98
|
+
expect(isOriginAllowedForCredentials(entry)).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("wildcard supports uppercase suffixes and requires the configured port", () => {
|
|
102
|
+
process.env[ENV_KEY] = "https://*.AGENT-SWARM.DEV:8443";
|
|
103
|
+
expect(isOriginAllowedForCredentials("https://app.agent-swarm.dev:8443")).toBe(true);
|
|
104
|
+
expect(isOriginAllowedForCredentials("https://app.agent-swarm.dev")).toBe(false);
|
|
105
|
+
expect(isOriginAllowedForCredentials("https://app.agent-swarm.dev:443")).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("mixes wildcard and exact entries including a separately listed apex", () => {
|
|
109
|
+
process.env[ENV_KEY] =
|
|
110
|
+
"https://*.agent-swarm.dev,https://agent-swarm.dev,http://localhost:5274";
|
|
111
|
+
expect(isOriginAllowedForCredentials("https://app.agent-swarm.dev")).toBe(true);
|
|
112
|
+
expect(isOriginAllowedForCredentials("https://agent-swarm.dev")).toBe(true);
|
|
113
|
+
expect(isOriginAllowedForCredentials("http://localhost:5274")).toBe(true);
|
|
114
|
+
expect(isOriginAllowedForCredentials("http://localhost:5275")).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("is an exact match, not a suffix/subdomain match", () => {
|
|
118
|
+
process.env[ENV_KEY] = "https://app.example.com";
|
|
119
|
+
expect(isOriginAllowedForCredentials("https://evil.app.example.com")).toBe(false);
|
|
120
|
+
expect(isOriginAllowedForCredentials("https://app.example.com.evil.example")).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("setCorsHeaders", () => {
|
|
125
|
+
test("opt-out reflects arbitrary origins without granting credentials", () => {
|
|
126
|
+
const warn = spyOn(console, "warn").mockImplementation(() => {});
|
|
127
|
+
process.env.CORS_ALLOW_ANY_ORIGIN = "true";
|
|
128
|
+
process.env[ENV_KEY] = "https://trusted.example";
|
|
129
|
+
warnIfCorsAllowsAnyOrigin();
|
|
130
|
+
warnIfCorsAllowsAnyOrigin();
|
|
131
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
132
|
+
expect(warn.mock.calls[0]![0]).toContain("CORS_ALLOW_ANY_ORIGIN=true");
|
|
133
|
+
warn.mockRestore();
|
|
134
|
+
const { res, headers } = fakeRes();
|
|
135
|
+
setCorsHeaders(fakeReq("https://evil.example"), res);
|
|
136
|
+
expect(headers.get("access-control-allow-origin")).toBe("https://evil.example");
|
|
137
|
+
expect(headers.has("access-control-allow-credentials")).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("denies a non-allowlisted origin: no Allow-Origin, no Allow-Credentials", () => {
|
|
141
|
+
process.env[ENV_KEY] = "https://app.example.com";
|
|
142
|
+
const { res, headers } = fakeRes();
|
|
143
|
+
setCorsHeaders(fakeReq("https://evil.example"), res);
|
|
144
|
+
expect(headers.has("access-control-allow-origin")).toBe(false);
|
|
145
|
+
expect(headers.has("access-control-allow-credentials")).toBe(false);
|
|
146
|
+
// Vary: Origin still set so caches don't serve a denied response to an allowed origin.
|
|
147
|
+
expect(headers.get("vary")).toBe("Origin");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("allows a listed origin with credentials", () => {
|
|
151
|
+
process.env[ENV_KEY] = "https://app.example.com";
|
|
152
|
+
const { res, headers } = fakeRes();
|
|
153
|
+
setCorsHeaders(fakeReq("https://app.example.com"), res);
|
|
154
|
+
expect(headers.get("access-control-allow-origin")).toBe("https://app.example.com");
|
|
155
|
+
expect(headers.get("access-control-allow-credentials")).toBe("true");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("no-Origin requests still get wildcard regardless of allowlist", () => {
|
|
159
|
+
process.env[ENV_KEY] = "https://app.example.com";
|
|
160
|
+
const { res, headers } = fakeRes();
|
|
161
|
+
setCorsHeaders(fakeReq(undefined), res);
|
|
162
|
+
expect(headers.get("access-control-allow-origin")).toBe("*");
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("secure defaults and reload", () => {
|
|
167
|
+
test.each([undefined, "", " "])("uses defaults for %j", (raw) => {
|
|
168
|
+
if (raw === undefined) delete process.env[ENV_KEY];
|
|
169
|
+
else process.env[ENV_KEY] = raw;
|
|
170
|
+
for (const origin of [
|
|
171
|
+
"https://app.agent-swarm.dev",
|
|
172
|
+
"https://a.b.agent-swarm.cloud",
|
|
173
|
+
"http://localhost:5274",
|
|
174
|
+
"http://127.0.0.1:5274",
|
|
175
|
+
"http://[::1]:5274",
|
|
176
|
+
"https://ui.swarm.localhost:1355",
|
|
177
|
+
]) {
|
|
178
|
+
expect(isOriginAllowedForCredentials(origin)).toBe(true);
|
|
179
|
+
}
|
|
180
|
+
for (const origin of [
|
|
181
|
+
"https://evil.example",
|
|
182
|
+
"null",
|
|
183
|
+
"https://agent-swarm.dev",
|
|
184
|
+
"http://localhost:9999",
|
|
185
|
+
]) {
|
|
186
|
+
const { res, headers } = fakeRes();
|
|
187
|
+
setCorsHeaders(fakeReq(origin), res);
|
|
188
|
+
expect(headers.has("access-control-allow-origin")).toBe(false);
|
|
189
|
+
expect(headers.has("access-control-allow-credentials")).toBe(false);
|
|
190
|
+
expect(headers.get("vary")).toBe("Origin");
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
test("custom lists replace defaults and opt-out changes are read dynamically", () => {
|
|
194
|
+
process.env[ENV_KEY] = "https://trusted.example";
|
|
195
|
+
expect(isOriginAllowedForCredentials("https://app.agent-swarm.dev")).toBe(false);
|
|
196
|
+
for (const value of ["true", "1", " TRUE "]) {
|
|
197
|
+
process.env.CORS_ALLOW_ANY_ORIGIN = value;
|
|
198
|
+
expect(isOriginAllowedForCredentials("https://evil.example")).toBe(false);
|
|
199
|
+
const { res, headers } = fakeRes();
|
|
200
|
+
setCorsHeaders(fakeReq("https://evil.example"), res);
|
|
201
|
+
expect(headers.get("access-control-allow-origin")).toBe("https://evil.example");
|
|
202
|
+
expect(headers.has("access-control-allow-credentials")).toBe(false);
|
|
203
|
+
}
|
|
204
|
+
for (const value of ["false", "0", "", "typo"]) {
|
|
205
|
+
process.env.CORS_ALLOW_ANY_ORIGIN = value;
|
|
206
|
+
expect(isOriginAllowedForCredentials("https://evil.example")).toBe(false);
|
|
207
|
+
}
|
|
208
|
+
process.env[ENV_KEY] = "https://evil.example";
|
|
209
|
+
expect(isOriginAllowedForCredentials("https://evil.example")).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("page-session CORS over HTTP", () => {
|
|
214
|
+
const originalPort = process.env.PORT;
|
|
215
|
+
const dir = mkdtempSync(join(tmpdir(), "cors-pages-"));
|
|
216
|
+
const server = createServer(async (req, res) => {
|
|
217
|
+
setCorsHeaders(req, res);
|
|
218
|
+
const url = new URL(req.url!, "http://localhost");
|
|
219
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
220
|
+
if (req.method === "OPTIONS") {
|
|
221
|
+
res.writeHead(204).end();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (await handlePageProxy(req, res)) return;
|
|
225
|
+
if (
|
|
226
|
+
await handlePages(
|
|
227
|
+
req,
|
|
228
|
+
res,
|
|
229
|
+
segments,
|
|
230
|
+
url.searchParams,
|
|
231
|
+
"38d36438-58a0-45b5-8602-a5d52b07c2f1",
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
return;
|
|
235
|
+
if (await handlePagesPublic(req, res, segments, url.searchParams)) return;
|
|
236
|
+
res.writeHead(404).end();
|
|
237
|
+
});
|
|
238
|
+
let base: string;
|
|
239
|
+
let pageId: string;
|
|
240
|
+
beforeAll(async () => {
|
|
241
|
+
initDb(join(dir, "test.sqlite"));
|
|
242
|
+
const port = await listenOnFreePort(server);
|
|
243
|
+
process.env.PORT = String(port);
|
|
244
|
+
base = `http://localhost:${port}`;
|
|
245
|
+
const response = await fetch(`${base}/api/pages`, {
|
|
246
|
+
method: "POST",
|
|
247
|
+
headers: { "Content-Type": "application/json" },
|
|
248
|
+
body: JSON.stringify({
|
|
249
|
+
slug: "cors-session",
|
|
250
|
+
title: "CORS",
|
|
251
|
+
contentType: "text/html",
|
|
252
|
+
authMode: "authed",
|
|
253
|
+
body: "<h1>Private</h1>",
|
|
254
|
+
}),
|
|
255
|
+
});
|
|
256
|
+
expect(response.status).toBe(201);
|
|
257
|
+
pageId = ((await response.json()) as { id: string }).id;
|
|
258
|
+
});
|
|
259
|
+
afterAll(async () => {
|
|
260
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
261
|
+
closeDb();
|
|
262
|
+
if (originalPort === undefined) delete process.env.PORT;
|
|
263
|
+
else process.env.PORT = originalPort;
|
|
264
|
+
rmSync(dir, { recursive: true, force: true });
|
|
265
|
+
});
|
|
266
|
+
test.each([
|
|
267
|
+
undefined,
|
|
268
|
+
"",
|
|
269
|
+
" ",
|
|
270
|
+
"bypass",
|
|
271
|
+
])("launch preflight, session cookie and page JSON use defaults for %j", async (raw) => {
|
|
272
|
+
const bypass = raw === "bypass";
|
|
273
|
+
if (bypass) process.env.CORS_ALLOW_ANY_ORIGIN = "true";
|
|
274
|
+
if (raw === undefined || bypass) delete process.env[ENV_KEY];
|
|
275
|
+
else process.env[ENV_KEY] = raw;
|
|
276
|
+
for (const origin of ["https://app.agent-swarm.dev", "https://evil.example"]) {
|
|
277
|
+
const allowed = origin.includes("agent-swarm.dev");
|
|
278
|
+
for (const method of ["OPTIONS", "POST"]) {
|
|
279
|
+
const response = await fetch(`${base}/api/pages/${pageId}/launch`, {
|
|
280
|
+
method,
|
|
281
|
+
headers: { Origin: origin },
|
|
282
|
+
});
|
|
283
|
+
expect(response.status).toBe(204);
|
|
284
|
+
expect(response.headers.get("access-control-allow-origin")).toBe(
|
|
285
|
+
allowed || bypass ? origin : null,
|
|
286
|
+
);
|
|
287
|
+
expect(response.headers.get("access-control-allow-credentials")).toBe(
|
|
288
|
+
allowed ? "true" : null,
|
|
289
|
+
);
|
|
290
|
+
if (method === "POST") {
|
|
291
|
+
const cookie = response.headers.get("set-cookie")!.split(";")[0]!;
|
|
292
|
+
for (const path of [`/p/${pageId}.json`, `/@swarm/api/pages/${pageId}`]) {
|
|
293
|
+
const page = await fetch(`${base}${path}`, {
|
|
294
|
+
headers: { Origin: origin, Cookie: cookie },
|
|
295
|
+
});
|
|
296
|
+
expect(page.status).toBe(200);
|
|
297
|
+
expect(page.headers.get("access-control-allow-origin")).toBe(
|
|
298
|
+
allowed || bypass ? origin : null,
|
|
299
|
+
);
|
|
300
|
+
expect(page.headers.get("access-control-allow-credentials")).toBe(
|
|
301
|
+
allowed ? "true" : null,
|
|
302
|
+
);
|
|
303
|
+
await page.text();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
});
|
|
@@ -2,8 +2,11 @@ import { describe, expect, mock, test } from "bun:test";
|
|
|
2
2
|
import {
|
|
3
3
|
buildCredStatusReport,
|
|
4
4
|
checkProviderCredentials,
|
|
5
|
+
isBedrockMode,
|
|
5
6
|
isCredCheckDisabled,
|
|
6
7
|
REQUIRED_CRED_VARS_BY_PROVIDER,
|
|
8
|
+
shouldRefreshBedrockStatus,
|
|
9
|
+
validateProviderCredentials,
|
|
7
10
|
} from "../commands/provider-credentials";
|
|
8
11
|
import { checkClaudeCredentials } from "../providers/claude-adapter";
|
|
9
12
|
import { checkClaudeManagedCredentials } from "../providers/claude-managed-adapter";
|
|
@@ -410,21 +413,69 @@ describe("checkPiMonoCredentials", () => {
|
|
|
410
413
|
expect(status.ready).toBe(false);
|
|
411
414
|
});
|
|
412
415
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const status = await checkPiMonoCredentials(env, {
|
|
416
|
+
// ─── BEDROCK_AUTH_MODE=bearer: explicit Bedrock API key ─────────────────────
|
|
417
|
+
|
|
418
|
+
test("BEDROCK_AUTH_MODE=bearer: missing AWS_BEARER_TOKEN_BEDROCK → not ready, no probe, no fall-through", async () => {
|
|
419
|
+
let probed = false;
|
|
420
|
+
// A standard key must not satisfy the bearer mode: it does not apply to Bedrock.
|
|
421
|
+
const env = { BEDROCK_AUTH_MODE: "bearer", AWS_REGION: "us-east-1", ANTHROPIC_API_KEY: "x" };
|
|
422
|
+
const status = await checkPiMonoCredentials(env, {
|
|
423
|
+
homeDir: HOME,
|
|
424
|
+
fs: noFiles,
|
|
425
|
+
bedrockProbe: async () => {
|
|
426
|
+
probed = true;
|
|
427
|
+
},
|
|
428
|
+
});
|
|
420
429
|
expect(status.ready).toBe(false);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
430
|
+
expect(status.missing).toEqual(["AWS_BEARER_TOKEN_BEDROCK"]);
|
|
431
|
+
expect(status.hint).toContain("AWS_BEARER_TOKEN_BEDROCK");
|
|
432
|
+
expect(probed).toBe(false);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("BEDROCK_AUTH_MODE=bearer: token without AWS_REGION → not ready with the region hint", async () => {
|
|
436
|
+
const env = { BEDROCK_AUTH_MODE: "bearer", AWS_BEARER_TOKEN_BEDROCK: "bedrock-api-key" };
|
|
437
|
+
const status = await checkPiMonoCredentials(env, {
|
|
438
|
+
homeDir: HOME,
|
|
439
|
+
fs: noFiles,
|
|
440
|
+
bedrockProbe: bedrockProbeSuccess,
|
|
441
|
+
});
|
|
442
|
+
expect(status.ready).toBe(false);
|
|
443
|
+
expect(status.hint).toContain("AWS_REGION");
|
|
444
|
+
expect(status.bedrockRegion).toBe("");
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("BEDROCK_AUTH_MODE=bearer: token + region → probe runs, ready via env", async () => {
|
|
448
|
+
const env = {
|
|
449
|
+
BEDROCK_AUTH_MODE: "bearer",
|
|
450
|
+
AWS_BEARER_TOKEN_BEDROCK: "bedrock-api-key",
|
|
451
|
+
AWS_REGION: "eu-central-1",
|
|
452
|
+
};
|
|
453
|
+
const status = await checkPiMonoCredentials(env, {
|
|
454
|
+
homeDir: HOME,
|
|
455
|
+
fs: noFiles,
|
|
456
|
+
bedrockProbe: async () => [{ id: "anthropic.claude-sonnet", name: "Claude Sonnet" }],
|
|
457
|
+
});
|
|
458
|
+
expect(status.ready).toBe(true);
|
|
459
|
+
expect(status.satisfiedBy).toBe("env");
|
|
460
|
+
expect(status.bedrockRegion).toBe("eu-central-1");
|
|
461
|
+
expect(status.bedrockModels).toEqual([
|
|
462
|
+
{ id: "anthropic.claude-sonnet", name: "Claude Sonnet" },
|
|
463
|
+
]);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("BEDROCK_AUTH_MODE=bearer: probe failure → ready:false with the classified hint", async () => {
|
|
467
|
+
const env = {
|
|
468
|
+
BEDROCK_AUTH_MODE: "bearer",
|
|
469
|
+
AWS_BEARER_TOKEN_BEDROCK: "bedrock-api-key",
|
|
470
|
+
AWS_REGION: "us-east-1",
|
|
471
|
+
};
|
|
472
|
+
const status = await checkPiMonoCredentials(env, {
|
|
473
|
+
homeDir: HOME,
|
|
474
|
+
fs: noFiles,
|
|
475
|
+
bedrockProbe: bedrockProbeAuthFail,
|
|
476
|
+
});
|
|
477
|
+
expect(status.ready).toBe(false);
|
|
478
|
+
expect(status.bedrockRegion).toBe("us-east-1");
|
|
428
479
|
});
|
|
429
480
|
|
|
430
481
|
test("BEDROCK_AUTH_MODE absent + no MODEL_OVERRIDE=amazon-bedrock: no probe", async () => {
|
|
@@ -689,6 +740,98 @@ describe("snapshot: every provider", () => {
|
|
|
689
740
|
|
|
690
741
|
// ─── REQUIRED_CRED_VARS_BY_PROVIDER documentation map ────────────────────────
|
|
691
742
|
|
|
743
|
+
describe("isBedrockMode (shared gate for the live test and the refresh loop)", () => {
|
|
744
|
+
test("recognizes both explicit modes and the MODEL_OVERRIDE prefix inference", () => {
|
|
745
|
+
expect(isBedrockMode({ BEDROCK_AUTH_MODE: "sdk" })).toBe(true);
|
|
746
|
+
expect(isBedrockMode({ BEDROCK_AUTH_MODE: "bearer" })).toBe(true);
|
|
747
|
+
expect(isBedrockMode({ BEDROCK_AUTH_MODE: "Bearer", MODEL_OVERRIDE: "some-model" })).toBe(true);
|
|
748
|
+
expect(isBedrockMode({ MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude" })).toBe(true);
|
|
749
|
+
expect(isBedrockMode({ MODEL_OVERRIDE: "anthropic/claude" })).toBe(false);
|
|
750
|
+
expect(isBedrockMode({})).toBe(false);
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
describe("shouldRefreshBedrockStatus (runner post-task refresh gate)", () => {
|
|
755
|
+
const INTERVAL = 5 * 60 * 1000;
|
|
756
|
+
const base = { harnessProvider: "pi", lastRefreshAt: 1_000, intervalMs: INTERVAL };
|
|
757
|
+
|
|
758
|
+
test("bearer-only pi configuration is refreshed once the interval has elapsed", () => {
|
|
759
|
+
const env = {
|
|
760
|
+
BEDROCK_AUTH_MODE: "bearer",
|
|
761
|
+
AWS_BEARER_TOKEN_BEDROCK: "tok",
|
|
762
|
+
AWS_REGION: "us-east-1",
|
|
763
|
+
};
|
|
764
|
+
expect(shouldRefreshBedrockStatus({ ...base, env, now: 1_000 + INTERVAL + 1 })).toBe(true);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
test("bearer mode stays throttled inside the interval", () => {
|
|
768
|
+
const env = {
|
|
769
|
+
BEDROCK_AUTH_MODE: "bearer",
|
|
770
|
+
AWS_BEARER_TOKEN_BEDROCK: "tok",
|
|
771
|
+
AWS_REGION: "us-east-1",
|
|
772
|
+
};
|
|
773
|
+
expect(shouldRefreshBedrockStatus({ ...base, env, now: 1_000 + INTERVAL })).toBe(false);
|
|
774
|
+
expect(shouldRefreshBedrockStatus({ ...base, env, now: 1_000 + 1 })).toBe(false);
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
test("sdk mode and the amazon-bedrock/ prefix inference refresh the same way", () => {
|
|
778
|
+
const now = 1_000 + INTERVAL + 1;
|
|
779
|
+
expect(shouldRefreshBedrockStatus({ ...base, env: { BEDROCK_AUTH_MODE: "sdk" }, now })).toBe(
|
|
780
|
+
true,
|
|
781
|
+
);
|
|
782
|
+
expect(
|
|
783
|
+
shouldRefreshBedrockStatus({
|
|
784
|
+
...base,
|
|
785
|
+
env: { MODEL_OVERRIDE: "amazon-bedrock/anthropic.claude" },
|
|
786
|
+
now,
|
|
787
|
+
}),
|
|
788
|
+
).toBe(true);
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
test("never fires outside Bedrock mode or for a non-pi harness", () => {
|
|
792
|
+
const now = 1_000 + INTERVAL + 1;
|
|
793
|
+
expect(shouldRefreshBedrockStatus({ ...base, env: { ANTHROPIC_API_KEY: "sk" }, now })).toBe(
|
|
794
|
+
false,
|
|
795
|
+
);
|
|
796
|
+
expect(
|
|
797
|
+
shouldRefreshBedrockStatus({ ...base, env: { MODEL_OVERRIDE: "anthropic/claude" }, now }),
|
|
798
|
+
).toBe(false);
|
|
799
|
+
expect(
|
|
800
|
+
shouldRefreshBedrockStatus({
|
|
801
|
+
...base,
|
|
802
|
+
harnessProvider: "claude",
|
|
803
|
+
env: { BEDROCK_AUTH_MODE: "bearer" },
|
|
804
|
+
now,
|
|
805
|
+
}),
|
|
806
|
+
).toBe(false);
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
describe("validateProviderCredentials: pi Bedrock pass-through", () => {
|
|
811
|
+
const saved = { ...process.env };
|
|
812
|
+
const restore = () => {
|
|
813
|
+
for (const key of Object.keys(process.env)) if (!(key in saved)) delete process.env[key];
|
|
814
|
+
Object.assign(process.env, saved);
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
test("bearer mode is a pass-through like sdk mode: no provider-key live test is issued", async () => {
|
|
818
|
+
const realFetch = globalThis.fetch;
|
|
819
|
+
// A stray provider key must not be live-tested: the Bedrock enumeration already ran.
|
|
820
|
+
process.env.BEDROCK_AUTH_MODE = "bearer";
|
|
821
|
+
process.env.AWS_BEARER_TOKEN_BEDROCK = "bedrock-api-key";
|
|
822
|
+
process.env.ANTHROPIC_API_KEY = "x";
|
|
823
|
+
globalThis.fetch = (async () => {
|
|
824
|
+
throw new Error("live test must not reach the network in Bedrock mode");
|
|
825
|
+
}) as typeof fetch;
|
|
826
|
+
try {
|
|
827
|
+
expect(await validateProviderCredentials("pi")).toEqual({ ok: true, latency_ms: 0 });
|
|
828
|
+
} finally {
|
|
829
|
+
globalThis.fetch = realFetch;
|
|
830
|
+
restore();
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
|
|
692
835
|
describe("REQUIRED_CRED_VARS_BY_PROVIDER", () => {
|
|
693
836
|
test("covers every supported provider", () => {
|
|
694
837
|
const providers = ["claude", "claude-managed", "codex", "devin", "opencode", "pi"] as const;
|