Skip to content

Commit 2471c67

Browse files
authored
refactor(plugin): add Python-free Windows OS primitives (#795)
* refactor(plugin): add Python-free Windows OS primitives * fix(native): exercise Windows directory delete sharing * fix(native): export Windows handle methods through Node-API * refactor: use Rust File for Windows I/O and locks
1 parent 01d9a68 commit 2471c67

12 files changed

Lines changed: 1544 additions & 224 deletions

File tree

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
name: native-windows
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
types: [opened, reopened, synchronize]
8+
9+
permissions:
10+
contents: read
11+
12+
concurrency:
13+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
14+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
15+
16+
jobs:
17+
primitives:
18+
name: primitives / win32-${{ matrix.arch }}
19+
runs-on: ${{ matrix.runner }}
20+
timeout-minutes: 30
21+
strategy:
22+
fail-fast: false
23+
matrix:
24+
include:
25+
- runner: windows-2022
26+
arch: x64
27+
target: x86_64-pc-windows-msvc
28+
- runner: windows-11-arm
29+
arch: arm64
30+
target: aarch64-pc-windows-msvc
31+
defaults:
32+
run:
33+
shell: pwsh
34+
working-directory: plugins/codex-security/native
35+
steps:
36+
- name: Checkout repository
37+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
38+
with:
39+
persist-credentials: false
40+
- name: Set up pnpm
41+
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
42+
with:
43+
package_json_file: package.json
44+
- name: Set up Node.js 22
45+
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
46+
with:
47+
node-version: "22.13.0"
48+
architecture: ${{ matrix.arch }}
49+
- name: Compile TypeScript tools
50+
working-directory: .
51+
run: |
52+
pnpm --dir sdk/typescript install --frozen-lockfile
53+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
54+
pnpm --dir sdk/typescript run build:ci
55+
- name: Set up Rust
56+
run: rustup toolchain install 1.97.1 --profile minimal --component rustfmt --component clippy --target ${{ matrix.target }}
57+
- name: Check Rust source
58+
run: |
59+
cargo fmt --check
60+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
61+
cargo clippy --locked --target ${{ matrix.target }} -- -D warnings
62+
- name: Build and verify with Node.js 22
63+
run: |
64+
node build.mjs
65+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
66+
node check.mjs
67+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
68+
$nativeNode = (Get-Command node).Source
69+
$env:PATH = ""
70+
& $nativeNode --expose-gc proof-windows.mjs
71+
- name: Compare the existing Python completion lock
72+
run: |
73+
$python = (Get-Command python -ErrorAction Stop).Source
74+
node --expose-gc proof-windows.mjs $python ../scripts
75+
- name: Set up Node.js 20
76+
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
77+
with:
78+
node-version: "20.0.0"
79+
architecture: ${{ matrix.arch }}
80+
- name: Verify the same artifact with Node.js 20
81+
run: |
82+
node check.mjs
83+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
84+
$nativeNode = (Get-Command node).Source
85+
$env:PATH = ""
86+
& $nativeNode --expose-gc proof-windows.mjs
87+
- name: Upload verified native artifact
88+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
89+
with:
90+
name: native-win32-${{ matrix.arch }}-${{ github.sha }}
91+
path: plugins/codex-security/native/dist/win32-${{ matrix.arch }}/windows.node
92+
if-no-files-found: error
93+
retention-days: 7

‎plugins/codex-security/native/Cargo.lock‎

Lines changed: 11 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎plugins/codex-security/native/Cargo.toml‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[package]
2-
name = "codex-security-native-unix"
2+
name = "codex-security-native"
33
version = "0.0.0"
44
edition = "2021"
55
rust-version = "1.97"
@@ -12,8 +12,13 @@ crate-type = ["cdylib"]
1212
[dependencies]
1313
napi = { version = "=3.12.2", default-features = false, features = ["napi8"] }
1414
napi-derive = "=3.6.3"
15+
16+
[target.'cfg(unix)'.dependencies]
1517
libc = "=0.2.189"
1618

19+
[target.'cfg(windows)'.dependencies]
20+
windows-sys = { version = "=0.61.2", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem"] }
21+
1722
[build-dependencies]
1823
napi-build = "=2.4.1"
1924

‎plugins/codex-security/native/README.md‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Unix native primitives
1+
# Native OS primitives
22

3-
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.
3+
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.
44

55
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.
66

@@ -32,3 +32,21 @@ node plugins/codex-security/native/check.mjs
3232
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.
3333

3434
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.
35+
36+
Windows uses `windows-binding.mts` and the same Rust crate. `WindowsHandle` owns a non-inheritable handle through Rust's `File`; 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.
37+
38+
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 exclusive whole-file locking. Rust's `File` supplies ordinary I/O, cursor-preserving truncation, `sync_all` for flush, and locks. Calls return numeric Windows errors, including 6 for closed handles and 33 for nonblocking lock contention. Buffer ranges, path encoding, and 64-bit seek arguments are checked before use. 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.
39+
40+
Build on Windows after compiling the TypeScript tools, then run:
41+
42+
```sh
43+
node plugins/codex-security/native/build.mjs
44+
node plugins/codex-security/native/check.mjs
45+
node --expose-gc plugins/codex-security/native/proof-windows.mjs
46+
```
47+
48+
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 whole-file locking and release. Blocking locks run in child processes. A separate Node 22 invocation uses the runner's Python to compare both directions of contention, unlock, close, and process-death release against the existing `msvcrt` byte-zero lock. Python is only an optional migration oracle:
49+
50+
```sh
51+
node --expose-gc plugins/codex-security/native/proof-windows.mjs python plugins/codex-security/scripts
52+
```

‎plugins/codex-security/native/binding.mts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import { fileURLToPath } from "node:url";
55

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

1013
export interface SyscallResult {
1114
value: number;

‎plugins/codex-security/native/build.mts‎

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,18 @@ import { join, resolve } from "node:path";
55
import { binaryPath, output, root } from "./binding.mjs";
66
import { checkPrivatePaths } from "./check.mjs";
77

8-
const extension = process.platform === "darwin" ? "dylib" : "so";
8+
let windowsTarget: string | undefined;
9+
if (process.platform === "win32") {
10+
const architecture =
11+
process.arch === "arm64"
12+
? "aarch64"
13+
: process.arch === "x64"
14+
? "x86_64"
15+
: undefined;
16+
if (architecture === undefined)
17+
throw new Error("Windows native builds support x64 and arm64.");
18+
windowsTarget = `${architecture}-pc-windows-msvc`;
19+
}
920
const cargoHome = resolve(
1021
root,
1122
process.env["CARGO_HOME"] ?? join(homedir(), ".cargo"),
@@ -20,12 +31,15 @@ const inheritedFlags =
2031
[];
2132
const flags = [
2233
...inheritedFlags,
34+
...(windowsTarget === undefined ? [] : ["-C", "target-feature=+crt-static"]),
2335
`--remap-path-prefix=${root}=codex-security-native`,
2436
`--remap-path-prefix=${cargoHome}=cargo`,
2537
`--remap-path-prefix=${sysroot}=rust-toolchain`,
2638
];
2739
const target = resolve(root, process.env["CARGO_TARGET_DIR"] ?? "target");
28-
execFileSync("cargo", ["build", "--release", "--locked"], {
40+
const args = ["build", "--release", "--locked"];
41+
if (windowsTarget !== undefined) args.push("--target", windowsTarget);
42+
execFileSync("cargo", args, {
2943
cwd: root,
3044
stdio: "inherit",
3145
env: {
@@ -38,10 +52,13 @@ execFileSync("cargo", ["build", "--release", "--locked"], {
3852
});
3953
const library = join(
4054
target,
55+
...(windowsTarget === undefined ? [] : [windowsTarget]),
4156
"release",
42-
`libcodex_security_native_unix.${extension}`,
57+
windowsTarget === undefined
58+
? `libcodex_security_native.${process.platform === "darwin" ? "dylib" : "so"}`
59+
: "codex_security_native.dll",
4360
);
44-
checkPrivatePaths(readFileSync(library), [root, cargoHome, sysroot]);
61+
checkPrivatePaths(readFileSync(library), [root, cargoHome, sysroot, target]);
4562
mkdirSync(output, { recursive: true });
4663
copyFileSync(library, binaryPath);
4764
console.log(`Built ${process.platform}-${process.arch} Node-API 8 primitives.`);

‎plugins/codex-security/native/check.mts‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ export function checkPrivatePaths(
1515
"/tmp/codex-security-python-",
1616
...buildPaths,
1717
]) {
18-
if (bytes.includes(Buffer.from(marker))) {
18+
if (
19+
bytes.includes(Buffer.from(marker)) ||
20+
bytes.includes(Buffer.from(marker, "utf16le"))
21+
) {
1922
throw new Error("Native payload contains a private build path.");
2023
}
2124
}
@@ -73,8 +76,23 @@ if (
7376
);
7477
}
7578
floor = "macOS 11.0";
79+
} else if (process.platform === "win32") {
80+
const header = bytes.readUInt32LE(0x3c);
81+
const machine = process.arch === "arm64" ? 0xaa64 : 0x8664;
82+
if (
83+
bytes.toString("ascii", 0, 2) !== "MZ" ||
84+
bytes.toString("ascii", header, header + 4) !== "PE\0\0" ||
85+
bytes.readUInt16LE(header + 4) !== machine
86+
) {
87+
throw new Error(
88+
"Native payload is not a PE image for this architecture.",
89+
);
90+
}
91+
floor = "Windows MSVC; Node 20 and 22 load proofs required";
7692
} else {
77-
throw new Error("This foundation verifies Linux and macOS artifacts only.");
93+
throw new Error(
94+
"This foundation verifies Linux, macOS, and Windows artifacts only.",
95+
);
7896
}
7997
console.log(
8098
JSON.stringify({

0 commit comments

Comments
 (0)