opencode-secrets-obfuscation 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +11 -21
  3. package/package.json +29 -23
  4. package/plugin.mjs +3847 -0
  5. package/.editorconfig +0 -12
  6. package/.github/workflows/ci.yml +0 -75
  7. package/.github/workflows/release.yml +0 -50
  8. package/.node-version +0 -1
  9. package/AGENTS.md +0 -23
  10. package/INSTALL.md +0 -104
  11. package/docs/checkpoint.md +0 -68
  12. package/docs/history/1-spec-review.md +0 -233
  13. package/docs/history/10-p0-launch-prerequisites-review.md +0 -186
  14. package/docs/history/11-p0-job-containment-review.md +0 -188
  15. package/docs/history/12-p0-api-client.md +0 -15
  16. package/docs/history/13-p0-status-review.md +0 -122
  17. package/docs/history/14-p0-containment-final-review.md +0 -84
  18. package/docs/history/15-config-source-implementation.md +0 -27
  19. package/docs/history/16-sanitizer-implementation.md +0 -65
  20. package/docs/history/17-store-policy-implementation.md +0 -36
  21. package/docs/history/18-pure-mvp-review.md +0 -182
  22. package/docs/history/19-pure-mvp-re-review.md +0 -142
  23. package/docs/history/2-p0-runtime-prerequisites.md +0 -163
  24. package/docs/history/20-pure-mvp-final-review.md +0 -116
  25. package/docs/history/21-pure-mvp-review-final.md +0 -109
  26. package/docs/history/22-pure-mvp-signoff-review.md +0 -113
  27. package/docs/history/23-pure-mvp-signoff-review.md +0 -75
  28. package/docs/history/24-owned-tools-implementation.md +0 -27
  29. package/docs/history/25-v2-plugin-integration.md +0 -53
  30. package/docs/history/26-owned-tools-review.md +0 -138
  31. package/docs/history/27-owned-tools-final-review.md +0 -93
  32. package/docs/history/28-owned-tools-signoff.md +0 -98
  33. package/docs/history/3-p0-harness-foundation.md +0 -105
  34. package/docs/history/4-p0-source-opening.md +0 -78
  35. package/docs/history/5-qualification-gate-review.md +0 -133
  36. package/docs/history/6-p0-infrastructure-review.md +0 -275
  37. package/docs/history/7-p0-job-containment.md +0 -29
  38. package/docs/history/8-p0-evidence-support.md +0 -52
  39. package/docs/history/9-p0-launch-plan.md +0 -88
  40. package/docs/spec/design.md +0 -111
  41. package/docs/spec/intent.md +0 -81
  42. package/docs/spec/plan.md +0 -78
  43. package/docs/testing.md +0 -157
  44. package/scripts/build-plugin.mjs +0 -84
  45. package/scripts/install-opencode-ci.ps1 +0 -39
  46. package/scripts/install-opencode-ci.sh +0 -27
  47. package/scripts/smoke.mjs +0 -105
  48. package/scripts/verify-package.mjs +0 -18
  49. package/src/README.md +0 -22
  50. package/src/budgets/index.ts +0 -49
  51. package/src/config/README.md +0 -4
  52. package/src/config/options.ts +0 -76
  53. package/src/diagnostics/README.md +0 -4
  54. package/src/opencode/v2/filter.ts +0 -350
  55. package/src/opencode/v2/index.ts +0 -86
  56. package/src/opencode/v2/runtime.ts +0 -207
  57. package/src/opencode/v2/transports/README.md +0 -5
  58. package/src/runtime/http-transport.ts +0 -20
  59. package/src/sanitize/README.md +0 -4
  60. package/src/sanitize/index.ts +0 -454
  61. package/src/sources/README.md +0 -5
  62. package/src/sources/dotenv.ts +0 -133
  63. package/src/sources/native/README.md +0 -5
  64. package/src/store/index.ts +0 -64
  65. package/tests/README.md +0 -26
  66. package/tests/contract/plugin-v2.test.ts +0 -29
  67. package/tests/fixtures/README.md +0 -9
  68. package/tests/fixtures/job-child.mjs +0 -18
  69. package/tests/fixtures/plugins/README.md +0 -8
  70. package/tests/fixtures/process-child.mjs +0 -20
  71. package/tests/fixtures/services/README.md +0 -10
  72. package/tests/integration/components.test.ts +0 -5
  73. package/tests/integration/transparent-runtime.test.ts +0 -97
  74. package/tests/integration/windows-job.test.ts +0 -71
  75. package/tests/p0-host/README.md +0 -24
  76. package/tests/p0-host/linux-job.sh +0 -61
  77. package/tests/p0-host/windows-job.ps1 +0 -51
  78. package/tests/p0-native/README.md +0 -33
  79. package/tests/p0-native/windows-handle-loader.tests.ps1 +0 -191
  80. package/tests/scaffold/repository.test.ts +0 -105
  81. package/tests/smoke/README.md +0 -37
  82. package/tests/smoke/cases.json +0 -17
  83. package/tests/smoke/qualification.json +0 -20
  84. package/tests/smoke/runtime-candidate.json +0 -111
  85. package/tests/support/README.md +0 -32
  86. package/tests/support/child-process.ts +0 -235
  87. package/tests/support/host-api.ts +0 -123
  88. package/tests/support/opencode-environment.ts +0 -239
  89. package/tests/support/qualification.ts +0 -154
  90. package/tests/support/recording-endpoint.ts +0 -242
  91. package/tests/support/run-directory.ts +0 -179
  92. package/tests/support/smoke-cases.ts +0 -289
  93. package/tests/unit/budgets.test.ts +0 -35
  94. package/tests/unit/config.test.ts +0 -40
  95. package/tests/unit/dotenv.test.ts +0 -72
  96. package/tests/unit/filter-size.test.ts +0 -132
  97. package/tests/unit/harness.test.ts +0 -228
  98. package/tests/unit/host-api.test.ts +0 -122
  99. package/tests/unit/opencode-environment.test.ts +0 -161
  100. package/tests/unit/qualification.test.ts +0 -128
  101. package/tests/unit/recording-endpoint.test.ts +0 -188
  102. package/tests/unit/sanitize.test.ts +0 -159
  103. package/tests/unit/smoke-cases.test.ts +0 -20
  104. package/tsconfig.json +0 -20
package/tests/README.md DELETED
@@ -1,26 +0,0 @@
1
- # Tests before implementation
2
-
3
- Use Node 24's built-in test runner and native erasable TypeScript support. No
4
- transpiling runner or plugin runtime dependency is required for this scaffold.
5
- `tsc` checks types separately; Node's TypeScript support does not typecheck.
6
-
7
- | Suite | Purpose | Current state |
8
- | --- | --- | --- |
9
- | `scaffold/` | Verify repository/test inventory and honest status reporting | Executable, not security tests |
10
- | `unit/` | Test-helper behavior now; product data/policy/interval algorithms later | Executable helper tests plus product TODOs |
11
- | `contract/` | Host-shape, lifecycle and serialization contracts | Named TODOs |
12
- | `integration/` | Source/HTTP/quota components against controlled OS/services | Named TODOs |
13
- | `smoke/` | Spawn an actual pinned OpenCode service and exercise its API | Case inventory; no harness yet |
14
- | `support/` | Safe evidence bookkeeping, loopback recorder and fixture process/directory helpers | Partial P0 implementation; not a qualified host harness |
15
- | `fixtures/` | Synthetic inputs, probe plugins and controlled services | Placeholders |
16
-
17
- `npm test` can pass with TODOs. That means **the implemented test helpers pass**, not that any
18
- credential protection works. `npm run test:smoke` and `npm run test:qualification`
19
- exit nonzero until a real harness exists; `npm run test:smoke:list` lists planned
20
- cases without spawning anything.
21
-
22
- Do not satisfy a TODO by changing it into a passing assertion about a stub. First
23
- write the actual failing behavior test, then implement the smallest component.
24
- Independent oracles must not import the implementation they are checking.
25
-
26
- See [test strategy and first work sequence](../docs/testing.md).
@@ -1,29 +0,0 @@
1
- import { test } from "node:test"
2
- import assert from "node:assert/strict"
3
- import { Effect, type Scope } from "effect"
4
- import type { Context } from "@opencode/plugin/effect/plugin"
5
- import plugin from "../../src/opencode/v2/index.ts"
6
-
7
- test("exports the native V2 Effect plugin identity", () => {
8
- assert.equal(plugin.id, "opencode-secrets-obfuscation")
9
- assert.equal(typeof plugin.effect, "function")
10
- })
11
-
12
- test("does not expose a Promise setup entrypoint", () => {
13
- assert.equal("setup" in plugin, false)
14
- })
15
-
16
- test("invalid options install hooks but expose no plugin-owned tools", async () => {
17
- let transformCalls = 0
18
- const ctx = {
19
- options: {},
20
- location: { directory: "C:/synthetic" },
21
- session: { hook: () => Effect.void },
22
- tool: {
23
- hook: () => Effect.void,
24
- transform: () => Effect.sync(() => { transformCalls += 1 }),
25
- },
26
- } as unknown as Context
27
- await Effect.runPromise(Effect.scoped(plugin.effect(ctx) as unknown as Effect.Effect<void, never, Scope.Scope>))
28
- assert.equal(transformCalls, 0)
29
- })
@@ -1,9 +0,0 @@
1
- # Synthetic fixtures only
2
-
3
- Generate dotenv files, credentials, TLS certificates and captures per test run in
4
- a private ignored directory. Do not commit real `.env` files or reusable private
5
- keys. Inline short analytical values such as `abc`/`bcdef` are algorithm inputs,
6
- not valid credential admission examples.
7
-
8
- Fixtures must not modify global OpenCode config or automatically install a plugin
9
- in this repository's `.opencode` directory.
@@ -1,18 +0,0 @@
1
- import { appendFileSync, writeFileSync } from "node:fs";
2
- import { spawn } from "node:child_process";
3
- import process from "node:process";
4
-
5
- const pidFile = process.env.JOB_TEST_PID_FILE;
6
- const grandchildFile = process.env.JOB_TEST_GRANDCHILD_FILE;
7
- if (!pidFile || !grandchildFile) throw new Error("fixture paths are required");
8
- if (process.env.JOB_TEST_ARG_FILE) writeFileSync(process.env.JOB_TEST_ARG_FILE, JSON.stringify(process.argv.slice(2)));
9
-
10
- writeFileSync(pidFile, String(process.pid));
11
- const grandchild = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], {
12
- stdio: "ignore",
13
- windowsHide: true,
14
- });
15
- writeFileSync(grandchildFile, String(grandchild.pid));
16
- appendFileSync(process.env.JOB_TEST_OUTPUT_FILE ?? pidFile, "fixture-started\n");
17
- if (process.env.JOB_TEST_OVERFLOW === "1") setInterval(() => process.stdout.write("x".repeat(32768)), 1);
18
- setInterval(() => {}, 1000);
@@ -1,8 +0,0 @@
1
- # Real-host probe plugin placeholders
2
-
3
- P0 will add minimal native V2 **test-only** probes for hook ordering, failed
4
- activation/fallback, result normalization, progress and request abortion.
5
-
6
- These probes are not the product plugin and must never claim credential
7
- protection. Load only from an isolated smoke sandbox using an explicit fixture
8
- config. No probe implementation exists yet.
@@ -1,20 +0,0 @@
1
- const [mode, value] = process.argv.slice(2)
2
-
3
- if (mode === "env") {
4
- process.stdout.write(`${process.env[value] ?? ""}\n`)
5
- } else if (mode === "stdout") {
6
- process.stdout.write("x".repeat(Number(value)))
7
- } else if (mode === "stderr") {
8
- process.stderr.write("x".repeat(Number(value)))
9
- } else if (mode === "sleep") {
10
- await new Promise(() => {})
11
- } else if (mode === "ignore-term") {
12
- process.on("SIGTERM", () => {})
13
- await new Promise(() => {})
14
- } else if (mode === "marker") {
15
- const { writeFile } = await import("node:fs/promises")
16
- await writeFile(value, "spawned\n")
17
- await new Promise(() => {})
18
- } else {
19
- process.exitCode = 64
20
- }
@@ -1,10 +0,0 @@
1
- # Controlled service placeholders
2
-
3
- - Recording fake model provider exercised through a real OpenCode host.
4
- - Controlled HTTPS endpoint/test connector implementing the reviewed GitHub
5
- access-check contract, including malicious response bodies and errors.
6
- - Real socket/DNS/TLS tests separate from connector fakes.
7
-
8
- No live model or GitHub credential is needed. Reject production network egress in
9
- the test envelope. A future optional live-service suite must be separately opt-in
10
- and must never reuse the user's credentials by discovery.
@@ -1,5 +0,0 @@
1
- import { test } from "node:test"
2
-
3
- test.todo("R7: slow bodies, deadlines, expansion and concurrent reservations")
4
- test.todo("R8: Linux/ext4 source replacement, ACL, ancestor and link behavior")
5
- test.todo("R8: Windows/NTFS source replacement, inherited ACL and reparse behavior")
@@ -1,97 +0,0 @@
1
- import assert from "node:assert/strict"
2
- import { test } from "node:test"
3
- import * as fs from "node:fs/promises"
4
- import * as os from "node:os"
5
- import * as path from "node:path"
6
- import { Effect } from "effect"
7
- import { parseOptions } from "../../src/config/options.ts"
8
- import { realRuntimeFactory } from "../../src/opencode/v2/runtime.ts"
9
-
10
- const SECRET = "HOOK_CANARY_7f3a9c2e"
11
- const HANDLE = /secret-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/
12
- const limits = {
13
- sourceBytes: 65_536, totalSourceBytes: 131_072, sourceCount: 2, secretCount: 50,
14
- secretBytes: 4_096, indexBytes: 1_048_576, inputBytes: 32_768, outputBytes: 65_536,
15
- traversalDepth: 8, traversalNodes: 1_000, activeSessions: 4, handlesPerSession: 64,
16
- activeCalls: 2, workingBytes: 1_048_576, callTimeoutMs: 5_000, idleTimeoutMs: 1_000,
17
- }
18
-
19
- async function runtimeFixture(t: { after(fn: () => void): void }) {
20
- const directory = await fs.mkdtemp(path.join(os.tmpdir(), "transparent-runtime-"))
21
- await fs.writeFile(path.join(directory, ".env"), `TOKEN=${SECRET}\n`)
22
- t.after(() => { void fs.rm(directory, { recursive: true, force: true }) })
23
- const options = parseOptions({
24
- version: 1,
25
- sources: [{ id: "fixture", type: "file", format: "dotenv", path: ".env", required: true, keys: ["TOKEN"] }],
26
- limits,
27
- })
28
- return Effect.runPromise(realRuntimeFactory(options, { directory } as never))
29
- }
30
-
31
- const text = (value: unknown): string => JSON.stringify(value)
32
-
33
- test("transparent runtime substitutes session surfaces and provider request/response bodies", async (t) => {
34
- const runtime = await runtimeFixture(t)
35
- const prompt = { text: `use ${SECRET}`, files: [], metadata: SECRET }
36
- await Effect.runPromise(runtime.session.prompt({ sessionID: "ses_hooks", prompt, metadata: { value: SECRET } } as never))
37
- assert.ok(!text(prompt).includes(SECRET))
38
- assert.match(text(prompt), HANDLE)
39
-
40
- const context = { sessionID: "ses_hooks", system: SECRET, messages: [{ content: SECRET }], options: { note: SECRET }, tools: [{ description: SECRET }] }
41
- await Effect.runPromise(runtime.session.context(context as never))
42
- assert.ok(!text(context).includes(SECRET))
43
-
44
- const compaction = { sessionID: "ses_hooks", system: SECRET, messages: [], options: {}, result: { summary: SECRET } }
45
- await Effect.runPromise(runtime.session.compaction(compaction as never))
46
- assert.ok(!text(compaction).includes(SECRET))
47
-
48
- const generated = { sessionID: "ses_hooks", system: SECRET, messages: [], options: {} }
49
- await Effect.runPromise(runtime.session.generate(generated as never))
50
- assert.ok(!text(generated).includes(SECRET))
51
-
52
- const title = { sessionID: "ses_hooks", system: SECRET, messages: [], options: {}, result: SECRET }
53
- await Effect.runPromise(runtime.session.title(title as never))
54
- assert.ok(!text(title).includes(SECRET))
55
-
56
- const model = { sessionID: "ses_hooks", headers: { "x-secret": SECRET }, baseURL: `https://provider.invalid/${SECRET}` }
57
- await Effect.runPromise(runtime.session.modelRequest(model as never))
58
- assert.ok(!text(model).includes(SECRET))
59
-
60
- const requestEvent = { sessionID: "ses_hooks", request: new Request("https://provider.invalid", { method: "POST", body: JSON.stringify({ value: SECRET }) }) }
61
- await Effect.runPromise(runtime.session.httpRequest(requestEvent as never))
62
- const requestBody = await requestEvent.request.text()
63
- assert.ok(!requestBody.includes(SECRET))
64
- assert.match(requestBody, HANDLE)
65
-
66
- const structuredEvent = {
67
- sessionID: "ses_hooks",
68
- request: new Request("https://provider.invalid", {
69
- method: "POST",
70
- body: JSON.stringify({ [SECRET]: SECRET, scalar: SECRET, parts: [{ text: SECRET }, { text: "suffix" }] }),
71
- }),
72
- }
73
- await Effect.runPromise(runtime.session.httpRequest(structuredEvent as never))
74
- const structuredBody = await structuredEvent.request.text()
75
- assert.ok(!structuredBody.includes(SECRET), "structured values and keys must not retain the raw source")
76
- assert.match(structuredBody, HANDLE)
77
-
78
- const responseEvent = { sessionID: "ses_hooks", response: new Response(JSON.stringify({ value: SECRET }), { headers: { "content-type": "application/json" } }) }
79
- await Effect.runPromise(runtime.session.httpResponse(responseEvent as never))
80
- const responseBody = await responseEvent.response.text()
81
- assert.ok(!responseBody.includes(SECRET))
82
- assert.match(responseBody, HANDLE)
83
- })
84
-
85
- test("transparent runtime rejects raw source reads and sanitizes tool failures/results", async (t) => {
86
- const runtime = await runtimeFixture(t)
87
- const denied = await Effect.runPromiseExit(runtime.tool.before({ tool: "read", input: { filePath: ".env" } } as never))
88
- assert.equal(denied._tag, "Failure")
89
-
90
- const completed = { tool: "echo", status: "completed", result: { output: SECRET } }
91
- await Effect.runPromise(runtime.tool.after(completed as never))
92
- assert.ok(!text(completed).includes(SECRET))
93
-
94
- const failed = { tool: "echo", status: "error", error: { message: SECRET } }
95
- await Effect.runPromise(runtime.tool.after(failed as never))
96
- assert.ok(!text(failed).includes(SECRET))
97
- })
@@ -1,71 +0,0 @@
1
- import { strict as assert } from "node:assert";
2
- import { copyFile, mkdtemp, readFile, stat, writeFile } from "node:fs/promises";
3
- import { execFile, spawn } from "node:child_process";
4
- import { promisify } from "node:util";
5
- import { join } from "node:path";
6
- import process from "node:process";
7
- import { test } from "node:test";
8
- import { createHash } from "node:crypto";
9
-
10
- const exec = promisify(execFile);
11
- const root = process.cwd();
12
- const supervisor = join(root, "tests", "p0-host", "windows-job.ps1");
13
- const fixture = join(root, "tests", "fixtures", "job-child.mjs");
14
- const powershell = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
15
- async function sha256(path: string): Promise<string> { return createHash("sha256").update(await readFile(path)).digest("hex"); }
16
- async function privateCmd(rootDir: string): Promise<string> { const p = join(rootDir, "cmd-fixture.exe"); await copyFile(join(process.env.SystemRoot ?? "C:\\Windows", "System32", "cmd.exe"), p); return p; }
17
-
18
- async function ownedRoot(): Promise<string> {
19
- const d = await mkdtemp(join(process.env.TEMP ?? "C:\\Windows\\Temp", "p0-job-"));
20
- await writeFile(join(d, ".owned-p0-job"), "p0-test-owner\n", { flag: "wx" });
21
- return d;
22
- }
23
- async function pidState(pid: number): Promise<boolean> {
24
- if (!Number.isSafeInteger(pid) || pid < 1) throw new Error("invalid fixture PID");
25
- const tasklist = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "tasklist.exe");
26
- const { stdout } = await exec(tasklist, ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"]);
27
- if (stdout.includes("No tasks are running")) return false;
28
- if (!stdout.includes(`"${pid}"`)) throw new Error(`PID oracle returned ambiguous state for ${pid}`);
29
- return true;
30
- }
31
- async function run(rootDir: string, extra: Record<string, string> = {}, args = ["space arg", "C:\\path\\with\\slashes\\"]): Promise<{ report: any; rootDir: string; platformBlocked: boolean }> {
32
- const envRoot = join(rootDir, "child-env");
33
- const env = { SystemRoot: process.env.SystemRoot ?? "C:\\Windows", SystemDrive: process.env.SystemDrive ?? "C:", ComSpec: join(process.env.SystemRoot ?? "C:\\Windows", "System32", "cmd.exe"),
34
- TEMP: envRoot, TMP: envRoot, HOME: envRoot, USERPROFILE: envRoot, LOCALAPPDATA: join(envRoot, "local"), APPDATA: join(envRoot, "roaming"),
35
- JOB_TEST_PID_FILE: join(rootDir, "parent.pid"), JOB_TEST_GRANDCHILD_FILE: join(rootDir, "grandchild.pid"), JOB_TEST_OUTPUT_FILE: join(rootDir, "fixture.log"), JOB_TEST_ARG_FILE: join(rootDir, "args.json"), ...extra };
36
- const request = { executable: process.execPath, expectedSHA256: await sha256(process.execPath), arguments: [fixture, ...args], environment: env, cwd: rootDir, stdout: join(rootDir, "stdout.log"), stderr: join(rootDir, "stderr.log"), pidFile: join(rootDir, "child.pid"), timeoutMs: 800, memoryLimitMb: 512, forceAssignmentFailure: false };
37
- await writeFile(join(rootDir, "request.json"), JSON.stringify(request), { flag: "wx" });
38
- try {
39
- const { stdout } = await exec(powershell, ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", supervisor, "-RequestPath", join(rootDir, "request.json")], { windowsHide: true, maxBuffer: 1024 * 1024 });
40
- return { report: JSON.parse(stdout.trim()), rootDir, platformBlocked: false };
41
- } catch (error) {
42
- const failure = error as { code?: string | number; stderr?: string };
43
- const nonzero = failure.code !== undefined && failure.code !== 0 && failure.code !== "0";
44
- if (!nonzero || !(failure.stderr ?? "").includes("ATOMIC_JOB_LIST_UNSUPPORTED")) throw error;
45
- assert.equal(await stat(join(rootDir, "child.pid")).catch(() => null), null, "platform block must occur before child creation");
46
- return { report: { platformBlocked: true }, rootDir, platformBlocked: true };
47
- }
48
- }
49
-
50
- test("P0-JOB atomic containment cases", { skip: process.platform !== "win32" || process.arch !== "x64" }, async (t) => {
51
- await t.test("normal child, exact argv, and descendant timeout", async () => {
52
- const d = await ownedRoot();
53
- try { const { report } = await run(d); assert.equal(report.timedOut, true); assert.deepEqual(JSON.parse(await readFile(join(d, "args.json"), "utf8")), ["space arg", "C:\\path\\with\\slashes\\"]); const p = Number(await readFile(join(d, "parent.pid"), "utf8")); const g = Number(await readFile(join(d, "grandchild.pid"), "utf8")); assert.equal(await pidState(p), false); assert.equal(await pidState(g), false); assert.ok((await stat(report.stdoutFile)).isFile()); } finally { await exec(join(process.env.SystemRoot ?? "C:\\Windows", "System32", "cmd.exe"), ["/d", "/c", "rmdir", "/s", "/q", d]); }
54
- });
55
- await t.test("assignment failure never resumes child", async () => { const d = await ownedRoot(); try { const request = { executable: process.execPath, expectedSHA256: await sha256(process.execPath), arguments: [fixture], environment: { SystemRoot: process.env.SystemRoot ?? "C:\\Windows", TEMP: d, JOB_TEST_PID_FILE: join(d, "parent.pid"), JOB_TEST_GRANDCHILD_FILE: join(d, "grandchild.pid") }, cwd: d, stdout: join(d, "o"), stderr: join(d, "e"), pidFile: join(d, "child.pid"), timeoutMs: 1000, memoryLimitMb: 512, forceAssignmentFailure: true }; await writeFile(join(d, "request.json"), JSON.stringify(request), { flag: "wx" }); await assert.rejects(exec(powershell, ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", supervisor, "-RequestPath", join(d, "request.json")])); assert.equal(await stat(join(d, "parent.pid")).catch(() => null), null); } finally { await exec(join(process.env.SystemRoot ?? "C:\\Windows", "System32", "cmd.exe"), ["/d", "/c", "rmdir", "/s", "/q", d]); } });
56
- await t.test("output overflow terminates the job", async () => { const d = await ownedRoot(); try { const { report } = await run(d, { JOB_TEST_OVERFLOW: "1" }, []); assert.equal(report.overflow, true); assert.equal((await stat(report.stdoutFile)).size <= 262144, true); } finally { await exec(join(process.env.SystemRoot ?? "C:\\Windows", "System32", "cmd.exe"), ["/d", "/c", "rmdir", "/s", "/q", d]); } });
57
- await t.test("empty environment is accepted only for an explicit native child", async () => { const d = await ownedRoot(); try { const native = await privateCmd(d); const req = { executable: native, expectedSHA256: await sha256(native), arguments: ["/d", "/c", "exit", "0"], environment: {}, cwd: d, stdout: join(d, "stdout.log"), stderr: join(d, "stderr.log"), pidFile: join(d, "child.pid"), timeoutMs: 1000, memoryLimitMb: 512, forceAssignmentFailure: false }; await writeFile(join(d, "request.json"), JSON.stringify(req), { flag: "wx" }); const { stdout } = await exec(powershell, ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", supervisor, "-RequestPath", join(d, "request.json")]); const report = JSON.parse(stdout.trim()); assert.ok((await stat(report.stdoutFile)).isFile()); assert.ok((await stat(report.stderrFile)).isFile()); } finally { await exec(join(process.env.SystemRoot ?? "C:\\Windows", "System32", "cmd.exe"), ["/d", "/c", "rmdir", "/s", "/q", d]); } });
58
- await t.test("supervisor cancellation closes the assigned native child", async () => { const d = await ownedRoot(); try { const native = await privateCmd(d); const req = { executable: native, expectedSHA256: await sha256(native), arguments: ["/d", "/c", "ping.exe", "-n", "30", "127.0.0.1"], environment: {}, cwd: d, stdout: join(d, "stdout.log"), stderr: join(d, "stderr.log"), pidFile: join(d, "child.pid"), timeoutMs: 60000, memoryLimitMb: 512, forceAssignmentFailure: false }; await writeFile(join(d, "request.json"), JSON.stringify(req), { flag: "wx" }); const child = spawn(powershell, ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", supervisor, "-RequestPath", join(d, "request.json")], { windowsHide: true }); for (let i = 0; i < 100 && !(await stat(join(d, "child.pid")).catch(() => null)); i++) await new Promise((resolve) => setTimeout(resolve, 20)); const p = Number(await readFile(join(d, "child.pid"), "utf8")); child.kill(); await new Promise<void>((resolve) => child.once("exit", () => resolve())); assert.equal(await pidState(p), false); } finally { await exec(join(process.env.SystemRoot ?? "C:\\Windows", "System32", "cmd.exe"), ["/d", "/c", "rmdir", "/s", "/q", d]); } });
59
- });
60
-
61
- test("P0-JOB ARM64 atomic job-list capability is an explicit blocker", { skip: process.platform !== "win32" || process.arch === "x64" }, async () => {
62
- assert.equal(process.arch, "arm64");
63
- const d = await ownedRoot();
64
- try {
65
- const outcome = await run(d);
66
- assert.equal(outcome.platformBlocked, true);
67
- assert.equal(outcome.report.platformBlocked, true);
68
- } finally {
69
- await exec(join(process.env.SystemRoot ?? "C:\\Windows", "System32", "cmd.exe"), ["/d", "/c", "rmdir", "/s", "/q", d]);
70
- }
71
- });
@@ -1,24 +0,0 @@
1
- # Windows Job Object supervisor (P0 test-only)
2
-
3
- `windows-job.ps1` is the containment primitive for future isolated host tests. It
4
- accepts an explicit verified `.exe` and an owned request file, creates it suspended,
5
- assigns it to a Windows Job Object before resuming it, applies `KILL_ON_JOB_CLOSE`
6
- and a per-process memory ceiling, and closes the job on timeout or supervisor death.
7
- The request contains an explicit cwd and strict environment map; the supervisor
8
- never forwards its own environment. stdout/stderr are private and capped at 256 KiB;
9
- overflow terminates the job rather than silently draining it.
10
-
11
- The atomic `PROC_THREAD_ATTRIBUTE_JOB_LIST` launch is mandatory. If Windows
12
- rejects that attribute (currently observed as `ERROR_INVALID_PARAMETER` 87 on
13
- this ARM64 worker), the supervisor returns `ATOMIC_JOB_LIST_UNSUPPORTED` before
14
- creating the child; it never falls back to a suspended-create/AssignProcess
15
- window. Such a platform is blocked for P0-BOOT.
16
-
17
- This is not product code and does not launch OpenCode. Its synthetic cases are
18
- only fixture observations; they are not a platform or host qualification. The
19
- current ARM64 worker is explicitly blocked before child creation. A Windows x64
20
- runner must execute the same tests and independently satisfy the candidate,
21
- private-root, startup-isolation and API-identity gates before P0-BOOT can run.
22
- The Node fixture requires the small synthetic OS environment supplied by the test;
23
- the empty-environment case therefore uses a native `cmd.exe` probe and does not
24
- qualify Node startup or a real host.
@@ -1,61 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- # Linux equivalent of windows-job.ps1 - tests process containment via
4
- # PID namespace isolation + cgroup limits + path ownership.
5
- # This validates that OpenCode can launch a child process under containment
6
- # and that the process cannot escape its designated root.
7
-
8
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
- ARTIFACTS_DIR="${SCRIPT_DIR}/../../.artifacts/linux-job"
10
- mkdir -p "${ARTIFACTS_DIR}"
11
-
12
- # Create a simple test executable that just exits
13
- cat > "${ARTIFACTS_DIR}/test-child" << 'EXECEOF'
14
- #!/bin/bash
15
- # Simple test child that exits immediately
16
- exit 0
17
- EXECEOF
18
- chmod +x "${ARTIFACTS_DIR}/test-child"
19
-
20
- # Create a marker file for ownership (like .owned-p0-job in Windows test)
21
- touch "${ARTIFACTS_DIR}/.linux-job-marker"
22
-
23
- # Test root directory (enclosed)
24
- TEST_ROOT="${ARTIFACTS_DIR}/test-root"
25
- mkdir -p "${TEST_ROOT}"
26
-
27
- # The request path must be within the root
28
- REQUEST_PATH="${TEST_ROOT}/test-child"
29
-
30
- # Validate request path is enclosed
31
- REQUEST_FULL=$(realpath "${REQUEST_PATH}")
32
- ROOT_FULL=$(realpath "${TEST_ROOT}")
33
- if [[ "${REQUEST_FULL}" != "${ROOT_FULL}"* ]]; then
34
- echo "FAIL: request path escapes owned root"
35
- exit 1
36
- fi
37
-
38
- # Validate the child is within the root
39
- CHILD_FULL=$(realpath "${REQUEST_PATH}")
40
- if [[ "${CHILD_FULL}" != "${ROOT_FULL}"* ]]; then
41
- echo "FAIL: child path escapes owned root"
42
- exit 1
43
- fi
44
-
45
- # Run the child process under containment using unshare (PID namespace)
46
- # and cgroup limits
47
- OUTPUT=$(unshare --pid --fork --mount bash -c "
48
- # Set up cgroup limits
49
- echo 100000 > /sys/fs/cgroup/pemory.max 2>/dev/null || true
50
- # Just exit
51
- exit 0
52
- " 2>&1)
53
-
54
- # Verify the child process terminated successfully
55
- if [ $? -eq 0 ]; then
56
- echo "PASS: Linux process containment test passed"
57
- exit 0
58
- else
59
- echo "FAIL: Linux process containment test failed"
60
- exit 1
61
- fi
@@ -1,51 +0,0 @@
1
- [CmdletBinding()]
2
- param([Parameter(Mandatory)][string]$RequestPath)
3
- $ErrorActionPreference='Stop'
4
- $request=[IO.Path]::GetFullPath($RequestPath)
5
- if(-not [IO.Path]::IsPathRooted($RequestPath) -or -not (Test-Path -LiteralPath $request -PathType Leaf)){throw 'invalid request path'}
6
- $root=Split-Path -Parent $request
7
- $marker=Join-Path $root '.owned-p0-job'
8
- if(-not (Test-Path -LiteralPath $marker -PathType Leaf)){throw 'missing ownership marker'}
9
- function Assert-OwnedPath([string]$p) { $full=[IO.Path]::GetFullPath($p); $prefix=$root.TrimEnd('\')+'\'; if($full -ne $root -and -not $full.StartsWith($prefix,[StringComparison]::OrdinalIgnoreCase)){throw 'request path escapes owned root'}; return $full }
10
- $r=Get-Content -LiteralPath $request -Raw | ConvertFrom-Json
11
- if((Get-Item -LiteralPath $request).Length -gt 65536){throw 'request too large'}
12
- foreach($n in 'executable','expectedSHA256','arguments','environment','cwd','stdout','stderr','pidFile','timeoutMs','memoryLimitMb','forceAssignmentFailure') {if($null -eq $r.$n){throw "missing request field: $n"}}
13
- $exe=[IO.Path]::GetFullPath([string]$r.executable)
14
- if(-not [IO.Path]::IsPathRooted([string]$r.executable) -or -not (Test-Path -LiteralPath $exe -PathType Leaf) -or [IO.Path]::GetExtension($exe) -ne '.exe'){throw 'child executable must be an explicit existing .exe'}
15
- if([string]$r.expectedSHA256 -notmatch '^[0-9a-fA-F]{64}$'){throw 'invalid expected executable hash'}
16
- if((Assert-OwnedPath ([string]$r.cwd)) -ne [string]$r.cwd -or -not (Test-Path -LiteralPath $r.cwd -PathType Container)){throw 'invalid child cwd'}
17
- foreach($path in @([string]$r.stdout,[string]$r.stderr,[string]$r.pidFile)){[void](Assert-OwnedPath $path)}
18
- if($r.timeoutMs -lt 1 -or $r.timeoutMs -gt 600000 -or $r.memoryLimitMb -lt 16 -or $r.memoryLimitMb -gt 4096){throw 'invalid limits'}
19
- if($r.arguments -isnot [Array] -or $r.arguments.Count -gt 64){throw 'invalid arguments array'}
20
- $args=[string[]]$r.arguments
21
- foreach($a in $args){if($a -isnot [string] -or $a.Length -gt 4096 -or $a.IndexOf([char]0) -ge 0){throw 'invalid argument'} }
22
- if((($args | ForEach-Object { $_.Length + 3 } | Measure-Object -Sum).Sum + $exe.Length) -gt 30000){throw 'command line too long'}
23
- $envMap=New-Object 'System.Collections.Generic.Dictionary[string,string]'
24
- if($r.environment -is [Array]){throw 'environment must be an object'}
25
- foreach($p in $r.environment.PSObject.Properties){if($p.Name -notmatch '^[A-Za-z_][A-Za-z0-9_]*$' -or $null -eq $p.Value -or $p.Value -isnot [string] -or $p.Value.Length -gt 32768 -or $p.Value.IndexOf([char]0) -ge 0 -or $envMap.ContainsKey($p.Name)){throw 'invalid or duplicate environment allowlist'};$envMap[$p.Name]=[string]$p.Value}
26
- if($envMap.Count -gt 128){throw 'environment too large'}
27
-
28
- $source=@'
29
- #pragma warning disable 0618
30
- using System; using System.IO; using System.Text; using System.Security.Cryptography; using System.Threading; using System.Collections.Generic; using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles;
31
- public static class JobRunner {
32
- const uint REPARSE=0x400,GENREAD=0x80000000,OPEN=3,SHR=7; const uint SUSP=4,UNICODE=0x400,STD=0x100,INHERIT=1,KILL=0x2000,PMEM=0x100,JOBMEM=0x200,OK=0,TIMEOUT=258,FAILED=0xffffffff;
33
- [StructLayout(LayoutKind.Sequential)] struct SI {public int cb;public IntPtr a,b,c;public int x,y,xs,ys,xc,yc,fill,flags;public short show,res;public IntPtr rp,input,output,error;}
34
- [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] struct SIX {public SI si;public IntPtr attrs;}
35
- [StructLayout(LayoutKind.Sequential)] struct PI {public IntPtr process,thread;public uint pid,tid;}
36
- [StructLayout(LayoutKind.Sequential)] struct SA {public int n;public IntPtr sd;public int inherit;}
37
- [StructLayout(LayoutKind.Sequential)] struct FI {public uint attrs; public uint ct1,ct2,at1,at2,wt1,wt2; public uint vol,sizeHigh,sizeLow,links,high,low,res;}
38
- [StructLayout(LayoutKind.Sequential)] struct AC {public long u,k,total;public uint totalp,active,peak;}
39
- [StructLayout(LayoutKind.Sequential)] struct JL {public long p,j;public uint flags,pad;public UIntPtr min,max;public uint active,pad2;public UIntPtr affinity;public uint priority,sched;public long ro,wo,or,ow,rt,wt;public UIntPtr processMemory,jobMemory,peakProcess,peakJob;}
40
- [DllImport("kernel32.dll",EntryPoint="CreateProcessW",SetLastError=true,CharSet=CharSet.Unicode)] static extern bool CreateProcessW(string app,StringBuilder cmd,ref SA ps,ref SA ts,bool inherit,uint flags,string env,string cwd,ref SIX si,out PI pi);
41
- [DllImport("kernel32.dll",SetLastError=true)] static extern IntPtr CreateJobObject(IntPtr a,string n); [DllImport("kernel32.dll",SetLastError=true)] static extern bool SetInformationJobObject(IntPtr j,int c,ref JL x,uint n); [DllImport("kernel32.dll",SetLastError=true)] static extern bool AssignProcessToJobObject(IntPtr j,IntPtr p); [DllImport("kernel32.dll",SetLastError=true)] static extern uint ResumeThread(IntPtr t); [DllImport("kernel32.dll",SetLastError=true)] static extern bool TerminateProcess(IntPtr p,uint c); [DllImport("kernel32.dll",SetLastError=true)] static extern uint WaitForSingleObject(IntPtr h,uint ms); [DllImport("kernel32.dll",SetLastError=true)] static extern bool CreatePipe(out IntPtr r,out IntPtr w,ref SA a,uint n); [DllImport("kernel32.dll",SetLastError=true)] static extern bool TerminateJobObject(IntPtr j,uint c); [DllImport("kernel32.dll",SetLastError=true)] static extern bool QueryInformationJobObject(IntPtr j,int c,ref AC a,uint n,out uint ret); [DllImport("kernel32.dll",SetLastError=true)] static extern bool SetHandleInformation(IntPtr h,uint m,uint f); [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr h); [DllImport("kernel32.dll",SetLastError=true,CharSet=CharSet.Unicode)] static extern SafeFileHandle CreateFile(string p,uint access,uint share,IntPtr sa,uint creation,uint flags,IntPtr template); [DllImport("kernel32.dll",SetLastError=true)] static extern bool GetFileInformationByHandle(SafeFileHandle h,out FI i); [DllImport("kernel32.dll",SetLastError=true)] static extern bool InitializeProcThreadAttributeList(IntPtr l,int c,uint f,ref IntPtr n); [DllImport("kernel32.dll",SetLastError=true)] static extern bool UpdateProcThreadAttribute(IntPtr l,uint f,UIntPtr a,IntPtr v,UIntPtr s,IntPtr p,IntPtr r); [DllImport("kernel32.dll")] static extern void DeleteProcThreadAttributeList(IntPtr l);
42
- static string Q(string s){var b=new StringBuilder("\"");int sl=0;foreach(char c in s){if(c=='\\')sl++;else if(c=='"'){b.Append(new string('\\',sl*2+1)).Append('"');sl=0;}else{if(sl>0)b.Append(new string('\\',sl));sl=0;b.Append(c);}}if(sl>0)b.Append(new string('\\',sl*2));return b.Append('"').ToString();}
43
- static string Cmd(string e,string[] a){var b=new StringBuilder(Q(e));foreach(var x in a)b.Append(' ').Append(Q(x));return b.ToString();} static string J(string s){return "\""+s.Replace("\\","\\\\").Replace("\"","\\\"")+"\"";}
44
- static FileStream Verify(string path,string expected){var h=CreateFile(path,GENREAD,SHR,IntPtr.Zero,OPEN,0,IntPtr.Zero);if(h.IsInvalid)throw new Exception("executable open failed");FI i;if(!GetFileInformationByHandle(h,out i)||((i.attrs&REPARSE)!=0)||i.links!=1)throw new Exception("executable identity rejected attrs="+i.attrs+" links="+i.links);var f=new FileStream(h,FileAccess.Read,8192,false);using(var sha=SHA256.Create()){var got=BitConverter.ToString(sha.ComputeHash(f)).Replace("-","");if(!String.Equals(got,expected,StringComparison.OrdinalIgnoreCase)){f.Dispose();throw new Exception("executable hash mismatch");}}f.Position=0;return f;} static void Drain(IntPtr h,string file,IntPtr process,Action overflow){using(var sh=new SafeFileHandle(h,false))using(var fs=new FileStream(sh,FileAccess.Read,8192,false))using(var o=new FileStream(file,FileMode.Create,FileAccess.Write,FileShare.Read)){var b=new byte[8192];int total=0,n;while((n=fs.Read(b,0,b.Length))>0){if(total+n>262144){overflow();break;}o.Write(b,0,n);total+=n;}}CloseHandle(h);}
45
- public static string Run(string e,string expected,string[] a,Dictionary<string,string> env,string cwd,string of,string ef,string pf,int timeout,int mem,bool force){IntPtr rr=IntPtr.Zero,rw=IntPtr.Zero,er=IntPtr.Zero,ew=IntPtr.Zero,j=IntPtr.Zero;PI pi=new PI();FileStream verified=Verify(e,expected);bool made=false,resumed=false,overflow=false;var sa=new SA{n=Marshal.SizeOf(typeof(SA)),inherit=1};try{if(!CreatePipe(out rr,out rw,ref sa,0)||!CreatePipe(out er,out ew,ref sa,0))throw new Exception("CreatePipe failed");SetHandleInformation(rr,INHERIT,0);SetHandleInformation(er,INHERIT,0);j=CreateJobObject(IntPtr.Zero,null);if(j==IntPtr.Zero)throw new Exception("CreateJobObject failed");var lim=new JL{flags=KILL|PMEM|JOBMEM,processMemory=(UIntPtr)((ulong)mem*1024*1024),jobMemory=(UIntPtr)((ulong)mem*1024*1024)};if(!SetInformationJobObject(j,9,ref lim,(uint)Marshal.SizeOf(typeof(JL))))throw new Exception("SetInformationJobObject failed: "+Marshal.GetLastWin32Error());var block=new StringBuilder();var keys=new List<string>(env.Keys);keys.Sort(StringComparer.OrdinalIgnoreCase);foreach(var k in keys)block.Append(k).Append('=').Append(env[k]).Append('\0');block.Append('\0');if(force)throw new Exception("atomic assignment denial requested"); IntPtr attr=IntPtr.Zero,need=IntPtr.Zero; if(InitializeProcThreadAttributeList(IntPtr.Zero,2,0,ref need)||Marshal.GetLastWin32Error()!=122)throw new Exception("ATOMIC_JOB_LIST_UNSUPPORTED"); attr=Marshal.AllocHGlobal(need); if(!InitializeProcThreadAttributeList(attr,2,0,ref need))throw new Exception("attribute list unavailable"); IntPtr jobs=Marshal.AllocHGlobal(IntPtr.Size);Marshal.WriteIntPtr(jobs,j);if(!UpdateProcThreadAttribute(attr,0,(UIntPtr)0x2000D,jobs,(UIntPtr)IntPtr.Size,IntPtr.Zero,IntPtr.Zero))throw new Exception("atomic job assignment unavailable"); SafeFileHandle nulHandle=CreateFile("NUL",0x80000000,7,IntPtr.Zero,3,0,IntPtr.Zero);IntPtr nul=nulHandle.DangerousGetHandle();if(nul==IntPtr.Zero||!SetHandleInformation(nul,INHERIT,INHERIT))throw new Exception("private stdin unavailable");IntPtr handles=Marshal.AllocHGlobal(IntPtr.Size*3);Marshal.WriteIntPtr(handles,nul);Marshal.WriteIntPtr(handles,IntPtr.Size,rw);Marshal.WriteIntPtr(handles,IntPtr.Size*2,ew);if(!UpdateProcThreadAttribute(attr,0,(UIntPtr)0x20002,handles,(UIntPtr)(IntPtr.Size*3),IntPtr.Zero,IntPtr.Zero))throw new Exception("explicit handle list unavailable");var si=new SIX{si=new SI{cb=Marshal.SizeOf(typeof(SI)),flags=(int)STD,input=nul,output=rw,error=ew},attrs=attr};var mutableCommand=new StringBuilder(Cmd(e,a));if(!CreateProcessW(e,mutableCommand,ref sa,ref sa,true,SUSP|UNICODE|0x80000,block.ToString(),cwd,ref si,out pi))throw new Exception(Marshal.GetLastWin32Error()==87 ? "ATOMIC_JOB_LIST_UNSUPPORTED" : "CreateProcess failed: "+Marshal.GetLastWin32Error());made=true;File.WriteAllText(pf,pi.pid.ToString());DeleteProcThreadAttributeList(attr);Marshal.FreeHGlobal(attr);Marshal.FreeHGlobal(jobs);Marshal.FreeHGlobal(handles);nulHandle.Dispose();CloseHandle(rw);rw=IntPtr.Zero;CloseHandle(ew);ew=IntPtr.Zero;if(ResumeThread(pi.thread)==FAILED)throw new Exception("resume failed");resumed=true;Thread t1=new Thread(()=>Drain(rr,of,pi.process,()=>{overflow=true;TerminateProcess(pi.process,2);}));Thread t2=new Thread(()=>Drain(er,ef,pi.process,()=>{overflow=true;TerminateProcess(pi.process,2);}));t1.Start();t2.Start();uint w=WaitForSingleObject(pi.process,(uint)timeout);bool timed=w==TIMEOUT;if(!TerminateJobObject(j,2))throw new Exception("job termination failed");AC ac=new AC();uint got=0;var until=DateTime.UtcNow.AddSeconds(3);do{if(!QueryInformationJobObject(j,2,ref ac,(uint)Marshal.SizeOf(typeof(AC)),out got))throw new Exception("job accounting query failed");if(ac.active==0)break;Thread.Sleep(20);}while(DateTime.UtcNow<until);if(ac.active!=0)throw new Exception("active job processes did not reach zero");CloseHandle(j);j=IntPtr.Zero;uint final=WaitForSingleObject(pi.process,2000);if(w==TIMEOUT&&final!=OK)throw new Exception("job did not terminate process");if(!t1.Join(3000)||!t2.Join(3000))throw new Exception("output drain did not terminate");return "{\"timedOut\":"+timed.ToString().ToLower()+",\"overflow\":"+overflow.ToString().ToLower()+",\"stdoutFile\":"+J(of)+",\"stderrFile\":"+J(ef)+"}";}finally{if(made&&!resumed)TerminateProcess(pi.process,1);if(j!=IntPtr.Zero)CloseHandle(j);if(pi.thread!=IntPtr.Zero)CloseHandle(pi.thread);if(pi.process!=IntPtr.Zero)CloseHandle(pi.process);if(rr!=IntPtr.Zero)CloseHandle(rr);if(rw!=IntPtr.Zero)CloseHandle(rw);if(er!=IntPtr.Zero)CloseHandle(er);if(ew!=IntPtr.Zero)CloseHandle(ew);if(verified!=null)verified.Dispose();}}
46
- [DllImport("kernel32.dll")] static extern IntPtr GetStdHandle(int n);
47
- }
48
- '@
49
- Add-Type -TypeDefinition $source
50
- $result=[JobRunner]::Run($exe,[string]$r.expectedSHA256,$args,$envMap,[string]$r.cwd,[string]$r.stdout,[string]$r.stderr,[string]$r.pidFile,[int]$r.timeoutMs,[int]$r.memoryLimitMb,[bool]$r.forceAssignmentFailure)
51
- Write-Output $result
@@ -1,33 +0,0 @@
1
- # P0-C native source-opening spike
2
-
3
- Run on the target Windows host with:
4
-
5
- ```powershell
6
- pwsh -NoProfile -File tests/p0-native/windows-handle-loader.tests.ps1
7
- ```
8
-
9
- The fixture compiles a test-only C# P/Invoke shim with PowerShell `Add-Type`; it
10
- adds no package or product dependency. It creates a private, ACL-hardened NTFS
11
- fixture before writing synthetic bytes, opens each path component relative to a
12
- directory handle through `NtCreateFile`, with `OBJ_DONT_REPARSE` and
13
- `FILE_OPEN_REPARSE_POINT` active, reads through the validated final handle, and
14
- checks NTFS volume identity, file identity, link count, and post-read identity.
15
- It uses native `CreateHardLinkW` and attempts symbolic-link and directory-
16
- junction adversarial cases without printing source bytes or paths. Setup errors
17
- are reported only as numeric Win32 codes and remain gaps, not rejection passes.
18
- The fixture uses the OS Junction primitive only as a fallback after the native
19
- reparse-ioctl creator reports its numeric setup error, while still exercising
20
- ancestor rejection through the native loader path.
21
-
22
- The native `GetSecurityInfo` check accepts only the current service SID, SYSTEM,
23
- and built-in Administrators, and rejects unknown/broad/deny ACEs, wrong owners,
24
- missing source read rights, missing ancestor replacement rights, and link count
25
- greater than one. The fixture also tests an inherited `BUILTIN\Users` ACE,
26
- opened-handle read after path replacement, and identity stability. Other ACE and
27
- reparse-tag variants remain untested.
28
-
29
- This is a feasibility fixture, not a qualified loader. It does not prove
30
- effective ACL evaluation for every token, ancestor replacement rights, atomic
31
- ACL/swap behavior, or Linux/ext4. A production helper still needs a reviewed
32
- implementation and independent tests for those properties before Windows is
33
- advertised.