Skip to content
Open
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
89 changes: 89 additions & 0 deletions .github/workflows/native-windows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: native-windows

on:
push:
branches: [main]
pull_request:
types: [opened, reopened, synchronize]

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
primitives:
name: primitives / win32-${{ matrix.arch }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- runner: windows-2022
arch: x64
target: x86_64-pc-windows-msvc
- runner: windows-11-arm
arch: arm64
target: aarch64-pc-windows-msvc
defaults:
run:
shell: pwsh
working-directory: plugins/codex-security/native
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
package_json_file: package.json
- name: Set up Node.js 22
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version: "22.13.0"
architecture: ${{ matrix.arch }}
- name: Compile TypeScript tools
working-directory: .
run: |
pnpm --dir sdk/typescript install --frozen-lockfile
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
pnpm --dir sdk/typescript run build:ci
- name: Set up Rust
run: rustup toolchain install 1.97.1 --profile minimal --component rustfmt --component clippy --target ${{ matrix.target }}
- name: Check Rust source
run: |
cargo fmt --check
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
cargo clippy --locked --target ${{ matrix.target }} -- -D warnings
- name: Build and verify with Node.js 22
run: |
node build.mjs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
node check.mjs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$nativeNode = (Get-Command node).Source
$env:PATH = ""
& $nativeNode --expose-gc proof-windows.mjs
- name: Set up Node.js 20
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version: "20.0.0"
architecture: ${{ matrix.arch }}
- name: Verify the same artifact with Node.js 20
run: |
node check.mjs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$nativeNode = (Get-Command node).Source
$env:PATH = ""
& $nativeNode --expose-gc proof-windows.mjs
- name: Upload verified native artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: native-win32-${{ matrix.arch }}-${{ github.sha }}
path: plugins/codex-security/native/dist/win32-${{ matrix.arch }}/windows.node
if-no-files-found: error
retention-days: 7
12 changes: 11 additions & 1 deletion plugins/codex-security/native/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion plugins/codex-security/native/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = "codex-security-native-unix"
name = "codex-security-native"
version = "0.0.0"
edition = "2021"
rust-version = "1.97"
Expand All @@ -12,8 +12,13 @@ crate-type = ["cdylib"]
[dependencies]
napi = { version = "=3.12.2", default-features = false, features = ["napi8"] }
napi-derive = "=3.6.3"

[target.'cfg(unix)'.dependencies]
libc = "=0.2.189"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "=0.61.2", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_IO"] }

[build-dependencies]
napi-build = "=2.4.1"

Expand Down
18 changes: 16 additions & 2 deletions plugins/codex-security/native/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Unix native primitives
# Native OS primitives

This foundation supplies the descriptor operations that Node does not expose. The SDK and CLI continue to use their existing helpers while Windows primitives and universal package assembly are completed.
This foundation supplies the OS operations that Node does not expose. The SDK and CLI continue to use their existing helpers while universal package assembly and migration proofs are completed.

The nine Node-API 8 functions are typed in `binding.mts`. Paths remain byte buffers. `statAt` never follows the final symlink; device and inode numbers are decimal strings so JavaScript does not round them. `openAt` and `duplicate` create descriptors with close-on-exec set. Node owns subsequent reads, writes, `fstat`, `fsync`, and close calls. `userHome` looks up raw username bytes through the operating system and returns raw home-directory bytes or a missing result, without Git.

Expand Down Expand Up @@ -32,3 +32,17 @@ node plugins/codex-security/native/check.mjs
Linux artifacts must import no glibc version newer than 2.28. macOS artifacts must declare a deployment target of 11.0 or earlier. A build from a newer Linux workstation can pass the behavioral proof and still fail this distribution check.

The `native-unix` workflow builds Linux artifacts in digest-pinned manylinux 2.28 images. It mounts the pinned Rust toolchain and fetched Cargo registry, builds offline, and blocks Python commands during compilation. macOS builds set `MACOSX_DEPLOYMENT_TARGET=11.0`. CI verifies separate x64 and arm64 artifacts on both platforms using Node 20.0.0 and 22.13.0. These artifacts are inputs to the later universal-package gate.

Windows uses `windows-binding.mts` and the same Rust crate. `WindowsHandle` owns a non-inheritable Win32 handle; explicit `close()` and garbage collection release it. Handles never cross into Node's CRT descriptor table. Paths and returned names are UTF-16LE buffers without a NUL terminator, preserving lone surrogates. Volume identities and file positions are decimal strings; file IDs retain all 128 bits in a buffer.

The binding exposes synchronous file and directory creation, attributes and reparse tags, identity and final/opened names, read/write/seek/size/EOF/flush, exact-handle rename and deletion, and byte-range locking. Calls return numeric Windows errors. Buffer ranges, path encoding, and 64-bit arguments are checked before FFI calls. Overlapped handles are unsupported because pending operations could retain native buffers beyond the call. Path authorization, ancestor traversal, and reparse-point policy remain the caller's responsibility.

Build on Windows after compiling the TypeScript tools, then run:

```sh
node plugins/codex-security/native/build.mjs
node plugins/codex-security/native/check.mjs
node --expose-gc plugins/codex-security/native/proof-windows.mjs
```

The `native-windows` workflow builds x64 and arm64 with MSVC and a static CRT. It checks PE architecture and private paths, then runs the same artifact on Node 22.13.0 and 20.0.0 with an empty `PATH`. The proof covers handle lifetime and garbage collection, ancestor replacement, junctions, exact-handle operations, raw UTF-16 and long paths, numeric errors, and cross-process byte-zero locking and release. Blocking locks run in child processes. Comparison with the existing Python `msvcrt` lock remains a separate migration gate before production routing.
5 changes: 4 additions & 1 deletion plugins/codex-security/native/binding.mts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { fileURLToPath } from "node:url";

export const root = dirname(fileURLToPath(import.meta.url));
export const output = join(root, "dist", `${process.platform}-${process.arch}`);
export const binaryPath = join(output, "unix.node");
export const binaryPath = join(
output,
process.platform === "win32" ? "windows.node" : "unix.node",
);

export interface SyscallResult {
value: number;
Expand Down
25 changes: 21 additions & 4 deletions plugins/codex-security/native/build.mts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@ import { join, resolve } from "node:path";
import { binaryPath, output, root } from "./binding.mjs";
import { checkPrivatePaths } from "./check.mjs";

const extension = process.platform === "darwin" ? "dylib" : "so";
let windowsTarget: string | undefined;
if (process.platform === "win32") {
const architecture =
process.arch === "arm64"
? "aarch64"
: process.arch === "x64"
? "x86_64"
: undefined;
if (architecture === undefined)
throw new Error("Windows native builds support x64 and arm64.");
windowsTarget = `${architecture}-pc-windows-msvc`;
}
const cargoHome = resolve(
root,
process.env["CARGO_HOME"] ?? join(homedir(), ".cargo"),
Expand All @@ -20,12 +31,15 @@ const inheritedFlags =
[];
const flags = [
...inheritedFlags,
...(windowsTarget === undefined ? [] : ["-C", "target-feature=+crt-static"]),
`--remap-path-prefix=${root}=codex-security-native`,
`--remap-path-prefix=${cargoHome}=cargo`,
`--remap-path-prefix=${sysroot}=rust-toolchain`,
];
const target = resolve(root, process.env["CARGO_TARGET_DIR"] ?? "target");
execFileSync("cargo", ["build", "--release", "--locked"], {
const args = ["build", "--release", "--locked"];
if (windowsTarget !== undefined) args.push("--target", windowsTarget);
execFileSync("cargo", args, {
cwd: root,
stdio: "inherit",
env: {
Expand All @@ -38,10 +52,13 @@ execFileSync("cargo", ["build", "--release", "--locked"], {
});
const library = join(
target,
...(windowsTarget === undefined ? [] : [windowsTarget]),
"release",
`libcodex_security_native_unix.${extension}`,
windowsTarget === undefined
? `libcodex_security_native.${process.platform === "darwin" ? "dylib" : "so"}`
: "codex_security_native.dll",
);
checkPrivatePaths(readFileSync(library), [root, cargoHome, sysroot]);
checkPrivatePaths(readFileSync(library), [root, cargoHome, sysroot, target]);
mkdirSync(output, { recursive: true });
copyFileSync(library, binaryPath);
console.log(`Built ${process.platform}-${process.arch} Node-API 8 primitives.`);
22 changes: 20 additions & 2 deletions plugins/codex-security/native/check.mts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ export function checkPrivatePaths(
"/tmp/codex-security-python-",
...buildPaths,
]) {
if (bytes.includes(Buffer.from(marker))) {
if (
bytes.includes(Buffer.from(marker)) ||
bytes.includes(Buffer.from(marker, "utf16le"))
) {
throw new Error("Native payload contains a private build path.");
}
}
Expand Down Expand Up @@ -73,8 +76,23 @@ if (
);
}
floor = "macOS 11.0";
} else if (process.platform === "win32") {
const header = bytes.readUInt32LE(0x3c);
const machine = process.arch === "arm64" ? 0xaa64 : 0x8664;
if (
bytes.toString("ascii", 0, 2) !== "MZ" ||
bytes.toString("ascii", header, header + 4) !== "PE\0\0" ||
bytes.readUInt16LE(header + 4) !== machine
) {
throw new Error(
"Native payload is not a PE image for this architecture.",
);
}
floor = "Windows MSVC; Node 20 and 22 load proofs required";
} else {
throw new Error("This foundation verifies Linux and macOS artifacts only.");
throw new Error(
"This foundation verifies Linux, macOS, and Windows artifacts only.",
);
}
console.log(
JSON.stringify({
Expand Down
Loading
Loading