9.5 KiB
ADR-115: Dependency Security System — scheduled audit + guarded in-range updater
| Field | Value |
|---|---|
| Decision ID | ADR-115 |
| Status | Accepted (2026-06-30) — implemented (audit + updater scripts, two scheduled workflows, policy doc); grpc-js HIGH fixed in lockfile |
| Date | 2026-06-30 |
| Author | AQE Core |
| Review Cadence | 3 months |
| Supersedes | — |
| Related | consumer-audit.yml (the shift-left half this extends), npm-publish.yml (publish gate), ADR-114 (sibling "guard a surface in CI" pattern), docs/guides/dependency-security-policy.md |
WH(Y) Decision Statement
In the context of a cold-outreach email correctly reporting that
@grpc/grpc-js@1.14.3 carried a HIGH CVE (CVE-2026-48068, server-crash DoS,
fixed in 1.14.4) that had sat in our lockfile while our CI stayed green — and a
bundled pitch to connect a third-party SaaS scanner (patchhog.dev) to the repo,
facing the fact that every existing dependency check —
consumer-audit.yml and the npm-publish.yml gate — is triggered only by
changes to package.json / package-lock.json, so a CVE disclosed against
an already-pinned, unchanged dependency is invisible until someone happens to
edit a dep file; and that the off-the-shelf fix (Dependabot/Renovate) is
actively wrong here because it prunes our 15 cross-platform optionalDependencies
and produces a lock that fails npm ci with EUSAGE,
we decided for building our own two-part system: (1) a scheduled daily
audit that re-runs the authoritative consumer-POV check against unchanged
deps and files a tracking issue on new HIGH/CRITICAL, and (2) a guarded
in-range updater (npm audit fix --package-lock-only by default) that asserts
no optional platform dep is dropped before opening a weekly security PR — both
reusing the existing consumer-POV audit logic, both using the gh CLI rather
than a third-party PR action,
and neglected Dependabot/Renovate (breaks our lockfile), auto-merge of
dependency PRs (humans review), flag-level or major-version automation (in-range
only — major bumps stay manual), and connecting any external security SaaS
(our own consumer-POV npm audit already detects what such pitches flag),
to achieve detection that no longer depends on someone touching a dep file — newly-disclosed CVEs on frozen pins surface on their own, on our schedule, not when a vendor emails us — plus low-risk, reviewable, optional-dep-safe update PRs,
accepting that the scheduled audit can file a noisy issue when an upstream
has no fix yet (triaged per the severity SLA), that "in-range only" means a
breaking-fix CVE still needs a human, and that maintaining the overrides block
and the optional-dep guard set remains a deliberate, periodic task.
Current state (grounded, verified 2026-06-30)
| Fact | Evidence |
|---|---|
| grpc-js HIGH was real and present | npm audit → GHSA-5375-pq7m-f5r2 (CVE-2026-48068) + GHSA-99f4-grh7-6pcq (CVE-2026-48069), both >=1.14.0 <1.14.4 |
| Fix is in-range | requested as ^1.14.3; npm update @grpc/grpc-js --package-lock-only → 1.14.4, HIGH 1→0, optional deps 90→90 |
| No scheduled dependency check existed | only init-chaos/corpus-mirror cron; consumer-audit.yml is paths:-filtered on dep files only |
| No Dependabot/Renovate config | none in .github/; and Dependabot is known to prune our optional @ruvector/* deps |
| Consumer-POV audit already proven | consumer-audit.yml tarball-install pattern (precedent: 15 CRITICAL protobufjs vulns at v3.9.13) |
| 15 optionalDependencies to protect | @ruvector/* (13) + hnswlib-node + rvlite |
The core problem in one line: AQE could detect a vulnerable dependency the moment someone edited deps, but had nothing watching the deps that never change.
Decision detail
1. Audit script (shared, importable)
scripts/dependency-audit.mjs — parses npm audit --json into a flat finding
list; two modes: default in-repo (fast, our view) and --consumer (pack →
clean-install → audit; the user's view, since root overrides don't apply
downstream). Exits 1 on any finding >= --level (default high). Writes
reports/dependency-audit.{json,md}. Exports runRepoAudit() for reuse.
2. Updater script (guarded)
scripts/dependency-update.mjs — default security mode runs
npm audit fix --package-lock-only (only vulnerable packages, in-range);
--all runs npm update --package-lock-only (broad maintenance). Neither uses
--force. Guard: snapshots the optional-dep set before/after and, if any
optionalDependencies entry is pruned from the lock, restores
package-lock.json from git and exits 1 rather than propose a cross-platform-
breaking lock. Writes reports/dependency-update.{json,md} with the audit delta.
3. Scheduled workflows
dependency-audit.yml— daily 07:00 UTC; consumer-POV audit; opens/updates a single tracking issue on HIGH/CRITICAL (ghCLI;issues: writeonly).dependency-update.yml— weekly Mon 06:00 UTC; runs the security updater; opens/refreshes a PR fromdeps/auto-security-updateif the lock moved (ghCLI; no third-party PR action).
4. npm scripts + policy
deps:audit, deps:audit:consumer, deps:update, deps:update:all;
process, SLAs, and the override-review cadence in
docs/guides/dependency-security-policy.md.
Consequences
- Positive: newly-disclosed CVEs on frozen pins are caught on a schedule;
update PRs are small, in-range, and optional-dep-safe; zero new third-party
trust (no SaaS, no PR action, no Dependabot); consumer-POV stays the source of
truth so
overrides-masked findings don't cause false alarms. - Negative / watch: scheduled audit may file issues for not-yet-fixed
upstreams (triage per SLA); breaking-fix CVEs still need a human; the optional-
dep guard set and
overridesblock are manually maintained. - Reversible: scripts and workflows are additive and independently removable; they touch no production runtime code.
Amendment (2026-07-23) — optional peers are consumer-reachable (issue #565)
Trigger. The daily audit filed #565:
4 HIGH findings on agentic-qe, @huggingface/transformers, onnxruntime-node,
and adm-zip (GHSA-xcpc-8h2w-3j85,
crafted ZIP triggers a 4 GB allocation) — all four the same chain, all four with
Fix: none.
What we had got wrong. @huggingface/transformers was declared an optional
peerDependency. We had already made the code side safe — it is loaded through a
lazy await import() and degrades to a clear "not installed" error — but
peerDependenciesMeta.optional: true does not mean "not installed". npm ≥7
auto-installs optional peers; optional only suppresses the error when the peer
cannot be resolved. So every npm install agentic-qe really did pull
@huggingface/transformers → onnxruntime-node → adm-zip@0.5.x, and the consumer
audit was right to flag it.
Why no in-range fix exists. adm-zip@0.6.0 is published, but the current
onnxruntime-node@1.27.0 still declares adm-zip: "^0.5.16", and the current
@huggingface/transformers@4.2.0 pins onnxruntime-node@1.24.3. Root overrides
would fix our own tree and, per the policy above, reach no consumer. There is no
version of this dependency chain a consumer can resolve to that is not vulnerable.
Decision. Remove @huggingface/transformers from peerDependencies /
peerDependenciesMeta. It stays a devDependency (so our own tests and the
in-process embedding path keep working) and becomes an explicit, documented opt-in
for users who want local in-process embeddings. Both load sites
(integrations/embeddings/base/EmbeddingGenerator.ts,
learning/real-embeddings.ts) raise an actionable install/endpoint message when it
is absent. adm-zip: ">=0.6.0" is added to overrides/resolutions for our own
dev tree.
Generalized rule. An optional peerDependency is consumer-reachable and is in
scope for the consumer audit. Making a dependency optional at the code level
(lazy import + graceful degradation) does not make it optional at the install
level. Removing an optional peer is not a breaking change — consumers who were
relying on the transitive install get a precise error telling them what to install.
Consequence. Users who want in-process embeddings must now
npm install @huggingface/transformers themselves, accepting the advisory
knowingly. Everyone using an embedding endpoint (the default) no longer carries a
HIGH CVE they never asked for.
Amendment (2026-08-30) — endpoint-first disclosure (issue #615)
An exact packed-consumer test installed agentic-qe@3.13.12 and then the
documented compatible local provider, @huggingface/transformers@4.2.0. A
production-only npm audit reported four HIGH findings across the provider and
its onnxruntime-node, adm-zip, and sharp dependency paths. Installing safe
top-level adm-zip and sharp versions did not remove the incompatible nested
copies. The unresolved advisories are GHSA-xcpc-8h2w-3j85 and
GHSA-f88m-g3jw-g9cj.
Therefore all missing-provider guidance recommends AQE_EMBEDDER_ENDPOINT
first. The local provider remains available only as an explicit security opt-in,
and its error and MCP degradation messages disclose both advisories. We do not
claim the vulnerable code paths are unexploitable without direct evidence, and
we do not represent root overrides as consumer protection.