chore(deps): update dependency sheeki03/tirith to v0.3.3 - #5618
chore(deps): update dependency sheeki03/tirith to v0.3.3#5618renovate-fullsend[bot] wants to merge 1 commit into
Conversation
Site previewPreview: https://947965b0-site.fullsend-ai.workers.dev Commit: |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
d68e995 to
15071d3
Compare
|
🤖 Finished Review · ✅ Success · Started 4:20 AM UTC · Completed 4:31 AM UTC |
ReviewFindingsHigh
Next steps:
Previous runReviewFindingsHigh
Next steps:
Previous run (2)ReviewFindingsHigh
Next steps:
Previous run (3)ReviewFindingsHigh
Next steps:
Previous run (4)ReviewFindingsHigh
Labels: PR modifies sandbox container image (images/sandbox/Containerfile) and is a Renovate dependency update Next steps:
Previous run (5)ReviewFindingsHigh
Labels: PR modifies sandbox container image (images/sandbox/Containerfile) and is a Renovate dependency update Next steps:
Previous run (6)ReviewFindingsHigh
Next steps:
Previous run (7)ReviewFindingsHigh
Next steps:
Previous run (8)ReviewFindingsHigh
Next steps:
Previous run (9)ReviewFindingsHigh
Next steps:
Previous run (10)ReviewFindingsHigh
Next steps:
Previous run (11)ReviewFindingsHigh
Next steps:
Previous run (12)ReviewFindingsHigh
Labels: PR modifies sandbox container image and is a dependency update Next steps:
|
15071d3 to
b9976b0
Compare
|
🤖 Finished Review · ✅ Success · Started 3:37 PM UTC · Completed 3:47 PM UTC |
b9976b0 to
9526609
Compare
|
🤖 Finished Review · ✅ Success · Started 4:21 AM UTC · Completed 4:34 AM UTC |
9526609 to
23abe7c
Compare
|
🤖 Finished Review · ✅ Success · Started 3:37 PM UTC · Completed 3:49 PM UTC |
23abe7c to
196ef1d
Compare
|
🤖 Finished Review · ✅ Success · Started 4:23 AM UTC · Completed 4:34 AM UTC |
196ef1d to
b00f9af
Compare
|
|
🤖 Finished Review · ✅ Success · Started 1:44 PM UTC · Completed 1:56 PM UTC |
b00f9af to
739989b
Compare
|
🤖 Finished Review · ✅ Success · Started 4:05 PM UTC · Completed 4:16 PM UTC |
739989b to
91bbf9a
Compare
|
🤖 Finished Review · ✅ Success · Started 4:19 AM UTC · Completed 4:30 AM UTC |
91bbf9a to
df3c18c
Compare
|
🤖 Finished Review · ✅ Success · Started 4:01 PM UTC · Completed 4:11 PM UTC |
df3c18c to
840986c
Compare
|
🤖 Finished Review · ✅ Success · Started 4:19 AM UTC · Completed 4:31 AM UTC |
840986c to
e49d9c4
Compare
|
🤖 Finished Review · ✅ Success · Started 3:53 PM UTC · Completed 4:06 PM UTC |
e49d9c4 to
df93994
Compare
|
🤖 Finished Review · ✅ Success · Started 4:19 AM UTC · Completed 4:30 AM UTC |
df93994 to
a8a5a2b
Compare
|
🤖 Finished Review · ✅ Success · Started 4:07 AM UTC · Completed 4:16 AM UTC |
| # in the GitHub release: https://github.com/sheeki03/tirith/releases | ||
| ARG TIRITH_VERSION=0.3.1 | ||
| ARG TIRITH_VERSION=0.3.3 | ||
| ARG TIRITH_SHA256_AMD64=571e6a300e4c444293476537a322666069e561c7f05283d6650f5b8ef83db3ac |
There was a problem hiding this comment.
[high] checksum mismatch
TIRITH_VERSION was bumped from 0.3.1 to 0.3.3 but the SHA256 checksums (TIRITH_SHA256_AMD64 on line 148 and TIRITH_SHA256_ARM64 on line 149) were not updated. They still contain the v0.3.1 hashes. The RUN block downloads v0.3.3 tarballs and verifies them with sha256sum -c against these stale hashes, so the container build will unconditionally fail. This issue was flagged in the prior review and remains unfixed.
Suggested fix: Fetch the v0.3.3 checksums from https://github.com/sheeki03/tirith/releases/download/v0.3.3/checksums.txt and update TIRITH_SHA256_AMD64 and TIRITH_SHA256_ARM64 on lines 148-149 to the correct values for the x86_64 and aarch64 tarballs respectively.
|
🤖 Finished Retro · ✅ Success · Started 1:08 PM UTC · Completed 1:23 PM UTC Commit: |
Retro: PR #5618 — Renovate tirith bump with 13 redundant review cyclesWhat happenedPR #5618 was a Renovate bot PR bumping Over 7 days (Aug 1–7), Renovate rebased the branch 13 times due to frequent merges to The fix agent ran once but was blocked by a bug: Review qualityThe review agent performed well on the substance: it correctly identified the checksum mismatch as a build-breaking issue on the very first review, researched the upstream release, and provided the exact remediation including correct SHA256 values. The protected-path finding was also appropriate. Evidence for existing issuesThis PR provides strong supporting evidence for several open issues:
Token costConservatively, 12 of the 13 review runs were redundant (the first review found everything). Each run took ~10 minutes. This represents significant wasted compute for findings that were already known and unactionable by the bot PR author. Proposals filed
|
Renovate Ignore NotificationBecause you closed this PR without merging, Renovate will ignore this update ( If you accidentally closed this PR, or if you changed your mind: rename this PR to get a fresh replacement PR. |
This PR contains the following updates:
0.3.1→0.3.3Warning
Some dependencies could not be looked up. Check the Dependency Dashboard for more information.
Release Notes
sheeki03/tirith (sheeki03/tirith)
v0.3.3Compare Source
Added
deobfuscatetext-normalization pass (zero-width / invisible strip, Unicode confusable skeleton, NFKC, inter-character whitespace collapse, bounded leetspeak fold, and short base64 / hex decode behind a printable gate). Prompt-injection and config-file scanning now run against these normalized variants in addition to the raw input; raw scanning is never replaced. Typoglycemia (character transposition) is out of scope for now.PromptInjectionObfuscated(High): a new rule for an injection seed that matches only after deobfuscation, where the obfuscation itself is the signal. Raw matches still fireIgnorePreviousInstructions/PromptInjectionInOutput.OutputDataExfiltration(High, MITRE T1041): a newexfilrule for markdown / URL beacons carrying a secret-shaped token or canary, secret-shaped URL query values, and "read a sensitive path then send / post / upload" directives (including the "do not tell the user" stealth directive).injection_seeds_custompolicy field: add your own prompt-injection seed regexes via.tirith/policy.yaml, user, or org policy. A repo-scoped policy may add seeds (it can only tighten). Invalid regexes are reported bytirith policy validateand skipped rather than failing the load.mcp_redact_injectionpolicy field (default off): opt-in to downgrade an injection-only MCP tool-output block to a redacted warning, blanking the seed spans and forwarding the rest. Only user / org scope can enable it (a repo-scoped policy cannot weaken this), and it refuses to downgrade when any non-injection finding blocks or when structured content is present.ExfilEndpointsource (Primary tier): a known-exfiltration-endpoint hostname source, populated from the CI threat feed.Fixed
.deb/verify-selfbyte mismatch (issue #146): the Debian package now ships the exact canonical release binary instead of a separate rebuild, so an apt-installed tirith passestirith verify-self. A release CI guard asserts the.debbinary is byte-identical to the published tarball binary. The.rpmis still rebuilt against the target distro's glibc (so it keeps running on RHEL / Rocky 9) and is intentionally not byte-identical.verify-selfno longer falsely flags source-built installs:cargo install, AUR, and the distribution.rpmare compiled from source or against a different libc, so they cannot be byte-compared to the generic release binary. They now report an honest "unverified" (exit 0) rather than a false "modified or replaced" failure, and AUR installs on Arch and its derivatives are detected as such. Homebrew is handled the same way: the homebrew-core formula builds from source (distributed as a bottle), so a mismatch downgrades to "unverified" namingbrew install sheeki03/tap/tirith, while the prebuilt tap binary still verifies.v$versiontov0.3.2$version).v0.3.2Compare Source
Added
M5 platform & reach:
tirith lab(offline adversarial training corpus); Windows / PowerShell rules (ExecutionPolicy bypass, Defender exclusions, inline IEX download-execute).M5.5 / M6 install & fix:
tirith fix(interactive safe-command rewriter);tirith installacross 12 backends;tirith doctor --quick.M7 output & egress firewall:
tirith view,output,share,redact,clipboard,logs; output-direction detection (OSC 52 clipboard writes, fake prompts, hyperlink / title manipulation);gateway run --filter-output,mcp-server --sanitize-tool-output.M8 operational context:
tirith context,ssh,iac,sudo,devcontainer,codespaces,prompt-status; labeled-prod, IaC-apply, sudo-escalation, and container rules.M9 workstation hygiene:
tirith hygiene,persistence,aliases,env,exec,path,hooks; credential-hygiene, persistence-foothold, PATH-hijack, and executable-provenance rules.M10 blast radius & isolation:
tirith preview,watch,taint,intend,baseline,temp-run,fetch --save; blast-radius, taint, and opt-in anomaly rules.M11 trust ecosystem:
tirith command-card,commands,canary,secret,incident; signed-attestation and canary-honeytoken rules.M12 browser & visual provenance:
tirith visual-audit,browser(Chrome native-messaging host),paste --with-source;paste_source_mismatchrule.M13 onboarding, dashboard & rule DSL:
tirith onboard,dashboard,rule(customwhen:predicate DSL),ai; AI-config drift rules; 7 policy templates.M14 IDE integration:
tirith lsplanguage server with editor analysis profiles.Agent-session guardrails:
tirith pending(decision registry),tirith audit verify(tamper-evident hash chain), per-rule suppression cooldown, checkpoint restore reporting, agent-memory config scanning, deferred outcome (tirith check --defer, exit code 4), and cross-event correlation rules.CLI transparency & safety UX:
tirith status(protection health, non-zero on degrade),tirith policy effective,tirith trust from-last-trigger, global--quiet/TIRITH_QUIET,tirith daemon start --detach;tirith check --suggest(renamed from--suggest-safe-command).Agent governance — engine wiring, bypass-path origin stamp, and shared YAML helper (M4 item 8 chunk 3) — chunk 3 of the per-agent governance work. Turns chunk 2's observation-only
agent_rulesschema into enforcement, fixes the engine's bypass-path double-log so aTIRITH=0-bypassed BLOCK records exactly one audit entry withagent_originpopulated, and consolidates the duplicatedyaml_safe_scalar/yaml_safe_inline_commenthelpers into a single shared module. (1) Engine wiring —apply_agent_rulesruns insidepost_process_verdict. A new helpercrate::escalation::apply_agent_rules(&mut Verdict, &Policy)consultspolicy::agent_decisionagainstverdict.agent_originand applies the minimal chunk-3 cut:Deniedforcesaction = Blockand appends a freshFinding { rule_id: RuleId::AgentDeniedByPolicy, severity: High, … }whose description names the matched origin (Debug-escaped) and the policy file path;AllowedandUnspecifiedleave the verdict alone (andagent_origin: Noneis treated asUnspecified— an engine path that never set an origin has nothing to match against). The splice is after escalation and before warning recording so escalation-driven Blocks stay Block and the Warn-recording path correctly skips when chunk 3 flips to Block. Existing detection findings are preserved —agent_ruleslayers on top, never replaces. Allow is NOT a bypass — a verdict the engine already blocked stays blocked even if the caller is on the allow-list; richer "trusted agent" semantics (severity overrides onallow,approval_required: true, per-originfail_mode) are deferred to a future chunk pending real telemetry from chunk-2'stirith agent sessionsreads. (2) NewRuleId::AgentDeniedByPolicywired through every safeguard touchpoint:verdict.rs(enum variant),scoring.rs(the structural arm ofis_threat_intel_rulereturnsfalse),build.rs(EXPECTED_RULES),golden_fixtures.rs(ALL_RULE_IDS+EXTERNALLY_TRIGGERED_RULESsince the rule fires from policy state, not from input characters),assets/data/rule_explanations.toml(full operator-facing entry — title, categorypolicy, severity rationale, threat-model recap, false-positive guidance pointing attirith agent allow, remediation). The fourgolden_fixtures.rssafeguard tests (test_all_rule_ids_have_fixture_coverage,test_no_url_rules_have_no_url_fixtures,test_extractor_ids_cover_rule_triggers,test_tier1_does_not_gate_findings) andtest_rule_id_list_is_completeall pass — every touchpoint stays in sync. (3) Bypass-path origin stamp. Pre-chunk-3, whenTIRITH=0was honored,engine::analyze_innercalledcrate::audit::log_verdictitself — inside the engine, before the CLI got a chance to setagent_origin. Then the CLI also calledlog_verdict, producing a double audit entry where the first entry was missing origin and the second one had it. Chunk 3 removes the engine's bypass-path audit entirely; the caller is now the single audit site for the engine-driven analysis paths (cli/check.rs,cli/paste.rs,mcp/tools.rs::call_check_command) and always stampsagent_originfirst.cli/paste.rsandmcp/tools.rspreviously SKIPPED audit onbypass_honored(trustinganalyze()to have logged); both now audit unconditionally after stamping origin.cli/check.rswas already auditing in this branch; it now stops double-logging because the engine no longer logs. The same review-round pass extended the stamping invariant to the analysis-then-audit paths that previously calledaudit::log_verdictwithout settingverdict.agent_origin:cli/install.rs(both the package-manager and URL forms) andcli/ecosystem.rs::scannow resolve the CLI origin (resolve_cli_origin(interactive)) and stamp it on the verdict before the audit write, sotirith installandtirith ecosystem scanaudit lines are attributed instead of landing intirith agent sessions' "unknown" group. The gateway path stampsAgentOrigin::Gatewayon the in-memory raw verdict in chunk 1 and persists the same constant in its local stderr-JSONLAuditEntry<'a>via a new always-setagent_originfield — closing a pre-chunk-3-follow-up gap where the in-memory verdict carried origin but the serialized line dropped it. The audit-aggregatortirith agent sessionsdocumentation incrates/tirith/src/main.rsis updated to reflect the closed audit sites; the help text is explicit that origin attribution is best-effort and a future analysis-then-audit path that does not stamp origin will still land its entries in the"unknown"group rather than be silently attributed. (4) Chunk-2 test retired.agent_rules_chunk2_loading_changes_no_verdict(incrates/tirith-core/src/policy.rs) was the explicit "chunk 3 must retire this" contract; it's gone, replaced by five chunk-3 tests incrates/tirith-core/src/escalation.rs::tests:agent_rules_deny_forces_block_on_allow_verdict,agent_rules_deny_keeps_block_on_already_blocked_verdict,agent_rules_allow_does_not_bypass_block,agent_rules_unspecified_leaves_verdict_unchanged, and the critical regression guardagent_rules_unset_does_not_introduce_finding(legacy policies with noagent_rulesblock stay byte-for-byte identical to pre-chunk-3 behavior). Plusapply_agent_rules_returns_true_only_on_denied(helper signature),apply_agent_rules_no_origin_is_treated_as_unspecified(None handling), andagent_rules_finding_description_escapes_hostile_origin_payload(Debug-format pinning so a future control-byte-bearing origin can't leak through the audit description). The narrower "engine::analyze itself ignores agent_rules" claim — which IS still true; enforcement lives inpost_process_verdict, not the raw engine — is pinned by a newengine_analyze_does_not_consult_agent_rulestest that replaces the retired one. The chunk-2 field-level invariant (agent_rules_chunk2_observation_only_invariant) is preserved with an updated docstring noting it now guards againstagent_rulesaccidentally bleeding into adjacent mechanisms (allowlist / blocklist / severity overrides). (5) Shared YAML helper module —crates/tirith/src/cli/yaml.rs. Pre-chunk-3,yaml_safe_scalarandyaml_safe_inline_commentlived as two byte-identical copies incli/mcp.rsandcli/agent.rs. Chunk 3 consolidates both into a singlepub(crate)module so future changes (and the DEL-escape post-process that previously had to be ported by hand) live in one place. Both call sites now importsafe_scalar as yaml_safe_scalar(and same forsafe_inline_comment+YAML_NEEDS_QUOTING_BYTES) so the existing call-site code and tests stay readable. The exhaustive round-trip test incli/mcp.rs::yaml_safe_scalar_round_trips_through_yaml_parser(every YAML special character, every C0 control byte, DEL, multi-byte UTF-8, empty string) still passes byte-for-byte; the shared module ships its own smoke-test suite (safe_scalar_empty_becomes_quoted,safe_scalar_plain_identifier_is_bare,safe_scalar_quotes_yaml_indicator_byte,safe_scalar_quotes_control_bytes,safe_scalar_escapes_del_for_yaml_roundtrip,safe_inline_comment_passes_safe_strings_unchanged,safe_inline_comment_escapes_control_bytes) so this module is green on its own. (6) Design doc updated —docs/agent-governance-design.md§ 5 flipped from "shipped in chunk 2 (observation-only)" to a full chunk-3 status table mappingAgentDecision→ verdict effect, and § 6's chunk-2 out-of-scope items "policy enforcement" and "the bypass-path double-log" are marked Resolved in chunk 3. A new chunk-3 out-of-scope block documents the deferred richer matcher payloads. Trust model unchanged from chunks 1 + 2. Every signal feedingAgentOriginis still operator-trust, never adversary-resistant —TIRITH_INTEGRATION, MCPclientInfo, CI env vars,is_terminal()are all settable by any process running as the user.agent_rulesenforcement layers on top of caller-claimed signals: a policy author who treats adenymatch as "this came from a hostile caller" is wrong; they're rejecting whatever the caller claimed their identity is. Use this for operator-trust scoping (the operator declares "I don't run my MCP server'stools/callon commands my CI ran"), not for adversarial security. Like every other engine path,apply_agent_rulesis off the tier-1/2/3 detection hot path and touches no network.Agent governance — CLI surface +
agent_rulespolicy schema (M4 item 8 chunk 2) — chunk 2 of the per-agent governance work, building on chunk 1'sAgentOriginscaffolding. Chunk 2 was observation-only: it made chunk 1's recorded signal inspectable and added the policy schema chunk 3 wires on, but no engine path consultedagent_rulesto change a verdict in this chunk. Enforcement landed in chunk 3. The original regression testagent_rules_chunk2_loading_changes_no_verdict(incrates/tirith-core/src/policy.rs) was retired in chunk 3 as that chunk's contract required. (1) New CLI group —tirith agent— four subcommands grouped under a single verb mirroringtirith mcp.tirith agent sessionsreads the local JSONL audit log, groups everyverdictentry by its recordedAgentOrigin, and reports per-group counts, last-seen timestamps, and an Allow/Warn/Block histogram (withWarnAckfolded into the warn bucket). Hook-telemetry rows are filtered out (those are not verdicts and carryagent_origin: Noneby design). Entries with noagent_originland in an explicit"unknown"bucket rather than being silently dropped — honesty over apparent tidiness.--format jsonemits a stable envelope (schema_version,log_path,group_count,total_entries,groups). A missing audit log is NOT an error: it reports plainly with zero groups and exits 0.tirith agent explain <query>drills into one session id (exact match) or command substring (case-insensitive, also against the rendered origin label so an operator can search for"claude-code"), surfacing up to 20 matches sorted newest-first with the attribution chain:AgentOrigin, redacted command, action, timestamps, rule IDs, bypass state, and policy path.tirith agent policy initscaffolds.tirith/agent-policy.yaml.examplefrom the audit log's distinct observed origins — every entry commented out by design (mirrorstirith mcp policy init's convention — importing a scaffold must never silently widen trust). The scaffold is deterministic: origins are sorted by(kind, payload), the header is fixed, and no timestamps are embedded, so two runs against the same log produce a byte-identical file. A missing log writes a header-only template rather than failing — the operator gets a starting point either way.--forceoverwrites an existing example; without it, the example is preserved.tirith agent allow --kind <kind> [--tool <name>]validates an(kind, tool?)matcher pair and prints the YAML snippet an operator pastes underagent_rules.allow:in their policy. It deliberately does NOT mutate any policy file —agent_rulesis observation-only today, and silently appending would suggest enforcement that does not exist yet; the operator integrates it themselves the same way they integratetirith mcp policy init's example output. Validation:kindmust behuman/agent/mcp/gateway/ci/ide;--toolon a payloadless kind (human,gateway) is rejected up-front because it would match nothing; an empty--tool ""is rejected for the same reason. (2) Policy schema —agent_rules— additive field onScanPolicyConfig's parentPolicystruct.AgentRules { allow: Vec<AgentMatcher>, deny: Vec<AgentMatcher> }, withAgentMatcher { kind: AgentOriginKind, tool: Option<String> }whereAgentOriginKindis a separate closed enum mirroringAgentOrigin's discriminator (cleaner deserialization, rejects an unknownkindat policy-load time rather than silently matching nothing). String matching is case-sensitive exact —claude-codedoes not matchClaude Code; the design doc records (Q2) that normalization is intentionally deferred until chunk 3 has a real telemetry sample set. (3) Pure decision helper —policy::agent_decision(&policy, &origin) -> AgentDecision— walksdenyin declaration order (first match →Denied), thenallow(first match →Allowed), elseUnspecified. The helper is not consulted by the engine in chunk 2; chunk 3 will wire it into the verdict pipeline. Atoolfilter applied toHumanorGatewayis harmless (those variants carry no caller-claimed payload — the helper matches nothing); the policy validator surfaces it as a warning so the operator notices the typo. (4) Policy validator updates —policy_validate.rsaddsagent_rulesto the known top-level fields list (so it doesn't trip the "unknown field" warning) and emits Warning-level diagnostics for matchers shaped wrong:toolon a payloadless kind, emptytool: "".threat_intelwas also added to the known list — a pre-existing typo-guard gap that's now closed. (5) Policy template updates — thefull,individual,ci-strict, andai-agent-heavytemplates produced bytirith policy initnow include documented but commented-outagent_rulesexamples tuned to each scenario (CI gets akind: ci, tool: github-actionsallow; ai-agent-heavy gets both allow and deny examples). All templates still validate. (6) Design doc updated —docs/agent-governance-design.md§ 5 flipped from "preview, not built in this chunk" to "shipped in chunk 2", with command-line examples, exit-code semantics, the matcher shape decision (closedkind+ free-formtool, per Q1's recommendation), and the schema-vs-enforcement boundary. Out-of-scope sections clearly delineate chunk-1, chunk-2, and chunk-3 boundaries. (7) YAML safety — server / origin payloads passed throughyaml_safe_scalarare quoted-and-escaped before they reach the example file or thetirith agent allowsnippet output. A maliciously-crafted tool name containing:,#, newline, or ANSI escape cannot break the YAML structure or inject control sequences when the operatorcats the file. The snippet round-trips cleanly throughserde_yamlafter pasting; a dedicated test covers a hostile-payload scaffold parsing back unchanged. Trust model unchanged from chunk 1. Every signal feedingAgentOriginremains operator-trust, never adversary-resistant —TIRITH_INTEGRATION, MCPclientInfo, CI env vars,is_terminal()are all settable by any process running as the user.agent_rulesis for filtering, dashboarding, and observability; if a policy decision must withstand a hostile environment, layer real authentication elsewhere. No newRuleId. Whether enforcement should produce a distinctRuleIdor reuse an existing one is a chunk-3 decision. Like every other observability surface, every command is a local file operation: it touches no network and is off the tier-1/2/3 detection hot path.Agent governance — design spike + observability scaffolding (M4 item 8 chunk 1) — the first artifact of the per-agent governance work, deliberately split off from any enforcement code. (1) Design document at
docs/agent-governance-design.mdrecording the threat model (every signal is operator-trust, not adversary-resistant —TIRITH_INTEGRATION, MCPclientInfo,is_terminal(), every CI provider env var is settable by any process running as the user), the closed enum surface, the population path throughVerdict→AuditEntry, a preview of the plannedtirith agent sessions / explain / policy init / allowsubcommands (chunk 2+), and a section of open questions for the next chunk. The design is explicit that chunk 1 is observation-only: no policy gate consumes the new signal, noRuleIdis added, noActionis changed, and the safeguard tests ingolden_fixtures.rsstay untouched. (2)AgentOriginenum atcrates/tirith-core/src/agent_origin.rs— a closed tagged union with six variants (Human { interactive },Agent { tool, version? },Mcp { client_name, client_version? },Gateway,Ci { provider? },Ide { name }). Every caller-supplied free-form string (tool name, client name, provider, IDE name, version) passes throughsanitize_caller_label/sanitize_caller_version: trim ASCII whitespace, drop ASCII control bytes + Unicode invisible / format / surrogate codepoints (the same classes the byte-scan rules already flag in command input — re-emitting them through the origin label would defeat the scan), and cap at 256 bytes for labels / 64 bytes for versions with char-boundary truncation so a multibyte UTF-8 sequence is never sliced mid-codepoint. A million-byte hostileTIRITH_INTEGRATIONcannot crash, cannot inject newlines into an audit log line, cannot inject ANSI escapes into a terminal, cannot bloat the entry — every constructor path is covered by a unit test. (3) CLI-side environment resolver —resolve_cli_origin(interactive)reads the current process env in fixed priority order:TIRITH_INTEGRATIONset →Agent, named CI provider env set (GITHUB_ACTIONS,GITLAB_CI,BUILDKITE,CIRCLECI,JENKINS_URL,TRAVIS,TF_BUILD,BITBUCKET_BUILD_NUMBER,TEAMCITY_VERSION,DRONE,CODEBUILD_BUILD_ID) →Ci { provider: Some(canonical-tag) }, genericCI=true→Ci { provider: None }, elseHuman { interactive }. The provider tag (github-actions, etc.) is a fixed string chosen by tirith — never assembled from attacker bytes.CI=falseis correctly treated as "not CI". (4) MCP-side per-session origin store atcrates/tirith-core/src/mcp/origin.rs— the dispatcher capturesinitialize.clientInfoonce and writes it to a process-scopedRwLock<Option<AgentOrigin>>; every subsequenttools/callreads it back. AbsentclientInforecordsMcp { client_name: "unknown-mcp-client" }rather than falling back toHuman— "this came from MCP" remains structurally true even when the client is anonymous.client_infois no longer#[allow(dead_code)]. (5) Plumbing into the verdict and audit layers —Verdict.agent_origin: Option<AgentOrigin>andAuditEntry.agent_origin: Option<AgentOrigin>are both additive, both serde-default-on-parse (oldlog.jsonllines without the field still load throughaudit_aggregator::AuditRecord), and bothskip_serializing_if = "Option::is_none"(a verdict that never had its origin set produces noagent_originfield on the wire). The CLIcheck/pastepaths stamp the origin on the raw verdict immediately afterengine::analyze; the MCP tool handlers (tirith_check_command,tirith_check_url,tirith_check_paste) read from the session store; the gateway request and notification paths both stampAgentOrigin::Gateway.log_verdict_with_rawreadsverdict.agent_originand copies it toAuditEntry.agent_origin— no caller wiring changes; populating the verdict is sufficient. Hook-telemetry and trust-change audit entries leaveagent_origin: None(those entry types are not verdicts and their existingintegrationfield already identifies the hook). No newRuleId, no policy enforcement, no schema bump. Chunk 1 ends here; chunk 2 wires thetirith agentsubcommands and the per-origin policy gate.MCP policy governance —
scan.trusted_mcp_servers,scan.mcp_allowed_tools, andtirith mcp policy init— chunk 3 of the Agent & MCP governance work. The policy fieldscan.trusted_mcp_serversalready existed in the schema but was not consulted by any rule; chunk 3 wires it through. An MCP server NAME listed there now suppresses every per-server MCP config finding (mcp_insecure_server,mcp_untrusted_server,mcp_suspicious_args,mcp_overly_permissive, andmcp_duplicate_server_namewhen the duplicate's name is itself trusted), and filters drift entries with that name out of themcp_server_driftfinding — so a server the operator has reviewed and accepted neither raises config-side noise nor counts as drift. The filter is fine-grained: when every drift is for a trusted server, no drift finding fires; when some drifts are for trusted servers and others are not, only the untrusted ones surface — the trusted entries are dropped, not "trust the whole batch or none". An unparseable lockfile still fires (we cannot know which servers it concerned), exactly as it did before. Trust matching is case-sensitive and exact-string — MCP server names are arbitrary identifiers, not URLs, so locale-insensitive folding would be the wrong default. New policy field —scan.mcp_allowed_tools— aHashMap<String, Vec<String>>keyed by MCP server name, listing the exact tool names that server may expose. Two effects, both surfacing through the existingmcp_server_driftrule (no newRuleId): (1) At drift time, when drift exposes a tool to a server whose name is a key inmcp_allowed_toolsand that tool is NOT in the allowed set, the drift finding is upgraded from the default Medium to High severity — adding a tool outside the allowed set is a stronger signal than ordinary drift. The ladder covers both drift shapes that can introduce new tool exposure: aChangeddrift (an existing server gains a new tool — checked viatools_added), and anAddeddrift (a brand-new server appears with a disallowed tool in its declared tool list). Without theAddedarm, an attacker could smuggle a disallowed tool by introducing a new server rather than mutating an existing one, and the finding would stay at the default Medium; the ladder applies symmetrically to both paths. Drift inside the allowed set keeps Medium; anmcp_allowed_toolsentry of[]for a server forbids ANY tool on that server (every new tool is out-of-set); aRemoveddrift does not feed the ladder because the ladder is about new exposure, never lost exposure. (2) At lockfile load, when the lockfile itself records tools outside the allowed set for a server (the failure mode of "an MCP tool was snuck pasttirith mcp lock"), a High-severitymcp_server_driftfinding fires naming the offending tools — alongside any other drift findings, so an operator sees both the immediate violation and the broader inventory state. A server NOT listed inmcp_allowed_toolsis unconstrained — the gate is opt-in. New command —tirith mcp policy init— scaffolds a starter MCP policy from the current.tirith/mcp.lock. It writes.tirith/mcp-policy.yaml.examplelisting every currently-locked server as atrusted_mcp_serverscandidate (deduplicated by name, since the same name can legitimately appear in two configs) and every currently-recorded tool list as anmcp_allowed_toolsentry. Every entry is commented out by design — importing the example must NEVER silently widen trust; the operator reviews the scaffold, uncomments what they intend to declare, and merges it into.tirith/policy.yamlthemselves. A separate.examplefile is cleaner than mutating an existingpolicy.yaml(the operator candiffthe scaffold against their working policy and integrate the bits they want). The scaffold is deterministic — runningmcp policy inittwice against the same lockfile produces a byte-identical example file; the lockfile is already sorted by(name, source_config), and the scaffold sorts further viaBTreeSet/BTreeMap. Server / tool names are YAML-quoted-and-escaped before they reach the example — a maliciously-crafted server name containing a:,#, newline, or ANSI escape cannot break the YAML structure or inject control sequences when the operatorcats the file.--forceoverwrites an existing example file; without it, an existing example is preserved (the operator may have edited it to track their working policy).--format jsonemits a structured planned-policy preview (server count, tool count, the scaffold itself) so a CI integration can ingest the proposal without reading the file. A missing lockfile is NOT fatal — a header-only scaffold is still written so the operator has a starting point; an unparseable lockfile IS fatal (exit 1) because we cannot tell what to list. No newRuleId. Chunk 3 reusesMcpServerDriftfor the lockfile-side disallowed-tool flag and the existingMcp*rules for the config-side suppression — the fourgolden_fixtures.rssafeguard tests, the structural arm inscoring.rs, theEXPECTED_RULESmap inbuild.rs, theALL_RULE_IDSlist, andrule_explanations.tomlneed no schema changes. The policy template (policy.yaml) gains documented but commented-outtrusted_mcp_servers/mcp_allowed_toolsexamples in thefull,individual,ci-strict, andai-agent-heavyvariants, so a freshtirith policy initshows what the fields look like even before the operator runsmcp policy init. Policy validation already acceptedscan.mcp_allowed_toolsas a known field name (it was added in the schema work that introduced the field). Like every othermcpsubcommand,policy initis a local file operation — no network, off the tier-1/2/3 detection hot path.MCP lockfile drift detection —
tirith mcp verify,tirith mcp diff, and themcp_server_driftscan rule — chunk 2 of the Agent & MCP governance work.tirith mcp verifyrebuilds the current inventory and compares it against the committed.tirith/mcp.lock: it exits 0 when the two match, 1 when drift is detected (a server added, removed, or altered — transport, env, declared tools, or URL credentials), and 2 on a usage error (no lockfile to verify against, lockfile unreadable, repo root unresolved). The exit-code split lets a CI integration distinguish "the lockfile is stale" (1, the build must fail) from "there is no lockfile to verify" (2, the build must abort with a clear message).tirith mcp diffshows the same drift but informationally — it exits 0 whether or not drift is present (drift is reported, not enforced), but a usage error (missing lockfile, unreadable lockfile, unresolvable repo root) still exits 2 so a piped consumer can distinguish "no drift" from "I could not check" — for inspecting a config change before refreshing the lockfile. Both support--format jsonwith a stable envelope (schema_version,repo_root,lock_path,command,lockfile_format_version,drift_count,added_count,removed_count,changed_count,in_sync, and a sorteddriftsarray; the drift list is byte-stable regardless of inventory order). Drift is also detected in thetirith scanFileScan path via a newRuleId::McpServerDriftrule (Severity Medium, "review me" not "block"): whentirith scanwalks a repository and reaches.tirith/mcp.lock, the rule parses the lockfile, rebuilds the inventory from the repo's MCP configs, and fires when the two differ — so a pre-commit hook or CI scan catches an MCP-surface change the same way it catches an un-pinned action or smuggled instruction. The structured drift core distinguishes every category from the chunk-2 brief: server added, server removed, transport changes (KindChanged,UrlChanged,UserinfoAdded/Removed/Swapped,CommandChanged,ArgsChanged,EnvChanged), per-env-variable changes (Added/Removed/ValueHashChanged), and per-tool changes (Added/Removed/Set/Reordered). It rides on the chunk-1 inventory hash: whencurrent_lock.inventory_hash == lockfile.inventory_hashthe drift is provably empty and the per-server diff is skipped entirely; only when the inventory hash differs does the merge walk by(name, source_config)produce structured drift entries. Moving an unchanged server between config files is a non-event (chunk 1'scontent_hashdeliberately excludessource_config) — drift only fires when the server itself changed. Privacy invariant. Neitherverifynordiff— nor the new scan rule — ever prints an env value, a URL userinfo string, or even a hash; only the name of the variable or credential that changed reaches the human output and the JSON envelope. The lockfile already stripped raw values into salted hashes in chunk 1; drift detection observes that the hash changed, never the underlying secret. Drift entries are safe to print to a terminal, serialize as JSON, and commit to an audit log. Like every othermcpsubcommand,verifyanddiffare local file operations — no network, off the tier-1/2/3 detection hot path — and discovery is repo-local only.MCP server inventory and lockfile —
tirith mcp lock— the first command of the Agent & MCP governance work.tirith mcp lockdiscovers every Model Context Protocol (MCP) server a repository declares — across.mcp.json/mcp.json/mcp_settings.jsonand the IDE config variants under.vscode/,.cursor/,.windsurf/,.cline/,.amazonq/,.continue/,.kiro/— parses each into a structured inventory (per server: its name, transport — a remoteurl, or a localcommand+args+ the subprocessenvit injects — and declaredtools), and writes a deterministic lockfile to.tirith/mcp.lockat the repository root. Both known config-shape variants (mcpServersand theserversalias) are handled; a server object declaring neither aurlnor acommandis captured with an explicit "unknown transport" rather than dropped. The lockfile carries a top-levelformat_version(currently 4), a per-server content hash (over name + transport — including a stdio server'senvand a URL transport'suserinfo_hash— + tools, independent of which config file the server lives in; every variable-length component is length-prefixed so the hash is collision-free), the captured config list, and aninventory_hashover the whole inventory; the lockfile's servers are sorted by(name, source_config)before the inventory hash is computed, so the file and its hash are byte-stable regardless of config-discovery order and agit diffof.tirith/mcp.lockshows exactly what changed in the repo's MCP surface. Env values are never persisted in the lockfile. An env value declared in.mcp.jsonis commonly a credential (API_TOKEN,GITHUB_PERSONAL_ACCESS_TOKEN,OPENAI_API_KEY, …) and.tirith/mcp.lockis designed to be committed, so each env entry serializes as{ name, value_hash }wherevalue_hashis the lowercase-hex SHA-256 ofname || ':' || value. The name acts as a per-entry salt so a low-entropy value (1,true) is not brute-forceable across servers, and the raw value is consumed and dropped inside the parser before reaching any struct field or output. Drift detection is unchanged in spirit — a value change still flipsvalue_hash, which still flips the per-server content hash — but a committed mcp.lock no longer carries plaintext secrets. URL userinfo (HTTP Basic Auth) is never persisted either. A URL declared ashttps://user:token@host:port/pathis the symmetric leak class: the lockfile is committed, so the rawuser:tokenwould land in version control. The same salted-hash scheme is applied to the URL transport: the URL is stored with userinfo stripped (https://host:port/path), and auserinfo_hash = sha256(server_name || ':' || userinfo)— salted by the MCP server's name — is recorded; it is folded into the per-server content hash so a userinfo swap registers as drift. A URL that carried no userinfo serializes withuserinfo_hashomitted (not set to a sentinel), so "no credential" is structurally distinct on the wire from "credential present"; a non-parseable URL is stored verbatim with no hash (we refuse to mangle a string we cannot structurally parse). Themcp lockhuman summary additionally debug-escapes every env name before printing ("{name:?}", which renders each control byte as a\u{NN}/\n/\rescape rather than the raw byte), so a maliciously-crafted name containing ANSI escapes, newlines, or other terminal control bytes cannot inject control sequences into the operator's terminal; a redacted URL whose source declared credentials prints with a fixed(credentials in source URL)annotation so the redaction is visible without revealing the credential or its hash. This is a local file operation — no network, off the tier-1/2/3 detection hot path — and discovery is repo-local only: user-level configs such as~/.claude/are never inventoried, and the guarantee is enforced — a config path that is a symlink, sits under a symlinked directory, or whose canonicalized path escapes the repo root is rejected rather than followed. A malformed configuration file (not valid JSON, or no MCP-server object) contributes no entries and is never an error or a panic — it is reported as "unparseable", and the lockfile reflects only the configs tirith could read. Finding no MCP configuration at all is likewise not an error: it is reported plainly and an empty-but-valid lockfile is still written as a baseline.tirith mcp lockis a new subcommand group; the existingtirith mcp-server(run tirith as an MCP server) is unchanged.--format jsonreports the captured config / server counts and the full lockfile document.tirith mcp verify/mcp diff(drift detection against a committed lockfile) are planned follow-up work.Package provenance / maintainer-risk scoring —
tirith package riskandtirith package explain(offline signals) — tirith can now score a package's supply-chain risk the waytirith scorescores a URL: a deterministic, fully explainable sum of named factors — no model, no learned weights, no statistical classifier — every score reproducible by hand. This is the offline-signals phase: every signal is computed without any network or registry-API call.tirith package risk <ecosystem> <name>prints the score and a one-line-per-signal summary;tirith package explain <ecosystem> <name>adds the factor-by-factor derivation (mirroringtirith score --explain); both support--format json. The offline signals: (1) Name vs. popular packages — the dominant factor, sourced from the local threat database'spopularset. A name that is a known-popular package scores 0 (recognized); a name one Levenshtein edit from a known-popular package scores high (the classic typosquat/slopsquat shape, viacheck_popular_distance); an unknown name gets a small baseline only — unknown is not the same as malicious. A new exact-matchThreatDb::is_popular_packageaccessor backs the "is recognized" check (the existingcheck_popular_distancedeliberately skips exact matches). (2) Known malicious typosquat — additive: the threat DB'styposquatindex independently lists this exact name as a malicious typosquat (viacheck_typosquat) — a confirmed bad name, stronger than a mere resemblance. (3) Install / lifecycle-script presence — additive, and only when the package content is locally available: tirith inspects a package directory you already have (auto-discovered undernode_modules/site-packagesrelative to the cwd, or an explicit--path) — it never downloads the package. For npm it readspackage.jsonfor a non-emptypreinstall/install/postinstalllifecycle hook; for PyPI it checks for asetup.py. (4) Bundled binary-blob presence — additive, also local-content-only: a bounded directory walk for native/compiled artifacts (.so,.dll,.dylib,.node,.wasm,.jar, …). The final score ismin(100, sum), with the clamp reported as an explicit negative factor so the breakdown always sums exactly to the displayed number — a test asserts that invariant across every signal combination. The score is advisory and standalone:package riskis not a detection rule, produces noVerdict, and changes noAction, exit code, or audit log; existing detection and verdicts are untouched. The offline signals are always computed; registry-API-backed provenance signals are an opt-in addition behind--online(see the next entry) and never touch the hot path.Package risk — registry-API-backed provenance signals (
tirith package risk --online) —package risk/package explaincan now optionally consult a package's registry API for provenance / maintainer-risk signals, on top of the always-on offline signals above. This is strictly opt-in and off the hot path: API calls run only behind an explicit--onlineflag (offline is the default, consistent with tirith's offline-first network policy),--offline/TIRITH_OFFLINEforce offline even with--online, and tirith never adds network totirith checkor any other path —package risk --onlineis the sole entry point. The registry is selected by the<ecosystem>argument: the npm registry API, the PyPI JSON API, and the crates.io API are supported (other ecosystems degrade gracefully). Six registry-API signals, each an explicit named factor in the same deterministic factor-sum model as the offline phase — no ML, no black-box scoring, every factor reproducible by hand: (1) package / version age — a package first published, or a latest version released, very recently (the textbook fresh-typosquat shape; the package-level signal is the larger one and subsumes the smaller version-level one); (2) abandoned ownership — the registry lists an established package with zero maintainers / owners (an account-takeover precursor; a single registry document carries the current owner set, not its history, so a literal transfer cannot be proven from it — an established package that has lost every listed owner is the shape that can be); (3) version spike — the latest version is an abnormal major-version jump from the previous one (a hijacked release often ships an inflated version to capture a semver range); (4) download counts — very low downloads over the registry's reported window (near-zero adoption is a weak signal); (5) repo / source-URL mismatch — the registry lists no usable source-repository URL (provenance cannot be traced to reviewable source); (6) yanked / deprecated status — the registry itself marks the latest version yanked or deprecated. Network hygiene: the registry HTTP client usesreqwestwith an explicit timeout and a response-size cap, exactly asrunner.rs/selfupdate.rsdo. Graceful degradation: a network failure, timeout, HTTP error, unparseable response, or unsupported ecosystem is never a crash, hang, or block — it degrades to the offline score with an honestapi signals: unavailable (reason). Successful registry responses are cached on disk with a TTL (under the tirith state dir) so repeatedpackage riskruns do not hammer the registries. Thepackage_risk::ApiSignalsseam now carries three states —not_computed(offline run, the default),available(an--onlinerun that reached the registry), andunavailable(an--onlinerun that degraded) — and--format jsonexposes the gathered provenance underapi_signals. Offline scoring is unchanged: the API signals only add factors, every offline-phase test still passes, and the score stays advisory and standalone (noVerdict, no exit-code or audit-log change).Install-command coverage — package-manager and infrastructure install patterns — tirith now detects high-risk patterns in package-manager and infrastructure install commands (apt/dnf/yum/zypper/pacman, brew, kubectl, helm, terraform), focusing on the dangerous pattern rather than the tool so a legitimate
apt install foo,brew install foo,kubectl apply -f ./local.yaml,terraform init, orhelm install ./chartstays clean. Seven new rules: (1)repo_add_from_pipe(High) — a network download piped throughteeinto an aptsources.listfile, or redirected straight into one, so the repo definition is added and its signing key trusted without any review; a local file piped intoteedoes not fire. (2)unsigned_repo_trust(High) — an apt sources entry marked[trusted=yes](tolerant of[arch=amd64 trusted=yes]), orapt-getrun with--allow-unauthenticated/--allow-insecure-repositories— all disable GPG signature verification. (3)gpg_check_disabled(High) —dnf/yum/zypperrun with--nogpgcheck, an inlinegpgcheck=0repo setting, or pacmanSigLevel = Never. (4)kubectl_apply_remote—kubectl apply/create/replace -fgiven a remote URL (High for a raw-content blob likeraw.githubusercontent.comor a URL-shortener that hides the source, Medium for any other remote URL); a local file or-kkustomize directory does not fire. (5)helm_untrusted_repo(Medium) —helm install/upgrade/repo add/pullpointed at a chart-repository host not in the recognized set; a local chart path does not fire. (6)terraform_remote_module(Medium) —terraform init -from-module(ortofu) sourcing a root module from a remote git/http(s)/code-hosting location rather than a local path or the Terraform Registry. (7)brew_untrusted_tap—brew installgiven a raw URL instead of a formula name (High), orbrew tappointed at an explicit non-GitHub git remote (Medium). All rules see through a single leadingsudo/doaswrapper (and its value-taking flags) and tolerate shell quoting. Detection is pure pattern matching — no registry or network calls on the hot path. Each rule has fulltirith explaindocumentation, a tier-1 PATTERN_TABLE entry, and golden fixtures covering both the attack shape and the benign baseline.tirith ecosystem scan— project dependency-manifest supply-chain scan — the directory-level companion totirith package risk:tirith ecosystem scan [path]walks a project, discovers and parses dependency manifests (npmpackage.json/package-lock.json, Pythonrequirements*.txt/pyproject.toml, RustCargo.toml, Gogo.mod, RubyGemfile), and scores every declared dependency through thepackage_riskengine — offline by default,--onlineadds the registry-API provenance signals. It also folds in slopsquat detection — AI-hallucinated package names: a conservative three-layer heuristic (the name is unknown to the threat DB, and shaped like an LLM hallucination, and sits near a real popular name) flags dependencies an attacker may have registered to catch a hallucinated import. Findings flow through the normalVerdict/Findingmodel, are explainable, respect the policy allowlist, and support--format json; slopsquat is integrated here, not shipped as a separate crate. Manifest parsers are total — a malformed manifest yields no findings, never a panic.tirith install— safe-install transaction —tirith install <npm|pip|cargo|url> <args…>wraps a real package install with pre-execution supply-chain risk analysis: it scores the package(s) with thepackage_riskengine and the install command with the install-command rules before anything runs, presents one explainableVerdict(a block refuses with bypass-per-policy, a warn requires acknowledgement, an allow proceeds), records the transaction (a working-directory checkpoint plus an audit-log entry), then runs the realnpm/pip/cargo install— or, for theurlform, the downloaded script via the existing safe-download path.--no-execanalyzes and records without running the install;--onlineadds registry-API provenance,--offline/TIRITH_OFFLINEforce offline;--format jsoncarries an explicitsandboxed: false. This is pre-execution install-risk analysis plus a recorded transaction — it does not sandbox or isolate the install (runtime sandboxing is an explicit tirith non-goal); the real install runs with the user's full privileges.CI / repo supply-chain scanning —
tirith scancovers your CI and infrastructure files —tirith scannow inspects the files a repository checks in to describe its own build and deploy pipeline, not just AI-config files. It detects the dangerous pattern, not the tool, so a hardened workflow, a digest-pinned image, a local Terraform module, and a normalpackage.jsonall stay clean — every detection ships with benign fixtures proving a legitimate file does not fire. Six new file-content scan rules, run on thetirith scanfile-walk path (no network, no registry calls): (1)workflow_unpinned_action(Medium) — a GitHub Actionsuses:reference in.github/workflows/*.ymlpinned to a mutable ref (a branch like@mainor a tag like@v3) instead of an immutable 40-character commit SHA; a SHA-pinned action and a local./action do not fire, and a workflow with many unpinned steps yields one folded finding. (2)workflow_dangerous_trigger(High) — thepull_request_targettrigger, which runs with repository secrets and a read/write token in the context of an untrusted fork's PR; the safepull_requesttrigger does not fire. (3)workflow_curl_pipe_shell(High) — acurl … | bash/wget … | shpipe-to-shell inside a workflowrun:step (single-line or|block scalar); acurl -o filedownload or a benignecho | grepdoes not fire. (4)workflow_untrusted_input(High) — an attacker-controllable${{ github.event.* }}expression (PR title, issue body, branch name, comment, …) interpolated directly into arun:shell step — the classic Actions script-injection sink; a trusted context value (github.sha) and the same expression in a non-run:with:block do not fire. (5)dockerfile_unpinned_image(Medium) — a DockerfileFROMon the mutablelatesttag or with no tag at all and no@sha256:digest pin; a digest-pinned image, a specific version tag, an earlier-build-stage reference, and a build-arg-templated image do not fire. (6)package_script_dangerous(High) — an npmpackage.jsonpreinstall/install/postinstalllifecycle hook (the hooks that run automatically onnpm install) whose command is a pipe-to-shell, a base64-decode-and-execute payload, an inline interpreter one-liner that shells out or opens a network connection, or a download-and-run sequence; a benignnode-gyp rebuild/tscbuild hook, and thepreparehook (commonly a benignhusky install), do not fire. Terraformmoduleblocks with a remote/untrustedsourcein a*.tffile and Helm chart dependencies from an untrusted repository inChart.yamlare also detected — these reuse the existingterraform_remote_module/helm_untrusted_reporule IDs, since a remote module or untrusted chart repo is the same risk class whether named on a command line or in a checked-in file. Each new rule has fulltirith explaindocumentation.tirith scanalso gains three built-in--profilevalues that tune which checks and severities apply:ci-hardening(every supply-chain check at full strength,fail-on high, for hardening a CI/CD pipeline),ai-agent-repo(keeps the injection / dangerous-trigger / dangerous-script findings, suppresses low-value pinning-hygiene noise, for a repo an AI agent operates in), andoss-maintainer(emphasises contributor-controllable attack surface — script injection, dangerous triggers, dangerous lifecycle scripts — and downgrades pinning-hygiene findings, for reviewing a contributed change). A built-in profile sets a defaultfail_onand a small explicit per-rule overlay that can suppress or re-grade a finding but never invent one; a policyscan.profiles.<name>entry of the same name overrides the built-in.tirith scan --format jsonexposes the new findings unchanged.AI-relevant file hidden-content scanning —
tirith scancatches content smuggled past a human reviewer —tirith scannow inspects file types an AI coding agent (or a renderer) reads and acts on, looking specifically for hidden / smuggled content — content a human reviewing the file would not see, but an agent or a renderer still processes. It extends the existing FileScan architecture (the same module pattern as the CI/repocifilerules) with a newaifilerule module; false positives are the explicit design priority, so a normal notebook, an ordinaryCLAUDE.mdwith visible instructions, and a plain SVG image all stay clean — every detection ships with benign fixtures proving a legitimate file does not fire. No network, no registry calls. Five new file-content scan rules across three file kinds: (1)notebook_hidden_content— a Jupyter notebook (*.ipynb) cell with content hidden from the rendered view: invisible / bidirectional / zero-width characters in the cell source (High), a long base64-encoded blob embedded in the source (Medium), or a cell hidden viametadata.jupyter.source_hiddenor ahide_inputtag (Medium). (2)notebook_suspicious_output— a stored cell output carrying smuggled content: invisible characters in a saved stream /text/plainoutput (High), or an embedded<script>, an inline event handler, ajavascript:URI, or CSS-hidden content in a savedtext/htmloutput (Medium). A normal computed output — a number, a pandas DataFrame HTML<table>, a plain-text result — does not fire. (3)agent_instruction_hidden(High) — an AI agent-instruction file (CLAUDE.md,AGENTS.md,.cursorrules, and similar) with a hidden directive: an instruction inside an HTML comment (Markdown renders an HTML comment to nothing) or a visually-hidden HTML element (hidden,aria-hidden,display:none). These files legitimately contain visible instructions, so ordinary visible instructions — and benign developer comments (<!-- TODO -->,prettier-ignore) — never fire; only hidden directives with an instruction-shaped phrase do. (4)svg_script_embedded(High) — an SVG image (*.svg) carrying executable content: an embedded<script>element, an inlineon*event-handler attribute, or ajavascript:URI; a static SVG image (paths, shapes, gradients, text) does not fire. (5)svg_external_reference(Medium) — an SVG that references content outside itself: a remotexlink:href/href, or an XXE external-entity declaration (<!ENTITY … SYSTEM "…">); an internalxlink:href="#id"fragment reference does not fire..svgis no longer treated as a binary/skip extension by the directory walk — an SVG is XML text and is now scanned. Invisible-character detection reuses tirith's existing bidi / zero-width / Unicode-tag classification rather than reimplementing it. Each new rule has fulltirith explaindocumentation and golden fixtures covering both the attack shape and the benign baseline; the fourgolden_fixtures.rstier-1 safeguard tests pass.tirith scan --format jsonexposes the new findings unchanged. DOCX / PPTX / ODT are deliberately out of scopConfiguration
📅 Schedule: (UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR has been generated by Mend Renovate.