opencode-secrets-obfuscation 0.1.0 → 0.1.1
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.
- package/LICENSE +21 -21
- package/README.md +1 -31
- package/package.json +29 -23
- package/plugin.mjs +3841 -0
- package/.editorconfig +0 -12
- package/.github/workflows/ci.yml +0 -75
- package/.github/workflows/release.yml +0 -50
- package/.node-version +0 -1
- package/AGENTS.md +0 -23
- package/INSTALL.md +0 -104
- package/docs/checkpoint.md +0 -68
- package/docs/history/1-spec-review.md +0 -233
- package/docs/history/10-p0-launch-prerequisites-review.md +0 -186
- package/docs/history/11-p0-job-containment-review.md +0 -188
- package/docs/history/12-p0-api-client.md +0 -15
- package/docs/history/13-p0-status-review.md +0 -122
- package/docs/history/14-p0-containment-final-review.md +0 -84
- package/docs/history/15-config-source-implementation.md +0 -27
- package/docs/history/16-sanitizer-implementation.md +0 -65
- package/docs/history/17-store-policy-implementation.md +0 -36
- package/docs/history/18-pure-mvp-review.md +0 -182
- package/docs/history/19-pure-mvp-re-review.md +0 -142
- package/docs/history/2-p0-runtime-prerequisites.md +0 -163
- package/docs/history/20-pure-mvp-final-review.md +0 -116
- package/docs/history/21-pure-mvp-review-final.md +0 -109
- package/docs/history/22-pure-mvp-signoff-review.md +0 -113
- package/docs/history/23-pure-mvp-signoff-review.md +0 -75
- package/docs/history/24-owned-tools-implementation.md +0 -27
- package/docs/history/25-v2-plugin-integration.md +0 -53
- package/docs/history/26-owned-tools-review.md +0 -138
- package/docs/history/27-owned-tools-final-review.md +0 -93
- package/docs/history/28-owned-tools-signoff.md +0 -98
- package/docs/history/3-p0-harness-foundation.md +0 -105
- package/docs/history/4-p0-source-opening.md +0 -78
- package/docs/history/5-qualification-gate-review.md +0 -133
- package/docs/history/6-p0-infrastructure-review.md +0 -275
- package/docs/history/7-p0-job-containment.md +0 -29
- package/docs/history/8-p0-evidence-support.md +0 -52
- package/docs/history/9-p0-launch-plan.md +0 -88
- package/docs/spec/design.md +0 -111
- package/docs/spec/intent.md +0 -81
- package/docs/spec/plan.md +0 -78
- package/docs/testing.md +0 -157
- package/scripts/build-plugin.mjs +0 -84
- package/scripts/install-opencode-ci.ps1 +0 -39
- package/scripts/install-opencode-ci.sh +0 -27
- package/scripts/smoke.mjs +0 -105
- package/scripts/verify-package.mjs +0 -18
- package/src/README.md +0 -22
- package/src/budgets/index.ts +0 -49
- package/src/config/README.md +0 -4
- package/src/config/options.ts +0 -76
- package/src/diagnostics/README.md +0 -4
- package/src/opencode/v2/filter.ts +0 -350
- package/src/opencode/v2/index.ts +0 -86
- package/src/opencode/v2/runtime.ts +0 -207
- package/src/opencode/v2/transports/README.md +0 -5
- package/src/runtime/http-transport.ts +0 -20
- package/src/sanitize/README.md +0 -4
- package/src/sanitize/index.ts +0 -454
- package/src/sources/README.md +0 -5
- package/src/sources/dotenv.ts +0 -133
- package/src/sources/native/README.md +0 -5
- package/src/store/index.ts +0 -64
- package/tests/README.md +0 -26
- package/tests/contract/plugin-v2.test.ts +0 -29
- package/tests/fixtures/README.md +0 -9
- package/tests/fixtures/job-child.mjs +0 -18
- package/tests/fixtures/plugins/README.md +0 -8
- package/tests/fixtures/process-child.mjs +0 -20
- package/tests/fixtures/services/README.md +0 -10
- package/tests/integration/components.test.ts +0 -5
- package/tests/integration/transparent-runtime.test.ts +0 -97
- package/tests/integration/windows-job.test.ts +0 -71
- package/tests/p0-host/README.md +0 -24
- package/tests/p0-host/linux-job.sh +0 -61
- package/tests/p0-host/windows-job.ps1 +0 -51
- package/tests/p0-native/README.md +0 -33
- package/tests/p0-native/windows-handle-loader.tests.ps1 +0 -191
- package/tests/scaffold/repository.test.ts +0 -105
- package/tests/smoke/README.md +0 -37
- package/tests/smoke/cases.json +0 -17
- package/tests/smoke/qualification.json +0 -20
- package/tests/smoke/runtime-candidate.json +0 -111
- package/tests/support/README.md +0 -32
- package/tests/support/child-process.ts +0 -235
- package/tests/support/host-api.ts +0 -123
- package/tests/support/opencode-environment.ts +0 -239
- package/tests/support/qualification.ts +0 -154
- package/tests/support/recording-endpoint.ts +0 -242
- package/tests/support/run-directory.ts +0 -179
- package/tests/support/smoke-cases.ts +0 -289
- package/tests/unit/budgets.test.ts +0 -35
- package/tests/unit/config.test.ts +0 -40
- package/tests/unit/dotenv.test.ts +0 -72
- package/tests/unit/filter-size.test.ts +0 -132
- package/tests/unit/harness.test.ts +0 -228
- package/tests/unit/host-api.test.ts +0 -122
- package/tests/unit/opencode-environment.test.ts +0 -161
- package/tests/unit/qualification.test.ts +0 -128
- package/tests/unit/recording-endpoint.test.ts +0 -188
- package/tests/unit/sanitize.test.ts +0 -159
- package/tests/unit/smoke-cases.test.ts +0 -20
- package/tsconfig.json +0 -20
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict"
|
|
2
|
-
import { createHash, randomUUID } from "node:crypto"
|
|
3
|
-
import { lstat, mkdir, readFile, rm, rmdir, symlink, writeFile } from "node:fs/promises"
|
|
4
|
-
import { tmpdir } from "node:os"
|
|
5
|
-
import { test } from "node:test"
|
|
6
|
-
import { dirname, join } from "node:path"
|
|
7
|
-
import { ChildProcessError, runChild, type ChildOptions } from "../support/child-process.ts"
|
|
8
|
-
import { createRunDirectory } from "../support/run-directory.ts"
|
|
9
|
-
|
|
10
|
-
const fixture = join(import.meta.dirname, "../fixtures/process-child.mjs")
|
|
11
|
-
const nodeHash = createHash("sha256").update(await readFile(process.execPath)).digest("hex")
|
|
12
|
-
const artifacts = join(import.meta.dirname, "../../.artifacts")
|
|
13
|
-
const staleFixedRoot = join(artifacts, "harness-test-root")
|
|
14
|
-
|
|
15
|
-
async function createPrivateTestRun(label: string) {
|
|
16
|
-
const root = join(artifacts, `harness-${label}-${process.pid}-${randomUUID()}`)
|
|
17
|
-
if (await lstat(root).catch(() => undefined)) throw new Error("TEST_RUN_ROOT_ALREADY_EXISTS")
|
|
18
|
-
try {
|
|
19
|
-
const run = await createRunDirectory(root)
|
|
20
|
-
return {
|
|
21
|
-
run,
|
|
22
|
-
cleanupRoot: async () => { await rmdir(root) },
|
|
23
|
-
}
|
|
24
|
-
} catch (error) {
|
|
25
|
-
await rmdir(root).catch(() => undefined)
|
|
26
|
-
throw error
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function removeTestRun(run: Awaited<ReturnType<typeof createPrivateTestRun>>): Promise<void> {
|
|
31
|
-
await rm(run.run.path, { recursive: true, force: true })
|
|
32
|
-
await run.cleanupRoot()
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function child(cwd: string, args: readonly string[], extra: Partial<ChildOptions> = {}): ChildOptions {
|
|
36
|
-
return {
|
|
37
|
-
executable: process.execPath,
|
|
38
|
-
expectedExecutableSha256: nodeHash,
|
|
39
|
-
args,
|
|
40
|
-
cwd,
|
|
41
|
-
env: {},
|
|
42
|
-
allowedEnv: [],
|
|
43
|
-
startupTimeoutMs: 2_000,
|
|
44
|
-
overallTimeoutMs: 2_000,
|
|
45
|
-
...extra,
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
test("run directories are private, marked, and cleanup refuses unsafe nested content", async () => {
|
|
50
|
-
const testRun = await createPrivateTestRun("private")
|
|
51
|
-
const { run } = testRun
|
|
52
|
-
const outside = join(run.path, "..", "outside.txt")
|
|
53
|
-
const link = join(run.path, "outside-link")
|
|
54
|
-
const childPath = join(run.path, "private-child")
|
|
55
|
-
try {
|
|
56
|
-
await writeFile(outside, "keep")
|
|
57
|
-
await mkdir(childPath, { mode: 0o700 })
|
|
58
|
-
await run.verifyPrivateChild(childPath)
|
|
59
|
-
await rmdir(childPath)
|
|
60
|
-
if (process.platform !== "win32") {
|
|
61
|
-
await symlink(outside, link)
|
|
62
|
-
await assert.rejects(run.cleanup(), (error: unknown) => error instanceof Error && error.message === "RUN_DIRECTORY_CONTENT_UNSAFE")
|
|
63
|
-
await rm(link)
|
|
64
|
-
}
|
|
65
|
-
assert.equal(await readFile(run.marker, "utf8"), "opencode-secrets-obfuscation run-directory v1\n")
|
|
66
|
-
await run.cleanup()
|
|
67
|
-
await assert.rejects(lstat(run.path), { code: "ENOENT" })
|
|
68
|
-
assert.equal(await readFile(outside, "utf8"), "keep")
|
|
69
|
-
} finally {
|
|
70
|
-
await rm(outside, { force: true })
|
|
71
|
-
await removeTestRun(testRun)
|
|
72
|
-
}
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
test("cleanup refuses a path without the exact ownership marker", async () => {
|
|
76
|
-
const testRun = await createPrivateTestRun("marker")
|
|
77
|
-
const { run } = testRun
|
|
78
|
-
try {
|
|
79
|
-
await writeFile(run.marker, "wrong\n")
|
|
80
|
-
await assert.rejects(run.cleanup(), (error: unknown) => error instanceof Error && error.message === "RUN_DIRECTORY_NOT_OWNED")
|
|
81
|
-
await writeFile(run.marker, "opencode-secrets-obfuscation run-directory v1\n")
|
|
82
|
-
await run.cleanup()
|
|
83
|
-
} finally {
|
|
84
|
-
await removeTestRun(testRun)
|
|
85
|
-
}
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
test("child execution uses only the supplied environment and cwd", async () => {
|
|
89
|
-
const testRun = await createPrivateTestRun("environment")
|
|
90
|
-
const { run } = testRun
|
|
91
|
-
try {
|
|
92
|
-
const result = await runChild(child(run.path, [fixture, "env", "HARNESS_SYNTHETIC"], {
|
|
93
|
-
env: { HARNESS_SYNTHETIC: "fixture-value" },
|
|
94
|
-
allowedEnv: ["HARNESS_SYNTHETIC"],
|
|
95
|
-
}))
|
|
96
|
-
assert.equal(result.exitCode, 0)
|
|
97
|
-
assert.equal(result.stdout, "fixture-value\n")
|
|
98
|
-
assert.equal(result.stderr, "")
|
|
99
|
-
assert.equal(result.cwd, run.path)
|
|
100
|
-
} finally {
|
|
101
|
-
await removeTestRun(testRun)
|
|
102
|
-
}
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
test("child output is bounded and teardown completes before cleanup", async () => {
|
|
106
|
-
const testRun = await createPrivateTestRun("output")
|
|
107
|
-
const { run } = testRun
|
|
108
|
-
try {
|
|
109
|
-
await assert.rejects(runChild(child(run.path, [fixture, "stdout", "64"], { stdoutLimitBytes: 16 })),
|
|
110
|
-
(error: unknown) => error instanceof ChildProcessError && error.code === "OUTPUT_LIMIT")
|
|
111
|
-
await run.cleanup()
|
|
112
|
-
} finally {
|
|
113
|
-
await removeTestRun(testRun)
|
|
114
|
-
}
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
test("overall timeout escalates a stubborn owned child and waits for close", { skip: process.platform !== "win32" }, async () => {
|
|
118
|
-
const testRun = await createPrivateTestRun("timeout")
|
|
119
|
-
const { run } = testRun
|
|
120
|
-
try {
|
|
121
|
-
await assert.rejects(runChild(child(run.path, [fixture, "ignore-term"], { overallTimeoutMs: 50 })),
|
|
122
|
-
(error: unknown) => error instanceof ChildProcessError && error.code === "TIMEOUT")
|
|
123
|
-
await run.cleanup()
|
|
124
|
-
} finally {
|
|
125
|
-
await removeTestRun(testRun)
|
|
126
|
-
}
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
test("pre-cancelled child execution does not spawn", async () => {
|
|
130
|
-
const testRun = await createPrivateTestRun("pre-cancel")
|
|
131
|
-
const { run } = testRun
|
|
132
|
-
const marker = join(run.path, "spawned")
|
|
133
|
-
const controller = new AbortController()
|
|
134
|
-
controller.abort()
|
|
135
|
-
try {
|
|
136
|
-
await assert.rejects(runChild(child(run.path, [fixture, "marker", marker], { signal: controller.signal })),
|
|
137
|
-
(error: unknown) => error instanceof ChildProcessError && error.code === "CANCELLED")
|
|
138
|
-
await assert.rejects(lstat(marker), { code: "ENOENT" })
|
|
139
|
-
} finally {
|
|
140
|
-
await removeTestRun(testRun)
|
|
141
|
-
}
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
test("post-spawn cancellation waits for the child to close before cleanup", async () => {
|
|
145
|
-
const testRun = await createPrivateTestRun("cancel")
|
|
146
|
-
const { run } = testRun
|
|
147
|
-
const controller = new AbortController()
|
|
148
|
-
try {
|
|
149
|
-
const pending = runChild(child(run.path, [fixture, "sleep", "5000"], { signal: controller.signal }))
|
|
150
|
-
setTimeout(() => controller.abort(), 25)
|
|
151
|
-
await assert.rejects(pending, (error: unknown) => error instanceof ChildProcessError && error.code === "CANCELLED")
|
|
152
|
-
await run.cleanup()
|
|
153
|
-
} finally {
|
|
154
|
-
await removeTestRun(testRun)
|
|
155
|
-
}
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
test("malformed options and executable identity mismatch fail safely", async () => {
|
|
159
|
-
await assert.rejects(runChild(null), (error: unknown) => error instanceof ChildProcessError && error.code === "INVALID_OPTIONS")
|
|
160
|
-
await assert.rejects(runChild({ args: [], env: {}, allowedEnv: [] }), (error: unknown) => error instanceof ChildProcessError && error.code === "INVALID_OPTIONS")
|
|
161
|
-
const testRun = await createPrivateTestRun("options")
|
|
162
|
-
const { run } = testRun
|
|
163
|
-
try {
|
|
164
|
-
await assert.rejects(runChild(child(run.path, [fixture], { expectedExecutableSha256: "0".repeat(64) })),
|
|
165
|
-
(error: unknown) => error instanceof ChildProcessError && error.code === "EXECUTABLE_MISMATCH")
|
|
166
|
-
} finally {
|
|
167
|
-
await removeTestRun(testRun)
|
|
168
|
-
}
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
test("Windows environment names cannot be duplicated case-insensitively", async () => {
|
|
172
|
-
const testRun = await createPrivateTestRun("windows-env")
|
|
173
|
-
const { run } = testRun
|
|
174
|
-
try {
|
|
175
|
-
const options = child(run.path, [fixture], {
|
|
176
|
-
env: { PATH: "one", path: "two" },
|
|
177
|
-
allowedEnv: ["PATH", "path"],
|
|
178
|
-
})
|
|
179
|
-
if (process.platform === "win32") {
|
|
180
|
-
await assert.rejects(runChild(options), (error: unknown) => error instanceof ChildProcessError && error.code === "INVALID_OPTIONS")
|
|
181
|
-
}
|
|
182
|
-
} finally {
|
|
183
|
-
await removeTestRun(testRun)
|
|
184
|
-
}
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
test("fresh missing private-root hierarchy is created without changing existing ancestors", async () => {
|
|
188
|
-
const testRun = await createPrivateTestRun("fresh-missing")
|
|
189
|
-
const { run } = testRun
|
|
190
|
-
try {
|
|
191
|
-
assert.equal((await lstat(run.path)).isDirectory(), true)
|
|
192
|
-
await run.cleanup()
|
|
193
|
-
} finally {
|
|
194
|
-
await removeTestRun(testRun)
|
|
195
|
-
}
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
test("root-owned sticky ancestor is accepted while the run directory remains current-owned", { skip: process.platform === "win32" }, async () => {
|
|
199
|
-
const freshRoot = join(tmpdir(), `opencode-harness-${process.pid}-${Date.now()}`, "nested")
|
|
200
|
-
const run = await createRunDirectory(freshRoot)
|
|
201
|
-
try {
|
|
202
|
-
await run.cleanup()
|
|
203
|
-
} finally {
|
|
204
|
-
await rmdir(freshRoot).catch(() => undefined)
|
|
205
|
-
await rmdir(dirname(freshRoot)).catch(() => undefined)
|
|
206
|
-
}
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
test("cross-volume child paths are rejected before ACL inspection on Windows", { skip: process.platform !== "win32" }, async () => {
|
|
210
|
-
const testRun = await createPrivateTestRun("cross-volume")
|
|
211
|
-
const { run } = testRun
|
|
212
|
-
try {
|
|
213
|
-
await assert.rejects(run.verifyPrivateChild("D:\\outside"), (error: unknown) => error instanceof Error && error.message === "RUN_DIRECTORY_UNSAFE_ROOT")
|
|
214
|
-
} finally {
|
|
215
|
-
await removeTestRun(testRun)
|
|
216
|
-
}
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
test("a stale legacy fixed root does not affect a fresh test root", async () => {
|
|
220
|
-
const testRun = await createPrivateTestRun("stale-root-regression")
|
|
221
|
-
try {
|
|
222
|
-
assert.notEqual(testRun.run.path, staleFixedRoot)
|
|
223
|
-
assert.equal(testRun.run.path.startsWith(`${staleFixedRoot}-`), false)
|
|
224
|
-
assert.equal((await lstat(testRun.run.path)).isDirectory(), true)
|
|
225
|
-
} finally {
|
|
226
|
-
await removeTestRun(testRun)
|
|
227
|
-
}
|
|
228
|
-
})
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict"
|
|
2
|
-
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"
|
|
3
|
-
import test from "node:test"
|
|
4
|
-
import { join, resolve } from "node:path"
|
|
5
|
-
import { requestHostInfo } from "../support/host-api.ts"
|
|
6
|
-
|
|
7
|
-
const password = "b".repeat(64)
|
|
8
|
-
const root = resolve(process.cwd(), ".artifacts", "host-api-test", "private")
|
|
9
|
-
|
|
10
|
-
interface FixtureResponse {
|
|
11
|
-
readonly status?: number
|
|
12
|
-
readonly headers?: Record<string, string>
|
|
13
|
-
readonly body?: string
|
|
14
|
-
readonly delay?: number
|
|
15
|
-
readonly redirect?: string
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async function fixture(response: FixtureResponse): Promise<{ server: Server; origin: string; close: () => Promise<void> }> {
|
|
19
|
-
const server = createServer((request: IncomingMessage, reply: ServerResponse) => {
|
|
20
|
-
if (request.url !== "/api/info" || request.method !== "GET") {
|
|
21
|
-
reply.writeHead(404).end()
|
|
22
|
-
return
|
|
23
|
-
}
|
|
24
|
-
const authenticated = request.headers.authorization === `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`
|
|
25
|
-
if (!authenticated) {
|
|
26
|
-
reply.writeHead(401).end()
|
|
27
|
-
return
|
|
28
|
-
}
|
|
29
|
-
const send = () => {
|
|
30
|
-
if (response.redirect) {
|
|
31
|
-
reply.writeHead(302, { location: response.redirect }).end()
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
|
-
reply.writeHead(response.status ?? 200, { "content-type": "application/json", ...response.headers })
|
|
35
|
-
reply.end(response.body ?? JSON.stringify({ version: "2.0.28", pid: 42, urls: [], paths: { tmp: join(root, "temp") } }))
|
|
36
|
-
}
|
|
37
|
-
if (response.delay === undefined) send()
|
|
38
|
-
else setTimeout(send, response.delay)
|
|
39
|
-
})
|
|
40
|
-
await new Promise<void>((resolvePromise, reject) => {
|
|
41
|
-
server.once("error", reject)
|
|
42
|
-
server.listen(0, "127.0.0.1", () => resolvePromise())
|
|
43
|
-
})
|
|
44
|
-
const address = server.address()
|
|
45
|
-
assert.equal(address !== null && typeof address === "object", true)
|
|
46
|
-
const origin = `http://127.0.0.1:${(address as { port: number }).port}`
|
|
47
|
-
return { server, origin, close: () => new Promise<void>((resolvePromise) => server.close(() => resolvePromise())) }
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function input(origin: string, extra: Record<string, unknown> = {}): Record<string, unknown> {
|
|
51
|
-
return { origin, password, expectedVersion: "2.0.28", expectedTmpRoot: root, ...extra }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function withFixture(response: FixtureResponse, run: (origin: string) => Promise<void>): Promise<void> {
|
|
55
|
-
const server = await fixture(response)
|
|
56
|
-
try { await run(server.origin) } finally { await server.close() }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
test("gets only the authenticated safe ServerInfo projection", async () => {
|
|
60
|
-
await withFixture({}, async (origin) => {
|
|
61
|
-
const result = await requestHostInfo(input(origin, { expectedPID: 42 }))
|
|
62
|
-
assert.deepEqual(result, { version: "2.0.28", pid: 42, urls: [], paths: { tmp: join(root, "temp") } })
|
|
63
|
-
})
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
test("projects required identity fields without requiring optional ServerInfo fields", async () => {
|
|
67
|
-
await withFixture({ body: JSON.stringify({ version: "2.0.28", pid: 42, urls: [], paths: { tmp: join(root, "temp"), home: "ignored" }, capabilities: { future: true } }) }, async (origin) => {
|
|
68
|
-
const result = await requestHostInfo(input(origin))
|
|
69
|
-
assert.equal(result.version === "2.0.28" && result.pid === 42 && result.paths.tmp === join(root, "temp"), true)
|
|
70
|
-
assert.equal(Object.hasOwn(result, "capabilities"), false)
|
|
71
|
-
})
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
test("rejects wrong version, wrong PID, and a temporary path outside the private root", async () => {
|
|
75
|
-
await withFixture({ body: JSON.stringify({ version: "wrong", pid: 42, urls: [], paths: { tmp: join(root, "temp") } }) }, async (origin) => {
|
|
76
|
-
await assert.rejects(requestHostInfo(input(origin)), { message: "HOST_API_VERSION" })
|
|
77
|
-
})
|
|
78
|
-
await withFixture({ body: JSON.stringify({ version: "2.0.28", pid: 7, urls: [], paths: { tmp: join(root, "temp") } }) }, async (origin) => {
|
|
79
|
-
await assert.rejects(requestHostInfo(input(origin, { expectedPID: 42 })), { message: "HOST_API_PID" })
|
|
80
|
-
})
|
|
81
|
-
await withFixture({ body: JSON.stringify({ version: "2.0.28", pid: 42, urls: [], paths: { tmp: resolve(root, "..", "not-private") } }) }, async (origin) => {
|
|
82
|
-
await assert.rejects(requestHostInfo(input(origin)), { message: "HOST_API_TMP_ROOT" })
|
|
83
|
-
})
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
test("rejects an unowned advertised URL", async () => {
|
|
87
|
-
await withFixture({ body: JSON.stringify({ version: "2.0.28", pid: 42, urls: ["http://127.0.0.1:9"], paths: { tmp: join(root, "temp") } }) }, async (origin) => {
|
|
88
|
-
await assert.rejects(requestHostInfo(input(origin)), { message: "HOST_API_URL" })
|
|
89
|
-
})
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
test("requires authentication and does not follow redirects", async () => {
|
|
93
|
-
await withFixture({ status: 401 }, async (origin) => {
|
|
94
|
-
await assert.rejects(requestHostInfo(input(origin)), { message: "HOST_API_STATUS" })
|
|
95
|
-
})
|
|
96
|
-
await withFixture({ redirect: "http://127.0.0.1:9/api/info" }, async (origin) => {
|
|
97
|
-
await assert.rejects(requestHostInfo(input(origin)), { message: "HOST_API_REDIRECT" })
|
|
98
|
-
})
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
test("bounds body and deadline, and reports fixed codes only", async () => {
|
|
102
|
-
await withFixture({ body: "{" + "x".repeat(70_000) }, async (origin) => {
|
|
103
|
-
await assert.rejects(requestHostInfo(input(origin)), { message: "HOST_API_BODY" })
|
|
104
|
-
})
|
|
105
|
-
await withFixture({ delay: 100 }, async (origin) => {
|
|
106
|
-
await assert.rejects(requestHostInfo(input(origin, { timeoutMs: 10 })), { message: "HOST_API_TIMEOUT" })
|
|
107
|
-
})
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
test("rejects malformed input without invoking getters or exposing arbitrary errors", async () => {
|
|
111
|
-
let invoked = false
|
|
112
|
-
const malformed: unknown[] = [
|
|
113
|
-
null, {}, { origin: "https://127.0.0.1:1", password, expectedVersion: "2.0.28", expectedTmpRoot: root },
|
|
114
|
-
{ origin: "http://localhost:1", password, expectedVersion: "2.0.28", expectedTmpRoot: root },
|
|
115
|
-
{ origin: "http://127.0.0.1:1/path", password, expectedVersion: "2.0.28", expectedTmpRoot: root },
|
|
116
|
-
{ origin: "http://127.0.0.1:1", password, expectedVersion: "2.0.28", expectedTmpRoot: root, get timeoutMs() { invoked = true; throw new Error("private") } },
|
|
117
|
-
]
|
|
118
|
-
for (const value of malformed) {
|
|
119
|
-
await assert.rejects(requestHostInfo(value), (error: unknown) => error instanceof Error && error.message === "HOST_API_INVALID")
|
|
120
|
-
}
|
|
121
|
-
assert.equal(invoked, false)
|
|
122
|
-
})
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict"
|
|
2
|
-
import test from "node:test"
|
|
3
|
-
import { join, resolve, sep } from "node:path"
|
|
4
|
-
import { buildOpenCodeEnvironment } from "../support/opencode-environment.ts"
|
|
5
|
-
|
|
6
|
-
// Public synthetic fixture password; real smoke runs generate their own CSPRNG value.
|
|
7
|
-
const password = "a".repeat(64)
|
|
8
|
-
const build = (input: Record<string, unknown>) => buildOpenCodeEnvironment({ password, ...input })
|
|
9
|
-
const root = resolve(process.cwd(), ".artifacts", "opencode-environment-test")
|
|
10
|
-
const trustedOs = {
|
|
11
|
-
userProfile: join(root, "home"),
|
|
12
|
-
home: join(root, "home"),
|
|
13
|
-
programData: join(root, "programdata"),
|
|
14
|
-
temp: join(root, "temp"),
|
|
15
|
-
tmp: join(root, "temp"),
|
|
16
|
-
path: join(root, "os", "bin"),
|
|
17
|
-
systemRoot: join(root, "os"),
|
|
18
|
-
comspec: join(root, "os", "cmd.exe"),
|
|
19
|
-
pathext: ".COM;.EXE;.BAT;.CMD",
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
test("builds only fixture-rooted OpenCode paths and a complete allowlist", () => {
|
|
23
|
-
const result = build({ root, trustedOs })
|
|
24
|
-
const expected = [result.cwd, ...Object.values(result.paths)]
|
|
25
|
-
|
|
26
|
-
for (const value of expected) {
|
|
27
|
-
assert.equal(value === root || value.startsWith(`${root}${sep}`), true)
|
|
28
|
-
}
|
|
29
|
-
assert.deepEqual(result.env, Object.fromEntries(result.allowedEnv.map((key: string) => [key, result.env[key]])))
|
|
30
|
-
assert.equal(result.env.OPENCODE_CONFIG_PROJECT_DISABLE, "1")
|
|
31
|
-
assert.equal(result.env.OPENCODE_DB, result.paths.database)
|
|
32
|
-
assert.deepEqual(JSON.parse(result.env.OPENCODE_CONFIG_CONTENT!), {
|
|
33
|
-
snapshots: false,
|
|
34
|
-
warming: false,
|
|
35
|
-
update: "disable",
|
|
36
|
-
})
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
test("does not copy ambient credentials, runtime controls, proxies, or plugin paths", () => {
|
|
40
|
-
const marker = "OPENCODE_ENVIRONMENT_TEST_MARKER_7f3a"
|
|
41
|
-
const previous = process.env[marker]
|
|
42
|
-
process.env[marker] = "harmless-marker"
|
|
43
|
-
try {
|
|
44
|
-
const result = build({ root, trustedOs })
|
|
45
|
-
assert.equal(Object.keys(result.env).includes(marker), false)
|
|
46
|
-
assert.equal(Object.keys(result.env).some((key) => /proxy|node_options|plugin|credential|token/i.test(key)), false)
|
|
47
|
-
} finally {
|
|
48
|
-
if (previous === undefined) delete process.env[marker]
|
|
49
|
-
else process.env[marker] = previous
|
|
50
|
-
}
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
test("rejects relative or escaping roots and unsafe trusted values", () => {
|
|
54
|
-
assert.throws(() => build({ root: "relative", trustedOs }), /OPENCODE_ENVIRONMENT_INVALID/)
|
|
55
|
-
assert.throws(
|
|
56
|
-
() => build({ root, trustedOs: { ...trustedOs, temp: join(root, "..", "other") } }),
|
|
57
|
-
/OPENCODE_ENVIRONMENT_INVALID/,
|
|
58
|
-
)
|
|
59
|
-
assert.throws(() => build({ root, trustedOs: { ...trustedOs, path: "bad\npath" } }), /OPENCODE_ENVIRONMENT_INVALID/)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
test("locks the boot config and rejects unsafe or malformed overrides", () => {
|
|
63
|
-
assert.throws(() => build({ root, trustedOs, configContent: { snapshots: true } }), /OPENCODE_ENVIRONMENT_INVALID/)
|
|
64
|
-
assert.throws(() => build({ root, trustedOs, configContent: { extra: false } }), /OPENCODE_ENVIRONMENT_INVALID/)
|
|
65
|
-
const cyclic: Record<string, unknown> = {}
|
|
66
|
-
cyclic.self = cyclic
|
|
67
|
-
assert.throws(() => build({ root, trustedOs, configContent: cyclic }), /OPENCODE_ENVIRONMENT_INVALID/)
|
|
68
|
-
const getter = Object.defineProperty({}, "snapshots", { get: () => false })
|
|
69
|
-
assert.throws(() => build({ root, trustedOs, configContent: getter }), /OPENCODE_ENVIRONMENT_INVALID/)
|
|
70
|
-
const result = build({ root, trustedOs })
|
|
71
|
-
assert.throws(() => (result.env as Record<string, string>).PATH = "changed", TypeError)
|
|
72
|
-
assert.throws(() => (result.allowedEnv as string[]).push("BAD"), TypeError)
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
test("malformed builder input cannot invoke accessors or echo arbitrary causes", () => {
|
|
76
|
-
let invoked = false
|
|
77
|
-
const inputs: unknown[] = [
|
|
78
|
-
null, {}, { root, password, trustedOs: null },
|
|
79
|
-
{ root: `${root}\0`, password, trustedOs },
|
|
80
|
-
{ root, password, trustedOs: { ...trustedOs, unexpected: "fixture" } },
|
|
81
|
-
{ root, password, trustedOs: { ...trustedOs, systemRoot: "relative" } },
|
|
82
|
-
{ get root() { invoked = true; throw new Error("synthetic-private") }, password, trustedOs },
|
|
83
|
-
{ root, password, trustedOs: { ...trustedOs, get path() { invoked = true; throw new Error("synthetic-private") } } },
|
|
84
|
-
]
|
|
85
|
-
for (const input of inputs) {
|
|
86
|
-
assert.throws(() => (buildOpenCodeEnvironment as (value: unknown) => unknown)(input),
|
|
87
|
-
(error: unknown) => error instanceof Error && error.message === "OPENCODE_ENVIRONMENT_INVALID")
|
|
88
|
-
}
|
|
89
|
-
assert.equal(invoked, false)
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
test("sandbox config merges host-neutral keys while boot locks win", () => {
|
|
93
|
-
const result = build({
|
|
94
|
-
root,
|
|
95
|
-
trustedOs,
|
|
96
|
-
sandboxConfig: {
|
|
97
|
-
plugins: ["./plugin.mjs"],
|
|
98
|
-
provider: { smoke: { npm: "@ai-sdk/openai-compatible" } },
|
|
99
|
-
temperature: 0,
|
|
100
|
-
},
|
|
101
|
-
})
|
|
102
|
-
assert.deepEqual(JSON.parse(result.env.OPENCODE_CONFIG_CONTENT!), {
|
|
103
|
-
plugins: ["./plugin.mjs"],
|
|
104
|
-
provider: { smoke: { npm: "@ai-sdk/openai-compatible" } },
|
|
105
|
-
temperature: 0,
|
|
106
|
-
snapshots: false,
|
|
107
|
-
warming: false,
|
|
108
|
-
update: "disable",
|
|
109
|
-
})
|
|
110
|
-
// configContent keeps its own lock alongside the sandbox channel.
|
|
111
|
-
assert.throws(() => build({ root, trustedOs, configContent: { warming: true }, sandboxConfig: { plugins: [] } }),
|
|
112
|
-
/OPENCODE_ENVIRONMENT_INVALID/)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
test("sandbox config cannot flip locked host behaviors or smuggle malformed payloads", () => {
|
|
116
|
-
assert.throws(() => build({ root, trustedOs, sandboxConfig: { snapshots: true } }), /OPENCODE_ENVIRONMENT_INVALID/)
|
|
117
|
-
assert.throws(() => build({ root, trustedOs, sandboxConfig: { warming: true } }), /OPENCODE_ENVIRONMENT_INVALID/)
|
|
118
|
-
assert.throws(() => build({ root, trustedOs, sandboxConfig: { update: "manual" } }), /OPENCODE_ENVIRONMENT_INVALID/)
|
|
119
|
-
assert.throws(() => build({ root, trustedOs, sandboxConfig: [] }), /OPENCODE_ENVIRONMENT_INVALID/)
|
|
120
|
-
assert.throws(() => build({ root, trustedOs, sandboxConfig: "plugins" }), /OPENCODE_ENVIRONMENT_INVALID/)
|
|
121
|
-
assert.throws(() => build({ root, trustedOs, sandboxConfig: { temperature: Number.NaN } }), /OPENCODE_ENVIRONMENT_INVALID/)
|
|
122
|
-
const cyclic: Record<string, unknown> = { plugins: [] as unknown[] }
|
|
123
|
-
;(cyclic.plugins as unknown[]).push(cyclic)
|
|
124
|
-
assert.throws(() => build({ root, trustedOs, sandboxConfig: cyclic }), /OPENCODE_ENVIRONMENT_INVALID/)
|
|
125
|
-
let invoked = false
|
|
126
|
-
const accessor = {
|
|
127
|
-
provider: {
|
|
128
|
-
get smoke() {
|
|
129
|
-
invoked = true
|
|
130
|
-
throw new Error("synthetic-private")
|
|
131
|
-
},
|
|
132
|
-
},
|
|
133
|
-
}
|
|
134
|
-
assert.throws(() => build({ root, trustedOs, sandboxConfig: accessor }), /OPENCODE_ENVIRONMENT_INVALID/)
|
|
135
|
-
assert.equal(invoked, false, "sandbox config accessors are never evaluated")
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
test("sandbox launch pins the host-control switches that keep runs self-contained", () => {
|
|
139
|
-
const result = build({ root, trustedOs })
|
|
140
|
-
const switches = [
|
|
141
|
-
"OPENCODE_DISABLE_MODELS_FETCH",
|
|
142
|
-
"OPENCODE_DISABLE_AUTOUPDATE",
|
|
143
|
-
"OPENCODE_DISABLE_FILEWATCHER",
|
|
144
|
-
"OPENCODE_DISABLE_PROJECT_CONFIG",
|
|
145
|
-
] as const
|
|
146
|
-
for (const key of switches) {
|
|
147
|
-
assert.equal(result.env[key], "1", `${key} must be set`)
|
|
148
|
-
assert.equal(result.allowedEnv.includes(key), true, `${key} must be allowlisted`)
|
|
149
|
-
}
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
test("authenticated launch requires an explicit run-owned password and rooted file database", () => {
|
|
153
|
-
const result = build({ root, trustedOs })
|
|
154
|
-
assert.equal(result.env.OPENCODE_PASSWORD === password, true)
|
|
155
|
-
assert.equal(result.allowedEnv.includes("OPENCODE_PASSWORD"), true)
|
|
156
|
-
assert.equal(result.env.OPENCODE_DB === join(root, "data", "opencode.db"), true)
|
|
157
|
-
for (const value of [undefined, "", "short", "a".repeat(63), `${password}\n`]) {
|
|
158
|
-
assert.throws(() => buildOpenCodeEnvironment({ root, trustedOs, password: value }),
|
|
159
|
-
(error: unknown) => error instanceof Error && error.message === "OPENCODE_ENVIRONMENT_INVALID")
|
|
160
|
-
}
|
|
161
|
-
})
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict"
|
|
2
|
-
import { test } from "node:test"
|
|
3
|
-
import { assessCases, type CaseEvidence, type RequiredCase } from "../support/qualification.ts"
|
|
4
|
-
|
|
5
|
-
const required: RequiredCase[] = [
|
|
6
|
-
{ id: "P0-BOOT", expectation: "behavior", platform: "win32", profile: "active-only" },
|
|
7
|
-
{ id: "P0-ABORT", expectation: "denied", platform: "win32", profile: "active-only" },
|
|
8
|
-
]
|
|
9
|
-
|
|
10
|
-
const evidence = (item: RequiredCase): CaseEvidence => ({
|
|
11
|
-
...item,
|
|
12
|
-
runID: "run-001",
|
|
13
|
-
buildID: "build-001",
|
|
14
|
-
status: "passed",
|
|
15
|
-
assertions: 1,
|
|
16
|
-
captureIDs: ["capture-001"],
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
test("qualification: complete matching executed evidence satisfies selected cases only", () => {
|
|
20
|
-
assert.deepEqual(assessCases(required, required.map(evidence), {
|
|
21
|
-
runID: "run-001", buildID: "build-001", profile: "active-only", platform: "win32",
|
|
22
|
-
}), { satisfied: true, failures: [] })
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
test("qualification: empty selection cannot qualify", () => {
|
|
26
|
-
assert.equal(assessCases([], [], {
|
|
27
|
-
runID: "run-001", buildID: "build-001", profile: "active-only", platform: "win32",
|
|
28
|
-
}).satisfied, false)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
for (const status of ["planned", "todo", "skipped", "failed", "unavailable"] as const) {
|
|
32
|
-
test(`qualification: ${status} is not executed passing evidence`, () => {
|
|
33
|
-
const records = required.map(evidence)
|
|
34
|
-
records[0] = { ...records[0]!, status }
|
|
35
|
-
const result = assessCases(required, records, {
|
|
36
|
-
runID: "run-001", buildID: "build-001", profile: "active-only", platform: "win32",
|
|
37
|
-
})
|
|
38
|
-
assert.equal(result.satisfied, false)
|
|
39
|
-
assert.ok(result.failures.some((failure) => failure.code === "CASE_NOT_PASSED"))
|
|
40
|
-
})
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
test("qualification: missing and duplicate evidence cannot satisfy a required case", () => {
|
|
44
|
-
for (const records of [[], [evidence(required[0]!), evidence(required[0]!), evidence(required[1]!)]]) {
|
|
45
|
-
assert.equal(assessCases(required, records, {
|
|
46
|
-
runID: "run-001", buildID: "build-001", profile: "active-only", platform: "win32",
|
|
47
|
-
}).satisfied, false)
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
test("qualification: stale build/run, wrong profile/platform and wrong expectation are rejected", () => {
|
|
52
|
-
for (const change of [
|
|
53
|
-
{ runID: "old-run" }, { buildID: "old-build" }, { profile: "enforced" as const },
|
|
54
|
-
{ platform: "linux" }, { expectation: "expected-gap" as const },
|
|
55
|
-
{ assertions: 0 }, { captureIDs: [] },
|
|
56
|
-
]) {
|
|
57
|
-
const records = required.map(evidence)
|
|
58
|
-
records[0] = { ...records[0]!, ...change }
|
|
59
|
-
assert.equal(assessCases(required, records, {
|
|
60
|
-
runID: "run-001", buildID: "build-001", profile: "active-only", platform: "win32",
|
|
61
|
-
}).satisfied, false)
|
|
62
|
-
}
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
test("qualification: expected-gap never satisfies an enforced profile", () => {
|
|
66
|
-
const item: RequiredCase = {
|
|
67
|
-
id: "P6-GATE", profile: "enforced", platform: "win32", expectation: "expected-gap",
|
|
68
|
-
}
|
|
69
|
-
assert.equal(assessCases([item], [evidence(item)], {
|
|
70
|
-
runID: "run-001", buildID: "build-001", profile: "enforced", platform: "win32",
|
|
71
|
-
}).satisfied, false)
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
test("qualification: an observed gap is valid only as explicitly selected active-only gap evidence", () => {
|
|
75
|
-
const item: RequiredCase = {
|
|
76
|
-
id: "P5-GAP", profile: "active-only", platform: "win32", expectation: "expected-gap",
|
|
77
|
-
}
|
|
78
|
-
assert.equal(assessCases([item], [{ ...evidence(item), status: "observed-gap" }], {
|
|
79
|
-
runID: "run-001", buildID: "build-001", profile: "active-only", platform: "win32",
|
|
80
|
-
}).satisfied, true)
|
|
81
|
-
assert.equal(assessCases([item], [evidence(item)], {
|
|
82
|
-
runID: "run-001", buildID: "build-001", profile: "active-only", platform: "win32",
|
|
83
|
-
}).satisfied, false)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
test("qualification: invalid IDs are not echoed into safe failure output", () => {
|
|
87
|
-
const bad = { ...required[0]!, id: "arbitrary-sensitive-input" }
|
|
88
|
-
const result = assessCases([bad], [evidence(bad)], {
|
|
89
|
-
runID: "run-001", buildID: "build-001", profile: "active-only", platform: "win32",
|
|
90
|
-
})
|
|
91
|
-
assert.equal(result.satisfied, false)
|
|
92
|
-
assert.equal(JSON.stringify(result).includes(bad.id), false)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
test("qualification: invalid requirements and unexpected evidence fail rather than disappear", () => {
|
|
96
|
-
const options = { runID: "run-001", buildID: "build-001", profile: "active-only" as const, platform: "win32" }
|
|
97
|
-
assert.equal(assessCases([required[0]!, required[0]!], required.map(evidence), options).satisfied, false)
|
|
98
|
-
assert.equal(assessCases(required, [...required.map(evidence), evidence({ ...required[0]!, id: "P0-EXTRA" })], options).satisfied, false)
|
|
99
|
-
assert.equal(assessCases(required, required.map(evidence), { ...options, runID: "" }).satisfied, false)
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
test("qualification: malformed external evidence always returns fixed failure codes", () => {
|
|
103
|
-
const options = { runID: "run-001", buildID: "build-001", profile: "active-only", platform: "win32" }
|
|
104
|
-
for (const inputs of [
|
|
105
|
-
[null, [], options], [required, null, options], [required, [], null],
|
|
106
|
-
[[null], [], options], [required, [null], options],
|
|
107
|
-
[required, [{ ...evidence(required[0]!), captureIDs: null }], options],
|
|
108
|
-
[required, [{ ...evidence(required[0]!), captureIDs: "sensitive-malformed-value" }], options],
|
|
109
|
-
[[{ ...required[0]!, id: {} }], [], options],
|
|
110
|
-
[required, [], { ...options, runID: {} }],
|
|
111
|
-
]) {
|
|
112
|
-
// Simulate raw JSON crossing into the assessor; TypeScript is not a validator.
|
|
113
|
-
const result = (assessCases as (...args: unknown[]) => { satisfied: boolean; failures: { code: string }[] })(...inputs)
|
|
114
|
-
assert.equal(result.satisfied, false)
|
|
115
|
-
assert.ok(result.failures.length > 0)
|
|
116
|
-
assert.equal(JSON.stringify(result).includes("sensitive-malformed-value"), false)
|
|
117
|
-
}
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
test("qualification: accessor properties are rejected without invoking them", () => {
|
|
121
|
-
let invoked = false
|
|
122
|
-
const item = { ...required[0]!, get id() { invoked = true; throw new Error("private-value") } }
|
|
123
|
-
const result = assessCases([item], [], {
|
|
124
|
-
runID: "run-001", buildID: "build-001", profile: "active-only", platform: "win32",
|
|
125
|
-
})
|
|
126
|
-
assert.equal(result.satisfied, false)
|
|
127
|
-
assert.equal(invoked, false)
|
|
128
|
-
})
|