Files
deepseek-harness/.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md
T
ZiyaZhang 44bd19056c docs: close the remaining silent-omission paths in the notices generator
Derive the manifest set from each pnpm-workspace.yaml members list, so a
new member area is read when declared. Locate Python requirement arrays
by TOML table and scan them quote-aware, so author-named dependency
groups and extras-bearing requirements are no longer dropped. Search the
nested Landlock store for metadata, reject a non-permissive runtime
license outright, and omit the dev-tooling sentence when it has no
subject.
2026-07-30 10:28:16 -07:00

8.3 KiB

Agent Note: Generated third-party notices

Status: implemented

English | 中文

Problem

Open-sourcing this repository requires disclosing the third-party software it depends on, with each project's license. The disclosure has to be complete, has to stay true as dependencies change, and has to say something a reader can act on — which of these packages end up on a user's machine, and which only build and test the repository.

A hand-written inventory answers none of those durably. Roughly a hundred rows of names and license strings derived from manifests drift silently the moment a package is added, removed, or relicensed, and nothing would notice.

Decision

THIRD_PARTY_NOTICES.md is generated by scripts/gen-third-party-notices.ts from the workspace manifests, vendor/README.md, the pyproject.toml files, and pnpm-workspace.yaml. The root README pair links the file from its License section.

Freshness is maintained, not merely enforced. A pre-commit job regenerates the file and stages it whenever a generator input is staged — any manifest, a workspace declaration, the root lock file, vendor/README.md, a pyproject.toml, the generator itself, or the script holding the build-time pin — so an unrelated dependency edit never has to come back and rerun a generator. The committed bytes are then asserted inside scripts/gen-third-party-notices.spec.ts, which the test lane already runs — the check adds no gate process, no scheduler slot, and no separate CI step. pnpm run verify-third-party-notices remains available for a standalone check.

One trigger gap is accepted rather than worked around: lefthook inspects only files present on disk, so deleting a manifest runs no job, and removing a package reaches the assertion in the test lane instead. Reconstructing the staged file list to include deletions was tried and does not work — lefthook filters the list against the working tree either way. The assertion is the backstop for exactly this case.

The file discloses direct dependencies only. The complete npm closure with pinned versions already lives in pnpm-lock.yaml (pnpm licenses list renders it) and the Python closure in python/sdk/uv.lock; re-materializing either as prose would be a second, worse copy.

Tiering is by declaring area, not by manifest section. A package is a runtime dependency when any manifest outside DEV_ONLY_AREAS — the root manifest, packages/support/, packages/client/test-runtime/, website/, examples/, native/ — names it under dependencies or optionalDependencies. Section names alone are wrong in both directions: a test-support package declares vitest under dependencies without shipping it, and the bin/dsh launcher execs through tsx, which no manifest declares as a runtime dependency at all (the generator marks it runtime explicitly).

The runtime tier deliberately covers every mountable plugin, not just what the CLI, Web UI, and Python runtime load by default. scripts/install.sh installs the repository itself, so a user's cordis.yml can mount any plugin package; @modelcontextprotocol/sdk and the OpenTelemetry packages reach real users even though no default assembly imports them. Under-disclosure is the costly direction for a legal notice.

The manifest set is derived from the packages: members each pnpm-workspace.yaml declares — the root one and the nested Landlock workspace's — so a new member area is read the day it is declared rather than the day someone remembers to extend a list. License and repository metadata come from the installed pnpm stores, both the root one and the Landlock workspace's, so the generator requires an installed tree and fails loud when a package resolves to neither, rather than emitting an empty cell. OVERRIDES carries the packages whose published manifest cannot answer — Rust-built npm bins that omit license, and the modelcontextprotocol/servers packages whose repository is mid MIT→Apache-2.0 relicensing, so their effective terms are per-contribution. A runtime dependency whose license is not on the permissive list is a hard error: shipping copyleft is a distribution decision, not something a regenerated table may absorb silently. Vendored packages are cross-checked against vendor/README.md and rejected if any is not MIT, and pnpm-workspace.yaml's patchedDependencies are listed under the runtime table because pnpm applies those patches at install time — shipped artifacts carry modified copies of @earendil-works/pi-tui and node-pty, and the patch files are the record of what changed.

Testing

The same spec that asserts freshness pins the tiering rule against fixture manifests — including the two cases that motivate it, a dependencies entry of a test-support package and a plugin package no app mounts. It also pins the parsers against the shapes that would otherwise drop a package without a word: a vendor/README.md table that stops covering a vendored directory, a requirement array holding extras ("httpx[http2]"), a requirement with no version at all, an author-named [dependency-groups] table, and a workspace member area absent from any hardcoded list. Each of those is a silent-omission path, which is the failure mode a disclosure file cannot afford.

Alternatives considered

Keep the hand-written file and review it at release time. Reviewing a hundred derived rows by eye is exactly the work a generator does correctly, and the file's own claim — that it lists every direct dependency — would be unverified between releases.

Verify through a dedicated doc-sync gate. That is how every other generated artifact here is checked, and it was the first shape of this change. It costs a gate process and a scheduler slot in a matrix that is already long, and — worse — its only failure mode is telling a contributor, minutes after they pushed an unrelated dependency bump, to go rerun a generator. Regenerating at commit time removes the interruption, and the assertion inside a spec the test lane already runs keeps the guarantee at no additional CI cost.

Enumerate the full transitive closure. The closure is thousands of packages, already recorded in the lock files with exact versions, and would bury the direct dependencies that a reader actually evaluates. The file points at the lock files and the pnpm licenses list renderer instead.

Tier by manifest section (dependencies vs devDependencies). Mechanically simple and wrong on real data in both directions, as the tiering paragraph above records.

Tier by reachability from the shipped assemblies only (apps/* plus python/sdk-runtime). This produces a tighter runtime tier, but classifies the MCP client and the OpenTelemetry exporter as development-only even though a user running the installed repository can mount them. It understates the disclosure, which is the wrong direction to err for a legal notice.

Emit the notices as a bilingual pair. Every other root document is paired, but the file is a table of upstream package names, SPDX identifiers, and URLs; the translatable surface is a handful of section blurbs. scripts/translation-pairing.ts scopes discovery to README*, .agents/notes/**, docs/**, and python/**, so a root non-README file is outside the bilingual corpus by construction, and the README pair carries the bilingual entry points into it.

Consequences

A dependency edit now carries a regenerated notices file into the same commit. Contributors pay one generator run — about a second — on commits that touch a manifest, and nothing on any other commit. Committing with hooks disabled defers the cost to a test-lane failure that names the command.

The generator needs an installed tree, which makes it heavier than a pure-source generator, and a new package with unusable published metadata needs an OVERRIDES entry rather than silently rendering a blank license. Both failures are loud and name the remedy.

The tiering rule is a policy encoded in one constant. Adding a workspace area that never ships — a second test-infrastructure tier, another site — requires extending DEV_ONLY_AREAS, or its dependencies will be disclosed as runtime.