Skip to content

Commit e2794a7

Browse files
authored
feat(catalog): browser-safe model-catalogue subpath (#186)
./runtime is a server barrel — importing any VALUE from it in client code ships @tangle-network/agent-runtime (node:util, child_process) into the route bundle, which throws on module load. This broke the legal-agent chat route and the tax-agent home composer. ./catalog re-exports only the pure catalogue pipeline; a source-graph test fails loud if the subpath ever reaches a node builtin.
1 parent e0b4c38 commit e2794a7

4 files changed

Lines changed: 69 additions & 0 deletions

File tree

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@
5858
"import": "./dist/runtime/index.js",
5959
"default": "./dist/runtime/index.js"
6060
},
61+
"./catalog": {
62+
"types": "./dist/catalog/index.d.ts",
63+
"import": "./dist/catalog/index.js",
64+
"default": "./dist/catalog/index.js"
65+
},
6166
"./eval": {
6267
"types": "./dist/eval/index.d.ts",
6368
"import": "./dist/eval/index.js",

src/catalog/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Browser-safe model-catalogue subpath.
3+
*
4+
* `./runtime` is a SERVER barrel: alongside the catalogue it re-exports
5+
* `createAgentRuntime`/`runAppToolLoop`, which import
6+
* `@tangle-network/agent-runtime` (node:util, child_process). Any client
7+
* component that imports a `/runtime` VALUE ships Node-only code into its
8+
* route bundle and the module throws on load — this broke the chat route on
9+
* legal-agent (#256) and the home composer on tax-agent (#372).
10+
*
11+
* This subpath re-exports ONLY the pure catalogue pipeline (no Node imports,
12+
* no agent-runtime) so pickers and client hooks have a safe import target.
13+
* Server code may keep importing from `./runtime`; the two share one source.
14+
*/
15+
export {
16+
buildCatalog,
17+
fetchModelCatalog,
18+
normalizeModelId,
19+
__resetCatalogCache,
20+
type CatalogModel,
21+
type ModelCatalog,
22+
type RouterModel,
23+
} from '../runtime/model-catalog'

tests/catalog-browser-safe.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { readFileSync } from 'node:fs'
2+
import { dirname, resolve } from 'node:path'
3+
import { describe, expect, it } from 'vitest'
4+
5+
/**
6+
* `./catalog` is the browser-safe catalogue subpath: client components import
7+
* `buildCatalog`/`CatalogModel` from it. If anything reachable from it gains a
8+
* Node-only or agent-runtime import, every consumer's client bundle crashes on
9+
* module load (legal-agent #256, tax-agent #372). Walk the source import graph
10+
* and fail loud on the first unsafe edge.
11+
*/
12+
const FORBIDDEN = [/^node:/, /^@tangle-network\/agent-runtime/, /^child_process$/, /^fs$/, /^util$/]
13+
14+
function localImports(file: string): string[] {
15+
const src = readFileSync(file, 'utf8')
16+
return [...src.matchAll(/from\s+['"]([^'"]+)['"]/g)].flatMap((m) => (m[1] ? [m[1]] : []))
17+
}
18+
19+
describe('catalog subpath browser-safety', () => {
20+
it('reaches no node builtins or agent-runtime imports', () => {
21+
const seen = new Set<string>()
22+
const queue = [resolve(__dirname, '../src/catalog/index.ts')]
23+
while (queue.length) {
24+
const file = queue.pop()!
25+
if (seen.has(file)) continue
26+
seen.add(file)
27+
for (const spec of localImports(file)) {
28+
for (const bad of FORBIDDEN) {
29+
expect(spec, `${file} imports forbidden "${spec}"`).not.toMatch(bad)
30+
}
31+
if (spec.startsWith('.')) {
32+
const base = resolve(dirname(file), spec)
33+
queue.push(base.endsWith('.ts') ? base : `${base}.ts`)
34+
}
35+
}
36+
}
37+
// sanity: the walk actually traversed into the shared pipeline
38+
expect([...seen].some((f) => f.endsWith('runtime/model-catalog.ts'))).toBe(true)
39+
})
40+
})

tsup.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export default defineConfig({
66
'tools/index': 'src/tools/index.ts',
77
'tangle/index': 'src/tangle/index.ts',
88
'runtime/index': 'src/runtime/index.ts',
9+
'catalog/index': 'src/catalog/index.ts',
910
'eval/index': 'src/eval/index.ts',
1011
'eval-campaign/index': 'src/eval-campaign/index.ts',
1112
'knowledge/index': 'src/knowledge/index.ts',

0 commit comments

Comments
 (0)