Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
18c3993
refactor(config): move config into module directory
ibetitsmike Aug 29, 2026
142ce0d
refactor(config): extract FileLeaseManager and ProvidersConfigStore
ibetitsmike Aug 29, 2026
8550235
refactor(config): add SecretsStore extraction checkpoint
ibetitsmike Aug 29, 2026
fa659a2
refactor(config): extract SecretsStore
ibetitsmike Aug 29, 2026
b3f7751
refactor(config): add session locator extraction checkpoint
ibetitsmike Aug 29, 2026
8a41ea6
🤖 refactor(config): rewire session paths to locator storage
ibetitsmike Aug 29, 2026
b43daa3
🤖 refactor(config): construct focused stores through factory
ibetitsmike Aug 29, 2026
08da94b
🤖 tests(config): consolidate redundant coverage
ibetitsmike Aug 29, 2026
d8fce1b
🤖 refactor(config): align extracted stores with static checks
ibetitsmike Aug 29, 2026
20b3cba
🤖 tests(config): repair extracted store harnesses
ibetitsmike Aug 30, 2026
0390656
🤖 fix(config): preserve custom session locators
ibetitsmike Aug 30, 2026
baeaab3
🤖 tests(config): preserve hook replay session path
ibetitsmike Aug 30, 2026
0617008
🤖 refactor(config): align store injection and session paths
ibetitsmike Aug 30, 2026
5d06bef
🤖 refactor(config): trim extracted store comments
ibetitsmike Aug 30, 2026
18ebed5
🤖 tests(config): document invalid sync fixture write
ibetitsmike Aug 30, 2026
af4aef0
🤖 tests(config): update session locator doubles
ibetitsmike Aug 30, 2026
67c464f
🤖 style(config): format rebase-resolution fallout
ibetitsmike Aug 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1488,7 +1488,8 @@ export default defineConfig([
// Temporarily allow sync fs methods in files with existing usage
// TODO: Gradually migrate these to async operations
files: [
"src/node/config.ts",
"src/node/config/index.ts",
"src/node/config/**/*.ts",
"src/cli/debug/**/*.ts",
"src/node/git.ts",
"src/desktop/main.ts",
Expand Down
6 changes: 3 additions & 3 deletions src/cli/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { createCli, FailedToExitError } from "trpc-cli";
import { router } from "@/node/orpc/router";
import { proxifyOrpc } from "./proxifyOrpc";
import type { ORPCContext } from "@/node/orpc/context";
import { Config } from "@/node/config";
import { createConfigStores } from "@/node/config";
import { ServiceContainer } from "@/node/services/serviceContainer";
import { createOrpcServer, type OrpcServer } from "@/node/orpc/server";

Expand All @@ -38,7 +38,7 @@ interface TestServerHandle {
async function createTestServer(authToken?: string): Promise<TestServerHandle> {
// Create temp dir for config
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "mux-cli-test-"));
const config = new Config(tempDir);
const stores = createConfigStores(tempDir);

// Mock BrowserWindow
const mockWindow: BrowserWindow = {
Expand All @@ -51,7 +51,7 @@ async function createTestServer(authToken?: string): Promise<TestServerHandle> {
} as unknown as BrowserWindow;

// Initialize services
const services = new ServiceContainer(config);
const services = new ServiceContainer(stores);
await services.initialize();
services.windowService.setMainWindow(mockWindow);

Expand Down
4 changes: 2 additions & 2 deletions src/cli/debug/costs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as fs from "fs";
import * as path from "path";
import * as fs from "fs";
import { defaultConfig } from "@/node/config";
import type { MuxMessage } from "@/common/types/message";
import { calculateTokenStats } from "@/common/utils/tokens/tokenStatsCalculator";
Expand All @@ -14,7 +14,7 @@ export async function costsCommand(workspaceId: string) {
console.log(`\n=== Cost Statistics for workspace: ${workspaceId} ===\n`);

// Load chat history
const sessionDir = defaultConfig.getSessionDir(workspaceId);
const sessionDir = path.join(defaultConfig.sessionsDir, workspaceId);
const chatHistoryPath = path.join(sessionDir, "chat.jsonl");

if (!fs.existsSync(chatHistoryPath)) {
Expand Down
3 changes: 2 additions & 1 deletion src/cli/debug/refinements.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as path from "path";
import { defaultConfig } from "@/node/config";
import {
MemoryRefinementActionSchema,
Expand Down Expand Up @@ -46,7 +47,7 @@ export async function refinementsCommand(
workspaceId: string,
opts: RefinementsCommandOptions = {}
): Promise<void> {
const sessionDir = opts.sessionDir ?? defaultConfig.getSessionDir(workspaceId);
const sessionDir = opts.sessionDir ?? path.join(defaultConfig.sessionsDir, workspaceId);

if (opts.rollback !== undefined) {
const result = await rollbackRefinement({
Expand Down
4 changes: 2 additions & 2 deletions src/cli/debug/replay-history.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env bun
import * as path from "path";

/**
* Debug script to replay a chat history and send a new message.
Expand All @@ -12,7 +13,6 @@
*/

import * as fs from "fs";
import * as path from "path";
import { parseArgs } from "util";
import { defaultConfig } from "@/node/config";
import type { MuxMessage } from "@/common/types/message";
Expand Down Expand Up @@ -89,7 +89,7 @@ async function main() {

// Create a temporary workspace
const workspaceId = `debug-replay-${Date.now()}`;
const sessionDir = defaultConfig.getSessionDir(workspaceId);
const sessionDir = path.join(defaultConfig.sessionsDir, workspaceId);
fs.mkdirSync(sessionDir, { recursive: true });

// Create workspace metadata
Expand Down
2 changes: 1 addition & 1 deletion src/cli/debug/replay-verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function resolveReplaySessionDir(workspaceId: string): {
};
}
return {
sessionDir: defaultConfig.getSessionDir(workspaceId),
sessionDir: path.join(defaultConfig.sessionsDir, workspaceId),
historyService: new HistoryService(defaultConfig),
};
}
Expand Down
4 changes: 2 additions & 2 deletions src/cli/debug/send-message.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as fs from "fs";
import * as path from "path";
import * as fs from "fs";
import { defaultConfig } from "@/node/config";
import type { MuxMessage } from "@/common/types/message";
import type { SendMessageOptions } from "@/common/orpc/types";
Expand All @@ -23,7 +23,7 @@ export function sendMessageCommand(
console.log();

// Load chat history to verify message exists if editing
const sessionDir = defaultConfig.getSessionDir(workspaceId);
const sessionDir = path.join(defaultConfig.sessionsDir, workspaceId);
const chatHistoryPath = path.join(sessionDir, "chat.jsonl");

if (!fs.existsSync(chatHistoryPath)) {
Expand Down
32 changes: 22 additions & 10 deletions src/cli/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { tool } from "ai";
import { z } from "zod";
import * as path from "path";
import * as fs from "fs/promises";
import { Config } from "../node/config";
import { createConfigStores } from "../node/config";
import { materializeResolvedTrust, replaceRunTrustProjects } from "./trust";
import { runBestEffortCleanup } from "./runCleanup";
import { DisposableTempDir } from "../node/services/tempDir";
Expand Down Expand Up @@ -501,7 +501,8 @@ async function main(): Promise<number> {
using tempDir = new DisposableTempDir("mux-run");

// Read credentials from the real config, then copy them into the private run config.
const realConfig = new Config();
const realStores = createConfigStores();
const realConfig = realStores.config;

// Session telemetry uses the private root by default. Benchmark/CI harnesses can pin it
// to collect chat.jsonl and session-usage.json after the process exits.
Expand All @@ -513,10 +514,14 @@ async function main(): Promise<number> {
return 1;
}
await using preparedSessionRoot = sessionRootOverride;
const config = await createRunConfig(tempDir.path, preparedSessionRoot);
const preparedConfig = await createRunConfig(tempDir.path, preparedSessionRoot);
const runStores = createConfigStores(preparedConfig.rootDir);
const config = runStores.config;

// Copy providers and secrets from real config to ephemeral config
const existingProviders = realConfig.loadProvidersConfig();
const realProvidersStore = realStores.providersConfigStore;
const runProvidersStore = runStores.providersConfigStore;
const existingProviders = realProvidersStore.loadProvidersConfig();
const providersFile = path.join(config.rootDir, "providers.jsonc");
await replacePrivateRunConfigFile(
providersFile,
Expand All @@ -526,7 +531,7 @@ async function main(): Promise<number> {
);

// Copy secrets so tools/MCP servers get project secrets (e.g., GH_TOKEN)
const existingSecrets = realConfig.loadSecretsConfig();
const existingSecrets = realStores.secretsStore.loadSecretsConfig();
const secretsFile = path.join(config.rootDir, "secrets.json");
await replacePrivateRunConfigFile(
secretsFile,
Expand Down Expand Up @@ -618,7 +623,7 @@ async function main(): Promise<number> {
if (!hasAnyConfiguredProvider(existingProviders)) {
const providersFromEnv = buildProvidersFromEnv();
if (hasAnyConfiguredProvider(providersFromEnv)) {
config.saveProvidersConfig(providersFromEnv);
runProvidersStore.saveProvidersConfig(providersFromEnv);
} else {
throw new Error(
"No provider credentials found. Configure providers.jsonc or set ANTHROPIC_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY / MOONSHOT_API_KEY."
Expand Down Expand Up @@ -654,7 +659,7 @@ async function main(): Promise<number> {
streamManager,
turnRequestBuilderBindings,
} = createCoreServices({
config,
...runStores,
policyService,
extensionMetadataPath: path.join(tempDir.path, "extensionMetadata.json"),
// Session config lives in tempDir (deleted on exit) — disable workspace.*
Expand All @@ -676,16 +681,23 @@ async function main(): Promise<number> {
// `xum run` uses createCoreServices directly (without ServiceContainer), so wire
// Codex OAuth explicitly to ensure Codex-routed OpenAI requests can load/refresh
// OAuth tokens from providers.jsonc.
const codexOauthService = new CodexOauthService(config, providerService);
const codexOauthService = new CodexOauthService(runProvidersStore, providerService);
turnRequestBuilderBindings.codexOauthService = codexOauthService;
// Same for Coder OAuth: coder:* models need per-request token loading/refresh.
// Bind it to the REAL config (not the ephemeral tempDir copy): Coder rotates
// the refresh token on every use, so persisting rotations only to tempDir
// would strand ~/.xum/providers.jsonc with a consumed (dead) refresh token
// once this CLI session exits.
const realProviderService = new ProviderService(realConfig, policyService);
const coderOauthService = new CoderOauthService(
const realFileLeaseManager = realStores.fileLeaseManager;
const realProviderService = new ProviderService(
realConfig,
policyService,
realProvidersStore,
realFileLeaseManager
);
const coderOauthService = new CoderOauthService(
realProvidersStore,
realFileLeaseManager,
realProviderService,
undefined,
// Policy-aware: an enforced forcedBaseUrl overrides the deployment URL for
Expand Down
8 changes: 5 additions & 3 deletions src/cli/runSessionRoot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { Config } from "@/node/config";
import { Config, ProvidersConfigStore } from "@/node/config";
import {
createRunConfig,
prepareRunSessionRootOverride,
Expand Down Expand Up @@ -62,7 +62,7 @@ describe("prepareRunSessionRootOverride", () => {
path.join(config.rootDir, "secrets.json"),
JSON.stringify({ token: "secret-value" })
);
const sessionDir = config.getSessionDir("workspace-1");
const sessionDir = path.join(config.sessionsDir, "workspace-1");
await fs.mkdir(sessionDir, { recursive: true });
await fs.writeFile(path.join(sessionDir, "chat.jsonl"), "chat");
await fs.writeFile(path.join(sessionDir, "session-usage.json"), "usage");
Expand Down Expand Up @@ -155,7 +155,9 @@ describe("prepareRunSessionRootOverride", () => {
JSON.stringify({ openai: { apiKey: "attacker-key", baseUrl: "https://attacker.example" } })
);

expect(config.loadProvidersConfig()?.openai?.baseUrl).toBe("https://safe.example");
expect(new ProvidersConfigStore(config.rootDir).loadProvidersConfig()?.openai?.baseUrl).toBe(
"https://safe.example"
);
expect(config.loadConfigOrDefault().projects.get("/trusted-project")?.trusted).toBe(true);
const replacementProviders = JSON.parse(
await fs.readFile(path.join(runRoot, "providers.jsonc"), "utf8")
Expand Down
6 changes: 3 additions & 3 deletions src/cli/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import type { BrowserWindow, WebContents } from "electron";

import { type AppRouter } from "@/node/orpc/router";
import type { ORPCContext } from "@/node/orpc/context";
import { Config } from "@/node/config";
import { createConfigStores } from "@/node/config";
import { ServiceContainer } from "@/node/services/serviceContainer";
import type { RouterClient } from "@orpc/server";
import { createOrpcServer, type OrpcServer } from "@/node/orpc/server";
Expand All @@ -43,7 +43,7 @@ interface TestServerHandle {
async function createTestServer(): Promise<TestServerHandle> {
// Create temp dir for config
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "mux-server-test-"));
const config = new Config(tempDir);
const stores = createConfigStores(tempDir);

// Mock BrowserWindow
const mockWindow: BrowserWindow = {
Expand All @@ -56,7 +56,7 @@ async function createTestServer(): Promise<TestServerHandle> {
} as unknown as BrowserWindow;

// Initialize services
const services = new ServiceContainer(config);
const services = new ServiceContainer(stores);
await services.initialize();
services.windowService.setMainWindow(mockWindow);

Expand Down
7 changes: 4 additions & 3 deletions src/cli/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Uses ServerService for server lifecycle management.
*/
import "source-map-support/register";
import { Config } from "@/node/config";
import { createConfigStores } from "@/node/config";
import { ServiceContainer } from "@/node/services/serviceContainer";
import { setOpenSSHHostKeyPolicyMode } from "@/node/runtime/sshConnectionPool";
import { cleanupObsoleteXumBinArtifacts, getXumHome } from "@/common/constants/paths";
Expand Down Expand Up @@ -127,8 +127,9 @@ async function main(): Promise<void> {
process.exit(1);
}

const config = new Config();
const serviceContainer = new ServiceContainer(config);
const stores = createConfigStores();
const config = stores.config;
const serviceContainer = new ServiceContainer(stores);
// Headless server has no interactive host-key dialog
setOpenSSHHostKeyPolicyMode("headless-fallback");
await serviceContainer.initialize();
Expand Down
52 changes: 35 additions & 17 deletions src/cli/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import { defaultModel } from "@/common/utils/ai/models";
import { normalizeModelInput } from "@/common/utils/ai/normalizeModelInput";
import { getErrorMessage } from "@/common/utils/errors";
import { resolveThinkingInput } from "@/common/utils/thinking/policy";
import { Config } from "@/node/config";
import { createConfigStores } from "@/node/config";
import type { Config, ConfigStores } from "@/node/config";
import { createRuntime } from "@/node/runtime/runtimeFactory";
import { AgentSession } from "@/node/services/agentSession";
import { CodexOauthService } from "@/node/services/codexOauthService";
Expand Down Expand Up @@ -207,14 +208,20 @@ function generateWorkspaceId(): string {
return `workflow-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}

async function copyPersistentConfig(realConfig: Config, config: Config): Promise<void> {
const existingProviders = realConfig.loadProvidersConfig();
async function copyPersistentConfig(
realStores: ConfigStores,
runStores: ConfigStores
): Promise<void> {
const realConfig = realStores.config;
const config = runStores.config;
const realProvidersStore = realStores.providersConfigStore;
const existingProviders = realProvidersStore.loadProvidersConfig();
if (existingProviders != null && hasAnyConfiguredProvider(existingProviders)) {
config.saveProvidersConfig(existingProviders);
runStores.providersConfigStore.saveProvidersConfig(existingProviders);
}
const existingSecrets = realConfig.loadSecretsConfig();
const existingSecrets = realStores.secretsStore.loadSecretsConfig();
if (Object.keys(existingSecrets).length > 0) {
await config.saveSecretsConfig(existingSecrets);
await runStores.secretsStore.saveSecretsConfig(existingSecrets);
}

const existingConfig = realConfig.loadConfigOrDefault();
Expand Down Expand Up @@ -336,15 +343,20 @@ async function createWorkflowContext(options: {
let realProviderService: ProviderService | undefined;
let policyService: PolicyService | undefined;
try {
const realConfig = new Config();
const config = new Config(tempDir.path);
await copyPersistentConfig(realConfig, config);

const existingProviders = realConfig.loadProvidersConfig();
const realStores = createConfigStores();
const realConfig = realStores.config;
const runStores = createConfigStores(tempDir.path);
const config = runStores.config;
await copyPersistentConfig(realStores, runStores);

const realProvidersStore = realStores.providersConfigStore;
const realFileLeaseManager = realStores.fileLeaseManager;
const runProvidersStore = runStores.providersConfigStore;
const existingProviders = realProvidersStore.loadProvidersConfig();
if (!hasAnyConfiguredProvider(existingProviders)) {
const providersFromEnv = buildProvidersFromEnv();
if (hasAnyConfiguredProvider(providersFromEnv)) {
config.saveProvidersConfig(providersFromEnv);
runProvidersStore.saveProvidersConfig(providersFromEnv);
}
}

Expand All @@ -362,20 +374,26 @@ async function createWorkflowContext(options: {
await policyService.initialize();

services = createCoreServices({
config,
...runStores,
policyService,
extensionMetadataPath: path.join(tempDir.path, "extensionMetadata.json"),
mcpConfig: realConfig,
});
codexOauthService = new CodexOauthService(config, services.providerService);
codexOauthService = new CodexOauthService(runProvidersStore, services.providerService);
services.turnRequestBuilderBindings.codexOauthService = codexOauthService;
// Bind Coder OAuth to the REAL config (not the ephemeral tempDir copy):
// Coder rotates the refresh token on every use, so persisting rotations
// only to tempDir would strand ~/.xum/providers.jsonc with a consumed
// (dead) refresh token once this CLI session exits.
realProviderService = new ProviderService(realConfig, policyService);
coderOauthService = new CoderOauthService(
realProviderService = new ProviderService(
realConfig,
policyService,
realProvidersStore,
realFileLeaseManager
);
coderOauthService = new CoderOauthService(
realProvidersStore,
realFileLeaseManager,
realProviderService,
undefined,
// Policy-aware: an enforced forcedBaseUrl overrides the deployment URL
Expand Down Expand Up @@ -462,7 +480,7 @@ function createWorkflowService(input: {
workspaceName: input.ctx.workspaceId,
workspacePath: input.ctx.workspacePath,
});
const workspaceSessionDir = input.ctx.config.getSessionDir(input.ctx.workspaceId);
const workspaceSessionDir = path.join(input.ctx.config.sessionsDir, input.ctx.workspaceId);

return new WorkflowService({
runStore: new WorkflowRunStore({ sessionDir: workspaceSessionDir }),
Expand Down
Loading
Loading