Skip to content

fix(safety): kill switches become ratchets; stop a dry-run GitHub write; breaker covers content-inspection closes#9115

Merged
JSONbored merged 2 commits into
mainfrom
fix/9049-9067-safety-bypass
Jul 27, 2026
Merged

fix(safety): kill switches become ratchets; stop a dry-run GitHub write; breaker covers content-inspection closes#9115
JSONbored merged 2 commits into
mainfrom
fix/9049-9067-safety-bypass

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

Three ways a safety control was silently bypassed. The first is live right now.

#9049 — the dry-run kill switch is defeated fleet-wide. The global default private-config layer carries agentDryRun: false, and resolveEffectiveSettings merges as {...dbSettings, ...manifestSettings} — so the manifest wins. Setting agent_dry_run=1 in the DB (dashboard, PUT /settings, loopover-mcp maintain, or the loopover_set_agent_dry_run MCP tool) was silently discarded on every read, for every repo. The tool writes the raw row back and never re-resolves effective settings, so you got a success message while ORB kept making real GitHub writes.

Both agentPaused and agentDryRun are now ratchets: true from either layer wins. Config-as-code can still pause or dry-run a repo — that's the point of the feature — it just can't un-pause what an operator set. Every other setting keeps manifest-wins semantics.

#9067 — a real GitHub write escaped the dry-run suppression. The structural invariant is that every installation write routes through makeInstallationOctokit, whose request hook suppresses mutations when mode ≠ live. dispatchVisualCaptureFallback escapes it with a raw timeoutFetch POST to .../dispatches — which starts an Actions run and burns CI minutes in the target repo. runVisualCapture short-circuits only on paused, so under dry_run it fired for real.

The mode is now threaded onto CaptureTarget and the writer itself refuses anything not live, with an absent mode treated as non-live — so the guarantee is structural rather than dependent on each caller remembering a check. (This compounded with #9049: dry-run being defeated fleet-wide masked this rather than preventing it.)

#9086 (breaker half) — six close paths skipped the precision breaker. downgradeCloseToHold keyed on closeKind === "heuristic" alone. The "deterministic ⇒ zero-hallucination" justification holds for identity-based closes (blacklist, contributor_cap read facts about an account) but not for content-inspection ones, whose determinism is a regex heuristic over free-text markdown — and screenshot_table is our top close reason all-time. A deterministic rule can still be systematically wrong; that's what the breaker is for. screenshot_table / review_nag / copycat are now breaker-eligible; identity-based closes keep their exemption.

Closes #9049
Closes #9067
Closes #9086

⚠️ Two deliberate policy reversals

  1. orb(gate): the six anti-abuse close paths are all exempt from the precision breaker AND from live recheck #9086 inverts a test asserting screenshot_table is breaker-exempt. A companion test pins the identity-based exemptions, so the narrowing is visible in both directions rather than looking like a blanket removal.
  2. orb(safety): manifest agentDryRun/agentPaused:false silently overrides the DB kill-switch — VERIFIED LIVE, dry-run is currently dead fleet-wide #9049 means a repo's .loopover.yml can no longer turn off a DB-set pause/dry-run. If any repo currently relies on that to opt out, it will now stay paused — worth a scan before merging, though the live config only sets agentDryRun: false, which is exactly the value this is meant to stop honoring.

Scope note

#9086 also asks to move the five anti-abuse short-circuits below the CI-pending settle, and lists three latent false-positive classes in the screenshot gate. Those are behavior changes to the gate itself rather than to the breaker, so I've left #9086's remaining items for a follow-up rather than bundling ordering changes into a safety PR.

Test plan

  • 5 ratchet cases through the real parseFocusManifest path: manifest false cannot clear either DB flag; manifest true still tightens; both-off stays off (no accidental fleet-wide pause); an absent key leaves the DB value intact in both directions
  • 2 dry-run refusal tests: dry_run and absent-mode both refuse to dispatch
  • 2 breaker tests: screenshot_table now downgrades to a hold; blacklist/contributor_cap still exempt
  • 15 existing capture fixtures updated to pass mode: "live" — they were failing precisely because the new refusal works
  • 100% line and branch coverage on all 55 added lines; 1806/1806 across 8 suites; migrations/schema-drift/docs/engine-parity/manifest gates green

…rite, subject content-inspection closes to the breaker

#9049 -- VERIFIED LIVE. The global default private-config layer carries
`agentDryRun: false`, and resolveEffectiveSettings merges as
`{...dbSettings, ...manifestSettings}`, so the manifest wins. Setting
`agent_dry_run=1` in the DB -- via the dashboard, PUT /settings,
`loopover-mcp maintain`, or the loopover_set_agent_dry_run MCP tool -- was
therefore SILENTLY DISCARDED on every read, for every repo. The tool writes the
raw row back and never re-resolves effective settings, so the operator got a
success message while ORB kept making real GitHub writes. A kill switch that
reports success and does nothing is worse than one that does not exist.

Both agentPaused and agentDryRun are now RATCHETS: `true` from EITHER layer
wins. Config-as-code can still pause or dry-run a repo (tightening -- the whole
point of the feature); it can no longer un-pause or un-dry-run what an operator
set. Every other setting keeps manifest-wins semantics unchanged.

#9067 -- the structural invariant is that every installation write routes
through makeInstallationOctokit, whose request hook suppresses mutations when
mode !== "live". dispatchVisualCaptureFallback escapes it with a raw
timeoutFetch POST to .../dispatches -- a real write that starts an Actions run
and burns CI minutes in the target repo. runVisualCapture short-circuits only
on `paused`, so under `dry_run` it fired for real, contradicting the documented
dry-run contract. The mode is now threaded onto CaptureTarget and the writer
itself refuses anything that is not "live", with an absent mode treated as
non-live -- so the guarantee is structural rather than dependent on each caller
remembering a check. (This compounded with #9049: dry-run was defeated
fleet-wide, which masked the misbehavior rather than preventing it.)

#9086 (breaker half) -- downgradeCloseToHold keyed on `closeKind === "heuristic"`
alone, so all six non-heuristic close paths were returned untouched by both the
project breaker and the per-rule precision check. The "deterministic =>
zero-hallucination" justification holds for the IDENTITY-based closes
(blacklist, contributor_cap read facts about an account) but not for the
CONTENT-INSPECTION ones, whose determinism is a regex heuristic over free-text
markdown -- and screenshot_table is our top close reason all-time. A
deterministic rule can still be systematically wrong, which is what the breaker
exists to catch. screenshot_table / review_nag / copycat are now breaker-eligible;
identity-based closes keep their exemption.

POLICY REVERSAL, called out explicitly: this inverts an existing test asserting
screenshot_table is breaker-exempt. A companion test pins the identity-based
exemptions so the narrowing is visible in both directions.

Closes #9049
Closes #9067
Closes #9086

Tests: 5 ratchet cases through the REAL parseFocusManifest path (manifest false
cannot clear either DB flag; manifest true still tightens; both-off stays off;
absent key leaves the DB value intact in both directions), 2 dry-run refusal
tests (dry_run and absent-mode both refuse to dispatch), and 2 breaker tests
(screenshot_table now downgrades; blacklist/contributor_cap still exempt). 100%
line+branch coverage on all 55 added lines; 1806/1806 across 8 suites; all
drift gates green.
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Logic backtest

Replayed 0 historical case(s) for linked_issue_scope_mismatch through the base (98e103c) and head (8ac6a9f) versions of its detection logic (corpus checksum 4f53cda18c2b).

Backtest comparison: linked_issue_scope_mismatch

Verdict: unchanged — no comparable axis moved.

Advisory only — this check never blocks merge (#8105).

@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.47%. Comparing base (6dc0e3c) to head (8ac6a9f).
⚠️ Report is 3 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9115      +/-   ##
==========================================
+ Coverage   90.56%   93.47%   +2.91%     
==========================================
  Files          96      717     +621     
  Lines       22490    58253   +35763     
  Branches     3884    20624   +16740     
==========================================
+ Hits        20367    54452   +34085     
- Misses       1945     2899     +954     
- Partials      178      902     +724     
Flag Coverage Δ
backend 93.47% <100.00%> (?)
control-plane ?
rees ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/queue/processors.ts 95.61% <ø> (ø)
src/review/visual/actions-fallback.ts 97.20% <100.00%> (ø)
src/review/visual/capture.ts 95.86% <ø> (ø)
src/settings/agent-actions.ts 98.55% <100.00%> (ø)
src/signals/focus-manifest.ts 99.59% <100.00%> (ø)

... and 808 files with indirect coverage changes

@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 26, 2026
@loopover-orb

loopover-orb Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Caution

🛑 LoopOver review result - fixes required

Review updated: 2026-07-26 21:22:27 UTC

8 files · 1 AI reviewer · no blockers · CI failing · unstable

🛑 Suggested Action - Manual Review

Review summary
This PR closes three real safety bypasses: (1) resolveEffectiveSettings now ratchets agentPaused/agentDryRun so a manifest can't un-pause or un-dry-run what an operator set in the DB, fixing the fleet-wide dry-run defeat from a global-default private-config layer winning a plain spread; (2) dispatchVisualCaptureFallback now refuses any dispatch when mode !== "live" (absent mode treated as non-live), closing the raw-timeoutFetch write that bypassed makeInstallationOctokit's mutation-suppression hook and fired for real under dry_run; (3) downgradeCloseToHold now extends the close-precision breaker to content-inspection close kinds (screenshot_table, review_nag, copycat) while correctly preserving the exemption for identity-based closes (blacklist, contributor_cap). Each fix is traced to its real root cause (source-layer merge order, missing writer-side check, overly broad breaker exemption) and each is backed by tests exercising the actual code path (ratchet in both directions, dry_run/absent-mode dispatch refusal, breaker downgrade vs. exemption preserved). I could not review src/settings/agent-actions.ts or src/signals/focus-manifest.ts in full (budget-omitted), but the diff hunks shown are internally consistent with the described behavior and the processors.ts/capture.ts wiring (mode threaded onto CaptureTarget and into the dispatch call) checks out from the full file content provided.

Nits — 6 non-blocking

CI checks failing

  • validate
  • validate-tests

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #9049, #9067, #9086
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ❌ 8/20 High review scope from cached public metadata (3 linked issues).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 13 registered-repo PR(s), 13 merged, 315 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 13 PR(s), 315 issue(s).
Improvement ✅ Minor risk: clean · value: minor · LLM: significant
Linked issue satisfaction

Partially addressed
The PR implements the core fix requested (#9049): agentPaused/agentDryRun are now ratchets in resolveEffectiveSettings, matching the exact acceptance criterion that a manifest false cannot override a DB-set kill switch, with strong test coverage added. However, the issue's fix list also calls for removing the unsafe `agentPaused: false`/`agentDryRun: false` lines from the shipped templates (config

Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: Python, TypeScript, Ruby, Go, MDX, Shell, Solidity, JavaScript
  • Official Gittensor activity: 13 PR(s), 315 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Then work through the remaining 2 steps in the Signals table above.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask &lt;question&gt; answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat &lt;question&gt; answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

Decision record
  • action: hold · clause: ai_review_inconclusive
  • config: 03a7f8b529a9 · pack: oss-anti-slop
  • record: 1a1f9559fda8 (schema v3, head 305c4d2)

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@loopover-orb loopover-orb Bot added the manual-review Gittensor contributor context label Jul 26, 2026
…ontracts

CI caught what my local runs missed: I had only been running test/unit, so the
test/integration suites that exercised the OLD fail-open ingest default were
never exercised locally. Fixed properly rather than by weakening the change:

- test/integration/{orb,ams}-ingest.test.ts: every request-shape test
  (200/400/413, and the dropped-connection 413 regression) now presents a real
  credential, because those assertions are about BODY handling and would
  otherwise stop at the new 401. The two "optional collector token" tests are
  rewritten to assert the actual new contract -- unset ⇒ 401, not 200.
- test/integration/orb-oauth.test.ts: the path-specificity test proves a request
  routes PAST the callback checks into the ingest handler's body handling, so it
  needs a credential to still reach that point.
- test/unit/actions-fallback.test.ts: the two direct dispatcher tests pass
  mode: "live", plus a new test pinning the refusal for dry_run, paused, and an
  absent mode.

Also verified the full local suite (unit + integration, 22,146 passing) after
building every artifact test:ci builds first -- the 327 failures I initially saw
were entirely missing MCP/miner/discovery-index/ui-kit bundles in a fresh
worktree, not this diff.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. manual-review Gittensor contributor context

Projects

None yet

1 participant