mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 00:05:49 +02:00
fix: preserve TOML table semantics during Codex startup trust
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
598e190cb3
commit
a200495cc7
3 files changed
+110
-11
No files matched your search
@@ -129,6 +129,52 @@ and require fresh Linux CI. Keep ARM startup sign-off unresolved until proven
|
||||
on a compatible target; package installation and direct execution alone do not
|
||||
qualify the native runner. All Docker/native builds remain remote.
|
||||
|
||||
Checkpoint `598e190cb308f4d7f8f43c188c2e7d88c3fc90d1` passes
|
||||
[ordinary CI](https://github.com/paperclipai/paperclip/actions/runs/38279960223):
|
||||
both required gates, 54 successful checks/statuses, and four conditional skips.
|
||||
Fresh review is 5/5; all 15 review threads are resolved. Human code-owner approval
|
||||
is still absent. The test-drive serves `2026.1005.0+350.git.598e190cb3`; all four
|
||||
execution configurations and both historical native runs survive restart.
|
||||
The verified Apple Silicon daemon remains unchanged, `needsBuild: false`, with
|
||||
no binary override, native compilation, Docker build, or provider turn locally.
|
||||
Receipts: `/private/tmp/ci-final-598e190-ci-review-receipt.json` and
|
||||
`/private/tmp/pr2-final-preservation-after.json`.
|
||||
|
||||
The [direct/native ARM diagnostic](https://github.com/paperclipai/paperclip/actions/runs/38280515437)
|
||||
records GNU libc 2.39, AppArmor enabled with restricted unprivileged user
|
||||
namespaces, an unconfined current profile, zero effective capabilities, and no
|
||||
seccomp filter. Native startup still fails at bubblewrap loopback `RTM_NEWADDR`.
|
||||
The direct control stopped before Codex spawn at a TOML preservation guard,
|
||||
so it does not establish the sandbox failure's cause. Receipt:
|
||||
`/private/tmp/ci-final-arm-direct-diagnostic-598-receipt.json`.
|
||||
|
||||
Pure, integrity-bound public `smol-toml` 1.9.1 reproduction identifies that guard
|
||||
failure: its null-prototype tables differ from the writer's spread-created
|
||||
ordinary objects. Preserve the parser's tables while changing only the intended
|
||||
trust field; keep strict comparison, original comments/bytes, private writes,
|
||||
and malformed-input rejection. No parser pin, semantic bypass, or permission
|
||||
change. The existing suite passes 22 controls across both table representations;
|
||||
real 1.9.1 treatment repairs five valid configurations and keeps two invalid
|
||||
configurations unchanged. Runner/server typechecks pass. Receipt:
|
||||
`/private/tmp/codex-default-startup-trust-fix-receipt.json`.
|
||||
|
||||
The [four-target performance qualifier](https://github.com/paperclipai/paperclip/actions/runs/38281631416)
|
||||
uses literal `598e190` product/workflow source. Observed Linux x64/ARM64 and Apple
|
||||
Silicon builds pass with actual concurrent JavaScript/native preparation.
|
||||
Cache restore is exercised; observed results are cold misses, not measured
|
||||
speedups. The cache writer is reachable through the existing verified master
|
||||
canary push, with exact source and repository guards; no write-gate expansion
|
||||
is needed. Linux daemon bytes match the measured GNU 2.39 baseline. Remaining
|
||||
producer/consumer results must be recorded before qualification claims.
|
||||
|
||||
Next action: freeze the narrow trust correction, repeat the bounded direct
|
||||
control with explicit corrected-tooling provenance, and retain native startup
|
||||
as mandatory. A private namespace negative control and bounded read-only
|
||||
AppArmor journal may help distinguish host denial. Do not modify host policy,
|
||||
relax Codex's sandbox, or substitute direct execution for native qualification.
|
||||
Require fresh CI/review after the correction. ARM startup and managed staging
|
||||
remain unresolved; no merge or production recommendation is made.
|
||||
|
||||
## Current addition: Linux ARM64
|
||||
|
||||
The user now requires ARM Linux support as well. Treat ARM Linux as ARM64
|
||||
|
||||
@@ -5,16 +5,66 @@ import {
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
realpathSync,
|
||||
statSync,
|
||||
symlinkSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { beforeEach, afterEach, describe, it, expect, vi } from "vitest";
|
||||
import { parse } from "smol-toml";
|
||||
import { trustCodexStartupRoot } from "./codex-startup-trust.js";
|
||||
|
||||
describe("isolated Codex startup trust", () => {
|
||||
const parserStyle = vi.hoisted(() => ({ nullPrototypeTables: false }));
|
||||
vi.mock("smol-toml", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("smol-toml")>();
|
||||
const setTablePrototypes = (value: unknown): void => {
|
||||
if (value === null || typeof value !== "object" || value instanceof Date) return;
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(setTablePrototypes);
|
||||
return;
|
||||
}
|
||||
Object.values(value).forEach(setTablePrototypes);
|
||||
Object.setPrototypeOf(value, parserStyle.nullPrototypeTables ? null : Object.prototype);
|
||||
};
|
||||
return {
|
||||
...actual,
|
||||
parse: (...args: Parameters<typeof actual.parse>) => {
|
||||
const result = actual.parse(...args);
|
||||
// Exercise both table representations independently of the installed
|
||||
// parser version while retaining the real TOML values.
|
||||
setTablePrototypes(result);
|
||||
return result;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{ name: "ordinary tables", nullPrototypeTables: false },
|
||||
{ name: "null-prototype tables", nullPrototypeTables: true },
|
||||
])("isolated Codex startup trust with $name", ({ nullPrototypeTables }) => {
|
||||
beforeEach(() => { parserStyle.nullPrototypeTables = nullPrototypeTables; });
|
||||
afterEach(() => { parserStyle.nullPrototypeTables = false; });
|
||||
|
||||
it.each(["missing", "empty"])("initializes %s config in an isolated home", (kind) => {
|
||||
const temp = realpathSync(mkdtempSync(join(tmpdir(), "codex-trust-empty-")));
|
||||
try {
|
||||
const cwd = join(temp, "project");
|
||||
const home = join(temp, "home");
|
||||
mkdirSync(cwd);
|
||||
if (kind === "empty") {
|
||||
mkdirSync(home);
|
||||
writeFileSync(join(home, "config.toml"), "");
|
||||
}
|
||||
trustCodexStartupRoot(home, cwd);
|
||||
const path = join(home, "config.toml");
|
||||
const updated = readFileSync(path, "utf8");
|
||||
expect(parse(updated)).toEqual({ projects: { [cwd]: { trust_level: "trusted" } } });
|
||||
expect(statSync(path).mode & 0o777).toBe(0o600);
|
||||
trustCodexStartupRoot(home, cwd);
|
||||
expect(readFileSync(path, "utf8")).toBe(updated);
|
||||
} finally { rmSync(temp, { recursive: true, force: true }); }
|
||||
});
|
||||
it.each(["missing", "table", "empty-table", "inline", "empty-inline", "inline-projects"])(
|
||||
"preserves managed comments and unrelated content with %s project configuration", (kind) => {
|
||||
const temp = realpathSync(mkdtempSync(join(tmpdir(), "codex-trust-comments-")));
|
||||
@@ -60,15 +110,17 @@ describe("isolated Codex startup trust", () => {
|
||||
mkdirSync(home);
|
||||
writeFileSync(
|
||||
join(home, "config.toml"),
|
||||
'model = "test"\n[mcp_servers.fixture]\nurl = "http://localhost/example"\n',
|
||||
'model = "test"\nstarted = 2026-10-12T12:00:00Z\nlimits = [inf, nan, 9007199254740991]\n[mcp_servers.fixture]\nurl = "http://localhost/example"\n',
|
||||
);
|
||||
const original = readFileSync(join(home, "config.toml"), "utf8");
|
||||
const unrelated = parse(original);
|
||||
trustCodexStartupRoot(home, cwd);
|
||||
trustCodexStartupRoot(home, cwd);
|
||||
expect(parse(readFileSync(join(home, "config.toml"), "utf8"))).toEqual({
|
||||
model: "test",
|
||||
mcp_servers: { fixture: { url: "http://localhost/example" } },
|
||||
...unrelated,
|
||||
projects: { [cwd]: { trust_level: "trusted" } },
|
||||
});
|
||||
expect(readFileSync(join(home, "config.toml"), "utf8")).toContain(original);
|
||||
expect(readFileSync(join(home, "config.toml"), "utf8")).not.toContain(
|
||||
`[projects."${temp}"]`,
|
||||
);
|
||||
|
||||
@@ -92,24 +92,25 @@ export function trustCodexStartupRoot(codexHome: string, cwd: string): void {
|
||||
const path = join(codexHome, "config.toml");
|
||||
const source = existsSync(path) ? readFileSync(path, "utf8") : "";
|
||||
const config = parse(source);
|
||||
const projects = config.projects ?? {};
|
||||
// Retain the parser's table prototypes so strict candidate comparison also
|
||||
// works with parsers that return null-prototype TOML tables.
|
||||
const projects = config.projects ?? parse("");
|
||||
if (
|
||||
typeof projects !== "object" ||
|
||||
Array.isArray(projects) ||
|
||||
projects instanceof Date
|
||||
)
|
||||
throw new Error("codex_startup_trust_invalid_projects");
|
||||
const project = projects[root] ?? {};
|
||||
const project = projects[root] ?? parse("");
|
||||
if (
|
||||
typeof project !== "object" ||
|
||||
Array.isArray(project) ||
|
||||
project instanceof Date
|
||||
)
|
||||
throw new Error("codex_startup_trust_invalid_project");
|
||||
config.projects = {
|
||||
...projects,
|
||||
[root]: { ...project, trust_level: "trusted" },
|
||||
};
|
||||
project.trust_level = "trusted";
|
||||
projects[root] = project;
|
||||
config.projects = projects;
|
||||
const updated = editTrust(source, root, config);
|
||||
if (updated === source) return;
|
||||
const temporary = resolve(codexHome, `config.toml.${randomUUID()}.tmp`);
|
||||
|
||||
Reference in new issue
Block a user