mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-10-23 14:51:18 +00:00
## Summary - Persist all permission decisions: allowed once, always allowed and rejected, with request context and known reasons. Distinguish CodeNomad, automatic Yolo and native settlements. - Keep receipts visible alongside transcript messages even with hidden tools; provide paginated source-less session receipts. Ownership-checked bounded reads, atomic profile storage, deletion cleanup and stale-read fencing. - Enrich completed native question receipts with selected-option descriptions, verbatim custom answers and a disclosure for other choices. Translate labels in all ten locales. ## Validation - Locked dependencies installed; server and UI TypeScript checks pass; production UI build passes. - 126 targeted server regressions during implementation, then 26 receipt/replier/service checks against the pinned client passed. - 20 receipt/dock browser tests and 21 history navigation tests pass; 5 question decoding/copy tests pass. - Rendered captures inspected at 393/1100px in light/dark, including long text and pagination. ## Persistence boundaries Native permission events are ephemeral: past lost decisions cannot be reconstructed. Receipts are CodeNomad annotations outside native search/copy/export. Storage namespaces follow execution host, native root and authenticated channel; credential rotation starts a new namespace. Bounded text excludes metadata/diffs. ## Review Autonomous independent gatekeeper review will be published and repeated until zero findings before the user-requested admin merge. ## Maintenance Existing oversized files touched: server/http-server.ts (~2415), server/api-types.ts (~717), server/index.ts (~707), ui/message-section.tsx (~1566), ui/lib/api-client.ts (~650).
243 lines
16 KiB
JavaScript
243 lines
16 KiB
JavaScript
// Native location authority fixture. Invoke with an absolute CLI path, or call
|
|
// testNativeLocationIdentity from an already-isolated native test. No discovery,
|
|
// shared daemon, user config or non-fixture database is ever used.
|
|
import assert from "node:assert/strict"
|
|
import { spawn, execFileSync } from "node:child_process"
|
|
import { randomUUID } from "node:crypto"
|
|
import { mkdtemp, mkdir, realpath, writeFile } from "node:fs/promises"
|
|
import os from "node:os"
|
|
import path from "node:path"
|
|
import { fileURLToPath } from "node:url"
|
|
import { setTimeout as delay } from "node:timers/promises"
|
|
import { OpenCode } from "@opencode/client"
|
|
import { tsImport } from "tsx/esm/api"
|
|
import Fastify from "fastify"
|
|
import replyFrom from "@fastify/reply-from"
|
|
import pino from "pino"
|
|
import { testNativeWorktreeFamily } from "./test-worktree-families-native.mjs"
|
|
import { testNativeWorktreeManagement } from "./test-native-worktree-management.mjs"
|
|
import { testNativeEventRelay } from "./test-native-event-relay.mjs"
|
|
|
|
export async function testNativeLocationIdentity({ client, connection, root, service }) {
|
|
const { contractProfile, runtimeIdentity } = await tsImport("../packages/server/src/opencode/compatibility/runtime.ts", import.meta.url)
|
|
const { locationRequestOptions } = await tsImport("../packages/server/src/opencode/compatibility/location.ts", import.meta.url)
|
|
const profile = connection.profile ? await connection.profile() : contractProfile(runtimeIdentity(connection.endpoint))
|
|
const directory = path.join(root, `identity-project-${randomUUID()}`)
|
|
const worktree = path.join(root, `identity-worktree-${randomUUID()}`)
|
|
await mkdir(directory)
|
|
const git = (...args) => execFileSync("git", args, { cwd: directory, stdio: "pipe" })
|
|
git("init")
|
|
git("config", "user.name", "CodeNomad isolated fixture")
|
|
git("config", "user.email", "fixture@example.invalid")
|
|
await writeFile(path.join(directory, "fixture.txt"), "synthetic native identity fixture\n")
|
|
git("add", "fixture.txt")
|
|
git("commit", "-m", "Synthetic native identity fixture")
|
|
git("worktree", "add", "-b", "fixture-worktree", worktree)
|
|
const rootLocation = await client.location.get({ location: { directory } })
|
|
const worktreeLocation = await client.location.get({ location: { directory: worktree } })
|
|
assert.equal(rootLocation.project.id, worktreeLocation.project.id)
|
|
await testNativeEventRelay({ client, location: rootLocation, service })
|
|
await testNativeWorktreeFamily({ client, profile, rootLocation, worktreeLocation })
|
|
await testNativeWorktreeManagement({ client, root })
|
|
if (profile !== "legacy") {
|
|
await assert.rejects(client.location.get({ location: { directory } }, locationRequestOptions({ directory, workspaceID: "wrk_fixture_one" })))
|
|
console.log("PASS: native modern worktree location and obsolete-selector rejection")
|
|
return
|
|
}
|
|
const locations = ["wrk_fixture_one", "wrk_fixture_two"].map(workspaceID => ({ directory: rootLocation.directory, workspaceID }))
|
|
for (const location of locations) {
|
|
const resolved = await client.location.get({ location: { directory: location.directory } }, locationRequestOptions(location))
|
|
assert.equal(resolved.directory, location.directory)
|
|
assert.equal(resolved.workspaceID, location.workspaceID)
|
|
}
|
|
const { registerInstanceProxyRoutes } = await tsImport("../packages/server/src/server/http-server.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 { EventBus } = await tsImport("../packages/server/src/events/bus.ts", import.meta.url)
|
|
const { InstanceEventBridge } = await tsImport("../packages/server/src/workspaces/instance-events.ts", import.meta.url)
|
|
const { evacuateWorktreeSessions } = await tsImport("../packages/server/src/workspaces/worktree-session-evacuation.ts", import.meta.url)
|
|
const logger = pino({ level: "silent" })
|
|
const normal = value => process.platform === "win32" ? path.resolve(value).toLowerCase() : path.resolve(value)
|
|
const ownsDirectory = value => [directory, worktree].some(candidate => normal(candidate) === normal(value))
|
|
const records = locations.map((location, index) => ({ id: String(index), path: directory, location }))
|
|
// A deliberately narrow fixture policy: each logical owner may access only its
|
|
// native workspace identity, including its synthetic linked worktree. Native
|
|
// location resolution, sessions, Form state, cursors and SSE are never mocked.
|
|
const manager = {
|
|
list: () => records,
|
|
get: id => records.find(record => record.id === id),
|
|
getSharedServiceConnection: async () => connection,
|
|
getSharedServiceEndpoint: async () => connection.endpoint,
|
|
getSharedServiceClient: async () => client,
|
|
getSessionEnvironment: () => sessionEnvironment({}),
|
|
getInstanceAuthorizationHeader: () => `Basic ${Buffer.from(`${connection.endpoint.auth.username}:${connection.endpoint.auth.password}`).toString("base64")}`,
|
|
getServiceDirectory: () => rootLocation.directory,
|
|
getServiceDirectoryForPath: async (_id, candidate) => ownsDirectory(candidate) ? candidate : undefined,
|
|
getWorktreeIdentityForPath: async (_id, candidate) => ownsDirectory(candidate) ? normal(candidate) : undefined,
|
|
ownsDirectory: async (_id, candidate) => ownsDirectory(candidate),
|
|
ownsPath: async (_id, candidate) => ownsDirectory(candidate),
|
|
ownsLocation: async (id, location) => {
|
|
if (!ownsDirectory(location.directory)) return false
|
|
if (location.workspaceID === undefined) return true
|
|
if (records.find(record => record.id === id)?.location.workspaceID !== location.workspaceID) return false
|
|
const resolved = await client.location.get({ location: { directory: location.directory } }, locationRequestOptions(location))
|
|
return normal(resolved.directory) === normal(location.directory) && resolved.workspaceID === location.workspaceID
|
|
},
|
|
subscribeToSharedService: signal => client.event.subscribe({ signal }),
|
|
}
|
|
const app = Fastify()
|
|
await app.register(replyFrom)
|
|
registerInstanceProxyRoutes(app, { workspaceManager: manager, logger, worktreeDeletionFence: { enter: () => () => {} } })
|
|
await app.listen({ host: "127.0.0.1", port: 0 })
|
|
const proxyClients = records.map(record => {
|
|
const baseUrl = `http://127.0.0.1:${app.server.address().port}/workspaces/${record.id}/instance/`
|
|
return OpenCode.make({ baseUrl, fetch: createInstanceFetch(baseUrl) })
|
|
})
|
|
const bus = new EventBus()
|
|
const routedForms = []
|
|
let connected = false
|
|
bus.on("instance.eventStatus", event => { if (event.status === "connected") connected = true })
|
|
bus.on("instance.event", event => { if (event.event.type === "form.created") routedForms.push(event) })
|
|
const bridge = new InstanceEventBridge({ workspaceManager: manager, eventBus: bus, logger })
|
|
const until = async predicate => {
|
|
const deadline = Date.now() + 15_000
|
|
while (!await predicate()) {
|
|
if (Date.now() > deadline) throw new Error("Native identity fixture timed out")
|
|
await delay(20)
|
|
}
|
|
}
|
|
const created = []
|
|
const forms = []
|
|
const formOptions = location => ({ headers: {
|
|
"x-opencode-directory": encodeURIComponent(location.directory), ...locationRequestOptions(location)?.headers,
|
|
} })
|
|
try {
|
|
bus.publish({ type: "workspace.started", workspace: records[0] })
|
|
await until(() => connected)
|
|
for (const [index, location] of locations.entries()) {
|
|
const proxy = proxyClients[index]
|
|
const options = locationRequestOptions(location)
|
|
for (let n = 0; n < 2; n++) {
|
|
const session = await proxy.session.create({ location: { directory: location.directory } }, options)
|
|
assert.deepEqual(session.location, location)
|
|
created.push(session)
|
|
}
|
|
const page = await proxy.session.list({ directory: location.directory, limit: 1 }, options)
|
|
assert.equal(page.data.length, 1)
|
|
assert.equal(page.data[0].location.workspaceID, location.workspaceID)
|
|
assert.ok(page.cursor.next)
|
|
const continuation = await proxy.session.list({ cursor: page.cursor.next })
|
|
assert.ok(continuation.data.every(session => session.location.workspaceID === location.workspaceID))
|
|
await assert.rejects(proxyClients[1 - index].session.list({ cursor: page.cursor.next }))
|
|
await assert.rejects(proxyClients[1 - index].session.get({ sessionID: page.data[0].id }))
|
|
for (const directory of [location.directory, worktreeLocation.directory]) {
|
|
const scoped = { ...location, directory }
|
|
const session = await proxy.session.create({ location: { directory } }, locationRequestOptions(scoped))
|
|
created.push(session)
|
|
const form = await client.session.form.create({ sessionID: "global", title: "Synthetic identity Form", fields: [{ key: "answer", type: "string" }] }, formOptions(scoped))
|
|
forms.push({ ...form, location: scoped, owner: index })
|
|
}
|
|
}
|
|
await until(() => routedForms.length === forms.length)
|
|
for (const form of forms) {
|
|
const routes = routedForms.filter(event => event.event.data.form.id === form.id)
|
|
assert.deepEqual(routes.map(event => event.instanceId), [String(form.owner)])
|
|
assert.deepEqual(routes[0].event.location, form.location)
|
|
const proxy = proxyClients[form.owner]
|
|
const list = await proxy.form.list({ location: { directory: form.location.directory } }, locationRequestOptions(form.location))
|
|
assert.ok(list.data.some(item => item.id === form.id))
|
|
const foreign = { ...form.location, workspaceID: locations[1 - form.owner].workspaceID }
|
|
const otherList = await proxyClients[1 - form.owner].form.list({ location: { directory: foreign.directory } }, locationRequestOptions(foreign))
|
|
assert.ok(!otherList.data.some(item => item.id === form.id))
|
|
await assert.rejects(proxyClients[1 - form.owner].session.form.cancel({ sessionID: "global", formID: form.id }, formOptions(form.location)))
|
|
if (form.owner === 0) await proxy.session.form.reply({ sessionID: "global", formID: form.id, answer: { answer: "fixture" } }, formOptions(form.location))
|
|
else await proxy.session.form.cancel({ sessionID: "global", formID: form.id }, formOptions(form.location))
|
|
assert.ok(!(await proxy.form.list({ location: { directory: form.location.directory } }, locationRequestOptions(form.location))).data.some(item => item.id === form.id))
|
|
}
|
|
for (const form of forms) {
|
|
// These real native lists return their resolved location even when empty.
|
|
// Spawning inside an arbitrary workspace ID requires an actual remote
|
|
// workspace provider; this fixture deliberately does not provision one.
|
|
for (const resource of ["shell", "pty"]) {
|
|
const listed = await proxyClients[form.owner][resource].list({ location: { directory: form.location.directory } }, locationRequestOptions(form.location))
|
|
assert.equal(listed.location.directory, form.location.directory)
|
|
assert.equal(listed.location.workspaceID, form.location.workspaceID, `proxy ${resource} list must retain native identity`)
|
|
}
|
|
}
|
|
await assert.rejects(evacuateWorktreeSessions({
|
|
client, projectDirectory: rootLocation.directory, targetDirectory: worktreeLocation.directory, rootDirectory: rootLocation.directory,
|
|
// Like the production manager's resolver, compare filesystem identities:
|
|
// Windows TEMP can be an 8.3 path while Git reports its long canonical root.
|
|
resolveDirectoryIdentity: directory => realpath(directory).catch(() => undefined),
|
|
resolveExactDirectory: directory => realpath(directory).catch(() => undefined),
|
|
remove: async () => {
|
|
for (const original of created.filter(session => session.location.directory === worktreeLocation.directory)) {
|
|
assert.deepEqual((await client.session.get({ sessionID: original.id })).location, { directory: rootLocation.directory })
|
|
}
|
|
throw new Error("Synthetic Git removal refusal")
|
|
},
|
|
}), /Synthetic Git removal refusal/)
|
|
for (const original of created) assert.deepEqual((await client.session.get({ sessionID: original.id })).location, original.location)
|
|
console.log("PASS: native same-directory identities, worktree/global Forms, Shell/PTY list scope, scoped SSE, foreign session/cursor refusal and exact move rollback")
|
|
} finally {
|
|
bridge.shutdown()
|
|
await Promise.allSettled(forms.map(form => client.session.form.cancel({ sessionID: "global", formID: form.id }, formOptions(form.location))))
|
|
await Promise.allSettled(created.map(session => client.session.remove({ sessionID: session.id })))
|
|
await app.close()
|
|
}
|
|
}
|
|
|
|
export async function runIsolated(cli, run = testNativeLocationIdentity) {
|
|
if (!cli || !path.isAbsolute(cli)) throw new Error("Pass an absolute isolated CLI executable path")
|
|
const temporaryRoot = path.join(os.tmpdir(), "opencode")
|
|
await mkdir(temporaryRoot, { recursive: true })
|
|
const root = await mkdtemp(path.join(temporaryRoot, "codenomad-location-native-"))
|
|
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("OPENCODE_") && !key.startsWith("XDG_")))
|
|
for (const key of ["XDG_DATA_HOME", "XDG_CONFIG_HOME", "XDG_STATE_HOME", "XDG_CACHE_HOME"]) env[key] = path.join(root, key)
|
|
const password = randomUUID()
|
|
Object.assign(env, {
|
|
HOME: root, USERPROFILE: root, OPENCODE_TEST_HOME: root,
|
|
OPENCODE_DB: path.join(root, "fixture.db"), OPENCODE_CONFIG_DIR: path.join(root, "config"),
|
|
OPENCODE_SERVER_PASSWORD: password, OPENCODE_CONFIG_PROJECT_DISABLE: "1",
|
|
OPENCODE_DISABLE_MODELS_FETCH: "1", OPENCODE_DISABLE_FFF: "1", OPENCODE_CONFIG_CONTENT: "{}",
|
|
})
|
|
await mkdir(env.OPENCODE_CONFIG_DIR)
|
|
let output = ""
|
|
const child = spawn(cli, ["serve", "--hostname", "127.0.0.1", "--port", "0", "--log-level", "debug", "--print-logs"], {
|
|
cwd: root, env, windowsHide: true,
|
|
})
|
|
const stopped = new Promise(resolve => child.once("close", resolve))
|
|
child.stdout.on("data", data => { output += data })
|
|
child.stderr.on("data", data => { output += data })
|
|
console.log(`Isolated location fixture: ${root}`)
|
|
try {
|
|
const deadline = Date.now() + 30_000
|
|
while (!/http:\/\/127\.0\.0\.1:\d+/.test(output)) {
|
|
if (child.exitCode !== null || Date.now() > deadline) throw new Error(`Isolated daemon failed to start: ${output}`)
|
|
await delay(20)
|
|
}
|
|
const baseUrl = output.match(/http:\/\/127\.0\.0\.1:\d+/)[0]
|
|
const { OpenCodeCliService } = await tsImport("../packages/server/src/workspaces/opencode-cli-service.ts", import.meta.url)
|
|
const { runtimeIdentity } = await tsImport("../packages/server/src/opencode/compatibility/runtime.ts", import.meta.url)
|
|
// Replace only CLI discovery; exercise production authenticated health probes.
|
|
const lifecycle = new OpenCodeCliService({ label: "Fixture", timeoutMs: 30_000,
|
|
command: args => ({ command: cli, args, options: {} }),
|
|
}, { execFile: async (_file, args) => ({ stdout: args.at(-1) === "password" ? password : baseUrl, stderr: "" }) })
|
|
const endpoint = await lifecycle.discover()
|
|
const identity = runtimeIdentity(endpoint)
|
|
const { createRuntimeTransport } = await tsImport("../packages/server/src/opencode/compatibility/transport.ts", import.meta.url)
|
|
const transport = createRuntimeTransport(endpoint)
|
|
const client = OpenCode.make({ baseUrl, fetch: transport.fetch })
|
|
const connection = { endpoint, client, ...transport, assertCurrent() {}, invalidate() {} }
|
|
console.log(`Testing official runtime ${identity.version}`)
|
|
assert.equal((await client.server.info()).version, identity.version)
|
|
console.log(`PASS: production ${identity.discovery} discovery and canonical client.server.info()`)
|
|
await run({ client, connection, root })
|
|
} finally {
|
|
child.kill()
|
|
await stopped
|
|
await writeFile(path.join(root, "daemon.log"), output)
|
|
}
|
|
}
|
|
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await runIsolated(process.argv[2])
|