fix: stop global settings resetting via project-local central DBs (#1787)
## Problem Operators intermittently saw **"all my global settings reset"** — including the global concurrency cap surfacing as `gate=semaphore` with a low value in scheduler queue logs. ## Root cause `CentralCore` is supposed to live at `~/.fusion/fusion-central.db`, but `resolveGlobalDir(dir)` returns an explicit dir **verbatim**, and several production call sites passed the **project** `.fusion/` dir: - `store.getSecretsStore()` → `new CentralCore(store.getFusionDir())` - dashboard secrets/proxy/node/secrets-sync/settings-sync routes → `new CentralCore(store.getFusionDir())` Each spawned a **stray per-project central DB** (`<project>/.fusion/fusion-central.db`) seeded with **default** global state (`globalMaxConcurrent=4`, empty secrets, default `centralSettings`) that shadowed the real global DB whenever a read/write hit one of those paths. Confirmed on disk: 14+ stray DBs at default `4` vs the real `~/.fusion` at the operator's actual value. ## Fix - Add `TaskStore.getGlobalSettingsDir()` returning the **resolved global dir** (`string`); route the secrets store + all the affected dashboard routes through it instead of `getFusionDir()`. - `getSecretsStore()` also passes the global dir to `MasterKeyManager` (co-locates the master key; makes the path test-exercisable). - Add a `resolveGlobalDir()` **guard** that throws on a project-local `.fusion/` dir (basename `.fusion` with a `.git` parent), with an explicit `FUSION_ALLOW_PROJECT_LOCAL_GLOBAL_DIR` opt-out for legitimately version-controlled custom global dirs. Inert under VITEST. ## Tests - `global-settings-guard.test.ts` — guard rejects project/worktree `.fusion` dirs, allows home + custom non-repo dirs. - `store-secrets-store-global-dir.test.ts` — **symptom-based**: `getSecretsStore()` creates the central DB in the global dir and **never** spawns a stray project-local `fusion-central.db`. - Added `getGlobalSettingsDir()` to route-test mock stores (the new getter is now called by the routes). ## Operator note Existing stray project-local `fusion-central.db` files (all default/empty in practice) should be removed; on the affected machine they were quarantined to `.legacy-central-db-backup-*` folders. ## Verification - `pnpm --filter @fusion/core run typecheck` / `@fusion/dashboard` typecheck — clean - core guard + regression tests pass; previously-affected route suites (proxy, nodes-sync, browse-directory, secrets-sync) pass (313/313) - `pnpm check:changesets` — clean; eslint on changed files — clean Companion to #1786 (global concurrency slider UI), which is independent. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- stage-review-badge-begin --> --- <a href="https://stagereview.app/Runfusion/Fusion/pull/1787"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://stagereview.app/assets/gh-open-in-stage-dark.svg"> <img src="https://stagereview.app/assets/gh-open-in-stage-light.svg" alt="Open in Stage"> </picture> </a> <!-- stage-review-badge-end --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Fixed intermittent resets of global settings (including the global concurrency cap). * Ensured global settings/secrets are read from and written to the correct global storage location rather than a project-local one. * Added a safeguard to prevent using a project-local settings directory when running inside a repository. * **Tests** * Added regression coverage for global directory resolution/guard behavior. * Updated route and secrets sync tests to validate the global directory selection used for proxying and sync. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Fix global settings (including the global concurrency cap) intermittently resetting to defaults.
|
||||
category: fix
|
||||
dev: Several production call sites built `new CentralCore(store.getFusionDir())`, pointing the central/global DB at the project's `.fusion/` instead of `~/.fusion/` and spawning stray per-project central DBs seeded with default global settings that shadowed real global state. Added `TaskStore.getGlobalSettingsDir()`, routed the secrets store plus the secrets/proxy/node/secrets-sync/settings-sync dashboard routes through it, and added a `resolveGlobalDir()` guard that throws on a project-local `.fusion/` dir (parent is a git repo) so the regression can't silently recur. Existing stray DBs were operator-quarantined.
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { resolveGlobalDir } from "../global-settings.js";
|
||||
@@ -68,3 +68,60 @@ describe("resolveGlobalDir() VITEST guard", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:GlobalDirGuard 2026-06-25-22:30:
|
||||
Regression for the "all my global settings reset" bug: production code that passed a project's `.fusion/` dir (e.g. store.getFusionDir()) to CentralCore/global stores spun up stray per-project central DBs seeded with default global settings that shadowed ~/.fusion. resolveGlobalDir() must refuse a project-local `.fusion/` dir (named `.fusion` inside a git repo) while still accepting the real home global dir and arbitrary non-repo custom dirs. Guard is intentionally inert under VITEST, so these tests clear VITEST to exercise it.
|
||||
*/
|
||||
describe("resolveGlobalDir() project-local .fusion guard", () => {
|
||||
it("throws when handed a project-local .fusion dir inside a git repo", () => {
|
||||
withVitestEnv(undefined, () => {
|
||||
withTempHome((homeDir) => {
|
||||
const projectRoot = join(homeDir, "code", "my-project");
|
||||
mkdirSync(join(projectRoot, ".git"), { recursive: true });
|
||||
const projectFusionDir = join(projectRoot, ".fusion");
|
||||
mkdirSync(projectFusionDir, { recursive: true });
|
||||
|
||||
expect(() => resolveGlobalDir(projectFusionDir)).toThrow(
|
||||
/refusing project-local '\.fusion' directory/,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("also catches a git-worktree project (.git file, not dir)", () => {
|
||||
withVitestEnv(undefined, () => {
|
||||
withTempHome((homeDir) => {
|
||||
const worktreeRoot = join(homeDir, "worktrees", "feature");
|
||||
mkdirSync(worktreeRoot, { recursive: true });
|
||||
writeFileSync(join(worktreeRoot, ".git"), "gitdir: /somewhere/.git/worktrees/feature\n");
|
||||
const worktreeFusionDir = join(worktreeRoot, ".fusion");
|
||||
mkdirSync(worktreeFusionDir, { recursive: true });
|
||||
|
||||
expect(() => resolveGlobalDir(worktreeFusionDir)).toThrow(
|
||||
/refusing project-local '\.fusion' directory/,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("allows the real home global dir", () => {
|
||||
withVitestEnv(undefined, () => {
|
||||
withTempHome((homeDir) => {
|
||||
const homeGlobal = join(homeDir, ".fusion");
|
||||
expect(resolveGlobalDir(homeGlobal)).toBe(homeGlobal);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("allows a custom non-repo global dir (no .git parent)", () => {
|
||||
withVitestEnv(undefined, () => {
|
||||
withTempHome((homeDir) => {
|
||||
const customDir = join(homeDir, "custom-global", ".fusion");
|
||||
mkdirSync(customDir, { recursive: true });
|
||||
// Parent has no `.git`, so it is not a project worktree.
|
||||
expect(resolveGlobalDir(customDir)).toBe(customDir);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { TaskStore } from "../store.js";
|
||||
|
||||
/*
|
||||
FNXC:GlobalDirGuard 2026-06-25-23:05:
|
||||
Symptom-based regression for the "all my global settings reset" bug. The root cause was getSecretsStore() (and dashboard routes) constructing CentralCore with `store.getFusionDir()` (the project's `.fusion/`), which created a stray per-project `fusion-central.db` seeded with default global state that shadowed the real global DB. These tests assert the INVARIANT directly: the secrets store's central DB lands in the resolved GLOBAL dir and NOT inside the project `.fusion/` dir, and that getGlobalSettingsDir() is distinct from getFusionDir(). Surface enumeration: this covers the store/secrets surface; the resolveGlobalDir guard surfaces are covered in global-settings-guard.test.ts.
|
||||
*/
|
||||
describe("TaskStore.getSecretsStore() central DB location (global, not project-local)", () => {
|
||||
let root: string;
|
||||
let globalDir: string;
|
||||
let store: TaskStore;
|
||||
|
||||
beforeEach(async () => {
|
||||
root = mkdtempSync(join(tmpdir(), "fn-secrets-global-dir-"));
|
||||
globalDir = join(root, ".fusion-global-settings");
|
||||
store = new TaskStore(root, globalDir, { inMemoryDb: true });
|
||||
await store.init();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("resolves getGlobalSettingsDir() to the global dir, distinct from getFusionDir()", () => {
|
||||
expect(store.getGlobalSettingsDir()).toBe(globalDir);
|
||||
expect(store.getFusionDir()).toBe(join(root, ".fusion"));
|
||||
expect(store.getGlobalSettingsDir()).not.toBe(store.getFusionDir());
|
||||
});
|
||||
|
||||
it("creates the secrets central DB in the global dir and never in the project .fusion/", async () => {
|
||||
await store.getSecretsStore();
|
||||
|
||||
// The central DB must live in the resolved global dir...
|
||||
expect(existsSync(join(globalDir, "fusion-central.db"))).toBe(true);
|
||||
// ...and must NOT have spawned a stray per-project central DB (the original bug).
|
||||
expect(existsSync(join(store.getFusionDir(), "fusion-central.db"))).toBe(false);
|
||||
});
|
||||
|
||||
it("returns a stable singleton secrets store across calls", async () => {
|
||||
const a = await store.getSecretsStore();
|
||||
const b = await store.getSecretsStore();
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
});
|
||||
@@ -14,9 +14,9 @@
|
||||
*/
|
||||
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { mkdir, readFile, writeFile, rename, chmod } from "node:fs/promises";
|
||||
import { existsSync, mkdirSync, renameSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, realpathSync, renameSync } from "node:fs";
|
||||
import type { GlobalSettings } from "./types.js";
|
||||
import { DEFAULT_GLOBAL_SETTINGS } from "./types.js";
|
||||
import { sanitizeCliAgentsSettings } from "./settings-schema.js";
|
||||
@@ -90,7 +90,44 @@ export function resolveGlobalDir(dir?: string): string {
|
||||
);
|
||||
}
|
||||
|
||||
if (hasExplicitDir) return dir;
|
||||
if (hasExplicitDir) {
|
||||
/*
|
||||
FNXC:GlobalDirGuard 2026-06-25-22:10:
|
||||
Production code must never point the central/global store at a project's `.fusion/` directory. Doing so silently spins up a stray per-project central DB seeded with DEFAULT global settings (globalMaxConcurrent=4, empty global secrets, default centralSettings), which then shadows the real `~/.fusion/fusion-central.db` and manifests as "all my global settings reset". Root cause was call sites passing `store.getFusionDir()` instead of the resolved global dir.
|
||||
Guard heuristic: a project `.fusion` dir is named `.fusion` and lives inside a git repo (its parent has a `.git` dir or worktree file), whereas the home global dir's parent (the home dir) is not a repo. We only flag dirs that differ from the home-resolved global dir, so legitimately-threaded global dirs and test temp dirs are unaffected. Skipped under VITEST (tests pass explicit temp dirs by design).
|
||||
|
||||
FNXC:GlobalDirGuard 2026-06-25-22:55:
|
||||
The heuristic is intentionally conservative but can't perfectly distinguish a project `.fusion` from a legitimately version-controlled custom global dir (e.g. a dotfiles repo with `~/dotfiles/.fusion` + `.git`). To avoid hard-crashing that rare setup, honor an explicit opt-out env var `FUSION_ALLOW_PROJECT_LOCAL_GLOBAL_DIR=true`. This is not reachable via normal production call sites (they resolve to ~/.fusion); it only matters for operators who deliberately configure a custom global dir inside a repo.
|
||||
|
||||
FNXC:GlobalDirGuard 2026-06-26-06:25:
|
||||
Order matters and the home-dir comparison must be normalized:
|
||||
- Run the CHEAP, read-only checks first (basename is `.fusion` AND its parent contains a `.git`). Only if both hold do we call `resolveGlobalDirForHome()` — which can perform a one-time legacy-dir rename — so we never trigger that filesystem side effect on the hot path (every explicit-dir call, e.g. getGlobalSettingsDir() in dashboard routes, previously hit it).
|
||||
- Compare against the home global dir using normalized real paths (realpathSync when the path exists, else resolve()), so a trailing slash, doubled separator, or symlinked home dir doesn't make the legitimate home global dir look like a foreign project dir and trip the guard.
|
||||
*/
|
||||
if (process.env.VITEST !== "true" && process.env.FUSION_ALLOW_PROJECT_LOCAL_GLOBAL_DIR !== "true") {
|
||||
const isFusionDirInsideRepo =
|
||||
basename(dir) === ".fusion" && existsSync(join(dirname(dir), ".git"));
|
||||
if (isFusionDirInsideRepo) {
|
||||
const homeGlobalDir = resolveGlobalDirForHome(getHomeDir());
|
||||
const normalize = (p: string): string => {
|
||||
try {
|
||||
return realpathSync.native(p);
|
||||
} catch {
|
||||
return resolve(p);
|
||||
}
|
||||
};
|
||||
if (normalize(dir) !== normalize(homeGlobalDir)) {
|
||||
throw new Error(
|
||||
`resolveGlobalDir(): refusing project-local '.fusion' directory '${dir}' for the central/global store. ` +
|
||||
"This would create a stray per-project central database seeded with default global settings and silently reset them. " +
|
||||
"Pass the resolved global dir (or omit the argument so it defaults to ~/.fusion); see TaskStore.getGlobalSettingsDir(). " +
|
||||
"If this really is your intended global dir (e.g. a version-controlled dotfiles repo), set FUSION_ALLOW_PROJECT_LOCAL_GLOBAL_DIR=true to override.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
return resolveGlobalDirForHome(getHomeDir());
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ import { normalizeTaskPriority } from "./task-priority.js";
|
||||
import { validateBranchGroupBranchName, filterTasksByBranchGroup } from "./branch-assignment.js";
|
||||
import { allowsAutoMergeProcessing } from "./task-merge.js";
|
||||
import { canAgentTakeImplementationTaskForExplicitRouting } from "./agent-role-policy.js";
|
||||
import { GlobalSettingsStore } from "./global-settings.js";
|
||||
import { GlobalSettingsStore, resolveGlobalDir } from "./global-settings.js";
|
||||
import { Database, SCHEMA_VERSION, toJson, toJsonNullable, fromJson } from "./db.js";
|
||||
import { ArchiveDatabase } from "./archive-db.js";
|
||||
import { detectLegacyData, migrateFromLegacy } from "./db-migrate.js";
|
||||
@@ -16693,6 +16693,17 @@ ${stepsSection}`;
|
||||
return this.fusionDir;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:GlobalDirGuard 2026-06-25-22:12:
|
||||
The resolved GLOBAL settings dir. Distinct from getFusionDir() which is this project's `.fusion/`. Any CentralCore/global-store construction MUST use this, never getFusionDir(); passing the project dir spins up a stray per-project central DB that shadows ~/.fusion and silently resets global settings.
|
||||
|
||||
FNXC:GlobalDirGuard 2026-06-25-22:50:
|
||||
Returns a fully-RESOLVED absolute path (string), not the raw optional field. Resolving here (rather than leaking CentralCore's `undefined → ~/.fusion` default to every caller) makes the contract honest and fires the project-local `.fusion` guard at this call site instead of deferring it to CentralCore construction. Under VITEST `this.globalSettingsDir` is always set to a temp dir, so resolveGlobalDir returns it verbatim and never throws the no-explicit-dir test error.
|
||||
*/
|
||||
getGlobalSettingsDir(): string {
|
||||
return resolveGlobalDir(this.globalSettingsDir);
|
||||
}
|
||||
|
||||
getTasksDir(): string {
|
||||
return this.tasksDir;
|
||||
}
|
||||
@@ -16715,14 +16726,16 @@ ${stepsSection}`;
|
||||
return this.secretsStore;
|
||||
}
|
||||
|
||||
const central = new CentralCore(this.getFusionDir());
|
||||
// FNXC:GlobalDirGuard 2026-06-25-22:13: Secrets live in the GLOBAL central DB (~/.fusion), not this project's `.fusion/`. Use the resolved global dir; passing getFusionDir() created a stray per-project central DB and reset global settings.
|
||||
const central = new CentralCore(this.getGlobalSettingsDir());
|
||||
await central.init();
|
||||
this.secretsCentralCore = central;
|
||||
const centralDb = (central as unknown as { db: import("./central-db.js").CentralDatabase | null }).db;
|
||||
if (!centralDb) {
|
||||
throw new Error("Central database unavailable for secrets store");
|
||||
}
|
||||
const masterKeyManager = new MasterKeyManager();
|
||||
// FNXC:GlobalDirGuard 2026-06-25-23:00: The master key is GLOBAL — pass the resolved global dir explicitly so it co-locates with the global central DB (matching prod ~/.fusion) and so getSecretsStore() is exercisable under tests (a bare new MasterKeyManager() throws under VITEST because resolveGlobalDir() requires an explicit dir there).
|
||||
const masterKeyManager = new MasterKeyManager({ globalDir: this.getGlobalSettingsDir() });
|
||||
const masterKeyProvider = () => masterKeyManager.getOrCreateKey();
|
||||
this.secretsStore = new SecretsStore(this.db, centralDb, masterKeyProvider);
|
||||
return this.secretsStore;
|
||||
|
||||
@@ -40,6 +40,7 @@ vi.mock("@fusion/core", async () => {
|
||||
|
||||
// Import after mocking
|
||||
import { browseDirectory } from "../../app/api.js";
|
||||
import { CentralCore } from "@fusion/core";
|
||||
|
||||
function mockFetchResponse(
|
||||
ok: boolean,
|
||||
@@ -71,6 +72,11 @@ class MockStoreForRoutes extends EventEmitter {
|
||||
return "/tmp/fn-944/.fusion";
|
||||
}
|
||||
|
||||
// FNXC:GlobalDirGuard 2026-06-26-06:25: Return a global dir DISTINCT from getFusionDir() so the regression test can assert the route constructs CentralCore with the global dir — a future revert to getFusionDir() then fails the CentralCore-constructor assertion below instead of silently passing.
|
||||
getGlobalSettingsDir(): string {
|
||||
return "/tmp/fn-944/.fusion-global";
|
||||
}
|
||||
|
||||
getDatabase() {
|
||||
return {
|
||||
exec: vi.fn(),
|
||||
@@ -149,6 +155,8 @@ describe("GET /api/browse-directory route handler", () => {
|
||||
expect(mockListNodes).toHaveBeenCalled();
|
||||
expect(mockClose).toHaveBeenCalled();
|
||||
expect(globalThis.fetch).not.toHaveBeenCalled();
|
||||
// FNXC:GlobalDirGuard 2026-06-26-06:25: The node-aware route must build CentralCore from the GLOBAL dir, never the project `.fusion/`. Asserting the distinct global path makes a revert to getFusionDir() ("/tmp/fn-944/.fusion") fail here.
|
||||
expect(vi.mocked(CentralCore)).toHaveBeenCalledWith("/tmp/fn-944/.fusion-global");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -47,6 +47,11 @@ class MockStore extends EventEmitter {
|
||||
return "/tmp/fn-test/.fusion";
|
||||
}
|
||||
|
||||
// FNXC:GlobalDirGuard 2026-06-25-23:10: Routes resolve the global central dir via getGlobalSettingsDir(); mock mirrors getFusionDir() (CentralCore is mocked) so route behavior matches pre-change.
|
||||
getGlobalSettingsDir(): string {
|
||||
return this.getFusionDir();
|
||||
}
|
||||
|
||||
getDatabase() {
|
||||
return {
|
||||
exec: vi.fn(),
|
||||
|
||||
@@ -88,6 +88,11 @@ class MockStore extends EventEmitter {
|
||||
return "/tmp/fn-4755-test/.fusion";
|
||||
}
|
||||
|
||||
// FNXC:GlobalDirGuard 2026-06-25-23:10: Routes resolve the global central dir via getGlobalSettingsDir(); mock mirrors getFusionDir() (CentralCore is mocked) so route behavior matches pre-change.
|
||||
getGlobalSettingsDir(): string {
|
||||
return this.getFusionDir();
|
||||
}
|
||||
|
||||
getDatabase() {
|
||||
return {
|
||||
exec: vi.fn(),
|
||||
|
||||
@@ -104,6 +104,11 @@ class MockStore extends EventEmitter {
|
||||
return "/tmp/fn-1821-test/.fusion";
|
||||
}
|
||||
|
||||
// FNXC:GlobalDirGuard 2026-06-25-23:10: Routes resolve the global central dir via getGlobalSettingsDir(); mock mirrors getFusionDir() (CentralCore is mocked) so route behavior matches pre-change.
|
||||
getGlobalSettingsDir(): string {
|
||||
return this.getFusionDir();
|
||||
}
|
||||
|
||||
getDatabase() {
|
||||
return {
|
||||
exec: vi.fn(),
|
||||
|
||||
@@ -30,6 +30,11 @@ class MockStore extends EventEmitter {
|
||||
return "/tmp/fn-1806/.fusion";
|
||||
}
|
||||
|
||||
// FNXC:GlobalDirGuard 2026-06-25-23:10: Routes resolve the global central dir via getGlobalSettingsDir(); mock mirrors getFusionDir() (CentralCore is mocked) so route behavior matches pre-change.
|
||||
getGlobalSettingsDir(): string {
|
||||
return this.getFusionDir();
|
||||
}
|
||||
|
||||
getDatabase() {
|
||||
return {
|
||||
exec: vi.fn(),
|
||||
|
||||
@@ -4463,7 +4463,8 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
// Node-aware proxying: route to remote node if nodeId is provided and not local
|
||||
if (nodeId) {
|
||||
const { CentralCore } = await import("@fusion/core");
|
||||
const central = new CentralCore(store.getFusionDir());
|
||||
// FNXC:GlobalDirGuard 2026-06-25-22:40: Node-aware proxy lookup uses GLOBAL central state — use getGlobalSettingsDir(), never getFusionDir() (project .fusion/), which spawns a stray per-project central DB and resets global settings.
|
||||
const central = new CentralCore(store.getGlobalSettingsDir());
|
||||
await central.init();
|
||||
|
||||
const localNodes = await central.listNodes();
|
||||
|
||||
@@ -34,7 +34,8 @@ async function proxyToRemoteNode(
|
||||
const timeoutMs = proxyOptions?.timeoutMs ?? 10_000;
|
||||
|
||||
const { CentralCore } = await import("@fusion/core");
|
||||
const central = new CentralCore(store.getFusionDir());
|
||||
// FNXC:GlobalDirGuard 2026-06-25-22:40: Node proxy state is GLOBAL — use getGlobalSettingsDir(), never getFusionDir() (project .fusion/), which spawns a stray per-project central DB and resets global settings. See register-settings-sync-inbound-routes.ts for full rationale.
|
||||
const central = new CentralCore(store.getGlobalSettingsDir());
|
||||
|
||||
try {
|
||||
await central.init();
|
||||
@@ -187,7 +188,7 @@ export function registerProxyRoutes(router: Router, deps: ProxyRoutesDeps): void
|
||||
const nodeId = req.params.nodeId as string;
|
||||
|
||||
const { CentralCore } = await import("@fusion/core");
|
||||
const central = new CentralCore(store.getFusionDir());
|
||||
const central = new CentralCore(store.getGlobalSettingsDir());
|
||||
|
||||
try {
|
||||
await central.init();
|
||||
@@ -360,7 +361,7 @@ export function registerProxyRoutes(router: Router, deps: ProxyRoutesDeps): void
|
||||
const remainingPath = Array.isArray(splat) ? splat.join("/") : splat;
|
||||
|
||||
const { CentralCore } = await import("@fusion/core");
|
||||
const central = new CentralCore(store.getFusionDir());
|
||||
const central = new CentralCore(store.getGlobalSettingsDir());
|
||||
|
||||
try {
|
||||
await central.init();
|
||||
|
||||
@@ -92,7 +92,8 @@ export const registerSecretsSyncInboundRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
router.post("/secrets/sync-receive", async (req, res) => {
|
||||
try {
|
||||
const { CentralCore } = await import("@fusion/core");
|
||||
const central = new CentralCore(store.getFusionDir());
|
||||
// FNXC:GlobalDirGuard 2026-06-25-22:40: Inbound secrets sync writes GLOBAL central state — use getGlobalSettingsDir(), never getFusionDir() (project .fusion/), which spawns a stray per-project central DB and resets global settings. See register-settings-sync-inbound-routes.ts for full rationale.
|
||||
const central = new CentralCore(store.getGlobalSettingsDir());
|
||||
await central.init();
|
||||
try {
|
||||
// Validate auth
|
||||
@@ -174,7 +175,7 @@ export const registerSecretsSyncInboundRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
router.get("/secrets/sync-export", async (req, res) => {
|
||||
try {
|
||||
const { CentralCore } = await import("@fusion/core");
|
||||
const central = new CentralCore(store.getFusionDir());
|
||||
const central = new CentralCore(store.getGlobalSettingsDir());
|
||||
await central.init();
|
||||
try {
|
||||
// Validate auth
|
||||
|
||||
@@ -52,7 +52,8 @@ export const registerSecretsSyncRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
router.post("/nodes/:id/secrets/push", async (req, res) => {
|
||||
try {
|
||||
const { CentralCore } = await import("@fusion/core");
|
||||
const central = new CentralCore(store.getFusionDir());
|
||||
// FNXC:GlobalDirGuard 2026-06-25-22:40: Secrets-sync node state is GLOBAL — use getGlobalSettingsDir(), never getFusionDir() (project .fusion/), which spawns a stray per-project central DB and resets global settings. See register-settings-sync-inbound-routes.ts for full rationale.
|
||||
const central = new CentralCore(store.getGlobalSettingsDir());
|
||||
await central.init();
|
||||
try {
|
||||
const node = await central.getNode(req.params.id);
|
||||
@@ -110,7 +111,7 @@ export const registerSecretsSyncRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
router.post("/nodes/:id/secrets/pull", async (req, res) => {
|
||||
try {
|
||||
const { CentralCore } = await import("@fusion/core");
|
||||
const central = new CentralCore(store.getFusionDir());
|
||||
const central = new CentralCore(store.getGlobalSettingsDir());
|
||||
await central.init();
|
||||
try {
|
||||
const node = await central.getNode(req.params.id);
|
||||
|
||||
@@ -74,7 +74,8 @@ export const registerSettingsSyncInboundRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
router.post("/settings/sync-receive", async (req, res) => {
|
||||
try {
|
||||
const { CentralCore } = await import("@fusion/core");
|
||||
const central = new CentralCore(store.getFusionDir());
|
||||
// FNXC:GlobalDirGuard 2026-06-25-22:20: Inbound settings sync writes GLOBAL central state, so it must use the resolved global dir (~/.fusion). Previously this (and the secrets/proxy/node routes) passed store.getFusionDir() — the project `.fusion/` — which created a stray per-project central DB seeded with default global settings, the root cause of intermittent "all my global settings reset". Mirror this requirement on every CentralCore construction in dashboard routes.
|
||||
const central = new CentralCore(store.getGlobalSettingsDir());
|
||||
await central.init();
|
||||
|
||||
// Validate auth - find local node and check apiKey
|
||||
@@ -181,7 +182,7 @@ export const registerSettingsSyncInboundRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
router.post("/settings/auth-receive", async (req, res) => {
|
||||
try {
|
||||
const { CentralCore } = await import("@fusion/core");
|
||||
const central = new CentralCore(store.getFusionDir());
|
||||
const central = new CentralCore(store.getGlobalSettingsDir());
|
||||
await central.init();
|
||||
|
||||
// Validate auth
|
||||
@@ -278,7 +279,7 @@ export const registerSettingsSyncInboundRoutes: ApiRouteRegistrar = (ctx) => {
|
||||
router.get("/settings/auth-export", async (req, res) => {
|
||||
try {
|
||||
const { CentralCore } = await import("@fusion/core");
|
||||
const central = new CentralCore(store.getFusionDir());
|
||||
const central = new CentralCore(store.getGlobalSettingsDir());
|
||||
await central.init();
|
||||
|
||||
// Validate auth
|
||||
|
||||
Reference in New Issue
Block a user