Repository navigation
v0.13.0
Track T Phase 3: trust evidence reaches agents in one place and goes into the evidence contract, with three new checks that each hold or fail against a cited source.
- Known-malicious packages: OSV
MAL-records for any PyPI or npm name are the firstCONFIRMED_MALICIOUSanswers, always with the record cited. - Dependency confusion: checked against your own private-name list.
- Starjacking: repository linkage checks whether a package is really from the repository it claims.
- One
trustblock independency_status, with the agent's stated reasons (intent,name_source) recorded but never used. - Evidence contract 2.2.0:
openpulse attest --trustrecords the trust checks intrust_assurance. - Opt-in local recording of agent questions and answers (
openpulse mcp --record), for research on how agents choose dependencies.
Verdicts and the frozen evaluation are unchanged. Two upgrade notes:
attestnow emits contract 2.2.0;- agents should read trust results from
dependency_status'strust.checks. The v0.12 top-level copies stay until v0.14.0.
Upgrade notes
openpulse attestemits evidence contract 2.2.0 (v0.13.0 item X5). It is 2.1 plus one required field,trust_assurance, which is null unless you pass--trust. Validate againstschemas/evidence-contract/v2.2/schema.json; a 2.1 validator rejects the new field. The hash rule is unchanged. The 2.1 schema stays published byte-for-byte.dependency_statusgathers its trust checks into onetrustblock (v0.13.0 item X4). Read them fromtrust.checks. The v0.12.0 top-level fieldsregistry_existence,provenanceandlookalikeare still there with the same values, listed underdeprecated, and are removed in v0.14.0. The checks added in v0.13.0 (malicious,dependency_confusion,repository_linkage) are only intrust.checks. File checks keep their owntrustblock unchanged. Verdicts are unchanged.
Added
-
Opt-in local recording of agent questions and answers (M8, v0.13.0 item X6).
openpulse mcp --record DIRappends one JSON line per tool call toDIR/mcp-<session>.jsonl. Each line holds the tool, its arguments, the outcome and a research summary: verdict, trust statuses, and the agent'sintentandname_source. The full answer is included unless it is over 256 KB. It is off by default and local only: nothing is sent anywhere. Recording never changes an answer, a write failure stops recording without failing a tool, and on POSIX the files are owner-only. This is the data source for the track T research questions on how agents choose dependencies (RQ1, RQ4). -
Evidence contract 2.2.0 with
trust_assurance(track T, T-F2, v0.13.0 item X5).openpulse attest --trustchecks each PyPI and npm dependency once per run and records it in every contract for that dependency:- The checks: malicious records, registry existence, provenance, repository linkage, dependency confusion and lookalike.
- Record shape: each record is fixed: check, T-F1 status, a fixed statement,
checked_atand the https URIs it cites. A summary counts every status, andused_in_verdictis alwaysfalse. Free-form evidence stays out, so the public shape is stable. - Hash: the JCS content hash covers the block, so
verify.mjschecks it with no change. - Budget: at most 4 lookups per dependency and 2000 per run.
--private-namesnames the private-name list (default.openpulse/private-packages.txtwhen present). - Never part of a verdict: assessments are identical with and without
--trust(tested). - Schema:
schemas/evidence-contract/v2.2/is derived from 2.1, which stays untouched, and its bytes are now pinned by a test.
-
One trust block, and the agent's reasons (track T, T-D, v0.13.0 item X4).
dependency_statusanswers carrytrust:checks: every trust record (malicious,registry_existence,provenance,repository_linkage,dependency_confusion,lookalike);summary: counts per status;attention: eachCONFIRMED_MALICIOUS,VERIFIED_FAILUREorSUSPICIOUS_INCONCLUSIVErecord, with its check and statement;time_frame: the records are live lookups at theirchecked_at, and in replay mode it says they are not as of the replay date.selection_context: agents may say why they ask.intentis a short sentence, stripped of control, bidi and zero-width characters and capped at 280 characters.name_sourceis one ofuser_request,existing_code,documentation,search_result,model_suggestionorother; any other value is recorded asunrecognized, not echoed. Both are recorded assupplied_by: agent,used_in_status: false, and never change a status or verdict (tested). This is the input for the M8 research questions on how agents choose dependencies.
-
Repository linkage in
dependency_statustrust.checks(track T, T-A4, v0.13.0 item X3). Registry metadata can claim any repository: npmrepository, PyPIproject_urls. A package that claims a popular one borrows its stars ("starjacking", threat T4). The newrepository_linkagerecord checks whether the claim holds in either direction:- Verified PyPI provenance settles it, without reading GitHub. If it names the claimed repository as publisher, the result is
VERIFIED_RELATIONSHIP. If it names a different one, the result isVERIFIED_FAILURE: the artifact was not built from the repository it claims. - Otherwise the claimed repository's manifest is read at its default branch (
package.json, following npm'srepository.directory, orpyproject.tomlthensetup.cfg). If it names the package back, the result isVERIFIED_RELATIONSHIP. If it names a different package, the result isSUSPICIOUS_INCONCLUSIVE, an indicator only, since monorepos and renames do this too. MISSING_EVIDENCE: no GitHub repository is claimed, or the repository has no readable manifest.NOT_PERFORMED: registry or GitHub trouble, offline, a spent budget, or a package on the private-name list.- Bounds and parsing: one budgeted lookup per package, cached per session. Manifests are parsed and never executed (
setup.pyis never read), and capped at 1 MB. Repository names, directories and the manifest's name are validated before they appear. GitHub repositories only. - Trust block: file checks run it for every non-private package in
trust. Never part of a verdict (tested).
- Verified PyPI provenance settles it, without reading GitHub. If it names the claimed repository as publisher, the result is
-
Dependency confusion against your private-name list (track T, T-A3, v0.13.0 item X2). List your internal package names in
.openpulse/private-packages.txt, oneecosystem:nameper line (PyPI:acme-billing,npm:@acme/ui-kit). OpenPulse asks the public registry about those names, and only those.SUSPICIOUS_INCONCLUSIVE: a public package has the name. If you did not publish it, an installer that also consults the public registry may resolve it instead of yours.MISSING_EVIDENCE: no public package has the name. It is unclaimed, which also means anyone can still register it.NOT_PERFORMED: the registry gave no usable answer, offline, budget spent, or the package is not on the list (or there is no list).- MCP:
dependency_statusgains adependency_confusionrecord intrust.checks, and the server reads the list from its project root on every call. A listed package skips the public-registryregistry_existenceandprovenancechecks, which describe someone else's package, so a private dependency no longer reads as a hallucinated name.maliciousandlookalikestill run. - Trust block:
check_dependency_fileruns it for listed packages intrust. - CLI:
openpulse confusion [LIST]checks every listed name, andopenpulse trust --private-names LISTuses a list (the default file when present). Both are report-only. - Validation: names are validated against the registry grammars, PyPI spellings are normalized (PEP 503), and a bad line is skipped by line number without echoing its text. The list is capped at 500 names and 256 KB. Never part of a verdict (tested).
-
Known-malicious records in
dependency_statustrust.checks(v0.13.0 item X1). For any PyPI or npm package, catalogued or not, the newmaliciousrecord asks OSV whether it publishes a malicious-package record (MAL-, the OpenSSF malicious-packages feed) for that package and version.CONFIRMED_MALICIOUScites each record by id and osv.dev URL (10 at most, with a total). This is the first check that can return this status, and only from a cited record, never from OpenPulse's own indicators. A record listed only under an alias counts; a withdrawn record does not.MISSING_EVIDENCE: no such record. The feed covers reported packages only, so this is not evidence of safety. There is no "verified" outcome.NOT_PERFORMED: OSV gave no usable answer, paged its answer without a record on the first page, offline, or a spent budget.- Trust checks:
check_dependency_fileruns it too, on added dependencies or withtrust_all, and lists a hit undertrust.attention. So doesopenpulse trust. - Bounds: one budgeted lookup per package, cached per session (failures never cached), and a capped response. Record ids are validated before they appear, and no advisory text is echoed. Never part of a verdict (tested).
— 2026-11-02
Track T Phase 1: for PyPI and npm, MCP answers now carry checks on whether a dependency can be trusted, so an AI coding agent sees them before it adds or changes one.
- Registry digests: locked digests are compared with what the registry publishes.
- PyPI provenance: attestations are verified with the new optional
[provenance]extra. - Lookalike names: names that resemble popular packages are flagged as indicators.
- Added dependencies: a file check against an earlier copy checks only what was added or changed, and
openpulse trustdoes the same from the CLI. - Untrusted text: text from files and upstreams never reaches an agent unvalidated.
Every check uses the closed T-F1 status vocabulary and is never part of a verdict. Verdicts, contracts and the frozen evaluation are unchanged. No upgrade steps are needed. The new trust, provenance, lookalike and digest_integrity fields are additive.
Added
-
Trust checks on added dependencies (track T, v0.12.0 item U5).
check_dependency_filetakes an earlier copy of the file asbase_path; the PyPI and npm dependencies added or changed since then get registry existence, provenance (PyPI) and lookalike indicators in a newtrustblock. It holds counts per check and status, and lists everyCONFIRMED_MALICIOUS,VERIFIED_FAILUREorSUSPICIOUS_INCONCLUSIVErecord underattention.trust_allchecks every dependency instead. With neither,trustis null and nothing extra is fetched.- Budget: trust checks run before the file-wide lookups, so the additions get the session budget first. The live checks are capped at 40 packages per file (the W4 bound), and the rest are counted as capped. Lookalike is local data, so every package gets it.
- MCP:
base_pathis confined to the project root likepath. - CLI:
openpulse trust FILE [--base EARLIER] [--offline] [--json]prints the same block. It is report-only and never exits non-zero. - Never part of a verdict (tested). PyPI spelling variants (
Zope_Interface,zope.interface) are the same dependency, not an addition. - Not yet: the GitHub Action on pull requests. It needs the base file from git, and gating on trust statuses is T-E1 (track T Phase 4).
-
Lookalike-name indicators in
dependency_status(track T, T-A2, v0.12.0 item U4). A PyPI or npm name is compared with the curated catalog and a pinned popular-name list. Every rule that fires is named, with the reference it matched:separator: equal once-,_and.are removed;edit_distance: within 1 edit, or 2 for names of 10+ characters, where a swap counts as one;confusable: equal under the UTS #39 skeleton restricted to ASCII;scope: npm@x/namewherenameis a reference.- Outcomes:
SUSPICIOUS_INCONCLUSIVEis an indicator, never a typosquat verdict. A reference name isVERIFIED_RELATIONSHIP. Nothing firing isMISSING_EVIDENCE, not evidence of authenticity. Local data only: no network, no budget, never part of a verdict. - Reference data (owner-approved sources), each built and pinned by the new
scripts/refresh_reference_names.pyand listed in the newNOTICE:npm-high-impact1.13.0 (MIT, 17,335 names), checked against npm's sha512 integrity and read as text, never executed;- the ASCII mappings of UTS #39
confusables.txt18.0.0 (Unicode License v3), pinned by sha256.
- PyPI's popular list: a dated BigQuery query over the CC BY 4.0 download dataset, printed by the tool for a maintainer with a Google Cloud account to run. Until then, PyPI names are compared with the curated catalog only, and answers show the empty list.
-
PyPI provenance in
dependency_status(track T, T-B1, v0.12.0 item U3). For a PyPI package with a version, the newprovenancefield verifies the release's PEP 740 attestations from PyPI's Integrity API, for up to 4 files (wheels first), each against the file's sha256. Verification usespypi-attestations(Sigstore), never cryptography of our own.- Repository cross-check: for curated packages, the attestation's publisher is compared with the catalog's GitHub repository; a different repository is
VERIFIED_FAILURE(starjacking). - Other outcomes: an attestation that doesn't verify is
VERIFIED_FAILURE. No published provenance isMISSING_EVIDENCE, never a failure, because most packages publish none.VERIFIED_RELATIONSHIPsays where and how the files were built, never that they are benign (Ultralytics' first malicious releases carried valid provenance). - Untrusted text: publisher fields and file names are validated before they appear or reach a URL.
- New optional extra
[provenance](owner-approved, Phase 0 decision 5), Python 3.11+:pypi-attestationsand its ~24 dependencies, hash-locked in the newrequirements-provenance.lock, whichscripts/compile_locks.pygenerates, constrained to the core, mcp and audit-tool pins. Without the extra, the field isNOT_PERFORMEDwith how to enable it. A new CIprovenancejob installs it from the lock, checks drift, audits it and runs its tests. - Lock checker fixes:
scripts/check_lock.pynow reads the provenance lock and parses>=floors that carry an environment marker. - Unchanged: verdicts.
- Repository cross-check: for curated packages, the attestation's publisher is compared with the catalog's GitHub repository; a different repository is
-
Digest integrity for lockfiles (track T, T-B4, v0.12.0 item U2).
check_dependency_file(MCP) compares each digest apackage-lock.json(integrity) orpoetry.lock(files[].hash) pins with what npm (dist.integrity, ordist.shasumfor old sha1 locks) or PyPI (urls[].digests.sha256) publishes for that version. A locked digest the registry doesn't publish isVERIFIED_FAILURE, listed underdigest_integrity.failures: the locked artifact is not the one the registry serves (substituted, re-uploaded or tampered).- Other outcomes: no usable locked digest is
MISSING_EVIDENCE, and no usable registry answer isNOT_PERFORMED, never a failure. - Safety: lockfile digests are validated before use, and names before any URL. Each check spends one lookup from the session budget, under the 40-per-file cap.
- Reader output unchanged:
core.lockfile_reader.lockfile_digestsreads the digests separately, so the dependency entries the verdict engine, contracts and ledger use are untouched. - Unchanged: verdicts. No new dependency; hashing only.
- Other outcomes: no usable locked digest is
Security
- Reader skip messages never carry file text to an agent (track T, T-F3, v0.12.0 item U1).
check_dependency_file(MCP) passed the readers' skip messages through as written. Those messages quote the file's own lines, and the agent writes the files it asks about. A quoted value is now shown only when it has a requirement shape (requests>=2.31.0,junit:junit,^4.2: no whitespace, no Markdown link). Any other value becomes<not shown>, and messages are stripped of control, bidi and zero-width characters and capped at 240 characters. The CLI keeps the full messages for humans, stripped of control and bidi characters.