diff --git a/.changeset/anthropic-credential-precedence.md b/.changeset/anthropic-credential-precedence.md new file mode 100644 index 0000000000..102b411022 --- /dev/null +++ b/.changeset/anthropic-credential-precedence.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Choose whether Anthropic lanes use your API key or your Claude subscription, and see which is in use. +category: feature +dev: New global setting `anthropicAuthPreference` ("api-key" | "subscription", default "api-key" — the historical precedence). Read in `resolveAnthropicRuntimeApiKey` (packages/engine/src/auth-storage.ts) straight from `~/.fusion/settings.json`, so it applies without a restart and needs no settings plumbing through `createFusionAuthStorage`. Settings → Authentication renders the control and an "In use" / "Overridden below" marker only when both Anthropic credentials are connected. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index 50318bfd76..fe281f99c4 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -51,6 +51,7 @@ Defaults from `DEFAULT_GLOBAL_SETTINGS`; key scope from `GLOBAL_SETTINGS_KEYS`. | Setting | Type | Default | Description | |---|---|---:|---| +| `anthropicAuthPreference` | `"api-key" \| "subscription"` | `"api-key"` | Which Anthropic credential wins when BOTH a raw API key and a Claude subscription OAuth login are configured. `"api-key"` keeps the historical precedence; `"subscription"` moves the raw key below the OAuth steps so a stale or revoked saved key cannot shadow a working subscription (the `401 invalid x-api-key` failure mode). Global, because credentials live in the global `~/.fusion/agent/auth.json`. Never removes a source — with one credential configured, resolution reaches it under either value. Surfaced in Settings → Authentication, where the two Anthropic cards show which credential is in use; the control appears only when both are connected. | | `themeMode` | `"dark" \| "light" \| "system"` | `"system"` | Dashboard theme mode. Fresh installs follow the operating system light/dark preference until the user chooses Light, Dark, or System. | | `colorTheme` | `ColorTheme` | `"shadcn-ember"` | Dashboard color theme preset. Aurora is a built-in navy/teal/violet palette with a misty light counterpart; Calm is a low-stimulation slate/sage palette with a misty light counterpart; Dawn uses indigo/plum dark surfaces, amber accents, and dawn-white light surfaces. Use `"shadcn-custom"` to show the separate custom shadcn color picker in Settings → Appearance and the Command Center theme card. | | `shadcnCustomColors` | `Record` | `undefined` | Optional shadcn design-token override map for `"shadcn-custom"` only. Keys are CSS token names such as `--accent`, `--bg`, `--surface`, `--card`, `--border`, `--text`, `--text-muted`, workflow status tokens, and `--color-success`/`--color-warning`/`--color-error`; values must be sanitized `#RGB` or `#RRGGBB` hex colors. Missing or invalid entries fall back to the `shadcn-custom` base defaults and are not applied to other themes. | @@ -58,7 +59,7 @@ Defaults from `DEFAULT_GLOBAL_SETTINGS`; key scope from `GLOBAL_SETTINGS_KEYS`. | `dashboardFontScalePct` | `number` | `100` | Dashboard font scale percentage used by Appearance settings. Valid range: `85` to `125`; applied pre-hydration via document root font-size so board typography (column headers/counts, task cards, and quick-entry text) scales with the setting from first paint. | | `dismissModalsOnOutsideClick` | `boolean` | `false` | Global dashboard preference for closing fixed modal overlays by clicking/tapping the backdrop. Off by default to prevent accidental modal dismissal; explicit close, cancel, and Escape paths remain available. | | `skipConfirmationDialogs` | `boolean` | `false` | Global-only operator preference that skips centralized confirmation dialogs for critical actions. When enabled, destructive actions such as deleting a task or resetting progress immediately take the dialog's primary/default action; project settings cannot enable it for shared-project collaborators. | -| `defaultProvider` | `string` | `undefined` | Default AI provider. Anthropic has three independent surfaces, all executing on the direct `anthropic` provider except the CLI: (1) **direct OAuth** — a Claude subscription/OAuth login drives `anthropic/*` selections; Fusion sends the OAuth token to `https://api.anthropic.com/v1` with Claude Code identity headers (the same path the Claude Code CLI uses), so a subscription needs no API key. Credentials live under the `anthropic-subscription` auth/status/usage/banner id but are resolved for the direct provider at runtime; they are never stored or resolved as raw `ANTHROPIC_API_KEY` material. (2) **raw API key** — `ANTHROPIC_API_KEY`, a `models.json` `apiKey`, or an `api_key` auth credential uses `x-api-key` on the same direct provider and takes precedence over OAuth. (3) **Claude CLI** — the explicit `pi-claude-cli` model provider runs sessions through the local `claude` CLI. There is no runtime rerouting between these surfaces. | +| `defaultProvider` | `string` | `undefined` | Default AI provider. Anthropic has three independent surfaces, all executing on the direct `anthropic` provider except the CLI: (1) **direct OAuth** — a Claude subscription/OAuth login drives `anthropic/*` selections; Fusion sends the OAuth token to `https://api.anthropic.com/v1` with Claude Code identity headers (the same path the Claude Code CLI uses), so a subscription needs no API key. Credentials live under the `anthropic-subscription` auth/status/usage/banner id but are resolved for the direct provider at runtime; they are never stored or resolved as raw `ANTHROPIC_API_KEY` material. (2) **raw API key** — `ANTHROPIC_API_KEY`, a `models.json` `apiKey`, or an `api_key` auth credential uses `x-api-key` on the same direct provider and takes precedence over OAuth by default (see [`anthropicAuthPreference`](#anthropicauthpreference) to invert this). (3) **Claude CLI** — the explicit `pi-claude-cli` model provider runs sessions through the local `claude` CLI. There is no runtime rerouting between these surfaces. | | `defaultModelId` | `string` | `undefined` | Default AI model ID. | | `modelPricingOverrides` | `Record` | `undefined` | Optional global Command Center pricing overrides keyed by lowercased `provider:model` or bare `:model`. Values store USD per 1M input, output, cache-read, and cache-write tokens plus optional `source`; they override the built-in pricing table for cost estimates only and are editable from Settings → Global Models → View pricing table. | | `modelPricingFetchedAt` | `string` | `undefined` | ISO timestamp for the last successful one-click pricing refresh from the Settings → Global Models pricing summary. | @@ -851,7 +852,7 @@ Anthropic has three independent authentication/routing paths: - **Anthropic Subscription** (`anthropic-subscription`) is Claude subscription OAuth. It powers login/logout, `/api/auth/status`, usage/subscription checks through `https://api.anthropic.com/api/oauth/usage`, and the OAuth re-login banner when no authenticated Anthropic API-key/CLI fallback is present. Legacy `anthropic` OAuth rows are treated as this subscription surface. It is **also an execution surface**: subscription OAuth resolves for the direct `anthropic` provider at runtime, so `anthropic/*` selections run on `https://api.anthropic.com/v1` with Claude Code identity headers (Bearer OAuth, no API key required), and `anthropic/*` rows appear in the model picker when subscription OAuth is connected. - **Claude CLI** (`pi-claude-cli`) is the CLI-backed execution provider. Use it when you want sessions to run through the local `claude` CLI; CLI availability does not prove the subscription OAuth status is valid. It is a separate, explicit choice — subscription OAuth does not require or reroute to it. When Claude CLI is enabled, model selectors show the registered `pi-claude-cli/*` rows (for example `pi-claude-cli/claude-sonnet-5`) in addition to the direct `anthropic/*` rows. -- **Anthropic API Key** (`anthropic` direct `/v1`) is raw API-key auth. It accepts `ANTHROPIC_API_KEY`, a `models.json` `apiKey`, or an `api_key` auth credential, uses `x-api-key`, and takes precedence over subscription OAuth when both are configured for the direct `https://api.anthropic.com/v1` provider. +- **Anthropic API Key** (`anthropic` direct `/v1`) is raw API-key auth. It accepts `ANTHROPIC_API_KEY`, a `models.json` `apiKey`, or an `api_key` auth credential, uses `x-api-key`, and by default takes precedence over subscription OAuth when both are configured for the direct `https://api.anthropic.com/v1` provider. That default is operator-selectable: set the global [`anthropicAuthPreference`](#anthropicauthpreference) to `"subscription"` to make the Claude subscription login win instead — useful when a stale or revoked saved key would otherwise shadow a working subscription and fail with `401 invalid x-api-key`. Anthropic can be connected with a raw API key from both Model Onboarding and **Settings → Authentication**. Anthropic API-key auth appears as a separate **Anthropic API Key** card, while Claude subscription OAuth appears as **Anthropic Subscription** with Login/Logout controls. On Fusion desktop, Anthropic Subscription OAuth login URLs are delegated to the operating system browser instead of an Electron child window so the existing polling/callback flow can complete. `/api/auth/status` returns only masked key hints for the API-key card. diff --git a/packages/core/src/index.gate.ts b/packages/core/src/index.gate.ts index 978a0e4573..60100f4c01 100644 --- a/packages/core/src/index.gate.ts +++ b/packages/core/src/index.gate.ts @@ -42,8 +42,8 @@ version of this file was falsified: 141/335 gate tests failed on missing etc. pulled in by production modules, not test files). */ -export { COLUMNS, DEFAULT_COLUMN, isColumn, normalizeColumn, normalizeColumnId, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, isMergeRequestContractShadowEnabled, resolvePersistAgentThinkingLog, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, SUPPORTED_LOCALES, DEFAULT_LOCALE, isLocale, AGENT_PERMISSIONS, PERMANENT_AGENT_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PROVISIONING_APPROVAL_MODES, SANDBOX_PROVISIONING_APPROVAL_MODES, AGENT_PERMISSION_POLICY_PRESET_IDS, LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, APPROVAL_REQUEST_STATUSES, APPROVAL_REQUEST_AUDIT_EVENT_TYPES, normalizeApprovalRequestActionCategory, isValidApprovalRequestTransition, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, DEFAULT_HEARTBEAT_PROCEDURE_PATH, getDefaultHeartbeatProcedurePath, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, PLANNER_OVERSIGHT_LEVELS, DEFAULT_PLANNER_OVERSIGHT_LEVEL, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, WORKFLOW_WORK_ITEM_KINDS, WORKFLOW_WORK_ITEM_STATES, HIGH_FANOUT_BLOCKER_TODO_THRESHOLD, STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS, DASHBOARD_USER_ID, normalizeMessageParticipant, validateMessageMetadata, resolveEphemeralTaskCreationPolicy, validateDockerNodeConfig, sanitizeDockerNodeConfigForResponse, normalizeMergeIntegrationWorktreeMode, normalizeMergeAdvanceAutoSyncMode, DEFAULT_GITLAB_API_BASE_URL, DEFAULT_GITLAB_INSTANCE_URL, resolveGitlabConfig, resolveGitlabEnabled, MERGE_ADVANCE_AUTO_SYNC_MODES, normalizeMergeConflictStrategy, normalizeMergeStrategyOverlapBehavior, normalizePostMergeAuditMode, POST_MERGE_AUDIT_MODES, normalizeMergeAuditAutoRecovery, MERGE_AUDIT_AUTO_RECOVERY_MODES, normalizeMergerMode, MERGER_MODES, normalizeAutoRecovery, AUTO_RECOVERY_MODES, buildResearchDocumentKey, REPO_OVERRIDE_RE, SHARED_STATE_SNAPSHOT_VERSION, sanitizeCliAgentSettings, sanitizeCliAgentsSettings, sanitizeMcpServers, CLI_AGENT_ADAPTER_IDS, CLI_AGENT_AUTONOMY_MODES, isMcpSecretRef, OVERSEER_INTERVENTION_MUTATION } from "./types.js"; -export type { Column, ColumnId, IssueInfo, IssueState, TaskSourceIssue, TaskGitLabTracking, TaskGitLabTrackedItem, GitLabTrackedItemKind, PrInfo, PrConflictState, PrConflictDiagnostics, PrCheckState, PrCheckStatus, PrStatus, BranchGroup, BranchGroupCreateInput, BranchGroupUpdate, BranchGroupPrState, Task, TaskTokenUsage, TaskTokenUsagePerModel, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, ArchivedTaskDocumentAdditionInput, ArchivedTaskDocumentAdditionResult, TaskDocumentWithTask, ArtifactType, Artifact, ArtifactCreateInput, ArtifactWithTask, NativeStructureRef, NativeStructureOpenTarget, NativeStructurePreviewPayload, NativeStructureUnavailablePayload, NativeStructurePreviewResult, TaskCreateInput, TaskSource, SourceType, TaskDetail, RetrySummary, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, DistributedTaskIdReserveInput, DistributedTaskIdReserveResult, DistributedTaskIdCommitInput, DistributedTaskIdCommitResult, DistributedTaskIdAbortInput, DistributedTaskIdAbortResult, DistributedTaskIdStateInput, DistributedTaskIdStateResult, AutostashOrphanRecord, AutostashOutcome, MergeDetails, MergeResult, MergeIntegrationWorktreeMode, MergeAdvanceAutoSyncMode, MergeConflictStrategy, CanonicalMergeConflictStrategy, MergeStrategyOverlapBehavior, PostMergeAuditMode, MergeAuditAutoRecoveryMode, MergerMode, MergerSettings, AutoRecoveryMode, AutoRecoveryFailureClass, AutoRecoverySettings, DirectMergeCommitStrategy, Settings, GlobalSettings, ProjectSettings, ReportMode, ReportActionType, ReportTarget, SecretsEnvConfig, WebSearchBackend, ResearchEnabledSources, ResearchGlobalDefaults, ResearchProjectLimits, ResearchProjectSettings, SandboxBackendName, SandboxFailureMode, SandboxPolicy, SandboxProjectSettings, EvalFollowUpPolicy, EvalProjectSettings, ResolvedEvalSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, Locale, ExecutionMode, PlannerOversightLevel, TaskPriority, MergeQueueEntry, MergeQueueEnqueueOptions, MergeQueueAcquireOptions, MergeQueueReleaseOutcome, MergeRequestState, MergeRequestRecord, MergeRequestWorkflowProjectionOptions, CompletionHandoffMarker, WorkflowWorkItem, WorkflowWorkItemDueFilter, WorkflowWorkItemKind, WorkflowWorkItemState, WorkflowWorkItemTransitionPatch, WorkflowWorkItemUpsertInput, HandoffEvidence, HandoffToReviewOptions, UnavailableNodePolicy, OwningNodeHandoffPolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, GithubIssueAction, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepGateMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, PermanentAgentActionCategory, PermanentAgentSensitiveActionCategory, PermanentAgentGatingContext, AgentPermissionPolicy, AgentPermissionPolicyRules, AgentPermissionPolicyToolRules, AgentPermissionPolicyActionCategory, AgentProvisioningApprovalMode, SandboxProvisioningApprovalMode, LegacyAgentPermissionPolicyActionCategory, ApprovalRequestActionCategoryInput, ApprovalRequestActionCategory, AgentPermissionPolicyDisposition, AgentPermissionPolicyPresetId, ApprovalRequestStatus, ApprovalRequestAuditEventType, ApprovalRequestActorSnapshot, ApprovalRequestTargetAction, ApprovalRequestAuditEvent, ApprovalRequest, ApprovalRequestCreateInput, ApprovalRequestDecisionInput, ApprovalRequestCompletionInput, ApprovalRequestListInput, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, CustomProvider, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, ProposedTaskMetadata, EphemeralTaskCreationPolicy, MessageReplyReference, Mailbox, CheckoutLease, CheckoutClaimPrecondition, TaskClaimRow, CentralClaimStore, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter, AgentMemoryInclusionMode, HeartbeatPromptTemplate, HeartbeatScopeDisciplineMode, WorktrunkSettings, WorktrunkOnFailure, TaskBranchContext, CliAgentSettings, McpSecretRef, McpSensitiveValue, McpStdioTransport, McpSseTransport, McpStreamableHttpTransport, McpTransport, McpServerDefinition, McpServersSettings, GitlabConfigSettingsSource, ResolvedGitlabConfig, ResolveGitlabConfigInput, GitlabAuthTokenType, PlannerOversightStage, PlannerInterventionAction, PlannerInterventionOutcome, PlannerInterventionSourceLink, PlannerInterventionEntry, BackupSettingsMigrationCandidate, BackupSettingsMigrationConflict } from "./types.js"; +export { COLUMNS, DEFAULT_COLUMN, isColumn, normalizeColumn, normalizeColumnId, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, isMergeRequestContractShadowEnabled, resolvePersistAgentThinkingLog, THINKING_LEVELS, ANTHROPIC_AUTH_PREFERENCES, THEME_MODES, COLOR_THEMES, SUPPORTED_LOCALES, DEFAULT_LOCALE, isLocale, AGENT_PERMISSIONS, PERMANENT_AGENT_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PROVISIONING_APPROVAL_MODES, SANDBOX_PROVISIONING_APPROVAL_MODES, AGENT_PERMISSION_POLICY_PRESET_IDS, LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, APPROVAL_REQUEST_STATUSES, APPROVAL_REQUEST_AUDIT_EVENT_TYPES, normalizeApprovalRequestActionCategory, isValidApprovalRequestTransition, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, DEFAULT_HEARTBEAT_PROCEDURE_PATH, getDefaultHeartbeatProcedurePath, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, PLANNER_OVERSIGHT_LEVELS, DEFAULT_PLANNER_OVERSIGHT_LEVEL, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, WORKFLOW_WORK_ITEM_KINDS, WORKFLOW_WORK_ITEM_STATES, HIGH_FANOUT_BLOCKER_TODO_THRESHOLD, STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS, DASHBOARD_USER_ID, normalizeMessageParticipant, validateMessageMetadata, resolveEphemeralTaskCreationPolicy, validateDockerNodeConfig, sanitizeDockerNodeConfigForResponse, normalizeMergeIntegrationWorktreeMode, normalizeMergeAdvanceAutoSyncMode, DEFAULT_GITLAB_API_BASE_URL, DEFAULT_GITLAB_INSTANCE_URL, resolveGitlabConfig, resolveGitlabEnabled, MERGE_ADVANCE_AUTO_SYNC_MODES, normalizeMergeConflictStrategy, normalizeMergeStrategyOverlapBehavior, normalizePostMergeAuditMode, POST_MERGE_AUDIT_MODES, normalizeMergeAuditAutoRecovery, MERGE_AUDIT_AUTO_RECOVERY_MODES, normalizeMergerMode, MERGER_MODES, normalizeAutoRecovery, AUTO_RECOVERY_MODES, buildResearchDocumentKey, REPO_OVERRIDE_RE, SHARED_STATE_SNAPSHOT_VERSION, sanitizeCliAgentSettings, sanitizeCliAgentsSettings, sanitizeMcpServers, CLI_AGENT_ADAPTER_IDS, CLI_AGENT_AUTONOMY_MODES, isMcpSecretRef, OVERSEER_INTERVENTION_MUTATION } from "./types.js"; +export type { Column, ColumnId, IssueInfo, IssueState, TaskSourceIssue, TaskGitLabTracking, TaskGitLabTrackedItem, GitLabTrackedItemKind, PrInfo, PrConflictState, PrConflictDiagnostics, PrCheckState, PrCheckStatus, PrStatus, BranchGroup, BranchGroupCreateInput, BranchGroupUpdate, BranchGroupPrState, Task, TaskTokenUsage, TaskTokenUsagePerModel, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, ArchivedTaskDocumentAdditionInput, ArchivedTaskDocumentAdditionResult, TaskDocumentWithTask, ArtifactType, Artifact, ArtifactCreateInput, ArtifactWithTask, NativeStructureRef, NativeStructureOpenTarget, NativeStructurePreviewPayload, NativeStructureUnavailablePayload, NativeStructurePreviewResult, TaskCreateInput, TaskSource, SourceType, TaskDetail, RetrySummary, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, DistributedTaskIdReserveInput, DistributedTaskIdReserveResult, DistributedTaskIdCommitInput, DistributedTaskIdCommitResult, DistributedTaskIdAbortInput, DistributedTaskIdAbortResult, DistributedTaskIdStateInput, DistributedTaskIdStateResult, AutostashOrphanRecord, AutostashOutcome, MergeDetails, MergeResult, MergeIntegrationWorktreeMode, MergeAdvanceAutoSyncMode, MergeConflictStrategy, CanonicalMergeConflictStrategy, MergeStrategyOverlapBehavior, PostMergeAuditMode, MergeAuditAutoRecoveryMode, MergerMode, MergerSettings, AutoRecoveryMode, AutoRecoveryFailureClass, AutoRecoverySettings, DirectMergeCommitStrategy, Settings, GlobalSettings, ProjectSettings, ReportMode, ReportActionType, ReportTarget, SecretsEnvConfig, WebSearchBackend, ResearchEnabledSources, ResearchGlobalDefaults, ResearchProjectLimits, ResearchProjectSettings, SandboxBackendName, SandboxFailureMode, SandboxPolicy, SandboxProjectSettings, EvalFollowUpPolicy, EvalProjectSettings, ResolvedEvalSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, AnthropicAuthPreference, ThemeMode, ColorTheme, Locale, ExecutionMode, PlannerOversightLevel, TaskPriority, MergeQueueEntry, MergeQueueEnqueueOptions, MergeQueueAcquireOptions, MergeQueueReleaseOutcome, MergeRequestState, MergeRequestRecord, MergeRequestWorkflowProjectionOptions, CompletionHandoffMarker, WorkflowWorkItem, WorkflowWorkItemDueFilter, WorkflowWorkItemKind, WorkflowWorkItemState, WorkflowWorkItemTransitionPatch, WorkflowWorkItemUpsertInput, HandoffEvidence, HandoffToReviewOptions, UnavailableNodePolicy, OwningNodeHandoffPolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, GithubIssueAction, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepGateMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, PermanentAgentActionCategory, PermanentAgentSensitiveActionCategory, PermanentAgentGatingContext, AgentPermissionPolicy, AgentPermissionPolicyRules, AgentPermissionPolicyToolRules, AgentPermissionPolicyActionCategory, AgentProvisioningApprovalMode, SandboxProvisioningApprovalMode, LegacyAgentPermissionPolicyActionCategory, ApprovalRequestActionCategoryInput, ApprovalRequestActionCategory, AgentPermissionPolicyDisposition, AgentPermissionPolicyPresetId, ApprovalRequestStatus, ApprovalRequestAuditEventType, ApprovalRequestActorSnapshot, ApprovalRequestTargetAction, ApprovalRequestAuditEvent, ApprovalRequest, ApprovalRequestCreateInput, ApprovalRequestDecisionInput, ApprovalRequestCompletionInput, ApprovalRequestListInput, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, CustomProvider, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, ProposedTaskMetadata, EphemeralTaskCreationPolicy, MessageReplyReference, Mailbox, CheckoutLease, CheckoutClaimPrecondition, TaskClaimRow, CentralClaimStore, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter, AgentMemoryInclusionMode, HeartbeatPromptTemplate, HeartbeatScopeDisciplineMode, WorktrunkSettings, WorktrunkOnFailure, TaskBranchContext, CliAgentSettings, McpSecretRef, McpSensitiveValue, McpStdioTransport, McpSseTransport, McpStreamableHttpTransport, McpTransport, McpServerDefinition, McpServersSettings, GitlabConfigSettingsSource, ResolvedGitlabConfig, ResolveGitlabConfigInput, GitlabAuthTokenType, PlannerOversightStage, PlannerInterventionAction, PlannerInterventionOutcome, PlannerInterventionSourceLink, PlannerInterventionEntry, BackupSettingsMigrationCandidate, BackupSettingsMigrationConflict } from "./types.js"; export { AGENT_VALID_TRANSITIONS, DUPLICATE_OF_METADATA_KEY, REPORT_ATTACHMENT_SOURCE, assertNotWorkspaceTaskMerge, isWorkspaceTask, WorkspaceTaskMergeError } from "./types.js"; export { resolveEntryPointBranchAssignment, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 721864a964..37ee18cbba 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,5 +1,5 @@ -export { COLUMNS, DEFAULT_COLUMN, isColumn, normalizeColumn, normalizeColumnId, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, isMergeRequestContractShadowEnabled, resolvePersistAgentThinkingLog, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, SUPPORTED_LOCALES, DEFAULT_LOCALE, isLocale, AGENT_PERMISSIONS, PERMANENT_AGENT_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PROVISIONING_APPROVAL_MODES, SANDBOX_PROVISIONING_APPROVAL_MODES, AGENT_PERMISSION_POLICY_PRESET_IDS, LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, APPROVAL_REQUEST_STATUSES, APPROVAL_REQUEST_AUDIT_EVENT_TYPES, normalizeApprovalRequestActionCategory, isValidApprovalRequestTransition, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, DEFAULT_HEARTBEAT_PROCEDURE_PATH, getDefaultHeartbeatProcedurePath, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, PLANNER_OVERSIGHT_LEVELS, DEFAULT_PLANNER_OVERSIGHT_LEVEL, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, WORKFLOW_WORK_ITEM_KINDS, WORKFLOW_WORK_ITEM_STATES, HIGH_FANOUT_BLOCKER_TODO_THRESHOLD, STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS, REVIEW_ARTIFACTS_MODES, LIVE_DEMO_ARTIFACT_MIME_TYPE, isReviewArtifact, parseReviewArtifactsModeOverride, resolveReviewArtifactsMode, classifyReviewArtifactTask, isReviewArtifactGenerationEligible, DASHBOARD_USER_ID, normalizeMessageParticipant, validateMessageMetadata, resolveEphemeralTaskCreationPolicy, validateDockerNodeConfig, sanitizeDockerNodeConfigForResponse, normalizeMergeIntegrationWorktreeMode, normalizeMergeAdvanceAutoSyncMode, DEFAULT_GITLAB_API_BASE_URL, DEFAULT_GITLAB_INSTANCE_URL, resolveGitlabConfig, resolveGitlabEnabled, MERGE_ADVANCE_AUTO_SYNC_MODES, normalizeMergeConflictStrategy, normalizeMergeStrategyOverlapBehavior, normalizePostMergeAuditMode, POST_MERGE_AUDIT_MODES, normalizeMergeAuditAutoRecovery, MERGE_AUDIT_AUTO_RECOVERY_MODES, normalizeMergerMode, MERGER_MODES, normalizeAutoRecovery, AUTO_RECOVERY_MODES, buildResearchDocumentKey, REPO_OVERRIDE_RE, SHARED_STATE_SNAPSHOT_VERSION, sanitizeCliAgentSettings, sanitizeCliAgentsSettings, sanitizeMcpServers, CLI_AGENT_ADAPTER_IDS, CLI_AGENT_AUTONOMY_MODES, isMcpSecretRef, OVERSEER_INTERVENTION_MUTATION } from "./types.js"; -export type { Column, ColumnId, IssueInfo, IssueState, TaskSourceIssue, TaskGitLabTracking, TaskGitLabTrackedItem, GitLabTrackedItemKind, PrInfo, PrConflictState, PrConflictDiagnostics, PrCheckState, PrCheckStatus, PrStatus, BranchGroup, BranchGroupCreateInput, BranchGroupUpdate, BranchGroupPrState, Task, TaskTokenUsage, TaskTokenUsagePerModel, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, ArchivedTaskDocumentAdditionInput, ArchivedTaskDocumentAdditionResult, TaskDocumentWithTask, ArtifactType, Artifact, ArtifactCreateInput, ArtifactWithTask, TaskCreateInput, TaskSource, SourceType, TaskDetail, RetrySummary, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, DistributedTaskIdReserveInput, DistributedTaskIdReserveResult, DistributedTaskIdCommitInput, DistributedTaskIdCommitResult, DistributedTaskIdAbortInput, DistributedTaskIdAbortResult, DistributedTaskIdStateInput, DistributedTaskIdStateResult, AutostashOrphanRecord, AutostashOutcome, MergeDetails, MergeResult, MergeIntegrationWorktreeMode, MergeAdvanceAutoSyncMode, MergeConflictStrategy, CanonicalMergeConflictStrategy, MergeStrategyOverlapBehavior, PostMergeAuditMode, MergeAuditAutoRecoveryMode, MergerMode, MergerSettings, AutoRecoveryMode, AutoRecoveryFailureClass, AutoRecoverySettings, DirectMergeCommitStrategy, Settings, GlobalSettings, ProjectSettings, ReportMode, ReportActionType, ReportTarget, SecretsEnvConfig, WebSearchBackend, ResearchEnabledSources, ResearchGlobalDefaults, ResearchProjectLimits, ResearchProjectSettings, SandboxBackendName, SandboxFailureMode, SandboxPolicy, SandboxProjectSettings, EvalFollowUpPolicy, EvalProjectSettings, ResolvedEvalSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, Locale, ExecutionMode, PlannerOversightLevel, ReviewArtifactsMode, ReviewArtifactTaskClassification, TaskPriority, MergeQueueEntry, MergeQueueEnqueueOptions, MergeQueueAcquireOptions, MergeQueueReleaseOutcome, MergeRequestState, MergeRequestRecord, MergeRequestWorkflowProjectionOptions, CompletionHandoffMarker, WorkflowWorkItem, WorkflowWorkItemDueFilter, WorkflowWorkItemKind, WorkflowWorkItemState, WorkflowWorkItemTransitionPatch, WorkflowWorkItemUpsertInput, HandoffEvidence, HandoffToReviewOptions, UnavailableNodePolicy, OwningNodeHandoffPolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, GithubIssueAction, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepGateMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, PermanentAgentActionCategory, PermanentAgentSensitiveActionCategory, PermanentAgentGatingContext, AgentPermissionPolicy, AgentPermissionPolicyRules, AgentPermissionPolicyToolRules, AgentPermissionPolicyActionCategory, AgentProvisioningApprovalMode, SandboxProvisioningApprovalMode, LegacyAgentPermissionPolicyActionCategory, ApprovalRequestActionCategoryInput, ApprovalRequestActionCategory, AgentPermissionPolicyDisposition, AgentPermissionPolicyPresetId, ApprovalRequestStatus, ApprovalRequestAuditEventType, ApprovalRequestActorSnapshot, ApprovalRequestTargetAction, ApprovalRequestAuditEvent, ApprovalRequest, ApprovalRequestCreateInput, ApprovalRequestDecisionInput, ApprovalRequestCompletionInput, ApprovalRequestListInput, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, CustomProvider, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, ProposedTaskMetadata, EphemeralTaskCreationPolicy, MessageReplyReference, Mailbox, CheckoutLease, CheckoutClaimPrecondition, TaskClaimRow, CentralClaimStore, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter, AgentMemoryInclusionMode, HeartbeatPromptTemplate, HeartbeatScopeDisciplineMode, WorktrunkSettings, WorktrunkOnFailure, TaskBranchContext, CliAgentSettings, McpSecretRef, McpSensitiveValue, McpStdioTransport, McpSseTransport, McpStreamableHttpTransport, McpTransport, McpServerDefinition, McpServersSettings, GitlabConfigSettingsSource, ResolvedGitlabConfig, ResolveGitlabConfigInput, GitlabAuthTokenType, PlannerOversightStage, PlannerInterventionAction, PlannerInterventionOutcome, PlannerInterventionSourceLink, PlannerInterventionEntry, ExecutorOverseerSignalMemory, BackupSettingsMigrationCandidate, BackupSettingsMigrationConflict } from "./types.js"; +export { COLUMNS, DEFAULT_COLUMN, isColumn, normalizeColumn, normalizeColumnId, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, isMergeRequestContractShadowEnabled, resolvePersistAgentThinkingLog, THINKING_LEVELS, ANTHROPIC_AUTH_PREFERENCES, THEME_MODES, COLOR_THEMES, SUPPORTED_LOCALES, DEFAULT_LOCALE, isLocale, AGENT_PERMISSIONS, PERMANENT_AGENT_ACTION_CATEGORIES, AGENT_PERMISSION_POLICY_ACTION_CATEGORIES, AGENT_PROVISIONING_APPROVAL_MODES, SANDBOX_PROVISIONING_APPROVAL_MODES, AGENT_PERMISSION_POLICY_PRESET_IDS, LEGACY_AGENT_PERMISSION_POLICY_ACTION_CATEGORY_ALIASES, APPROVAL_REQUEST_STATUSES, APPROVAL_REQUEST_AUDIT_EVENT_TYPES, normalizeApprovalRequestActionCategory, isValidApprovalRequestTransition, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, DEFAULT_HEARTBEAT_PROCEDURE_PATH, getDefaultHeartbeatProcedurePath, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, PLANNER_OVERSIGHT_LEVELS, DEFAULT_PLANNER_OVERSIGHT_LEVEL, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, WORKFLOW_WORK_ITEM_KINDS, WORKFLOW_WORK_ITEM_STATES, HIGH_FANOUT_BLOCKER_TODO_THRESHOLD, STALE_HIGH_FANOUT_BLOCKER_AGE_THRESHOLD_MS, REVIEW_ARTIFACTS_MODES, LIVE_DEMO_ARTIFACT_MIME_TYPE, isReviewArtifact, parseReviewArtifactsModeOverride, resolveReviewArtifactsMode, classifyReviewArtifactTask, isReviewArtifactGenerationEligible, DASHBOARD_USER_ID, normalizeMessageParticipant, validateMessageMetadata, resolveEphemeralTaskCreationPolicy, validateDockerNodeConfig, sanitizeDockerNodeConfigForResponse, normalizeMergeIntegrationWorktreeMode, normalizeMergeAdvanceAutoSyncMode, DEFAULT_GITLAB_API_BASE_URL, DEFAULT_GITLAB_INSTANCE_URL, resolveGitlabConfig, resolveGitlabEnabled, MERGE_ADVANCE_AUTO_SYNC_MODES, normalizeMergeConflictStrategy, normalizeMergeStrategyOverlapBehavior, normalizePostMergeAuditMode, POST_MERGE_AUDIT_MODES, normalizeMergeAuditAutoRecovery, MERGE_AUDIT_AUTO_RECOVERY_MODES, normalizeMergerMode, MERGER_MODES, normalizeAutoRecovery, AUTO_RECOVERY_MODES, buildResearchDocumentKey, REPO_OVERRIDE_RE, SHARED_STATE_SNAPSHOT_VERSION, sanitizeCliAgentSettings, sanitizeCliAgentsSettings, sanitizeMcpServers, CLI_AGENT_ADAPTER_IDS, CLI_AGENT_AUTONOMY_MODES, isMcpSecretRef, OVERSEER_INTERVENTION_MUTATION } from "./types.js"; +export type { Column, ColumnId, IssueInfo, IssueState, TaskSourceIssue, TaskGitLabTracking, TaskGitLabTrackedItem, GitLabTrackedItemKind, PrInfo, PrConflictState, PrConflictDiagnostics, PrCheckState, PrCheckStatus, PrStatus, BranchGroup, BranchGroupCreateInput, BranchGroupUpdate, BranchGroupPrState, Task, TaskTokenUsage, TaskTokenUsagePerModel, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, ArchivedTaskDocumentAdditionInput, ArchivedTaskDocumentAdditionResult, TaskDocumentWithTask, ArtifactType, Artifact, ArtifactCreateInput, ArtifactWithTask, TaskCreateInput, TaskSource, SourceType, TaskDetail, RetrySummary, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, DistributedTaskIdReserveInput, DistributedTaskIdReserveResult, DistributedTaskIdCommitInput, DistributedTaskIdCommitResult, DistributedTaskIdAbortInput, DistributedTaskIdAbortResult, DistributedTaskIdStateInput, DistributedTaskIdStateResult, AutostashOrphanRecord, AutostashOutcome, MergeDetails, MergeResult, MergeIntegrationWorktreeMode, MergeAdvanceAutoSyncMode, MergeConflictStrategy, CanonicalMergeConflictStrategy, MergeStrategyOverlapBehavior, PostMergeAuditMode, MergeAuditAutoRecoveryMode, MergerMode, MergerSettings, AutoRecoveryMode, AutoRecoveryFailureClass, AutoRecoverySettings, DirectMergeCommitStrategy, Settings, GlobalSettings, ProjectSettings, ReportMode, ReportActionType, ReportTarget, SecretsEnvConfig, WebSearchBackend, ResearchEnabledSources, ResearchGlobalDefaults, ResearchProjectLimits, ResearchProjectSettings, SandboxBackendName, SandboxFailureMode, SandboxPolicy, SandboxProjectSettings, EvalFollowUpPolicy, EvalProjectSettings, ResolvedEvalSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, AnthropicAuthPreference, ThemeMode, ColorTheme, Locale, ExecutionMode, PlannerOversightLevel, ReviewArtifactsMode, ReviewArtifactTaskClassification, TaskPriority, MergeQueueEntry, MergeQueueEnqueueOptions, MergeQueueAcquireOptions, MergeQueueReleaseOutcome, MergeRequestState, MergeRequestRecord, MergeRequestWorkflowProjectionOptions, CompletionHandoffMarker, WorkflowWorkItem, WorkflowWorkItemDueFilter, WorkflowWorkItemKind, WorkflowWorkItemState, WorkflowWorkItemTransitionPatch, WorkflowWorkItemUpsertInput, HandoffEvidence, HandoffToReviewOptions, UnavailableNodePolicy, OwningNodeHandoffPolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, GithubIssueAction, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepGateMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, PermanentAgentActionCategory, PermanentAgentSensitiveActionCategory, PermanentAgentGatingContext, AgentPermissionPolicy, AgentPermissionPolicyRules, AgentPermissionPolicyToolRules, AgentPermissionPolicyActionCategory, AgentProvisioningApprovalMode, SandboxProvisioningApprovalMode, LegacyAgentPermissionPolicyActionCategory, ApprovalRequestActionCategoryInput, ApprovalRequestActionCategory, AgentPermissionPolicyDisposition, AgentPermissionPolicyPresetId, ApprovalRequestStatus, ApprovalRequestAuditEventType, ApprovalRequestActorSnapshot, ApprovalRequestTargetAction, ApprovalRequestAuditEvent, ApprovalRequest, ApprovalRequestCreateInput, ApprovalRequestDecisionInput, ApprovalRequestCompletionInput, ApprovalRequestListInput, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, CustomProvider, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, ProposedTaskMetadata, EphemeralTaskCreationPolicy, MessageReplyReference, Mailbox, CheckoutLease, CheckoutClaimPrecondition, TaskClaimRow, CentralClaimStore, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter, AgentMemoryInclusionMode, HeartbeatPromptTemplate, HeartbeatScopeDisciplineMode, WorktrunkSettings, WorktrunkOnFailure, TaskBranchContext, CliAgentSettings, McpSecretRef, McpSensitiveValue, McpStdioTransport, McpSseTransport, McpStreamableHttpTransport, McpTransport, McpServerDefinition, McpServersSettings, GitlabConfigSettingsSource, ResolvedGitlabConfig, ResolveGitlabConfigInput, GitlabAuthTokenType, PlannerOversightStage, PlannerInterventionAction, PlannerInterventionOutcome, PlannerInterventionSourceLink, PlannerInterventionEntry, ExecutorOverseerSignalMemory, BackupSettingsMigrationCandidate, BackupSettingsMigrationConflict } from "./types.js"; export type { NativeStructureRef, NativeStructureEmbed, NativeStructureOpenTarget, NativeStructurePreviewPayload, NativeStructureUnavailablePayload, NativeStructurePreviewResult } from "./types.js"; export type { SymbolLockStatus, diff --git a/packages/core/src/settings-schema.ts b/packages/core/src/settings-schema.ts index da0635f649..be75e0ea45 100644 --- a/packages/core/src/settings-schema.ts +++ b/packages/core/src/settings-schema.ts @@ -88,6 +88,12 @@ export const DEFAULT_GLOBAL_SETTINGS = { FNXC:DashboardTheming 2026-07-03-00:00: Fresh installs must follow the operating system theme until the user explicitly chooses Light, Dark, or System. Keep this global default aligned with dashboard and desktop pre-hydration fallbacks. */ + /* + FNXC:ProviderAuth 2026-07-24-17:05: + Default to the historical precedence (raw API key beats subscription OAuth, FN-7391/FN-7396) + so upgrading never silently moves an operator's traffic from their key onto their subscription. + */ + anthropicAuthPreference: "api-key", themeMode: "system", /* FNXC:DashboardTheming 2026-06-30-00:00: diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 0cb172ce69..3ded2df035 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -137,6 +137,7 @@ import { DEFAULT_PLANNER_OVERSIGHT_LEVEL, COMPLETION_DOCUMENTATION_MODES, REVIEW_ARTIFACTS_MODES, + ANTHROPIC_AUTH_PREFERENCES, THEME_MODES, COLOR_THEMES, SUPPORTED_LOCALES, @@ -148,6 +149,7 @@ import type { PlannerOversightLevel, CompletionDocumentationMode, ReviewArtifactsMode, + AnthropicAuthPreference, ThemeMode, ColorTheme, Locale, @@ -161,6 +163,7 @@ export { DEFAULT_PLANNER_OVERSIGHT_LEVEL, COMPLETION_DOCUMENTATION_MODES, REVIEW_ARTIFACTS_MODES, + ANTHROPIC_AUTH_PREFERENCES, THEME_MODES, COLOR_THEMES, SUPPORTED_LOCALES, @@ -172,6 +175,7 @@ export type { PlannerOversightLevel, CompletionDocumentationMode, ReviewArtifactsMode, + AnthropicAuthPreference, ThemeMode, ColorTheme, Locale, diff --git a/packages/core/src/types/execution-and-ui.ts b/packages/core/src/types/execution-and-ui.ts index 8b99625050..5451aa4cd0 100644 --- a/packages/core/src/types/execution-and-ui.ts +++ b/packages/core/src/types/execution-and-ui.ts @@ -52,6 +52,21 @@ while user-facing tasks can opt in without making every task generate media. export const REVIEW_ARTIFACTS_MODES = ["off", "user-facing", "on"] as const; export type ReviewArtifactsMode = (typeof REVIEW_ARTIFACTS_MODES)[number]; +/* +FNXC:ProviderAuth 2026-07-24-17:05: +Anthropic credentials can be present twice at once: a raw API key AND a Claude +subscription OAuth login. Runtime auth resolution has to pick one, and until now the raw +key always won silently — an operator who logged in with their subscription but had an old +(or revoked) key saved kept getting `401 invalid x-api-key` from lanes that call the +Anthropic endpoint directly, with nothing in the UI explaining which credential was in use. +This preference makes the choice explicit and operator-owned. It is GLOBAL because +credentials live in the global `~/.fusion/agent/auth.json`, not per project. +Default "api-key" preserves the historical precedence (FN-7391/FN-7396), so upgrading +changes no existing behavior. +*/ +export const ANTHROPIC_AUTH_PREFERENCES = ["api-key", "subscription"] as const; +export type AnthropicAuthPreference = (typeof ANTHROPIC_AUTH_PREFERENCES)[number]; + /** Theme mode for light/dark/system preference */ export const THEME_MODES = ["dark", "light", "system"] as const; export type ThemeMode = (typeof THEME_MODES)[number]; diff --git a/packages/core/src/types/settings-scope.ts b/packages/core/src/types/settings-scope.ts index d1efa2e26c..499549fe53 100644 --- a/packages/core/src/types/settings-scope.ts +++ b/packages/core/src/types/settings-scope.ts @@ -11,6 +11,7 @@ import type { Locale, ReviewArtifactsMode, ThemeMode, + AnthropicAuthPreference, } from "./execution-and-ui.js"; import type { AutoRecoverySettings, @@ -291,6 +292,15 @@ export interface BackupSettingsMigrationConflict { export interface GlobalSettings { /** Maximum PostgreSQL server connections for Fusion's embedded database. Applied on the next Fusion restart. */ embeddedPostgresMaxConnections?: number; + /** + * FNXC:ProviderAuth 2026-07-24-17:05: + * Which Anthropic credential wins when BOTH a raw API key and a Claude subscription OAuth + * login are present. Global because credentials are global. "api-key" (default) preserves + * the historical precedence; "subscription" makes the OAuth login win so a stale or revoked + * saved key can no longer shadow it with `401 invalid x-api-key`. With only one credential + * configured this setting changes nothing — resolution falls through to whatever exists. + */ + anthropicAuthPreference?: AnthropicAuthPreference; /** Theme mode preference: dark, light, or system (follows OS). Default: "dark". */ themeMode?: ThemeMode; /** Color theme preference for accent colors and styling. Default: "shadcn-ember"; "default" and "ocean" remain valid explicit legacy selections. */ diff --git a/packages/dashboard/app/components/SettingsModal.tsx b/packages/dashboard/app/components/SettingsModal.tsx index 5878fb88f4..5b2c5fe0a9 100644 --- a/packages/dashboard/app/components/SettingsModal.tsx +++ b/packages/dashboard/app/components/SettingsModal.tsx @@ -4539,6 +4539,15 @@ export function SettingsModal({ handleSubmitManualCode, onReopenOnboarding, }} + /* + FNXC:ProviderAuth 2026-07-24-17:05: + Authentication is presentational like every other section — the shell keeps + ownership of persistence. It needs the form only for the Anthropic + credential-precedence row, which belongs beside the two Anthropic cards + rather than buried in a general settings list. + */ + form={form} + setForm={setForm} /> ); case "hermes-runtime": diff --git a/packages/dashboard/app/components/__tests__/AuthenticationSection.anthropicPrecedence.test.tsx b/packages/dashboard/app/components/__tests__/AuthenticationSection.anthropicPrecedence.test.tsx new file mode 100644 index 0000000000..e8cc684754 --- /dev/null +++ b/packages/dashboard/app/components/__tests__/AuthenticationSection.anthropicPrecedence.test.tsx @@ -0,0 +1,148 @@ +/* +FNXC:ProviderAuth 2026-07-24-17:05: +An operator can hold two live Anthropic credentials at once — a raw API key and a Claude +subscription OAuth login — and runtime auth silently preferred the key. A stale or revoked +saved key therefore produced `401 invalid x-api-key` on every lane that calls Anthropic +directly, while BOTH cards still read "✓ Active" and nothing named the credential in use. + +Invariant asserted here: when (and only when) both Anthropic credentials are connected, +Settings names which one is in use, and the operator can change it. The control writes the +global `anthropicAuthPreference` through the shell-owned form, never directly. +*/ + +import { describe, expect, it, vi } from "vitest"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { useState } from "react"; +import { AuthenticationSection, type AuthenticationSectionData } from "../settings/sections/AuthenticationSection"; +import type { AuthProvider } from "../../api"; +import type { Settings } from "@fusion/core"; + +vi.mock("../ProviderIcon", () => ({ + ProviderIcon: ({ provider }: { provider: string }) => {provider}, +})); +vi.mock("../PluginSlot", () => ({ PluginSlot: () => null })); +vi.mock("../LoginInstructions", () => ({ LoginInstructions: () => null })); +vi.mock("../LoadingSpinner", () => ({ LoadingSpinner: () => null })); +vi.mock("../OAuthManualCodeForm", () => ({ OAuthManualCodeForm: () => null })); +vi.mock("../CustomProvidersSection", () => ({ CustomProvidersSection: () => null })); + +const apiKeyCard: AuthProvider = { + id: "anthropic-api-key", + name: "Anthropic API Key", + type: "api_key", + authenticated: true, +} as AuthProvider; + +const subscriptionCard: AuthProvider = { + id: "anthropic-subscription", + name: "Claude Subscription", + type: "oauth", + authenticated: true, +} as AuthProvider; + +function renderSection(providers: AuthProvider[], initialForm: Partial = {}) { + const observed: { form: Partial } = { form: initialForm }; + + function Harness() { + const [form, setForm] = useState>(initialForm); + observed.form = form; + const auth = { + addToast: vi.fn(), + authProviders: providers, + authLoading: false, + authActionInProgress: null, + apiKeyInputs: {}, + setApiKeyInputs: vi.fn(), + apiKeyErrors: {}, + opencodeApiKeyRefreshStatus: {}, + deviceCodes: {}, + loginInstructions: {}, + manualCodeConfigs: {}, + manualCodeInputs: {}, + setManualCodeInputs: vi.fn(), + manualCodeSubmitInProgress: null, + loadAuthStatus: vi.fn(), + handleLogin: vi.fn(), + handleLogout: vi.fn(), + handleCancelLogin: vi.fn(), + handleSaveApiKey: vi.fn(), + handleClearApiKey: vi.fn(), + handleSubmitManualCode: vi.fn(), + } as unknown as AuthenticationSectionData; + return ( + + ); + } + + render(); + return observed; +} + +describe("Anthropic credential precedence in Settings → Authentication", () => { + it("names the credential in use when both Anthropic credentials are connected", () => { + renderSection([apiKeyCard, subscriptionCard]); + + // Default preference is the API key, matching runtime resolution. + expect(screen.getByTestId("auth-precedence-active-anthropic-api-key")).toBeTruthy(); + expect(screen.getByTestId("auth-precedence-overridden-anthropic-subscription")).toBeTruthy(); + }); + + it("moves the in-use marker when the operator prefers the subscription", () => { + renderSection([apiKeyCard, subscriptionCard], { anthropicAuthPreference: "subscription" }); + + expect(screen.getByTestId("auth-precedence-active-anthropic-subscription")).toBeTruthy(); + expect(screen.getByTestId("auth-precedence-overridden-anthropic-api-key")).toBeTruthy(); + }); + + it("writes the operator's choice into the shell-owned settings form", () => { + const observed = renderSection([apiKeyCard, subscriptionCard]); + + fireEvent.change(screen.getByLabelText(/Anthropic credential to use/i), { + target: { value: "subscription" }, + }); + + expect(observed.form.anthropicAuthPreference).toBe("subscription"); + }); + + it("hides the control when only one Anthropic credential is connected", () => { + // Nothing to disambiguate — resolution reaches the single credential either way. + renderSection([apiKeyCard, { ...subscriptionCard, authenticated: false }]); + + expect(screen.queryByLabelText(/Anthropic credential to use/i)).toBeNull(); + expect(screen.queryByTestId("auth-precedence-active-anthropic-api-key")).toBeNull(); + }); + + it("hides the control when the shell does not supply the settings form", () => { + const auth = { + addToast: vi.fn(), + authProviders: [apiKeyCard, subscriptionCard], + authLoading: false, + authActionInProgress: null, + apiKeyInputs: {}, + setApiKeyInputs: vi.fn(), + apiKeyErrors: {}, + opencodeApiKeyRefreshStatus: {}, + deviceCodes: {}, + loginInstructions: {}, + manualCodeConfigs: {}, + manualCodeInputs: {}, + setManualCodeInputs: vi.fn(), + manualCodeSubmitInProgress: null, + loadAuthStatus: vi.fn(), + handleLogin: vi.fn(), + handleLogout: vi.fn(), + handleCancelLogin: vi.fn(), + handleSaveApiKey: vi.fn(), + handleClearApiKey: vi.fn(), + handleSubmitManualCode: vi.fn(), + } as unknown as AuthenticationSectionData; + + render(); + + expect(screen.queryByLabelText(/Anthropic credential to use/i)).toBeNull(); + }); +}); diff --git a/packages/dashboard/app/components/settings/sections/AuthenticationSection.tsx b/packages/dashboard/app/components/settings/sections/AuthenticationSection.tsx index 12095ebc6f..e0b4a9671e 100644 --- a/packages/dashboard/app/components/settings/sections/AuthenticationSection.tsx +++ b/packages/dashboard/app/components/settings/sections/AuthenticationSection.tsx @@ -14,6 +14,8 @@ import { LoadingSpinner } from "../../LoadingSpinner"; import { OAuthManualCodeForm } from "../../OAuthManualCodeForm"; import { CustomProvidersSection } from "../../CustomProvidersSection"; import { SettingsHelpTip } from "../SettingsHelpTip"; +import { SettingsSelectRow } from "../SettingsSelectRow"; +import type { SectionBaseProps } from "./context"; import { copyTextToClipboard } from "../../../utils/copyToClipboard"; import { appendTokenQuery } from "../../../auth"; import { openExternalUrl } from "../../../utils/open-external"; @@ -48,7 +50,12 @@ export interface AuthenticationSectionData { } export interface AuthenticationSectionProps { auth: AuthenticationSectionData; + /** Shell-owned settings form; used only for the Anthropic credential-precedence row. */ + form?: SectionBaseProps["form"]; + setForm?: SectionBaseProps["setForm"]; } +const ANTHROPIC_API_KEY_PROVIDER_ID = "anthropic-api-key"; +const ANTHROPIC_SUBSCRIPTION_PROVIDER_ID = "anthropic-subscription"; const ANTHROPIC_AUTH_PROVIDER_PRIORITY: Record = { "claude-cli": 0, "anthropic-subscription": 1, @@ -75,7 +82,7 @@ const compareAuthProviderDisplayOrder = (a: AuthProvider, b: AuthProvider) => { } return a.id.localeCompare(b.id); }; -export function AuthenticationSection({ auth }: AuthenticationSectionProps) { +export function AuthenticationSection({ auth, form, setForm }: AuthenticationSectionProps) { const { t } = useTranslation("app"); const { projectId, addToast, authProviders, authLoading, authActionInProgress, apiKeyInputs, setApiKeyInputs, apiKeyErrors, opencodeApiKeyRefreshStatus, deviceCodes, loginInstructions, manualCodeConfigs, manualCodeInputs, setManualCodeInputs, manualCodeSubmitInProgress, loadAuthStatus, handleLogin, handleLogout, handleCancelLogin, handleSaveApiKey, handleClearApiKey, handleSubmitManualCode, onReopenOnboarding, } = auth; const hasSeparatedAnthropicProvider = authProviders.some((p) => p.id === "anthropic-subscription" || p.id === "anthropic-api-key"); @@ -127,6 +134,59 @@ export function AuthenticationSection({ auth }: AuthenticationSectionProps) { } return (); }; + /* + FNXC:ProviderAuth 2026-07-24-17:05: + Anthropic is the one provider an operator can hold two live credentials for at once — a + raw API key AND a Claude subscription OAuth login. Runtime auth has to pick one, and the + choice was previously invisible: a saved key silently won, so an operator who logged in + with their subscription but still had a stale/revoked key stored got `401 invalid + x-api-key` from lanes that call the Anthropic endpoint directly, with nothing on this + screen explaining why. Surface the conflict where the credentials are managed and let the + operator pick. The row renders only when BOTH are actually connected — with one credential + there is nothing to disambiguate and the control would be noise. + */ + const anthropicApiKeyConnected = authProviders.some((p) => p.id === ANTHROPIC_API_KEY_PROVIDER_ID && p.authenticated); + const anthropicSubscriptionConnected = authProviders.some((p) => p.id === ANTHROPIC_SUBSCRIPTION_PROVIDER_ID && p.authenticated); + const showAnthropicPrecedence = Boolean(form && setForm) && anthropicApiKeyConnected && anthropicSubscriptionConnected; + const anthropicAuthPreference = form?.anthropicAuthPreference === "subscription" ? "subscription" : "api-key"; + const preferenceIsInEffect = (providerId: string) => showAnthropicPrecedence + && (anthropicAuthPreference === "subscription" + ? providerId === ANTHROPIC_SUBSCRIPTION_PROVIDER_ID + : providerId === ANTHROPIC_API_KEY_PROVIDER_ID); + /* + Live state, not description: which of two connected credentials the engine will actually + send. Rendered on the card itself so the answer is where the operator is looking. + */ + const renderAnthropicPrecedenceBadge = (provider: AuthProvider) => { + if (!showAnthropicPrecedence) { + return null; + } + if (provider.id !== ANTHROPIC_API_KEY_PROVIDER_ID && provider.id !== ANTHROPIC_SUBSCRIPTION_PROVIDER_ID) { + return null; + } + return preferenceIsInEffect(provider.id) + ? ( + {t("settings.auth.credentialInUse", "In use")} + ) + : ( + {t("settings.auth.credentialOverridden", "Overridden below")} + ); + }; + const renderAnthropicPrecedenceRow = () => showAnthropicPrecedence + ? ( setForm?.((f) => ({ + ...f, + anthropicAuthPreference: value === "subscription" ? "subscription" : "api-key", + }))}/>) + : null; const showAuthenticatedGroup = authenticatedProviders.length > 0; const showAvailableGroup = unauthenticatedProviders.length > 0; const providerSupportsApiKey = (provider: AuthProvider) => provider.type === "api_key"; @@ -234,12 +294,14 @@ export function AuthenticationSection({ auth }: AuthenticationSectionProps) { {t("settings.auth.statusActive", "✓ Active")} + {renderAnthropicPrecedenceBadge(provider)} {provider.authenticated && provider.keyHint && ({t("settings.authentication.key", "Key: ")}{provider.keyHint})} {provider.type !== "api_key" &&
{renderAuthenticatedOAuthActions(provider)}{renderProviderAuthError(provider)}
} {providerSupportsApiKey(provider) && renderApiKeySection(provider)} ))} + {renderAnthropicPrecedenceRow()} )} {showAvailableGroup && (
{t("settings.auth.groupAvailable", "Available")}
diff --git a/packages/engine/src/__tests__/auth-storage-anthropic-preference.test.ts b/packages/engine/src/__tests__/auth-storage-anthropic-preference.test.ts new file mode 100644 index 0000000000..18954b3ed8 --- /dev/null +++ b/packages/engine/src/__tests__/auth-storage-anthropic-preference.test.ts @@ -0,0 +1,147 @@ +/* +FNXC:ProviderAuth 2026-07-24-17:05: +Regression tests for the operator-selectable Anthropic credential precedence. + +Reported symptom class: an operator holding BOTH a raw Anthropic API key and a Claude +subscription OAuth login always ran on the raw key, because runtime resolution put it first +unconditionally. When that saved key was stale or revoked, every lane that calls the Anthropic +endpoint directly failed with `401 invalid x-api-key` while the subscription card still showed +"Active" — and nothing in the product explained which credential was in use. + +Invariant: `anthropicAuthPreference` decides which credential wins WHEN BOTH EXIST, and +never removes a source — with only one credential configured, resolution reaches it under +either setting. Default stays "api-key" so upgrades do not silently move traffic. +*/ + +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createFusionAuthStorage, getFusionAuthPath } from "../auth-storage.js"; + +const RAW_API_KEY = "sk-ant-api03-raw-key-from-settings-card"; +const SUBSCRIPTION_ACCESS_TOKEN = "sk-ant-oat01-subscription-access-token"; + +function writeAuth(homeDir: string, credentials: Record): void { + mkdirSync(join(homeDir, ".fusion", "agent"), { recursive: true }); + writeFileSync(getFusionAuthPath(homeDir), JSON.stringify(credentials)); +} + +function writeGlobalSettings(homeDir: string, settings: Record): void { + mkdirSync(join(homeDir, ".fusion"), { recursive: true }); + writeFileSync(join(homeDir, ".fusion", "settings.json"), JSON.stringify(settings)); +} + +/** A subscription OAuth credential that is still valid, so no refresh is attempted. */ +function liveSubscriptionCredential() { + return { + type: "oauth", + access: SUBSCRIPTION_ACCESS_TOKEN, + refresh: "sk-ant-ort01-refresh-token", + expires: Date.now() + 60 * 60_000, + }; +} + +describe("Anthropic credential precedence (anthropicAuthPreference)", () => { + const originalHome = process.env.HOME; + const originalApiKeyEnv = process.env.ANTHROPIC_API_KEY; + let homeDir: string; + + beforeEach(async () => { + homeDir = await mkdtemp(join(tmpdir(), "fusion-anthropic-pref-")); + process.env.HOME = homeDir; + // The env key is a separate fallback source; keep it out of these assertions. + delete process.env.ANTHROPIC_API_KEY; + }); + + afterEach(async () => { + if (homeDir) { + await rm(homeDir, { recursive: true, force: true }); + } + if (originalHome === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = originalHome; + } + if (originalApiKeyEnv === undefined) { + delete process.env.ANTHROPIC_API_KEY; + } else { + process.env.ANTHROPIC_API_KEY = originalApiKeyEnv; + } + }); + + it("defaults to the raw API key when both credentials exist and no preference is stored", async () => { + writeAuth(homeDir, { + anthropic: { type: "api_key", key: RAW_API_KEY }, + "anthropic-subscription": liveSubscriptionCredential(), + }); + + const storage = createFusionAuthStorage(); + + // Historical precedence (FN-7391/FN-7396) — upgrading must not move traffic. + await expect(storage.getApiKey("anthropic")).resolves.toBe(RAW_API_KEY); + }); + + it("keeps the raw API key first when the preference is explicitly api-key", async () => { + writeAuth(homeDir, { + anthropic: { type: "api_key", key: RAW_API_KEY }, + "anthropic-subscription": liveSubscriptionCredential(), + }); + writeGlobalSettings(homeDir, { anthropicAuthPreference: "api-key" }); + + const storage = createFusionAuthStorage(); + + await expect(storage.getApiKey("anthropic")).resolves.toBe(RAW_API_KEY); + }); + + it("resolves the subscription token over a stale saved key when the operator prefers subscription", async () => { + writeAuth(homeDir, { + anthropic: { type: "api_key", key: RAW_API_KEY }, + "anthropic-subscription": liveSubscriptionCredential(), + }); + writeGlobalSettings(homeDir, { anthropicAuthPreference: "subscription" }); + + const storage = createFusionAuthStorage(); + + /* + The exact reported failure: with the raw key winning, this returned a key that pi-ai + sends as `x-api-key` (it lacks the `sk-ant-oat` marker) and Anthropic rejects with + `401 invalid x-api-key`. The OAuth token routes as a Bearer credential instead. + */ + await expect(storage.getApiKey("anthropic")).resolves.toBe(SUBSCRIPTION_ACCESS_TOKEN); + }); + + it("still falls back to the raw key under the subscription preference when no OAuth credential exists", async () => { + writeAuth(homeDir, { anthropic: { type: "api_key", key: RAW_API_KEY } }); + writeGlobalSettings(homeDir, { anthropicAuthPreference: "subscription" }); + + const storage = createFusionAuthStorage(); + + // The preference disambiguates; it must never remove the only credential present. + await expect(storage.getApiKey("anthropic")).resolves.toBe(RAW_API_KEY); + }); + + it("resolves the subscription token under the api-key preference when no raw key exists", async () => { + writeAuth(homeDir, { "anthropic-subscription": liveSubscriptionCredential() }); + writeGlobalSettings(homeDir, { anthropicAuthPreference: "api-key" }); + + const storage = createFusionAuthStorage(); + + await expect(storage.getApiKey("anthropic")).resolves.toBe(SUBSCRIPTION_ACCESS_TOKEN); + }); + + it("falls back to the historical precedence when the settings file is unreadable", async () => { + writeAuth(homeDir, { + anthropic: { type: "api_key", key: RAW_API_KEY }, + "anthropic-subscription": liveSubscriptionCredential(), + }); + mkdirSync(join(homeDir, ".fusion"), { recursive: true }); + writeFileSync(join(homeDir, ".fusion", "settings.json"), "{ this is not json"); + + const storage = createFusionAuthStorage(); + + // A corrupt settings file must not strand credential resolution. + await expect(storage.getApiKey("anthropic")).resolves.toBe(RAW_API_KEY); + }); +}); diff --git a/packages/engine/src/auth-storage.ts b/packages/engine/src/auth-storage.ts index 15b60b185a..bd48bdf00f 100644 --- a/packages/engine/src/auth-storage.ts +++ b/packages/engine/src/auth-storage.ts @@ -421,6 +421,37 @@ function resolveStoredCredentialApiKey(providerId: string, credential: StoredCre * can return them as a fallback when neither Fusion auth nor legacy auth.json * contains a key for the provider. */ +/** Global settings file that carries the operator's Anthropic credential preference. */ +export function getFusionGlobalSettingsPath(home = getHomeDir()): string { + return join(home, ".fusion", "settings.json"); +} + +/* +FNXC:ProviderAuth 2026-07-24-17:05: +Read the operator's `anthropicAuthPreference` straight off the global settings file rather +than threading a Settings object down here. Credential resolution runs deep inside +createFnAgent (via createFusionAuthStorage, which takes no arguments) and is shared by every +host — CLI, dashboard, desktop, daemon — so a settings parameter would have to be plumbed +through all of them. The preference is global by definition (credentials live in the global +auth.json), and this file is already the sibling of the auth/models files this module reads +synchronously. Re-read per resolution so toggling the setting takes effect on the next lane +without a restart; a missing/corrupt file falls back to the historical "api-key" precedence. +*/ +function readAnthropicAuthPreference(home = getHomeDir()): "api-key" | "subscription" { + const settingsPath = getFusionGlobalSettingsPath(home); + if (!existsSync(settingsPath)) { + return "api-key"; + } + try { + const parsed = JSON.parse(readFileSync(settingsPath, "utf-8")) as { + anthropicAuthPreference?: unknown; + }; + return parsed?.anthropicAuthPreference === "subscription" ? "subscription" : "api-key"; + } catch { + return "api-key"; + } +} + function readModelsJsonApiKeys(home = getHomeDir()): Map { const apiKeys = new Map(); const modelsPath = getModelRegistryModelsPath(home); @@ -784,11 +815,27 @@ export function createFusionAuthStorage(): FusionAuthStorage { FNXC:ProviderAuth 2026-07-01-14:55: Anthropic runtime auth (`getApiKey("anthropic")`) resolves in precedence order: (1) raw API key, (2) legacy `anthropic` OAuth, (3) separated `anthropic-subscription` OAuth, (4) models.json / ModelRegistry fallback raw key. Raw key wins so an explicit `ANTHROPIC_API_KEY` keeps using x-api-key; subscription/OAuth tokens must resolve here so the built-in provider runs them on `/v1` with Claude Code impersonation. Do NOT gate OAuth behind the CLI or reroute it to an `/v1` `anthropic-subscription` provider — that reintroduced the #1857 regression (FN-7391/FN-7396). */ - if (!rawProviderLoggedOut) { + /* + FNXC:ProviderAuth 2026-07-24-17:05: + `anthropicAuthPreference` selects which credential wins when BOTH are configured. + "api-key" (default) keeps the order documented above. "subscription" moves the raw-key + step BELOW the OAuth steps so a stale or revoked saved key can no longer shadow a working + Claude subscription login — the failure mode that surfaced as `401 invalid x-api-key` on + lanes that call the Anthropic endpoint directly. Neither setting REMOVES a source: with + only one credential present, resolution falls through to it either way. + */ + const preferSubscription = readAnthropicAuthPreference() === "subscription"; + const resolveRawApiKey = (): string | undefined => { + if (rawProviderLoggedOut) return undefined; const anthropicApiKeyCredential = selectStoredCredentialByType(ANTHROPIC_PROVIDER_ID, "api_key"); - if (anthropicApiKeyCredential) { - return resolveStoredCredentialApiKey(ANTHROPIC_PROVIDER_ID, anthropicApiKeyCredential); - } + return anthropicApiKeyCredential + ? resolveStoredCredentialApiKey(ANTHROPIC_PROVIDER_ID, anthropicApiKeyCredential) + : undefined; + }; + + if (!preferSubscription) { + const rawKey = resolveRawApiKey(); + if (rawKey) return rawKey; } const subscriptionLoggedOut = loggedOutProviders.has(ANTHROPIC_SUBSCRIPTION_PROVIDER_ID); @@ -812,6 +859,12 @@ export function createFusionAuthStorage(): FusionAuthStorage { } } + // Subscription-preferred: the raw key is the fallback once no OAuth credential resolved. + if (preferSubscription) { + const rawKey = resolveRawApiKey(); + if (rawKey) return rawKey; + } + if (!rawProviderLoggedOut) { /* FNXC:ProviderAuth 2026-06-30-13:28: