@vellumai/assistant 0.11.3 → 0.11.4-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 +11 -6
- package/docs/architecture/memory.md +11 -0
- package/docs/architecture/turn-actor.md +70 -0
- package/docs/flux-turn-detection-spike.md +243 -0
- package/docs/stt-provider-onboarding.md +3 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/openapi.yaml +140 -38
- package/package.json +1 -1
- package/scripts/voice-ttft-spike.ts +3 -3
- package/src/__tests__/app-compiler.test.ts +38 -3
- package/src/__tests__/attachments-store.test.ts +21 -12
- package/src/__tests__/byok-default-profile-ensure.test.ts +2 -0
- package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
- package/src/__tests__/call-site-routing-provider.test.ts +1 -1
- package/src/__tests__/channel-availability-routes.test.ts +14 -1
- package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
- package/src/__tests__/channel-delivery-store.test.ts +14 -14
- package/src/__tests__/config-loader-backfill.test.ts +3 -3
- package/src/__tests__/config-schema.test.ts +25 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop.test.ts +28 -20
- package/src/__tests__/conversation-attention-store.test.ts +63 -0
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
- package/src/__tests__/conversation-fork-crud.test.ts +69 -0
- package/src/__tests__/conversation-fork-referential.test.ts +67 -0
- package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
- package/src/__tests__/conversation-queue.test.ts +55 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
- package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
- package/src/__tests__/credential-prompt-route.test.ts +7 -10
- package/src/__tests__/custom-profile-ensure.test.ts +5 -1
- package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
- package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
- package/src/__tests__/document-append-idempotency.test.ts +233 -0
- package/src/__tests__/edit-propagation.test.ts +0 -7
- package/src/__tests__/helpers/mock-actor-context.ts +49 -0
- package/src/__tests__/helpers/mock-conversation.ts +13 -1
- package/src/__tests__/injector-chain.test.ts +63 -41
- package/src/__tests__/injector-disk-pressure.test.ts +11 -23
- package/src/__tests__/llm-context-resolution.test.ts +73 -1
- package/src/__tests__/llm-schema.test.ts +5 -2
- package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
- package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
- package/src/__tests__/mtime-cache.test.ts +1 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -20
- package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
- package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
- package/src/__tests__/scoped-approval-grants.test.ts +11 -6
- package/src/__tests__/secret-ingress-channel.test.ts +0 -1
- package/src/__tests__/skills.test.ts +32 -0
- package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
- package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
- package/src/__tests__/turn-events-store.test.ts +43 -0
- package/src/__tests__/user-plugin-loader.test.ts +1 -1
- package/src/__tests__/visible-app-context.test.ts +16 -9
- package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
- package/src/__tests__/worker-plugin-surface.test.ts +77 -0
- package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
- package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
- package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
- package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
- package/src/acp/__tests__/auth-required.test.ts +161 -0
- package/src/acp/acp-claude-oauth.ts +19 -2
- package/src/acp/agent-process.test.ts +100 -0
- package/src/acp/agent-process.ts +29 -26
- package/src/acp/auth-required.ts +102 -0
- package/src/acp/session-manager.test.ts +119 -0
- package/src/acp/session-manager.ts +68 -2
- package/src/api/events/acp-auth-required.ts +55 -0
- package/src/api/index.ts +7 -0
- package/src/apps/app-store.ts +3 -0
- package/src/bundler/package-resolver.ts +2 -30
- package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
- package/src/calls/call-controller.ts +9 -2
- package/src/calls/call-setup-flow.ts +0 -1
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/calls/voice-session-bridge.ts +71 -16
- package/src/calls/voice-triage-escalate.ts +104 -2
- package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
- package/src/channels/config.ts +13 -0
- package/src/channels/plugin-channel-declarations.ts +108 -0
- package/src/channels/types.ts +30 -0
- package/src/cli/AGENTS.md +5 -2
- package/src/cli/commands/credentials.help.ts +2 -2
- package/src/cli/commands/inference-providers.ts +1 -1
- package/src/cli/commands/mcp.help.ts +13 -4
- package/src/cli/commands/mcp.ts +9 -0
- package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
- package/src/cli/commands/memory/index.help.ts +43 -1
- package/src/cli/commands/memory/memory-v3.ts +64 -0
- package/src/cli/commands/stt.help.ts +27 -2
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/upgrade-plugin.ts +42 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
- package/src/config/__tests__/default-provider.test.ts +6 -1
- package/src/config/__tests__/profile-materialization.test.ts +75 -19
- package/src/config/bundled-skills/acp/SKILL.md +6 -7
- package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
- package/src/config/call-site-defaults.ts +4 -5
- package/src/config/default-profile-catalog.ts +82 -11
- package/src/config/default-profile-names.ts +4 -1
- package/src/config/default-provider-resolution.ts +4 -0
- package/src/config/llm-context-resolution.ts +11 -3
- package/src/config/llm-resolver.ts +28 -1
- package/src/config/profile-materialization.ts +70 -22
- package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
- package/src/config/schemas/call-site-catalog.ts +4 -4
- package/src/config/schemas/live-voice.ts +57 -23
- package/src/config/schemas/llm.ts +59 -32
- package/src/config/schemas/mcp.ts +23 -0
- package/src/config/schemas/plugin-updates.ts +6 -2
- package/src/config/schemas/stt.ts +1 -0
- package/src/context/outbound-sanitize.ts +96 -1
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
- package/src/daemon/conversation-agent-loop.ts +17 -6
- package/src/daemon/conversation-messaging.ts +5 -1
- package/src/daemon/conversation-notifiers.ts +9 -1
- package/src/daemon/conversation-process.ts +9 -6
- package/src/daemon/conversation-runtime-assembly.ts +3 -4
- package/src/daemon/conversation-tool-setup.ts +1 -2
- package/src/daemon/conversation.ts +48 -0
- package/src/daemon/mcp-reload-service.ts +36 -6
- package/src/daemon/process-message.ts +13 -3
- package/src/daemon/providers-setup.ts +6 -3
- package/src/daemon/trust-context-types.ts +29 -0
- package/src/daemon/wake-conversation-ops.ts +3 -2
- package/src/documents/document-store.ts +138 -5
- package/src/hooks/hook-loader.ts +3 -3
- package/src/hooks/registry.ts +50 -6
- package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
- package/src/inbound/oauth-callback-url.ts +61 -0
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
- package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
- package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
- package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
- package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
- package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
- package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
- package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
- package/src/live-voice/live-voice-archive.ts +2 -0
- package/src/live-voice/live-voice-metrics.ts +57 -32
- package/src/live-voice/live-voice-photo.ts +1 -2
- package/src/live-voice/live-voice-session.ts +535 -314
- package/src/live-voice/progress-narration.ts +277 -0
- package/src/live-voice/protocol.ts +21 -1
- package/src/mcp/__tests__/effective-config.test.ts +238 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
- package/src/mcp/client.ts +16 -11
- package/src/mcp/effective-config.ts +113 -0
- package/src/mcp/manager.ts +11 -6
- package/src/mcp/mcp-auth-orchestrator.ts +13 -22
- package/src/mcp/mcp-oauth-provider.ts +205 -240
- package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
- package/src/monitoring/plugin-auto-update.ts +128 -24
- package/src/notifications/signal.ts +1 -0
- package/src/permissions/confirmation-guardian-request.test.ts +15 -11
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/question-guardian-request.test.ts +14 -6
- package/src/permissions/question-guardian-request.ts +1 -2
- package/src/persistence/attachments-store.ts +8 -1
- package/src/persistence/bookmark-crud.ts +3 -7
- package/src/persistence/conversation-attention-store.ts +16 -45
- package/src/persistence/conversation-crud.ts +33 -4
- package/src/persistence/conversation-lineage.ts +9 -0
- package/src/persistence/conversation-queries.ts +108 -41
- package/src/persistence/delivery-crud.ts +38 -29
- package/src/persistence/external-conversation-store.ts +32 -4
- package/src/persistence/llm-request-log-store.ts +4 -10
- package/src/persistence/llm-usage-store.ts +8 -3
- package/src/persistence/message-reads.test.ts +197 -0
- package/src/persistence/message-reads.ts +211 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
- package/src/persistence/real-user-turn-filter.ts +27 -3
- package/src/persistence/steps.ts +9 -0
- package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
- package/src/plugin-api/conversation-turn.ts +168 -5
- package/src/plugin-api/index.ts +21 -5
- package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
- package/src/plugins/defaults/memory/AGENTS.md +4 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
- package/src/plugins/defaults/memory/buffer-format.ts +165 -0
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
- package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
- package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
- package/src/plugins/defaults/memory/indexer.ts +3 -1
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
- package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
- package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
- package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
- package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
- package/src/plugins/defaults/memory/v3/prune.ts +2 -0
- package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
- package/src/plugins/defaults/memory/worker.ts +6 -3
- package/src/plugins/external-plugin-loader.ts +47 -0
- package/src/plugins/mcp-servers.ts +361 -0
- package/src/plugins/mtime-cache.ts +23 -49
- package/src/plugins/worker-plugin-surface.ts +33 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
- package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
- package/src/providers/__tests__/retry-callsite.test.ts +5 -2
- package/src/providers/call-site-routing.ts +30 -3
- package/src/providers/connection-resolution.ts +194 -11
- package/src/providers/inference/auth.ts +6 -0
- package/src/providers/inference/connection-availability.ts +24 -2
- package/src/providers/inference/connections.ts +2 -0
- package/src/providers/model-intents.ts +26 -6
- package/src/providers/openai/codex-models.ts +2 -1
- package/src/providers/provider-send-message.ts +32 -3
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
- package/src/providers/speech-to-text/provider-catalog.ts +99 -8
- package/src/providers/speech-to-text/resolve.ts +25 -2
- package/src/routes/worker.ts +17 -5
- package/src/runtime/access-request-helper.ts +9 -12
- package/src/runtime/agent-wake.ts +3 -3
- package/src/runtime/pre-first-message-gate.ts +4 -0
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
- package/src/runtime/routes/channel-availability-routes.ts +32 -14
- package/src/runtime/routes/channel-route-shared.ts +0 -6
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
- package/src/runtime/routes/conversation-list-routes.ts +112 -1
- package/src/runtime/routes/conversation-query-routes.ts +40 -27
- package/src/runtime/routes/credential-prompt-routes.ts +4 -7
- package/src/runtime/routes/default-provider-routes.ts +15 -0
- package/src/runtime/routes/inbound-message-handler.ts +17 -41
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
- package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
- package/src/runtime/routes/inference-profiles-routes.ts +20 -11
- package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/mcp-auth-routes.ts +148 -57
- package/src/runtime/routes/plugins-routes.ts +21 -3
- package/src/runtime/routes/stt-routes.ts +31 -25
- package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
- package/src/runtime/routes/user-route-dispatcher.ts +39 -14
- package/src/runtime/routes/user-route-import.ts +108 -0
- package/src/schedule/worker.ts +6 -0
- package/src/security/oauth2.ts +6 -22
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
- package/src/stt/__tests__/types.test.ts +94 -0
- package/src/stt/daemon-batch-transcriber.ts +10 -0
- package/src/stt/stt-stream-session.ts +8 -4
- package/src/stt/types.ts +103 -0
- package/src/subagent/manager.ts +1 -3
- package/src/subagent/types.ts +7 -6
- package/src/tools/acp/spawn.test.ts +97 -0
- package/src/tools/acp/spawn.ts +32 -0
- package/src/tools/document/document-tool.ts +12 -3
- package/src/tools/registry.ts +2 -1
- package/src/tools/workflows/run-workflow.ts +1 -2
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +63 -0
- package/src/tts/reasoning-tag-filter.ts +110 -0
- package/src/workspace/byok-default-profile-ensure.ts +61 -18
- package/src/workspace/custom-profile-ensure.ts +4 -24
- package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
- package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
- package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
- package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/provider-commit-message-generator.ts +7 -5
- package/src/live-voice/__tests__/front-decision.test.ts +0 -645
- package/src/live-voice/front-decision.ts +0 -476
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
4
4
|
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
@@ -574,4 +574,65 @@ describe("plugin routes", () => {
|
|
|
574
574
|
);
|
|
575
575
|
expect(other.status).toBe(404);
|
|
576
576
|
});
|
|
577
|
+
|
|
578
|
+
test("a new route export is not bound to a stale helper after upgrade", async () => {
|
|
579
|
+
// Reproduces the browser plugin 500: `/frame` loaded `src/http.ts`
|
|
580
|
+
// first, then an upgrade added `requireNumber` to http.ts and a new
|
|
581
|
+
// `/input` that imports it. Cache-busting only the entry file rebound
|
|
582
|
+
// input.ts to the cached http.ts, which had no such export.
|
|
583
|
+
const plugin = "stale-helper";
|
|
584
|
+
const helperPath = join(getWorkspacePluginsDir(), plugin, "src", "http.ts");
|
|
585
|
+
mkdirSync(dirname(helperPath), { recursive: true });
|
|
586
|
+
writeFileSync(
|
|
587
|
+
helperPath,
|
|
588
|
+
`export function label() { return "v1"; }\n`,
|
|
589
|
+
);
|
|
590
|
+
writePluginHandler(
|
|
591
|
+
plugin,
|
|
592
|
+
"frame.ts",
|
|
593
|
+
`import { label } from "../src/http.js";
|
|
594
|
+
export function GET() { return Response.json({ label: label() }); }`,
|
|
595
|
+
);
|
|
596
|
+
writePluginHandler(
|
|
597
|
+
plugin,
|
|
598
|
+
"input.ts",
|
|
599
|
+
`import { label } from "../src/http.js";
|
|
600
|
+
export function POST() { return Response.json({ label: label() }); }`,
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
const dispatcher = makeDispatcher();
|
|
604
|
+
const frame1 = await dispatcher.dispatch(
|
|
605
|
+
`plugins/${plugin}/frame`,
|
|
606
|
+
makeRequest("GET"),
|
|
607
|
+
);
|
|
608
|
+
expect(frame1.status).toBe(200);
|
|
609
|
+
expect((await frame1.json()).label).toBe("v1");
|
|
610
|
+
|
|
611
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
612
|
+
writeFileSync(
|
|
613
|
+
helperPath,
|
|
614
|
+
`export function label() { return "v2"; }
|
|
615
|
+
export function extra() { return true; }\n`,
|
|
616
|
+
);
|
|
617
|
+
writePluginHandler(
|
|
618
|
+
plugin,
|
|
619
|
+
"input.ts",
|
|
620
|
+
`import { extra } from "../src/http.js";
|
|
621
|
+
export function POST() { return Response.json({ extra: extra() }); }`,
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
const input = await dispatcher.dispatch(
|
|
625
|
+
`plugins/${plugin}/input`,
|
|
626
|
+
makeRequest("POST"),
|
|
627
|
+
);
|
|
628
|
+
expect(input.status).toBe(200);
|
|
629
|
+
expect((await input.json()).extra).toBe(true);
|
|
630
|
+
|
|
631
|
+
const frame2 = await dispatcher.dispatch(
|
|
632
|
+
`plugins/${plugin}/frame`,
|
|
633
|
+
makeRequest("GET"),
|
|
634
|
+
);
|
|
635
|
+
expect(frame2.status).toBe(200);
|
|
636
|
+
expect((await frame2.json()).label).toBe("v2");
|
|
637
|
+
});
|
|
577
638
|
});
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* GET /v1/channels/available — return the channels this assistant can
|
|
5
5
|
* surface to clients (Contacts / GuardianChannels views, etc.) along with
|
|
6
6
|
* their display metadata (label, subtitle, icon, verification capability,
|
|
7
|
-
* setup-message copy).
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* switches.
|
|
7
|
+
* setup-message copy). A fixed base list, plus `email` when an inbox is
|
|
8
|
+
* registered, plus the channels installed plugins bring by declaring
|
|
9
|
+
* ingress. One list, because a plugin channel is a channel; each row's
|
|
10
|
+
* `source` says who contributes it. Clients should treat the response as
|
|
11
|
+
* authoritative and stop carrying their own per-channel switches.
|
|
12
12
|
*
|
|
13
13
|
* Distinct from `/v1/channels/readiness` (which answers "is this channel
|
|
14
14
|
* configured and working?"). Availability answers "could this channel be
|
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
import { z } from "zod";
|
|
19
19
|
|
|
20
20
|
import { isA2AEnabled } from "../../a2a/feature-gate.js";
|
|
21
|
+
import { discoverPluginChannels } from "../../channels/plugin-channel-declarations.js";
|
|
21
22
|
import {
|
|
22
|
-
|
|
23
|
+
type AvailableChannel,
|
|
23
24
|
CHANNEL_METADATA,
|
|
24
25
|
type ChannelId,
|
|
25
26
|
type ChannelInfo,
|
|
@@ -73,7 +74,7 @@ async function hasRegisteredInbox(): Promise<boolean> {
|
|
|
73
74
|
|
|
74
75
|
async function handleGetChannelAvailability(
|
|
75
76
|
_args: RouteHandlerArgs,
|
|
76
|
-
): Promise<{ channels:
|
|
77
|
+
): Promise<{ channels: AvailableChannel[] }> {
|
|
77
78
|
const ids: ChannelId[] = [...BASE_AVAILABLE_CHANNELS];
|
|
78
79
|
if (await hasRegisteredInbox()) {
|
|
79
80
|
ids.push("email");
|
|
@@ -86,14 +87,26 @@ async function handleGetChannelAvailability(
|
|
|
86
87
|
// contains channels that BASE_AVAILABLE_CHANNELS / the email branch
|
|
87
88
|
// explicitly chose, so the lookup is always defined — filter to satisfy
|
|
88
89
|
// the type system without a non-null assertion.
|
|
89
|
-
const
|
|
90
|
+
const builtIn: AvailableChannel[] = ids
|
|
90
91
|
.map((id) => CHANNEL_METADATA[id])
|
|
91
|
-
.filter((info): info is ChannelInfo => info !== undefined)
|
|
92
|
-
|
|
92
|
+
.filter((info): info is ChannelInfo => info !== undefined)
|
|
93
|
+
.map((info) => ({ ...info, source: "default" as const }));
|
|
94
|
+
// One list, because a plugin channel is a channel: what differs is who
|
|
95
|
+
// contributes it, which is what `source` says. Built-ins lead, so a client
|
|
96
|
+
// rendering in order gets a stable list that plugin installs append to.
|
|
97
|
+
return { channels: [...builtIn, ...(await discoverPluginChannels())] };
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
const channelInfoSchema = z.object({
|
|
96
|
-
|
|
101
|
+
// A string rather than the channel enum: a plugin channel's id is its
|
|
102
|
+
// plugin name, and `source` is what says which kind a row is.
|
|
103
|
+
id: z.string(),
|
|
104
|
+
source: z
|
|
105
|
+
.string()
|
|
106
|
+
.describe(
|
|
107
|
+
"`default` for a channel the assistant ships, `plugin:<name>` for one " +
|
|
108
|
+
"an installed plugin brings",
|
|
109
|
+
),
|
|
97
110
|
label: z.string(),
|
|
98
111
|
subtitle: z.string(),
|
|
99
112
|
icon: z.string(),
|
|
@@ -117,14 +130,19 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
117
130
|
description:
|
|
118
131
|
"Return the channels this assistant can surface to clients, with " +
|
|
119
132
|
"display metadata (label, icon, verification capability, setup " +
|
|
120
|
-
"copy).
|
|
121
|
-
"
|
|
133
|
+
"copy). A fixed base list plus `email` when an inbox is registered, " +
|
|
134
|
+
"plus the channels installed plugins bring by declaring ingress " +
|
|
135
|
+
"routes. Each carries a `source` saying which of those it is.",
|
|
122
136
|
tags: ["channels"],
|
|
123
137
|
handler: handleGetChannelAvailability,
|
|
124
138
|
responseBody: z.object({
|
|
125
139
|
channels: z
|
|
126
140
|
.array(channelInfoSchema)
|
|
127
|
-
.describe(
|
|
141
|
+
.describe(
|
|
142
|
+
"Available channels in display order: the ones the assistant " +
|
|
143
|
+
"ships, then the ones installed plugins bring by declaring " +
|
|
144
|
+
"ingress routes. `source` distinguishes them.",
|
|
145
|
+
),
|
|
128
146
|
}),
|
|
129
147
|
},
|
|
130
148
|
];
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* Shared types, constants, and utilities used across channel route modules.
|
|
3
3
|
*/
|
|
4
4
|
import type { ChannelId } from "../../channels/types.js";
|
|
5
|
-
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
|
|
6
5
|
import {
|
|
7
6
|
type ApprovalAction,
|
|
8
7
|
type ApprovalDecisionResult,
|
|
@@ -10,11 +9,6 @@ import {
|
|
|
10
9
|
isApprovalAction,
|
|
11
10
|
} from "../channel-approval-types.js";
|
|
12
11
|
|
|
13
|
-
/** Canonicalize assistantId for channel ingress paths. */
|
|
14
|
-
export function canonicalChannelAssistantId(_assistantId: string): string {
|
|
15
|
-
return DAEMON_INTERNAL_ASSISTANT_ID;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
12
|
// ---------------------------------------------------------------------------
|
|
19
13
|
// Actor role
|
|
20
14
|
// ---------------------------------------------------------------------------
|
|
@@ -159,13 +159,13 @@ async function handleExchange(args: RouteHandlerArgs) {
|
|
|
159
159
|
|
|
160
160
|
const existing = getConnection(db, CONNECTION_NAME);
|
|
161
161
|
if (existing) {
|
|
162
|
-
// Stamp the provider with the auth: this row
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
// otherwise
|
|
162
|
+
// Stamp the provider with the auth: this row IS the subscription route
|
|
163
|
+
// (auth modality = provider identity), and dispatch derives the openai
|
|
164
|
+
// upstream from the identity per-request. Stamping also heals a
|
|
165
|
+
// claiming row whose provider would otherwise misroute the fresh token.
|
|
166
166
|
const updateResult = updateConnection(db, CONNECTION_NAME, {
|
|
167
167
|
auth: authInput,
|
|
168
|
-
provider: "
|
|
168
|
+
provider: "chatgpt",
|
|
169
169
|
});
|
|
170
170
|
if (!updateResult.ok) {
|
|
171
171
|
log.error(
|
|
@@ -177,7 +177,7 @@ async function handleExchange(args: RouteHandlerArgs) {
|
|
|
177
177
|
} else {
|
|
178
178
|
const createResult = createConnection(db, {
|
|
179
179
|
name: CONNECTION_NAME,
|
|
180
|
-
provider: "
|
|
180
|
+
provider: "chatgpt",
|
|
181
181
|
auth: authInput,
|
|
182
182
|
});
|
|
183
183
|
if (!createResult.ok) {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* GET /v1/conversations — paginated conversation list
|
|
5
5
|
* GET /v1/conversations/unread-count - count of unseen foreground conversations
|
|
6
|
+
* GET /v1/conversations/sections - per-section totals for the sidebar index
|
|
6
7
|
* POST /v1/conversations/seen — record a seen signal (single)
|
|
7
8
|
* POST /v1/conversations/seen/bulk — record seen signals (batch)
|
|
8
9
|
* POST /v1/conversations/unread — mark a conversation unread
|
|
@@ -29,6 +30,7 @@ import { resolveConversationId } from "../../persistence/conversation-key-store.
|
|
|
29
30
|
import {
|
|
30
31
|
type ConversationListFilter,
|
|
31
32
|
countConversations,
|
|
33
|
+
countConversationSections,
|
|
32
34
|
countUnreadConversations,
|
|
33
35
|
listConversations,
|
|
34
36
|
listPinnedConversations,
|
|
@@ -155,6 +157,42 @@ const unreadConversationCountResponseSchema = z.object({
|
|
|
155
157
|
count: z.number(),
|
|
156
158
|
});
|
|
157
159
|
|
|
160
|
+
const sectionCountFields = {
|
|
161
|
+
total: z.number(),
|
|
162
|
+
unread: z.number(),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* One renderable sidebar section. Discriminated by `kind` so clients get a
|
|
167
|
+
* typed union: group sections carry their metadata inline (one consistent
|
|
168
|
+
* snapshot, no join against `GET /v1/groups` that could disagree with it),
|
|
169
|
+
* channel sections carry only the id (labels are client-side i18n), and
|
|
170
|
+
* `chats` is the ungrouped-native bucket. In a view without channel sections
|
|
171
|
+
* the Chats card holds every ungrouped row: the buckets are disjoint, so
|
|
172
|
+
* that reading is `chats` plus the sum of `channel` entries.
|
|
173
|
+
*/
|
|
174
|
+
const conversationSectionSchema = z.discriminatedUnion("kind", [
|
|
175
|
+
z.object({ kind: z.literal("pinned"), ...sectionCountFields }),
|
|
176
|
+
z.object({
|
|
177
|
+
kind: z.literal("group"),
|
|
178
|
+
groupId: z.string(),
|
|
179
|
+
name: z.string(),
|
|
180
|
+
icon: z.string().nullable(),
|
|
181
|
+
sortPosition: z.number(),
|
|
182
|
+
...sectionCountFields,
|
|
183
|
+
}),
|
|
184
|
+
z.object({
|
|
185
|
+
kind: z.literal("channel"),
|
|
186
|
+
channelId: z.string(),
|
|
187
|
+
...sectionCountFields,
|
|
188
|
+
}),
|
|
189
|
+
z.object({ kind: z.literal("chats"), ...sectionCountFields }),
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
const conversationSectionsResponseSchema = z.object({
|
|
193
|
+
sections: z.array(conversationSectionSchema),
|
|
194
|
+
});
|
|
195
|
+
|
|
158
196
|
// ---------------------------------------------------------------------------
|
|
159
197
|
// Helpers
|
|
160
198
|
// ---------------------------------------------------------------------------
|
|
@@ -292,6 +330,64 @@ function handleGetUnreadConversationCount() {
|
|
|
292
330
|
return { count: countUnreadConversations() };
|
|
293
331
|
}
|
|
294
332
|
|
|
333
|
+
/**
|
|
334
|
+
* One row per non-empty sidebar section, no conversation rows. What this
|
|
335
|
+
* exists to answer without a list drain: which sections render at all,
|
|
336
|
+
* and what their rail badges say.
|
|
337
|
+
*
|
|
338
|
+
* A count row whose `group_id` names no existing non-system group is
|
|
339
|
+
* skipped rather than surfaced: those rows are unreachable as a section
|
|
340
|
+
* (the sidebar builds group sections from the groups table), and a
|
|
341
|
+
* dangling id is a transient state around group deletion, not a section.
|
|
342
|
+
*/
|
|
343
|
+
function handleGetConversationSections() {
|
|
344
|
+
const counts = countConversationSections();
|
|
345
|
+
const groupsById = new Map(listGroups().map((g) => [g.id, g]));
|
|
346
|
+
const sections: Array<z.infer<typeof conversationSectionSchema>> = [];
|
|
347
|
+
|
|
348
|
+
for (const row of counts.groups) {
|
|
349
|
+
if (row.groupId === "system:pinned") {
|
|
350
|
+
sections.push({ kind: "pinned", total: row.total, unread: row.unread });
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const meta = groupsById.get(row.groupId);
|
|
354
|
+
if (!meta || meta.isSystemGroup) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
sections.push({
|
|
358
|
+
kind: "group",
|
|
359
|
+
groupId: row.groupId,
|
|
360
|
+
name: meta.name,
|
|
361
|
+
icon: meta.icon,
|
|
362
|
+
sortPosition: meta.sortPosition,
|
|
363
|
+
total: row.total,
|
|
364
|
+
unread: row.unread,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for (const row of counts.channels) {
|
|
369
|
+
if (row.channel === "vellum") {
|
|
370
|
+
sections.push({ kind: "chats", total: row.total, unread: row.unread });
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
sections.push({
|
|
374
|
+
kind: "channel",
|
|
375
|
+
channelId: row.channel,
|
|
376
|
+
total: row.total,
|
|
377
|
+
unread: row.unread,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/* Every other section exists only when it has rows; Chats is the leftover
|
|
382
|
+
bucket and renders regardless, so its counts are part of the contract
|
|
383
|
+
even at zero. */
|
|
384
|
+
if (!sections.some((s) => s.kind === "chats")) {
|
|
385
|
+
sections.push({ kind: "chats", total: 0, unread: 0 });
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return { sections };
|
|
389
|
+
}
|
|
390
|
+
|
|
295
391
|
function handleRecordSeen({ body = {}, headers }: RouteHandlerArgs) {
|
|
296
392
|
const rawConversationId = body.conversationId as string | undefined;
|
|
297
393
|
if (!rawConversationId) {
|
|
@@ -482,7 +578,7 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
482
578
|
type: "string",
|
|
483
579
|
required: false,
|
|
484
580
|
description:
|
|
485
|
-
'Filter to a single group, so each sidebar section can load independently of the paginated list. Pass "system:all" for conversations in no group, "system:pinned" for the Pinned section, or a custom group id. A group-scoped request is ordered
|
|
581
|
+
'Filter to a single group, so each sidebar section can load independently of the paginated list. Pass "system:all" for conversations in no group, "system:pinned" for the Pinned section, or a custom group id. A group-scoped request is recency-ordered like every list read (COALESCE(last_message_at, updated_at) descending) and never has pinned rows appended to it. Omit to span every group.',
|
|
486
582
|
},
|
|
487
583
|
],
|
|
488
584
|
responseBody: listConversationsResponseSchema,
|
|
@@ -503,6 +599,21 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
503
599
|
responseBody: unreadConversationCountResponseSchema,
|
|
504
600
|
handler: handleGetUnreadConversationCount,
|
|
505
601
|
},
|
|
602
|
+
{
|
|
603
|
+
operationId: "getConversationSections",
|
|
604
|
+
endpoint: "conversations/sections",
|
|
605
|
+
method: "GET",
|
|
606
|
+
policy: {
|
|
607
|
+
requiredScopes: ["chat.read"],
|
|
608
|
+
allowedPrincipalTypes: ACTOR_PRINCIPALS,
|
|
609
|
+
},
|
|
610
|
+
summary: "Sidebar section index",
|
|
611
|
+
description:
|
|
612
|
+
"One row per renderable sidebar section (Pinned, each non-empty custom group, each origin channel with unassigned conversations, Chats) with total and unread counts and no conversation rows. Lets a client know which sections exist, and what their badges say, without fetching any conversation list. Totals follow the standard listing visibility; unread applies the same rules as GET /v1/conversations/unread-count scoped to the section. Chats is always present, even at zero: it is the leftover bucket and renders regardless.",
|
|
613
|
+
tags: ["conversations"],
|
|
614
|
+
responseBody: conversationSectionsResponseSchema,
|
|
615
|
+
handler: handleGetConversationSections,
|
|
616
|
+
},
|
|
506
617
|
{
|
|
507
618
|
operationId: "recordConversationSeen",
|
|
508
619
|
endpoint: "conversations/seen",
|
|
@@ -41,7 +41,10 @@ import {
|
|
|
41
41
|
withSuppressedConfigDiskWrites,
|
|
42
42
|
withSuppressedConfigDiskWritesSync,
|
|
43
43
|
} from "../../config/loader.js";
|
|
44
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
completeCustomProfile,
|
|
46
|
+
mergePreservingUnknownKeys,
|
|
47
|
+
} from "../../config/profile-materialization.js";
|
|
45
48
|
import { AssistantConfigSchema } from "../../config/schema.js";
|
|
46
49
|
import { getSchemaAtPath } from "../../config/schema-utils.js";
|
|
47
50
|
import {
|
|
@@ -50,6 +53,7 @@ import {
|
|
|
50
53
|
LLMConfigFragment,
|
|
51
54
|
ProfileEntry,
|
|
52
55
|
routingIdentityModelIssue,
|
|
56
|
+
unknownLlmProviderIssue,
|
|
53
57
|
} from "../../config/schemas/llm.js";
|
|
54
58
|
import { VALID_MEMORY_EMBEDDING_PROVIDERS } from "../../config/schemas/memory-storage.js";
|
|
55
59
|
import { ServiceModeSchema } from "../../config/schemas/services.js";
|
|
@@ -96,6 +100,7 @@ import { type LogRow } from "../../persistence/llm-request-log-store.js";
|
|
|
96
100
|
import { getMemoryRecallLogByMessageIds } from "../../plugins/defaults/memory/memory-recall-log-store.js";
|
|
97
101
|
import { getMemoryV2ActivationLogByMessageIds } from "../../plugins/defaults/memory/v2/activation-log-store.js";
|
|
98
102
|
import { getMemoryV3SelectionForInspectorByMessageIds } from "../../plugins/defaults/memory/v3/selection-log-store.js";
|
|
103
|
+
import { writableProfileProviderIssue } from "../../providers/connection-resolution.js";
|
|
99
104
|
import { ROUTING_IDENTITY_PROVIDERS } from "../../providers/inference/auth.js";
|
|
100
105
|
import { PROVIDERS_REQUIRING_BASE_URL_AND_MODELS } from "../../providers/inference/auth.js";
|
|
101
106
|
import {
|
|
@@ -1252,30 +1257,6 @@ function assertInvariantProfilesPreserved(
|
|
|
1252
1257
|
}
|
|
1253
1258
|
}
|
|
1254
1259
|
|
|
1255
|
-
/**
|
|
1256
|
-
* `{...raw, ...completed}` recursively: completed (schema-known) values win,
|
|
1257
|
-
* raw keys the schema stripped survive at every depth.
|
|
1258
|
-
*/
|
|
1259
|
-
function mergePreservingUnknownKeys(
|
|
1260
|
-
raw: Record<string, unknown>,
|
|
1261
|
-
completed: Record<string, unknown>,
|
|
1262
|
-
): Record<string, unknown> {
|
|
1263
|
-
const out: Record<string, unknown> = { ...raw, ...completed };
|
|
1264
|
-
for (const [key, value] of Object.entries(completed)) {
|
|
1265
|
-
const rawValue = raw[key];
|
|
1266
|
-
if (
|
|
1267
|
-
readPlainObject(value) !== undefined &&
|
|
1268
|
-
readPlainObject(rawValue) !== undefined
|
|
1269
|
-
) {
|
|
1270
|
-
out[key] = mergePreservingUnknownKeys(
|
|
1271
|
-
rawValue as Record<string, unknown>,
|
|
1272
|
-
value as Record<string, unknown>,
|
|
1273
|
-
);
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
return out;
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
1260
|
/**
|
|
1280
1261
|
* Custom profiles persist as complete overrides: any user-source, non-mix
|
|
1281
1262
|
* profile entry this write creates or changes is completed against the
|
|
@@ -1343,7 +1324,9 @@ function completeChangedCustomProfiles(
|
|
|
1343
1324
|
* would let the pair reach disk and be stripped on the next read — a silent
|
|
1344
1325
|
* no-op where the user expects a saved override.
|
|
1345
1326
|
*/
|
|
1346
|
-
function
|
|
1327
|
+
function collectProviderEntries(
|
|
1328
|
+
raw: Record<string, unknown>,
|
|
1329
|
+
): [string, { provider?: unknown; model?: unknown }][] {
|
|
1347
1330
|
const llm = raw.llm as
|
|
1348
1331
|
| {
|
|
1349
1332
|
default?: { provider?: unknown; model?: unknown } | null;
|
|
@@ -1371,10 +1354,40 @@ function assertRoutableIdentityEntries(raw: Record<string, unknown>): void {
|
|
|
1371
1354
|
}
|
|
1372
1355
|
}
|
|
1373
1356
|
}
|
|
1357
|
+
return entries;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function assertRoutableIdentityEntries(
|
|
1361
|
+
preWrite: Record<string, unknown>,
|
|
1362
|
+
raw: Record<string, unknown>,
|
|
1363
|
+
): void {
|
|
1364
|
+
const entries = collectProviderEntries(raw);
|
|
1365
|
+
const priorProviders = new Map(
|
|
1366
|
+
collectProviderEntries(preWrite).flatMap(([label, entry]) =>
|
|
1367
|
+
typeof entry.provider === "string" ? [[label, entry.provider]] : [],
|
|
1368
|
+
),
|
|
1369
|
+
);
|
|
1374
1370
|
for (const [label, entry] of entries) {
|
|
1375
1371
|
if (typeof entry.provider !== "string") {
|
|
1376
1372
|
continue;
|
|
1377
1373
|
}
|
|
1374
|
+
// The provider schema is an open string (a stored value outside the
|
|
1375
|
+
// known set parses instead of stripping its profile), so write-time
|
|
1376
|
+
// membership is enforced here and at the profiles route. Scoped to
|
|
1377
|
+
// providers this write introduces or changes: a stored value outside
|
|
1378
|
+
// the known set is readable and dispatchable by design, and
|
|
1379
|
+
// re-validating it on every write would make all later settings saves
|
|
1380
|
+
// fail with no in-product repair path. Profiles accept entry names;
|
|
1381
|
+
// call-site fragments and the legacy default blob stay vendor-only
|
|
1382
|
+
// (overrides become model-only with the entries demolition).
|
|
1383
|
+
if (entry.provider !== priorProviders.get(label)) {
|
|
1384
|
+
const providerIssue = label.startsWith("llm.profiles.")
|
|
1385
|
+
? writableProfileProviderIssue(entry.provider)
|
|
1386
|
+
: unknownLlmProviderIssue(entry.provider);
|
|
1387
|
+
if (providerIssue) {
|
|
1388
|
+
throw new BadRequestError(`${providerIssue} (${label})`);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1378
1391
|
const issue = routingIdentityModelIssue(
|
|
1379
1392
|
entry.provider,
|
|
1380
1393
|
typeof entry.model === "string" ? entry.model : undefined,
|
|
@@ -1396,7 +1409,7 @@ export async function commitConfigWrite(
|
|
|
1396
1409
|
const preWrite = loadRawConfig();
|
|
1397
1410
|
completeChangedCustomProfiles(preWrite, raw);
|
|
1398
1411
|
assertInvariantProfilesPreserved(preWrite, raw);
|
|
1399
|
-
assertRoutableIdentityEntries(raw);
|
|
1412
|
+
assertRoutableIdentityEntries(preWrite, raw);
|
|
1400
1413
|
|
|
1401
1414
|
// Suppress the file-watcher callback for the duration of the debounce
|
|
1402
1415
|
// window. Without this, the ConfigWatcher detects the config.json write
|
|
@@ -55,14 +55,11 @@ const CredentialPromptParams = z.object({
|
|
|
55
55
|
*
|
|
56
56
|
* The prompt blocks until the user answers, so by the time a caller reads the
|
|
57
57
|
* result the secure input is already gone from the UI. A bare `ok: true` reads
|
|
58
|
-
* as "the prompt opened successfully", which
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* that never reappears, and the flow loops. State the close explicitly so the
|
|
62
|
-
* only available reading is the correct one.
|
|
58
|
+
* as "the prompt opened successfully", which invites a follow-up telling the
|
|
59
|
+
* user to paste into a surface that has already closed. Stating the close
|
|
60
|
+
* leaves only the correct reading.
|
|
63
61
|
*/
|
|
64
|
-
const PROMPT_CLOSED_NOTICE =
|
|
65
|
-
"The secure prompt has closed — do not ask the user to enter this value again.";
|
|
62
|
+
const PROMPT_CLOSED_NOTICE = "The secure prompt has closed.";
|
|
66
63
|
|
|
67
64
|
export type CredentialPromptResult = {
|
|
68
65
|
ok: boolean;
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
DEFAULT_PROVIDER_CHOICES,
|
|
26
26
|
DefaultProviderSchema,
|
|
27
27
|
} from "../../config/schemas/llm.js";
|
|
28
|
+
import { ROUTING_IDENTITY_PROVIDERS } from "../../providers/inference/auth.js";
|
|
28
29
|
import {
|
|
29
30
|
computeConnectionAvailability,
|
|
30
31
|
CONNECTION_AVAILABILITY_STATUSES,
|
|
@@ -93,6 +94,20 @@ async function handlePutDefaultProvider({
|
|
|
93
94
|
)}; "connectionName" is optional and must be a non-empty string.`,
|
|
94
95
|
);
|
|
95
96
|
}
|
|
97
|
+
// Routing identities dispatch through their canonical row regardless of
|
|
98
|
+
// any stored pin (`resolveRoutingIdentity`), so a noncanonical
|
|
99
|
+
// connectionName would be judged by availability and the deletion guards
|
|
100
|
+
// while inference uses a different row. Reject it here rather than
|
|
101
|
+
// persisting a pin that status and dispatch disagree about.
|
|
102
|
+
const { provider, connectionName } = result.data;
|
|
103
|
+
if (connectionName != null && ROUTING_IDENTITY_PROVIDERS.has(provider)) {
|
|
104
|
+
const canonical = resolveDefaultConnectionName({ provider });
|
|
105
|
+
if (connectionName !== canonical) {
|
|
106
|
+
throw new BadRequestError(
|
|
107
|
+
`Provider "${provider}" always dispatches through its canonical connection "${canonical}". Omit "connectionName" or pass "${canonical}".`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
96
111
|
setDefaultProvider(result.data);
|
|
97
112
|
return handleGetDefaultProvider();
|
|
98
113
|
}
|