gen-third-party-notices.ts 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792
  1. /**
  2. * Generate `THIRD_PARTY_NOTICES.md` from the workspace manifests: every
  3. * external dependency named by a workspace `package.json`, the vendored-package
  4. * manifest in `vendor/README.md`, the Python `pyproject.toml` files, and the
  5. * pnpm patch list. License and repository metadata come from the installed
  6. * store, so the tree must be installed. `--check` verifies the committed
  7. * artifact. Tier policy and ownership live in
  8. * `.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md`.
  9. */
  10. import { existsSync, globSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
  11. import { dirname, resolve } from 'node:path'
  12. import * as yaml from 'js-yaml'
  13. import { parse as parseToml, type TomlTableWithoutBigInt, type TomlValueWithoutBigInt } from 'smol-toml'
  14. import parseSpdx from 'spdx-expression-parse'
  15. import { browserBundledExternals } from './browser-bundled-externals.ts'
  16. const root = resolve(import.meta.dirname, '..')
  17. const OUT = 'THIRD_PARTY_NOTICES.md'
  18. /** Dependency-declaration kinds a consumer resolves at runtime. */
  19. const RUNTIME_KINDS = ['dependencies', 'optionalDependencies'] as const
  20. /** All manifest sections that name an external package this file must disclose. */
  21. const ALL_KINDS = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'] as const
  22. /**
  23. * Workspace areas that never reach a user: repository tooling and gates (the
  24. * root manifest), test infrastructure, the documentation site, and the native
  25. * launcher's build workspace. A runtime
  26. * declaration by anything outside these areas is a disclosure-relevant
  27. * runtime dependency because any plugin package can be mounted from a user's
  28. * `cordis.yml`.
  29. */
  30. const DEV_ONLY_AREAS = [
  31. 'package.json',
  32. 'packages/test-support/',
  33. 'packages/test-support/client-runtime/',
  34. 'website/',
  35. 'native/',
  36. ] as const
  37. /** First-party public native packages: reachable at runtime but not third-party. */
  38. const FIRST_PARTY = new Set([
  39. '@deepseek-ai/node-addon-system',
  40. '@deepseek-ai/node-addon-system-darwin-arm64',
  41. '@deepseek-ai/node-addon-system-darwin-x64',
  42. '@deepseek-ai/node-addon-system-linux-arm64',
  43. '@deepseek-ai/node-addon-system-linux-x64',
  44. ])
  45. /** Official SDK identity covered by the project's narrow owner authorization. */
  46. export const CLAUDE_AGENT_SDK_PACKAGE = '@anthropic-ai/claude-agent-sdk'
  47. const CLAUDE_PLATFORM_PACKAGE_PREFIX = `${CLAUDE_AGENT_SDK_PACKAGE}-`
  48. const CLAUDE_PLATFORM_DECLARED_LICENSE = 'SEE LICENSE IN LICENSE.md'
  49. /**
  50. * Whether a non-permissive runtime declaration has an identity-scoped owner
  51. * authorization. This does not reclassify its terms as permissive.
  52. * @param name - exact npm package identity.
  53. * @returns true only for the official Claude Agent SDK package.
  54. */
  55. export function isOwnerAuthorizedRuntime(name: string): boolean {
  56. return name === CLAUDE_AGENT_SDK_PACKAGE
  57. }
  58. /**
  59. * Metadata overrides where the installed manifest is wrong or unreachable.
  60. * Each entry documents why the store cannot answer.
  61. */
  62. const OVERRIDES: Record<string, { license?: string; repo?: string }> = {
  63. // Rust workspaces publishing npm bins without `license` in package.json.
  64. 'oxlint': { license: 'MIT', repo: 'https://github.com/oxc-project/oxc' },
  65. 'oxlint-tsgolint': { license: 'MIT', repo: 'https://github.com/oxc-project/tsgolint' },
  66. // `license: SEE LICENSE IN LICENSE`: the servers repo is mid MIT→Apache-2.0
  67. // relicensing, so the effective terms are per-contribution.
  68. '@modelcontextprotocol/server-everything': { license: 'MIT / Apache-2.0', repo: 'https://github.com/modelcontextprotocol/servers' },
  69. '@modelcontextprotocol/server-filesystem': { license: 'MIT / Apache-2.0', repo: 'https://github.com/modelcontextprotocol/servers' },
  70. // No repository field in the published manifest.
  71. 'node-addon-require-builtin': { repo: 'https://www.npmjs.com/package/node-addon-require-builtin' },
  72. // No `license` field in the published manifest; the tarball's LICENSE.txt is the MIT text.
  73. }
  74. /**
  75. * Python dependencies are few and named directly in `pyproject.toml` files
  76. * without installed metadata to harvest, so license/repo are recorded here and
  77. * the generator fails when a manifest names a package this map misses.
  78. */
  79. const PYTHON_METADATA: Record<string, { license: string; repo: string; role: string }> = {
  80. pydantic: { license: 'MIT', repo: 'https://github.com/pydantic/pydantic', role: 'runtime dependency of `deepseek-harness-sdk`' },
  81. hatchling: { license: 'MIT', repo: 'https://github.com/pypa/hatch', role: 'build backend' },
  82. pytest: { license: 'MIT', repo: 'https://github.com/pytest-dev/pytest', role: 'test-only' },
  83. }
  84. type PythonMetadata = typeof PYTHON_METADATA
  85. /** The `package.json` fields this generator reads. */
  86. export interface Manifest {
  87. name?: string
  88. version?: string
  89. private?: boolean
  90. license?: string
  91. dependencies?: Record<string, string>
  92. devDependencies?: Record<string, string>
  93. optionalDependencies?: Record<string, string>
  94. peerDependencies?: Record<string, string>
  95. }
  96. /** One disclosed external npm dependency. */
  97. interface ExternalDep {
  98. name: string
  99. license: string
  100. repo: string
  101. /** True when some shipped workspace consumer reaches it through runtime dependency edges. */
  102. runtime: boolean
  103. }
  104. /** Read and parse a workspace-relative `package.json`. */
  105. function readManifest(rel: string): Manifest {
  106. return JSON.parse(readFileSync(resolve(root, rel), 'utf8')) as Manifest
  107. }
  108. /**
  109. * Manifest globs, derived from the workspace declarations rather than listed
  110. * here, so a new member area (`tools/*`) is read the day it is declared.
  111. * @returns one glob per manifest-bearing location, repository-relative.
  112. */
  113. export function manifestPatterns(rootMembers: readonly string[]): string[] {
  114. return [
  115. 'package.json',
  116. ...rootMembers.map(member => `${member}/package.json`),
  117. ]
  118. }
  119. /** The `packages:` member globs declared by one pnpm workspace file. */
  120. function workspaceMembers(rel: string): string[] {
  121. const declared = (yaml.load(readFileSync(resolve(root, rel), 'utf8')) as { packages?: unknown }).packages
  122. if (!Array.isArray(declared) || declared.length === 0) {
  123. throw new Error(`gen-third-party-notices: ${rel} declares no workspace members; the manifest set cannot be derived.`)
  124. }
  125. return declared.map(member => String(member))
  126. }
  127. /**
  128. * Every workspace manifest, keyed by repository-relative path, plus the set of
  129. * workspace package names. Paths are normalized to `/` at ingestion: Node's
  130. * `fs.globSync` returns OS-native separators, and the area matching in
  131. * `tierExternalDeps` compares `/`-suffixed prefixes, so Windows backslashes
  132. * would silently push dev-area manifests into the runtime tier.
  133. */
  134. function loadWorkspaceManifests(): { manifests: Map<string, Manifest>; names: Set<string> } {
  135. const patterns = manifestPatterns(workspaceMembers('pnpm-workspace.yaml'))
  136. const manifests = new Map<string, Manifest>()
  137. const names = new Set<string>()
  138. for (const pattern of patterns) {
  139. for (const path of globSync(pattern, { cwd: root })) {
  140. const normalized = path.replaceAll('\\', '/')
  141. const manifest = readManifest(normalized)
  142. manifests.set(normalized, manifest)
  143. if (manifest.name !== undefined) names.add(manifest.name)
  144. }
  145. }
  146. if (manifests.size < 100) throw new Error(`gen-third-party-notices: only ${manifests.size} workspace manifests found; the glob set is stale.`)
  147. return { manifests, names }
  148. }
  149. type VirtualManifest = Manifest & {
  150. claudeCodeVersion?: string
  151. license?: string
  152. repository?: string | { url?: string }
  153. homepage?: string
  154. }
  155. /** One platform payload declared by the official Claude Agent SDK. */
  156. export interface ClaudePlatformPayload {
  157. readonly name: string
  158. readonly version: string
  159. }
  160. /** Current SDK and CLI distribution facts derived from the installed SDK manifest. */
  161. export interface ClaudeDistribution {
  162. readonly sdkVersion: string
  163. readonly claudeCodeVersion: string
  164. readonly payloads: ClaudePlatformPayload[]
  165. }
  166. function requiredManifestString(
  167. value: string | undefined,
  168. field: string,
  169. ): string {
  170. if (value === undefined || value.length === 0) {
  171. throw new Error(`gen-third-party-notices: ${CLAUDE_AGENT_SDK_PACKAGE} has no ${field}.`)
  172. }
  173. return value
  174. }
  175. /**
  176. * Derive the official platform payload set without a version or platform
  177. * allowlist. Only identities in the SDK's own package namespace are covered.
  178. * @param manifest - installed official SDK manifest.
  179. * @returns current SDK, CLI, and optional platform payload facts.
  180. */
  181. export function claudeDistributionFromManifest(
  182. manifest: VirtualManifest,
  183. ): ClaudeDistribution {
  184. if (manifest.name !== CLAUDE_AGENT_SDK_PACKAGE) {
  185. throw new Error(
  186. `gen-third-party-notices: expected ${CLAUDE_AGENT_SDK_PACKAGE} manifest, got ${JSON.stringify(manifest.name)}.`,
  187. )
  188. }
  189. const sdkVersion = requiredManifestString(manifest.version, 'version')
  190. const claudeCodeVersion = requiredManifestString(
  191. manifest.claudeCodeVersion,
  192. 'claudeCodeVersion',
  193. )
  194. const entries = Object.entries(manifest.optionalDependencies ?? {})
  195. if (entries.length === 0) {
  196. throw new Error(
  197. `gen-third-party-notices: ${CLAUDE_AGENT_SDK_PACKAGE} declares no optional platform payloads.`,
  198. )
  199. }
  200. const payloads = entries.map(([name, version]) => {
  201. if (!name.startsWith(CLAUDE_PLATFORM_PACKAGE_PREFIX)) {
  202. throw new Error(
  203. `gen-third-party-notices: ${CLAUDE_AGENT_SDK_PACKAGE} optional dependency ${name} is outside its authorized platform-payload identity.`,
  204. )
  205. }
  206. return {
  207. name,
  208. version: requiredManifestString(version, `${name} optional dependency version`),
  209. }
  210. }).sort((left, right) => left.name.localeCompare(right.name))
  211. return { sdkVersion, claudeCodeVersion, payloads }
  212. }
  213. /**
  214. * Resolve one package's manifest inside a pnpm virtual store. The prefix scan
  215. * matches ordinary `@scope+name@version` directory names; pnpm 11 truncates
  216. * long names (a peer-suffixed name past the length limit becomes
  217. * `<prefix>_<hash>`), so a content scan falls back over the whole store when
  218. * the prefix misses.
  219. *
  220. * @param virtual - the `.pnpm` virtual store directory to scan.
  221. * @param name - the external package name, exactly as `node_modules` spells it.
  222. * @param expectedVersion - exact version required when the store retains more than one.
  223. * @returns the parsed manifest, or `undefined` when neither the prefix match
  224. * nor the content scan finds the requested package version.
  225. */
  226. export function virtualManifest(
  227. virtual: string,
  228. name: string,
  229. expectedVersion?: string,
  230. ): VirtualManifest | undefined {
  231. const prefix = `${name.replace('/', '+')}@`
  232. const entries = readdirSync(virtual)
  233. for (const entry of entries.filter(dir => dir.startsWith(prefix))) {
  234. const manifest = JSON.parse(readFileSync(resolve(virtual, entry, 'node_modules', name, 'package.json'), 'utf8')) as VirtualManifest
  235. if (expectedVersion === undefined || manifest.version === expectedVersion) return manifest
  236. }
  237. for (const dir of entries) {
  238. const candidate = resolve(virtual, dir, 'node_modules', name, 'package.json')
  239. if (existsSync(candidate)) {
  240. const manifest = JSON.parse(readFileSync(candidate, 'utf8')) as VirtualManifest
  241. if (expectedVersion === undefined || manifest.version === expectedVersion) return manifest
  242. }
  243. }
  244. return undefined
  245. }
  246. const workspaceLinkedManifestCache = new Map<string, VirtualManifest | undefined>()
  247. /**
  248. * Resolve the package version selected for a declaring workspace instead of an
  249. * unrelated historical version that still occupies the shared virtual store.
  250. * @param name - external package identity.
  251. * @param manifests - workspace manifests already loaded by the caller, so one
  252. * load serves every dependency instead of a full re-read per name.
  253. * @returns the first current workspace link for that package, when installed.
  254. */
  255. function workspaceLinkedManifest(name: string, manifests: Map<string, Manifest>): VirtualManifest | undefined {
  256. if (workspaceLinkedManifestCache.has(name)) return workspaceLinkedManifestCache.get(name)
  257. for (const [path, manifest] of manifests) {
  258. if (!ALL_KINDS.some(kind => name in (manifest[kind] ?? {}))) continue
  259. const linked = resolve(root, dirname(path), 'node_modules', name, 'package.json')
  260. if (!existsSync(linked)) continue
  261. const found = JSON.parse(readFileSync(linked, 'utf8')) as VirtualManifest
  262. workspaceLinkedManifestCache.set(name, found)
  263. return found
  264. }
  265. workspaceLinkedManifestCache.set(name, undefined)
  266. return undefined
  267. }
  268. /** Resolve one installed external package manifest from either pnpm store. */
  269. function installedManifest(name: string, manifests: Map<string, Manifest>, expectedVersion?: string): VirtualManifest | undefined {
  270. const linked = workspaceLinkedManifest(name, manifests)
  271. if (linked !== undefined && (expectedVersion === undefined || linked.version === expectedVersion)) return linked
  272. let manifest: (Manifest & { license?: string; repository?: string | { url?: string }; homepage?: string }) | undefined
  273. // Workspace-local link farms can expose a dependency that is not linked at
  274. // the repository root; both are backed by the root workspace's lockfile.
  275. for (const store of ['node_modules', 'native/system/node_modules']) {
  276. const direct = resolve(root, store, name, 'package.json')
  277. if (existsSync(direct)) {
  278. const candidate = JSON.parse(readFileSync(direct, 'utf8')) as typeof manifest
  279. if (expectedVersion === undefined || candidate?.version === expectedVersion) {
  280. manifest = candidate
  281. break
  282. }
  283. }
  284. const virtual = resolve(root, store, '.pnpm')
  285. if (!existsSync(virtual)) continue
  286. manifest = virtualManifest(virtual, name, expectedVersion)
  287. if (manifest !== undefined) break
  288. }
  289. return manifest
  290. }
  291. /** License and repository URL for an installed external package, from the pnpm store. */
  292. function installedMetadata(name: string, manifests: Map<string, Manifest>): { license: string; repo: string } {
  293. const override = OVERRIDES[name]
  294. const manifest = installedManifest(name, manifests)
  295. const license = override?.license ?? manifest?.license
  296. const rawRepo = typeof manifest?.repository === 'string' ? manifest.repository : manifest?.repository?.url ?? manifest?.homepage
  297. const repo = override?.repo ?? normalizeRepo(rawRepo)
  298. if (license === undefined || repo === undefined) {
  299. throw new Error(`gen-third-party-notices: cannot resolve ${license === undefined ? 'license' : 'repository'} for ${name}; run \`pnpm install\`, or add an OVERRIDES entry.`)
  300. }
  301. return { license, repo }
  302. }
  303. function collectClaudeDistribution(manifests: Map<string, Manifest>): ClaudeDistribution {
  304. const manifest = installedManifest(CLAUDE_AGENT_SDK_PACKAGE, manifests)
  305. if (manifest === undefined) {
  306. throw new Error(
  307. `gen-third-party-notices: cannot resolve ${CLAUDE_AGENT_SDK_PACKAGE}; run \`pnpm install\`.`,
  308. )
  309. }
  310. const distribution = claudeDistributionFromManifest(manifest)
  311. let installedPayloads = 0
  312. for (const payload of distribution.payloads) {
  313. const installed = installedManifest(payload.name, manifests, payload.version)
  314. if (installed === undefined) continue
  315. installedPayloads += 1
  316. if (
  317. installed.name !== payload.name
  318. || installed.version !== payload.version
  319. || installed.license !== CLAUDE_PLATFORM_DECLARED_LICENSE
  320. ) {
  321. throw new Error(
  322. `gen-third-party-notices: installed ${payload.name} does not match its SDK-declared version and ${CLAUDE_PLATFORM_DECLARED_LICENSE} license field.`,
  323. )
  324. }
  325. }
  326. if (installedPayloads === 0) {
  327. throw new Error(
  328. 'gen-third-party-notices: no SDK-declared Claude platform payload is installed; install optional dependencies before regenerating.',
  329. )
  330. }
  331. return distribution
  332. }
  333. /** Normalize a manifest repository/homepage value to a browsable https URL. */
  334. function normalizeRepo(raw: string | undefined): string | undefined {
  335. if (raw === undefined || raw === '') return undefined
  336. let url = raw
  337. .replace(/^git\+ssh:\/\/git@/, 'https://')
  338. .replace(/^git\+/, '')
  339. .replace(/^git:\/\//, 'https://')
  340. .replace(/^github:/, 'https://github.com/')
  341. .replace(/\.git$/, '')
  342. if (!url.startsWith('http')) url = `https://github.com/${url}`
  343. return url
  344. }
  345. /**
  346. * Direct npm dependencies distributed through installed runtime libraries or
  347. * browser builds. Tooling declarations alone do not imply distribution.
  348. */
  349. function collectNpmDeps(manifests: Map<string, Manifest>, names: Set<string>, browser: ReadonlySet<string>): ExternalDep[] {
  350. return [...tierExternalDeps(manifests, names, browser)]
  351. .filter(([name]) => !FIRST_PARTY.has(name))
  352. .sort(([a], [b]) => a.localeCompare(b))
  353. .map(([name, runtime]) => ({ name, ...installedMetadata(name, manifests), runtime }))
  354. }
  355. /**
  356. * Tier every external dependency the workspace declares.
  357. * @param manifests - workspace manifests keyed by repository-relative path.
  358. * @param names - every workspace package name, which never counts as external.
  359. * @param browser - Direct third-party packages resolved by the browser builds.
  360. * @returns each external package mapped to whether it is a runtime dependency.
  361. */
  362. export function tierExternalDeps(
  363. manifests: Map<string, Manifest>, names: Set<string>, browser: ReadonlySet<string> = new Set(),
  364. ): Map<string, boolean> {
  365. const tiers = new Map<string, boolean>()
  366. // `tsx` is runtime by fiat: the root source-run scripts execute through its ESM hook.
  367. tiers.set('tsx', true)
  368. for (const [path, manifest] of manifests) {
  369. const devOnly = DEV_ONLY_AREAS.some(area => (area.endsWith('/') ? path.startsWith(area) : path === area))
  370. for (const kind of ALL_KINDS) {
  371. for (const [dep, range] of Object.entries(manifest[kind] ?? {})) {
  372. if (names.has(dep) || range.startsWith('workspace:')) continue
  373. const runtime = browser.has(dep) || !devOnly && (RUNTIME_KINDS as readonly string[]).includes(kind)
  374. tiers.set(dep, (tiers.get(dep) ?? false) || runtime)
  375. }
  376. }
  377. }
  378. for (const name of browser) {
  379. if (!names.has(name) && !tiers.has(name)) throw new Error(`gen-third-party-notices: browser package ${name} has no workspace dependency declaration`)
  380. }
  381. return tiers
  382. }
  383. /** A vendored package row parsed out of the `vendor/README.md` manifest table. */
  384. export interface VendoredRow {
  385. npmName: string
  386. /** The name this package carries upstream; MIT attribution names the fork's origin, not our scope. */
  387. upstreamName: string
  388. upstream: string
  389. }
  390. /**
  391. * Parse the vendored-package manifest table out of `vendor/README.md`.
  392. * @param text - the complete `vendor/README.md` contents.
  393. * @returns one row per manifest-table entry, in table order.
  394. */
  395. export function parseVendoredRows(text: string): VendoredRow[] {
  396. const rows: VendoredRow[] = []
  397. for (const line of text.split('\n')) {
  398. const match = new RegExp(String.raw`^\| \x60\S+\/\x60 \| \x60([^\x60]+)\x60 \| \x60([^\x60]+)\x60 \| \S+ \| `
  399. + String.raw`(https:\/\/\S+?)(?: \([^)]*\))? \| \x60[0-9a-f]+\x60 \|$`).exec(line)
  400. if (match === null) continue
  401. const [, npmName, upstreamName, upstream] = match
  402. if (npmName === undefined || upstreamName === undefined || upstream === undefined) continue
  403. rows.push({ npmName, upstreamName, upstream })
  404. }
  405. return rows
  406. }
  407. /**
  408. * Parse the vendored manifest table and confirm it accounts for every vendored
  409. * directory. The `vendor/` tree — not the table — is the set that must be
  410. * disclosed, so a row that stops matching the table format is a hard error
  411. * rather than a package that quietly vanishes from the notices.
  412. */
  413. function collectVendored(): VendoredRow[] {
  414. const rows = parseVendoredRows(readFileSync(resolve(root, 'vendor/README.md'), 'utf8'))
  415. const onDisk = new Map<string, string>()
  416. for (const entry of readdirSync(resolve(root, 'vendor'), { withFileTypes: true })) {
  417. if (!entry.isDirectory()) continue
  418. const manifest = readManifest(`vendor/${entry.name}/package.json`)
  419. if (manifest.name !== undefined) onDisk.set(manifest.name, entry.name)
  420. }
  421. const parsed = new Set(rows.map(row => row.npmName))
  422. const missing = [...onDisk.keys()].filter(name => !parsed.has(name))
  423. if (missing.length > 0) {
  424. throw new Error(`gen-third-party-notices: vendor/README.md has no manifest-table row for ${missing.join(', ')}; its table format changed or the sync is incomplete.`)
  425. }
  426. for (const row of rows) {
  427. const dir = onDisk.get(row.npmName)
  428. if (dir === undefined) throw new Error(`gen-third-party-notices: vendored package ${row.npmName} from vendor/README.md has no vendor/ directory.`)
  429. const license = readManifest(`vendor/${dir}/package.json`).license
  430. if (license !== 'MIT') {
  431. throw new Error(`gen-third-party-notices: vendored ${row.npmName} declares license ${JSON.stringify(license)}; the vendored section assumes MIT throughout.`)
  432. }
  433. }
  434. return rows
  435. }
  436. /** Whether a parsed TOML value is a table rather than an array or scalar. */
  437. function isTomlTable(value: TomlValueWithoutBigInt | undefined): value is TomlTableWithoutBigInt {
  438. return value !== undefined && typeof value === 'object' && !Array.isArray(value)
  439. }
  440. /** Parse one PEP 508 requirement string into its distribution name. */
  441. function parsePythonRequirement(requirement: string): string {
  442. const name = /^\s*([a-zA-Z][a-zA-Z0-9._-]*)\s*(?:\[[^\]]*\])?\s*(?:[<>=!~;@].*)?$/.exec(requirement)?.[1]
  443. if (name === undefined) {
  444. throw new Error(`gen-third-party-notices: cannot read a distribution name from the requirement ${JSON.stringify(requirement)}.`)
  445. }
  446. return name
  447. }
  448. /** Add the string requirements from one parsed TOML array. */
  449. function collectPythonRequirementArray(
  450. names: string[],
  451. value: TomlValueWithoutBigInt | undefined,
  452. location: string,
  453. allowGroupIncludes = false,
  454. ): void {
  455. if (value === undefined) return
  456. if (!Array.isArray(value)) {
  457. throw new Error(`gen-third-party-notices: ${location} must be an array.`)
  458. }
  459. for (const item of value) {
  460. if (typeof item === 'string') {
  461. names.push(parsePythonRequirement(item))
  462. continue
  463. }
  464. if (allowGroupIncludes && isTomlTable(item) && typeof item['include-group'] === 'string' && Object.keys(item).length === 1) {
  465. continue
  466. }
  467. throw new Error(`gen-third-party-notices: ${location} contains an unsupported requirement entry.`)
  468. }
  469. }
  470. /** Read an optional TOML table and reject a present non-table value. */
  471. function optionalTomlTable(value: TomlValueWithoutBigInt | undefined, location: string): TomlTableWithoutBigInt | undefined {
  472. if (value === undefined || isTomlTable(value)) return value
  473. throw new Error(`gen-third-party-notices: ${location} must be a table.`)
  474. }
  475. /**
  476. * Parse a `pyproject.toml` project identity and every requirement it declares:
  477. * `requires` under
  478. * `[build-system]`, `dependencies` under `[project]`, and every key under
  479. * `[project.optional-dependencies]` and `[dependency-groups]`. A TOML parser
  480. * owns comments, quoted keys, escapes, and array boundaries; unsupported
  481. * requirement forms fail instead of disappearing from the notices.
  482. * @param text - the complete `pyproject.toml` contents.
  483. * @returns the local project name and declared requirement names.
  484. */
  485. function parsePyproject(text: string): { projectName?: string; requirements: string[] } {
  486. const names: string[] = []
  487. const document = parseToml(text, { integersAsBigInt: false })
  488. const buildSystem = optionalTomlTable(document['build-system'], '[build-system]')
  489. const project = optionalTomlTable(document.project, '[project]')
  490. const projectName = project?.name
  491. if (projectName !== undefined && typeof projectName !== 'string') {
  492. throw new Error('gen-third-party-notices: [project].name must be a string.')
  493. }
  494. collectPythonRequirementArray(names, buildSystem?.requires, '[build-system].requires')
  495. collectPythonRequirementArray(names, project?.dependencies, '[project].dependencies')
  496. const optional = optionalTomlTable(project?.['optional-dependencies'], '[project.optional-dependencies]')
  497. for (const [group, requirements] of Object.entries(optional ?? {})) {
  498. collectPythonRequirementArray(names, requirements, `[project.optional-dependencies].${group}`)
  499. }
  500. const groups = optionalTomlTable(document['dependency-groups'], '[dependency-groups]')
  501. for (const [group, requirements] of Object.entries(groups ?? {})) {
  502. collectPythonRequirementArray(names, requirements, `[dependency-groups].${group}`, true)
  503. }
  504. return projectName === undefined
  505. ? { requirements: names }
  506. : { projectName, requirements: names }
  507. }
  508. /**
  509. * Read every requirement name declared by one `pyproject.toml`.
  510. * @param text - the complete `pyproject.toml` contents.
  511. * @returns each declared requirement's distribution name, in file order.
  512. */
  513. export function parsePyprojectRequirements(text: string): string[] {
  514. return parsePyproject(text).requirements
  515. }
  516. /** Normalize a Python distribution name according to the packaging name rule. */
  517. function normalizePythonDistributionName(name: string): string {
  518. return name.toLowerCase().replace(/[-_.]+/g, '-')
  519. }
  520. /**
  521. * Resolve external Python dependencies after excluding local project names.
  522. * @param pyprojects - complete local `pyproject.toml` contents.
  523. * @param metadata - disclosure metadata for every external dependency.
  524. * @returns disclosed dependencies in normalized name order.
  525. */
  526. export function collectPythonDependencies(
  527. pyprojects: string[],
  528. metadata: PythonMetadata = PYTHON_METADATA,
  529. ): { name: string; license: string; repo: string; role: string }[] {
  530. const parsed = pyprojects.map(parsePyproject)
  531. const firstParty = new Set(parsed.flatMap(({ projectName }) => (
  532. projectName === undefined ? [] : [normalizePythonDistributionName(projectName)]
  533. )))
  534. const found = new Set(parsed
  535. .flatMap(({ requirements }) => requirements.map(normalizePythonDistributionName))
  536. .filter(name => !firstParty.has(name)))
  537. return [...found].sort((a, b) => a.localeCompare(b)).map((name) => {
  538. const entry = metadata[name]
  539. if (entry === undefined) throw new Error(`gen-third-party-notices: python dependency ${name} is missing from PYTHON_METADATA.`)
  540. return { name, ...entry }
  541. })
  542. }
  543. /** Direct Python dependencies named by the `pyproject.toml` manifests under `python/`. */
  544. function collectPython(): { name: string; license: string; repo: string; role: string }[] {
  545. const manifests = globSync('python/*/pyproject.toml', { cwd: root })
  546. if (manifests.length === 0) throw new Error('gen-third-party-notices: no python/*/pyproject.toml found; the Python tree moved.')
  547. return collectPythonDependencies(manifests.map(path => readFileSync(resolve(root, path), 'utf8')))
  548. }
  549. /** pnpm-patched external packages, from `pnpm-workspace.yaml`. */
  550. function collectPatched(): { spec: string; patch: string }[] {
  551. const workspace = yaml.load(readFileSync(resolve(root, 'pnpm-workspace.yaml'), 'utf8')) as { patchedDependencies?: Record<string, string> }
  552. return Object.entries(workspace.patchedDependencies ?? {}).map(([spec, patch]) => ({ spec, patch }))
  553. }
  554. /** SPDX identifiers this project may ship without further review. */
  555. const PERMISSIVE_LICENSES = new Set(['MIT', 'ISC', 'BSD-2-Clause', 'BSD-3-Clause', 'Apache-2.0', '0BSD', 'Unlicense', 'CC0-1.0', 'BlueOak-1.0.0', 'Python-2.0'])
  556. /** Evaluate a parsed SPDX expression under the repository's license policy. */
  557. function isPermissiveSpdx(expression: ReturnType<typeof parseSpdx>): boolean {
  558. if ('conjunction' in expression) {
  559. return expression.conjunction === 'and'
  560. ? isPermissiveSpdx(expression.left) && isPermissiveSpdx(expression.right)
  561. : isPermissiveSpdx(expression.left) || isPermissiveSpdx(expression.right)
  562. }
  563. return expression.plus !== true
  564. && expression.exception === undefined
  565. && PERMISSIVE_LICENSES.has(expression.license)
  566. }
  567. /**
  568. * Whether an SPDX expression grants terms this project may ship under.
  569. * `OR` needs one permissive alternative, because the consumer chooses; `AND`
  570. * needs all of them, because every obligation applies. Anything that is not a
  571. * recognized permissive identifier — copyleft, an exception clause, or a
  572. * license this list has never seen — evaluates to false, so an unfamiliar
  573. * expression fails closed rather than passing on a partial match.
  574. * @param license - the SPDX expression from the package manifest.
  575. * @returns true when the expression's obligations are all permissive.
  576. */
  577. export function isPermissive(license: string): boolean {
  578. // Some npm manifests use a slash for a choice despite SPDX requiring `OR`.
  579. const normalized = license.replace(/\s*\/\s*/g, ' OR ').trim()
  580. try {
  581. return isPermissiveSpdx(parseSpdx(normalized))
  582. } catch {
  583. return false
  584. }
  585. }
  586. /**
  587. * Reject unapproved non-permissive licenses on installed or browser-bundled code.
  588. * @param dependencies - Disclosed runtime package identities and declared licenses.
  589. * @throws When a runtime package has no permissive license or exact owner authorization.
  590. */
  591. export function assertRuntimeLicenses(dependencies: readonly { name: string; license: string }[]): void {
  592. const rejected = dependencies.filter(dep => !isPermissive(dep.license) && !isOwnerAuthorizedRuntime(dep.name))
  593. if (rejected.length > 0) {
  594. throw new Error(`gen-third-party-notices: runtime ${rejected.map(dep => `${dep.name} (${dep.license})`).join(', ')} is not a permissive license; review the distribution terms and record the decision before regenerating.`)
  595. }
  596. }
  597. /**
  598. * Render the sentence that isolates non-permissive development tooling, or
  599. * nothing at all when every development dependency is permissive.
  600. * @param deps - development dependencies whose license is not permissive.
  601. * @returns the paragraph to place after the development table.
  602. */
  603. function renderNonPermissiveNote(deps: ExternalDep[]): string {
  604. if (deps.length === 0) return ''
  605. const named = deps.map(dep => `\`${dep.name}\` (${dep.license})`)
  606. const subject = named.length === 1 ? named[0] : `${named.slice(0, -1).join(', ')} and ${named.at(-1)}`
  607. return `\n${subject} ${named.length === 1 ? 'runs' : 'run'} only as development tooling; their code is not linked into or distributed with any DeepSeek Harness artifact.\n`
  608. }
  609. /** Render one npm dependency table. */
  610. function renderNpmTable(deps: ExternalDep[]): string {
  611. const lines = ['| Package | License |', '| --- | --- |']
  612. for (const dep of deps) lines.push(`| [\`${dep.name}\`](${dep.repo}) | ${dep.license} |`)
  613. return lines.join('\n')
  614. }
  615. function renderClaudeDistribution(
  616. distribution: ClaudeDistribution | undefined,
  617. ): string {
  618. if (distribution === undefined) return ''
  619. const rows = distribution.payloads.map(payload =>
  620. `| [\`${payload.name}\`](https://www.npmjs.com/package/${payload.name}) | ${payload.version} | ${CLAUDE_PLATFORM_DECLARED_LICENSE} |`,
  621. )
  622. return `
  623. ## Official Claude Code platform payloads
  624. The project owner authorizes distribution of every version of the official \`${CLAUDE_AGENT_SDK_PACKAGE}\` package and the official Claude Code CLI/platform payloads that each version declares through \`optionalDependencies\`. This identity-scoped authorization does not classify their declared terms as permissive and does not cover any unrelated runtime package; version, declared-license, and payload-set changes still require the ordinary dependency, lockfile, compatibility, terms, and notices review.
  625. The installed SDK ${distribution.sdkVersion} declares the following optional platform packages. Each carries the official Claude Code ${distribution.claudeCodeVersion} executable; the package identities and versions come from the SDK manifest, while the declared license field is verified against the platform payload installed for the current host.
  626. | Optional platform package | Version | Declared license |
  627. | --- | --- | --- |
  628. ${rows.join('\n')}
  629. `
  630. }
  631. /**
  632. * Render the complete notices document.
  633. * @returns The exact bytes THIRD_PARTY_NOTICES.md must hold after resolving browser inputs.
  634. */
  635. export async function render(): Promise<string> {
  636. const browser = await browserBundledExternals(root)
  637. // The linked-manifest cache is keyed by name only, so it must not outlive
  638. // the manifests map it was resolved from; render() owns that single load.
  639. workspaceLinkedManifestCache.clear()
  640. const { manifests, names } = loadWorkspaceManifests()
  641. const npm = collectNpmDeps(manifests, names, browser)
  642. const runtimeDeps = npm.filter(dep => dep.runtime)
  643. const devDeps = npm.filter(dep => !dep.runtime)
  644. const vendored = collectVendored()
  645. const python = collectPython()
  646. const patched = collectPatched()
  647. const claudeDistribution = runtimeDeps.some(
  648. dep => dep.name === CLAUDE_AGENT_SDK_PACKAGE,
  649. )
  650. ? collectClaudeDistribution(manifests)
  651. : undefined
  652. const nonPermissiveDev = devDeps.filter(dep => !isPermissive(dep.license))
  653. assertRuntimeLicenses(runtimeDeps)
  654. const patchedLines = patched.map(({ spec, patch }) => `- \`${spec}\` — [\`${patch}\`](${patch})`)
  655. return `<!-- Generated by scripts/gen-third-party-notices.ts — do not edit by hand.
  656. Run \`pnpm run gen-third-party-notices\` to regenerate. -->
  657. # Third-Party Notices
  658. DeepSeek Harness is licensed under [MIT](LICENSE). It depends on the third-party software listed below. Each project remains under its own license; nothing in this file changes those terms.
  659. This file lists **direct** dependencies declared by the workspace and the explicitly disclosed official Claude Code platform payload closure. It is generated from the workspace manifests by \`scripts/gen-third-party-notices.ts\`: a pre-commit hook regenerates it whenever a staged file changes one of its inputs, and \`scripts/gen-third-party-notices.spec.ts\` asserts in the test lane that the committed bytes match. Deleting a manifest runs no hook, so that case is caught by the assertion instead. Run \`pnpm run verify-third-party-notices\` for the standalone check.
  660. The complete npm transitive closure, including the Landlock launcher workspace, is recorded with exact pinned versions in [\`pnpm-lock.yaml\`](pnpm-lock.yaml) — inspect it with \`pnpm licenses list\`. The Python closure is recorded separately in [\`python/sdk/uv.lock\`](python/sdk/uv.lock).
  661. ## Vendored source (\`vendor/\`)
  662. The Cordis framework and its foundation libraries are source-vendored into this repository rather than consumed from npm, and republished under the \`@deepseek-ai\` scope. All are MIT-licensed; each directory preserves its upstream \`LICENSE\` file. Exact upstream commits and local modifications are recorded in [\`vendor/README.md\`](vendor/README.md).
  663. | Package | Upstream name | Upstream | License |
  664. | --- | --- | --- | --- |
  665. ${vendored.map(row => `| \`${row.npmName}\` | \`${row.upstreamName}\` | [${row.upstream.replace('https://', '')}](${row.upstream}) | MIT |`).join('\n')}
  666. ## Runtime npm dependencies
  667. External packages installed for runtime use or distributed inside the prebuilt browser artifacts. Browser inputs are resolved through the shipping tsdown and Vite configurations, independently of npm dependency sections. The tier covers every plugin a user can mount from \`cordis.yml\` — not only what the \`dsh\` CLI, Web UI, and Python SDK runtime load by default.
  668. ${renderNpmTable(runtimeDeps)}
  669. pnpm applies local patches to the following packages at install time, so shipped artifacts carry modified copies; each patch file is the complete record of the modification:
  670. ${patchedLines.join('\n')}
  671. ${renderClaudeDistribution(claudeDistribution)}
  672. ## Development-only npm dependencies
  673. External packages **directly declared** for development, tests, types, or tooling, without a runtime installation or browser-build relationship. A package here may still be pulled in transitively by a runtime dependency — \`pnpm-lock.yaml\` is the authority on that full closure.
  674. ${renderNpmTable(devDeps)}
  675. ${renderNonPermissiveNote(nonPermissiveDev)}
  676. ## Python SDK dependencies (\`python/\`)
  677. Direct dependencies of the \`pyproject.toml\` manifests, plus \`uv\` as the development workflow tool.
  678. | Package | License | Role |
  679. | --- | --- | --- |
  680. ${python.map(dep => `| [\`${dep.name}\`](${dep.repo}) | ${dep.license} | ${dep.role} |`).join('\n')}
  681. | [\`uv\`](https://github.com/astral-sh/uv) | MIT / Apache-2.0 | development workflow tool |
  682. ## First-party native packages
  683. \`@deepseek-ai/node-addon-system\` (and its platform packages) is built and released from this repository under BSD 3-Clause. It is listed here for completeness; it is first-party, not third-party.
  684. `
  685. }
  686. /** CLI entry: default writes the notices, `--check` fails if the committed copy
  687. * is stale. Guarded behind an entry-point check so importing this module for
  688. * tests neither regenerates the committed file nor calls process.exit. */
  689. async function main(): Promise<void> {
  690. const content = await render()
  691. if (process.argv.includes('--check')) {
  692. let committed: string | null = null
  693. try {
  694. committed = readFileSync(resolve(root, OUT), 'utf8')
  695. } catch {
  696. // Only ENOENT (not yet generated) is expected; a present-but-unreadable
  697. // file is not a state this repo produces, and the remedy is the same.
  698. committed = null
  699. }
  700. if (committed === content) {
  701. console.log(`gen-third-party-notices: ${OUT} is up to date.`)
  702. process.exit(0)
  703. }
  704. console.error(`gen-third-party-notices: ${OUT} is stale. Run \`pnpm run gen-third-party-notices\` and commit ${OUT}.`)
  705. process.exit(1)
  706. }
  707. writeFileSync(resolve(root, OUT), content)
  708. console.log(`gen-third-party-notices: wrote ${OUT}.`)
  709. }
  710. if (process.argv[1] !== undefined && import.meta.filename === resolve(process.argv[1])) {
  711. await main()
  712. }