diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec0b4bdd..92d0137f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,7 +77,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: tangle-network/agent-knowledge - ref: 115f940faeea2cfc7c59175a4a129b8f61807d27 # v6.1.8 + ref: f455a68b93c71bb4c9fa5561172e28857faedb2f # v6.1.9 path: .cohort/agent-knowledge persist-credentials: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 517b38d7..debf19a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.108.2 + +- Adopt Knowledge 6.1.9, which loads `proper-lockfile` lazily. + It pulled in `graceful-fs`, which patches Node's `fs` at module scope; workerd exposes those as getter-only accessors, so Cloudflare rejected the whole Worker during startup validation on upload (`Cannot set property close of # which has only a getter [code: 10021]`). + Because this package pins Knowledge at an exact version and re-exports it from the root barrel, every Cloudflare Worker importing `runToolLoop` from `@tangle-network/agent-runtime` was undeployable. +- Fail `verify:package` when any installed `@tangle-network` package statically imports a module that patches a Node builtin. + The defect was one package away and this repo's CI could not see it, because `wrangler deploy --dry-run` bundles without executing module scope. + ## 0.108.1 - Align Runtime with Eval 0.134.2 and Knowledge 6.1.8 so every knowledge and runtime evaluation uses complete multishot judge cost accounting. diff --git a/docs/api/primitive-catalog.md b/docs/api/primitive-catalog.md index c2a85e8f..a9f9737c 100644 --- a/docs/api/primitive-catalog.md +++ b/docs/api/primitive-catalog.md @@ -7,7 +7,7 @@ # Primitive catalog — the never-stale anti-reinvention inventory -> **GENERATED** from `@tangle-network/agent-runtime@0.108.1` and `@tangle-network/agent-eval@0.134.2` by `scripts/gen-primitive-catalog.mjs`. Do NOT hand-edit — run `pnpm run docs:api`. This is the mechanical companion to the JUDGMENT in `canonical-api.md` (§2 decision table + §1.5 AgentProfile law): that doc says WHICH primitive to reach for and what NOT to build; this catalog proves WHAT exists. Per-symbol signatures + `file:line` live in the per-module pages under `docs/api/`. +> **GENERATED** from `@tangle-network/agent-runtime@0.108.2` and `@tangle-network/agent-eval@0.134.2` by `scripts/gen-primitive-catalog.mjs`. Do NOT hand-edit — run `pnpm run docs:api`. This is the mechanical companion to the JUDGMENT in `canonical-api.md` (§2 decision table + §1.5 AgentProfile law): that doc says WHICH primitive to reach for and what NOT to build; this catalog proves WHAT exists. Per-symbol signatures + `file:line` live in the per-module pages under `docs/api/`. ## 1. agent-runtime — own public surface diff --git a/docs/canonical-api.md b/docs/canonical-api.md index 232ea5fa..ab54bc9a 100644 --- a/docs/canonical-api.md +++ b/docs/canonical-api.md @@ -4,7 +4,7 @@ Generated signatures and the complete export list live in docs/api/. Run pnpm docs:freshness after editing this file. --> -> **Version 0.108.1.** +> **Version 0.108.2.** > [`docs/api/primitive-catalog.md`](./api/primitive-catalog.md) lists every export and import path. > `agent-eval` must satisfy `>=0.134.2 <0.135.0`. > `sandbox` must satisfy `>=0.15.0 <0.16.0`. diff --git a/package.json b/package.json index 291235a8..e6421b53 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tangle-network/agent-runtime", - "version": "0.108.1", + "version": "0.108.2", "description": "Shared task-lifecycle skeleton for agents: a recursive loop kernel for chat turns, one-shot tasks, and multi-attempt loops, with trace capture and eval-gated self-improvement. Domain behavior lives in adapters; scoring and ship-gates in @tangle-network/agent-eval.", "homepage": "https://github.com/tangle-network/agent-runtime#readme", "repository": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf4894e3..5bc95131 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,8 +16,8 @@ catalogs: specifier: 0.36.0 version: 0.36.0 '@tangle-network/agent-knowledge': - specifier: 6.1.8 - version: 6.1.8 + specifier: 6.1.9 + version: 6.1.9 '@tangle-network/agent-profile-materialize': specifier: 0.9.2 version: 0.9.2 @@ -46,7 +46,7 @@ importers: dependencies: '@tangle-network/agent-knowledge': specifier: 'catalog:' - version: 6.1.8 + version: 6.1.9 '@tangle-network/agent-profile-materialize': specifier: 'catalog:' version: 0.9.2(@tangle-network/agent-interface@0.36.0) @@ -110,7 +110,7 @@ importers: version: 0.36.0 '@tangle-network/agent-knowledge': specifier: 'catalog:' - version: 6.1.8 + version: 6.1.9 '@tangle-network/agent-runtime': specifier: workspace:* version: link:.. @@ -878,8 +878,8 @@ packages: '@tangle-network/agent-interface@0.36.0': resolution: {integrity: sha512-l48SiAY5Atx6ZTL86/XP2wSUPfJJ1sga82/F6V0KqwVkE5MHWiFql6wsLoqk0lc7s4EJe6+cKky+2zzxFuxDpg==} - '@tangle-network/agent-knowledge@6.1.8': - resolution: {integrity: sha512-hKLNkhV9h27Qgp+1SCtYbon6sIMS7iK+2/j1nfEwjLzMBHuka3vJn7xLEJMk/MR0vfJa+iWPEVpfeUMoYczi0w==} + '@tangle-network/agent-knowledge@6.1.9': + resolution: {integrity: sha512-datald4wDSJbZgnQpoxFZ3wRVdlqMez3iVokjNYXdpx1nLBaZeIA17fWAUixpTZjO/cEtVeqtM9AqhNwGmJ7bg==} engines: {node: '>=20.19.0'} hasBin: true @@ -2313,7 +2313,7 @@ snapshots: '@noble/hashes': 1.8.0 zod: 4.4.3 - '@tangle-network/agent-knowledge@6.1.8': + '@tangle-network/agent-knowledge@6.1.9': dependencies: '@tangle-network/agent-eval': 0.134.2 '@tangle-network/agent-interface': 0.36.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 800718b6..656c0cf4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -19,7 +19,7 @@ catalog: '@types/node': 26.1.1 '@tangle-network/agent-eval': 0.134.2 '@tangle-network/agent-interface': 0.36.0 - '@tangle-network/agent-knowledge': 6.1.8 + '@tangle-network/agent-knowledge': 6.1.9 '@tangle-network/agent-profile-materialize': 0.9.2 '@tangle-network/sandbox': 0.15.2 publint: 0.3.22 diff --git a/scripts/verify-package-exports.mjs b/scripts/verify-package-exports.mjs index f9d0c5cb..e67c24f9 100644 --- a/scripts/verify-package-exports.mjs +++ b/scripts/verify-package-exports.mjs @@ -1,5 +1,13 @@ import { spawnSync } from 'node:child_process' -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' @@ -9,6 +17,26 @@ import { requiredPackedDevelopmentDependency, } from './lib/packed-package-test.mjs' +// `graceful-fs` patches Node's `fs` at MODULE scope (`fs.close = ...`), and +// `proper-lockfile` statically imports it. workerd exposes those as getter-only +// accessors, so the assignment throws while Cloudflare validates an uploaded +// Worker — `Cannot set property close of # which has only a getter +// [code: 10021]` — and the ENTIRE Worker is rejected, with no request frame and +// nothing the app can catch. +// +// This is a transitive hazard, which is why it belongs here and not only in the +// package that trips it: agent-runtime pins `@tangle-network/agent-knowledge` +// at an exact version and re-exports it from the root barrel, so when +// agent-knowledge shipped a static `import { lock } from 'proper-lockfile'`, +// every Cloudflare product importing `runToolLoop` from this package became +// undeployable. The defect was one package away and this repo's CI could not +// see it. Scanning the INSTALLED @tangle-network tree is what closes that gap. +// +// `wrangler deploy --dry-run` bundles without executing module scope, so it is +// structurally blind to this class — a static check on the shipped bytes is the +// only thing that catches it before a real upload does. +const edgeUnsafeStaticImports = ['graceful-fs', 'proper-lockfile'] + const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') const tempRoot = mkdtempSync(join(tmpdir(), 'agent-runtime-package-')) const suppliedTarball = process.argv[2] ? resolve(process.argv[2]) : undefined @@ -299,6 +327,7 @@ try { // This fixture type-checks with its declared dev toolchain; ambient production // install settings must not silently omit TypeScript or the Node declarations. run('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund'], appDir) + assertNoEdgeUnsafeStaticImports(join(appDir, 'node_modules', '@tangle-network')) run('npm', ['exec', '--', 'tsc', '-p', 'tsconfig.json'], appDir) run( @@ -614,6 +643,95 @@ try { rmSync(tempRoot, { recursive: true, force: true }) } +function assertNoEdgeUnsafeStaticImports(scopeDir) { + assertEdgeUnsafeStaticImportMatcher() + if (!existsSync(scopeDir)) throw new Error(`expected an installed scope at ${scopeDir}`) + const packageDirs = firstPartyPackageDirs(scopeDir) + if (packageDirs.length === 0) throw new Error(`no @tangle-network packages installed at ${scopeDir}`) + const offenders = [] + let scanned = 0 + for (const packageDir of packageDirs) { + // Only the package's OWN shipped files. Third-party code inside a nested + // `node_modules` is allowed to patch — `proper-lockfile` itself statically + // imports `graceful-fs`, and that is fine as long as no first-party bundle + // statically imports `proper-lockfile`. + for (const file of ownJavascriptFiles(packageDir)) { + scanned += 1 + const source = readFileSync(file, 'utf8') + for (const specifier of edgeUnsafeStaticImports) { + if (hasStaticImport(source, specifier)) { + offenders.push(`${file.slice(scopeDir.length + 1)} -> ${specifier}`) + } + } + } + } + if (offenders.length > 0) { + throw new Error( + [ + 'an installed @tangle-network package statically imports a module that patches a Node builtin.', + 'Cloudflare rejects the whole Worker on upload with code 10021, and a dry run cannot see it.', + 'Fix it in the owning package with a dynamic import() inside the function that needs it.', + ...offenders.map((offender) => ` - ${offender}`), + ].join('\n'), + ) + } + process.stdout.write( + `Edge module-load safety: ${packageDirs.length} first-party packages, ${scanned} shipped files, no builtin patching.\n`, + ) +} + +function hasStaticImport(source, specifier) { + const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const quoted = `["']${escaped}["']` + // Bound imports, bare side-effect imports, re-exports, and CommonJS require. + // Dynamic import() is deliberately excluded because it defers module scope. + return new RegExp( + `(?:\\bfrom\\s*${quoted}|\\bimport\\s*${quoted}|\\brequire\\(\\s*${quoted}\\s*\\))`, + ).test(source) +} + +// The matcher is the whole guard, so prove it still separates the forms it +// claims to separate before trusting a clean scan. +function assertEdgeUnsafeStaticImportMatcher() { + const specifier = 'proper-lockfile' + const cases = [ + ['bound ESM import', `import value from '${specifier}'`, true], + ['bare side-effect ESM import', `import '${specifier}'`, true], + ['ESM re-export', `export { value } from '${specifier}'`, true], + ['CommonJS require', `require('${specifier}')`, true], + ['dynamic import', `await import('${specifier}')`, false], + ] + for (const [label, source, expected] of cases) { + if (hasStaticImport(source, specifier) !== expected) { + throw new Error(`edge-unsafe static import matcher failed: ${label}`) + } + } +} + +/** Every installed `@tangle-network/*` directory, including nested copies. */ +function firstPartyPackageDirs(scopeDir) { + const dirs = [] + for (const entry of readdirSync(scopeDir, { withFileTypes: true })) { + if (!entry.isDirectory() || entry.name.startsWith('.')) continue + const packageDir = join(scopeDir, entry.name) + dirs.push(packageDir) + const nestedScope = join(packageDir, 'node_modules', '@tangle-network') + if (existsSync(nestedScope)) dirs.push(...firstPartyPackageDirs(nestedScope)) + } + return dirs +} + +function ownJavascriptFiles(directory) { + const files = [] + for (const entry of readdirSync(directory, { withFileTypes: true })) { + if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue + const path = join(directory, entry.name) + if (entry.isDirectory()) files.push(...ownJavascriptFiles(path)) + else if (/\.(?:js|mjs|cjs)$/.test(entry.name)) files.push(path) + } + return files +} + function run(command, args, cwd) { const result = spawnSync(command, args, { cwd, diff --git a/src/testing/fixtures/agent-improvement-proposal.json b/src/testing/fixtures/agent-improvement-proposal.json index 94ea5ff3..8aa4da6e 100644 --- a/src/testing/fixtures/agent-improvement-proposal.json +++ b/src/testing/fixtures/agent-improvement-proposal.json @@ -1,6 +1,6 @@ { "changedSurfaces": ["prompt"], - "digest": "sha256:08d05582d3b22918b525a09c5e12f2707c484f962bf8f7120da86fa24fb342ad", + "digest": "sha256:4145f2be9d14c8ff4b254d7739a1fa405b5866ce63a499bcd2ff0dae38132829", "evaluation": { "decision": { "contributingChecks": [ @@ -4810,7 +4810,7 @@ ], "metadata": { "fixture": "agent-improvement-proposal", - "runtimeVersion": "0.108.1" + "runtimeVersion": "0.108.2" }, "objectives": [ { @@ -4921,8 +4921,8 @@ "baselineContentHash": "sha256:5c21ee53e513fc604cb09754e21c392b24a424da0ef37dbf8f1ee4a8a0b08f09", "candidateContentHash": "sha256:60fcbb1c728194bd51d7d19cb732d1c3f1881dce7e0a6266b41c8b98cfd65693", "kind": "agent-eval-loop", - "recordDigest": "sha256:7fa5934d75bc213405a15288d66f453f05ba42c52c907583e25e31ef074761cf", - "runId": "agent-runtime-0.108.1-proposal-fixture", + "recordDigest": "sha256:d547a3a917d2f87c1c02c49b3feb2510baff0bf2d0254632b12875b08b831696", + "runId": "agent-runtime-0.108.2-proposal-fixture", "schema": "agent-candidate-experiment" } }, @@ -4949,5 +4949,5 @@ ], "kind": "agent-improvement-proposal", "proposedAt": "2026-07-10T01:00:00.000Z", - "runId": "agent-runtime-0.108.1-proposal-fixture" + "runId": "agent-runtime-0.108.2-proposal-fixture" } diff --git a/src/testing/fixtures/agent-profile-improvement-proposal.json b/src/testing/fixtures/agent-profile-improvement-proposal.json index 2a7fea33..662c189e 100644 --- a/src/testing/fixtures/agent-profile-improvement-proposal.json +++ b/src/testing/fixtures/agent-profile-improvement-proposal.json @@ -1,6 +1,6 @@ { "changedSurfaces": ["prompt", "skills"], - "digest": "sha256:35274b75919ceb1cabd118c49c89908d27c373dc58902f20e4b4e31cbe3e9b8e", + "digest": "sha256:24e0f96f6548cb72e31cd43d5175af388d618bcfae2b13b98409a6e4fa64ce1f", "evaluation": { "decision": { "contributingChecks": [ @@ -1695,7 +1695,7 @@ ], "metadata": { "fixture": "agent-profile-improvement-proposal", - "runtimeVersion": "0.108.1" + "runtimeVersion": "0.108.2" }, "objectives": [ {