Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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 #<Object> 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.
Expand Down
2 changes: 1 addition & 1 deletion docs/api/primitive-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/canonical-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
14 changes: 7 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
120 changes: 119 additions & 1 deletion scripts/verify-package-exports.mjs
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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 #<Object> 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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 5 additions & 5 deletions src/testing/fixtures/agent-improvement-proposal.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"changedSurfaces": ["prompt"],
"digest": "sha256:08d05582d3b22918b525a09c5e12f2707c484f962bf8f7120da86fa24fb342ad",
"digest": "sha256:4145f2be9d14c8ff4b254d7739a1fa405b5866ce63a499bcd2ff0dae38132829",
"evaluation": {
"decision": {
"contributingChecks": [
Expand Down Expand Up @@ -4810,7 +4810,7 @@
],
"metadata": {
"fixture": "agent-improvement-proposal",
"runtimeVersion": "0.108.1"
"runtimeVersion": "0.108.2"
},
"objectives": [
{
Expand Down Expand Up @@ -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"
}
},
Expand All @@ -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"
}
4 changes: 2 additions & 2 deletions src/testing/fixtures/agent-profile-improvement-proposal.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"changedSurfaces": ["prompt", "skills"],
"digest": "sha256:35274b75919ceb1cabd118c49c89908d27c373dc58902f20e4b4e31cbe3e9b8e",
"digest": "sha256:24e0f96f6548cb72e31cd43d5175af388d618bcfae2b13b98409a6e4fa64ce1f",
"evaluation": {
"decision": {
"contributingChecks": [
Expand Down Expand Up @@ -1695,7 +1695,7 @@
],
"metadata": {
"fixture": "agent-profile-improvement-proposal",
"runtimeVersion": "0.108.1"
"runtimeVersion": "0.108.2"
},
"objectives": [
{
Expand Down
Loading