diff --git a/packages/opencode/scripts/build-tui.test.ts b/packages/opencode/scripts/build-tui.test.ts index 22d7500c..8bf036cd 100644 --- a/packages/opencode/scripts/build-tui.test.ts +++ b/packages/opencode/scripts/build-tui.test.ts @@ -713,7 +713,7 @@ test('graph.ga_roots', async () => { 'src/tui/quota-layout.tsx', 'src/tui/route-layout.ts', ]) - checkGaGraph(source, 'source') + checkGaGraph(source, 'source', product) const map = JSON.parse(await readFile(join(product, GA_TUI_ROOT.map), 'utf8')) expect(map.compiler).toBe('@cortexkit/common-auth/tui-build@0.24.0') for (const [variant, tree, entry, kind] of [ @@ -721,7 +721,7 @@ test('graph.ga_roots', async () => { [map.runtime, GA_TUI_ROOT.runtime, GA_TUI_ROOT.runtimeEntry, 'runtime'], ] as const) { const graph = await auditGraph(join(product, tree, entry)) - checkGaGraph(graph, kind) + checkGaGraph(graph, kind, product) expect(graph.files.length).toBe(source.files.length) expect( variant.files @@ -742,25 +742,92 @@ test('graph.ga_roots', async () => { }) test('graph.ga_policy_refusals', async () => { - const ok = { files: ['/x/src/ga/tui/host-ga.tsx'], externals: ['solid-js'] } - checkGaGraph(ok, 'source') + const pkgRoot = '/x' + const antigravityPkgRoot = '/work/antigravity/packages/opencode' + checkGaGraph( + { + files: ['/x/src/ga/tui/host-ga.tsx'], + externals: ['solid-js'], + }, + 'source', + pkgRoot, + ) + checkGaGraph( + { + files: [ + '/work/antigravity/packages/opencode/src/ga/tui/host-ga.tsx', + '/work/antigravity/packages/opencode/src/common-auth-embedded/quota/codec.js', + ], + externals: ['solid-js'], + }, + 'source', + antigravityPkgRoot, + ) const refusals: Array< - [Parameters[0], 'source' | 'runtime', string] + [ + Parameters[0], + 'source' | 'runtime', + string, + string | undefined, + ] > = [ [ { files: ['/x/src/rpc/port-file.ts'], externals: [] }, 'source', 'forbidden GA source', + pkgRoot, + ], + [ + { + files: ['/work/antigravity/packages/opencode/src/rpc/port-file.ts'], + externals: [], + }, + 'source', + 'forbidden GA source', + antigravityPkgRoot, + ], + [ + { + files: [ + '/work/antigravity/packages/opencode/src/plugin/src/storage.ts', + ], + externals: [], + }, + 'source', + 'forbidden GA source', + antigravityPkgRoot, + ], + [ + { + files: [ + '/work/antigravity/packages/opencode/src/antigravity/src/client.ts', + ], + externals: [], + }, + 'source', + 'forbidden GA source', + antigravityPkgRoot, + ], + [ + { + files: ['/work/other/src/ga/tui/host-ga.tsx'], + externals: [], + }, + 'source', + 'forbidden GA source', + antigravityPkgRoot, ], [ { files: ['/x/src/tui/host-v1.tsx'], externals: [] }, 'source', 'forbidden GA source', + pkgRoot, ], [ { files: ['/x/src/sidebar-state.ts'], externals: [] }, 'source', 'forbidden GA source', + pkgRoot, ], [ { @@ -769,16 +836,19 @@ test('graph.ga_policy_refusals', async () => { }, 'source', 'forbidden GA source', + pkgRoot, ], [ { files: ['/x/src/plugin/storage.ts'], externals: [] }, 'source', 'forbidden GA source', + pkgRoot, ], [ { files: [], externals: ['@opencode-ai/plugin/tui'] }, 'source', 'forbidden GA external', + pkgRoot, ], [ { @@ -787,21 +857,29 @@ test('graph.ga_policy_refusals', async () => { }, 'source', 'forbidden GA external', + pkgRoot, ], [ { files: [], externals: ['@opentui/solid-ga'] }, 'source', 'forbidden GA external', + pkgRoot, + ], + [ + { files: [], externals: ['solid-js'] }, + 'runtime', + 'not a host module', + pkgRoot, ], - [{ files: [], externals: ['solid-js'] }, 'runtime', 'not a host module'], [ { files: [], externals: ['opentui:runtime-module:solid-js-ga'] }, 'runtime', 'not a host module', + pkgRoot, ], ] - for (const [graph, kind, message] of refusals) - expect(() => checkGaGraph(graph, kind)).toThrow(message) + for (const [graph, kind, message, root] of refusals) + expect(() => checkGaGraph(graph, kind, root)).toThrow(message) }) test('build.roots_failure_removes_every_root', async () => { diff --git a/packages/opencode/scripts/build-tui.ts b/packages/opencode/scripts/build-tui.ts index 7be0503f..48ff2dd1 100644 --- a/packages/opencode/scripts/build-tui.ts +++ b/packages/opencode/scripts/build-tui.ts @@ -159,10 +159,22 @@ const RUNTIME_MODULE = 'opentui:runtime-module:' export function checkGaGraph( graph: Graph, emitted: 'source' | 'raw' | 'runtime', + packageRoot: string = PACKAGE_ROOT, ) { - for (const path of graph.files) - if (GA_FORBIDDEN_SOURCE.test(portable(path))) + for (const path of graph.files) { + const rel = relative(packageRoot, path) + if ( + rel === '' || + rel === '..' || + rel.startsWith(`..${sep}`) || + rel.startsWith('../') || + isAbsolute(rel) + ) + throw new Error(`graph.ga: forbidden GA source ${path}`) + const candidate = `/${portable(rel)}` + if (GA_FORBIDDEN_SOURCE.test(candidate)) throw new Error(`graph.ga: forbidden GA source ${path}`) + } for (const edge of graph.externals) { if (emitted === 'runtime') { // Emitted runtime code must name host runtime modules, never a package @@ -218,14 +230,24 @@ function checkExternal(specifier: string, tui: boolean, types: boolean) { */ export async function auditGraph( entry: string, - { tui = true, types = false } = {}, + { + tui = true, + types = false, + packageRoot = PACKAGE_ROOT, + }: { tui?: boolean; types?: boolean; packageRoot?: string } = {}, ): Promise { + const rootDir = existsSync(packageRoot) + ? realpathSync(packageRoot) + : resolve(packageRoot) const files = new Set() const externals = new Set() async function visit(path: string): Promise { path = realpathSync(path) if (files.has(path)) return - const embedded = portable(path).split('/common-auth-embedded/')[1] + const candidate = isInsideDirectory(rootDir, path) + ? `/${portable(relative(rootDir, path))}` + : portable(path) + const embedded = candidate.split('/common-auth-embedded/')[1] if ( embedded && !types && @@ -240,7 +262,7 @@ export async function auditGraph( tui && !types && /(?:\/rpc\/(?:index|rpc-server|server-registry)\.[jt]s$|\/plugin\/(?:storage|accounts|account-manager|account-storage|quota|rotation|logger|debug)\.|\/common-auth-embedded\/(?:fs\/(?!private-files\.js$)|store\/)|\/tools\/)/.test( - portable(path), + candidate, ) ) throw new Error(`graph: forbidden TUI source ${path}`) @@ -540,7 +562,7 @@ export async function verifySelectors( ) const graph = await buildPhase( `selectors.auditGraph ${tree}/${entry}`, - () => auditGraph(join(packageRoot, tree, entry)), + () => auditGraph(join(packageRoot, tree, entry), { packageRoot }), ) if (graph.files.some((path) => path.endsWith('/selector.js'))) throw new Error('build.selector_inert: imported selector copy') @@ -594,14 +616,14 @@ export async function buildTui({ verifyPrerequisites(), ) const source = await buildPhase(`buildTui.auditGraph source ${entry}`, () => - auditGraph(join(packageRoot, entry)), + auditGraph(join(packageRoot, entry), { packageRoot }), ) const types = await buildPhase(`buildTui.auditGraph types ${entry}`, () => - auditGraph(join(packageRoot, entry), { types: true }), + auditGraph(join(packageRoot, entry), { types: true, packageRoot }), ) if (ga) { - checkGaGraph(source, 'source') - checkGaGraph(types, 'source') + checkGaGraph(source, 'source', packageRoot) + checkGaGraph(types, 'source', packageRoot) } await buildPhase('buildTui.mkdir dist', () => mkdir(join(packageRoot, 'dist'), { recursive: true }), @@ -654,9 +676,14 @@ export async function buildTui({ if (!file.output.endsWith('/selector.js')) { const emitted = await buildPhase( `buildTui.auditGraph emitted ${file.output}`, - () => auditGraph(join(packageRoot, file.output)), + () => auditGraph(join(packageRoot, file.output), { packageRoot }), ) - if (ga) checkGaGraph(emitted, variant === map.raw ? 'raw' : 'runtime') + if (ga) + checkGaGraph( + emitted, + variant === map.raw ? 'raw' : 'runtime', + packageRoot, + ) } } for (const edge of variant.externals) checkExternal(edge, true, false)