Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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: 3 additions & 0 deletions docs-site/src/content/docs/guides/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,9 @@ the provider store moves to `<dataBaseDir>/.zcode/v2/provider_config.json`.
`ZCODE_DATA_BASE_DIR` is a separate native selector and is not an alias for `ZCODE_DATA_DIR`.

Unreadable, malformed or relative bootstrap settings leave the active root unknown.
The bootstrap must be a regular file of at most 4 MiB; symbolic links and special files
are refused during export and integration path discovery. Repair the bootstrap file or
supply an explicit `ZCODE_DATA_DIR` to select the intended data root.
Apply, overwrite and refresh refuse with a repair message instead of claiming a successful
legacy write. An existing explicit provider file can still establish the active store.
Ownership recorded at the original default config or store keeps its path, fingerprints
Expand Down
6 changes: 2 additions & 4 deletions src/clients/config-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "nod
import { basename, dirname, isAbsolute, join, resolve, win32 } from "node:path";
import { shouldInjectApiAuthHeader, standaloneCodexRoutingTarget } from "../codex/inject";
import { FORMAT_MEDIA_TYPE, serializeDocument, type ConfigFormat } from "../integrations/serialize";
import { fileIO } from "../integrations/config-io";
import { canonicalizeReasoningEfforts } from "../reasoning-effort";
import { expandFastExportModels } from "./config-export/fast-models";
import { probeHostname } from "../server/proxy-liveness";
Expand Down Expand Up @@ -475,10 +476,7 @@ export type ZcodePathIO = {
readText: (path: string) => { kind: "text"; text: string } | { kind: "missing" } | { kind: "failed"; code?: string };
};
const zcodePathIO: ZcodePathIO = {
readText: path => {
try { return { kind: "text", text: readFileSync(path, "utf8") }; }
catch (error) { return (error as NodeJS.ErrnoException).code === "ENOENT" ? { kind: "missing" } : { kind: "failed" }; }
},
readText: path => fileIO().readText(path),
};
export class ZcodeSettingsPathError extends ClientPathError {}

Expand Down
1 change: 1 addition & 0 deletions structure/clients/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ observed. Naming the store without the last three would be naming a file we cann

ZCode resolves absolute `dataBaseDir` from the default-home `~/.zcode/v2/setting.json` through the caller's `IntegrationIO`, moving data to `<base>/.zcode/v2`. `ZCODE_DATA_DIR` overrides the `.zcode` root directly; `ZCODE_PERSONAL_PROVIDER_CONFIG_FILE` overrides the store. Native `ZCODE_DATA_BASE_DIR` is not an alias.
The registry resolves config, detection and store from one bootstrap read. Synchronous observations re-read after coordinator awaits; store-lock and restore probes share the injected IO. Malformed, unreadable or relative settings refuse apply/overwrite/refresh as an unknown active root. Only a present explicit provider file can establish the store independently.
Default ZCode bootstrap path resolution uses the same bounded regular-file reader as integration operations. A final symlink, FIFO or file above 4 MiB leaves the active root unknown; only a missing bootstrap file permits the default-root fallback. An explicit `ZCODE_DATA_DIR` still bypasses bootstrap discovery.

Owned legacy config and default-root store paths retain their fingerprints and recovery evidence after relocation: refresh refuses, disable and undo use the original file, and an older undo refuses while a different path owns the record. Unknown settings retain recognized historical recovery targets; an unrecognized prior selector must be restored before mutation. Contract: `tests/clients/integrations-current-store.test.ts`.

Expand Down
61 changes: 60 additions & 1 deletion tests/providers/zcode-client.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { describe, expect, test } from "bun:test";
import { join } from "node:path";
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import {
ClientPathError,
LOOPBACK_API_KEY_PLACEHOLDER,
OPENCODE_PROVIDER_ID,
ZcodeSettingsPathError,
buildClientConfig,
buildClientConfigText,
buildClientContribution,
buildZcodeStoreContribution,
zcodeBootstrapSettingsPath,
zcodeConfigPath,
zcodeHomeDir,
type ExportContext,
Expand All @@ -16,6 +21,8 @@ import {
} from "../../src/clients/config-export";
import type { OcxConfig } from "../../src/types";
import { handleZcodeCommand } from "../../src/cli/integrations";
import { repoPath } from "../helpers/repo-root";
import { removeTreeWithRetry } from "../helpers/remove-tree";

const CONFIG = {
port: 10100,
Expand Down Expand Up @@ -298,3 +305,55 @@ describe("ocx zcode CLI alias", () => {
expect(requests.some(r => r.path === "/api/client-integrations/restore" && r.method === "POST" && (r.body as { opId?: string }).opId === "op-1")).toBe(true);
});
});

function withBootstrapFixture(run: (home: string, settings: string) => void): void {
const home = mkdtempSync(join(tmpdir(), "ocx-zcode-bootstrap-"));
const settings = zcodeBootstrapSettingsPath(home);
mkdirSync(dirname(settings), { recursive: true });
try { run(home, settings); } finally { removeTreeWithRetry(home); }
}
test("default ZCode bootstrap lookup preserves absence and ordinary relocation", () => {
withBootstrapFixture((home, settings) => {
expect(zcodeHomeDir({}, home)).toBe(join(home, ".zcode"));
const base = join(home, "relocated");
writeFileSync(settings, JSON.stringify({ dataBaseDir: base }));
expect(zcodeHomeDir({}, home)).toBe(join(base, ".zcode"));
expect(zcodeConfigPath({}, home)).toBe(join(base, ".zcode", "v2", "config.json"));
});
});
test("default ZCode bootstrap lookup enforces the shared four-MiB boundary", () => {
withBootstrapFixture((home, settings) => {
const bounded = "{}" + " ".repeat(4 * 1024 * 1024 - 2);
writeFileSync(settings, bounded);
expect(zcodeHomeDir({}, home)).toBe(join(home, ".zcode"));
writeFileSync(settings, bounded + " ");
expect(() => zcodeHomeDir({}, home)).toThrow(ZcodeSettingsPathError);
expect(readFileSync(settings, "utf8")).toBe(bounded + " ");
expect(zcodeHomeDir({ ZCODE_DATA_DIR: join(home, "explicit") }, home)).toBe(join(home, "explicit"));
});
});
test("default ZCode bootstrap lookup refuses final symlinks without changing the target", () => {
withBootstrapFixture((home, settings) => {
const target = join(home, "target.json");
const content = JSON.stringify({ dataBaseDir: join(home, "relocated") });
writeFileSync(target, content);
symlinkSync(target, settings, "file");
expect(() => zcodeHomeDir({}, home)).toThrow(ZcodeSettingsPathError);
expect(readFileSync(target, "utf8")).toBe(content);
expect(lstatSync(settings).isSymbolicLink()).toBe(true);
unlinkSync(target);
expect(() => zcodeHomeDir({}, home)).toThrow(ZcodeSettingsPathError);
});
});
test.skipIf(process.platform === "win32")("default ZCode bootstrap lookup refuses a FIFO without waiting for a writer", () => {
withBootstrapFixture((home, settings) => {
expect(spawnSync("mkfifo", [settings], { timeout: 2_000 }).status).toBe(0);
const result = spawnSync(process.execPath, ["--eval", `
const { zcodeHomeDir, ZcodeSettingsPathError } = await import("./src/clients/config-export.ts");
try { zcodeHomeDir({}, process.env.ZCODE_BOOTSTRAP_TEST_HOME); process.exitCode = 2; }
catch (error) { if (!(error instanceof ZcodeSettingsPathError)) throw error; }
`], { cwd: repoPath(), env: { ...process.env, ZCODE_BOOTSTRAP_TEST_HOME: home }, timeout: 5_000 });
expect(result.error).toBeUndefined();
expect(result.status).toBe(0);
});
}, 10_000);
Loading