@vellumai/assistant 0.9.0 → 0.9.1-staging.1
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/ARCHITECTURE.md +18 -34
- package/bun.lock +7 -8
- package/docs/activation-funnel-telemetry.md +4 -4
- package/docs/architecture/security.md +29 -28
- package/docs/stt-provider-onboarding.md +3 -5
- package/docs/workflows-testing.md +13 -44
- package/docs/workflows.md +3 -5
- package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
- package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
- package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
- package/node_modules/@vellumai/gateway-client/src/index.ts +17 -6
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
- package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
- package/openapi.yaml +135 -59
- package/package.json +2 -1
- package/scripts/sync-llm-catalog.ts +6 -15
- package/scripts/sync-web-search-catalog.ts +3 -11
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +14 -26
- package/src/__tests__/agent-loop-compaction-strip.test.ts +240 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
- package/src/__tests__/app-dir-path-guard.test.ts +0 -1
- package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
- package/src/__tests__/avatar-identity-sync.test.ts +2 -27
- package/src/__tests__/btw-routes.test.ts +6 -8
- package/src/__tests__/checker.test.ts +0 -3
- package/src/__tests__/config-loader-backfill.test.ts +103 -6
- package/src/__tests__/config-watcher.test.ts +0 -18
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
- package/src/__tests__/credential-broker.test.ts +449 -1
- package/src/__tests__/credential-execution-tools.test.ts +0 -1
- package/src/__tests__/credential-prompt-route.test.ts +3 -4
- package/src/__tests__/credential-routes.test.ts +360 -0
- package/src/__tests__/credential-security-invariants.test.ts +2 -13
- package/src/__tests__/dynamic-page-surface.test.ts +101 -1
- package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
- package/src/__tests__/gateway-only-guard.test.ts +3 -7
- package/src/__tests__/identity-routes.test.ts +0 -189
- package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
- package/src/__tests__/invite-redemption-service.test.ts +4 -4
- package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
- package/src/__tests__/llm-catalog-parity.test.ts +0 -22
- package/src/__tests__/llm-resolver.test.ts +49 -24
- package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
- package/src/__tests__/onboarding-persona-write.test.ts +1 -1
- package/src/__tests__/persona-resolver.test.ts +11 -14
- package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
- package/src/__tests__/registry.test.ts +2 -7
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
- package/src/__tests__/schedule-routes.test.ts +0 -30
- package/src/__tests__/schedule-tools.test.ts +2 -18
- package/src/__tests__/skill-execute-input.test.ts +46 -1
- package/src/__tests__/skill-runtime-path.test.ts +2 -3
- package/src/__tests__/subagent-tools.test.ts +116 -0
- package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
- package/src/__tests__/token-manager.test.ts +519 -0
- package/src/__tests__/tool-executor.test.ts +0 -79
- package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
- package/src/__tests__/trusted-contact-verification.test.ts +6 -6
- package/src/__tests__/voice-invite-redemption.test.ts +2 -2
- package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
- package/src/__tests__/workspace-greetings.test.ts +152 -0
- package/src/agent/loop.ts +25 -5
- package/src/api/README.md +6 -6
- package/src/api/responses/conversation-message.ts +2 -4
- package/src/api/responses/home.ts +0 -4
- package/src/approvals/guardian-request-resolvers.ts +2 -2
- package/src/calls/relay-access-wait.ts +1 -1
- package/src/calls/voice-session-bridge.ts +2 -2
- package/src/cli/commands/plugins.ts +143 -2
- package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +313 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +253 -2
- package/src/cli/lib/diff-plugin.ts +346 -0
- package/src/cli/lib/install-from-github.ts +105 -17
- package/src/cli/lib/merge-plugin-tree.ts +228 -0
- package/src/cli/lib/plugin-fingerprint.ts +14 -0
- package/src/cli/lib/upgrade-plugin.ts +270 -10
- package/src/cli/program.ts +0 -2
- package/src/config/bundled-skills/subagent/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
- package/src/config/bundled-skills/workflows/SKILL.md +0 -1
- package/src/config/bundled-tool-registry.ts +2 -7
- package/src/config/call-site-defaults.ts +12 -2
- package/src/config/feature-flag-registry.json +1 -17
- package/src/config/inference-profile-validation.ts +26 -0
- package/src/config/loader.ts +4 -0
- package/src/config/profile-order.ts +28 -0
- package/src/config/schemas/elevenlabs.ts +0 -1
- package/src/config/schemas/platform.ts +0 -8
- package/src/config/seed-inference-profiles.ts +25 -20
- package/src/contacts/contact-store.ts +87 -96
- package/src/contacts/contacts-write.ts +5 -21
- package/src/context/compactor.ts +2 -2
- package/src/credential-execution/process-manager.ts +55 -14
- package/src/credential-execution/prompted-credential.ts +2 -3
- package/src/daemon/config-watcher.ts +0 -4
- package/src/daemon/conversation-agent-loop.ts +15 -4
- package/src/daemon/conversation-slash.ts +2 -23
- package/src/daemon/conversation-tool-setup.ts +11 -3
- package/src/daemon/conversation.ts +2 -0
- package/src/daemon/handlers/config-channels.ts +20 -16
- package/src/daemon/handlers/config-slack-channel.ts +2 -3
- package/src/daemon/lifecycle.ts +0 -7
- package/src/daemon/message-types/conversations.ts +3 -3
- package/src/daemon/message-types/sync.ts +0 -1
- package/src/daemon/orphan-reaper.test.ts +0 -19
- package/src/daemon/orphan-reaper.ts +2 -24
- package/src/daemon/server.ts +0 -10
- package/src/home/relationship-state.ts +2 -4
- package/src/memory/__tests__/memory-retrospective-job.test.ts +195 -401
- package/src/memory/bookmark-crud.ts +1 -2
- package/src/memory/db-init.ts +7 -17
- package/src/memory/embedding-backend.ts +23 -0
- package/src/memory/embedding-billing-breaker.ts +96 -0
- package/src/memory/jobs-store.ts +25 -13
- package/src/memory/jobs-worker.ts +52 -0
- package/src/memory/memory-retrospective-constants.ts +4 -4
- package/src/memory/memory-retrospective-job.ts +19 -227
- package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +62 -0
- package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +311 -0
- package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/run-migrations.ts +41 -0
- package/src/memory/migrations/validate-migration-state.ts +1 -1
- package/src/memory/schema/contacts.ts +0 -4
- package/src/messaging/providers/slack/adapter.ts +1 -1
- package/src/notifications/adapters/shared.ts +29 -0
- package/src/notifications/adapters/slack.ts +5 -32
- package/src/notifications/adapters/telegram.ts +2 -20
- package/src/notifications/broadcaster.ts +10 -1
- package/src/notifications/home-feed-side-effect.ts +4 -3
- package/src/notifications/notification-utils.ts +17 -19
- package/src/notifications/types.ts +7 -0
- package/src/oauth/AGENTS.md +5 -24
- package/src/plugin-api/constants.ts +1 -1
- package/src/plugin-api/index.ts +6 -1
- package/src/plugin-api/model-profiles.ts +33 -0
- package/src/plugin-api/types.ts +50 -2
- package/src/plugins/defaults/index.ts +25 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +54 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +107 -7
- package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
- package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
- package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
- package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +1 -1
- package/src/prompts/persona-resolver.ts +2 -2
- package/src/runtime/AGENTS.md +0 -1
- package/src/runtime/actor-trust-resolver.ts +12 -44
- package/src/runtime/btw-sidechain.ts +3 -6
- package/src/runtime/channel-approval-types.ts +18 -45
- package/src/runtime/channel-invite-transports/telegram.ts +4 -4
- package/src/runtime/channel-verification-service.ts +4 -3
- package/src/runtime/invite-redemption-service.ts +3 -3
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +218 -1
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
- package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
- package/src/runtime/routes/btw-routes.ts +1 -27
- package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +2 -2
- package/src/runtime/routes/credential-routes.ts +40 -16
- package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
- package/src/runtime/routes/identity-routes.ts +1 -296
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +171 -5
- package/src/runtime/routes/schedule-routes.ts +0 -22
- package/src/runtime/routes/workflow-routes.test.ts +4 -43
- package/src/runtime/routes/workflow-routes.ts +0 -28
- package/src/runtime/routes/workspace-greetings.ts +55 -0
- package/src/runtime/sync/resource-sync-events.ts +1 -11
- package/src/schedule/inference-profile.ts +2 -14
- package/src/subagent/manager.ts +6 -0
- package/src/subagent/types.ts +6 -0
- package/src/tools/AGENTS.md +3 -3
- package/src/tools/browser/browser-execution.ts +1 -1
- package/src/tools/network/web-search-error.ts +1 -1
- package/src/tools/permission-checker.ts +1 -1
- package/src/tools/schedule/create.ts +3 -9
- package/src/tools/schedule/update.ts +2 -10
- package/src/tools/side-effects.ts +2 -17
- package/src/tools/skills/execute.ts +34 -0
- package/src/tools/subagent/spawn.ts +34 -9
- package/src/tools/tool-approval-handler.ts +1 -3
- package/src/tools/tool-manifest.ts +0 -2
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/workflows/run-workflow.test.ts +8 -18
- package/src/util/platform.ts +2 -2
- package/src/workflows/capabilities.ts +2 -3
- package/src/workflows/run-manager.test.ts +0 -25
- package/src/workflows/run-manager.ts +2 -24
- package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
- package/src/__tests__/credential-security-e2e.test.ts +0 -362
- package/src/__tests__/credential-vault-unit.test.ts +0 -1528
- package/src/__tests__/credential-vault.test.ts +0 -1706
- package/src/__tests__/identity-intro-cache.test.ts +0 -315
- package/src/__tests__/secret-onetime-send.test.ts +0 -182
- package/src/cli/commands/__tests__/task.test.ts +0 -914
- package/src/cli/commands/task.ts +0 -771
- package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
- package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
- package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
- package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
- package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
- package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
- package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
- package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
- package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
- package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
- package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
- package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
- package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
- package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
- package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
- package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
- package/src/memory/preloaded-apps.ts +0 -116
- package/src/runtime/routes/identity-intro-cache.ts +0 -172
- package/src/tools/credentials/vault.ts +0 -712
|
@@ -417,6 +417,24 @@ describe("loadConfig startup behavior", () => {
|
|
|
417
417
|
expect(raw.memory?.jobs?.workerConcurrency).toBe(4);
|
|
418
418
|
});
|
|
419
419
|
|
|
420
|
+
test("strips daemon.reapOrphanedSubprocesses from existing user configs", () => {
|
|
421
|
+
// `daemon.reapOrphanedSubprocesses` is a deprecated opt-in flag: the
|
|
422
|
+
// orphan-subprocess reaper runs by default whenever the daemon is PID 1 on
|
|
423
|
+
// Linux. Existing configs that have it written to disk should load cleanly
|
|
424
|
+
// with the field silently stripped.
|
|
425
|
+
writeConfig({
|
|
426
|
+
provider: "anthropic",
|
|
427
|
+
daemon: { reapOrphanedSubprocesses: true, standaloneRecording: false },
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
loadConfig();
|
|
431
|
+
|
|
432
|
+
const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
433
|
+
expect(raw.daemon?.reapOrphanedSubprocesses).toBeUndefined();
|
|
434
|
+
// Sibling fields under daemon are preserved
|
|
435
|
+
expect(raw.daemon?.standaloneRecording).toBe(false);
|
|
436
|
+
});
|
|
437
|
+
|
|
420
438
|
test("still writes a default config on first launch when file is absent", () => {
|
|
421
439
|
// Discoverability: when no config.json exists, write one populated with
|
|
422
440
|
// all schema defaults so users can see and edit available options.
|
|
@@ -674,6 +692,70 @@ describe("loadConfig startup behavior", () => {
|
|
|
674
692
|
expect(raw.llm.activeProfile).toBe("balanced");
|
|
675
693
|
});
|
|
676
694
|
|
|
695
|
+
test("on-platform managed profiles reconcile to the code template on every boot", () => {
|
|
696
|
+
// Headline behavior: on-platform installs now reconcile managed profile
|
|
697
|
+
// content from the code template on every boot (same as off-platform), so
|
|
698
|
+
// model/config updates ship in a release without a workspace migration.
|
|
699
|
+
process.env.IS_PLATFORM = "true";
|
|
700
|
+
|
|
701
|
+
writeConfig({
|
|
702
|
+
llm: {
|
|
703
|
+
profiles: {
|
|
704
|
+
balanced: {
|
|
705
|
+
source: "managed",
|
|
706
|
+
provider: "anthropic",
|
|
707
|
+
model: "old-model-from-previous-release",
|
|
708
|
+
maxTokens: 1,
|
|
709
|
+
provider_connection: "anthropic-managed",
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
activeProfile: "balanced",
|
|
713
|
+
},
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
// Non-hatch boot (no overlay). Content is refreshed from the template.
|
|
717
|
+
mergeDefaultConfigAndSeedInferenceProfiles();
|
|
718
|
+
const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
719
|
+
|
|
720
|
+
expect(raw.llm.profiles.balanced.model).toBe("claude-sonnet-4-6");
|
|
721
|
+
expect(raw.llm.profiles.balanced.maxTokens).toBe(16000);
|
|
722
|
+
expect(raw.llm.profiles.balanced.provider_connection).toBe(
|
|
723
|
+
"anthropic-managed",
|
|
724
|
+
);
|
|
725
|
+
expect(raw.llm.activeProfile).toBe("balanced");
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
test("on-platform reseed preserves user-edited label and status on managed profiles", () => {
|
|
729
|
+
// The only two fields a user may override on a managed profile — label and
|
|
730
|
+
// status — survive the on-platform reconcile, exactly as off-platform.
|
|
731
|
+
process.env.IS_PLATFORM = "true";
|
|
732
|
+
|
|
733
|
+
writeConfig({
|
|
734
|
+
llm: {
|
|
735
|
+
profiles: {
|
|
736
|
+
balanced: {
|
|
737
|
+
source: "managed",
|
|
738
|
+
provider: "anthropic",
|
|
739
|
+
model: "old-model-from-previous-release",
|
|
740
|
+
provider_connection: "anthropic-managed",
|
|
741
|
+
label: "My Default",
|
|
742
|
+
status: "disabled",
|
|
743
|
+
},
|
|
744
|
+
},
|
|
745
|
+
activeProfile: "balanced",
|
|
746
|
+
},
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
mergeDefaultConfigAndSeedInferenceProfiles();
|
|
750
|
+
const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
751
|
+
|
|
752
|
+
// Content refreshes from the template...
|
|
753
|
+
expect(raw.llm.profiles.balanced.model).toBe("claude-sonnet-4-6");
|
|
754
|
+
// ...but the user's label and status overrides are preserved.
|
|
755
|
+
expect(raw.llm.profiles.balanced.label).toBe("My Default");
|
|
756
|
+
expect(raw.llm.profiles.balanced.status).toBe("disabled");
|
|
757
|
+
});
|
|
758
|
+
|
|
677
759
|
test("off-platform reseed preserves user-edited label on managed profiles (Codex P1 on PR #30362)", () => {
|
|
678
760
|
// Simulate a user who renamed the managed "balanced" profile via
|
|
679
761
|
// PUT /v1/config/llm/profiles/balanced { label: "My Default" }.
|
|
@@ -770,7 +852,13 @@ describe("loadConfig startup behavior", () => {
|
|
|
770
852
|
expect("status" in raw.llm.profiles.balanced).toBe(false);
|
|
771
853
|
});
|
|
772
854
|
|
|
773
|
-
test("platform
|
|
855
|
+
test("platform overlay fragment wins its hatch boot, then content reconciles to the code template (label preserved)", () => {
|
|
856
|
+
// The overlay is authoritative for the boot it is supplied: its `balanced`
|
|
857
|
+
// fragment lands verbatim and is never polluted by template fields it omits
|
|
858
|
+
// (no maxTokens/thinking leak in). On the next boot — overlay archived —
|
|
859
|
+
// managed profile *content* reconciles to the code template, since the
|
|
860
|
+
// templates are the single source of truth for content. Only the
|
|
861
|
+
// user/overlay-set `label` survives the reconcile.
|
|
774
862
|
process.env.IS_PLATFORM = "true";
|
|
775
863
|
|
|
776
864
|
writeConfig({
|
|
@@ -824,6 +912,7 @@ describe("loadConfig startup behavior", () => {
|
|
|
824
912
|
const config = loadConfig();
|
|
825
913
|
const mainAgentConfig = resolveCallSiteConfig("mainAgent", config.llm);
|
|
826
914
|
|
|
915
|
+
// Hatch boot: overlay fragment is preserved verbatim (preserveProfileNames).
|
|
827
916
|
expect(config.llm.activeProfile).toBe("balanced");
|
|
828
917
|
expect(config.llm.profiles.balanced).toEqual({
|
|
829
918
|
source: "managed",
|
|
@@ -844,16 +933,24 @@ describe("loadConfig startup behavior", () => {
|
|
|
844
933
|
expect(raw.llm.profiles.balanced.maxTokens).toBeUndefined();
|
|
845
934
|
expect(raw.llm.profiles.balanced.thinking).toBeUndefined();
|
|
846
935
|
|
|
936
|
+
// Next boot, no overlay: content reconciles to the anthropic-managed code
|
|
937
|
+
// template; only the overlay-set label is carried across.
|
|
847
938
|
mergeDefaultConfigAndSeedInferenceProfiles();
|
|
848
939
|
|
|
849
940
|
const afterRestart = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
850
941
|
expect(afterRestart.llm.activeProfile).toBe("balanced");
|
|
851
|
-
expect(afterRestart.llm.profiles.balanced).
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
942
|
+
expect(afterRestart.llm.profiles.balanced.provider).toBe("anthropic");
|
|
943
|
+
expect(afterRestart.llm.profiles.balanced.provider_connection).toBe(
|
|
944
|
+
"anthropic-managed",
|
|
945
|
+
);
|
|
946
|
+
expect(afterRestart.llm.profiles.balanced.model).toBe("claude-sonnet-4-6");
|
|
947
|
+
expect(afterRestart.llm.profiles.balanced.maxTokens).toBe(16000);
|
|
948
|
+
expect(afterRestart.llm.profiles.balanced.thinking).toEqual({
|
|
949
|
+
enabled: true,
|
|
950
|
+
streamThinking: true,
|
|
856
951
|
});
|
|
952
|
+
// The user/overlay-set label is the one field that survives the reconcile.
|
|
953
|
+
expect(afterRestart.llm.profiles.balanced.label).toBe("Platform Balanced");
|
|
857
954
|
});
|
|
858
955
|
|
|
859
956
|
test("quarantines corrupt config before merging hatch overlay", () => {
|
|
@@ -231,24 +231,6 @@ describe("ConfigWatcher workspace file handlers", () => {
|
|
|
231
231
|
expect(evictCallCount).toBe(1);
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
-
test("SOUL.md change triggers identity intro refetch notification", async () => {
|
|
235
|
-
let introCallCount = 0;
|
|
236
|
-
watcher.start(
|
|
237
|
-
onConversationEvict,
|
|
238
|
-
undefined,
|
|
239
|
-
undefined,
|
|
240
|
-
undefined,
|
|
241
|
-
undefined,
|
|
242
|
-
undefined,
|
|
243
|
-
() => {
|
|
244
|
-
introCallCount += 1;
|
|
245
|
-
},
|
|
246
|
-
);
|
|
247
|
-
simulateFileChange(WORKSPACE_DIR, "SOUL.md");
|
|
248
|
-
await new Promise((r) => setTimeout(r, WAIT_MS));
|
|
249
|
-
expect(introCallCount).toBe(1);
|
|
250
|
-
});
|
|
251
|
-
|
|
252
234
|
test("IDENTITY.md change triggers onConversationEvict", async () => {
|
|
253
235
|
watcher.start(onConversationEvict);
|
|
254
236
|
simulateFileChange(WORKSPACE_DIR, "IDENTITY.md");
|
|
@@ -366,6 +366,7 @@ import { runAgentLoopImpl } from "../daemon/conversation-agent-loop.js";
|
|
|
366
366
|
interface CapturedAgentLoopRun {
|
|
367
367
|
callSite: LLMCallSite | undefined;
|
|
368
368
|
overrideProfile: string | undefined;
|
|
369
|
+
forceOverrideProfile: boolean | undefined;
|
|
369
370
|
resolvedOverrideProfile: string | undefined;
|
|
370
371
|
resolvedMaxInputTokens: number | undefined;
|
|
371
372
|
}
|
|
@@ -383,6 +384,7 @@ function makeCtx(
|
|
|
383
384
|
captured.push({
|
|
384
385
|
callSite: options.callSite,
|
|
385
386
|
overrideProfile: options.overrideProfile,
|
|
387
|
+
forceOverrideProfile: options.forceOverrideProfile,
|
|
386
388
|
resolvedOverrideProfile: options.resolveOverrideProfile?.(),
|
|
387
389
|
resolvedMaxInputTokens: options.resolveContextWindow?.().maxInputTokens,
|
|
388
390
|
});
|
|
@@ -620,6 +622,26 @@ describe("runAgentLoopImpl — per-conversation inferenceProfile", () => {
|
|
|
620
622
|
expect(captured.length).toBeGreaterThan(0);
|
|
621
623
|
for (const call of captured) {
|
|
622
624
|
expect(call.overrideProfile).toBe("fast");
|
|
625
|
+
expect(call.forceOverrideProfile).toBeUndefined();
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
test("explicit options.forceOverrideProfile is passed to AgentLoop.run", async () => {
|
|
630
|
+
const captured: CapturedAgentLoopRun[] = [];
|
|
631
|
+
const ctx = makeCtx(captured, {
|
|
632
|
+
conversationType: "background",
|
|
633
|
+
inferenceProfile: null,
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
await runAgentLoopImpl(ctx, "hello", "msg-1", () => {}, {
|
|
637
|
+
overrideProfile: "fast",
|
|
638
|
+
forceOverrideProfile: true,
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
expect(captured.length).toBeGreaterThan(0);
|
|
642
|
+
for (const call of captured) {
|
|
643
|
+
expect(call.overrideProfile).toBe("fast");
|
|
644
|
+
expect(call.forceOverrideProfile).toBe(true);
|
|
623
645
|
}
|
|
624
646
|
});
|
|
625
647
|
|
|
@@ -1,14 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
2
3
|
import { tmpdir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
5
6
|
|
|
6
7
|
import { credentialKey } from "../security/credential-key.js";
|
|
8
|
+
import { _resetBackend, setSecureKeyAsync } from "../security/secure-keys.js";
|
|
7
9
|
import { CredentialBroker } from "../tools/credentials/broker.js";
|
|
8
10
|
import {
|
|
9
11
|
_setMetadataPath,
|
|
10
12
|
upsertCredentialMetadata,
|
|
11
13
|
} from "../tools/credentials/metadata-store.js";
|
|
14
|
+
import { BROWSER_FILL_CAPABILITY } from "../tools/credentials/tool-policy.js";
|
|
15
|
+
import { setStorePathForTesting } from "./encrypted-store-test-helpers.js";
|
|
16
|
+
|
|
17
|
+
const TEST_DIR = join(
|
|
18
|
+
tmpdir(),
|
|
19
|
+
`vellum-broker-test-${randomBytes(4).toString("hex")}`,
|
|
20
|
+
);
|
|
21
|
+
const STORE_PATH = join(TEST_DIR, "keys.enc");
|
|
12
22
|
|
|
13
23
|
describe("CredentialBroker", () => {
|
|
14
24
|
let broker: CredentialBroker;
|
|
@@ -222,3 +232,441 @@ describe("CredentialBroker", () => {
|
|
|
222
232
|
});
|
|
223
233
|
});
|
|
224
234
|
});
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Behavior that reads stored secret values, exercised against the encrypted
|
|
238
|
+
// store backend (transient injection, multi-template serverUseById, revokeAll,
|
|
239
|
+
// and canonical/legacy capability-key resolution).
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
describe("CredentialBroker (encrypted store backend)", () => {
|
|
243
|
+
let broker: CredentialBroker;
|
|
244
|
+
|
|
245
|
+
beforeEach(() => {
|
|
246
|
+
if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
|
|
247
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
248
|
+
setStorePathForTesting(STORE_PATH);
|
|
249
|
+
_resetBackend();
|
|
250
|
+
_setMetadataPath(join(TEST_DIR, "metadata.json"));
|
|
251
|
+
broker = new CredentialBroker();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
afterEach(() => {
|
|
255
|
+
_setMetadataPath(null);
|
|
256
|
+
setStorePathForTesting(null);
|
|
257
|
+
_resetBackend();
|
|
258
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe("transient credentials", () => {
|
|
262
|
+
test("consume returns transient value and deletes it", () => {
|
|
263
|
+
// GIVEN a credential with metadata and an injected one-time transient value
|
|
264
|
+
upsertCredentialMetadata("svc", "key", { allowedTools: ["tool1"] });
|
|
265
|
+
broker.injectTransient("svc", "key", "one-time-secret");
|
|
266
|
+
|
|
267
|
+
// WHEN the credential is authorized and consumed
|
|
268
|
+
const auth = broker.authorize({
|
|
269
|
+
service: "svc",
|
|
270
|
+
field: "key",
|
|
271
|
+
toolName: "tool1",
|
|
272
|
+
});
|
|
273
|
+
expect(auth.authorized).toBe(true);
|
|
274
|
+
if (!auth.authorized) return;
|
|
275
|
+
const result = broker.consume(auth.token.tokenId);
|
|
276
|
+
|
|
277
|
+
// THEN the transient value is returned alongside the storage key
|
|
278
|
+
expect(result.success).toBe(true);
|
|
279
|
+
expect(result.value).toBe("one-time-secret");
|
|
280
|
+
expect(result.storageKey).toBe(credentialKey("svc", "key"));
|
|
281
|
+
|
|
282
|
+
// AND a subsequent authorize+consume no longer has the transient value
|
|
283
|
+
const auth2 = broker.authorize({
|
|
284
|
+
service: "svc",
|
|
285
|
+
field: "key",
|
|
286
|
+
toolName: "tool1",
|
|
287
|
+
});
|
|
288
|
+
expect(auth2.authorized).toBe(true);
|
|
289
|
+
if (!auth2.authorized) return;
|
|
290
|
+
const result2 = broker.consume(auth2.token.tokenId);
|
|
291
|
+
expect(result2.success).toBe(true);
|
|
292
|
+
expect(result2.value).toBeUndefined();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("browserFill uses transient value when available", async () => {
|
|
296
|
+
// GIVEN a credential with a transient value and no stored value
|
|
297
|
+
upsertCredentialMetadata("github", "token", {
|
|
298
|
+
allowedTools: ["browser_fill_credential"],
|
|
299
|
+
});
|
|
300
|
+
broker.injectTransient("github", "token", "transient-ghp-123");
|
|
301
|
+
|
|
302
|
+
// WHEN browserFill runs
|
|
303
|
+
let filledValue: string | undefined;
|
|
304
|
+
const result = await broker.browserFill({
|
|
305
|
+
service: "github",
|
|
306
|
+
field: "token",
|
|
307
|
+
toolName: "browser_fill_credential",
|
|
308
|
+
fill: async (v) => {
|
|
309
|
+
filledValue = v;
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// THEN the transient value is filled
|
|
314
|
+
expect(result.success).toBe(true);
|
|
315
|
+
expect(filledValue).toBe("transient-ghp-123");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("browserFill consumes transient value — second fill falls back to stored", async () => {
|
|
319
|
+
// GIVEN a credential with both a stored value and a transient value
|
|
320
|
+
upsertCredentialMetadata("github", "token", {
|
|
321
|
+
allowedTools: ["browser_fill_credential"],
|
|
322
|
+
});
|
|
323
|
+
await setSecureKeyAsync(credentialKey("github", "token"), "stored-value");
|
|
324
|
+
broker.injectTransient("github", "token", "transient-value");
|
|
325
|
+
|
|
326
|
+
// WHEN browserFill runs twice
|
|
327
|
+
let filled1: string | undefined;
|
|
328
|
+
await broker.browserFill({
|
|
329
|
+
service: "github",
|
|
330
|
+
field: "token",
|
|
331
|
+
toolName: "browser_fill_credential",
|
|
332
|
+
fill: async (v) => {
|
|
333
|
+
filled1 = v;
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
let filled2: string | undefined;
|
|
337
|
+
await broker.browserFill({
|
|
338
|
+
service: "github",
|
|
339
|
+
field: "token",
|
|
340
|
+
toolName: "browser_fill_credential",
|
|
341
|
+
fill: async (v) => {
|
|
342
|
+
filled2 = v;
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// THEN the first fill uses the transient and the second falls back to stored
|
|
347
|
+
expect(filled1).toBe("transient-value");
|
|
348
|
+
expect(filled2).toBe("stored-value");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("browserFill preserves transient value on fill failure", async () => {
|
|
352
|
+
// GIVEN a credential with a transient value
|
|
353
|
+
upsertCredentialMetadata("github", "token", {
|
|
354
|
+
allowedTools: ["browser_fill_credential"],
|
|
355
|
+
});
|
|
356
|
+
broker.injectTransient("github", "token", "transient-preserved");
|
|
357
|
+
|
|
358
|
+
// WHEN the first fill throws
|
|
359
|
+
const result1 = await broker.browserFill({
|
|
360
|
+
service: "github",
|
|
361
|
+
field: "token",
|
|
362
|
+
toolName: "browser_fill_credential",
|
|
363
|
+
fill: async () => {
|
|
364
|
+
throw new Error("Playwright timeout");
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
expect(result1.success).toBe(false);
|
|
368
|
+
let filled: string | undefined;
|
|
369
|
+
const result2 = await broker.browserFill({
|
|
370
|
+
service: "github",
|
|
371
|
+
field: "token",
|
|
372
|
+
toolName: "browser_fill_credential",
|
|
373
|
+
fill: async (v) => {
|
|
374
|
+
filled = v;
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// THEN the transient value survives for the second fill
|
|
379
|
+
expect(result2.success).toBe(true);
|
|
380
|
+
expect(filled).toBe("transient-preserved");
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("serverUse uses transient value when available", async () => {
|
|
384
|
+
// GIVEN a credential with a transient value
|
|
385
|
+
upsertCredentialMetadata("vercel", "api_token", {
|
|
386
|
+
allowedTools: ["deploy"],
|
|
387
|
+
});
|
|
388
|
+
broker.injectTransient("vercel", "api_token", "transient-vercel-tok");
|
|
389
|
+
|
|
390
|
+
// WHEN serverUse runs
|
|
391
|
+
const result = await broker.serverUse({
|
|
392
|
+
service: "vercel",
|
|
393
|
+
field: "api_token",
|
|
394
|
+
toolName: "deploy",
|
|
395
|
+
execute: async (v) => {
|
|
396
|
+
expect(v).toBe("transient-vercel-tok");
|
|
397
|
+
return "deployed";
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// THEN the transient value is used for execution
|
|
402
|
+
expect(result.success).toBe(true);
|
|
403
|
+
expect(result.result).toBe("deployed");
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("serverUse consumes transient — subsequent call has no value without stored key", async () => {
|
|
407
|
+
// GIVEN a credential with only a transient value (no stored value)
|
|
408
|
+
upsertCredentialMetadata("vercel", "api_token", {
|
|
409
|
+
allowedTools: ["deploy"],
|
|
410
|
+
});
|
|
411
|
+
broker.injectTransient("vercel", "api_token", "transient-only");
|
|
412
|
+
|
|
413
|
+
// WHEN serverUse runs twice
|
|
414
|
+
await broker.serverUse({
|
|
415
|
+
service: "vercel",
|
|
416
|
+
field: "api_token",
|
|
417
|
+
toolName: "deploy",
|
|
418
|
+
execute: async () => "ok",
|
|
419
|
+
});
|
|
420
|
+
const result = await broker.serverUse({
|
|
421
|
+
service: "vercel",
|
|
422
|
+
field: "api_token",
|
|
423
|
+
toolName: "deploy",
|
|
424
|
+
execute: async () => {
|
|
425
|
+
throw new Error("should not be called");
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// THEN the second call fails because no stored value remains
|
|
430
|
+
expect(result.success).toBe(false);
|
|
431
|
+
expect(result.reason).toContain("no stored value");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("injectTransient replaces previous transient for same key", () => {
|
|
435
|
+
// GIVEN two transient values injected for the same credential
|
|
436
|
+
upsertCredentialMetadata("svc", "key", { allowedTools: ["t"] });
|
|
437
|
+
broker.injectTransient("svc", "key", "first");
|
|
438
|
+
broker.injectTransient("svc", "key", "second");
|
|
439
|
+
|
|
440
|
+
// WHEN the credential is authorized and consumed
|
|
441
|
+
const auth = broker.authorize({
|
|
442
|
+
service: "svc",
|
|
443
|
+
field: "key",
|
|
444
|
+
toolName: "t",
|
|
445
|
+
});
|
|
446
|
+
if (!auth.authorized) return;
|
|
447
|
+
const result = broker.consume(auth.token.tokenId);
|
|
448
|
+
|
|
449
|
+
// THEN the most recent transient value wins
|
|
450
|
+
expect(result.value).toBe("second");
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("transient value for one credential does not affect another", () => {
|
|
454
|
+
// GIVEN a transient value injected for svcA only
|
|
455
|
+
upsertCredentialMetadata("svcA", "key", { allowedTools: ["t"] });
|
|
456
|
+
upsertCredentialMetadata("svcB", "key", { allowedTools: ["t"] });
|
|
457
|
+
broker.injectTransient("svcA", "key", "val-a");
|
|
458
|
+
|
|
459
|
+
// WHEN both credentials are authorized and consumed
|
|
460
|
+
const authB = broker.authorize({
|
|
461
|
+
service: "svcB",
|
|
462
|
+
field: "key",
|
|
463
|
+
toolName: "t",
|
|
464
|
+
});
|
|
465
|
+
if (!authB.authorized) return;
|
|
466
|
+
const resultB = broker.consume(authB.token.tokenId);
|
|
467
|
+
const authA = broker.authorize({
|
|
468
|
+
service: "svcA",
|
|
469
|
+
field: "key",
|
|
470
|
+
toolName: "t",
|
|
471
|
+
});
|
|
472
|
+
if (!authA.authorized) return;
|
|
473
|
+
const resultA = broker.consume(authA.token.tokenId);
|
|
474
|
+
|
|
475
|
+
// THEN only svcA carries the transient value
|
|
476
|
+
expect(resultB.success).toBe(true);
|
|
477
|
+
expect(resultB.value).toBeUndefined();
|
|
478
|
+
expect(resultA.value).toBe("val-a");
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
describe("serverUseById edge cases", () => {
|
|
483
|
+
test("serverUseById with multiple injection templates returns all", async () => {
|
|
484
|
+
// GIVEN a credential with two injection templates and a stored value
|
|
485
|
+
const meta = upsertCredentialMetadata("multi", "api_key", {
|
|
486
|
+
allowedTools: ["proxy"],
|
|
487
|
+
injectionTemplates: [
|
|
488
|
+
{
|
|
489
|
+
hostPattern: "*.fal.ai",
|
|
490
|
+
injectionType: "header",
|
|
491
|
+
headerName: "Authorization",
|
|
492
|
+
valuePrefix: "Key ",
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
hostPattern: "gateway.fal.ai",
|
|
496
|
+
injectionType: "header",
|
|
497
|
+
headerName: "X-Fal-Key",
|
|
498
|
+
},
|
|
499
|
+
],
|
|
500
|
+
});
|
|
501
|
+
await setSecureKeyAsync(
|
|
502
|
+
credentialKey("multi", "api_key"),
|
|
503
|
+
"multi-secret",
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
// WHEN serverUseById resolves the credential by id
|
|
507
|
+
const result = await broker.serverUseById({
|
|
508
|
+
credentialId: meta.credentialId,
|
|
509
|
+
requestingTool: "proxy",
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// THEN both injection templates are returned without leaking the secret
|
|
513
|
+
expect(result.success).toBe(true);
|
|
514
|
+
if (!result.success) return;
|
|
515
|
+
expect(result.injectionTemplates).toHaveLength(2);
|
|
516
|
+
expect(result.injectionTemplates[0].hostPattern).toBe("*.fal.ai");
|
|
517
|
+
expect(result.injectionTemplates[1].hostPattern).toBe("gateway.fal.ai");
|
|
518
|
+
expect(JSON.stringify(result)).not.toContain("multi-secret");
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
describe("revokeAll", () => {
|
|
523
|
+
test("revokeAll clears all tokens and subsequent consume fails", () => {
|
|
524
|
+
// GIVEN two active tokens for a credential
|
|
525
|
+
upsertCredentialMetadata("svc", "key", { allowedTools: ["t1", "t2"] });
|
|
526
|
+
const a1 = broker.authorize({
|
|
527
|
+
service: "svc",
|
|
528
|
+
field: "key",
|
|
529
|
+
toolName: "t1",
|
|
530
|
+
});
|
|
531
|
+
const a2 = broker.authorize({
|
|
532
|
+
service: "svc",
|
|
533
|
+
field: "key",
|
|
534
|
+
toolName: "t2",
|
|
535
|
+
});
|
|
536
|
+
expect(broker.activeTokenCount).toBe(2);
|
|
537
|
+
|
|
538
|
+
// WHEN revokeAll is called
|
|
539
|
+
broker.revokeAll();
|
|
540
|
+
|
|
541
|
+
// THEN the token count is zero and consuming either token fails
|
|
542
|
+
expect(broker.activeTokenCount).toBe(0);
|
|
543
|
+
if (a1.authorized) {
|
|
544
|
+
expect(broker.consume(a1.token.tokenId).success).toBe(false);
|
|
545
|
+
}
|
|
546
|
+
if (a2.authorized) {
|
|
547
|
+
expect(broker.consume(a2.token.tokenId).success).toBe(false);
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test("revokeAll on empty broker is a no-op", () => {
|
|
552
|
+
// GIVEN a broker with no active tokens
|
|
553
|
+
expect(broker.activeTokenCount).toBe(0);
|
|
554
|
+
|
|
555
|
+
// WHEN revokeAll is called
|
|
556
|
+
broker.revokeAll();
|
|
557
|
+
|
|
558
|
+
// THEN the token count remains zero
|
|
559
|
+
expect(broker.activeTokenCount).toBe(0);
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
describe("canonical capability key", () => {
|
|
564
|
+
test("authorize succeeds with canonical key when metadata has canonical key", () => {
|
|
565
|
+
// GIVEN metadata allowing the canonical browser-fill capability key
|
|
566
|
+
upsertCredentialMetadata("github", "token", {
|
|
567
|
+
allowedTools: [BROWSER_FILL_CAPABILITY],
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// WHEN authorizing with the canonical key
|
|
571
|
+
const result = broker.authorize({
|
|
572
|
+
service: "github",
|
|
573
|
+
field: "token",
|
|
574
|
+
toolName: BROWSER_FILL_CAPABILITY,
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// THEN authorization succeeds
|
|
578
|
+
expect(result.authorized).toBe(true);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test("authorize succeeds with canonical key when metadata has legacy alias", () => {
|
|
582
|
+
// GIVEN metadata stored under the legacy browser_fill_credential alias
|
|
583
|
+
upsertCredentialMetadata("github", "token", {
|
|
584
|
+
allowedTools: ["browser_fill_credential"],
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// WHEN authorizing with the canonical key
|
|
588
|
+
const result = broker.authorize({
|
|
589
|
+
service: "github",
|
|
590
|
+
field: "token",
|
|
591
|
+
toolName: BROWSER_FILL_CAPABILITY,
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// THEN authorization succeeds
|
|
595
|
+
expect(result.authorized).toBe(true);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("authorize succeeds with legacy alias when metadata has canonical key", () => {
|
|
599
|
+
// GIVEN metadata allowing the canonical capability key
|
|
600
|
+
upsertCredentialMetadata("github", "token", {
|
|
601
|
+
allowedTools: [BROWSER_FILL_CAPABILITY],
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// WHEN authorizing with the legacy alias
|
|
605
|
+
const result = broker.authorize({
|
|
606
|
+
service: "github",
|
|
607
|
+
field: "token",
|
|
608
|
+
toolName: "browser_fill_credential",
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// THEN authorization succeeds
|
|
612
|
+
expect(result.authorized).toBe(true);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test("serverUse with canonical key works when metadata has legacy alias", async () => {
|
|
616
|
+
// GIVEN metadata stored under the legacy alias with a stored value
|
|
617
|
+
upsertCredentialMetadata("vercel", "api_token", {
|
|
618
|
+
allowedTools: ["browser_fill_credential"],
|
|
619
|
+
});
|
|
620
|
+
await setSecureKeyAsync(
|
|
621
|
+
credentialKey("vercel", "api_token"),
|
|
622
|
+
"vercel-tok",
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
// WHEN serverUse runs with the canonical key
|
|
626
|
+
const result = await broker.serverUse({
|
|
627
|
+
service: "vercel",
|
|
628
|
+
field: "api_token",
|
|
629
|
+
toolName: BROWSER_FILL_CAPABILITY,
|
|
630
|
+
execute: async (v) => v,
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// THEN execution succeeds with the stored value
|
|
634
|
+
expect(result.success).toBe(true);
|
|
635
|
+
expect(result.result).toBe("vercel-tok");
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
test("non-aliased tool names are unaffected by alias resolution", () => {
|
|
639
|
+
// GIVEN metadata allowing a non-aliased custom tool
|
|
640
|
+
upsertCredentialMetadata("svc", "key", {
|
|
641
|
+
allowedTools: ["custom_tool"],
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
// WHEN authorizing with that custom tool
|
|
645
|
+
const result = broker.authorize({
|
|
646
|
+
service: "svc",
|
|
647
|
+
field: "key",
|
|
648
|
+
toolName: "custom_tool",
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// THEN authorization succeeds
|
|
652
|
+
expect(result.authorized).toBe(true);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
test("non-aliased tool denied when only canonical key is allowed", () => {
|
|
656
|
+
// GIVEN metadata allowing only the canonical capability key
|
|
657
|
+
upsertCredentialMetadata("svc", "key", {
|
|
658
|
+
allowedTools: [BROWSER_FILL_CAPABILITY],
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// WHEN authorizing with an unrelated tool
|
|
662
|
+
const result = broker.authorize({
|
|
663
|
+
service: "svc",
|
|
664
|
+
field: "key",
|
|
665
|
+
toolName: "unrelated_tool",
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// THEN authorization is denied
|
|
669
|
+
expect(result.authorized).toBe(false);
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
});
|
|
@@ -136,7 +136,6 @@ describe("secure tool installation is separate from credential grants", () => {
|
|
|
136
136
|
test("manage_secure_command_tool does not share tool name with grant tools", () => {
|
|
137
137
|
// The tool name must not collide with existing credential grant tools
|
|
138
138
|
const grantToolNames = [
|
|
139
|
-
"credential_store",
|
|
140
139
|
"credential_grant",
|
|
141
140
|
"credential_revoke",
|
|
142
141
|
"credential_list",
|