Skip to content

v0.13.0

Choose a tag to compare

@github-actions github-actions released this 02 Nov 10:35
· 57 commits to main since this release
a641958

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 first CONFIRMED_MALICIOUS answers, 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 trust block in dependency_status, with the agent's stated reasons (intent, name_source) recorded but never used.
  • Evidence contract 2.2.0: openpulse attest --trust records the trust checks in trust_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:

  • attest now emits contract 2.2.0;
  • agents should read trust results from dependency_status's trust.checks. The v0.12 top-level copies stay until v0.14.0.

Upgrade notes

  • openpulse attest emits 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 against schemas/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_status gathers its trust checks into one trust block (v0.13.0 item X4). Read them from trust.checks. The v0.12.0 top-level fields registry_existence, provenance and lookalike are still there with the same values, listed under deprecated, and are removed in v0.14.0. The checks added in v0.13.0 (malicious, dependency_confusion, repository_linkage) are only in trust.checks. File checks keep their own trust block unchanged. Verdicts are unchanged.

Added

  • Opt-in local recording of agent questions and answers (M8, v0.13.0 item X6). openpulse mcp --record DIR appends one JSON line per tool call to DIR/mcp-<session>.jsonl. Each line holds the tool, its arguments, the outcome and a research summary: verdict, trust statuses, and the agent's intent and name_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 --trust checks 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_at and the https URIs it cites. A summary counts every status, and used_in_verdict is always false. Free-form evidence stays out, so the public shape is stable.
    • Hash: the JCS content hash covers the block, so verify.mjs checks it with no change.
    • Budget: at most 4 lookups per dependency and 2000 per run. --private-names names the private-name list (default .openpulse/private-packages.txt when 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_status answers carry trust:

    • checks: every trust record (malicious, registry_existence, provenance, repository_linkage, dependency_confusion, lookalike);
    • summary: counts per status;
    • attention: each CONFIRMED_MALICIOUS, VERIFIED_FAILURE or SUSPICIOUS_INCONCLUSIVE record, with its check and statement;
    • time_frame: the records are live lookups at their checked_at, and in replay mode it says they are not as of the replay date.
    • selection_context: agents may say why they ask. intent is a short sentence, stripped of control, bidi and zero-width characters and capped at 280 characters. name_source is one of user_request, existing_code, documentation, search_result, model_suggestion or other; any other value is recorded as unrecognized, not echoed. Both are recorded as supplied_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_status trust.checks (track T, T-A4, v0.13.0 item X3). Registry metadata can claim any repository: npm repository, PyPI project_urls. A package that claims a popular one borrows its stars ("starjacking", threat T4). The new repository_linkage record 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 is VERIFIED_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's repository.directory, or pyproject.toml then setup.cfg). If it names the package back, the result is VERIFIED_RELATIONSHIP. If it names a different package, the result is SUSPICIOUS_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.py is 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).
  • 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, one ecosystem:name per 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_status gains a dependency_confusion record in trust.checks, and the server reads the list from its project root on every call. A listed package skips the public-registry registry_existence and provenance checks, which describe someone else's package, so a private dependency no longer reads as a hallucinated name. malicious and lookalike still run.
    • Trust block: check_dependency_file runs it for listed packages in trust.
    • CLI: openpulse confusion [LIST] checks every listed name, and openpulse trust --private-names LIST uses 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_status trust.checks (v0.13.0 item X1). For any PyPI or npm package, catalogued or not, the new malicious record asks OSV whether it publishes a malicious-package record (MAL-, the OpenSSF malicious-packages feed) for that package and version.

    • CONFIRMED_MALICIOUS cites 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_file runs it too, on added dependencies or with trust_all, and lists a hit under trust.attention. So does openpulse 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 trust does 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_file takes an earlier copy of the file as base_path; the PyPI and npm dependencies added or changed since then get registry existence, provenance (PyPI) and lookalike indicators in a new trust block. It holds counts per check and status, and lists every CONFIRMED_MALICIOUS, VERIFIED_FAILURE or SUSPICIOUS_INCONCLUSIVE record under attention.

    • trust_all checks every dependency instead. With neither, trust is 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_path is confined to the project root like path.
    • 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/name where name is a reference.
    • Outcomes: SUSPICIOUS_INCONCLUSIVE is an indicator, never a typosquat verdict. A reference name is VERIFIED_RELATIONSHIP. Nothing firing is MISSING_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.py and listed in the new NOTICE:
      • npm-high-impact 1.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.txt 18.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 new provenance field 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 uses pypi-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 is MISSING_EVIDENCE, never a failure, because most packages publish none. VERIFIED_RELATIONSHIP says 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-attestations and its ~24 dependencies, hash-locked in the new requirements-provenance.lock, which scripts/compile_locks.py generates, constrained to the core, mcp and audit-tool pins. Without the extra, the field is NOT_PERFORMED with how to enable it. A new CI provenance job installs it from the lock, checks drift, audits it and runs its tests.
    • Lock checker fixes: scripts/check_lock.py now reads the provenance lock and parses >= floors that carry an environment marker.
    • Unchanged: verdicts.
  • Digest integrity for lockfiles (track T, T-B4, v0.12.0 item U2). check_dependency_file (MCP) compares each digest a package-lock.json (integrity) or poetry.lock (files[].hash) pins with what npm (dist.integrity, or dist.shasum for old sha1 locks) or PyPI (urls[].digests.sha256) publishes for that version. A locked digest the registry doesn't publish is VERIFIED_FAILURE, listed under digest_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 is NOT_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_digests reads the digests separately, so the dependency entries the verdict engine, contracts and ledger use are untouched.
    • Unchanged: verdicts. No new dependency; hashing only.

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.