diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 98e82ee..ffca6b8 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -64,6 +64,7 @@ Opening a PR prefills the [pull request template](pull_request_template.md), whi
- TypeScript strict mode, ES modules (`.js` extensions in imports)
- No additional linter/formatter configured — follow existing patterns
- Keep dependency ownership explicit. Core must remain importable and runnable without optional integrations
+- Pin what stays in the repository; keep ranges on what a consumer installs. Every `devDependencies` entry is pinned to an exact version and the root [`.npmrc`](../.npmrc) sets `save-exact=true`, but core's `dependencies`, every `peerDependencies`, and otel's `@open-multi-agent/core` stay semver ranges. `save-exact` will pin a new dependency added to one of those blocks, so widen it by hand — CI fails when one is left exact
- Add new optional provider SDKs as peer dependencies and load them lazily with dynamic `import()`
- Keep OpenTelemetry APIs, SDKs, semantic-convention packages, and exporters in `@open-multi-agent/otel`, never in the core root import
- Version `@open-multi-agent/otel` independently from core and express core compatibility in its dependency range
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f9f034c..6189bc5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -23,7 +23,7 @@ jobs:
with:
node-version: 22
cache: npm
- - run: npm ci
+ - run: npm ci --ignore-scripts
- run: npm run lint
test:
@@ -38,7 +38,7 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
cache: npm
- - run: npm ci
+ - run: npm ci --ignore-scripts
- run: npm test
coverage:
@@ -53,7 +53,7 @@ jobs:
with:
node-version: 22
cache: npm
- - run: npm ci
+ - run: npm ci --ignore-scripts
- run: npm run test:coverage
- uses: codecov/codecov-action@v5
with:
@@ -70,7 +70,7 @@ jobs:
with:
node-version: 22
cache: npm
- - run: npm ci
+ - run: npm ci --ignore-scripts
- run: npm run build
- name: Smoke-test that the built entry points import and the CLI runs
working-directory: packages/core
@@ -112,6 +112,28 @@ jobs:
exit 1
fi
done
+ - name: Assert the published dependency blocks stay semver ranges
+ run: |
+ set -euo pipefail
+ # save-exact=true in the root .npmrc pins whatever `npm install`
+ # saves. That is right for the tree that never leaves this
+ # repository and wrong for the blocks a consumer installs: an exact
+ # version there overrides their own resolution and stops their
+ # `npm audit fix` from reaching a patched build until we cut a
+ # release. A new dependency added to one of these has to be widened
+ # by hand, so assert it rather than trusting the habit.
+ fail=0
+ for manifest in packages/core/package.json packages/otel/package.json packages/create-oma-app/package.json; do
+ for block in dependencies peerDependencies; do
+ pinned=$(jq -r --arg b "$block" '(.[$b] // {}) | to_entries[] | select(.value | test("^[0-9]")) | "\(.key)@\(.value)"' "$manifest" | tr '\n' ' ')
+ if [ -n "$pinned" ]; then
+ echo "::error::$manifest .$block ships to consumers and must stay semver ranges, but these are exact: $pinned"
+ fail=1
+ fi
+ done
+ done
+ test "$fail" -eq 0
+
- name: Typecheck the create-oma-app template against core
run: npm run typecheck:template -w create-oma-app
- name: Assert the npm tarball ships exactly the expected files
@@ -232,6 +254,6 @@ jobs:
with:
node-version: 22
cache: npm
- - run: npm ci
+ - run: npm ci --ignore-scripts
- name: End-to-end scaffold smoke test (pack -> generate -> install -> run gate)
run: npm run test:scaffold
diff --git a/.github/workflows/provider-canary.yml b/.github/workflows/provider-canary.yml
index 896eb11..a14fdf9 100644
--- a/.github/workflows/provider-canary.yml
+++ b/.github/workflows/provider-canary.yml
@@ -22,7 +22,7 @@ jobs:
with:
node-version: 22
cache: npm
- - run: npm ci
+ - run: npm ci --ignore-scripts
- name: Require the DeepSeek canary credential
env:
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index d6ce667..d5a7beb 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -103,7 +103,7 @@ jobs:
node-version: 24
registry-url: https://registry.npmjs.org
- - run: npm ci
+ - run: npm ci --ignore-scripts
- run: npm run build
- name: Publish missing packages, tag, and create the GitHub Release
diff --git a/.github/workflows/release-bot.yml b/.github/workflows/release-bot.yml
index 478fa6a..0bfd40c 100644
--- a/.github/workflows/release-bot.yml
+++ b/.github/workflows/release-bot.yml
@@ -56,7 +56,7 @@ jobs:
node-version: 24
cache: npm
- - run: npm ci
+ - run: npm ci --ignore-scripts
- name: Configure auditable bot commit identity
env:
diff --git a/.github/workflows/supply-chain-audit.yml b/.github/workflows/supply-chain-audit.yml
new file mode 100644
index 0000000..0ddaa18
--- /dev/null
+++ b/.github/workflows/supply-chain-audit.yml
@@ -0,0 +1,98 @@
+name: Supply chain audit
+
+# Two questions about the dependencies that actually reach a consumer of the
+# published packages, asked on a schedule rather than on every pull request.
+#
+# `--omit=dev` is the whole point of running this separately. A full audit of
+# this repository reports well over a dozen advisories, every one of them in
+# the development tree (test runner, provider SDK fixtures, scaffolder
+# tooling) where nothing ships to npm. Auditing that tree on a schedule would
+# produce a permanently red job that everyone learns to ignore. The production
+# tree is small enough that a finding in it is worth reading.
+#
+# The two steps fail differently on purpose:
+#
+# - `npm audit` REPORTS. A new advisory upstream is not this repository
+# misbehaving, and there may be no fixed version to move to yet. The
+# finding lands in the run summary; a maintainer decides what it means.
+# - `npm audit signatures` FAILS. It compares the bytes npm served against
+# the registry's own signature, so a mismatch is a tampering or corruption
+# signal rather than upstream churn. Every production package verifies
+# today, so this starts green and only turns red when something is wrong.
+#
+# Neither step gates a pull request. Scheduling this an hour before the weekly
+# release bot means a finding is on the record before the release PR is drafted.
+
+on:
+ workflow_dispatch:
+ schedule:
+ # Friday 09:41 UTC, an hour ahead of the release bot's 10:23. Off the top
+ # of the hour for the same reason release-bot.yml documents: the most
+ # contended slot is the one GitHub is most likely to drop.
+ - cron: '41 9 * * 5'
+
+permissions:
+ contents: read
+
+concurrency:
+ group: supply-chain-audit-${{ github.ref }}
+ cancel-in-progress: false
+
+jobs:
+ audit:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: npm
+ - run: npm ci --ignore-scripts
+
+ - name: Report advisories against the production dependency tree
+ run: |
+ set -uo pipefail
+ npm audit --omit=dev --json > "$RUNNER_TEMP/audit.json"
+ audit_exit=$?
+ npm audit --omit=dev > "$RUNNER_TEMP/audit.txt" 2>&1 || true
+ cat "$RUNNER_TEMP/audit.txt"
+
+ # A non-zero exit means either "advisories found" or "the audit could
+ # not run", and only the second one is this workflow's problem. A
+ # registry failure still writes well-formed JSON, so check for the
+ # metadata block rather than trusting the exit code or the parse.
+ if ! node -e 'const r = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")); if (r.error || !r.metadata?.vulnerabilities) process.exit(1)' "$RUNNER_TEMP/audit.json"; then
+ echo "::error::npm audit did not produce a report (exit $audit_exit); the production tree was not actually checked"
+ exit 1
+ fi
+
+ node - "$RUNNER_TEMP/audit.json" "$RUNNER_TEMP/audit.txt" <<'REPORT' >> "$GITHUB_STEP_SUMMARY"
+ const fs = require('node:fs')
+ const report = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'))
+ const counts = report.metadata?.vulnerabilities ?? {}
+ const total = counts.total ?? 0
+
+ console.log('## Production dependency advisories\n')
+ console.log(`Audited \`${report.metadata?.dependencies?.prod ?? '?'}\` production dependencies with \`npm audit --omit=dev\`.\n`)
+
+ if (total === 0) {
+ console.log('No advisories against the dependencies that reach a consumer of the published packages.')
+ } else {
+ console.log('| Severity | Count |')
+ console.log('|---|---|')
+ for (const level of ['critical', 'high', 'moderate', 'low', 'info']) {
+ if (counts[level]) console.log(`| ${level} | ${counts[level]} |`)
+ }
+ console.log('')
+ console.log('Full report
\n')
+ console.log('```')
+ console.log(fs.readFileSync(process.argv[3], 'utf8').trimEnd())
+ console.log('```\n')
+ console.log(' \n')
+ console.log('This step reports rather than fails. Decide whether the advisory reaches a caller of the published packages before acting on it.')
+ }
+ REPORT
+
+ - name: Verify registry signatures and provenance attestations
+ run: npm audit signatures --omit=dev
diff --git a/.gitignore b/.gitignore
index a12b8e2..a597493 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,7 +7,10 @@ oma-dashboards/
.agent-workspace/
.github/brand/frames/
.claude/
+# Local npm configuration, which may carry registry credentials. The
+# repository's own root .npmrc is tracked and is the one exception.
.npmrc
+!/.npmrc
.env
# Benchmark output. The harness in bench/src/ is tracked; everything it
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..e2e7159
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,30 @@
+# Repository-level npm configuration. It applies to every workspace and to
+# contributors and CI alike.
+
+# Save an exact version rather than a caret range when `npm install `
+# adds or upgrades a dependency, so an upgrade is always a reviewable diff
+# instead of something a later install resolves on its own.
+#
+# The line this draws is what ships. Anything that stays inside the repository
+# is pinned: every `devDependencies` entry, and the dependencies of the
+# private release bot. Anything a consumer installs keeps a range, because an
+# exact version there overrides their resolution and stops their own
+# `npm audit fix` from reaching a patched build without waiting for a release:
+#
+# - `dependencies` in packages/core/package.json, the only published
+# runtime block in the repository
+# - every `peerDependencies` entry, which states what a consumer may bring
+# - `@open-multi-agent/core` in packages/otel/package.json, which expresses
+# core compatibility
+#
+# save-exact does not rewrite manifests that already carry a range, but it
+# will pin a NEW dependency added to one of those blocks, so widen it by hand.
+# The `package` job in ci.yml asserts both published blocks and fails if
+# either is ever saved exactly.
+save-exact=true
+
+# `min-release-age` is deliberately absent. It first shipped in npm 11.10.0,
+# and the CI matrix runs Node 20 and 22, which bundle npm 10.8.2 and 10.9.8.
+# Those two would ignore the key without a word, so the quarantine window
+# would hold on one leg of the matrix and not the other two. It also has no
+# effect on `npm ci`, which installs the lockfile verbatim.
diff --git a/AGENTS.md b/AGENTS.md
index 9b1468a..9f4d0a4 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -40,6 +40,7 @@ Examples and core E2E tests may require real provider credentials. Unit tests mo
- Change source files, tests, templates, or docs rather than generated `dist/` output.
- Add or update tests for behavior changes. Update user-facing docs and examples when public behavior changes, or state why they are not applicable.
- Keep dependency ownership explicit. Core must remain importable and runnable without optional integrations.
+- **Pin what stays in the repository; keep ranges on what a consumer installs.** Every `devDependencies` entry and the private release bot's dependencies are pinned exactly, and the root [`.npmrc`](.npmrc) sets `save-exact=true` so a new one arrives pinned. The published blocks stay semver ranges — core's `dependencies`, every `peerDependencies`, and otel's `@open-multi-agent/core` — because an exact version there overrides a consumer's own resolution and blocks their `npm audit fix` until we cut a release. `save-exact` will pin a new dependency added to one of those blocks, so widen it by hand; the `package` job in [`.github/workflows/ci.yml`](.github/workflows/ci.yml) fails when one is left exact.
- Add optional provider SDKs as peer dependencies and load them lazily with dynamic `import()`. Do not maintain a fixed dependency or adapter count in documentation.
- OpenTelemetry APIs, SDKs, semantic-convention packages, and exporters belong in `@open-multi-agent/otel`, never in the core root import. The application owns its tracer/provider lifecycle unless an API explicitly says otherwise.
- Treat `docs/` as the source of truth for subsystem behavior. Keep this file to rules and concise invariants; link to docs instead of copying long explanations.
diff --git a/package-lock.json b/package-lock.json
index a538ba4..a6fa995 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,8 +12,8 @@
"packages/*"
],
"devDependencies": {
- "tsx": "^4.21.0",
- "typescript": "^5.6.0"
+ "tsx": "4.21.0",
+ "typescript": "5.9.3"
},
"engines": {
"node": ">=20.0.0"
@@ -5354,18 +5354,18 @@
"oma": "dist/cli/oma.js"
},
"devDependencies": {
- "@agentclientprotocol/sdk": "^1.2.1",
- "@ai-sdk/openai": "^4.0.0",
- "@ai-sdk/provider": "^4.0.0",
- "@aws-sdk/client-bedrock-runtime": "^3.1040.0",
- "@google/genai": "^1.48.0",
- "@modelcontextprotocol/sdk": "^1.18.0",
- "@types/node": "^22.0.0",
- "@vitest/coverage-v8": "^2.1.9",
- "ai": "^7.0.0",
- "tsx": "^4.21.0",
- "typescript": "^5.6.0",
- "vitest": "^2.1.0"
+ "@agentclientprotocol/sdk": "1.2.1",
+ "@ai-sdk/openai": "4.0.27",
+ "@ai-sdk/provider": "4.0.4",
+ "@aws-sdk/client-bedrock-runtime": "3.1101.0",
+ "@google/genai": "1.48.0",
+ "@modelcontextprotocol/sdk": "1.29.0",
+ "@types/node": "22.19.15",
+ "@vitest/coverage-v8": "2.1.9",
+ "ai": "7.0.47",
+ "tsx": "4.21.0",
+ "typescript": "5.9.3",
+ "vitest": "2.1.9"
},
"engines": {
"node": ">=20.0.0"
@@ -5432,9 +5432,9 @@
"create-oma-app": "dist/index.js"
},
"devDependencies": {
- "@types/node": "^22.0.0",
- "typescript": "^5.6.0",
- "vitest": "^2.1.0"
+ "@types/node": "22.19.15",
+ "typescript": "5.9.3",
+ "vitest": "2.1.9"
},
"engines": {
"node": ">=20.0.0"
@@ -5448,13 +5448,13 @@
"@open-multi-agent/core": "^1.11.0"
},
"devDependencies": {
- "@opentelemetry/api": "^1.9.0",
- "@opentelemetry/exporter-trace-otlp-http": "^0.221.0",
- "@opentelemetry/sdk-trace-base": "^2.9.0",
- "@opentelemetry/sdk-trace-node": "^2.10.0",
- "@types/node": "^22.0.0",
- "typescript": "^5.6.0",
- "vitest": "^2.1.0"
+ "@opentelemetry/api": "1.9.1",
+ "@opentelemetry/exporter-trace-otlp-http": "0.221.0",
+ "@opentelemetry/sdk-trace-base": "2.10.0",
+ "@opentelemetry/sdk-trace-node": "2.10.0",
+ "@types/node": "22.19.15",
+ "typescript": "5.9.3",
+ "vitest": "2.1.9"
},
"engines": {
"node": ">=20.0.0"
@@ -5468,15 +5468,15 @@
"version": "0.0.0",
"dependencies": {
"@open-multi-agent/core": "*",
- "zod": "^3.23.0"
+ "zod": "3.25.76"
},
"bin": {
"oma-release-bot": "dist/cli.js"
},
"devDependencies": {
- "@types/node": "^22.0.0",
- "typescript": "^5.6.0",
- "vitest": "^2.1.0"
+ "@types/node": "22.19.15",
+ "typescript": "5.9.3",
+ "vitest": "2.1.9"
},
"engines": {
"node": ">=20.0.0"
diff --git a/package.json b/package.json
index 31a755e..c9b9e78 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,7 @@
"node": ">=20.0.0"
},
"devDependencies": {
- "tsx": "^4.21.0",
- "typescript": "^5.6.0"
+ "tsx": "4.21.0",
+ "typescript": "5.9.3"
}
}
diff --git a/packages/core/package.json b/packages/core/package.json
index c7235ec..9f2c6ac 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -134,17 +134,17 @@
}
},
"devDependencies": {
- "@agentclientprotocol/sdk": "^1.2.1",
- "@ai-sdk/openai": "^4.0.0",
- "@ai-sdk/provider": "^4.0.0",
- "@aws-sdk/client-bedrock-runtime": "^3.1040.0",
- "@google/genai": "^1.48.0",
- "@modelcontextprotocol/sdk": "^1.18.0",
- "@types/node": "^22.0.0",
- "@vitest/coverage-v8": "^2.1.9",
- "ai": "^7.0.0",
- "tsx": "^4.21.0",
- "typescript": "^5.6.0",
- "vitest": "^2.1.0"
+ "@agentclientprotocol/sdk": "1.2.1",
+ "@ai-sdk/openai": "4.0.27",
+ "@ai-sdk/provider": "4.0.4",
+ "@aws-sdk/client-bedrock-runtime": "3.1101.0",
+ "@google/genai": "1.48.0",
+ "@modelcontextprotocol/sdk": "1.29.0",
+ "@types/node": "22.19.15",
+ "@vitest/coverage-v8": "2.1.9",
+ "ai": "7.0.47",
+ "tsx": "4.21.0",
+ "typescript": "5.9.3",
+ "vitest": "2.1.9"
}
}
diff --git a/packages/create-oma-app/package.json b/packages/create-oma-app/package.json
index 7b79dc3..06f7ae3 100644
--- a/packages/create-oma-app/package.json
+++ b/packages/create-oma-app/package.json
@@ -56,8 +56,8 @@
"node": ">=20.0.0"
},
"devDependencies": {
- "@types/node": "^22.0.0",
- "typescript": "^5.6.0",
- "vitest": "^2.1.0"
+ "@types/node": "22.19.15",
+ "typescript": "5.9.3",
+ "vitest": "2.1.9"
}
}
diff --git a/packages/otel/package.json b/packages/otel/package.json
index 155612f..260cd1c 100644
--- a/packages/otel/package.json
+++ b/packages/otel/package.json
@@ -58,12 +58,12 @@
"@opentelemetry/api": "^1.9.0"
},
"devDependencies": {
- "@opentelemetry/api": "^1.9.0",
- "@opentelemetry/exporter-trace-otlp-http": "^0.221.0",
- "@opentelemetry/sdk-trace-base": "^2.9.0",
- "@opentelemetry/sdk-trace-node": "^2.10.0",
- "@types/node": "^22.0.0",
- "typescript": "^5.6.0",
- "vitest": "^2.1.0"
+ "@opentelemetry/api": "1.9.1",
+ "@opentelemetry/exporter-trace-otlp-http": "0.221.0",
+ "@opentelemetry/sdk-trace-base": "2.10.0",
+ "@opentelemetry/sdk-trace-node": "2.10.0",
+ "@types/node": "22.19.15",
+ "typescript": "5.9.3",
+ "vitest": "2.1.9"
}
}
diff --git a/packages/release-bot/package.json b/packages/release-bot/package.json
index 87422f5..d9d6db0 100644
--- a/packages/release-bot/package.json
+++ b/packages/release-bot/package.json
@@ -21,11 +21,11 @@
},
"dependencies": {
"@open-multi-agent/core": "*",
- "zod": "^3.23.0"
+ "zod": "3.25.76"
},
"devDependencies": {
- "@types/node": "^22.0.0",
- "typescript": "^5.6.0",
- "vitest": "^2.1.0"
+ "@types/node": "22.19.15",
+ "typescript": "5.9.3",
+ "vitest": "2.1.9"
}
}
diff --git a/packages/release-bot/tests/package-boundary.test.ts b/packages/release-bot/tests/package-boundary.test.ts
index 4711fa6..1906800 100644
--- a/packages/release-bot/tests/package-boundary.test.ts
+++ b/packages/release-bot/tests/package-boundary.test.ts
@@ -15,7 +15,7 @@ describe('release bot package boundary', () => {
expect(manifest.publishConfig).toBeUndefined()
expect(manifest.dependencies).toEqual({
'@open-multi-agent/core': '*',
- zod: '^3.23.0',
+ zod: '3.25.76',
})
})
})