Repository navigation
fix(validators,diff,exports): pinning completeness, drift alerts, PURL encoding & strict I-JSON - #141
Merged
Merged
Conversation
…ent boundary Trust boundary: the descriptor-relative confined reader between the scanned checkout and every collector. A fixed-path probe that found no file was silent, so a checkout carrying claude.md, .Mcp.json or .claude/Settings.json had nothing inventoried on the case-sensitive scan host while case-insensitive developer machines load exactly those files. A missing fixed path now records a failure for each existing case or normalisation variant, listed descriptor-relatively. Any symlink failed the scan, so ordinary layouts such as CLAUDE.md -> AGENTS.md had no passing configuration. A relative symlink whose target, resolved lexically against the link's own directory, is a regular file or directory beneath the root, not excluded and reached without a further link, is now read through its target; every opened component still uses O_NOFOLLOW and the link is re-resolved after the read. Walks yield in-root file links under their own name and skip in-root directory links, whose contents are walked under the target's name. Absolute, escaping, dangling, chained, excluded-target and special-file links stay failures with a named reason. Integrity checks of an in-root link hash its target the same way. parse_python turns the parser's MemoryError and RecursionError into a per-file ValueError, so one deeply nested file can no longer discard a whole collector's inventory, and accepts a UTF-8 byte-order mark as CPython does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
… signatures Trust boundary: A2A agent cards, client configuration and SDK source in the scanned checkout, which the a2a collector parses without contacting agents. One OAuth scope of a few megabytes made the token-shape regex backtrack quadratically and held a scan for hours; scopes over 512 characters are now withheld unmatched and both alternatives start only at a word boundary. Cards under public/.well-known or in a package directory were not read and left no gap. Cards are now found in any .well-known directory and at the root paths, case variants of those paths fail, identical cards published under both well-known names merge into one record, and a card built in Python code is a coverage gap. Python SDK references no longer fail the collector: they are read under their own budget, an unreadable file is no gap, and adding an unrelated file changes nothing in the inventory. Signatures (count, alg, kid), capability flags, extensions and the provider origin are recorded without claiming verification and enter the structure digest (profile a2a-card-structure/v3), so stripping a signature or enabling push notifications alerts without the binding key. Schemes typed "none" or unknown, anonymous legacy names, and a legacy authentication object on a card that uses the 0.3 or 1.0 declarations no longer count as declared authentication. Every client endpoint field gets the transport check, both client list names are read, names are bounded and neutralised in findings, and records carry root-relative paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: RAG configuration files and Python sources in the scanned checkout, which the rag collector reads into exported inventories. Source URIs were exported as origins with no digest or binding, and controls only by truthiness, so repointing a source within one host, changing an index or setting "sanitization": "off" passed scan, validate and diff. Each source now carries a structure digest (profile rag-source-structure/v1) over its field names, exported identities, URI shapes, inline-document digest and control states, an HMAC over the whole source under NEXUS_SYMBIOTE_SOURCE_BINDING_KEY (rag-source-values/v1), and a low completeness risk naming the key when values are unbound. Only a boolean true, or an object whose enabled is true, declares a control. Inline documents were copied into the BOM as source URIs; they are now counted and digested, never exported. Connection strings, user:secret@host forms, token-shaped values and URLs with a backslash authority are exported as redaction markers, and store types and other identities only when they are plain identifiers, so source_credentials_omitted is true by construction. The transport check reads the stripped URI's scheme and also flags ws, git, redis and other clear-text schemes. Parsing every Python file no longer fails the collector or the gates: the import heuristic reads under its own budget, skips files it cannot parse and lists one constant surface instead of every file. A data file named knowledge_base.json is not taken for a configuration, unnamed sources are identified by their file, both source list names are read, and paths are root-relative. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…h full hidden-character checks Trust boundary: instruction, rule, skill and command files in the scanned checkout that agent hosts pass to the model. A .clinerules directory failed the collector, and .cursor/rules, .github/instructions, .github/prompts, .windsurf/rules, .claude/rules, .claude/agents, GEMINI.md, CLAUDE.local.md and skill scripts were neither inventoried nor checked. Every file in those locations is now recorded by digest, and hidden-character detection runs on every UTF-8 file. A CLAUDE.md that imports @docs/policy.md kept its digest while the imported instructions changed. In-root @path imports of Claude memory files and GEMINI.md are now followed to five hops through the confined reader and inventoried, and imports outside the root are coverage gaps. Detection now covers every format character, variation selectors, U+180E, Hangul fillers and invisible operators, while emoji joiner sequences, presentation selectors after pictographs, the three recommended subdivision flags and a leading byte-order mark are no longer reported as high or critical; directional marks are medium. HTML and Markdown comments, hidden when rendered, are a low finding that reports only their count and length. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: the confined reader's fixed-path probes through an in-root directory symlink. With .claude linked to another directory of the checkout, probing the absent .claude/settings.local.json was reported as a dangling link, so an ordinary layout failed collection. A file missing below a linked directory that exists is now absent, as it would be without the link; a link whose own target directory is missing stays a failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: IDE agent settings, plugin manifests and marker files in the scanned checkout, which hosts apply to every contributor who opens it. From .claude/settings.json the inventory kept only the permission mode and whether the allow list held one of seven literal grants, so adding a hook command, removing a deny rule, adding apiKeyHelper, a plugin marketplace or enableAllProjectMcpServers, or respelling a grant as Bash(**), Edit(//**) or mcp__server changed nothing. Each settings file is now bound by a structure digest over every key and redacted value (claude-settings-structure/v1, environment values by name only) and, with NEXUS_SYMBIOTE_SOURCE_BINDING_KEY, an HMAC over the whole files; findings cover hook commands, MCP auto-approval, credential helpers, plugins and marketplaces, additional directories, the bypassPermissions and acceptEdits modes and blanket grants parsed from the rule syntax. A plugin repository's own manifests and hooks are bound the same way, and settings nested in a subdirectory, which a host started there applies, are separate records. Markers and settings are read at fixed paths, so case variants are reported and in-root links followed. Codex config.toml approval and sandbox settings are read (its MCP servers are a coverage gap), Gemini CLI is recognised, and .vscode/extensions.json is read as VS Code reads it, with comments and trailing commas but no duplicate keys. Paths are root-relative. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: a native inventory is input from whoever produced it (a
supplier, an earlier or later build, a pull request), and every reading
command parses it: validate, risk, diff, summary, vex, blast-radius and
baseline.
Abuse cases addressed:
- A document from a newer build, or one declaring an unknown format
version, failed with the same opaque message as a corrupt file.
spec_version is now checked first (this build reads 1.0.x and 1.1.x)
and refused with a message that names the supported versions and never
echoes document text. New inventories are 1.1.0, which marks
scan-root-relative paths.
- Dependency edges silently dropped unknown keys, so a misspelt
"condition" became an unconditional relationship. Edges now forbid extra
fields, and booleans, integers and timestamps are as strict as the
published schema ("no", 0, "3" and numeric timestamps are refused).
- AIBOM.load followed symbolic links and blocked on a FIFO, and
AIBOM.save wrote through a link, non-atomically. Both now use the
project's no-follow, regular-file and atomic-write primitives.
- scan and import-shadowscan could write an inventory larger than the
16 MiB every reader accepts. One limit now governs writer and reader:
oversized output is written compactly or refused with the limit named,
load errors name the class of problem without quoting the document, and
baseline reports a staged-load failure without a traceback.
- bom_ref and edge endpoints are limited to 16,384 characters, so one
reference cannot be repeated into every validation message.
The published schema is regenerated from the model, carries $schema and
$id, and marks the types and fields no collector populates as reserved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: validate and risk judge an inventory someone else supplied, and scan, validate and risk honour a policy file from the checkout unless --no-project-policy is passed. Neither the inventory nor that policy may turn a gate green without evidence from the reviewer's checkout, or relax a gate without the relaxation being recorded. Abuse cases addressed: - Integrity counted a tool descriptor that merely agreed with its own recorded digest as a verified hash, so a hand-written inventory, or one supplementary served-descriptor record added to an honest scan, turned integrity from unknown to PASS. Only a descriptor matching the tool collected from the project's MCP configuration now counts; anything else is unknown, and a run that verified nothing says so. - Project reconciliation compared components only: a supplier could delete or forge relationships, rewrite the repository agent's autonomy, references or authorizations, or add components claiming verified assurance, under a "project-reconciled" label. Relationships between local components and the repository agent (matched by identity across checkout paths) are now compared; components or edges asserting verified assurance or internal-verified trust without local evidence fail; other supplier additions are listed as supplier claims under the basis project-reconciled-with-supplier-claims. validate --project, like risk --project, needs at least one local subject. - A supplied inventory could make validate re-read one large file once per component and per case-variant hash key, or build one message per dangling reference that repeats a megabytes-long reference. Digests are cached per file identity and algorithm, every artifact read (including model inspection) is charged to a 30 GiB budget beyond which evidence is unknown, hash keys are case-folded, and issue lists are bounded. - The discovery producer check passed vacuously when an inventory dropped every ShadowScan marker while the signed report was still present; it now fails when a report lies beneath the project or the operator signature policy names one. --require-discovery-signature lets CI that ignores the project policy require the signer, and an unverified producer is reported instead of passing silently. - Imported ShadowScan model and tool findings could never pass integrity. Claims bound to the local report by reconciliation are judged as imported claims; intrinsic findings such as an unsafe format still fail. - One false positive forced a category-wide ignore_categories or a disabled gate. risk.suppressions accepts exactly one category on one component with a reason, owner and expiry; an expired entry fails the gate, and every entry is listed in policy_weakened, in risk JSON and, through scan, in the inventory metadata with the governing policy. - policy_weakened omitted a raised warn_threshold and extra exclude_paths, the policy file was located twice, and a .yaml file could silently shadow a stricter .yml. validate, risk and drift now share one typed coverage evaluation (core/coverage.py); scan and reconciliation share one merge rule (core/collection.py), so identical duplicates from two collectors are kept once and components measured from bytes are recorded as observed. Every failing gate carries a reason in its report object, and risk JSON records the effective gate parameters with their source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: operators decide from the documentation which results to trust and which settings a checkout may change; a policy key that is undocumented, or a guarantee stated more broadly than the code enforces, leads to gates that are weaker than they appear. - docs/policy-reference.md describes every .nexus-symbiote.yaml key, its default and effect on each gate, the new suppressions, what policy_weakened reports, where the governing policy is recorded and what is not supported; the quoted error messages were reproduced from this tree. It is linked from the documentation hub. - docs/security-model.md, docs/shadowscan-integration.md and docs/verification-source-binding.md state the wider reconciliation scope, what component assurance means, how imported claims are judged, the producer check for stripped inventories, and that a tool descriptor counts only when it matches the project's configuration. - SECURITY.md names the operator-side --require-discovery-signature flag, notes that the project setting is ignored under --no-project-policy, and states the validation work budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…r blast radius Trust boundary: agent.yaml is checkout-controlled input that the repository assembler reads on every scan, and blast-radius is the operator's answer to "what does this compromised component reach". Abuse cases closed: - A declared local model compared a root-relative manifest path with the absolute path collectors record, so every such scan aborted with no inventory; a non-conforming or foreign agent.yaml aborted the same way. Manifest problems are now recorded as scan failures and the inventory is still written; validate --project stays strict. - The manifest ignored the operator's exclude_paths, so a pull request could name an excluded file (for example secrets/service.env) and have its unsalted digest exported. Exclusions now bind the manifest and every path it names, recorded as a coverage gap. - Declared instructions were inventoried twice and manifest-only ones were never checked for hidden characters; they now map to the prompt collector's record (digest must match) or are checked like it. YAML anchors, aliases, tags and lone surrogates are refused. - Registry packages and container images were graph orphans, so blast-radius reported no affected agent for a compromised dependency. They are linked to the repository agent (depends-on), MCP servers to the package they launch (registered-as), and blast-radius reports dependents, says when a component is unlinked, and runs one traversal per direction instead of one per agent. Graph issue messages are bounded. - Assembly looked up each tool's server with a linear scan; it now uses an index. - The path-inferred agent's reference hashed the absolute checkout path; it is now agent:repository:inferred wherever the tree is checked out. baseline gains --no-project-policy and --signature-policy so the reviewed snapshot and the candidate can be taken under the same policy. The manifest is documented in docs/agent-manifest.md with a published JSON Schema. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…ucer Trust boundary: the native writer and the human-readable risk report are what an operator sees of an inventory produced elsewhere. - to_native_json built the compact serialization of every inventory even when the indented one fit the 16 MiB limit, doubling the cost of every scan and save; the compact form is now produced only for oversized inventories. - Human-readable risk did not say when ShadowScan evidence was accepted without a producer check, although the JSON output did; it now prints "Discovery producer: not verified (unsigned evidence accepted)", so an unauthenticated discovery report is not mistaken for a verified one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…elationships Trust boundary: diff --fail-on-alert is the CI gate between a reviewed snapshot and the change under review; whatever it rates info passes. Abuse cases closed: - A removed component was rated info and never alerted, so replacing a reviewed dependency or base image with one the collector does not record (open-ai for openai, an attacker image for a pinned ollama image), or deleting a guardrail requirement, passed the gate. Removals now alert: high for MCP servers and clients, tools, models, instruction and skill files, guardrails, IDE and A2A configuration and any record that carried integrity evidence, medium otherwise. - Coverage was compared as one blob with one constant high alert, so an added Python file failed the gate exactly like a failed collector and trained teams to re-baseline without review. Coverage is now compared field by field: a new failure, gap or exclusion, a worse collector status, fewer collectors, a scope change or a ShadowScan regression alerts high and names itself (as a multiset, so a second identical gap is new); a changed list of scanned surfaces and improvements are info; gaps both snapshots share are reported as a note. - Added and removed relationships were unrated and invisible in text output; they are rated (high for new invokes, delegates-to and acts-as-identity, removed uses-guardrail and approved-under) and printed. - Trust-tier and signing-status upgrades in a supplied BOM passed as info; they are medium. Added risk findings carry their own severity, and agent_card_url, provider, source_uri, signing_key_reference and the declared autonomy level rate high. Opaque values compare as JSON, so true is no longer equal to 1. - A supplied BOM with dangling references, an unsupported spec_version or swapped arguments compared clean; new graph problems and unsupported versions alert, and a reversed order is noted. - Inserting one ShadowScan finding, or reordering CycloneDX dependencies, raised an alert for every later component or edge; positional indexes no longer take part in the comparison. A new Nexus SymBiote release is an info note rather than a medium alert. docs/drift-gate.md documents where the reviewed baseline must live and what each alert means. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…nized values Trust boundary: a ShadowScan report found in the checkout is untrusted, unsigned-by-default discovery evidence that the scan and every gate import. Abuse cases closed: - The collector imported the first of .shadowscan/report.json, shadowscan-report.json and shadowscan.json and never looked at the others, so a committed copy at a higher-precedence name silently replaced the report the pipeline had just generated, hiding a new critical agent from scan, validate and risk. More than one report is now a scan failure that names the paths and imports neither; the inventory records the path it read under surfaces_scanned. - A risk level, shadow flag or surface the projection does not recognize (Critical instead of critical, "true" instead of true) was dropped while the finding counted as mapped and the source as complete. Such values are now projection limitations and a coverage gap, and contradictory CycloneDX type, group and kind combinations stay unmapped (projection profile v4). The summary's by_risk_level and shadow counts are cross-checked. - json.loads accepted UTF-16 and UTF-32 reports that verify-discovery cannot read; reports must be UTF-8 (an optional byte-order mark is accepted). - SARIF and VEX could present unauthenticated imported heuristics as this tool's own findings (the Action's SARIF upload and the vex command had no check). Both now omit imported findings and record how many they omitted. The integration guide's opening flow no longer ends in a command that fails for every repository, and the report paths, limits and rerun caveats are documented. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…ursion Trust boundary: LineageIndex decides whether a principal may retrieve a memory, cache or index version, given its ancestry and ACL revisions. Abuse cases closed: - An agent that can append to a lineage could make every later retrieval raise RecursionError after about 1,000 generations, and rebuilding the index was quadratic in its depth. Construction now uses Kahn's algorithm, withdrawal uses a children index, and the ancestry check is iterative with one visited set per retrieval. - update_acl accepted any revision string, so an earlier, wider ACL could be reinstated under its old revision and replay retrievals approved against it. A revision already used for a version is now refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: the README recipe is what most CI pipelines copy; a gate that reads its baseline from the pull request it reviews is self-attestation. The README drift recipe now takes the baseline under the same policy as the candidate, says that removals and coverage regressions fail the gate while an added source file does not, and links the guide that explains where a reviewed baseline must live. The collector table points to the optional agent.yaml manifest, the documentation hub lists both new pages, and the SECURITY.md limitation on instruction files outside the fixed paths now says a manifest can bring them into the inventory and the hidden-character check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…olicy Trust boundary: model files, their configuration and everything beside them come from the scanned checkout and are hostile input. Only the operator's signature policy, kept outside the checkout, can establish their integrity. Abuse cases closed: - A pull request swaps an ONNX external-data file, a SavedModel variables file or one shard of a sharded checkpoint. The inventory recorded only the graph file, so diff and validate stayed silent. Bound weight files are now hashed and recorded per model, and validate --project observes them again. - A model directory declares auto_map custom code, sets trust_remote_code in a config behind an empty config.json, or edits the chat template in tokenizer_config.json. None of it changed the inventory. Every config, tokenizer file, chat template and declared Python module is now digested, and declared custom code is a high finding. - Model references used the checkout directory's name and absolute paths, so same-named models collided and an inventory made elsewhere could not be reconciled. References and recorded paths are now root-relative. - An older artifact signed by the same identity verified under the signer policy. Policy version 2 adds expected_sha256 and operator digest pins; every weight file must be covered, the verification evidence is recorded on the component, and policy entries that nothing verifies, or a policy stored inside the project or hard-linked, fail. - Renamed pickles, text-protocol pickles and torch ZIP checkpoints behind other model suffixes are rated code-executing, an unreadable pickle keeps that rating, and UTF-16 SafeTensors headers are no longer safe. One SafeTensors validator serves the scan and validate. OpenSSF Model Signing (DSSE) bundles are refused with an explicit reason; that format is not verified yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: operators decide what to trust from these pages, so they must say which model files the inventory binds, which policy entries establish integrity, and what stays unverified. Abuse case: an operator follows the old advice to sign with OpenSSF Model Signing, or keeps the signer policy in the repository, and believes the models are verified while nothing checks them. The new model integrity page states what is bound, the version 1 and 2 policy formats, how to produce a signature the tool verifies, the evidence it records and what it does not establish. SECURITY.md lists the remaining model limitations, and the migration guide explains the changed model references. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: a GGUF file's metadata keys are bytes chosen by whoever wrote the file, and the template key names reach the inventory as the source of each digested chat template. Abuse case: a key such as "tokenizer.chat_template.<ESC>[31m..." carried terminal control characters into the BOM, where any consumer that prints the source verbatim would render them. Names outside letters, digits, "_" and "-" are now recorded by position instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: everything a collector reads from the scanned checkout is untrusted; how much it may read is decided by the operator alone. Collection bounds were constants: 20,000 directory entries, 4 MiB per file, 32 MiB per collector, 5 GiB for model weights and 4 MiB for tokenizers. Ordinary checkouts and model directories crossed them, the failure did not say which bound was hit, and nothing could raise one, so the only way past was report-only mode, which switches the gates off. src/nexus_symbiote/limits.py defines the named limits, their defaults and hard maximums. They are set only with the root option --limit NAME=VALUE or NEXUS_SYMBIOTE_LIMITS, the option winning per name; a malformed setting is a usage error whose message never repeats what was supplied. Nothing in the checkout, its policy file included, can change a limit, and every limit still fails closed: reaching one records a scan failure that names the limit, its value, the root-relative path and how to raise it, without file content. Raising a limit moves where collection stops; it cannot hide a stop or pass a gate, and lowering one only adds failures. The confined reader also charges the root listing to the entry budget, reports an exhausted budget once, skips and reports names that are not UTF-8 instead of writing an inventory no command can load, refuses JSON numbers that overflow to infinity, checks policy exclusions in O(depth) instead of entries times files (20,000 exclusions made a 200-file walk take 67 s), and walks ".." in an operator path through the real parent, so "link/../x" fails closed instead of naming another file. validate and verify-artifact resolve --project like scan. Defaults raised where ordinary projects exceed them: entries 100,000; model-bytes 64 GiB, now also for tokenizers; model-total-bytes 256 GiB. docs/collection-limits.md lists every limit, its default, what happens when it is reached and how to raise it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…r reads Trust boundary: everything under the scanned checkout is attacker-controlled input to the registry collector, including manifests a pull request adds or edits. The inventory, its coverage claim and the drift gate must not be steered by how a declaration is spelled, which file a package manager prefers, or how expensive a file is to parse. Abuse cases this closes: - Spelling and scope evasion: names were matched before PEP 503 normalisation and only after a "/", so llama.index.core, mcp.server.fetch, mcp, fastmcp, @anthropic-ai/sdk, @openai/agents and @langchain/core were never inventoried while coverage read complete. Names are now normalised first and matched against one documented catalog (names, prefixes, words, npm scopes); an npm alias is classified and recorded by the package it installs. - Precedence and override files: npm-shrinkwrap.json is now the lock source when present (never replaced by package-lock.json); npm overrides, yarn resolutions, pnpm.overrides, workspaces, [tool.uv.sources], uv override/constraint dependencies, PDM overrides and a Pipfile are coverage gaps; peerDependencies, PEP 735 dependency groups, build requirements and uv and PDM development dependencies are read; all eight default Compose file names, Containerfile and Compose models are read. - Lock records taken at face value: a resolved URL with a backslash, empty host, non-https scheme, control or non-ASCII character is a failure; missing or SHA-1-only integrity, another registry and a tarball naming another package or version are findings; an exact spec the lock contradicts is a failure; package URLs from another registry carry repository_url. - Compose read by regex: files are composed as YAML nodes (aliases shared, merge keys followed once per mapping within a step budget), so quotes, flow mappings and block scalars are read correctly and comments or environment values never become components; one image in several files is one component. - Ratings hidden by order: a URL declaration behind a pin is a finding and removes the PyPI package URL; marker-only pins are low; the rating no longer depends on which file is read first; --hash values are recorded for drift. - Denial of service: the Dockerfile directive and heredoc patterns backtracked quadratically (hours at 4 MiB); they are replaced by linear readers proved equal on exhaustive inputs. pyproject.toml is bounded (256 KiB, 16-dot keys) before tomllib, quoted option text over 16 KiB is not given to shlex, and a deeply nested marker fails its own line instead of the whole collector. - Identity: images record their registry type, host and normalised repository without redaction collapsing tags, digest-pinned images get pkg:oci URLs, and package identifiers with an unknown purl type or qualifier are not projected. Every path the collector records is relative to the scan root. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: an MCP server's served declarations (tool descriptors, initialize instructions, prompts and resource templates) are untrusted input that can change on the server without any change to the repository. Abuse case: a remote or pinned stdio server rewrites a tool description, adds a tool, or keeps tools/list byte-identical and changes its initialize instructions to carry injected directions. discover-mcp output could not enter an inventory or diff, verify-live failed for any inventory that also held a remote server and refused title, outputSchema and _meta, and no path observed instructions, prompts or resource templates, so these rug pulls raised no alert. - discover-mcp --record writes a native inventory of a complete observation: observed server and tool components, negotiated protocol, server identity, capabilities, redacted instructions, prompts, resource templates and a server declaration digest. diff rates a changed tool definition, served tool set or declaration digest as critical. - hash_tool_definition binds title, outputSchema and _meta when present; every fingerprint without them is unchanged. Static configuration, stdio and HTTP intake share the profile, and known-answer vectors pin it. - verify-live reports servers reached over HTTP as not verifiable instead of failing the run, accepts the current descriptor fields, compares the server declaration, gives each failure a reason code, records input digests, requires --project, refuses a launch policy inside the repository of the project or the inventory, with more than one link or writable by others, and --record exits 1 on drift unless --accept-drift. - The client negotiates MCP 2025-11-25, 2025-06-18, 2025-03-26 or 2024-11-05, decodes chunked SSE through http.client with the WHATWG line rules, bounds served schemas before the JSON Schema check and records any check failure, refuses special-purpose addresses from an explicit list, ignores SSLKEYLOGFILE and lets the operator set clientInfo and User-Agent. - The pinned stdio session skips notifications and reports a failure after tools/call as ToolCallError or ExecutionOutcomeUnknown, never as a denial. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: OSV replies are untrusted network input, and the package names and versions sent to OSV leave the operator's machine. Abuse case: advisories exited 0 when it found advisories and reported complete: true when nothing was checked, so a CI step passed known vulnerabilities; one malformed reply ended the run with a traceback and discarded every earlier result; private package names were sent without the documentation saying so or an option to exclude them; pinned MCP launcher packages were never queried. - Exit 1 when an advisory is found (--no-fail-on-advisory reports only) or when coverage is partial or none, including an inventory with no exact PyPI or npm package. The report states coverage, checked_at, the tool version, the inventory digest, skipped and excluded components, the exact queries and what was sent. - Query /v1/querybatch and follow each query's pages under a page cap. A failed or malformed reply stops only its batch: every unfinished component gets a fixed reason, other results are kept, and HTTP framing errors are recorded instead of raised. - Add --exclude NAME (a trailing * matches a prefix), --exclude-private-origins and --dry-run, which lists the identities it would send and runs offline. - Query MCP servers that carry an exact pkg:pypi or pkg:npm package URL reconciled with their launcher package and pinned version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
Trust boundary: offline commands run on untrusted checkouts and must not reach the network or start processes. Abuse case: a refactor drops an entry from a guard table, or a collector gains a network call in a branch that only direct unit tests reach, and the suite stays green. Code run in a subinterpreter sees neither the audit hook nor the patched entry points. - Parametrize the guard tests over _SOCKET_METHODS, _RESOLVER_FUNCTIONS, _CONNECTION_FUNCTIONS, _PROCESS_FUNCTIONS and _AUDIT_EVENTS, keep the documented minimum of each table, and check that the documentation names every entry. - Cover connect on a Unix-domain socket, a socket created before the guard, every process entry point in a child interpreter, and collectors called directly on hostile input. - Refuse subinterpreter creation while offline. Raising from the cpython.PyInterpreterState_New audit event crashes CPython 3.12, so the creation functions are replaced instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
The runtime gateway admits tool calls on evidence the mediated agent presents: snapshots, trust statuses, approvals and the companion BOM. Several checks were weaker than the contracts promised. - The snapshot high-water mark moved only when a call was admitted, so an agent that kept presenting serial N after the operator issued N+1 kept grants the operator had removed until N expired (up to 24 hours). An authentic newer snapshot now becomes current as soon as it is presented, even in a denied call, or registered with register_snapshot, and lower serials are denied. - Approval quorums counted keys under one actor label, so one person with two enrolled keys met a two-person rule. They now count distinct approvers, and the approval's actor must be one of them. - Any ring key could sign for any tenant and one key could serve every purpose. SignerScope limits keys to tenants, deployments, agents, tools and a validity period; overlapping rings are refused. - A missing state file silently became an empty store, reopening approval replay and rollback after a restart. Stores are created explicitly, never recreated, and created owner-only without races. - Input schemas were read as 2020-12 with Python regex semantics, so a value with a trailing newline passed an anchored pattern and draft-07 dependencies were ignored. Declared dialects and ECMA-262 patterns are honoured. - Endpoint revocations were free text that a typo made ineffective. Endpoint identities now follow one grammar. - Standard MCP descriptor fields (title, outputSchema, _meta) denied every call; they are accepted when the approved digest covers them. - Worker start-up shared the one-second evaluation budget, so slow hosts denied every call; start-up now has its own budget. - Arguments were rewritten before execution in ways the approval did not bind; the executor now receives them as sent. Every denial raises AuthorizationError with a stable reason code, each denial path has a test that asserts its code, CLI errors are specific and never tracebacks, sign refuses contracts that can never verify, and runtime digest and runtime key-id compute what approvals bind. on_decision records every outcome without argument values. Known-answer vectors pin the signed wire format, and the new contract reference specifies it. invoke_pinned_stdio keeps the whole gateway configuration through bind_adapters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
The runtime guide is what an operator follows to put tool calls behind the gateway. It showed one key named approval.pem signing a snapshot, gave no way to compute the digests contracts bind, did not say that a superseded snapshot stays usable until it is registered or revoked, that the evidence must come from an operator channel rather than the mediated agent, or that one-use and rollback guarantees hold per state file. Operators were left with shortcuts such as one key for every purpose and autonomous grants. The guide now uses one key per purpose with encrypted private keys, runtime digest and runtime key-id, explicit state initialization, snapshot registration, approval quorums by person, signer scopes, the decision callback, the worker's start-up budget, schema dialects and the argument number domain, and links the contract reference. The documentation hub and the README command table point to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…red floors The published dependency ranges are what PyPI and pre-commit users install, but CI only ever ran the single hand-maintained lock: the declared floors were never installed (the suite could not run on the click 8.1 floor), Dependabot widened ranges with every check green, and the Action lock was a hand copy of CI-lock blocks. A range change or a stale copy could ship an untested or unreviewed dependency set to every consumer. - scripts/check_locks.py (`make lock`, `make lock-upgrade`, `make lock-check`) regenerates requirements-ci.lock, requirements-action.lock and the new requirements-floor.lock with a pinned uv and a recorded upload cut-off (a week of cooldown on upgrades), and checks offline that each lock was generated from the current declarations, that every pin is an exact hashed version inside its range, that the Action lock repeats the CI lock's versions and hashes without test tools, and that the floor lock tests exactly each declared lower bound. A Dependabot range change now fails until the locks are regenerated. - Floors raised to what installs and is tested together: click 8.2.1 (separate stderr capture), pydantic 2.12 (required by the sigstore extra), packaging 24.2 and jsonschema 4.25 (required by the build backend and the CycloneDX extra), hatchling 1.27 (PEP 639 licence files, core metadata 2.4) for the build system as well as the dev group. - rfc8785 is held to 0.1.x because it defines every tool fingerprint; cryptography may move two majors ahead, since only Ed25519 and serialization are used and the newest releases are now tested on a schedule. - The no-op yaml extra is gone, `all` refers to the other extras, development tools move to a PEP 735 dependency group so they are no longer published as an extra, and the directly imported sigstore-models is declared. - Every lock install uses --only-binary=:all: and pip check; Dependabot waits seven days before proposing a release. The regenerated CI and Action locks keep every reviewed version and hash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…the parsers The suite read whatever the developer or runner exported (a source-binding key, an operating mode, GITHUB_* variables), had no per-test time limit, let a test that started skipping stay green, verified the pip and BuildKit emulation only with hand-picked examples, and judged the security modules by a global coverage average. A hostile manifest line that the collector reads differently from pip, or an untested branch in a confinement or signature module, could therefore pass every gate unnoticed. - tests/conftest.py removes the variables the tool, the Action runner and the gate scripts read, gives HOME and the XDG directories a private temporary directory, fails a test that leaves an operating mode enforced, restores the scanner registry, fails a test that exceeds its time limit with its traceback (SIGALRM, no new dependency), lists every skip, and with --strict-skips fails the run on any skip other than a host file-system difference. The wheel acceptance run uses the same conftest. tests/support.py shares load_script. - tests/test_parser_properties.py checks the requirements collector against packaging however a line is decorated, that an unparseable AI requirement or an undecodable file leaves a gap, Dockerfile FROM and stage handling, that a planted secret never survives redaction (known values, URLs, assignments, authorization headers, tool descriptors), that no collected file kind can crash `scan`, and that hostile inputs parse in bounded time. The default Hypothesis profile is deterministic; a `fuzz` profile explores further. - scripts/check_coverage.py adds floors for the confinement, hashing, crypto, signature-policy, validator, differ, offline-guard, runtime, policy, redaction and terminal modules, fails when one is not measured, ratchets the package floors to 91%/83%, and prints the measured figures to the job summary instead of documents quoting stale numbers. - First-generation CLI, integration, validator and policy tests now assert the exact verdict, reason or change set their names promise; dead importorskip guards on required dependencies are plain imports; a CycloneDX library without 1.7 support fails instead of skipping the strict conformance tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…orkflow integrity Verified CI is produced by the pull request's own workflow files, and check_workflows.py only checked pins and shell expressions in the top-level files: a change could empty the dependency-audit or CodeQL job while keeping its name, add a pull_request_target workflow, persist checkout credentials or reach unpinned actions through a local action. The advisory gate audited only the Python 3.12 Linux environment (never the 3.11- and Windows-only pins, but the runner's own pip), had no exception path, and ran only on pushes. - ci.yml: a gating `floor` job runs the full suite on requirements-floor.lock on Python 3.11; every job installs with --only-binary=:all: and pip check and runs with --strict-skips; quality checks the locks; the distribution job runs twine check --strict and scripts/check_installed_wheel.py, which scans the demo agent with a TypeScript MCP server in every export format and scores it on freshly resolved dependencies (the release compatibility job runs the same check from the candidate's sdist); CI runs on pull requests to main only; runners are pinned to ubuntu-24.04. - check_dependency_advisories.py --lock audits every pin of the named locks whatever its marker, lists runner-only distributions without auditing them, and honours reviewed waivers in config/dependency-advisory-waivers.json only for one advisory of one exact version, with a reason and an expiry at most 90 days ahead; expired, malformed or stale waivers fail. - dependencies.yml audits the locks daily and the floor lock separately, runs the suite weekly and on dependency changes against the newest releases the ranges admit, and runs the randomized property tests weekly; platforms.yml runs the case-insensitive and confinement branches on macOS (non-gating); scorecard.yml publishes OpenSSF Scorecard results; scheduled CodeQL also analyzes the workflows with the actions queries. - check_workflows.py follows local actions, rejects privileged triggers, writable top-level permissions, jobs without timeouts and persisted checkout credentials, requires a full version comment that agrees for each pinned commit and is not older than the action's floor, forbids continue-on-error in ci.yml and release.yml, and requires each gate job to keep its command. action.yml's CodeQL pin comment names its full version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…imits Trust boundary: the documented guarantees that operators rely on when they gate on scan, validate and diff. The known limitations, README rows and security model still said that Claude Code hooks, deny rules, skill scripts, subagents, rule directories and most invisible characters were not recorded, that symlinks are refused, and that a lone surrogate in rag_config.json breaks exports. They now state what is collected, which values only the binding key binds, how case variants and in-root symlinks are handled, and what stays uncovered (nested instruction files, skill front matter, Codex MCP servers). The binding guide documents the a2a-card-structure/v3 profile, the RAG and Claude Code settings profiles, the authentication rules and how to generate a binding key. The demo walkthrough lists the two low completeness findings in its score. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…tings 0.1.0 reached PyPI 100 minutes after a 6,443-line merge; the 24-hour soak that stands in for a second reviewer was a convention, the signed tag came a day after publication, and the preflight could not see the release-tag ruleset or the environments' administrator bypass that the README's verified install relies on. Its CI evidence was the single newest run for the SHA, so anyone could block publication by opening a pull request at main's head commit. - check_repository_controls.py selects the newest trusted run (push or dispatch of ci.yml on the branch in this repository) from the whole listing, so a pull-request or fork run cannot displace it, while a newer trusted failure still wins over an older success. - With --publish it requires an active, unbypassable ruleset stopping refs/tags/v* from being moved or deleted, and an environment with a required reviewer, administrator bypass off and deployments limited to main. With --publish pypi it requires the commit to have been on main for 24 hours (from the later of its commit time and its first push run), unless the new soak_override input states a reason, which is printed reduced to plain prose, and requires the signed, annotated v<version> tag on that commit with a signature GitHub reports as verified. - release.yml passes --publish, renames the step to what it checks, audits the locks rather than the environment, runs the suite with --strict-skips, keeps release artifacts for 90 days, and writes the environment SBOM against requirements-action.lock: every installed distribution must be locked at its version, unrepresentable records fail instead of vanishing, and the legacy licence field is used when it is a name. - The full protection profile requires code-owner review, and CODEOWNERS says what it does and names the gate paths. docs/releasing.md tags before publishing, documents the GitHub release and evidence attachment, a release freeze and the post-release records, and keeps a dated release record; docs/repository-controls.md describes the new checks, the advisory waivers, workflow integrity, Scorecard and a leased branch-cleanup runbook. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…undecodable link targets Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…e budget and read NFKC spellings Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
… import bookkeeping Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
… lists The nested-import hash-order test started a subprocess without its own timeout, which the suite's infrastructure test forbids. The pnpm redaction test expected 20,000 identical gaps to collapse to one set element; since failures and gaps are bounded per collection, the last listed gap also counts the ones not listed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…nner's package Only fixed variable names counted as package sources, so a server's env could relocate the runner's user or global configuration into the checkout (HOME, XDG_CONFIG_HOME, PREFIX, DESTDIR, GOENV, ZDOTDIR, a PATH with relative entries) or replace the package outright (UV_OVERRIDE, UV_CONSTRAINT, PIP_CONSTRAINT), and the record kept version_pinned and its package URL. Location variables whose value may lie in the project, and replacement variables, now make the identity unknown (a high finding and a gap); any other location, and the runner's own configuration namespace (UV_*, PIP_*, PIPX_*, YARN_*, BUN_*, DENO_*, COREPACK_*, GO*, DOCKER_*), is a package source with the package URL withheld. A PATH of absolute directories keeps the identity, and display-only settings are ignored. verify-live skipped any server labelled remote, even one whose inventory records a launch command that the operator's launch policy pins. Such a server is now checked through its pinned launcher, or fails with ambiguous-transport; the collector keeps local_process for any server with a command and adds a low transport_ambiguity when it also declares a remote type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…and exports
Field, header and environment names were exported after a character-set
check only, so a secret pasted as a key ("ghp_..." as a server field, an
http header name or an env name, also in .codex/config.toml) appeared in
declared_fields, header_names, the env keys and the static configuration
digest's preimage, and from there in JSON, CycloneDX and SPDX output.
One rule now decides whether a name may be exported: a plain field name
that matches no known token shape and holds no 24-character run mixing
letters and digits. Other names become "[redacted]" (numbered for env
keys, which must stay unique), so the digest still changes when one is
added.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…ld token-shaped names, check command-carrying remote servers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…rge every scanned word
The launcher grammar split shell scripts with shlex, which removes quoting
but performs none of a shell's expansions, and read cmd lines word by
word, so brace and pattern expansion ({npx,-y,evil}, np?, /usr/bin/np[x]),
parameter expansion (${X:-/usr/bin/npx}, ${X#/}npx), ANSI-C strings
($'\x6e'px), eval, zsh's =cmd and cmd's @ ; , = prefixes and
%PATH:~0,0% substrings all started a floating package while the record
said "local executable", and one could even carry a false pinned
identity. Text a shell or cmd reads again is now classified only when
every word reads as written:
- sh/bash/zsh -c scripts and command lines: no character a shell
rewrites, no whitespace or other character inside a word; quoting is
removed as a POSIX shell removes it. fish, env -S, wsl, eval and
PowerShell -Command must also be free of quoting.
- cmd /c: the program word is a plain path, and no word holds % ! " ^
or whitespace other than spaces and tabs.
- An expansion in or across the command's last path segment depends on
the environment.
- Arguments a program passes on are read as written and as a shell or
cmd may rewrite them (quoting removed, prefixes dropped, expansions
empty, open or at their default, brace lists expanded with a bound,
patterns matched against the program vocabulary); command substitution
and $'...' strings are unknown.
The argument scan charged only the option word it stepped on, so every
runner name started an uncharged forward scan and npm_command rewrote
long words letter by letter: a 1 MB configuration took a minute of CPU
and passed. Every examined word is now charged, npm_command does
constant work on long words, and multi-word readings charge their copy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…installers as unknown - uv run (and uv run --script) downloads and runs a script named by URL, and reads one from standard input with "-"; such a launch was recorded as a local "server.py" with no finding, and with --with mcp==1.2.3 as a pinned pkg:pypi/mcp@1.2.3. It is now a URL source: a high finding and a gap, and no package URL. - Win32 drops trailing dots and spaces from a file name, so npx.cmd. names npx.cmd; normalize_command now does the same, so a trailing dot no longer turns every runner, shell and inline-code gap into a local program. - pip, pip3, python -m pip, gem, cargo and conda-family installs fetch an attacker-chosen package and run its build code; they are unclassified runners (a high finding and a gap) when they install, and local programs otherwise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…emote uv scripts, trailing dots and installers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
The known-limitations catalogue now describes the merged launcher classifier: text a shell or cmd reads again must read as written, package installs and remote uv scripts are gaps, environment that moves a runner's configuration withholds the identity, and token-shaped names are redacted. It also states the limits that remain: a runner named by a relative path or shadowed by a Windows file in the launch directory is still classified by its name, and uvx -w and a bare pwsh script are gaps. The Unreleased changelog lists the final-review fixes and the dropped rich dependency. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
…ment-dependent failures - Python 3.14 limits recursion by the C stack it actually has, so on a runner with a large stack json.loads accepted 100,000 levels of nesting and the runtime commands failed later with json-out-of-domain instead of json-invalid. strict_json now refuses input nested more than 256 levels with an explicit, non-recursive check, the same on every interpreter and stack size (reproduced on 3.14.6 with an unlimited stack). - The waiver-expiry test computed its dates when the tests were collected and compared them with the date when the test ran, so a run that crossed midnight UTC turned "91 days" into the 90-day limit. Both now come from one day. - The "newest releases in the declared ranges" jobs ran the locked_only test that checks the Action lock's closure, which differs from the newest releases by design; they now deselect locked_only tests as the floor job does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYWyP187ErPJ9Tkdpt7Fnx
… alerts, and strict I-JSON
…ith #131 to #138 The branch was based on the pre-squash history of #130. Its change is re-applied on current main; conflicts with later fixes on main are resolved in favour of main where main already covers the same problem. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015EESHz3nNgsvwVBKtotKHB
Collaborator
Author
|
Reconciled with Re-applied on current
Checked locally: ruff, format, mypy strict, and 2,180 validator, diff, export, hashing and live tests. CI on this head is the full check. Posted by Claude Code on behalf of the maintainer; not an independent human review. Generated by Claude Code |
…140 Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015EESHz3nNgsvwVBKtotKHB
aisecnomad
pushed a commit
that referenced
this pull request
Nov 12, 2026
…131 to #141 Conflicts in README.md, ROADMAP.md and docs/known-limitations.md take main's text; CLAUDE.md, action.yml, docs/drift-gate.md and CHANGELOG.md are merged by hand. tests/test_modes.py compares the socket connect method by identity now that the suite installs its own guard against outbound connections. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015EESHz3nNgsvwVBKtotKHB
aisecnomad
added a commit
that referenced
this pull request
Nov 12, 2026
…t hermeticity & 0.2.0 alignment (#142) The release verification step fails on each unmet condition, the macOS smoke job runs on pull requests with the confinement and collector tests, the Action refuses a subset of collectors while validation is on, and the test suite blocks outbound connections. The version is 0.2.0 in the package metadata, changelog and citation file, with the changelog entries for #139 to #142. Reconciled with main after #131 to #141. Review added: the documented example digests are regenerated for the new version, the security policy and tutorial match the new gate-protection guidance, and a signer policy entry in another case is refused on every file system, which repairs the macOS job that had failed on main since #140. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015EESHz3nNgsvwVBKtotKHB
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Remediates the Integrity & Risk Validators, Drift Differ, Package/SARIF/VEX Exporters, Live Verification & Core Canonical Serialization findings from the end-to-end security & architecture audit (validator/live side of
NS-MED-01,NS-MED-02,NS-MED-03,NS-MED-04, strict I-JSON items ofNS-MED-07, A2A/timestamp items ofNS-LOW-01, andNS-LOW-02).This PR is 3 of 4 strictly file-disjoint subsystem PRs targeting
claude/audit-remediation(0overlapping paths across PRs 1–4; merges cleanly in any order).Findings Remediated
validators/integrity.py,validators/risk.py,live/inventory.py, andlive/verify.pyso unpinned MCP runner invocations, floating container digests, missing tool/prompt digests, and authority-claim mismatches surface deterministic integrity/drift findings rather than passing completeness gates.diff/differ.pyso capability escalations, trust-tier downgrades, pinning regressions, and authority/digest mutations emit high-severity drift alerts even when component names/versions remain unchanged.generators/package_identifiers.pywith strict RFC 3986 / PURL-spec segment encoding, namespace/name validation, and qualifier sanitization so crafted model/package identifiers cannot inject arbitrary PURL qualifiers, path traversal segments, or control characters into CycloneDX/SPDX exports.generators/sarif.pyandgenerators/vex.pyto normalize rule identifiers, preserve deterministic location/fingerprint bindings, and reject non-conforming vulnerability/status projections.core/hashing.py,core/aibom.py, andcore/evidence.pyto rejectNaN/Infinity, surrogates, and out-of-range IEEE-754 integers during native AIBOM save and canonical evidence digest computation.core/models.pyandcore/evidence.py.Touched Files (13 exclusive paths)
src/nexus_symbiote/core/aibom.pysrc/nexus_symbiote/core/evidence.pysrc/nexus_symbiote/core/hashing.pysrc/nexus_symbiote/core/models.pysrc/nexus_symbiote/diff/differ.pysrc/nexus_symbiote/generators/package_identifiers.pysrc/nexus_symbiote/generators/sarif.pysrc/nexus_symbiote/generators/vex.pysrc/nexus_symbiote/live/inventory.pysrc/nexus_symbiote/live/verify.pysrc/nexus_symbiote/validators/integrity.pysrc/nexus_symbiote/validators/risk.pytests/test_audit_pr3_validators_exports.pyVerification
ruff check src tests scripts&ruff format --check src tests scripts— passedmypy src/nexus_symbiote scripts— 0 issues in 88 source filesscripts/check_security_regressions.py— 18 cases, 0 failuresscripts/check_repository_hygiene.py— passedpytestacross 19 validator, diff, export, schema-contract, live, and PR 3 regression suites — 589 passed, 0 failed