mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-10-21 13:54:39 +00:00
## Summary Qualify **OpenCode 2.0.22**, align server/UI client and plugin pins, and recommend this release. Keep the demonstrated **2.0.7** global minimum and **2.0.20** Codex Usage feature-local requirement. Block the new native `parentID` create variant in the workspace proxy: upstream ignores the supplied location and inherits the parent's directory. Require inspectable JSON objects to close opaque-body bypasses. Root creation and the existing authorized fork route remain available. Started at `dev@0983db3c` (#828 Windows test harness), then integrated `dev@97c361c3` (#789 Code Mode presentation). Conversation remains attached to `D:\CodeNomad`; all work and tests use explicit separate paths and synthetic data. ## Upstream audit: 2.0.21 → 2.0.22 - Authenticated native OpenAPI retains **116 paths**, with **11 schema differences**: session creation gains optional `parentID`, parent-not-found response/description; provider settings gain optional `headerTimeout` and `chunkTimeout=false`; configured model capability overrides become partial. No new proxy route or consumed mandatory API. - Published declarations: **4/30 client, 2/60 plugin, 3/36 protocol, 4/102 schema** files change. Plugin session domains expose `remove` and `compact`; CodeNomad does not add calls to them. Identifier namespace export is additive. - An isolated native fixture proves parent creation inherits the parent location despite a different supplied directory. Mock and production-proxy fixtures reject owned/foreign parents; malformed values and opaque content types are covered before any native request. Owned root creation stays available. - Runtime changes include HTTP header/chunk/whole-response timeout handling and bounded timeout retries, provider cache/error/tool-history fixes, model capability defaults, and legacy MCP shutdown cleanup. - Lock changes are limited to six `@opencode` packages and two workspace entries. Integrity/dependency metadata matches independently installed published packages; clean `npm ci --ignore-scripts` passes. Optional OpenTUI peers are not installed by CodeNomad. ## Isolated local qualification **Windows / Node 24.20.0**, isolated profiles, databases, services and synthetic providers/credentials. No shared-daemon or personal-history mutations. - Previous **2.0.21 dependencies + runtime 2.0.22**: native pruning/history/proxy acceptance passes. - New **2.0.22 dependencies + runtimes 2.0.7 and 2.0.22**: seven suites pass (automation, pruning/history/navigation, per-send environment, inclusive forks, idle/busy side questions, blank-session cleanup, Git-degraded recovery), plus explicit native proxy suites. - Native/UI pruning acceptance passes through the committed parent runner, with completion and actual child exit verified. This uses #828's early dependency-import workaround, not the old late-import order. No claim that underlying Windows heap corruption is repaired. - Current compaction-isolation acceptance passes: session B progresses while session A's provider response is held; bounded outline reads retain daemon heartbeats. Fixture runtime selection is extended to include 2.0.22; its assertions are unchanged. This supplemental fixture was not qualified on 2.0.7 (explicit runtime selector rejects it). - Native Codex Usage passes on 2.0.22 with synthetic OAuth/key credentials and mocked quota HTTP; not a live-account/WSL test. - Native migrations **2.0.3 → 2.0.22** and **beta-19271 → 2.0.22** preserve full history/forks/inbox/provider selection. - Packed standalone pruning plugin installed outside the repository passes native acceptance. - Native npm upgrade **2.0.15 → 2.0.22** passes; authenticated daemon remains **2.0.15 / PID 43876** before and after. - Full server run: **794 passed / 6 skipped / 0 failed**. Final guard revision separately passes **68 focused server tests** and native proxy suites on minimum/current. Aggregate runs use `--test-force-exit`. - Targeted client/store/reducer/usage UI: **63/63** with `--conditions=browser`, `--test-force-exit`, bounded timeout. Initial broad UI run used the wrong Node conditions and hung at `prioritized-read.test.ts`; only its owned processes were stopped. That incomplete run is **not green**. The same prioritized-read tests pass under the browser condition used by CI. No production fix is inferred from this launch correction. - **41 browser scenarios** pass: setup/install, auth recovery, tool images, rendered Codex Usage and compaction responsiveness. Browser tests do not use forced exit. - Server/UI/Electron typechecks and production server/UI/plugin builds pass. Revalidation after #789 integration and independent gatekeeper are reported separately below. Local results do not certify all real providers, Linux/macOS/WSL, or installed Electron/Tauri hosts. Historical Windows import and Git cleanup failures remain limitations, not repaired by this upgrade. Remote CI is not continuously monitored and no global CI-green claim is made. Size note: existing `packages/server/src/server/http-server.ts` is ~2,359 lines; `packages/server/src/server/__tests__/instance-proxy.test.ts` is ~1,139 lines. Changes are narrowly scoped; no unrelated size-driven refactor. ## Independent gatekeeper Review launched on exact head `0a7615476034eabbff54967aacc8a93ab0e6ab12` against `dev@97c361c33f6fa7de30be5dde31c0e26e12bab574`; verdict will be published before any admin merge. Findings will be corrected and reviewed again until zero actionable findings.
113 lines
6.8 KiB
JavaScript
113 lines
6.8 KiB
JavaScript
import assert from "node:assert/strict"
|
|
import path from "node:path"
|
|
import Fastify from "fastify"
|
|
import replyFrom from "@fastify/reply-from"
|
|
import pino from "pino"
|
|
import { OpenCode } from "@opencode/client"
|
|
import { tsImport } from "tsx/esm/api"
|
|
|
|
// Called only by the isolated native fixture; no discovery or user storage.
|
|
export async function testNativeProxy({ client, baseUrl, root, authorization, runtimeFetch, connection, exercise }) {
|
|
const { registerInstanceProxyRoutes } = await tsImport("../packages/server/src/server/http-server.ts", import.meta.url)
|
|
const { PROMPT_INLINE_FILE_LIMITS } = await tsImport("../packages/server/src/api-types.ts", import.meta.url)
|
|
const { sessionEnvironment } = await tsImport("../packages/server/src/workspaces/session-environment.ts", import.meta.url)
|
|
const { createInstanceFetch } = await tsImport("../packages/ui/src/lib/sdk-manager.ts", import.meta.url)
|
|
const app = Fastify()
|
|
await app.register(replyFrom)
|
|
const owns = candidate => path.resolve(candidate) === path.resolve(root)
|
|
registerInstanceProxyRoutes(app, {
|
|
workspaceManager: {
|
|
get: () => ({ id: "native", path: root }),
|
|
getSharedServiceEndpoint: async () => ({ url: baseUrl }),
|
|
...(connection ? { getSharedServiceConnection: async () => connection } : {}),
|
|
getInstanceAuthorizationHeader: () => authorization,
|
|
getServiceDirectory: () => root,
|
|
getSharedServiceClient: async () => client,
|
|
getSessionEnvironment: () => sessionEnvironment({}),
|
|
...(runtimeFetch ? { getSharedServiceFetch: async () => runtimeFetch } : {}),
|
|
getWorktreeIdentityForPath: async (_id, directory) => owns(directory) ? root : undefined,
|
|
ownsDirectory: async (_id, directory) => owns(directory),
|
|
ownsLocation: async (_id, location) => owns(location.directory) && location.workspaceID === undefined,
|
|
ownsPath: async (_id, candidate) => owns(candidate),
|
|
},
|
|
worktreeDeletionFence: { enter: () => () => {} },
|
|
logger: pino({ level: "silent" }),
|
|
})
|
|
await app.listen({ host: "127.0.0.1", port: 0 })
|
|
const proxyBase = `http://127.0.0.1:${app.server.address().port}/workspaces/native/instance/`
|
|
const forward = createInstanceFetch(proxyBase)
|
|
const proxy = OpenCode.make({ baseUrl: proxyBase, fetch: async (input, init) => {
|
|
const response = await forward(input, init)
|
|
if (!response.ok) assert.fail(`${init?.method} ${input}: ${response.status} ${await response.text()}`)
|
|
return response
|
|
} })
|
|
const created = []
|
|
try {
|
|
const location = { directory: root }
|
|
const info = await proxy.location.get({ location })
|
|
assert.ok(info.project.id)
|
|
assert.ok(Array.isArray(await proxy.project.list()))
|
|
for (const resource of [proxy.agent, proxy.provider, proxy.model, proxy.command, proxy.plugin, proxy.mcp]) {
|
|
assert.ok(Array.isArray((await resource.list({ location })).data))
|
|
}
|
|
assert.ok(Array.isArray((await proxy.permission.request.list({ location })).data))
|
|
for (let index = 0; index < 3; index++) created.push(await proxy.session.create({ location }))
|
|
const sessionID = created[0].id
|
|
for (const parentID of [sessionID, "ses_foreign_parent"]) {
|
|
const rejected = await app.inject({ method: "POST", url: "/workspaces/native/instance/api/session",
|
|
payload: { parentID, location } })
|
|
assert.equal(rejected.statusCode, 403, "Native parent creation must not bypass workspace authority")
|
|
}
|
|
console.log("PASS: native parent-create variant stays closed through the production proxy; ordinary root creation remains available")
|
|
await proxy.session.update({ sessionID, title: "Stable proxy fixture" })
|
|
assert.equal((await proxy.session.get({ sessionID })).title, "Stable proxy fixture")
|
|
const page = await proxy.session.list({ directory: root, limit: 1 })
|
|
assert.equal(page.data.length, 1)
|
|
assert.ok(page.cursor.next)
|
|
const continuation = await proxy.session.list({ cursor: page.cursor.next, limit: 1 })
|
|
assert.equal(continuation.data.length, 1)
|
|
assert.notEqual(continuation.data[0].id, page.data[0].id)
|
|
assert.equal(typeof await proxy.session.active(), "object")
|
|
await proxy.session.instructions.entry.put({ sessionID, key: "fixture", value: "Fixture instruction" })
|
|
await proxy.session.instructions.entry.remove({ sessionID, key: "fixture" })
|
|
await proxy.session.wait({ sessionID })
|
|
const header = { headers: { "x-opencode-directory": encodeURIComponent(root) } }
|
|
for (const owner of [sessionID, "global"]) {
|
|
for (const action of ["reply", "cancel"]) {
|
|
const form = await client.session.form.create({
|
|
sessionID: owner, title: "Fixture form", fields: [{ key: "answer", type: "string" }],
|
|
}, owner === "global" ? header : undefined)
|
|
const listed = await proxy.form.list({ location })
|
|
assert.ok(listed.data.some(item => item.id === form.id))
|
|
const input = { sessionID: owner, formID: form.id }
|
|
if (action === "reply") await proxy.session.form.reply({ ...input, answer: { answer: "ok" } }, header)
|
|
else await proxy.session.form.cancel(input, header)
|
|
assert.ok(!(await proxy.form.list({ location })).data.some(item => item.id === form.id))
|
|
}
|
|
}
|
|
await exercise?.(proxy, sessionID)
|
|
const inlineSessionID = created[1].id
|
|
const inlineFile = Buffer.alloc(PROMPT_INLINE_FILE_LIMITS.maxFileBytes, 0x61).toString("base64")
|
|
await proxy.session.prompt({
|
|
sessionID: inlineSessionID,
|
|
text: "Acknowledge the attached transport fixture.",
|
|
files: [{ name: "inline-limit.txt", uri: `data:text/plain;base64,${inlineFile}` }],
|
|
})
|
|
await proxy.session.wait({ sessionID: inlineSessionID }, { signal: AbortSignal.timeout(60_000) })
|
|
assert((await proxy.message.list({ sessionID: inlineSessionID, limit: 20 })).data.some(message => message.type === "user"))
|
|
console.log(`PASS: ${PROMPT_INLINE_FILE_LIMITS.maxFileBytes}-byte inline file through real proxy and OpenCode endpoint`)
|
|
// Export remains a direct native read; it is deliberately not added to the
|
|
// guarded UI allowlist merely for this fixture.
|
|
const exported = await client.session.export({ sessionID })
|
|
await proxy.session.remove({ sessionID })
|
|
const restored = await proxy.session.import({ ...exported, location })
|
|
assert.equal(restored.id, sessionID)
|
|
assert.equal((await proxy.message.list({ sessionID, limit: 100 })).data.length, exported.messages.length)
|
|
console.log("PASS: generated stable client through real proxy: catalogs, sessions, native cursors, active envelope, instructions, wait and session/global Forms")
|
|
} finally {
|
|
// Import can fail after its source has already been removed. Cleanup must
|
|
// neither replace that failure with a 404 nor leave the HTTP listener open.
|
|
try { await Promise.allSettled(created.map(session => client.session.remove({ sessionID: session.id }))) }
|
|
finally { await app.close() }
|
|
}
|
|
}
|