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', }) }) })