Skip to content
Merged
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
16 changes: 15 additions & 1 deletion .gitleaks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,21 @@ description = "Bundled TUI shortcut and permission translation key"
condition = "AND"
paths = ['''(^|/)dist/chunks/(chunk|launcher)-[A-Z0-9]+\.js$''']
regexTarget = "match"
regexes = ['''^defaultKeys: "ctrl\+shift\+down"$''', '''^labelKey: "permission\.scope\.byArgvPrefix2"$''']
regexes = ['''^defaultKeys: ?"ctrl\+shift\+down"$''', '''^labelKey: ?"permission\.scope\.byArgvPrefix2"$''']

[[rules.allowlists]]
description = "Bundled node-forge public PKCS12 algorithm names"
condition = "AND"
paths = ['''(^|/)dist/chunks/chunk-[A-Z0-9]+\.js$''']
regexTarget = "match"
regexes = ['''^pbeWithSHAAnd3-KeyTripleDES-CBC","pbewithSHAAnd40BitRC2-CBC"$''']

[[rules.allowlists]]
description = "Bundled IM binding migration guard, no credential value"
condition = "AND"
paths = ['''(^|/)dist/chunks/chunk-[A-Z0-9]+\.js$''']
regexTarget = "match"
regexes = ['''^[A-Za-z_$][A-Za-z0-9_$]*\.resolvedProjectKey\|\|[A-Za-z_$][A-Za-z0-9_$]*\.mutationReceipts===void $''']

[[rules.allowlists]]
description = "node-forge PKCS12 function alias, no key material"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
ThinkingContent,
UserMessage,
} from '@earendil-works/pi-ai';
import { convertToLlm } from '@earendil-works/pi-coding-agent';
import { convertToLlm } from '@earendil-works/pi-coding-agent/messages';

import { imageDimensions } from './image-dimensions.js';

Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core/src/pi-turn-runner/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
createEditTool,
createReadTool,
createWriteTool,
} from '@earendil-works/pi-coding-agent';
} from '@earendil-works/pi-coding-agent/tools';
import { createBashEnvSpawnHook, resolveBashEnvPolicy } from '../bash-subprocess-env.js';
import type { TSchema } from '@sinclair/typebox';
import type { RuntimeTool, ToolExecutionContext } from '../tools/index.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* absorbed by the manager's `safetyMarginTokens` / `reserveTokens` headroom.
*/

import { convertToLlm } from '@earendil-works/pi-coding-agent';
import { convertToLlm } from '@earendil-works/pi-coding-agent/messages';
import type { AgentMessage } from '@earendil-works/pi-agent-core';
import type { Api, Model, Tool } from '@earendil-works/pi-ai';

Expand Down
4 changes: 2 additions & 2 deletions packages/agent-tools/src/desktop/local-pi-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {
createEditTool,
createReadTool,
createWriteTool,
getShellConfig,
} from '@earendil-works/pi-coding-agent';
} from '@earendil-works/pi-coding-agent/tools';
import { getShellConfig } from '@earendil-works/pi-coding-agent/shell';
import type { AgentTool, AgentToolResult } from '@earendil-works/pi-agent-core';
import { access } from 'node:fs/promises';
import { isAbsolute, resolve as resolvePath } from 'node:path';
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-tools/src/shared/read-guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
/**
* Replicated from `third_party/pi-mono/packages/coding-agent/src/utils/mime.ts`
* `detectSupportedImageMimeType` — the function is NOT exported from
* `@earendil-works/pi-coding-agent` (package exposes only the root entry),
* `@earendil-works/pi-coding-agent`,
* so we keep a byte-exact copy here. KEEP IN SYNC on pi upstream syncs:
* the whole point of this replica is that the exemption face equals pi's
* image-branch acceptance face (JPEG minus JPEG-LS, PNG minus APNG, GIF,
Expand Down
25 changes: 21 additions & 4 deletions packages/config/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1649,11 +1649,28 @@ function syncManagedPresetBaseUrl(configPath: string): void {
)
return;

const originalContent = fs.readFileSync(configPath);
(options as Record<string, unknown>).baseURL = presetBaseURL;
writePrivateConfigFileSync(
configPath,
yaml.dump(raw, { indent: 2, lineWidth: -1, noRefs: true }),
);
try {
writePrivateConfigFileSync(
configPath,
yaml.dump(raw, { indent: 2, lineWidth: -1, noRefs: true }),
);
} catch (error) {
// This on-disk sync is optional, but a failure after truncation is not safe
// to hide. Only continue when the original document is still intact.
let unchanged = false;
try {
unchanged = fs.readFileSync(configPath).equals(originalContent);
} catch {
// Preserve the original write error if integrity cannot be verified.
}
if (!unchanged) throw error;
// Do not include the error message: config errors may contain credentials.
console.warn(
"[config] managed preset baseURL sync skipped; config file unchanged, using runtime provider settings",
);
}
}

function buildPresetEntry(key: PresetKey) {
Expand Down
12 changes: 7 additions & 5 deletions packages/config/src/private-config-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ export function writePrivateConfigFileSync(
content: string | Buffer,
exclusive = false,
): void {
const fd = fs.openSync(
filePath,
exclusive ? "wx" : "a",
PRIVATE_CONFIG_FILE_MODE,
);
// Append handles cannot be truncated on Windows. Defer truncation until
// permissions have been restricted, including for an existing POSIX file.
const flags =
fs.constants.O_WRONLY |
fs.constants.O_CREAT |
(exclusive ? fs.constants.O_EXCL : 0);
const fd = fs.openSync(filePath, flags, PRIVATE_CONFIG_FILE_MODE);
try {
fs.fchmodSync(fd, PRIVATE_CONFIG_FILE_MODE);
fs.ftruncateSync(fd, 0);
Expand Down
168 changes: 168 additions & 0 deletions packages/config/test/managed-preset-sync.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import fs from "node:fs";
import os from "node:os";
import { join } from "node:path";
import yaml from "js-yaml";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
getConfig,
resetConfig,
setLegacyByokProviderMigrationEnabled,
setManagedPresetBaseUrlSyncEnabled,
} from "../src/config.js";
import { loadConfigFromFile } from "../src/file-loader.js";

const secret = "synthetic-preset-sync-secret";
const oldBaseURL = "https://agent.minimax.io/mavis/api/v1/llm/v1";
const presetBaseURL =
"https://matrix-overseas-pre.example.invalid/mavis/api/v1/llm/v1";
const original = yaml.dump({
logLevel: "debug",
provider: { minimax: { options: { baseURL: oldBaseURL, apiKey: secret } } },
custom_provider: { example: { options: { apiKey: secret }, models: {} } },
});
let root: string;
let file: string;

beforeEach(() => {
root = fs.mkdtempSync(join(os.tmpdir(), "managed-preset-sync-"));
file = join(root, "config.yaml");
vi.stubEnv("__MAVIS_RUNTIME_MANAGED", "1");
vi.stubEnv("__MAVIS_RUNTIME_DATA_DIR", root);
vi.stubEnv("MINIMAX_DATA_DIR", root);
vi.stubEnv("MAVIS_REGION", "en");
vi.stubEnv("MAVIS_BUILD_ENV", "staging");
setLegacyByokProviderMigrationEnabled(false);
setManagedPresetBaseUrlSyncEnabled(true);
resetConfig();
fs.writeFileSync(file, original, { mode: 0o600 });
});

afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllEnvs();
resetConfig();
setLegacyByokProviderMigrationEnabled(true);
setManagedPresetBaseUrlSyncEnabled(true);
fs.rmSync(root, { recursive: true, force: true });
});

describe.each([
["default config", () => getConfig()],
["explicit config", () => loadConfigFromFile(file, { dataDir: root })],
] as const)("%s preset synchronization", (_name, load) => {
it("persists the current preset and preserves user settings", () => {
const config = load();
expect(config.logLevel).toBe("debug");
expect(config.provider.minimax?.options).toMatchObject({
baseURL: presetBaseURL,
apiKey: secret,
});
const persisted = yaml.load(fs.readFileSync(file, "utf8"));
expect(persisted).toMatchObject({
custom_provider: { example: { options: { apiKey: secret } } },
provider: {
minimax: { options: { baseURL: presetBaseURL, apiKey: secret } },
},
});
});

it.each(["openSync", "fchmodSync", "ftruncateSync"] as const)(
"loads the effective preset when %s fails without changing the file",
(operation) => {
const failure = Object.assign(new Error(secret), { code: "EPERM" });
if (operation === "openSync") {
const open = fs.openSync;
vi.spyOn(fs, "openSync").mockImplementation((path, flags, mode) => {
if (
flags === "a" ||
(typeof flags === "number" && flags & fs.constants.O_WRONLY)
) {
throw failure;
}
return open(path, flags, mode);
});
} else {
vi.spyOn(fs, operation).mockImplementation(() => {
throw failure;
});
}
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const config = load();
expect(config.logLevel).toBe("debug");
expect(config.provider.minimax?.options).toMatchObject({
baseURL: presetBaseURL,
apiKey: secret,
});
expect(fs.readFileSync(file, "utf8")).toBe(original);
expect(warn).toHaveBeenCalledTimes(1);
expect(warn).toHaveBeenCalledWith(
expect.stringContaining("preset baseURL sync skipped"),
);
expect(JSON.stringify(warn.mock.calls)).not.toContain(secret);
},
);

it("does not hide a failure after the original file has been truncated", () => {
const failure = new Error("synthetic write failure");
vi.spyOn(fs, "writeFileSync").mockImplementation(() => {
throw failure;
});
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
expect(() => load()).toThrow(failure);
expect(warn).not.toHaveBeenCalled();
expect(fs.readFileSync(file, "utf8")).toBe("");
});

it("preserves the write error when the original file cannot be verified", () => {
const failure = new Error("synthetic truncate failure");
let failed = false;
vi.spyOn(fs, "ftruncateSync").mockImplementation(() => {
failed = true;
throw failure;
});
const read = fs.readFileSync;
vi.spyOn(fs, "readFileSync").mockImplementation((...args) => {
if (failed && args[0] === file) throw new Error("synthetic read failure");
return read(...args);
});
expect(() => load()).toThrow(failure);
});
});

it("honors the disabled preset synchronization policy", () => {
setManagedPresetBaseUrlSyncEnabled(false);
const truncate = vi.spyOn(fs, "ftruncateSync");
expect(getConfig().provider.minimax?.options?.baseURL).toBe(presetBaseURL);
expect(truncate).not.toHaveBeenCalled();
expect(fs.readFileSync(file, "utf8")).toBe(original);
});

it.each([
["prod", oldBaseURL],
["test", presetBaseURL],
] as const)(
"preserves %s provider policy after a safe sync failure",
(buildEnv, expectedBaseURL) => {
vi.stubEnv("MAVIS_BUILD_ENV", buildEnv);
// A staging endpoint needs syncing in both prod and test builds.
fs.writeFileSync(file, original.replace(oldBaseURL, presetBaseURL));
vi.spyOn(fs, "ftruncateSync").mockImplementation(() => {
throw new Error("synthetic failure");
});
vi.spyOn(console, "warn").mockImplementation(() => {});
const config = getConfig();
expect(config.provider.minimax?.options?.baseURL).toBe(expectedBaseURL);
expect(config.provider.minimax?.options?.apiKey).toBe(secret);
},
);

it("still reports failure when creating the required initial config", () => {
fs.unlinkSync(file);
const failure = Object.assign(new Error("synthetic create failure"), {
code: "EPERM",
});
vi.spyOn(fs, "openSync").mockImplementation(() => {
throw failure;
});
expect(() => getConfig()).toThrow(failure);
});
85 changes: 85 additions & 0 deletions packages/config/test/private-config-file.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import fs from "node:fs";
import os from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { writePrivateConfigFileSync } from "../src/private-config-file.js";

let root: string;
let file: string;

beforeEach(() => {
root = fs.mkdtempSync(join(os.tmpdir(), "private-config-write-"));
file = join(root, "config.yaml");
});

afterEach(() => {
vi.restoreAllMocks();
fs.rmSync(root, { recursive: true, force: true });
});

// These use the native filesystem on every OS, including Windows.
describe("private config writes", () => {
it.each([false, true])(
"creates a missing file (exclusive=%s)",
(exclusive) => {
writePrivateConfigFileSync(file, "logLevel: info\n", exclusive);
expect(fs.readFileSync(file, "utf8")).toBe("logLevel: info\n");
if (process.platform !== "win32") {
expect(fs.statSync(file).mode & 0o777).toBe(0o600);
}
},
);

it.each(["logLevel: info\n", Buffer.from("logLevel: info\n")])(
"replaces longer content without appending or retaining a suffix",
(content) => {
fs.writeFileSync(
file,
"logLevel: debug\n# old credentials and trailing data\n",
);
writePrivateConfigFileSync(file, content);
expect(fs.readFileSync(file, "utf8")).toBe("logLevel: info\n");
},
);

it("does not overwrite an existing file in exclusive mode", () => {
fs.writeFileSync(file, "original");
expect(() =>
writePrivateConfigFileSync(file, "replacement", true),
).toThrow();
expect(fs.readFileSync(file, "utf8")).toBe("original");
});

it.each(["fchmodSync", "ftruncateSync"] as const)(
"closes the descriptor and stops writing when %s fails",
(operation) => {
fs.writeFileSync(file, "original");
const failure = Object.assign(new Error("synthetic failure"), {
code: "EPERM",
});
vi.spyOn(fs, operation).mockImplementation(() => {
throw failure;
});
const write = vi.spyOn(fs, "writeFileSync");
const close = vi.spyOn(fs, "closeSync");
expect(() => writePrivateConfigFileSync(file, "replacement")).toThrow(
failure,
);
expect(write).not.toHaveBeenCalled();
expect(close).toHaveBeenCalledTimes(1);
expect(fs.readFileSync(file, "utf8")).toBe("original");
},
);

it("propagates a write failure and closes the descriptor", () => {
const failure = new Error("synthetic write failure");
vi.spyOn(fs, "writeFileSync").mockImplementation(() => {
throw failure;
});
const close = vi.spyOn(fs, "closeSync");
expect(() => writePrivateConfigFileSync(file, "replacement")).toThrow(
failure,
);
expect(close).toHaveBeenCalledTimes(1);
});
});
Loading