Methodology

Version 2.3.1 · In effect · Published 2026-09-17

Attestari methodology

Version
2.3.1
Status
In effect
Effective
2026-09-17

Versions in effect, oldest first: 1.3.0 (from 2026-09-11, superseded)1.4.0 (from 2026-09-14, superseded)1.5.0 (from 2026-09-14, superseded)2.0.0 (from 2026-09-15, superseded)2.1.0 (from 2026-09-16, superseded)2.1.1 (from 2026-09-16, superseded)2.2.0 (from 2026-09-16, superseded)2.3.0 (from 2026-09-17, superseded)2.3.1 (in effect since 2026-09-17). Every version keeps its permanent address; a decision cites the version and the document hash it was made under.

This document is the complete set of rules by which Attestari examines a published package, decides each finding, grades the package, and publishes the result. It is the only place those rules live in prose, and attestari/policy.py is the only place the decision rules live in code; the two carry the same version number and a test refuses to run the policy when they disagree. Every published decision cites the version that produced it and the SHA-256 of this file as it was when the decision was made, together with the model, the prompt and the verifier that produced the finding (section 12). Nothing in a run is decided by a person; the two things a person still does are listed in section 13.

1. What this document is for

Attestari examines software packages that an AI agent may install and grant access to: MCP servers and agent tooling published to public registries. We report what we find, and we publish the evidence alongside it so a reader can check our work rather than take it on faith.

A finding is an opinion, formed by applying the rules below to material anyone can download. This document states those rules completely. If a finding cannot be traced to a rule in this document and to evidence a reader can independently retrieve, it should not have been published, and we want to hear about it.

The document also states, at length, what we do not examine (section 15). That section is not a disclaimer bolted on at the end. It is the part most likely to matter to someone deciding whether a passing result means anything.

2. Definitions

subject: a specific published version of a package in a public registry, for example @scope/server at version 1.4.2 on npm. Ratings are always of a version, never of a project in general.

artifact: the file a package manager actually downloads and installs: the npm tarball or the PyPI distribution. This, not the source repository, is what we examine.

tool surface: the set of tools a subject registers with an agent, together with the descriptions the agent reads. Determined by parsing the artifact's source.

finding: a statement about a subject, tied to a severity, a quoted span of text, the file and line it came from, and the hash of the stored evidence containing it.

not analyzed: a tool we could not examine. Distinct from a tool we examined and found nothing in. The two are never reported as the same thing.

gradeable: a subject for which every advertised tool was examined, every check ran, and every finding's evidence re-verified: the guards in section 9. Only a gradeable subject can carry a grade.

attested manifest: the hash of each readable tool's name, description bytes and schema, and the hash over all of them, computed as section 7 defines and recorded on every decision row. What a gateway pins to when it wants the tools it was shown to be the tools it examined.

capability inventory: what the artifact's code is able to reach, as declared by the source: network destinations, remote code, files, processes. Recorded as context, never measured for precision, never graded (section 20).

3. We examine the artifact, not the repository

Most software auditing reads the public source repository. We do not, and the distinction is deliberate: the repository is not what runs on your machine. The published artifact is.

A package's repository can be clean while the tarball served by the registry contains code that was never committed there. That gap is how a large share of real registry compromises work, and auditing the repository looks straight past it.

We read repository metadata, such as ownership, contributors, and whether the latest commit carries a signature, as context about who stands behind a subject. We do not treat repository contents as evidence of what a subject does.

What this costs us. We do not currently compare the artifact against the repository at the published tag. That comparison would detect a file shipped with no traceable origin in the source, which is a strong signal we would like to have. It is a known gap, recorded in section 15.

4. How subjects are selected

Selection is mechanical and reproducible. A candidate list records the exact queries used, the ranking rule, the page sizes, the timestamp of every fetch, and the snapshot identifier of any dataset consulted. Anyone re-running the same selection against the same sources gets the same list. What it selects is a sample of each registry, not the registry: every count we report is a count over the packages we selected, and says nothing about the packages we did not.

npm candidates come from the registry's public search API across a fixed set of queries, ranked by monthly download count confirmed against the downloads API for a stated window. PyPI candidates come from the simple index, with download counts drawn from a public dataset derived from PyPI's own download statistics.

Exclusions are recorded, not silent. Every candidate that surfaced and was not selected appears in the same file with the reason it was dropped: ranked below the cut, duplicate of a scoped variant, no download figure available. A reader can see what we passed over.

Conflicts. Packages published by Attestari, The Elite360 Corporation, or any account we control are excluded by a hard rule, checked against package name, scope, maintainers, publisher, author, contact addresses, and repository URLs. We do not rate our own work.

A tagged package is not necessarily a server. Selection casts a wide net using MCP-related keywords and naming patterns, which catches libraries and SDKs alongside actual servers. We do not resolve this by maintaining a hand-curated exclusion list. The tool-surface determination in section 5 decides what is a server, and sample figures always separate confirmed server, examined from confirmed server, could not examine from not a server.

Delegation. A selected package can delegate its server to another package: it imports or re-exports the server from a declared dependency, or its only runtime behaviour is to load another package (a one-file compatibility shim left behind by a rename, an entry point that spawns another package's command). The code an agent is then exposed to is the target's, and a rule that examined only the shim would let a publisher move a graded server out of the sample by renaming it. So when the surface determination of section 5 finds that an examined package delegates, the target package joins the population for the same run, whatever its download figures and wherever it stands on the candidate list. It is fetched at the exact version the declared dependency resolves to at the registry when the run examines it (the highest release the specifier admits, pre-releases excluded unless the specifier names one), and it is examined and decided like any selected package. Both rows record the relationship: the delegating package's row carries delegates_to (the target's registry and name, the declared specifier, the version resolved and how it was resolved), and the target's row carries delegated_from (every delegating package in the run, with its version). A target that itself delegates is followed the same way, to the end of the chain and at most three hops deep; a target that is already in the sample is examined once and carries both records. Delegation changes nothing about the delegating package's own outcome: a shim that registers no tools is still refused under no_registration_site (section 9), and the grade of what it runs is on the target's row.

5. The checks

Seven checks are declared in attestari/checks/, and this version states the standing of each. Three run against every subject, and each of those states what it reads and what it is entitled to conclude. Four are declared with their inputs fixed and do not run; a subject is never marked clean on a check that did not run, and a report lists them as not assessed (section 15). None of the seven executes the subject's code. An eighth, remote_code_fetch, ran against every subject from 1.0.0 through 2.2.0 and is retired in this version: what it looked for is recorded by the capability inventory (section 20), read from the syntax tree by the extractor rather than by a check, as facts that carry no severity.

Check Standing in this version May grade
undetermined_tool_surface runs on every subject no: it decides scope and refusal (section 9), never a severity
model_directed_text runs on every subject yes: the one check in MEASURED_CHECKS (section 10, section 18)
remote_code_fetch retired in this version: its patterns are capability entries (section 20) no
publisher_provenance runs on every subject no: decided at info pending measured precision (section 18)
undeclared_network_calls declared, does not run no
dependency_risk declared, does not run no
abandonment declared, does not run no
fork_divergence declared, does not run no

The set of checks and tags whose findings may carry a gradeable severity is MEASURED_CHECKS in attestari/policy.py. In this version it holds model_directed_text with every tag and nothing else (decision D5, section 10). A check enters that set only by a measurement recorded in section 18.

undetermined_tool_surface: can the tool surface be determined at all?

Reads: the source files in the artifact, as syntax trees. Each JavaScript, TypeScript, TSX and Python member is parsed with tree-sitter; the grammars are error-tolerant, so a file with a syntax error still yields a tree and nothing falls back to text matching. Every fact the outcome rests on is a node of that tree: a call site, a definition, an import, an identifier or a string literal. Text inside strings, comments and docstrings never counts as a registration site. Concludes: a parsed set of tools with their descriptions, or one of these outcomes:

  • not_a_server: no registration site, and the source consumes the tool API (it lists or calls another server's tools, imports an SDK client) or defines it (a method named registerTool or tool with a body, a server class). A client, adapter, proxy, SDK, framework, instrumentation, CLI, mock or scanner. Such a subject is out of scope: it is not a server, so it is neither graded nor refused, and it is excluded from every population count (decision D4).
  • minified_only: registration text exists only in minified or obfuscated bundles. The operator cannot inspect what they install; this is a medium finding whose evidence is the bundle's bytes and the length of its longest line, not a coverage gap.
  • runtime_generated: a registration site exists and the file that registers builds its tools from external data (an OpenAPI document, a remote catalog). A genuine limit, reported as such.
  • declaration_not_parsed: a registration site exists in readable source and the extractor could not read it. This is the only outcome that is honestly our gap; the site is recorded.
  • no_registration_site: neither a registration site nor a consumption pattern.
  • archive_unreadable: the artifact is not a readable tar or zip.
  • extraction_budget_exceeded: reading the package's source passed the per-package budget of wall time or resident memory (section 9). Nothing past that point was read, and the row records the measured figures beside the budget.

Every outcome but parsed refuses the grade under its own code (section 9), except not_a_server, which is out of scope. A subject whose surface cannot be read is not thereby condemned. It is a subject about which we will not offer an opinion, because the thing an agent would actually be exposed to was never visible to us.

Registration sites. Tools are read only at the places where a server registers them, and nowhere else. In JavaScript and TypeScript those are .tool(), .registerTool(), .addTool() and defineTool() calls; setRequestHandler(ListToolsRequestSchema, handler) and setRequestHandler("tools/list", handler); case "tools/list": and if (method === "tools/list") dispatchers; declarations typed Tool or Tool[]; a class with getName() and getConfig(); and a table under tools: handed to a factory whose name contains Server or mcp. In Python they are the @x.tool decorator, the call form x.tool(...)(fn), x.add_tool(fn), and a Tool(...) reached from a @x.list_tools() handler or a list[Tool] declaration. A tool-shaped table that no registration site reaches (an agent framework's function definitions, a CLI command list, a documentation map, a test fixture, a schema table whose registered names are built at run time) is not a tool.

Identifier resolution. A name, a description or a schema written somewhere other than the registration call is followed to its text through the tree: through declarations and later assignments, destructuring, for...of, for...in, forEach and map, function parameters (through the call being resolved and through the call sites of the function, new X() and super()), function returns, { ...spread }, this.x through the class body and its constructor chain, import and require of a sibling module of the same package, Object.values, Object.entries and Object.assign, .push(), .filter, .map and .concat, f.call(thisArg, ...), and wrappers that pass a value through unchanged. The extractor does not follow a feature flag that gates a merge, a tool list built inside an asynchronous function from awaited data, a member whose name is computed, or a registration that happens in another package. What it cannot resolve is recorded as such, never guessed.

Description states. A description that resolves to a string literal, or to a concatenation of literals with no substitution, is readable. A template literal or an f-string with substitutions stays partial: its literal segments are read as their own byte spans and each substitution is a hole, whatever the resolution above could say about the value substituted. A description the resolution above does not carry to literal text is unreadable. The three states and what each one permits are defined under model_directed_text, below.

Vendored code. Bundled third-party code inside the artifact (a copy of an SDK, a schema library, a GraphQL implementation), recognised by a vendor-style path segment, by a bundler's module comment, or by the library's own vocabulary on the tree, is listed in the surface's vendored inventory with its path, the library and the line range. The checks examine it like any other member, and it is never a source of tools: a registration-shaped call or a tool-shaped object inside vendored code registers nothing of the subject's.

Minified bundles. A member whose longest line exceeds 800 bytes, or whose lines average more than 200 bytes, is a minified or obfuscated bundle. It is parsed like any other member when it is the file being read, and an import from readable source still resolves into it, since a generated tool table is often one long line. It is never a call site of a wrapper defined elsewhere: the calls inside a bundle are not indexed, so a registration helper declared in readable source is not followed to its call sites inside a bundle. Registration text that exists only in bundles is minified_only, above.

Extractor version. The extractor carries its own version (EXTRACTOR_VERSION in attestari/surface/extract.py), recorded by every run and on every decision row (section 12). This version of the document describes the syntax-tree extractor syntax-tree-3, which reads the whole MCP surface below and the capability inventory of section 20; the versions before it described the earlier syntax-tree readers and, before 2.1.0, a token-stream reader. The pair of methodology version and extractor version is checked against the version table in attestari/methodology.py before anything is decided, and a run whose surfaces were read by an extractor this document does not describe is not decided under it.

The rest of the MCP surface: prompts, resources and resource templates

A server exposes more than tools, and since this version the extractor reads the rest of what it declares, at the registration sites of each kind and through the same identifier resolution as tools. Every entry is recorded on the surface row with its kind.

Prompts. In JavaScript and TypeScript, .prompt() and .registerPrompt() calls, and the prompts array a setRequestHandler(ListPromptsRequestSchema, handler) or "prompts/list" dispatcher returns; in Python, the @x.prompt decorator, x.add_prompt(Prompt(...)), and a Prompt(...) reached from a @x.list_prompts() handler. For each prompt the name, the description and the argument schema are recorded, the description in one of the three states below. A prompt's messages are text the client sends to the model, so they are recorded too: where the handler returns them as literal text (a string, a template, a message object whose text is a literal, a list of message constructors), each literal is its own prompt_text entry with its own byte spans, a template with substitutions being partial like any other; where the handler builds its messages some other way, or the messages come from a prompts/get handler the extractor does not read, the prompt carries one prompt_text entry recorded as unreadable. The text a client receives exists in either case, and the second case says it was not read.

Resources and resource templates. In JavaScript and TypeScript, .resource() and .registerResource() calls, whose second argument is a URI literal (a resource) or a new ResourceTemplate(...) (a template), and the resources and resourceTemplates arrays the matching list handlers return; in Python, the @x.resource("uri") decorator (a template when the URI carries {} parameters), x.add_resource(Resource(...)), and Resource(...) and ResourceTemplate(...) reached from @x.list_resources() and @x.list_resource_templates() handlers. The name, the URI or URI template, and the description are recorded.

Sampling and elicitation. Presence only: the first place the source asks the client to sample (createMessage / create_message) or to elicit input from the user (elicitInput / elicit) is recorded as a file and line on the surface row. Nothing is read from the call.

How they are examined. Every description among them, and every readable prompt_text entry, is examined by model_directed_text under exactly the rules stated for a tool description below: the same three states, the same three categories, the same verifier, the same MEASURED_CHECKS standing, and the same gate consequences (an unreadable description or prompt text is not_analyzed; a partial one with nothing found in its readable segments refuses the grade as partial_description). A finding carries the kind of entry it quotes as surface_kind (tool, prompt, resource or resource_template, a prompt's message text counting as prompt) and the tag surface:<kind> for every kind but tool, so a finding on a tool keeps the identity it had before the other kinds existed. The attested manifest of section 7 is still a manifest of tools; prompts and resources are not hashed into it in this version.

model_directed_text: does a tool description contain instructions aimed at the model?

Reads: the literal text of each parsed tool description, and since this version each prompt description, each readable literal of a prompt's message text, and each resource description (the subsection above), passed to a language model as clearly delimited evidence, never as free text that could be read as instruction. What follows is written for a tool description and applies to every entry the same way. Concludes: specific quoted spans that direct the reading model's behaviour, each categorised and verified.

Before any description is read, each tool's description is in one of three states, read from the parsed surface, never guessed:

  • readable: a single literal, or a concatenation of literals with no substitution ("a" + "b", ["a", "b"].join("\n")). Every byte the client receives is in the source, and the check reads all of it.
  • partial: a template with substitutions (`Reads ${what}`, an f-string). Each literal segment is read as its own byte span; each substitution is a hole, recorded with its position, that the verifier never matches inside. A finding in a segment is a valid finding. The absence of findings in a partial description does not make the tool clean: a package with a partial tool that produced no finding is refused a grade (partial_description, section 9) unless a high finding already places it at F, where the floor is established by evidence.
  • unreadable: a variable, a property, a call, a configuration passed by reference that the identifier resolution above does not carry to literal text, or a minified bundle. The text a client will receive exists and was not examined. Each such tool is recorded as not_analyzed with the reason descriptions_not_statically_readable, and the package is refused a grade by the gate in section 9.

A tool that declares no description at all (absent) has nothing for this check to read, and for this check that is a clean outcome (no_descriptions_present). A surface that does not record the state of a description is treated as unreadable.

A tool description is read by the agent, not by the user. Text placed there can instruct the agent to exfiltrate data, override the user's stated intent, or conceal what it is doing. Those three categories, exfiltrate_data, override_user and conceal_from_user, are the only ones that become findings, and each carries severity high. Routing, ordering and rate guidance ("call X first", "no more than three calls") is never a finding. The check records only a count of such spans per tool; the spans themselves are transcribed only by a separate inventory pass that a run must opt into (attestari run --inventory), and nothing that decides reads it.

Two boundaries of those categories are stated here because real descriptions have tested them. Parameter documentation is not a finding unless the parameter requests data outside the tool's declared purpose: a parameter that must carry the user's prompt, the user's credentials, files, conversation history, or configuration the tool does not need to do what it says is exfiltrate_data, wherever in the description the text appears and however it is phrased. Documenting a parameter does not license routing data through it. Retry and resume guidance about the tool's own calls after an interruption ("keep the run id and resume", "an interrupted call is not a cancellation") is routing guidance, not override_user, unless it directs the model to disregard an explicit stop or cancel from the user.

Every proposed span is checked by a deterministic verification step: the quoted text must occur byte for byte inside the stored file, within the description of the named tool. A proposal that step rejects never becomes a finding. The verifier carries its own version (VERIFIER_VERSION in attestari/checks/model_directed_text.py), recorded on every decision row; a change to what it accepts or rejects bumps that version.

The analysis prompt encodes the evidence so that it cannot be interpreted as instruction by the model reading it. This is both a correctness measure and a safety one: an analyser that can be manipulated by the text it audits is not an analyser. The SHA-256 of the prompt bytes and the name of the model are recorded with every decision (section 12), so a reader can tell which analyser produced a row.

Context tags on exfiltration findings

An exfiltrate_data finding says that a description routes data through the tool that the tool does not need. It does not by itself say where the data goes, whether the publisher said so, or whether the operator can turn it off. Three informational tags answer those questions from the package bytes alone, and each is applied only when the analyser can establish it; a tag that cannot be established is absent, and the decision row records why. None of the three changes a severity, a decision or a grade, and none is part of the finding's identity. They are read by the deterministic analyser in attestari/checks/exfil_context.py, at decision time, from the stored artifact:

  • destination_publisher_owned: every network destination the source names is on a domain the package declares as its own, that is the host of its homepage or repository URL, read from the manifest inside the artifact (package.json, pyproject.toml, PKG-INFO) or from the registry record. A shared code host or a registry (github.com, gitlab.com, npmjs.com, pypi.org) is never a publisher's own domain. A network destination is the host of an absolute URL literal in a source file that also makes a network call, or assigned to a name that a network call uses.
  • disclosed_in_readme: a line of the README names a transmit host together with a sending verb, or discloses telemetry (telemetry, usage data, instrumentation, analytics, with collect, send, report, transmit, upload or share). A documentation link that merely mentions the domain is not a disclosure.
  • opt_out_available: a source file that reads the environment names an opt-out variable (OPT_OUT, DO_NOT_TRACK, DISABLE_TELEMETRY and their variants), or a source file that tests for a file names an opt-out file.

The analyser reads text; it executes nothing and traces no data flow. The tags therefore say what the package declares and names, not what a particular byte of the user's data reaches. A reader weighing a finding should read them as exactly that.

Reads: registry metadata, publisher attestations, maintainer records, repository ownership, commit signatures. Concludes: what is present and what is absent in the chain between a named party and this artifact.

Provenance findings describe absences more often than problems, and absences are common: most registry packages carry no build attestation and no signed commits. We report them because their accumulation is meaningful, not because any one of them is damning. Severity reflects this: a single provenance tag is low, and a finding reaches high only when a second, independent provenance tag on the same package corroborates it (decision D6).

The account-age rule (account_younger_than_package: the account that owns the source repository was created after the package's first release) is informational pending measured precision (section 18). A registry name transferred to a new owner is enough to trigger it, so until its precision is measured it is decided at info (decision D5), as is every other provenance finding.

The four declared checks that do not run

undeclared_network_calls (hosts contacted by the code that neither the README nor the tool descriptions mention), dependency_risk (known advisories, unpinned or deprecated direct dependencies, install-time scripts), abandonment (archived repositories, no commits in a stated period, unanswered issue growth) and fork_divergence (packages published from forks, and the byte-level difference between shipped code and upstream) are declared with the inputs each will consume, so that the evidence a run collects already covers them. They produce no result row, and the gate (section 9) requires a result row only of the three checks that run. A report lists them as not assessed, which means exactly that. The network destinations the capability inventory records (section 20) are the raw material of the first of them, and are published as inventory, not as a finding, until the check runs and is measured.

6. Severity

Severity describes exposure to the person who installs and runs the subject. It is not a measure of intent, and it makes no claim about whether a publisher meant any harm.

High: an operator running this subject is exposed to something they would not reasonably expect from its stated purpose: code they cannot inspect, credentials reaching a destination they did not choose, or instructions to the agent that work against the operator.

Medium: a property that materially weakens the operator's ability to know what they are running, or that concentrates control in a way worth knowing about before installing.

Low: a property of the publisher record that leaves the operator with no one to hold to account, without by itself changing what the package does. A provenance finding with a single tag is decided low; a second, independent tag on the same package raises it to high (decision D6, section 10).

Info: a factual observation, usually an absence, that is common across the registry and meaningful mainly in aggregate or alongside other findings. It is published as context and has no effect on the grade.

We do not use a borrowed scale such as CVSS. These are not vulnerabilities with exploit mechanics; they are properties of what a package does and what can be known about it. The grade (section 11) is read off the highest confirmed severity.

7. Evidence and verification

Every artifact we fetch is stored by content hash. Every finding cites the hash of the evidence it came from, the path within it, and the line range, alongside the quoted span itself.

At decision time, the quoted span is re-verified byte for byte against the stored evidence (guards G6 and G7 in section 9). A span that does not re-verify does not become a published finding, and its subject becomes ungradeable rather than passing quietly.

Every published report carries instructions for reproducing its central claims from public sources in a few minutes, without our software and without trusting us. If you cannot check a finding yourself, we have not finished writing it.

No personal data in published evidence. A published blob never contains an email address, a phone number or a street address. This is a rule, not a judgement. Evidence is published in one of two forms, defined exactly in docs/publish-contract.md (section 9a):

  • A finding that cites a structured upstream response (a registry record, a GitHub API response) is published with a projection: a canonical JSON object carrying only the member the finding cites, copied byte for byte, the JSON pointer of the object it was taken from, and the SHA-256 of the whole upstream response as provenance (source_sha256). The projection's own hash is the evidence hash the published finding verifies against; the upstream bytes are never published.
  • A finding that cites a file from the package artifact is published with that file whole: it is public code. It is scanned all the same.

Every blob of either kind is scanned for the patterns the contract lists before it is published. A finding whose evidence cannot be published without personal data is withheld, not redacted and not softened, and its subject is published as not_gradeable with the reason evidence_unpublishable. The stored evidence, the decision and the finding id are untouched; only publication is refused.

Attested manifest

For each parsed tool whose description is readable (section 5), the tool hash is SHA-256 over: the tool name in UTF-8, one 0x00 byte, the description bytes exactly as extracted (description_raw), one 0x00 byte, and the schema component. When the declared input schema is a JSON literal, the schema component is its canonical JSON (RFC 8785: sorted keys, no insignificant whitespace, UTF-8). When it is any other expression, a zod schema, a builder call, a reference to a constant, the schema component is the schema's source bytes exactly as extracted, taken the same way description_raw is: a zod schema is hashed as the bytes of its expression, and only a JSON literal is canonicalised. When no schema is declared, the component is empty. The manifest hash is SHA-256 over the tool hashes, as 32-byte digests, sorted bytewise and concatenated; hexadecimal is only how either hash is printed. Both are computed by attestari/manifest.py, and a fixed-vector test pins this definition to that code.

A tool whose description is not readable has no tool hash, and a subject with any such tool has no manifest hash: the manifest is not attested, which is an outcome, not an omission. Both hashes are recorded on every decision row of the subject (tool_hashes, one entry per parsed tool with its hash or the reason it has none, and manifest_hash; section 12), and published with the result. The lookup API serves them for a package version at GET /manifest/:name/:version, so a gateway can pin the tools it was shown to the tools this pass examined.

8. Inputs to a decision

A run directory written by attestari run (see attestari/pipeline.py): findings.json, results.json, gradeable.json, surfaces.json, records.json, not_analyzed.json, summary.json, plus the content-addressed evidence store the run cited (evidence/blobs/<sha256>). The policy reads these files and the blobs. It never calls the network or a model.

9. Package gradeability (fail closed)

The failure we care most about avoiding is not a wrong grade. It is a passing grade on something nobody looked at. A subject that could not be examined must never be indistinguishable from one that was examined and found clean.

So the grading gate fails closed. A package is graded only when every guard below holds. Any guard that fails, or cannot be evaluated, makes the package not_gradeable; every reason found is recorded, in order, and every finding of that package is withheld. There are no exceptions and no severity that is exempt.

  • G1 gate recorded. gradeable.json exists and has a row for the package. A missing file blocks every package in the run; a missing row blocks that package.
  • G2 gate passes as recorded. The row says ok: true.
  • G3 gate passes when recomputed. attestari.grade.gradeability is re-run at decision time from results.json and the package's surfaces.json row (tools with descriptions, the entries parsed of every kind, parsed tool names, advertised tool names) and must return ok. A package with no surfaces row cannot be recomputed and is blocked.
  • G4 no tool left unanalysed. No not_analyzed entry for the package in results.json or in not_analyzed.json. A refused, truncated or unreplayed model call is an outcome, never a clean result.
  • G5 every check ran and none errored. Each configured check (publisher_provenance, undetermined_tool_surface, model_directed_text) has a result row for the package, and summary.json records no failure for the package that is a crash: a failure row whose stage is fetch or a check id, or that carries a traceback. Recorded HTTP failures of a source (a 404 repository, an unreachable website) are inputs to the checks, not crashes, and do not block by themselves.
  • G6 every excerpt re-verifies byte for byte. For each finding of the package: the cited blob exists in the evidence store, and the UTF-8 bytes of evidence.excerpt occur inside it. When the finding cites lines (line_start..line_end), the bytes must occur within those lines. An empty excerpt fails. One unverifiable finding blocks the whole package.
  • G7 model-derived findings passed the verifier. For each model_directed_text finding that quotes a description or a prompt's text (every one except description_reassigned_at_runtime), the excerpt must lie inside one of the description spans of an entry (a tool, a prompt, a prompt's message text, a resource or a resource template; section 5) declared at the same path in the package's surfaces.json row: the same test attestari.checks.model_directed_text.verify_proposal applies when the proposal is made. A proposal the verifier discarded is never a finding; a finding that would not pass the verifier now blocks the package.

The gate recomputed in G3 refuses a package for any of the following reasons, and records each one it finds:

Reason code Meaning
no_surface_examined No tool, prompt, resource or template was parsed from the published source. The subject's advertised behaviour was never visible. A server that exposes prompts, resources or templates and no tool is graded like any other when every entry that was found was fully read and analysed; the rules above decide that. (Through 2.3.0 this row was no_tools_examined and counted tools alone.)
advertised_tools_not_parsed Documentation advertises tools the extractor did not find. The mismatch is itself the finding.
not_analyzed One or more entries could not be analysed: a model provider declined the request, or a tool, prompt or resource declares a description (or a prompt returns message text) the extractor could not read as a literal (descriptions_not_statically_readable, section 5). Either is recorded as its own outcome and never counted as a clean result.
model_check_skipped The description analysis did not run on a subject that has parsed tool descriptions.
partial_description A description (of a tool, a prompt, a resource, or a prompt's message text) was read with holes and nothing was found in its readable segments; that does not establish a clean entry (section 5). Not applied when a high finding already places the package at F.
minified_only, runtime_generated, declaration_not_parsed, no_registration_site, archive_unreadable The surface outcome of section 5, refusing the grade under its own name. not_a_server is not a refusal: the package is out of scope (D4).
extraction_budget_exceeded Reading the package's source passed the per-package budget of wall time or resident memory, so nothing past that point was read and no tool list exists to grade. The budget is measured, not estimated: every row records the wall time and the resident memory its extraction took beside the budget it ran under, and those figures are published with the row.

The gate map. Every reason code a decision or a publication can carry, the guard or decision that records it, and one plain-English line for each, is the table GATE_MAP in attestari/policy.py. attestari decide exports it as analysis/gate_map.json beside the code and as gate_map.json inside the run, and the website reads the exported file rather than restating it. A test fails when a reason code known to the policy or to the gate has no entry, and when the exported file differs from the table. Each code also has a fixture under analysis/fixtures/reason_codes/<code>/, a run directory and the evidence it cites, on which attestari decide (or, for evidence_unpublishable, attestari publish) records exactly that code; a test decides every fixture and fails when a code has none.

"We cannot tell you what this does" is a legitimate published outcome, and for a meaningful share of popular packages it is the accurate one.

10. Finding decisions

The policy named severity-ladder decides each finding of a gradeable package:

  • D1 confirmed. A verified finding whose decided severity is high, medium or low is confirmed at that severity. The decided severity starts from the severity the check assigned and is changed only by D5 and D6 below; the row records both.
  • D2 noted. A verified finding whose decided severity is info is noted: published as context, no effect on the grade.
  • D3 withheld. Every finding of a package that is not gradeable is withheld, and its row lists the package's blocking reasons. A withheld finding is never published as a finding.
  • D4 out of scope. A package whose surface is not_a_server (section 5) is out of scope: it is published with the verdict out_of_scope and the reason not_a_server, every finding of it is withheld, and it is counted apart from the population rather than as a refusal. Out of scope is not a grade and not a refusal; it says the artifact is not the kind of thing this methodology rates.
  • D5 measured checks only. Only a check whose precision has been measured (section 18) may place a finding above info. The policy carries the set of measured (check, tag) pairs, MEASURED_CHECKS; in this version it holds model_directed_text with every tag and nothing else. A finding from any other check or tag is decided at info and noted, whatever severity the check assigned; the assigned severity stays on the row. Every check still runs and every finding is still recorded. Adding a pair to the set is a methodology change that must cite the measurement in section 18.
  • D6 provenance corroboration. A publisher_provenance finding is decided high only when a second, independent provenance tag is present on the same package; a single tag is decided low. D6 is applied before D5, so while publisher_provenance is unmeasured the outcome is still info.

There are no suppression rules in this version. A finding that should not count is a methodology change (add a rule here, bump the version) or an appeal (section 16), never an edit to a run.

11. Package grade

From the confirmed findings of a gradeable package:

grade condition
A no confirmed finding (only noted context, or nothing)
B highest confirmed severity is low
C highest confirmed severity is medium
F any confirmed finding of severity high

A package that is not gradeable has no grade; its row carries blocked_by instead.

12. What every decision row records

Every row, package or finding, carries:

  • policy, methodology_version, methodology_sha256: the rules it was decided under, by number and by the hash of this document's bytes;
  • code_commit, the git commit of the code that decided (code_dirty says whether the tree had uncommitted changes), and decided_at;
  • analyzer_model, the model the run's description check called (empty when the run made no model call), prompt_sha256, the SHA-256 of the findings call's system prompt bytes, and verifier_version, the version of the deterministic verifier (section 5). The run header records the prompt layout and version beside them, and says whether the prompt hash was recorded by the run itself or reconstructed from the code at decision time;
  • extractor_version, the version of the surface extractor (section 5) that read the tool surface the row was decided on, which with methodology_version forms a pair the version table in attestari/methodology.py must allow before anything is decided;
  • tool_hashes, one entry per parsed tool of the subject with its tool hash or the reason it has none, and manifest_hash, the attested manifest of section 7, or the reason the subject has none;

and every package row also carries capabilities, the capability inventory of section 20 as the surface row recorded it, each entry with its own verification against the evidence store, capabilities_omitted, the number of distinct facts beyond the row cap, and mcp_surface, the shape of the surface of section 5 (how many tools, prompts, prompt texts, resources and resource templates, and whether sampling and elicitation appear); and every finding row also carries the finding's evidence_sha256 (the blob it was verified against) and excerpt_sha256, the verification outcome, the rule that produced the decision, surface_kind, the kind of entry a model_directed_text finding quotes (section 5), and, on an exfiltrate_data finding, the context tags of section 5 with the evidence for each. The run-level header repeats the stamps and records the SHA-256 of every input file. The rows are written twice, as one document (decisions.json) and as one JSON object per line (decisions.jsonl: the header, then each package, then each finding, every line carrying the stamps), and summarised for reading in decisions.md. A rerun over unchanged inputs with the same methodology and code produces identical rows apart from decided_at.

13. Automated decision, human responsibility

Findings are decided by the rules in this document, applied by software, without a person reviewing each one. This is a deliberate choice. Rules applied by a machine are applied identically to the two-hundredth subject and the first; a person working through hundreds of findings is not.

What is not automated is the rules themselves. Changing them is an explicit act that produces a new version of this document (section 21), and every finding cites the version that produced it. The methodology, not any individual judgement call, is what stands behind a rating.

Running the rules is not a human act and needs no approval. The two things a person does:

  1. Appeals. A vendor disputing a finding files an appeal against its finding id (attestari appeal file). A person resolves it (attestari appeal resolve) as upheld, dismissed or withdrawn. Appeals are an append-only event log beside the run (appeals.json); nothing in decisions.json is edited. Publication applies them: an upheld appeal publishes the finding as withdrawn_on_appeal and recomputes that package's grade without it; an open appeal publishes the finding marked under_appeal, unchanged. What an appeal can be about, and how we answer one, is in section 16.
  2. Methodology changes. Editing this document and the matching constant, as described in section 21.

Publication is separate. Producing findings and publishing them about a named third party are distinct steps. Automation covers the first. The second is attestari publish <run>: one command, not a review queue, run only after the disclosure period in section 19. It refuses a run whose decisions were made under a methodology version or document hash other than the current one, and never overwrites an earlier publication.

14. Scheduled operation

attestari cycle --candidates <file> performs one cycle with no human input: it resumes the most recent failed or cancelled run over that candidates file if there is one, otherwise starts a new run; then decides the run under this methodology, diffs it against the previous decided run over the same candidates file, and writes decisions.json, decisions.jsonl, decisions.md and diff.json into the run directory. --every <seconds> repeats on that cadence in-process; a scheduler may instead invoke the one-shot form. The scheduled pass runs weekly.

15. Known limits

These are the things a passing result does not tell you. They are stated here rather than in a footnote because a reader who does not know them will over-read our findings.

We do not run the code. Analysis is static. A subject that behaves differently when executed, against a particular host, after a delay, or only in the presence of certain credentials, presents none of that to us.

We do not examine binaries. Where a package's substance is a compiled executable it downloads, we can describe the launcher and the fetch precisely, and we can say nothing whatsoever about what the binary does.

Text assigned at runtime is invisible to us. A tool description that is benign in source and rewritten when the module loads reaches the agent in its rewritten form. We detect and report that a description is reassigned at runtime, but we cannot read what it becomes.

Tool names built by expression may be missed. Where a tool's name is computed rather than written literally, our extractor can fail to find a tool that genuinely exists. A name, a configuration or a description written somewhere other than the registration call is followed to its text through the syntax tree by the identifier resolution section 5 lists, including through a sibling module of the same package; what that resolution does not carry to literal text is reported as declaration_not_parsed (section 5), and the gate in section 9 catches an advertised tool we did not find as a mismatch rather than passing the subject silently.

A minified bundle is opaque to us and to you. Where a package ships its registration only in a minified or obfuscated bundle, we report that as a medium finding and read nothing from it. We do not reconstruct source from a bundle.

Tools generated at run time are not in the artifact. Where the file that registers tools builds them from an OpenAPI document or a remote catalog, the tool list exists only when the server runs. We report the site and read nothing further.

Prompt text built at run time is unreadable to us. A prompt whose handler assembles its messages from data, from a file, or in a prompts/get handler the extractor does not read reaches the model as text we never saw. We record the prompt and say its text is unreadable, and the subject is refused a grade on that account (section 9), never passed on the strength of its description.

A publisher who reads these rules can write around the model check. Section 5 publishes the classification rules in full, and the language-model check applies them to prose. A description written with those rules in hand can carry the same instruction in a form the model does not recognise, and the check is measured (section 18) only against cases that were not written against it. The deterministic checks do not share that weakness: the description states, the surface taxonomy, the capability inventory and the reassignment finding read structure, not meaning, and a description that is rewritten when the module loads is reported as such whatever the rewritten text says. A clean model check on a package whose author has read this document is weaker evidence than a clean model check on one whose author has not.

The context tags and the capability inventory read text, not behaviour. A destination is a host the source names; a disclosure is a sentence the README carries; an opt-out is a variable the code reads. Where a destination is computed, a disclosure lives outside the README, or an opt-out is honoured by some code paths and not others, the tags and the inventory do not see it, and they say nothing either way.

We do not diff the artifact against the repository. A file present in the published package with no counterpart in the source tree is a signal we do not currently produce. See section 3.

Four declared checks do not run. Undeclared network destinations, dependency risk, maintenance status, and fork divergence are declared in section 5 and not assessed. Where a report lists these as not assessed, that is what it means, not that they were assessed and found clean.

A finding is about one version, at one time. Registries are mutable. A result describes the artifact we fetched, identified by hash, on the date stated. It says nothing about what the same version number serves tomorrow.

16. Notice, appeals, and corrections

Notice before publication. Before any finding about a named subject is published, we send it to the publisher at the contact address in their registry record or repository, and wait 30 days. The same finding goes to the registry on the same day (section 19). That period is for correcting us, and a publisher is under no obligation to respond.

Appeals. A publisher may dispute any finding at any time, before or after publication, by writing to appeals@attestari.ai. An appeal that identifies a factual error, such as a misquoted span, a wrong line reference, or a claim about code that is not there, is checked against the stored evidence. If we are wrong, we correct it.

An appeal that disputes our interpretation rather than our facts will be considered and may be published alongside the finding. We do not remove an accurate finding because a publisher objects to it.

Corrections. Corrections are published, dated, and describe what was wrong and what changed. We do not silently edit a published finding.

Remediation. When a publisher fixes the thing a finding describes, a re-examination of the new version is free and prompt, and the updated result is published.

17. Independence

A publisher cannot pay us, in money or in kind, for a rating, for a better rating, or for the removal of a finding. There is no paid review, no expedited review, and no arrangement under which a rated party influences its own result.

The subjects we examine are chosen by the mechanical rule in section 4. They are not chosen by request, and they are not chosen by who has paid for anything.

Where Attestari earns revenue, the source is disclosed. Any commercial relationship with a party that is also a subject is disclosed on that subject's published result.

Publisher tools. Attestari may offer a publisher a tool that applies these checks to an artifact that has not been published. Its output is never published, never stored where the scheduled pass can read it, and never affects a grade. The public grade of a subject is computed only from the registry artifact, only by the scheduled pass, only under the methodology in force, whether or not the publisher used such a tool. The same holds for every other thing sold to the people deciding whether to install: a lookup, a manifest, a watch, a badge, an attestation of inspection. Each shows what the scheduled pass published and never changes it.

18. Measured performance

These figures are preliminary. They are measured on a small held-out corpus, and they are restated whenever the corpus or the check changes; they are evidence that the check finds known attacks, not a general detection rate. None of the measured cases was written against the rules in section 5 (the adaptive adversary, section 15).

The corpus. The held-out half of the disclosed-cases set (seeds/heldout/cases.json) records nine cases. Four carry a published sample and are measured: three tool-poisoning disclosures by third-party security researchers and one in-the-wild observation from our own run (blender-mcp 1.9.1), with 39 expected spans between them (23 in the three disclosures, 16 in blender-mcp). The other five are pattern-only records whose disclosure printed no sample; they are counted in the corpus and never measured. Three further disclosed cases are worked examples inside the prompt (seeds/fewshot/) and are never measured. Precision is measured against a control set of 456 tool descriptions in 16 widely-used packages reviewed by hand (runs/twenty/surfaces.json).

Detection and precision, as a range across four independent samples of the same prompt version (the model is sampled once per call, so two runs of the same evaluation can differ):

  • 4 of 4 measured cases detected in every sample, both in-the-wild cases among them
  • 32 to 33 of 39 expected spans found, every one in its expected category: 16 to 17 of 23 on the three disclosures (17 in one sample of four), 16 of 16 on blender-mcp in every sample
  • 0 false positives across 456 clean tools in every sample
  • 0 proposals rejected by the verifier in any sample

Six spans were missed in every sample and one further span was found in one sample of four; the other 32 were found in all four. Every missed span sits in a description on which other spans were found; no measured case was missed whole in any sample. The rug-pull case whose poisoned text is assigned when the module loads, and so never appears in the source we read (section 15), is now a worked example in the prompt and is not measured.

Where the figures come from: the four evaluation runs recorded at runs/eval-heldout-2026-09-14.4, runs/eval-var-1, runs/eval-var-2 and runs/eval-var-3 (2026-09-14, claude-opus-5, prompt version 2026-09-14.4, the separated prompt layout, each against an evidence store whose proposal cache was empty so that every call went to the model), scored by attestari eval against the held-out manifest and the clean set above. In those evaluations only, each seed sample's evidence block cited the publication that disclosed it; a production run carries no such citation.

The admission test. A check enters MEASURED_CHECKS (decision D5, section 10) only by passing this test, and no check enters it without passing it: at least 200 labelled spans, precision at least 95%, at most 1 false positive per 500 clean descriptions. The figures the test is applied to are those recorded in this section, from the evaluation runs named above, and any restatement of them is dated here. For model_directed_text, the check in the set, the recorded figures are: 456 clean descriptions with 0 false positives in every sample (at most 1 per 500 holds); 32 to 33 spans found of 39 expected with 0 false positives, a precision of 100% on the found spans (95% holds); and a labelled corpus of 39 expected spans in the four measured cases beside the 456 descriptions labelled clean. The corpus holds tool descriptions only. This version applies the check to prompt descriptions, prompt message text and resource descriptions under the same rules; no labelled corpus of those exists yet, the admission test has not been run on them apart, and a reader should weigh a finding on a prompt or a resource with that in mind. Restating the corpus is a methodology change (section 21).

The shortfall. model_directed_text was admitted on 39 attack spans and 456 clean descriptions, against a published bar of 200 labelled spans and 1,000 clean descriptions. The corpus is being built to that bar. Until it is, the "measured" label carries this note on every page that shows it.

The population pass. The scheduled pass over the 200 most-downloaded candidates of both registries (runs/20260914T234604Z, candidates file top200.json, decided 2026-09-15 under prompt version 2026-09-14.4 with claude-opus-5) records, over the 200 packages selected:

  • 21 out of scope (not_a_server), leaving a population of 179 confirmed or presumed servers
  • 61 gradeable: 59 graded A and 2 graded F, both on exfiltrate_data findings of the description check (blender-mcp 1.9.1, sixteen findings; @shopify/dev-mcp 1.15.2, one finding)
  • 118 not gradeable, every one under not_analyzed recorded by the gate (G2) and confirmed when recomputed (G3); the gate's own codes over the population: no_registration_site 46, advertised_tools_not_parsed 41, not_analyzed 22, declaration_not_parsed 16, partial_description 11, minified_only 6, runtime_generated 2, archive_unreadable 1
  • 553 findings recorded: 17 confirmed, 181 noted at info (53 of them decided at info by D5, 34 from publisher_provenance and 19 from remote_code_fetch, the latter now the capability inventory), 355 withheld with their packages

These counts are counts over the selected sample (section 4), not over either registry. A grade of A in that pass means the one measured check found nothing in every readable description of the subject; it does not speak to the four declared checks that do not run, or to anything in section 15. The same run, decided again under this version, produces the same grades: the changes of this version add what a row records and change nothing a row decides.

19. Disclosure

Disclosure runs on a fixed clock, the same for every subject, and the clock is not ours to shorten or extend for anyone.

Day 0, registry and publisher. On the day a finding of severity high or medium is decided against a named subject, the finding, its evidence and the methodology version go to the registry's security contact (npm and PyPI each publish one) and to the publisher at the contact address in their registry record or repository. A finding a registry can act on before publication is worth more to the operators exposed to it than a finding published first.

Day 30, publication. The decision is published thirty days after day 0, by the command in section 13, in the form of docs/publish-contract.md. It is published whether or not the publisher or the registry has responded, with any open appeal marked as such (section 16) and any correction the notice period produced already applied. A fixed version is examined and published on the same clock as any other version.

What is never disclosed early. Nothing goes to anyone before day 0, and nothing goes to any party other than the registry and the publisher before day 30. A commercial customer of Attestari sees a finding when everyone else does.

Noted findings (severity info, decision D2), out-of-scope packages and refused packages are published on the same day-30 clock without a day-0 notice: they state an absence, a scope or a refusal, not a risk that a registry can act on.

20. Capability inventory

A grade says whether the description check found an instruction to the agent that works against the operator. It does not say what the package's code can reach. The capability inventory does, as far as text can: for each examined subject it records what the artifact's source can reach, as facts read from the syntax tree by the extractor (attestari/surface/capabilities.py), with no model involved, no severity, and no effect on any decision or grade. It is never graded.

What is recorded. One entry per distinct fact, a fact being a kind, a tag and a target, on the package's surface row and, from there, on its decision row (capabilities, section 12):

  • network: a call that opens a connection (fetch, http/https, axios, got, ky, undici, WebSocket, net/tls; requests, httpx, urllib, aiohttp, websockets, socket and the like). The target is the host of the URL the call is given when that URL is a literal, or a template or f-string whose literal prefix carries the host; the literal itself when it is not a URL; dynamic when there is no literal to read.
  • filesystem: a read or a write (read / write) through node:fs and fs/promises, Deno and Bun file calls, open(), pathlib, os, shutil, tempfile, aiofiles. The target is the path literal, followed through path.join, Path(...) / ... and os.path.join when every part is a literal, else dynamic.
  • process: a process spawned (spawn: an executable with arguments) or a shell command run (exec: a command line handed to a shell), through child_process, execa, subprocess, os.system, os.exec*, asyncio subprocesses. The target is the command literal or dynamic.
  • environment: a variable read (process.env.X, Deno.env.get, os.environ, os.getenv). The target is the variable name, or dynamic when the name is computed or the whole environment is read.
  • credentials: an environment variable whose name matches a secret pattern (env: key, token, secret, password, credential, private key and their variants), or a path literal that names a known credential file (file: .ssh, .aws, .netrc, .npmrc, .pypirc, .git-credentials, .kube, .docker, .env, credentials.json and the like). The entry records that the source names it.
  • remote_code: code obtained or built at run time: dynamic_evaluation (eval, new Function, vm.*, exec), remote_import (an import() or require() of an http(s) URL, a pip install from a URL), dynamic_import (an import whose specifier is computed), and the install scripts of package.json that download (install_script_download) or download and run something (install_script_download_execute): the one kind read from a manifest rather than from a tree.

Every entry cites its file, its line, the SHA-256 of that file and the exact bytes of the call, a span like every other span this document records: it is verified byte for byte against the stored evidence when the row is decided (the file is recovered from the stored artifact and checked against its SHA-256; the inventory does not copy its files into the evidence store, the artifact being there already), and the verification outcome is on the entry. Within one file, a fact seen more than once is one entry carrying a count. A minified bundle is read like any other member and its entries say so (bundle); vendored regions are skipped. A subject whose extraction stopped at the budget (section 9) has no inventory: nothing read in part is presented as an inventory of the whole.

What is not recorded. Any inference. The retired remote_code_fetch check (1.0.0 through 2.2.0) concluded that a package downloads and runs something by tying a network call, a file write and a spawn together through identifiers; this version records the network call, the write and the spawn each as a fact and leaves the conclusion to the reader. The inventory reads names as written: a client bound under another name, a wrapper around a library call, a computed member, a call reached through a dependency, are not resolved and are not recorded, and the absence of an entry is never a statement that the package cannot do the thing. Nothing here is a finding, nothing here is decided, and no entry moves a grade: the declared checks of section 5 that would turn a fact into a finding (undeclared_network_calls first among them) enter MEASURED_CHECKS only by the admission test of section 18, and this inventory is their raw material until then.

21. Versioning

This document carries a version: the methodology_version: line at the top, MAJOR.MINOR.PATCH. Every published finding cites the version in force when it was produced and the SHA-256 of the document. Superseded versions remain available at their own permanent addresses, so a finding published in 2026 can still be read against the rules that made it in 2026.

A change to what a check examines, what a severity means, or when we refuse to grade is a version change. Correcting a typo is not. A change to what a row records, or to how and when a result is disclosed, is a version change too, because a reader of an older row must be able to tell what it does not carry.

Changing any rule is a deliberate act: edit this document, bump methodology_version, bump METHODOLOGY_VERSION in attestari/methodology.py, add a line to the Versions section, copy the document to a new snapshot under the site's content/methodology/, and commit. A published snapshot is never edited.

Versions

  • 1.0.0 (2026-09-11): first automated policy. Guards G1-G7, decisions D1-D3, grade ladder A/B/C/F, appeals and publication as the two human acts.
  • 1.1.0 (2026-09-11): the public methodology around the unchanged rules: what the document is for, definitions, artifact not repository, subject selection, the four checks, severity definitions, evidence and verification, known limits, notice, appeals and corrections, independence, measured performance, versioning. Guards, decisions and the grade ladder are unchanged from 1.0.0.
  • 1.2.0 (2026-09-11): two corrections. Evidence: no personal data in published evidence; structured responses are published as projections of the cited member with the upstream hash as provenance, files ship whole and scanned, and a finding that cannot be evidenced without personal data is withheld with its subject refused (evidence_unpublishable). Model check: "no literal tool descriptions" is split into no_descriptions_present (clean) and descriptions_not_statically_readable (not analysed, refuses the grade through not_analyzed), read from the parsed surface and failing closed when the surface does not say.
  • 1.3.0 (2026-09-11): the surface taxonomy and three description states. A surface that did not parse is not_a_server (out of scope, decision D4), minified_only (a medium finding), runtime_generated, declaration_not_parsed, no_registration_site or archive_unreadable, each refusing the grade under its own code except the first. A description is readable, partial (literal segments read, substitutions are holes; findings valid, absence not clean: partial_description) or unreadable. The extractor resolves names, configurations and descriptions written as constants declared in the same file, reads class-based getName() / getConfig() declarations, accepts schema and args as schema keys, and reads Python call-form registration x.tool(name=...)(fn).
  • 1.4.0 (2026-09-14): unmeasured checks decide at info. Decision D5: only checks whose precision has been measured (section 18) may grade; the set MEASURED_CHECKS holds model_directed_text alone, so every remote_code_fetch and publisher_provenance finding is recorded and published as context at info. Decision D6: a provenance finding is high only with a second, independent provenance tag on the same package, otherwise low. Section 5 marks remote_code_fetch and the account-age rule as informational pending measured precision. Section 7 gains "Attested manifest": a tool hash per readable tool and a manifest hash per subject, defined in attestari/manifest.py and pinned by a fixed-vector test. Section 17 gains "Publisher tools": a publisher's own use of these checks never affects a grade. Section 6 no longer names anonymous_publisher as the only low finding (D6 decides any single-tag provenance finding low); section 5 records routing guidance as a count per tool, the inventory being a separate opt-in pass.
  • 1.5.0 (2026-09-14): two boundaries of the model check stated in section 5. Parameter documentation is not a finding unless the parameter requests data outside the tool's declared purpose: a parameter that must carry the user's prompt, credentials, files, history or configuration the tool does not need is exfiltrate_data wherever in the description it appears. Guidance about retrying or resuming the tool's own calls after an interruption is routing guidance, not override_user, unless it directs the model to disregard an explicit user stop or cancel. Prompt version 2026-09-14.4 carries both. Section 4 speaks of a sample, not a population. Section 15 names the adaptive adversary: a publisher who reads the published rules can write around the model check; the deterministic checks and the reassignment finding do not share that weakness. Section 18 is marked preliminary, states the held-out corpus size, and is restated from the evaluation run under 2026-09-14.4. Guards, decisions and the grade ladder are unchanged.
  • 2.0.0 (2026-09-15): Section 5 lists the eight declared checks with the standing of each, names MEASURED_CHECKS, gives the verifier a version, and adds the three context tags on exfiltrate_data findings (destination_publisher_owned, disclosed_in_readme, opt_out_available), informational and read from the package bytes at decision time. Section 7 states that a zod schema is hashed as its source bytes and that both manifest hashes ride on every decision row and are served per package version. Section 9 adds the gate map (every reason code, its gate, one line, exported as gate_map.json) and a fixture per reason code. Section 12 adds analyzer_model, prompt_sha256, verifier_version, tool_hashes and manifest_hash to every row, and decisions.jsonl beside decisions.json. Section 17 extends the publisher-tools rule to everything sold to installers. Section 18 adds the population pass of 2026-09-14 beside the held-out figures. New section 19, Disclosure: registry and publisher on day 0, publication on day 30, on one clock for everyone. New section 20, Capability inventory: the remote_code_fetch findings and the named network destinations are inventory, declared and not measured, in place of the check's earlier severity standing. Versioning moves to section 21. Guards G1-G7, decisions D1-D6, MEASURED_CHECKS, the prompt and the grade ladder are unchanged, and the population pass of 2026-09-14 decides to the same grades under this version.
  • 2.3.1 (2026-09-17): the version in force, a patch of 2.3.0. Section 9's reason code no_tools_examined is replaced by no_surface_examined: a package is refused for an empty surface only when no tool, prompt, resource or resource template was parsed, so a server that exposes prompts or resources and no tool is graded like any other when every entry found was fully read and analysed (the not_analyzed, partial_description and model_check_skipped rules decide that, as they have since 2.3.0). Section 18 states the shortfall of the measured check against the admission bar in plain words, and the "measured" label carries that note wherever it is shown. The gate map, its fixture and the tests follow. No check, severity, guard, decision, prompt, verifier, extractor (syntax-tree-3) or grade ladder changes; the served run, decided again under this version, is reported with every changed package in analysis/reports/methodology-2-3-1-2026-09-17.md.
  • 2.3.0 (2026-09-17): Section 20 becomes real: the capability inventory is a deterministic, no-model reading of the syntax tree recording, per subject, what the code can reach (network hosts, file system reads and writes, processes spawned, environment variables read, credentials named, code obtained or built at run time), as facts with file:line evidence verified byte for byte, never a severity and never a grade effect; remote_code_fetch is retired as a check, its patterns being capability entries, and section 5 lists three checks that run. Section 5 gains the rest of the MCP surface: prompts and the literal text of their messages, resources and resource templates, read at their registration sites by extractor syntax-tree-3, their descriptions examined by model_directed_text under the same rules and the same MEASURED_CHECKS standing as tool descriptions, with the kind of entry on the finding (surface_kind); sampling and elicitation are recorded by presence. Section 9's not_analyzed and partial_description cover every kind, and G7 checks an excerpt against every kind's spans. Section 12 adds capabilities, capabilities_omitted, mcp_surface and surface_kind. Section 18 states the admission test to MEASURED_CHECKS verbatim and where the numbers of the one check in the set are recorded. Section 15 names prompt text built at run time. Guards G1-G7, decisions D1-D6, MEASURED_CHECKS, the prompt, the verifier and the grade ladder are unchanged from 2.2.0.
  • 2.2.0 (2026-09-16): Section 4 gains the delegation rule: a package that delegates its server to another package (imports or re-exports the server from a declared dependency, or whose only runtime behaviour is to load another package) brings the target into the population for the same run, at the version the declared dependency resolves to, regardless of download data or list position, and both rows record the relationship (delegates_to, delegated_from, with the exact version resolved). Section 5's list of outcomes names extraction_budget_exceeded, which section 9 has carried since 2.1.1, and states the minified call-site rule the extractor has applied since syntax-tree-2: a bundle is never a call site of a wrapper defined elsewhere, while an import still resolves into it. The extractor version stays syntax-tree-2, paired with this version in the version table. No other rule, severity, gate or MEASURED_CHECKS changes: guards G1-G7, decisions D1-D6, the prompt, the verifier and the grade ladder are unchanged from 2.1.1.
  • 2.1.1 (2026-09-16): a patch of 2.1.0. Section 9's reason-code table gains extraction_budget_exceeded: reading a package's source past a per-package budget of wall time or resident memory refuses the grade under that code, and the budget is measured and published with every row. Section 15's sentence about names resolved from "a constant declared in the same file" now describes the syntax-tree resolution section 5 describes. The extractor version paired with this document is syntax-tree-2: no syntax tree is kept across files, bundled third-party code is never indexed or resolved through, and extraction runs under the measured budget. No other rule, severity, gate or MEASURED_CHECKS changes: guards G1-G7, decisions D1-D6, the prompt, the verifier and the grade ladder are unchanged from 2.1.0.
  • 2.1.0 (2026-09-16): Section 5 describes the syntax-tree surface extractor in place of the token-stream reader: source is parsed with tree-sitter, tools are read only at the MCP registration sites the section lists, names, descriptions and schemas are followed through the identifier resolution the section lists (including sibling modules of the same package), a template literal stays partial, and bundled third-party code is inventoried as vendored and is never a source of tools. The extractor carries its own version, recorded by every run and, per section 12, on every decision row; the pair of methodology version and extractor version is checked against the version table before anything is decided. No rule, severity, gate or MEASURED_CHECKS changes: guards G1-G7, decisions D1-D6, the prompt, the verifier and the grade ladder are unchanged from 2.0.0.

Version 2.3.1 · In effect · Published 2026-09-17 · Effective 2026-09-17

Permanent address: https://attestari.ai/methodology/v/2.3.1

SHA-256 of this document, as decisions cite it: 0be40849028ae8362fb3d6c6dfd096cb63ec039127f73c8987c42c5fd3be1e53