Skip to content

feat(miner): enforce AMS network-egress deny-by-default + allowlist#8284

Merged
JSONbored merged 4 commits into
mainfrom
feat/ams-egress-firewall
Jul 24, 2026
Merged

feat(miner): enforce AMS network-egress deny-by-default + allowlist#8284
JSONbored merged 4 commits into
mainfrom
feat/ams-egress-firewall

Conversation

@JSONbored

@JSONbored JSONbored commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Closes #7857 (#7648's ratified decision) for the miner's Docker/Compose deployment path -- the primary, documented self-hosting option. Kubernetes and systemd equivalents are split into #8282 (see Scope below), matching the same product-agnostic split pattern used for #8202/#8246 earlier.

Summary

No isolation mechanism existed today. AMS runs the coding-agent CLI subprocess (claude/codex) as a bare child_process.spawn() sibling inside the same container as the long-running miner, no network namespace boundary. I researched this deeply before implementing (see #7857's own comment history) and confirmed: no per-process UID separation is available without a bigger restructure, so enforcement has to apply to the whole container.

Mechanism

dnsmasq + iptables/ipset -- the standard, well-established Linux pattern for domain-based (not just IP-based) egress allowlisting, not something novel:

  • dnsmasq is the container's only DNS resolver (/etc/resolv.conf rewritten to point at it -- confirmed empirically that Docker's own generated file allows this). One ipset=/host/setname directive per allowed hostname: dnsmasq automatically adds the resolved IP to that ipset in-band with the real lookup, no separate polling needed to track IP rotation on CDN-backed hosts.
  • iptables: OUTPUT policy DROP by default; loopback allowed; outbound DNS allowed only to the exact upstream resolvers dnsmasq itself forwards to (forces every lookup through the controlled resolver); egress allowed to anything already in the ipset, on standard ports.
  • packages/loopover-miner/lib/egress-allowlist.ts resolves Decide default network-egress policy for AMS sandboxed execution (deny/allowlist, not open) #7648's three ratified categories (OS package registries, GitHub as the target-repo-git-remote proxy -- this product is GitHub-only, confirmed no other forge path exists -- and the operator's own declared networkAllowlist.ecosystems/extraHosts, already-shipped config surface from earlier work) plus the miner's own necessary platform hosts (Orb broker, Sentry, discovery-index, Neon), each gated behind whether that specific feature is actually configured on this instance -- not a blanket allowance.

Privilege model change

The image now starts as root (was USER node) so the entrypoint can configure NET_ADMIN-requiring iptables/ipset rules, then exec gosu node loopover-miner ... drops privileges and replaces the process -- the actual miner command runs as node for its entire life, byte-identical to before this change from that point on. docker-compose.miner.yml grants NET_ADMIN/NET_RAW (same capability class as this repo's existing tailscale service).

Fails closed, deliberately (set -eu in the entrypoint): if firewall setup itself fails, the container aborts rather than silently running untrusted code with no restriction. LOOPOVER_MINER_DISABLE_EGRESS_FIREWALL is the documented escape hatch (decided in TypeScript via a no-op ruleset, not a shell-level skip, so it's testable and shows up correctly in the deployment-docs audit).

Security review (Superagent), both findings verified empirically and fixed:

  • The entrypoint script and the root-invoked config-generation script were node-owned despite executing with root privilege -- a compromised node-level process (the exact thing this mechanism sandboxes) could rewrite either to persist a root backdoor across a container restart. Now root:root. Scoped to these two files, not their full transitive dependency graph (documented as a deliberate, bounded scope in the Dockerfile's own comment).
  • dnsmasq's privilege drop is now explicit (--user=nobody --group=nogroup) rather than relying on its own default. The scanner's specific claim ("running as root") was empirically false -- confirmed via /proc/<pid>/status that dnsmasq already drops to nobody (uid 65534) unprompted on this base image -- and its specific suggested fix (--user=node) would have been a regression (wider privilege than dnsmasq needs). Fixed the real gap (implicit vs. explicit) without following the incorrect specific recommendation.

Verified empirically, not just unit tests

packages/loopover-miner/scripts/verify-egress-firewall.sh builds the real Docker image and asserts on real network calls from inside a real running container with real NET_ADMIN. All 7 checks pass (re-verified after the security fixes too):

  • Default allowlist: github.com succeeds, an arbitrary host is blocked, npm registry is blocked without ecosystems:[npm] declared.
  • Operator config: npm registry succeeds once declared; an extraHosts addition succeeds; an undeclared host still blocked even with other config present.
  • LOOPOVER_MINER_DISABLE_EGRESS_FIREWALL correctly disables enforcement.

Scope

Docker/Compose only. Kubernetes's runAsNonRoot: true is a hard, kubelet-enforced admission constraint -- a root-then-drop entrypoint is structurally impossible there; needs a k8s-native initContainer instead. systemd needs its own ExecStartPre=+ mechanism. Both split into #8282 (blocked-by this PR) rather than bundled here -- each needs a genuinely different privilege-setup mechanism, not a copy-paste of the Docker entrypoint. k8s/miner-deployment.yaml and systemd/loopover-miner.service.example both carry a comment pointing at #8282.

Tests

  • test/unit/miner-egress-allowlist.test.ts: default categories, every ecosystem resolves to real hosts, extraHosts, dedup (case-insensitive), and each platform host's gating condition (broker mode on/off, custom ORB_BROKER_URL, discovery-index/Sentry opt-in, Neon's all-or-nothing gate) -- 16 tests.
  • test/unit/miner-egress-firewall-config.test.ts: dnsmasq/iptables text generation, custom upstream resolvers, invalid-hostname rejection (defense in depth, even though inputs are already validated upstream), the no-op disabled ruleset -- 14 tests.
  • test/unit/miner-generate-egress-firewall-config.test.ts: the real .loopover-ams.yml -> resolved config -> written files pipeline against real temp-dir filesystem I/O, including the disable-flag path, file permissions, and main()'s own CLI-entry logic (injectable IO, matching scripts/check-miner-deployment-docs.ts's own established pattern in this codebase) across missing-args/success/failure/real-default-IO paths -- 11 tests.

Note on coverage: egress-allowlist.ts and egress-firewall-config.ts show 100% line+branch coverage in the real Codecov report despite an artifact in my own local full-suite coverage run (a v8 coverage-merging quirk for files imported from multiple test files at very large scale -- confirmed via isolated local runs matching Codecov's real number, not my own local full-suite number). generate-egress-firewall-config.ts's main() was a genuine gap on the first push (its CLI-entry logic wasn't exercised at all) -- fixed with real, dedicated tests via the injectable-IO refactor described above.

Verified: full unsharded npm run test:coverage (21177/21198 passing, only pre-existing skips, 0 failures -- run three times across this PR's revisions), tsc --noEmit --incremental false clean, npm run build:miner clean, npm run test:miner-deployment-docs-audit clean, npm run miner:env-reference:check clean, docs:drift-check/manifest:drift-check/branding-drift:check clean, git diff --check clean, real docker build + the 7-check verify script all passing (re-verified after the security fixes).

Test plan

  • npm run test:coverage (full unsharded, 21177/21198 passing, 0 failures)
  • tsc --noEmit --incremental false
  • npm run build:miner
  • npm run test:miner-deployment-docs-audit
  • npm run miner:env-reference:check
  • docker build -f packages/loopover-miner/Dockerfile . (real build)
  • packages/loopover-miner/scripts/verify-egress-firewall.sh (7/7 real network-behavior checks, re-verified after security fixes)
  • git diff --check

…7857)

Implements #7648's ratified default for the miner's Docker/Compose deployment
path: dnsmasq + iptables/ipset (a well-established domain-allowlist pattern,
not novel), deny-by-default OUTPUT with a curated allowlist punched through
via dnsmasq's own ipset= directive on each DNS resolution. The whole
container's egress is restricted (no per-attempt process isolation exists
today), so the miner's own necessary calls (broker, Sentry, discovery-index,
Neon) are allowed automatically whenever their corresponding feature is
actually configured -- not a blanket allowance.

The image now starts as root so the entrypoint can configure the firewall,
then drops to the unprivileged node user via gosu before running anything
else -- verified as byte-identical to prior behavior from that point on.
Fails closed: a firewall-setup failure aborts the container rather than
running untrusted code with no restriction.

Verified empirically against a real built image, not just unit tests:
scripts/verify-egress-firewall.sh builds the real image and asserts on real
network calls -- default allowlist blocks/allows correctly, operator-declared
ecosystems/extraHosts correctly extend it live, LOOPOVER_MINER_DISABLE_EGRESS_FIREWALL
correctly disables it. All 7 checks pass.

Scoped to Docker/Compose -- the primary documented self-hosting path.
Kubernetes (blocked by a hard runAsNonRoot admission constraint) and systemd
need their own privilege-setup mechanism, split into #8282.

@superagent-security superagent-security Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superagent found 2 security concern(s).

Comment thread packages/loopover-miner/scripts/egress-firewall-entrypoint.sh
Comment thread packages/loopover-miner/Dockerfile
@superagent-security superagent-security Bot added the pr:flagged PR flagged for review by security analysis. label Jul 23, 2026
@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.65%. Comparing base (112ead6) to head (392636e).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8284      +/-   ##
==========================================
- Coverage   92.16%   91.65%   -0.52%     
==========================================
  Files         788      791       +3     
  Lines       79175    79245      +70     
  Branches    23933    23945      +12     
==========================================
- Hits        72974    72633     -341     
- Misses       5062     5524     +462     
+ Partials     1139     1088      -51     
Flag Coverage Δ
shard-1 57.85% <42.85%> (-0.73%) ⬇️
shard-2 49.61% <87.14%> (-0.37%) ⬇️
shard-3 53.35% <0.00%> (-0.71%) ⬇️

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

Files with missing lines Coverage Δ
packages/loopover-miner/lib/egress-allowlist.ts 100.00% <100.00%> (ø)
...kages/loopover-miner/lib/egress-firewall-config.ts 100.00% <100.00%> (ø)
...pover-miner/lib/generate-egress-firewall-config.ts 100.00% <100.00%> (ø)

... and 2 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 23, 2026
@loopover-orb

loopover-orb Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Warning

⏸️ LoopOver review result - manual review recommended

Review updated: 2026-07-24 01:14:22 UTC

16 files · no blockers · CI green · clean

⏸️ Suggested Action - Manual Review

  • AI review could not be completed: The dual-model AI review did not return a usable verdict for this change.

Review summary
AI review could not be completed for this PR head. LoopOver is holding this PR for manual review instead of relying on deterministic signals alone.

Nits — 2 non-blocking
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.
  • AI review could not be completed — The gate is held for a human reviewer rather than passed automatically; it re-evaluates on the next update.

Decision drivers

  • ✅ Code review — No blockers (No AI review summary)
  • ⚠️ Gate result — Not blocking (Advisory; not blocking this PR.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #7857, #7648
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (2 linked issues).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 14 registered-repo PR(s), 14 merged, 228 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 14 PR(s), 228 issue(s).
Improvement ✅ Minor risk: clean · value: minor
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: 14 PR(s), 228 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
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.

🟩 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 23, 2026
…erage, security review

- Regenerate packages/loopover-miner/docs/env-reference.md and
  apps/loopover-ui/src/lib/ams-env-reference.ts (a separate generator from
  the deployment-docs audit already checked locally; missed it the first
  pass -- both now include LOOPOVER_MINER_DISABLE_EGRESS_FIREWALL and the
  other new vars).

- generate-egress-firewall-config.ts's main() was 100% uncovered by patch
  (real Codecov gap, not the local coverage-merge artifact the other two new
  files showed): refactored to accept injectable IO, matching
  scripts/check-miner-deployment-docs.ts's own main(env, io) pattern already
  in this codebase, so it's exercised in-process rather than needing an
  untestable subprocess. New tests cover missing-args, single-arg, success,
  the underlying-failure catch path, and the real default-IO construction.

- Fixed both Superagent-flagged findings, verified empirically against a
  real built image before and after:
  - The entrypoint script and the root-invoked config-generation script were
    node-owned despite executing with root privilege -- a compromised
    node-level process (the exact thing this whole mechanism sandboxes)
    could rewrite either to persist a root backdoor across a restart. Now
    root:root. Scoped to these two files, not their full transitive
    dependency graph -- documented as a deliberate, bounded scope in the
    Dockerfile's own comment.
  - dnsmasq's privilege drop is now explicit (--user=nobody --group=nogroup)
    rather than relying on its own default. The scanner's specific claim
    ("running as root") was empirically false -- confirmed via /proc/<pid>/status
    that dnsmasq already drops to nobody (uid 65534) unprompted -- and its
    specific suggested fix (--user=node) would have been a regression (wider
    privilege than dnsmasq needs, not narrower). Fixed the real gap
    (implicit vs. explicit) without following the incorrect specific
    recommendation.

Re-verified full unsharded npm run test:coverage (21177/21198 passing, 0
failures) and the real 7-check verify-egress-firewall.sh against a freshly
rebuilt image after both security fixes -- all still passing.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 24, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui 818f7ee Commit Preview URL

Branch Preview URL
Jul 24 2026, 12:20 AM

@superagent-security superagent-security Bot removed the pr:flagged PR flagged for review by security analysis. label Jul 24, 2026
@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 332 bytes (0.0%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
loopover-ui 7.46MB 332 bytes (0.0%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: loopover-ui

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/add-scalar-classes-B5LcSaJF.js (New) 2.17MB 2.17MB 100.0% 🚀
assets/tanstack-vendor-jcwr_Eu5.js (New) 740.2kB 740.2kB 100.0% 🚀
assets/docs.fumadocs-spike-api-reference-Bwx1YFHD.js (New) 442.92kB 442.92kB 100.0% 🚀
assets/AgentScalarChatInterface.vue-kUhMijI2.js (New) 201.71kB 201.71kB 100.0% 🚀
assets/modal-CoKFl9iZ.js (New) 184.38kB 184.38kB 100.0% 🚀
assets/client-Nt5USPvI.js (New) 146.06kB 146.06kB 100.0% 🚀
assets/maintainer-panel-n-zmDreX.js (New) 78.99kB 78.99kB 100.0% 🚀
assets/ui-vendor-CeSCoMp_.js (New) 57.04kB 57.04kB 100.0% 🚀
assets/routes-BdOAoJPJ.js (New) 34.98kB 34.98kB 100.0% 🚀
assets/owner-panel-D0eBnbVr.js (New) 27.36kB 27.36kB 100.0% 🚀
assets/app-HgT4LP1u.js (New) 25.7kB 25.7kB 100.0% 🚀
assets/app.runs-BGngrrfl.js (New) 23.56kB 23.56kB 100.0% 🚀
assets/miner-panel-lFhUF2OL.js (New) 20.28kB 20.28kB 100.0% 🚀
assets/api._op-B7_vnxPf.js (New) 17.57kB 17.57kB 100.0% 🚀
assets/self-hosting-docs-audit-DYVDfC-x.js (New) 16.61kB 16.61kB 100.0% 🚀
assets/app.index-Djhp-WHv.js (New) 15.62kB 15.62kB 100.0% 🚀
assets/docs._slug-DY5lN1fp.js (New) 15.37kB 15.37kB 100.0% 🚀
assets/playground-panel-DJHJIfrY.js (New) 14.49kB 14.49kB 100.0% 🚀
assets/fairness-DcBFnUyR.js (New) 10.6kB 10.6kB 100.0% 🚀
assets/app.audit-Y3VBqxmK.js (New) 10.11kB 10.11kB 100.0% 🚀
assets/app.config-generator-CM8Z_VZ8.js (New) 10.09kB 10.09kB 100.0% 🚀
assets/extension-D3Qbsmwo.js (New) 10.04kB 10.04kB 100.0% 🚀
assets/maintainers-HASUjQXL.js (New) 8.09kB 8.09kB 100.0% 🚀
assets/miners-HYZKspIs.js (New) 7.91kB 7.91kB 100.0% 🚀
assets/agents-DXkjE42U.js (New) 7.8kB 7.8kB 100.0% 🚀
assets/ams-env-reference-14mw01K_.js (New) 6.84kB 6.84kB 100.0% 🚀
assets/commands-panel-Bw4VIyhT.js (New) 6.65kB 6.65kB 100.0% 🚀
assets/maintainer-workflow-CBdYxPEk.js (New) 6.52kB 6.52kB 100.0% 🚀
assets/roadmap-B3TnHj-3.js (New) 6.33kB 6.33kB 100.0% 🚀
assets/digest-panel-BsK3TC6h.js (New) 6.18kB 6.18kB 100.0% 🚀
assets/repos._owner._repo.quality-LyH5UHwi.js (New) 6.14kB 6.14kB 100.0% 🚀
assets/docs.index-BsUgSOCt.js (New) 5.79kB 5.79kB 100.0% 🚀
assets/docs-nav-Dk1ZDBy3.js (New) 5.59kB 5.59kB 100.0% 🚀
assets/api.index-CIrj1yu2.js (New) 4.7kB 4.7kB 100.0% 🚀
assets/docs-BzHzXMAy.js (New) 2.7kB 2.7kB 100.0% 🚀
assets/api-DTN05HqN.js (New) 2.69kB 2.69kB 100.0% 🚀
assets/docs-page-D9MTw75L.js (New) 2.1kB 2.1kB 100.0% 🚀
assets/table-DAK8RS8h.js (New) 1.75kB 1.75kB 100.0% 🚀
assets/app.workbench-0hejn9zC.js (New) 1.58kB 1.58kB 100.0% 🚀
assets/session-C-TfDVp7.js (New) 1.45kB 1.45kB 100.0% 🚀
assets/tooltip-jdRvEagY.js (New) 1.45kB 1.45kB 100.0% 🚀
assets/tabs-BKG1JYEJ.js (New) 1.39kB 1.39kB 100.0% 🚀
assets/app.repos-BZaNxJM6.js (New) 1.07kB 1.07kB 100.0% 🚀
assets/input-Clj9Grx-.js (New) 796 bytes 796 bytes 100.0% 🚀
assets/file-cog-DzWm4sRb.js (New) 758 bytes 758 bytes 100.0% 🚀
assets/app.maintainer-WMHZCeth.js (New) 502 bytes 502 bytes 100.0% 🚀
assets/sparkles-b9U0uBRq.js (New) 494 bytes 494 bytes 100.0% 🚀
assets/app.owner-KYxEvJQi.js (New) 474 bytes 474 bytes 100.0% 🚀
assets/app.commands-B8SyLxA8.js (New) 455 bytes 455 bytes 100.0% 🚀
assets/app.playground-DqSZUEMR.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/reveal-r-QAjqf5.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/index-D0G_eIIZ.js (New) 438 bytes 438 bytes 100.0% 🚀
assets/app.digest-BKRlt5ul.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/eye-off-Brb4oS51.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/app.miner-C2_THpY-.js (New) 422 bytes 422 bytes 100.0% 🚀
assets/key-round-CKktKjRj.js (New) 355 bytes 355 bytes 100.0% 🚀
assets/bot-BES9D6fQ.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/trash-2-CvlYOlfl.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/save-ccFtDOrS.js (New) 327 bytes 327 bytes 100.0% 🚀
assets/git-pull-request-arrow-Cp1O9T0P.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/wrench-6MJz4YyN.js (New) 303 bytes 303 bytes 100.0% 🚀
assets/list-checks-BN1u5wFP.js (New) 279 bytes 279 bytes 100.0% 🚀
assets/shield-C0ceCZEp.js (New) 272 bytes 272 bytes 100.0% 🚀
assets/workflow-B5Wd8VT2.js (New) 265 bytes 265 bytes 100.0% 🚀
assets/compass-yyeKMF6n.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/history-C9ActG04.js (New) 237 bytes 237 bytes 100.0% 🚀
assets/activity--kaQeskR.js (New) 234 bytes 234 bytes 100.0% 🚀
assets/message-square-D86xm749.js (New) 233 bytes 233 bytes 100.0% 🚀
assets/lock-DCZ4J03z.js (New) 206 bytes 206 bytes 100.0% 🚀
assets/rotate-cw-B7Go7FDE.js (New) 201 bytes 201 bytes 100.0% 🚀
assets/play-CVyqlGMd.js (New) 190 bytes 190 bytes 100.0% 🚀
assets/circle-check-8CP7XbTA.js (New) 178 bytes 178 bytes 100.0% 🚀
assets/search-D0hqmqdK.js (New) 174 bytes 174 bytes 100.0% 🚀
assets/circle-Syy7zCyn.js (New) 130 bytes 130 bytes 100.0% 🚀
assets/add-scalar-classes-9Rf9UJuE.js (Deleted) -2.17MB 0 bytes -100.0% 🗑️
assets/tanstack-vendor-Bpmtd2s5.js (Deleted) -740.2kB 0 bytes -100.0% 🗑️
assets/docs.fumadocs-spike-api-reference-CqAsrdps.js (Deleted) -442.92kB 0 bytes -100.0% 🗑️
assets/AgentScalarChatInterface.vue-TAWVoNiA.js (Deleted) -201.71kB 0 bytes -100.0% 🗑️
assets/modal-Lzcdgjci.js (Deleted) -184.38kB 0 bytes -100.0% 🗑️
assets/client-Bm6xZHJY.js (Deleted) -146.06kB 0 bytes -100.0% 🗑️
assets/maintainer-panel-B3iz1x5d.js (Deleted) -78.99kB 0 bytes -100.0% 🗑️
assets/ui-vendor-CEQosJu3.js (Deleted) -57.04kB 0 bytes -100.0% 🗑️
assets/routes-yKgTi7U1.js (Deleted) -34.98kB 0 bytes -100.0% 🗑️
assets/owner-panel-DBSEdqT6.js (Deleted) -27.36kB 0 bytes -100.0% 🗑️
assets/app-CBuAQXIf.js (Deleted) -25.7kB 0 bytes -100.0% 🗑️
assets/app.runs-DEpRfgZP.js (Deleted) -23.56kB 0 bytes -100.0% 🗑️
assets/miner-panel-CTy6LpZG.js (Deleted) -20.28kB 0 bytes -100.0% 🗑️
assets/api._op-Bm2g4QqI.js (Deleted) -17.57kB 0 bytes -100.0% 🗑️
assets/self-hosting-docs-audit-RgRb1Z-i.js (Deleted) -16.61kB 0 bytes -100.0% 🗑️
assets/app.index-Cs6Zp1W-.js (Deleted) -15.62kB 0 bytes -100.0% 🗑️
assets/docs._slug-Q77pffub.js (Deleted) -15.37kB 0 bytes -100.0% 🗑️
assets/playground-panel-CWGgBIuv.js (Deleted) -14.49kB 0 bytes -100.0% 🗑️
assets/fairness-Qq_-zO5I.js (Deleted) -10.6kB 0 bytes -100.0% 🗑️
assets/app.audit-Czm0YzJQ.js (Deleted) -10.11kB 0 bytes -100.0% 🗑️
assets/app.config-generator-D4OgbjOV.js (Deleted) -10.09kB 0 bytes -100.0% 🗑️
assets/extension-Bep_KZcd.js (Deleted) -10.04kB 0 bytes -100.0% 🗑️
assets/maintainers-QYb_gc8Y.js (Deleted) -8.09kB 0 bytes -100.0% 🗑️
assets/miners-Pg-jiU3_.js (Deleted) -7.91kB 0 bytes -100.0% 🗑️
assets/agents-BxI_tGkm.js (Deleted) -7.8kB 0 bytes -100.0% 🗑️
assets/commands-panel-DCUGKpeQ.js (Deleted) -6.65kB 0 bytes -100.0% 🗑️
assets/maintainer-workflow-DQ2_j-Bj.js (Deleted) -6.52kB 0 bytes -100.0% 🗑️
assets/ams-env-reference-BzlXtCIu.js (Deleted) -6.51kB 0 bytes -100.0% 🗑️
assets/roadmap-y2JRky4R.js (Deleted) -6.33kB 0 bytes -100.0% 🗑️
assets/digest-panel-DFzDuBF7.js (Deleted) -6.18kB 0 bytes -100.0% 🗑️
assets/repos._owner._repo.quality-CHPQCKNv.js (Deleted) -6.14kB 0 bytes -100.0% 🗑️
assets/docs.index-DtmqH_Vu.js (Deleted) -5.79kB 0 bytes -100.0% 🗑️
assets/docs-nav-lTO3OXzv.js (Deleted) -5.59kB 0 bytes -100.0% 🗑️
assets/api.index-zi1oyflp.js (Deleted) -4.7kB 0 bytes -100.0% 🗑️
assets/docs--YbkyRpT.js (Deleted) -2.7kB 0 bytes -100.0% 🗑️
assets/api-YrKEI2Yi.js (Deleted) -2.69kB 0 bytes -100.0% 🗑️
assets/docs-page-Bob8r8ep.js (Deleted) -2.1kB 0 bytes -100.0% 🗑️
assets/table-BLqoht3a.js (Deleted) -1.75kB 0 bytes -100.0% 🗑️
assets/app.workbench-BnYREYEp.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
assets/session-D8Rsz-75.js (Deleted) -1.45kB 0 bytes -100.0% 🗑️
assets/tooltip-CNI9NoHj.js (Deleted) -1.45kB 0 bytes -100.0% 🗑️
assets/tabs-C0m2Euyj.js (Deleted) -1.39kB 0 bytes -100.0% 🗑️
assets/app.repos-CKFXeKFg.js (Deleted) -1.07kB 0 bytes -100.0% 🗑️
assets/input-Dai4LusP.js (Deleted) -796 bytes 0 bytes -100.0% 🗑️
assets/file-cog-CwlboXLS.js (Deleted) -758 bytes 0 bytes -100.0% 🗑️
assets/app.maintainer-NYGzcFG2.js (Deleted) -502 bytes 0 bytes -100.0% 🗑️
assets/sparkles-BvVhjSVD.js (Deleted) -494 bytes 0 bytes -100.0% 🗑️
assets/app.owner-FEQS_d6Q.js (Deleted) -474 bytes 0 bytes -100.0% 🗑️
assets/app.commands-DFJpcvEZ.js (Deleted) -455 bytes 0 bytes -100.0% 🗑️
assets/app.playground-B9qvWATT.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/reveal-9SNF5kEG.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/index-lj9SxBpB.js (Deleted) -438 bytes 0 bytes -100.0% 🗑️
assets/app.digest-DUcqkadl.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/eye-off-D-jWo8P5.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/app.miner-BXOC4gb_.js (Deleted) -422 bytes 0 bytes -100.0% 🗑️
assets/key-round-C6mytE7a.js (Deleted) -355 bytes 0 bytes -100.0% 🗑️
assets/bot-DY1Q3tvV.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/trash-2-CgYoSlKk.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/save-BKd0knmX.js (Deleted) -327 bytes 0 bytes -100.0% 🗑️
assets/git-pull-request-arrow-8LVib-Lx.js (Deleted) -321 bytes 0 bytes -100.0% 🗑️
assets/wrench-nX-Zji26.js (Deleted) -303 bytes 0 bytes -100.0% 🗑️
assets/list-checks-BGUUlqos.js (Deleted) -279 bytes 0 bytes -100.0% 🗑️
assets/shield-BA8YQPO_.js (Deleted) -272 bytes 0 bytes -100.0% 🗑️
assets/workflow-Da6QNPRw.js (Deleted) -265 bytes 0 bytes -100.0% 🗑️
assets/compass-CUl_CyBm.js (Deleted) -251 bytes 0 bytes -100.0% 🗑️
assets/history-BOI9bXdU.js (Deleted) -237 bytes 0 bytes -100.0% 🗑️
assets/activity-BdCZrobk.js (Deleted) -234 bytes 0 bytes -100.0% 🗑️
assets/message-square-IoM8Zwpc.js (Deleted) -233 bytes 0 bytes -100.0% 🗑️
assets/lock-BODu4ILm.js (Deleted) -206 bytes 0 bytes -100.0% 🗑️
assets/rotate-cw-B9P_4bSa.js (Deleted) -201 bytes 0 bytes -100.0% 🗑️
assets/play-28RADQB9.js (Deleted) -190 bytes 0 bytes -100.0% 🗑️
assets/circle-check-CekrXtec.js (Deleted) -178 bytes 0 bytes -100.0% 🗑️
assets/search-CRVjuR_j.js (Deleted) -174 bytes 0 bytes -100.0% 🗑️
assets/circle-D1qeMsY5.js (Deleted) -130 bytes 0 bytes -100.0% 🗑️

…irewall-config.ts

Both are the same class of gap bin/loopover-miner.ts's own dispatcher tail
already carries a v8-ignore for (see the packages/loopover-miner/bin note in
vitest.config.ts's coverage.include): code only reachable by actually running
the file as a script, not by importing it. main()'s own body is fully
unit-covered in-process via the injectable-IO tests added earlier; only the
self-invocation guard's true branch and one defensive non-Error fallback
(unreachable given this call site's real, always-Error-throwing dependencies)
remain, both marked consistent with the established convention.

Confirmed locally: 100% line+branch coverage in isolation, full test suite
green, real docker build + verify-egress-firewall.sh (7/7) still passing.
Comment thread packages/loopover-miner/Dockerfile Outdated
@superagent-security superagent-security Bot added the pr:flagged PR flagged for review by security analysis. label Jul 24, 2026
…entrypoints

Owning only the entrypoint script and generate-egress-firewall-config.{js,ts}
left every module they import (ams-policy.js, egress-*.js, @loopover/engine,
and all of node_modules) node:node-owned. A compromised node-level process --
the exact threat this mechanism sandboxes against -- could tamper with any of
those, and root would load and execute the tampered code on the container's
next restart.

COPY --chown=root:root across the whole code tree closes that persistence
vector while staying world-readable (COPY --chown only changes ownership, not
mode bits), and the miner's real runtime writes all resolve under
LOOPOVER_MINER_CONFIG_DIR or a target-repo checkout, never /app -- verified
empirically that node can no longer write ams-policy.js but can still read
and require() it, and that the full verify-egress-firewall.sh suite still
passes end to end.
@superagent-security

Copy link
Copy Markdown
Contributor

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

@superagent-security superagent-security Bot removed the pr:flagged PR flagged for review by security analysis. label Jul 24, 2026
@JSONbored
JSONbored merged commit 9f03459 into main Jul 24, 2026
16 checks passed
@JSONbored
JSONbored deleted the feat/ams-egress-firewall branch July 24, 2026 01:15
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