Repository navigation
Releases: IkarusAILabs/OpenPulse
Release list
v0.15.0
Track T Phase 4 (T-E2): the v0.14 trust policy now also applies where dependencies enter, before anything is installed. The same policy that fails CI fails a commit and a pre-install CI step, with no model involved.
openpulse gate: applies the policy to dependency files alone, comparing with a git ref so only what changed is trust-checked.- Pre-commit hook:
openpulse-gatechecks the dependencies a commit adds or changes. - The Action's
gateinput: runs beforepip installornpm ci, with the base branch's files and policy, on a shallow checkout. - Bypass routes:
--no-verifyis caught by the CI gate. A lockfile regenerated after the gate, an ungated file, and file names outside the hook's pattern are documented limits, each with a workaround.
Nothing new blocks without a policy, and verdicts, the evidence contract and the frozen evaluation are unchanged. No upgrade notes.
Added
openpulse gate: the trust policy before install (track T, T-E2, v0.15.0 item Z1).openpulse gate FILE... [--base-ref REF | --base FILE...] [--policy PATH]applies the trust policy to dependency files alone, with no events needed. It is for a CI step beforepip installornpm ci(--base-ref origin/main), and for a pre-commit hook (--base-ref HEAD). Seedocs/TRUST_POLICY.md, "Before install".- Comparison: with
--base-ref, each file's copy at that ref is read withgit show, and trust checks run only for added or changed dependencies. A file new since the ref counts as added. - The same evaluation as
checkandattest: the blocklist covers every dependency, lockfile hashes edited in place are caught, andNOT_PERFORMEDnever blocks. - Exit codes: 0 pass, 1 violation, 2 usage error. Without a policy it passes and says so. Deleted and non-dependency files are skipped.
- Comparison: with
- Pre-commit hook
openpulse-gate(v0.15.0 item Z2)..pre-commit-hooks.yamlrunsopenpulse gate --base-ref HEADon staged dependency files (requirements*.txt,pyproject.toml,poetry.lock,package-lock.json,Cargo.toml,Cargo.lock,go.mod,pom.xml), so a commit is checked only for the dependencies it adds or changes. Seedocs/TRUST_POLICY.md, "Pre-commit hook".- First commit:
openpulse gate --base-ref HEADin a repository with no commits treats every file as new instead of failing. Any other unknown ref is still a usage error.
- First commit:
- The Action's
gateinput: the trust policy before install (v0.15.0 item Z3). Withgate: <dependency files>, the Action runs onlyopenpulse gate(no events, contracts or SARIF), so it can sit beforepip installornpm ci. Seedocs/GITHUB_ACTION.md, "Before install".- Pull requests: base copies come through the contents API (a shallow checkout is enough), and only added or changed dependencies are trust-checked. An unreadable base copy means that file is checked in full.
- Settings: the policy and the private-name list come from the base branch when it has them, so a pull request cannot weaken or delete its own gate.
- Dogfood: CI runs the gate input on every push, once passing and once blocked by the blocklist.
- CLI without the Action:
docs/TRUST_POLICY.md, "In CI, before install".
- Pre-install bypass routes pinned by tests (v0.15.0 item Z4).
tests/test_gate_bypass.pynow also covers the routes around the hook and the CI gate:- Covered:
git commit --no-verifyis caught by the CI gate, which compares with the base branch. - Documented limits: dependency files outside the hook's name pattern, a lockfile regenerated after the gate, and an install from a file the gate was not given.
- Each is listed with what to do instead in
docs/TRUST_POLICY.md, "Bypass routes".
- Covered:
Changed
openpulse gate: a missing or invalid policy file is a usage error (exit 2), not a violation (exit 1).
v0.14.0
Track T Phase 4 (T-E1): trust conditions you prohibit now fail CI deterministically, with no model involved, because advisory answers to an agent are not a security boundary.
- Trust policy:
.openpulse/policy.ymlprohibits trust conditions, read bycheck,attestand the GitHub Action. It is backed by measured false-positive rates: lookalike names fire on 18.8% of real popular npm names, so indicators must name the packages they apply to. - The Action on pull requests: it checks trust for the dependencies a pull request adds or changes, and uses the base branch's policy so a pull request cannot weaken its own gate.
- Exports: trust results appear in SARIF, the job summary and DefectDojo, with levels set by your policy, never by a risk score.
- Bypass tests: they found and closed two pull-request bypasses, and document the routes the gate cannot cover.
Nothing new blocks without a policy, and verdicts and the frozen evaluation are unchanged. One upgrade note: dependency_status drops the v0.12 top-level trust fields, which were deprecated in v0.13.0; read trust.checks.
Upgrade notes
dependency_statusno longer repeatsregistry_existence,provenanceandlookalikeat the top level (v0.14.0 item Y6). They were deprecated in v0.13.0. Read them fromtrust.checks, where they have been since v0.13.0 with the same values. Thedeprecatedfield is gone too. Nothing else in the answer changes, and verdicts are unchanged.
Security
- A lockfile hash edited in place no longer passes the pull-request gate (v0.14.0 item Y5). With
--base-lockfile(the Action'strust: pull-request), the digest check ran only for entries with a new name or version. A pull request that kept an entry's name and version but changed its hash, swapping the artifact, was never checked. Digests are now checked for every entry whose locked digests differ from the base. Found by the new bypass tests. - A pull request can no longer weaken its own trust gate in the Action (v0.14.0 item Y5). With
trust: pull-request, the action used the pull request's own.openpulse/policy.ymland.openpulse/private-packages.txt, so a pull request could delete its prohibitions or list a malicious name as private. The action now uses the base branch's copies when they exist. A policy the pull request adds is still used, because there was nothing to weaken. If the base copies can't be read, the action warns and uses the pull request's.
Added
-
Gate bypass tests (track T evaluation category 8, v0.14.0 item Y5).
tests/test_gate_bypass.pypins each route around the trust gate.docs/TRUST_POLICY.md, "Bypass routes", lists each route as covered or not, with what to do instead.- Covered: lockfile-only additions, hashes edited in place, respelled blocklisted names, an agent ignoring the MCP warning, and a pull request weakening its own policy.
- Not covered: removing the workflow step (use a required status check), unpinned entries, version-pinned blocklist entries, sources that don't answer, installs outside the gated files, and internal names missing from the private-name list.
-
Trust records in SARIF, the job summary and DefectDojo (track T, T-E1, v0.14.0 item Y4). This answers an open question from the Phase 0 trust design: how T-F1 statuses map onto these outputs without implying a risk score.
-
Where they appear:
attest --sarifand--defectdojo(and the GitHub Action) report trust records from contract 2.2.0trust_assurance, plus trust policy violations, as their own results underopenpulse/trust/<check>. Each appears once per file, dependency, check and status, with the https references it cites. -
Levels follow your policy, not a score:
- prohibited by your policy (including the blocklist): SARIF
error, DefectDojo High; - otherwise CONFIRMED_MALICIOUS or VERIFIED_FAILURE:
warning, Low; - otherwise SUSPICIOUS_INCONCLUSIVE:
note, Info; - other statuses are omitted unless prohibited.
No
security-severityis emitted. - prohibited by your policy (including the blocklist): SARIF
-
Job summary: a separate Trust checks table.
-
Consistency: SARIF and DefectDojo share one item list and one identity per record. Verdict results are unchanged (tested).
-
-
The GitHub Action checks trust on pull requests (track T, T-E1, v0.14.0 item Y3). New action inputs, both off by default so existing workflows stay offline and unchanged:
trust:off,pull-requestorall. Withpull-request, the action reads the base branch's copy ofmanifestandlockfilethrough the GitHub contents API withgithub-token. The token is in the environment of that one step only. Trust checks then run only for the dependencies the pull request adds or changes.- New files: a file the pull request adds counts entirely as added.
- Unreadable base: if a base copy can't be read, every dependency is checked; a failure never narrows the checks.
policy: a trust policy file (default.openpulse/policy.ymlwhen present). A violation fails the step even withfail-on: none.- CLI:
openpulse attest --base-manifest PATH --base-lockfile PATHdoes the same from the command line. Trust checks and the policy's trust conditions apply to added or changed dependencies, and the blocklist to all of them. - CI: the action job now dogfoods a blocklist policy that must fail the step.
-
Trust policy: prohibited conditions fail the run (track T, T-E1, v0.14.0 item Y2).
.openpulse/policy.yml(or--policy PATH) is read byopenpulse checkandopenpulse attestand evaluated without any model. Seedocs/TRUST_POLICY.md.prohibit: for any package,confirmed_malicious,not_found_in_registry,digest_mismatch,provenance_failureandrepository_mismatch. For named packages only,missing_provenance,lookalike_name,public_private_nameandunconfirmed_repository. A policy that names an indicator without packages is refused, because the Y1 measurements show lookalike fires on 18.8% of real popular npm names. Conditions with no organic false-positive set warn that they are experimental.blocklist:ECOSYSTEM:NAME[==VERSION]patterns; no lookups needed.- What never blocks: silence.
NOT_PERFORMEDandMISSING_EVIDENCEnever fail a run unless a scoped condition says so. - When it fails: a violation exits 1 after every output is written, alongside
--fail-on. With no policy, gates are unchanged.
-
Track T measurements (v0.14.0 item Y1).
python benchmarks/trust.pymeasures every trust check offline and writesbenchmarks/results/trust.jsonanddocs/research/TRACK_T_MEASUREMENTS.md. The owner accepted these measurements as the false-positive prerequisite for policy gates (T-E1, decision 11). They are not the registered study.- Synthetic cases: 33 cases with 37 labelled expectations, from the roadmap's evaluation plan (existence, lookalike, dependency confusion, provenance and repository linkage, digest integrity, known-malicious records). They run against a stub world in the reserved
opfx-namespace. Every case gets its expected status, with no false positives on the synthetic negatives; that shows the checks do what they claim, not how often real attacks are caught. - Degraded sources: with every source unreachable, or the budget spent, no live check claims anything (0 of 284 records).
- Untrusted text: hostile names are refused before any request and never echoed. An instruction in an OSV summary never reaches a record.
- Organic control (the informative number): leave-one-out lookalike over 1000 real popular npm names. 18.8% look like another popular name; 9.7% without the
scoperule, which fires on legitimate@types/<name>-style packages. So lookalike stays an indicator: a policy should prohibit it only for a named scope. - Labels per policy condition: each condition a policy can prohibit is labelled measured, measured; indicator or experimental (no organic set yet: dependency confusion, unconfirmed repository linkage).
- Tests run a quick sample and check that the committed report matches the committed results.
- Synthetic cases: 33 cases with 37 labelled expectations, from the roadmap's evaluation plan (existence, lookalike, dependency confusion, provenance and repository linkage, digest integrity, known-malicious records). They run against a stub world in the reserved
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...
v0.12.0
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.
v0.11.1
A patch release:
- Complete monthly reports. Whole-catalog reports no longer lose NVD advisories to NVD's rate limit. Requests are paced, retried, and can use
NVD_API_KEY. A source that still fails is named in the report as "unknown, not absent" instead of reading as "nothing found". The November 2026 report run, and the 2026-10-30 early edition before it, each silently lost a different set of projects' advisories. - Five untrusted-text hardening fixes for the MCP tools (track T, T-F3).
No contract or verdict changes.
Security
- Lifecycle cycle strings and engine reasons are checked at the agent boundary (track T, T-F3 fix B, v0.12.0 item U1). Release-cycle names come from endoflife.date and become event scope versions, which the engine interpolates into
reasonstrings returned to MCP agents. A bridged lifecycle event whose scope isn't version-shaped is now withheld from the answer. It is counted undersources(status: withheld) and innot_established, never dropped silently.verdict.reasonandcauses[].reasonalso go throughstrip_unsafe. This is checked incore/agent_toolson purpose:analyzers/lifecycle_events.pyis imported by the frozen evaluation's replay, so the engine, the CLI and the evaluation are unchanged. Every real cycle in the bundled fixtures (24) is version-shaped. - Error text carries no control, bidi or zero-width characters (track T, T-F3 fix C, v0.12.0 item U1).
collectors.errors.redact, and with itsafe_message, every collector error record, the GitHub reader's skip reasons and webhook delivery errors, now turns C0/C1 control characters into a space and removes bidi overrides and isolates, directional marks and zero-width characters (strip_unsafe). This error text reaches terminals, webhooks and MCP agents, where those characters could reorder or hide what is shown. Stripping runs before redaction, so a hidden character can't split a secret past it. - Repository metadata is validated field by field (track T, T-F3 fix E, v0.12.0 item U1). The MCP answer's
repositoryblock passed GitHub'sfull_name,url,licenseandpushed_atthrough as given. Each now must have its shape (the GitHub owner/name grammar, an SPDX-style id, an ISO timestamp) or it isnull. The URL is built from the validated name, never taken from the API. The GitHub repository grammar also usedre.matchwith$, which accepted a trailing newline ("owner "). It now usesfullmatch. - Advisory ids are validated before they are listed or put in a URL (track T, T-F3 fix D, v0.12.0 item U1). The MCP answer's
advisorieslisted every advisory id and builthttps://osv.dev/vulnerability/<id>from it. These ids are upstream strings. Only an id with the OSV<DB>-<ENTRYID>shape (CVE, GHSA, PYSEC, RUSTSEC, GO, MAL, RHSA…) is now listed, percent-encoded in its URL. Anything else is withheld and counted in the newids_withheldfield. The verdict andaffecting_this_version_countare unchanged. - Agent inputs must be identifiers, and are never echoed (track T, T-F3 fix A, v0.12.0 item U1). The MCP tools took package names, versions, ecosystems and image refs as free text and repeated them in
dependency,verdict.reasonandcauses[].reason. So an agent, or text injected into it, could make OpenPulse return arbitrary instructions inside fields the agent trusts.- What's accepted: these values must now match a general identifier shape (letters, digits and
@ / . _ : + ~ -for names; similar sets for versions, ecosystems and image refs). Every ecosystem's real names fit, including legacy npm names with capitals. - Tool arguments: a failing argument is refused with a reason that doesn't repeat it.
- File entries:
check_dependency_fileskips a failing entry like an unpinned one ("entry N: package is not an identifier - skipped, value not shown"). The agent writes the files it asks about. - Unchanged: every dependency in the bundled fixtures passes the shape.
- What's accepted: these values must now match a general identifier shape (letters, digits and
Fixed
- NVD requests stay inside NVD's rate limit, are retried, and can use an API key (
NVD_API_KEY). A whole-catalogopenpulse reportmakes one NVD request per project, unpaced and keyless. NVD allows 5 requests per 30 s without a key and answers the excess with 403, so each run silently lost a different set of projects' advisories: vitess in November 2026, timescaledb in the early edition.- Pacing: requests are now paced process-wide, 6.5 s apart without a key and 0.7 s with one.
- Retry: a 403, 429 or 503 is retried up to 3 times, honouring
Retry-Afteror waiting out NVD's 30 s window. - Key: read from
NVD_API_KEYwhen none is passed, and never logged. - Cost: a keyless whole-catalog report takes about 12 minutes longer; a key (free from NVD) removes most of that.
- The monthly report names sources that did not answer, instead of reading them as "nothing found". The November 2026 run lost vitess's NVD advisories to NVD's rate limit, and the report showed vitess as having no security changes. Each project's failed sources (name, error category and HTTP status, never the error text) are now listed in the executive summary and in "Data gaps and limitations", as "unknown, not absent", and counted in the metadata sidecar under
coverage.source_errors. A report without failures is unchanged.
v0.11.0 "Early Warning Operations"
Scheduled alerts an operator can leave running, and a first track T check for AI coding agents.
- Webhooks: a scheduled webhook posts each alert once. Failed posts are retried and dead-lettered, not lost, and with a secret every post is signed.
- CI: jobs can gate on upcoming deadlines (
warnings --fail-on). - MCP: answers state their lookup limits, and
dependency_statussays whether a PyPI or npm package and version exist.
This release changes webhook output: a scheduled check --webhook posts only new alerts, so cache .openpulse/deliveries in CI. Verdicts, contracts and the frozen evaluation are unchanged. Read the upgrade notes before upgrading scheduled jobs.
Upgrade notes
- A scheduled
check --webhookposts each alert once (#78, v0.11.0 item W1). It used to post the whole digest on every run. It now posts only alerts not delivered before, and nothing when there are none. An alert is one affected cause of a verdict (dependency, event, relationship, impact and the event's content), or a repository whose inspection was not COMPLETE. State is kept in.openpulse/deliveries/alerts.json; in GitHub Actions, carry it in the cache like the ledger (docs/SCHEDULED_CHECKS.md).--delivery-state ''restores the old behaviour. Terminal output, verdicts and exit codes are unchanged.
Added
- Does this package exist? (track T, T-M1).
dependency_status(MCP) now reports whether a PyPI or npm package and version exist in their registry (registry_existence). The common hallucination shapes are covered: an invented package, and a real package with an invented version.- Statuses: records use the closed T-F1 vocabulary. A 404 is
VERIFIED_FAILURE, an identity fact and never evidence of malice. Anything else (a 5xx, 429, redirect, timeout, offline, a spent budget, an unsupported ecosystem) isNOT_PERFORMED, never "absent". - Untrusted text (T-F3): names and versions are validated against the ecosystem's grammar before they go into a URL, and refused otherwise. Requests are HEAD only, so no upstream text is read. Statements are fixed templates.
- Limits: each check spends one lookup from the session budget (W4). It is cached per session, and
NOT_PERFORMEDis never cached. - Unchanged: the verdict, the contract and the frozen evaluation. No new dependency.
- Statuses: records use the closed T-F1 vocabulary. A 404 is
- MCP: stated lookup limits and a cross-session cache (v0.11.0 item W4, C5).
- Budget: one server session makes at most 200 live lookups; cached answers are free. Every tool answer carries a
lookupsblock (budget, used, remaining, per-source timeout, cache TTL). - Past the budget: no lookup is made. The answer lists it under
not_establishedwith the reason, andproject_lifecyclereturns a tool error that says so. - Per-file cap:
check_dependency_filenames the projects its 40-project cap skipped (live_lookups_capped). It used to skip them silently. - Cache:
project_lifecyclecaches successful lookups on disk across sessions (OPENPULSE_CACHE_DIR; empty disables) with the session's one-hour TTL. Failures are never cached, and an answer served from the cache gives its age (cached_age_seconds).
- Budget: one server session makes at most 200 live lookups; cached answers are free. Every tool answer carries a
- Signed webhook payloads (#78, v0.11.0 item W3). With
OPENPULSE_WEBHOOK_SECRETor--webhook-secret-file(at least 32 characters), everycheck --webhookpost carriesX-OpenPulse-TimestampandX-OpenPulse-Signature: v1=<hex>, an HMAC-SHA256 over"<timestamp>." + bodyon the exact bytes sent.core/webhook_signing.verify_webhook_signature(stdlib only) verifies it over the raw body, in constant time, and rejects a timestamp more than 300 seconds off, which bounds replay. The secret is never printed or stored. Without a secret, posts are byte-for-byte unchanged. warnings --fail-on {overdue,critical,high,medium,low}(#78, v0.11.0 item W2). A CI job can fail on deadlines. The command exits 1 when a warning at or above the severity is in the window; the order is OVERDUE > CRITICAL > HIGH > MEDIUM > LOW, socriticalincludes OVERDUE. As with every gate, it also exits 1 on FAILED repository coverage. The reason goes to stderr, so--output jsonstays parseable. Without the flag,warningsonly reports, as before.- Persistent alert delivery state (#78, v0.11.0 item W1, C1).
core/delivery.pygives each alert a stable ID and records its delivery.- Retry: a failed post is retried once in the run, then left
pending. Later runs retry it after a backoff (5 minutes, doubling, at most 6 hours). - Dead letter: after 5 failed runs the alert becomes
dead_letter. It is kept with its last (redacted) error and never posted again, and the run says so. - One run at a time: the state is written atomically under an OS lock held across load, post and save, so two runs at once can't both send. A corrupt state file is moved aside, never overwritten.
- Configuration:
--delivery-state/OPENPULSE_DELIVERY_STATE.
- Retry: a failed post is retried once in the run, then left
v0.10.0 "Verified Releases & Safe Gates"
Every release is now built from its tag with hash-locked tools and carries signed build provenance, and no output or CI gate treats an input OpenPulse did not fully read as clean. This release changes outputs and gates:
--fail-on(CLI and Action) now fails on a repository it could not read.openpulse attestemits evidence contract 2.1.0, where runs with no repository sayNOT_APPLICABLEcoverage.- A token file with two non-empty lines is refused.
Webhooks connect only to the address that passed the SSRF check. Verdicts and the frozen evaluation are unchanged. Read the upgrade notes before upgrading CI gates or contract consumers.
Upgrade notes
openpulse attestemits evidence contract 2.1.0, and runs with no repository sayNOT_APPLICABLEcoverage (#109, v0.10.0 item V3). Contracts from--watchlist,--sbom,--spdx,--lockfile,--manifestor--imagesruns saidinspection_coverage.status: COMPLETEalthough no repository was scanned; they now sayNOT_APPLICABLE. A consumer validating against the 2.0 schema must switch toschemas/evidence-contract/v2.1/schema.json, and one that switches on the status must accept the new value. Runs with--github-repoare unchanged. The Action'sinspection-coverageoutput can beNOT_APPLICABLE, which raises no warning. Hash and every other value are unchanged.--github-token-fileaccepts exactly one non-empty line, holding a visible-ASCII token (#138, v0.10.0 item V5). A leading blank line is now skipped instead of rejected. A file with two non-empty lines, which used the first and ignored the rest, is now refused, because it's ambiguous which line is the token. A token with a space, a control character or a non-ASCII character is refused with a clear message, instead of failing later inside the HTTP client. No message contains the token. The read goes through the shared bounded reader (core.bounded_io), so its size error now reads "is N bytes (limit 4096)".--fail-onnow fails on FAILED repository coverage (#101, v0.10.0 item V2).check --fail-on,attest --fail-onand the GitHub Action'sfail-onexited 0 when a--github-reposcan FAILED, so a CI gate could pass on a repository OpenPulse could not read. They now exit 1 (after writing every output, forattest), as--strictalready did, and say why. PARTIAL coverage is unchanged: it fails only--fail-on-partial. Without a gating flag, FAILED coverage stays report-only.- Webhook delivery ignores
HTTP(S)_PROXY(finding M5, v0.10.0 item V4, #242). A proxy resolves the webhook host itself, which would undo the DNS pinning below, so--webhookdelivery now connects directly. A host that can only reach the webhook through a proxy can no longer deliver.
Security
- DNS-pinned webhook transport (finding M5, v0.10.0 item V4, #242). The webhook SSRF check resolved the host, then httpx connected by hostname and resolved it again, so a resolver that answered with a public address first and
127.0.0.1or169.254.169.254second (DNS rebinding) steered delivery past the check. The host is now resolved once, every returned address is checked, and the connection goes to a validated IP while TLS SNI, certificate verification and theHostheader stay on the original hostname (a custom httpcore network backend under httpx's transport; no new dependency). IPv4-mapped (::ffff:a.b.c.d), 6to4 (2002::/16) and NAT64 (64:ff9b::/96,64:ff9b:1::/48) addresses are unwrapped first, and a refusal names the embedded IPv4 and its wrapping. The rest of the policy is unchanged: https only unless--webhook-allow-http, no redirects, capped response bodies, and no URL path in output.
Added
- A warning when the GitHub token file is readable by other users (#139, v0.10.0 item V5). On POSIX, if the file's mode grants group or other access (
mode & 0o077),check,digest,warnings,attestanddiscoverprintwarning: GitHub token file <path> is readable by other users; run chmod 600 <path>to stderr. The run continues; the warning prints the path, never the token. The README example now runschmod 600. - Attested releases (#111, v0.10.0 item V1). A
v*tag runs.github/workflows/release.yml.- Build:
scripts/build_release.pybuilds the sdist and wheel from the tagged tree. It uses setuptools' own PEP 517 hooks with the hash-locked setuptools from.github/ci-tools.lock, without build isolation, so no build tool is fetched at build time. - Refused builds: a tag that differs from the package version, and a locked setuptools below
[build-system].requires. - Checks before publishing: the wheel is smoke-tested in a clean venv from outside the checkout, and the deterministic SBOM is regenerated twice and compared.
- Provenance: every file gets Sigstore-signed SLSA build provenance (
actions/attest-build-provenance); verify withgh attestation verify <file> --repo IkarusAILabs/OpenPulse. - On the GitHub release: the sdist, wheel,
sbom.cdx.jsonandSHA256SUMS, attached without replacing files that are already there. - Permissions: each job gets only what it needs:
id-tokenonly for the job that signs,contents: writeonly for the job that attaches. - Not done: nothing is published to PyPI; that waits on owner decision 7.
- Build:
Fixed
- Digest, warnings and the webhook body now say when a repository was not fully read (#102, v0.10.0 item V3). With a FAILED or PARTIAL
--github-reposcan,openpulse digestandopenpulse warningsused to drop the inspection result. Their body read "Nothing upcoming or newly-effective within the warning window", with no hint that the repository was never read, andcheck --digest --webhookposted the same. All three now carry> ⚠️ Repository inspection coverage: <status> — <reasons>. Results are not a clean bill of health.in the rendered body (andcoverage_notein--output json), so it reaches the webhook too. Output is unchanged when coverage is COMPLETE or no repository is inspected. - The CI tools lock now satisfies the build floor. Dependabot raised
[build-system].requirestosetuptools>=84.2.1, while.github/ci-tools.lockstayed at 84.1.0. The lock now pins 84.2.1, with the hashes PyPI publishes, and a test fails if the two drift apart again.
v0.9.0 "Anchored Trust"
Authority, independence and history integrity now rest on verifiable facts or a secret key, never on strings the caller supplies. With a history key, regular openpulse anchor checkpoints kept outside .openpulse/ and openpulse verify in the schedule, local history is tamper-evident; without a key it stays corruption-detecting. This release changes outputs: without a history key no lead time or sweep first-detection date is published, openpulse attest emits evidence contract 2.0.0 with a new content hash, and a chained history with no tip is refused. Verdicts of the frozen evaluation are unchanged. Read the upgrade notes before upgrading CI gates or contract consumers.
Upgrade notes
openpulse attestemits evidence contract 2.0.0 (v0.9.0 item B7). A consumer that validates against the 1.2 schema must switch toschemas/evidence-contract/v2/schema.json, because a 2.0 document fails the 1.2contract_versionconst. A consumer that checksprovenance.content_hashmust recompute it with RFC 8785 (JCS), asdocs/EVIDENCE_CONTRACT.mddescribes. Every other value is unchanged. SARIF, DefectDojo and Dependency-Track exports read contract fields and need no change.- Sweeps without a history key no longer publish first-detection dates (completes v0.9.0 item B3c).
sweep,observeandreport --with-sweepnow give a change afirst_detected_atonly when the repository's observation history verifies under a history key (VALID_KEYED), and never earlier than its adoption. Without a key, distribution findings carry no first detection or lead time, and the monthly report narrates them with "First detected by OpenPulse: Unknown", leaves them out of the lead-time list under "Upcoming Changes", and adds the unkeyed note. Stored records keep their dates; only the published claim is withheld. Change detection, diffs and findings are unchanged. - A chained observation history with no tip pointer is now refused (v0.9.0 item B4). It was adopted silently, so a truncated history with its
tip.jsondeleted went unnoticed.observeandsweepnow report it as ROLLBACK_SUSPECTED and write nothing. A history saved by an OpenPulse old enough to write no tip is refused the same way. Accept it once, after checking it, withopenpulse observe --namespace <ns> --repo <repo> --rebaseline --reason "...". - Lead times now come only from keyed history (v0.9.0 item B3c).
digest,warnings,reportandcheckpublish a first detection and a lead time only from detection-ledger entries that verify under a history key. WithoutOPENPULSE_HISTORY_KEY(or--history-key-file), no lead time is published, and the digest, warnings and report say so in a note. Verdicts, deadlines and severities are unchanged. Make a key withopenpulse history-key generate; the next keyedcheck --ledgerrun adopts existing entries, and their lead times count from that adoption. The GitHub Action gains itshistory-keyinput in B3d.
Added
- Evidence contract 2.0.0: an RFC 8785 (JCS) content hash (v0.9.0 item B7, finding L8).
- The hash:
content_hashis SHA-256 over the JCS form of the document, without the hash itself, the build time and the signature fields. Any consumer with a JCS library recomputes it without OpenPulse's serializer. The new requiredprovenance.content_hash_methodisjcs-sha256. - Code:
core/jcs.pyis an RFC 8785 encoder (stdlib only). It rejects what JCS cannot represent: NaN, infinities, integers beyond 2**53, lone surrogates and non-string keys.build_contract,v2_content_hash,verify_content_hashandvalidate_contractare new; the last two dispatch oncontract_version. - Verifier:
conformance/evidence-contract/verify.mjsis a standalone Node verifier with no dependencies. - Unchanged: the 1.0, 1.1 and 1.2 schemas, and the 1.x builder
build_v1_contract, which the frozen evaluation uses.
- The hash:
- History anchors:
openpulse anchorandopenpulse verify(v0.9.0 item B5, finding H2). They catch deletions that take their own evidence with them, which the store cannot see from inside: a repository's whole history folder (tip included), a ledger entry, the rotation log.-
openpulse anchor [--out FILE]writes a checkpoint with:- each observation history's tip, record count and re-baseline count;
- one leaf per ledger entry (
first_seen,adopted_at), plus an RFC 6962 Merkle root over the leaves; - a hash of each rotation-log entry.
With a history key it is MACed (
key_id,mac). It refuses to write inside.openpulse/: keep it in git or as a CI artifact. -
openpulse verify --anchor FILEexits 1 on any violation: an anchored tip no longer in its chain (ROLLBACK), a wiped history (WIPED), a deleted or backdated ledger entry (LEDGER_DELETED, LEDGER_REWRITTEN), a dropped rotation or re-baseline log entry, or an anchor whose MAC fails.- Growth after the anchor is fine.
- A re-baseline after the anchor is a notice only when its log entry verifies under the key.
--require-signedalso refuses an anchor the supplied key did not sign.--jsonprints the result.
-
Docs:
docs/SCHEDULED_CHECKS.mdadds anchoring to the scheduled CI recipe. README, METHODOLOGY and SECURITY now say that local history is tamper-evident with a history key plus anchors kept out of the store, and corruption-detecting without a key.
-
- Rollback and downgrade detection for observation history (v0.9.0 item B4, finding H2).
- New refusals: ROLLBACK_SUSPECTED, for a chained history whose tip pointer is missing (before, it was adopted). DOWNGRADE, for an unchained record after chained ones, or an unkeyed record after keyed ones. They join ROLLBACK and BROKEN, and the refusal message says how to re-baseline.
- Explicit re-baseline:
openpulse observe --rebaseline --reason "..."(one line, up to 500 characters). A rollback whose remaining chain verifies is accepted at its current tip. A broken or downgraded history, or a tip with no data, is moved toquarantine-<time>/, never deleted, and the chain restarts at genesis. - Logging: every re-baseline is logged in
rebaselines.json(time, reason, previous status, accepted tip or quarantine folder), MACed under the history key when one is set. The new record is a baseline: no change claims cross a re-baseline. - Not covered: a full wipe of a repository's folder, tip included, still looks like a first run. So does deleting the ledger's
rotations.json. Detecting those needs an anchor outside the store (B5).
- Lead times now come only from keyed history (v0.9.0 item B3c).
digest,warnings,reportandcheckpublish a first detection and a lead time only from detection-ledger entries that verify under a history key. WithoutOPENPULSE_HISTORY_KEY(or--history-key-file), no lead time is published, and the digest, warnings and report say so in a note. Verdicts, deadlines and severities are unchanged. Make a key withopenpulse history-key generate; the next keyedcheck --ledgerrun adopts existing entries, and their lead times count from that adoption. The GitHub Action is unaffected: it runsattest, which neither records nor publishes lead times (corrected in B3d; the B3c note said otherwise). Scheduled CI that runscheck --ledgerneeds the key as a secret and a ledger carried between runs: seedocs/SCHEDULED_CHECKS.md.
Added
- Keyed history in scheduled CI, documented (v0.9.0 item B3d).
docs/SCHEDULED_CHECKS.mdshows how to keep the history key in an Actions secret, carry.openpulse/between scheduled runs withactions/cache, and rotate the key. B3d was planned as ahistory-keyinput on the GitHub Action; the Action runsattest, which keeps no history, so such an input would do nothing and is not added. It is reconsidered if the Action ever records history. - Keyed adoption of older history (v0.9.0 item B3c, finding H2). History written before a key existed is adopted, never re-MACed as if it had always been keyed.
- Observations: unkeyed records followed only by keyed ones now verify VALID_KEYED. The first keyed record's chain hash pins the older records, and
verify_keyed_historyreports itsobserved_atasadopted_at. An unkeyed record after a keyed one is still VALID_UNKEYED. A change'sfirst_detected_atfrom a keyed sweep is never earlier than the adoption. - Ledger: a keyed write to an unkeyed entry adopts it. It keeps
first_seenas context, gainsadopted_at(covered by the MAC), and later keyed writes keep that time. The newkeyed_first_seen(..., keys)returns the later offirst_seenandadopted_at, and only for a VALID_KEYED entry, with the rotation log applied. - Readers:
build_digest,build_warnings,prepare_reportandbuild_reporttakehistory_keys.digestandwarningsgain--history-key-fileand--history-previous-keys-file. Without keys they publish no lead time and setlead_time_note.
- Observations: unkeyed records followed only by keyed ones now verify VALID_KEYED. The first keyed record's chain hash pins the older records, and
- History key rotation (v0.9.0 item B3b, finding H2). A key can now be retired without losing its history, and a leaked retired key cannot extend it.
- Retired keys are verify-only:
OPENPULSE_HISTORY_KEYS_PREVIOUS(comma- or space-separated) or--history-previous-keys-file(one per line,#comments) onobserve,sweep,reportandcheck. They need a current key. New records are always MACed with the current key. - Observations: when a history's newest keyed run is under a retired key, the next record is a handover: it carries
previous_key_idandhandover_mac, a MAC under the retired key.verify_keyed_historynow walks keyed runs. A record under the retired key after its handover, a key change without a handover, or a handover that does not follow its key's run is BROKEN. A history keyed with a key that is neither current nor supplied as retired is still refused (KEY_MISMATCH). - Ledger: an entry under a reti...
- Retired keys are verify-only:
v0.8.0
The release that TR-2026-01 describes. This release changes verdicts: the audit fixes (D1–D15, F1–F4) withhold clearances the earlier engine gave, completeness now needs a recorded basis, and the CLI and MCP tools bound evidence age by default. Read the upgrade notes before upgrading CI gates.
Upgrade notes
- The CLI's verdict commands bound evidence age (TR-2026-01 N4). This changes verdicts, as it did for the agent tools in PR #200.
check,attest,digestandwarningsnow run in the current mode: a NOT_AFFECTED needs evidence fetched within 90 days of the start of today (UTC). Older evidence makes it RELATED ("stale"), never a clearance.--max-evidence-age-days Nand--as-of DATEset the frame.--replayre-evaluates recorded evidence with no bound and says so on stderr, and contracts record the frame inevaluation_context. Contradicted events never clear in either mode. Pipelines that replay old fixtures should pass--replay.
Documentation
-
Roadmap: track T, AI Dependency Trust Assurance (planned; a separate future study).
docs/ROADMAP.mdadds the following:- what exists today and what is planned;
- an initial threat model that separates demonstrated techniques from unmeasured prevalence;
- six workstreams: identity and impersonation, provenance and integrity, reputation integrity, AI selection evidence, policy enforcement, and evidence contracts;
- phases 0–4, with a small first milestone: registry existence for PyPI and npm;
- a proposed evaluation plan with research questions RQ1–RQ5.
Nothing is implemented, and no behaviour, interface, contract or dependency changes. Track R and the TR-2026-01 registration are unchanged.
Added
-
Adversarial-input tests (
tests/test_adversarial_inputs.py). They pin, for each attacker capability the twelfth review lists, what the event-scope engine does:- inputs that cannot yield a negative: contradicting evidence, a contradictory event pair, stale or retracted events, unresolvable references, and injected upstream text;
- two trusted inputs: a wrong completeness declaration, and a curated bare-name alias for a package without an ecosystem.
-
Evaluation: strata, pair-level bounds, deduplicated burden (
evaluation/registration.py,run.py):- results by completeness basis and identity status (REGISTRATION §4);
- the pair-level false-clear share, with both an independent-pairs exact bound and an event-clustered bound;
- per inventory and cycle, the actionable share and the unique review items after deduplication.
-
Evaluation: the registration amendments of 2026-10-30, implemented (
evaluation/REGISTRATION.md§2, §4, §5, §5a). This adds:- policy R, the existing-representation baseline: written rules for encoding an event as OSV or OpenVEX (
evaluation/representations/ENCODING.md), and a careful consumer (evaluation/existing_representation.py) with exact purl identity and V's pinned comparators. Each event's encoding is frozen by SHA-256 in the case set, andrun.py --require-encodingsrefuses an event without one; - test S3 (C − R, Holm-corrected with S1 and S2), guardrail G4, and the attribution steps B→R→V→C;
- the review-burden measures W1–W3 (
run.py --inventories) and the reliability measures D1–D2 (--annotations).
Six development-set events are encoded (dataset revision 2026.10.30-3), so R's numbers there are illustrative only. No OpenPulse verdict changes.
- policy R, the existing-representation baseline: written rules for encoding an event as OSV or OpenVEX (
-
Ecosystem-qualified package scopes (F3). A package-scope value may be a purl without a version (
pkg:pypi/redis). It names exactly one package: a dependency of that ecosystem and name is AFFECTS_PACKAGE whatever its curated project, a package of another ecosystem is excluded (only from a scope declared complete with a basis), and anything unreadable is undetermined. Bare values keep working and are read through curated identity. -
Evidence contract 1.2.0: what a verdict does and does not assert (TR-2026-01 sections 5.1-5.2, ROADMAP R-B6). Every document now carries three new things:
verdict_meaning: direct scope membership as a closed vocabulary; embedded components, availability, maintenance and security consequence markednot_evaluated; and a fixed statement with no upstream text;scope.completeness: whether the scope was declared complete, its basis, and whether that admits a negative;evaluation_context: whether a freshness bound was applied.
openpulse attestemits 1.2. The 1.1 and 1.0 schemas are unchanged and their documents still validate and verify. Upgrade note: a consumer that validates against the 1.1 schema must switch toschemas/evidence-contract/v1.2/schema.json, because a 1.2 document fails 1.1 validation (additionalProperties: false). -
MCP answers bound evidence age (TR-2026-01 N4). This changes verdicts.
dependency_statusandcheck_dependency_filenow run in a current-assessment mode: a negative needs evidence fetched within 90 days (configurable) of now, so a stale event can associate a dependency but no longer clear it. Answers carry anevaluation_contextblock (contract 1.2 fields plusmode), and every cause carries itsscope_completeness. An explicit replay mode (Python API only) disables the bound and says so inevaluation_context,not_establishedandnote. -
MCP responses state verdict meaning.
verdict.meaningcarries the same closed vocabularies, every cause carriesdirect_scope, and the fixed note now says that NOT_AFFECTED is not a clearance either.
Fixed
-
A package named like an uncatalogued event project was treated as that project (F4). This changes verdicts. A package that resolves only to itself, because no curated mapping exists, is now RELATED (identity unresolved) against that project's events, even when its name is the project's. It was previously version-tested, and could be cleared. Images already behaved this way (D10). capa (PyPI
flare-capa) and react-native-fbsdk (npm) are now curated, so their package pins keep their project tie. -
Package names in a package scope compare in canonical form (F2). This changes verdicts. A Maven
groupId:artifactIddependency now matches a listedgroupId/artifactId, and PyPI names fold per PEP 503 (Apache.Airflow=apache_airflow). A scope value naming the event's own project still covers every package curated as that project; that rule is now part of the specification (TR-2026-01 §5.3,conformance/independent/SPEC.md§4.4) and of the conformance gate. -
Every catalog entry now curates its packages (F1, completed). This changes verdicts.
- Libraries list the packages that are the project, Maven coordinates included.
groupId:artifactIdfrom manifests and SBOMs now matches the catalog'sgroupId/artifactId. - Servers and tools declare none, so a package that only shares their name is unresolved (RELATED), not the project. Examples: PyPI
kubernetes(the Python client), npmmongodb(the Node.js driver), npmangular(AngularJS 1.x), PyPIkafkaand PyPIpostgres. - Curated iText modules such as
com.itextpdf:signnow resolve to iText. - Packages with no stated ecosystem resolve by name, as before. A catalog test requires every new entry to state its packages.
- Libraries list the packages that are the project, Maven coordinates included.
-
MCP tools resolved package identity by name (F1, follow-up).
dependency_statusandcheck_dependency_filenow resolve packages per ecosystem, as the verdict engine does. PyPIredisis reported, and looked up, as redis-py. -
A package name was an identity across ecosystems (F1). This changes verdicts. Package dependencies resolved by name alone, so PyPI
redis6.2.0 was AFFECTS_VERSION under Redis server 6.2 end of life, although PyPIredisis redis-py, the Python client. The catalog also mapped the Redis server to PyPIredisfor OSV queries, so redis-py advisories were attributed to the server. The decision-policy evaluation (evaluation/) found it; the conformance oracle could not, because it takes identity as given.- A package with a stated ecosystem now resolves through curated (ecosystem, name) mappings: the new
packageslist on catalog entries, plus theirosvblock. - A name that only shares an alias with a project whose packages are curated is now unresolved (RELATED), never that project.
- Packages with no ecosystem (or purl's
generic) resolve by name as before. - redis-py and node-redis are curated as their own projects, so PyPI and npm
redisnow have no association with Redis server events. - Redis declares no packages, and OSV queries for PyPI
redisnow go toredis-py.
- A package with a stated ecosystem now resolves through curated (ecosystem, name) mappings: the new
This changes verdicts. All of the following could report NOT_AFFECTED (a clearance) for a dependency that is in an event's scope. They were found by the conformance audit in technical report TR-2026-01.
- Multi-digest events checked only the first digest (D1). When an event named several digests of one repository, a dependency pinning any digest after the first was cleared. Every named artifact is now compared: any match is AFFECTED, and one undetermined comparison (digest against tag) withholds the clearance.
- OSV ranges collapsed to their last interval (D2).
introduced 1.0, fixed 1.2, introduced 2.0, fixed 2.3was read as[2.0, 2.3), so1.1was cleared. Ranges are now walked as unions of intervals.GITand unknown range types, and versions at or past alimit, are undetermined. An advisory is cleared only if every affected entry clears the version. Withdrawn advisories no longer assert anything. - A clearance from one event hid an unresolved one (D3). The headline verdict is no longer NOT_AFFECTED while another event's cause is RELATED or AFFECTS_PROJECT. Per-cause verdicts were already kept, and
affectedis unchanged. - Heuristic identity excluded dependencies (D5). "This image is a different project" now needs a VERIFIED resolution.
docker.io/bitnami/<x>images that resolve only through the ge...
v0.7.1
A hardening patch. No verdict or output changes.
Security
- Collector responses are size-capped (#155). Every collector (endoflife.date, OSV, GitHub, registries) now streams its HTTP response and stops at a byte cap (32 MiB by default), counting decoded bytes so a gzip bomb is caught. Oversized responses fail that source with an error instead of exhausting memory. This closes A6b, the last unbounded input channel.
- Every install is hash-checked (#156). CI, the MCP job and the GitHub Action install with
pip install --require-hashesfrom complete, hash-pinned locks generated byscripts/compile_locks.py. A re-uploaded or tampered package with the same version now fails the install.scripts/check_lock.py --exactalso fails CI on any installed package the lock doesn't list. The Action installsrequirements-runtime.lock(no dev tools), then OpenPulse with--no-deps.
Fixed
- The lock was incomplete (#156).
typing-inspection(pulled in by pydantic) andcolorama(Windows) weren't listed, so they floated. The locks are now resolved for every platform at once.
Notes
- The
mcpextra needs Python 3.11 or newer (MCP SDK 2.x requires it). The rest of OpenPulse still supports 3.10.
Install: pip install "ikarus-openpulse[mcp] @ git+https://github.com/IkarusAILabs/OpenPulse@v0.7.1"
If you are upgrading from before v0.7.0, read the v0.7.0 notes first: #133 changes verdicts.