Repository navigation
0.5.0
Release 0.5.0 — the package audit reaches the published index, and the gaps get names.
Attached: our own CycloneDX 1.5 SBOM, generated by the publish workflow from this commit
before the tarball was uploaded. It covers every shipped source file with a sha256, and records
that this package has no third-party runtime dependencies.
The npm tarball is built from the same commit by GitHub Actions and carries SLSA provenance:
npm view @zhiliangtech/agentgate@0.5.0 dist.attestations
npm view @zhiliangtech/agentgate@0.5.0 dist.integrity
What changed
refresh never passed --audit to build-index. Every scheduled rebuild therefore dropped the
package audit and left thousands of audited packages described as "a package is declared here" —
no error, no log line, the artifact on disk the whole time. With it joined, repository records
carry a packageManifest component and /v1/index/summary names what could not be measured
instead of folding it into one word.
Three vocabularies became finer, in the same direction: a difference that was being averaged away
is now two facts. Metadata that could not be read says whether the registry has no such package
or this run could not reach it. A hook script says whether the package does not ship it, a CDN
did not answer, or this step did not ask. And a declared install hook pointing at a file the
package does not contain became its own finding, install-hook-script-missing-from-package,
rather than an unknown nobody could sign off on.
The four MCP tools declare outputSchema, and each description says when to use it and which
sibling answers the other case.
Full notes: CHANGELOG.md, and
what an existing installation has to do:
docs/operations/upgrade.md.
Full changelog entry
-
A file the published package does not ship is now a finding instead of a gap.
registryProvenance
marked the evidence incomplete whenever a hook script could not be read, so
@yagyeshvyas/vibeguard— whose manifest declares"postinstall": "node scripts/postinstall.js"
while the tarball contains no such file — was recorded asinstall-hook-script-unavailable
(unknown) withcomplete: false. We had asked both unpkg and jsdelivr and both answered. A
checked absence is knowledge, the same way a repository tree with no manifest in it is; the rule
now says so, and the finding is what is actually true:install-hook-script-missing-from-package
(high), with evidence that is complete. A CDN that never answered, or a ref this step chose not
to fetch, still leaves the evidence incomplete — those are worth retrying, and this one is not. -
A hook script we could not read now says which of three things happened.
fetchHookScript
returnednullfor a file the package does not ship, for a CDN that never answered, and for a
path this step refused to request — one word,install-hook-script-unavailable, for all three.
The first is knowledge about the package; the second is knowledge about this run and is worth
retrying; the third is this build's own choice, and refs past the five-per-package cap now say
hook-script-not-fetchedrather than borrowing the word for a missing file. The reason travels
into the finding's evidence and into the provenance digest, so a recorded review covers it. -
The index carries the audit's own reason for a package it could not read, instead of one word for
both cases.auditReasonmapped everymetadata-unavailabletopackage-metadata-unavailable,
so the published coverage summary still merged "the registry has no such package" with "this run
could not reach the registry" — the two facts the audit had just started telling apart. An audit
written before it recorded a reason still gets the old word rather than silence. -
agentgate refreshhands--audittobuild-index. It did not, so every scheduled run rebuilt
the published index without the package audit and the repository records fell back to "a package
is declared here" — 6,199 audited packages' worth of evidence, dropped on a four-hourly timer with
no error and no log line. The artifact was on disk the whole time. This is the quiet version of
the mistake the rest of this section is about: not a wrong claim, but a capability that exists in
the code and never reaches the file anyone reads. -
A package we could not get metadata for now says which of two different things happened.
fetchNpmDocumentandfetchPypiDocumentreturnednullfor every failure, so a 404
("the registry does not have this package") and a timeout or a 429 ("this run could not reach
the registry") arrived at the record as the samemetadata-unavailablewith no reason at all.
The first is a fact about the package; the second is a fact about the run. Merging them meant the
published file could not be used to tell a wrong coordinate from a failed fetch — the same shape
of mistake asno-package-declared-in-repositorybelow, in a field with a wider blast radius.
The fetch functions keep their existing signatures and now have…Outcometwins that carry a
reason (package-not-found,registry-unreachable,registry-http-<status>,
metadata-not-json,package-name-not-requested), and the audit writes it down. The last one
is deliberately not called "invalid name": npm still serves legacy packages that predate the
lowercase rule (JSONStreamandBase64answer 200 today), soisNpmPackageNamedescribes what
this step will ask about, not what the registry has. -
The repository record no longer claims something it never checked. Its
packageManifest
component isskippedwith reasonpackage-not-inspected(was
no-package-declared-in-repository). The old string read as a finding about the repository —
"there is no package declared here" — when this build simply never looks: repository records are
built from the census and the classification, and neither carries package data. Three
repositories that do declare apackage.json(firecrawl/firecrawl-mcp-server, upstash/context7,
apify/apify-mcp-server) were recorded as declaring none. The reason now names what this build
did, and nothing about the repository. -
The classification step now keeps the manifest path instead of only using it to pick a kind.
classifyPathsalready matchedpackage.json,pyproject.toml,Cargo.toml,go.modand the
rest — a repository with a server fileanda manifest is whatserver-likemeans — and then
discarded the path. It costs nothing to keep: the file tree was already fetched and parsed. With
it, the reason a package was not read has three honest states rather than one:
manifest-found-not-inspected(the tree lists a manifest; this step does not read it),
no-package-manifest-in-repository(the tree was read and holds none — a checked absence), and
package-not-inspected(a classification written before the field existed, which proves nothing
either way). None of the three can make a recordclean. -
packages/collect/scripts/fetch-manifests.mjsreads the manifest a repository declared and
extracts the coordinates a package audit needs — registry, name, version — frompackage.json,
pyproject.toml,Cargo.toml,composer.json,pom.xml,*.csprojandgo.mod. It reads
fromraw.githubusercontent.com, which does not spend GitHub API quota, and it writes down the
URL and a sha256 of the bytes it read, so anyone can fetch the same file and check the digest. It
does not derive a name from the repository name and does not invent a version: a manifest without
one yieldsversion: null, which the policy layer already treats as missing evidence. A manifest
it cannot read is reported asunreadablewith the HTTP status, and one it can read but cannot
parse is reported with the parse error — neither throws. -
packages/collect/scripts/audit-packages.mjstakes those coordinates and runs the same audit the
registry path runs, so a repository record can carry measured evidence instead of a component that
is skipped because nobody looked. The server object it builds carries notransport: inventing
stdiowould add a finding about a transport nobody checked. Hook scripts are fetched so their
status is real rather than "missing because we did not ask". A registry this step cannot audit yet
is reported asunsupported-registry, and a manifest with no package name as
no-package-name-in-manifest— both are facts, and neither is a pass. -
build-index.mjs --audit <package-audit.json>joins that audit back into the index, so a
repository record can carry the package coordinate and a measuredpackageManifestblock
instead of an emptypackagesarray. Without the flag the records are exactly what they were.The invariant holds, and it is now spelled out. A repository record still can never be
clean,
because a third required component says why:repositorySource, skipped with reason
source-not-read. The metadata block reads a repository's public metadata; the package block
reads the package it publishes; neither reads the repository itself, which is what a reader
would have to inspect to say anything about the server. Before this, "never clean" rested on the
package half being skipped wholesale. It now rests on the gap that actually remains — and when
that gap is closed, the component is removed rather than quietly reinterpreted. -
A second mapping:
agentgate framework --id eu-aiacovers the record-keeping obligations of
Regulation (EU) 2024/1689 — Article 12 and Article 19. Eight items, and the count is published
with them: 2 ours, 4 the customer's, 2 a third party's. Almost nothing is ours, and that is the
honest result rather than a modest one — we do not run the customer's AI system and we do not
produce its logs. What we can do is make the artefacts that do exist checkable
(content-digest,archive-integrity) and count what could not be read
(coverage-accounting,scan-execution).The entry that names Articles 12(3) and 12(4) says 未覆盖 and belongs to a third party,
because this version of the mapping only read 12(1), 12(2), 19(1) and 19(2) — and it says which
of them it read, in thesourcefield, because a mapping that does not say what it read is a
mapping nobody can check. The article text itself is not reproduced; only the paragraph numbers
and our own wording, the same rule the CSA mapping follows. -
The four MCP tools declare what they return, and each description says when to use it. Glama
scores tool definitions and publishes the rubric
(glama-ai/tool-definition-quality-score);
two of its six levers were unaddressed — no tool carried anoutputSchema, and no description
named the sibling to use for the other case.The schemas are written from the handler's own return values, not from intent.
protocol.mjsputs the handler'sstructuredobject intostructuredContent, so a schema here
describes bytes a caller already receives. A test calls every tool and compares the declared
schema against the sent value; breaking one field (total: integer→string) fails with
inventory_tools.summary.total: declared string, sent integer.