Skip to content

Sandboxed adapter protocol

External tools are untrusted evidence producers. They do not evaluate controls, change policy, or mark assertions as passing. The scanner accepts only schema-valid observations and leaves the final assertion assessment to the deterministic engine.

Protocol

prc-adapter-jsonl-v1 is a line-delimited JSON protocol over standard input and standard output. The scanner sends exactly three messages:

{"type":"hello","protocol":"prc-adapter-jsonl-v1","run_id":"<64 lowercase hex characters>"}
{"type":"input","subject":{"target_name":"example","inventory_digest":"<64 lowercase hex characters>"},"facts":{},"config":{}}
{"type":"execute"}

An adapter may emit bounded log, observation, and artifact messages and must end with exactly one summary. Standard output is protocol-only. Standard error is bounded diagnostic text. A summary is an implementation execution status, not a control result.

Raw standard error is not echoed in CLI JSON. The runner reports only its byte count and SHA-256 digest. Protocol messages may still describe sensitive target facts, so operators must treat the transcript as assessment evidence and apply appropriate access, redaction, and retention controls.

The decoder rejects:

  • unknown or duplicate JSON fields;
  • blank, oversized, excessive, or trailing messages;
  • absolute, non-normalized, or escaping artifact and source paths;
  • malformed digests, statuses, locations, and counters;
  • missing summaries or messages after a summary; and
  • fields through which an adapter tries to declare pass, suppress a finding, or otherwise take evaluator authority.

The machine contract is adapter-message.schema.json. The checked-in release suite includes completed, unsupported, timeout, malformed, resource-limit, undeclared-output, and explicit evaluator-authority attack transcripts.

Manifest v0.4 also defines two closed native-output protocols. The first, prc-adapter-gitleaks-json-v1, accepts only Gitleaks 8.30.0's reviewed official image digest, exact scanner-owned current-tree command, and JSON report contract. The scanner supplies the SHA-256-pinned upstream default ruleset on standard input, forces full redaction, ignores target-owned Gitleaks configuration, ignore files, and gitleaks:allow comments, and disables archive and recursive-decoding expansion. The normalizer rejects unknown or duplicate fields, unredacted findings, history or symlink metadata, paths outside the snapshot, inconsistent fingerprints, invalid coordinates, and excessive findings. It retains only rule identity and normalized location metadata.

The exact redacted native report is represented by a media type, byte count, and SHA-256 artifact descriptor in the transcript; raw report content is not copied into the durable run record. This preserves content provenance without persisting matched source context. An empty report becomes an explicit not_found observation. One or more findings become found observations, but neither the tool nor normalizer can declare an assertion pass or failure.

The second closed protocol, prc-adapter-syft-cyclonedx-json-v1, accepts only Syft 1.51.0's reviewed non-root image digest and the scanner-owned directory command. The scanner injects an empty configuration at a reserved .prc path, so target .syft.yaml or environment defaults cannot disable catalogers or redirect output. The normalizer requires Syft's exact tool identity and CycloneDX 1.7 document envelope, rejects duplicate keys, malformed component, package-URL, dependency, or metadata structures, and caps the component and output counts.

Syft generates a fresh timestamp and serial number for identical input. The normalizer removes those optional values and the source component's non-semantic bom-ref, then canonically orders components, properties, and dependencies. The resulting application/vnd.cyclonedx+json;version=1.7 artifact is content-addressed and byte-for-byte deterministic for the same sealed inventory. When --state-dir is supplied, the bytes are written before the run that references them. A successful value observation proves only that this repository-inventory SBOM was generated; it does not claim that a built artifact is complete, vulnerability-free, acceptably licensed, or production ready.

The third closed protocol, prc-adapter-grype-json-v1, accepts only Grype 0.116.1's reviewed image digest, exact scanner-owned command, and enriched JSON report. It injects scanner-owned configuration that disables application and database updates, external sources, target ignores, and VEX policy. The container runs without network access against one explicitly supplied, read-only grype-db directory. The runner hashes and seals that directory while the normalizer independently requires a valid schema-v6 database, an official archive URL with a SHA-256 checksum, a build age no greater than 120 hours, and valid EPSS, KEV, and NVD provider provenance.

Grype output is decoded with duplicate and unknown-field rejection and bounded nested records. Ignored matches, package alerts, escaping paths, unsupported tool or database identities, incomplete matching evidence, or unsafe numeric values fail the execution. The deterministic normalized artifact retains only the findings and provenance needed for assessment; report timestamps and bulk raw metadata do not affect its digest. A not_found observation is time-bound evidence for the catalog gate, while any found observation remains a factual tool result for the engine to assess.

An assertion binding owns the meaning of an observation outcome. Bindings that omit an outcome policy retain the conservative analysis default: not_found passes and found fails. A binding may instead declare a nonempty pass_outcomes list and a disjoint, possibly empty fail_outcomes list from the closed found, not_found, value, and unsupported vocabulary. Any unconfigured outcome remains Unknown; a configured pass mixed with another unconfigured outcome is Conflicting. The SBOM assertion therefore treats only value as passing and has no failing tool outcome: malformed, missing, unsupported, partial, or contradictory output never becomes Pass.

Capability manifest

Every external adapter has a strict adapter-manifest.schema.json. Manifest v0.4 binds the exact protocol and output schema, compatible engine APIs, publisher and owner identities, immutable tool version and supported format versions, declared observation kinds, maintenance state, and known limitations. Output validation rejects an observation kind that is not declared by that exact manifest. The archived v0.1 through v0.3 schemas remain available for record interpretation but are not accepted for new execution.

Trust is deliberately not self-declared by the adapter. A registry or explicit local operator grant must bind trust to the exact manifest digest and publisher; the manifest alone cannot promote itself to first-party or verified status. The current experimental runner deliberately supports only a narrow subset:

  • OCI execution through Docker or Podman;
  • an image reference pinned by a sha256 digest and explicit registry host;
  • a private, read-only snapshot containing the regular files in the sealed inventory plus any exact protocol-owned policy input described below;
  • explicitly declared, content-hashed, read-only external data directories for reviewed native adapters whose immutable datasets are too large to copy into each project snapshot;
  • no image pull during a scan;
  • no network;
  • no secret handles;
  • no child processes for generic JSONL adapters, with narrowly reviewed, PID-bounded OS-task allowances for the pinned Gitleaks, Syft, and Grype binaries;
  • an optional bounded scratch tmpfs; and
  • explicit wall-time, memory, CPU, process, line, message, stdin, stdout, and stderr limits.

Before execution, the scanner reopens and hashes every inventoried regular file while copying it to a private temporary snapshot. Scanner-excluded paths and symlinks are not copied. Protocol-owned path remapping may prevent a target file from changing analyzer policy without dropping its content: the Gitleaks protocol relocates the root .gitleaksignore, scans its original bytes, and maps any resulting location back to .gitleaksignore. The Syft and Grype protocols add only their digest-bound scanner configurations under the inventory-excluded .prc namespace. A changed type, size, or digest stops execution. The snapshot has a 4 GiB safety ceiling, is mounted read-only, and is removed after the run. Its own deterministic digest is sealed into the OCI plan and checked again immediately before and after the container runs, so observations cannot silently refer to different bytes than the inventory.

External data is a separate trust boundary. A manifest can declare only a bounded named directory mounted at its reserved /prc-inputs/NAME path. The operator supplies the host directory explicitly. The scanner rejects empty, oversized, over-populated, symlink-containing, non-regular, missing, extra, or duplicate mounts; hashes every file before planning; seals the digest, file count, byte count, and destination into the OCI plan; and rehashes the directory immediately before and after execution. The durable execution records this content identity but never the host path. A data directory changing during a run is an execution error, not evidence. This mechanism does not download or refresh data and does not let an adapter expand its declared mounts.

The generated OCI command also drops all Linux capabilities, enables no-new-privileges, uses the invoking non-root UID/GID so private snapshot permissions need not be broadened, disables swap beyond the memory ceiling, caps open files, makes the image root filesystem read-only, and removes the named container. The runner refuses root and Windows hosts. Timeout cleanup targets only the scanner-generated container name. The plan records the OCI client binary digest and execution stops if the plan, manifest, runtime binary, or bound snapshot changes after capability evaluation.

An OCI runtime is a security control, not proof of perfect isolation. Operators must keep the runtime and host patched, review adapter images, and treat runtime or resource-control failure as an execution error. Podman documents that some rootless resource limits depend on the host cgroup configuration.

Registry lockfile and revocation

adapter-registry.schema.json defines the local registry trust root. Each entry pins the adapter ID, manifest SHA-256, publisher ID, registry-assigned trust, lifecycle status, and normalized relative manifest path. The loader hashes and validates the manifest, rejects publisher or lifecycle drift, rejects symlinked or escaping paths, and verifies current engine compatibility.

revoked entries remain effective even when the compromised manifest has been removed. Default resolution permits only first-party-sandboxed and verified-community entries; it denies deprecated, unverified-community, and local entries. A manifest cannot alter these registry decisions. Detached, scoped Ed25519 verification is available through an explicitly selected publisher trust store. The current repository does not yet publish an official release trust store or signed registry, so a public adapter distribution channel is not yet complete.

Validate and inspect a lockfile without executing anything:

prc adapter registry-validate \
  --file /path/to/adapter-registry.yaml \
  --format json

Inspect and validate

Validate the full release fixture suite without executing an adapter:

prc adapter fixture-validate \
  --suite fixtures/adapters/fixture-suite.yaml \
  --format json

The strict adapter-fixture-suite.schema.json binds every transcript to the canonical digest of one exact manifest. A case may reduce the manifest's line, message, or stdout ceiling to exercise a resource failure, but it cannot increase adapter authority or resource limits. The runner rejects symlinks and paths outside the suite, hashes the suite and transcript corpus, and evaluates every case twice. It compares exact disposition, summary status, error class, and observation ID, kind, and outcome. CI fails when any expectation drifts or either evaluation differs.

The resulting adapter-fixture-report.schema.json is release evidence for protocol compatibility only. It does not prove the upstream analyzer's detection accuracy, image provenance, runtime isolation, or suitability for the production profile. A real adapter still needs its own tool-specific corpus, reviewed pinned image, registry entry, publisher trust, and exact catalog binding.

Validate a transcript without executing an adapter:

prc adapter validate-output \
  --manifest fixtures/adapters/fixture-adapter.yaml \
  --file fixtures/adapters/valid-output.jsonl

Render the exact OCI command without starting a container:

prc adapter plan-oci \
  --manifest /path/to/pinned-adapter.yaml \
  --target /path/to/project \
  --runtime docker

Run an already-present pinned image in the bounded OCI environment and emit a content-addressed execution record bound to the exact manifest and inventory:

prc adapter run-oci \
  --manifest /path/to/pinned-adapter.yaml \
  --target /path/to/project \
  --runtime docker

--pull=never means the command fails if the exact image is not already available. For a manifest that declares external data, repeat --data NAME=/path/to/directory for both plan-oci and run-oci. Current v0.3 execution records include required resolution and data_inputs identities. An explicit manifest records the publisher and local-explicit operator grant; a registry resolution additionally binds the registry ID, revision, content digest, and registry-assigned trust. Changing any provenance field changes the execution ID. Version-specific v0.1 and v0.2 records remain valid for archived runs, but v0.1 records cannot be supplied as evidence to a new scan. Current run results embed these records. A scan may execute up to 16 adapters when applicable assertions bind every exact adapter ID, manifest SHA-256 digest, and observation kind. Repeat --adapter-manifest for an explicit local set:

prc scan \
  --target /path/to/project \
  --catalog-root /path/to/trusted/catalog \
  --mode verify-local \
  --adapter-manifest /path/to/first-pinned-adapter.yaml \
  --adapter-manifest /path/to/second-pinned-adapter.yaml \
  --adapter-runtime docker

Scan-level data bindings use an adapter-qualified name so multi-adapter runs cannot consume one another's inputs:

prc scan \
  --target /path/to/project \
  --catalog-root /path/to/trusted/catalog \
  --profile prc/supply-chain \
  --mode verify-local \
  --adapter-manifest /path/to/pinned-adapter.yaml \
  --adapter-data 'prc.adapter.example@1.0/example-db=/path/to/database-cache'

For registry-assigned trust and revocation, resolve the same catalog-pinned adapter through a lockfile instead:

prc scan \
  --target /path/to/project \
  --catalog-root /path/to/trusted/catalog \
  --mode verify-local \
  --adapter-registry /path/to/adapter-registry.yaml \
  --adapter-id prc.adapter.first@1.0 \
  --adapter-id prc.adapter.second@1.0 \
  --adapter-runtime docker

--adapter-manifest is the explicit local-operator path and is mutually exclusive with --adapter-registry. Adapter paths and IDs cannot repeat. Both paths still require an exact manifest digest binding in an applicable catalog assertion; registry approval cannot authorize a catalog-unbound adapter. The scanner resolves, validates, and authorizes the complete set before invoking the runtime, then executes the set in deterministic adapter-ID and manifest-digest order. When project configuration supplies a maximum duration, one shared deadline bounds the complete set rather than restarting the budget per adapter.

The explicit mode grants only the reviewed no-network OCI capability envelope; authorization is checked before the OCI runtime is invoked. The adapter cannot declare an assertion assessment: the engine maps found, not_found, unsupported, incomplete, and conflicting observations through the catalog's immutable binding. The execution and each resulting evidence envelope are bound to the exact inventory digest. Offline execution-record import is deliberately unsupported because a content digest alone does not prove that a tool ran.

The default PRC-A-CORE-013 binding pins prc.adapter.gitleaks@8.30 and the canonical manifest digest for adapters/gitleaks-v8.30.0.yaml. It remains Blocked in an ordinary inspect-mode core scan because inspect mode never launches containers. To produce executed evidence, an operator must first pull the exact image digest and then explicitly run prc verify, which selects --mode verify-local with the bundled manifest. The alias rejects custom adapter, profile, and AI overrides. The checked-in protocol fixture still exercises generic JSONL plumbing in tests but is not authorized by the production profile.

docker pull \
  ghcr.io/gitleaks/gitleaks@sha256:691af3c7c5a48b16f187ce3446d5f194838f91238f27270ed36eef6359a574d9

prc verify /path/to/project

The command scans only the sealed current-tree snapshot. It does not scan Git history, files above 10 MiB, symlink targets, archives, or recursively decoded content. Gitleaks rules are heuristic; both false positives and false negatives remain possible, so this binding is one selected analysis class rather than a claim of complete secret or static-analysis coverage.

Normal validation and scanner-release workflows pull that exact digest and run the clean and suppression-resistant finding cases through the production OCI runner. A nonempty test-image override that differs from the reviewed digest is an error, not a skipped test.

Design references

  • OCI content descriptors define digest-addressed references and require retrieved content to be checked against its digest.
  • Docker container run documents read-only roots, capability removal, resource limits, and no-new-privileges.
  • Podman run documents the corresponding rootless container and resource controls.
  • Gitleaks v8.30.0 is the pinned upstream release; its official CLI documents directory scans, JSON reports, full redaction, timeouts, and bounded target-file size.
  • Gitleaks default configuration is vendored as a deterministic gzip archive, attributed in THIRD_PARTY_NOTICES.md, and verified in both compressed and exact decompressed forms before every execution.