Skip to content

fix(master): refuse tmpfs options Swarm cannot express - #65

Closed
echobt wants to merge 605 commits into
mainfrom
fix/swarm-tmpfs-refuse-undroppable-options
Closed

fix(master): refuse tmpfs options Swarm cannot express#65
echobt wants to merge 605 commits into
mainfrom
fix/swarm-tmpfs-refuse-undroppable-options

Conversation

@echobt

Copy link
Copy Markdown
Contributor

Why

The Swarm backend translated a docker-run tmpfs spec by keeping size= and discarding every other option.

The own_runner job container installs the miner agent into /tmp/.local under a read-only rootfs, so its spec is /tmp:rw,nosuid,exec,size=2g. tmpfs is noexec unless told otherwise. Routing that workload through Swarm produced a noexec /tmp, where the agent loads no shared object and dies with failed to map segment from shared object — the same symptom that already cost hours to trace on the broker path (fixed in #64).

The broker path is what production runs today, so this was latent, not live. It becomes live the moment the Swarm path is enabled.

What

Swarm cannot express the flag at all. Verified against Docker 29.2.1 on the prod host:

attemptresult
type=tmpfs,...,execinvalid field 'exec' must be a key=value pair
type=tmpfs,...,tmpfs-options=execunknown option 'tmpfs-options'
type=tmpfs,destination=/tmp,tmpfs-size=1048576parses (fails later only because the node is not a swarm manager)

So translation is impossible and silence is the worst option. This change:

  • translates size= and mode= to tmpfs-size / tmpfs-mode;
  • accepts the flags Swarm already applies by default (rw, noexec, nosuid, nodev);
  • refusesexec, and refuses any other untranslatable option, instead of dropping it.

Tests

New tests/unit/test_swarm_tmpfs_exec.py, written failing first: the two refusal tests failed with DID NOT RAISE before the change while the three translation tests already passed, pinning that existing specs keep working. 5 passed after.

echobt added 30 commits July 7, 2026 14:55
Two bugs kept base-supervisor from staying up after bootstrap:
1. Type=notify readiness: `uv run` launches the supervisor as a child
(uv stays the unit main PID), so the sd_notify READY came from the
child and systemd (NotifyAccess=main) ignored it, leaving the unit
stuck `activating` -> crash-loop at start timeout. Set NotifyAccess=all
so systemd accepts the child's READY/WATCHDOG pings.
2. image-updater false timeout: the forward roll issues
`docker service update --image` WITHOUT --detach, so the CLI blocks
through Swarm's health-gated stop-first rollout (pull + 40s
health-start + 45s monitor), exceeding the 60s subprocess timeout and
raising a false SwarmBackendError that bypassed the convergence/rollback
path. Raise DEFAULT_COMMAND_TIMEOUT_SECONDS to 300s; the updater runs on
its own daemon thread so a long roll never starves the other loops.
The host systemd supervisor runs outside the swarm overlay and cannot
resolve the overlay service DNS in docker.broker_url
(http://base-docker-broker:8082), so the broker-health probe failed
forever and permanently tripped the BrokerHealthGate. That false trip
gates self-update's pre-swap check (and weight submission) OFF, silently
disabling supervisor self-update even though the broker is healthy.
Add supervisor.broker_health_url so the host probe targets the broker's
host-published port (127.0.0.1:8082 on the manager); None falls back to
docker.broker_url for in-overlay callers (proxy). Set it in the canonical
master.yaml. Reaper/config-sync/image-updater already ignore the gate.
Two durability gaps in the auto-update pipeline:
1. config-sync EROFS: ProtectSystem=full mounts /etc read-only, so
config-sync's atomic write of the node-local master.yaml (+ .digest
sidecar) under /etc/base failed with OSError Errno 30. Latent until a
real canonical change (broker_health_url) forced the first write. Add
ReadWritePaths=/etc/base so config-sync can converge the host config.
2. Manifest publish flake: the github-actions[bot] force-push to the
release branch intermittently 403s on the first attempt (a re-run
clears it). Wrap the push in a bounded retry-with-backoff so the
self-update manifest publishes without a manual re-run.
Add an offline network-egress guard (pytest plugin -p no_external_egress) plus
its unit test so both default suites can be proven to run with ZERO real egress
to lium.io / api.targon.com (loopback + AF_UNIX stay allowed). Wire a
worker_plane_enabled toggle into the mission master: with the flag OFF no
capability is owned by the worker plane, so gpu units route to online gpu
validators byte-for-byte as pre-mission (VAL-MASTER-013). Document the exact
reproducible flags-OFF verification procedure in the harness docs.
The master's agent-challenge eval job + analyzer run on base_jobs_internal
(--internal, no egress), so the gateway PUBLIC IP is unreachable from there; the
proxy is multi-homed onto that overlay precisely so they can reach it by service
name. Point CHALLENGE_LLM_GATEWAY_BASE_URL at the internal overlay service URL
http://base-master-proxy:19080 (install-swarm.sh static path + cli_app dynamic
own_runner env), fixing zero-gateway-call evals.
…erminate (VAL-CROSS-005)
Add an opt-in (BASE_LIVE_PROVIDER_TESTS=1) live validation that provisions a
real Lium pod via the real `base worker deploy --provider lium` CLI, runs a
base worker agent inside the pod (pod_worker_agent.py) that enrolls with a LOCAL
mission master over a reverse SSH tunnel, pulls+executes a gpu unit on the CPU
stub, and posts an ExecutionProof stamped with the lium provider + real pod id;
then DELETE + verify gone + GET /pods empty + balance delta <= $2. Pod is
terminated on every path (try/finally) so a failure never leaks a billable pod.
… image)
base worker deploy --provider lium hit two real gaps: Lium's edge WAF 403s
any POST body carrying a loopback URL (baked into the template env), and the
private-namespace placeholder WORKER_IMAGE fails pod creation (CREATION_FAILED).
- is_loopback_url() + build_worker_pod_env/LiumClient.ensure_template now strip
loopback master/broker/gateway URLs from the WAF-sensitive template body; the
agent resolves master_url at runtime from config.
- require_worker_image() makes worker.deploy.image + image_digest a required,
digest-pinned config for provider deploys (fail-fast, clear error); no more
silent pin of an un-pullable private image.
- docs/miner/worker-plane.md: publish + digest-pin procedure (flagged as a
user/release action) + WAF/CREATION_FAILED troubleshooting; config example.
Offline respx/unit tests only; full base gate green.
Confirmed live (single green-lit deploy) that Targon has no single
POST /workloads/deploy route (returns 405); the real flow is two-step
register-then-deploy: POST /workloads (-> uid, state registered) then
POST /workloads/{uid}/deploy (-> provisioning). Refactor TargonClient.deploy
to create-then-deploy, surfacing an insufficient-credit failure at either
step as the typed InsufficientCreditsError (never retried). Send the correct
uppercase type RENTAL. Mirror the LiumClient sub-1h lifetime guard into
provision: 0<max_lifetime_hours<1 raises CostGuardrailError instead of
int-truncating termination_hours to 0. respx tests updated; base gate green
(1540 passed, cov 88.4%, mypy/ruff clean).
…gging churn
Attach a recording handler directly to the base.supervisor.image_updater
logger (mirroring test_supervisor_weights.py / test_supervisor_weight_submit.py)
instead of relying on caplog. Importing bittensor raises every already-created
logger to CRITICAL, which filtered the WARNING/ERROR records before caplog could
capture them, making these tests order-dependent in the full suite. No product
code change.
Add one-command blank-server bring-up to install-swarm.sh (+ install-worker.sh):
- ensure_docker(): idempotent Docker Engine auto-install (get-docker|apt),
SKIP_DOCKER_INSTALL opt-out, dry-run safe, runs before preflight.
- --auto-secrets: auto-generate/derive/mint all 13 internal secrets
(openssl rand, asyncpg DSNs, Fernet key, central-gate token via
mint-central-gate-token); YUNWU_API_KEY stays the sole external input.
Persisted mode-600 to SECRETS_ENV_FILE for rotation coherence.
- ensure_validator_wallet() + auto-seed MOCK_METAGRAPH from the derived
ss58 hotkey (validator_permit) via the public master image.
- --skip-ghcr-login + {}-config footgun fix so broker/proxy /root/.docker
binds resolve on public-image deploys.
- --turnkey umbrella flag ties it together (blank box -> full validator).
Tests: ensure_docker, auto-secrets (DSN derivation/persistence/Fernet/
idempotency/mint-plan), wallet+metagraph, worker ensure_docker, ghcr-skip
footgun; docs updated. Full suite green (coverage 89%).
… + flush harness master logs
worker_unit_status keyed faulted_units by work_unit_id alone; harden to the
(challenge_slug, work_unit_id) tuple so same-id units under different challenge
slugs never collide (regression test added). Line-buffer + basicConfig the
mission_master harness so its drill logs are non-empty/inspectable after SIGTERM
teardown.
base-supervisor.service ExecStart=/usr/local/bin/uv run ...; a blank box has
no uv. Add idempotent, dry-run-safe ensure_uv() (astral installer, UV_INSTALL_DIR
pinned to /usr/local/bin, SKIP_UV_INSTALL opt-out), gated on INSTALL_SUPERVISOR,
wired before preflight in both main() and main_validator_node(). +6 tests.
Two bugs surfaced installing --turnkey on a fresh non-master server:
- daemon.json had live-restore:true, which docker rejects at swarm init/join
('incompatible with swarm mode') and is inert on swarm nodes anyway. Removed
from all three daemon templates (validator/cpu-worker/gpu-worker).
- ADVERTISE_ADDR defaulted to the hardcoded master IP; now auto-detects this
host's primary IPv4 (default-route src) with a swarm_init guard. Kept a
PRODUCTION_ADVERTISE_ADDR constant for the canonical-config consistency test.
Tests: daemon-no-live-restore regression + advertise-addr auto-detect/override;
updated master-config consistency test; README updated. Full suite green.
…ount fallback
- lium.provision: move pod-id extraction inside the try so a 2xx rent with a
non-JSON body still terminates+verifies the just-rented pod (no leaked pod);
_extract_pod_id raises a typed LiumError on an unparseable body.
- targon._extract_gpu_count: fall back to the top-level numeric gpu_count when
spec.gpu_count is 0/unknown, so per-GPU price (and max_price filtering) is
correct for multi-GPU shapes.
- worker.deploy.image_tag documented as informational-only (never consumed by
the digest-pinned deploy path) in settings, config, and the miner docs.
# Conflicts:
#	src/base/cli_app/main.py
#	tests/unit/test_image_updater.py
PRISM Compute Plane: miner-funded GPU worker plane (base)
…h auto-update
Add an authoritative Compute Requirements table to the validator guide
(submit-only 2 vCPU/4 GB; base agent-challenge 8 vCPU/32 GB; PRISM needs no
extra compute because GPU eval is delegated to the miner-funded worker plane)
and reconcile the minimum-requirements FAQ to point at it.
Document deploy/swarm/install-swarm.sh as the automatic one-command install:
dry-run by default (--apply executes), --validator-node brings up an
auto-updatable base-validator-agent Swarm service plus a node-local
base-supervisor image-updater (the auto-update), --install-supervisor enables
the systemd control-plane unit. Include a dry-run-then-apply quick-start with
required env and VALIDATOR_CAPABILITIES cpu vs delegated-PRISM selection.
…s root
Turnkey one-shot base-master CLI runs (central-gate token mint, validator
wallet gen, runtime-uid inspect) now use IMAGE_MASTER_CLI (mutable :latest)
instead of the digest-pinned IMAGE_MASTER, so newly-added CLI subcommands
(e.g. mint-central-gate-token) always exist. Deployed services keep the
pinned IMAGE_MASTER (the supervisor image-updater rolls them forward). The
mint docker run now uses --user 0:0 so it can read the root-owned mode-600
mint.yaml + gateway secret temp files. Adds regression tests for the
ephemeral-vs-service image split and the --user 0:0 root flag.
docs(validator): compute-requirements table + turnkey install with auto-update
…ickstart to docs/deploy.md
Reduce the README to a one-screen overview (banner, nav, architecture + weight
mermaid, roles/worker-plane/docs tables of <a href> links) and move the long
deploy walkthrough into docs/deploy.md. Tighten every docs/ page and add a
coordination-flow mermaid to architecture.md. All doc-contract tests preserved.
echobtand others added 27 commits July 29, 2026 07:04
…wlist
fix(master): allowlist public FE agent-challenge reads on proxy
Align package docs with joinbase unattested scoring and miner day-1 flow.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Allow bare GET /benchmarks (metadata object) in the enabled-mode Agent
Challenge proxy allowlist. The frontend already requests this path, but
only /benchmarks/tasks was permitted. The two endpoints return different
shapes, so the frontend path cannot simply be swapped, and joinbase kept
rendering a Base 404 after the public submissions allowlist shipped.
Extend the proxy forward matrix and the FE public GET path list to cover
the bare benchmarks route. Deny coverage for internal, owner, evidence and
TEE neighbors is unchanged.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
In-process terminal_bench own_runner argv omitted --cache-root and
--digest-manifest that runner.py already wires. pydantic-settings does
not export CHALLENGE_* into the subprocess env, so production trials
fell back to broken defaults. Mirror runner.py before the optional
--model block.
…default
parents[3] only worked in the src checkout; installed wheels overshot into
site-packages' grandparent and lacked dataset-digest.json. force-include the
golden file and resolve via importlib.resources plus an ancestor walk.
hatch force-include needs golden/dataset-digest.json at /app during
pip install; both runtime and terminal-bench-runner stages now COPY it
before install so CI image builds stop failing FileNotFoundError.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…esolution
fix(agent-challenge): resolve TB digest manifest in installed wheel
Pass --cache-root/--digest-manifest from own_runner, ship golden/dataset-digest.json in the wheel, and COPY it into Docker build context so installed images stop failing TaskDefNotFoundError.
Task guests incorrectly inherited the DooD job-client --read-only posture,
so verifier upload_tests failed with "mkdir: cannot create directory
'/tests': Read-only file system" while the container was still running.
Keep cap-drop/nnp/pids hardening; leave rootfs writable for harbor paths
and apt-based test.sh. Wire DooD env into upload_tests/collect_verifier_dir.
fix(agent-challenge): stop RO-rootfs on own_runner task containers
Regression guard for the compose long-syntax tmpfs.mode decimal footgun
that made /tmp mode 3361 and blocked uid 1000 Terminal-Bench builds.
Compose long-syntax tmpfs.mode: 1777 is decimal (octal 3361). Short
syntax /tmp:size=256m,mode=1777 matches the validator compose and yields
sticky world-writable /tmp for the master validator process.
…ng secrets
Lock both directions: count-dataset-tokens stays verbatim and genuine
credential shapes remain scrubbed from public task-event text.
Match secret/token as hyphen-delimited segments and shield the known
Terminal-Bench task-id catalog so ids like count-dataset-tokens are not
replaced with [REDACTED_SECRET] in public task events.
Contract that score 0.0 serializes passed=false with status completed,
score 1.0 serializes passed=true, and terminal event metadata carries
the same flag when score is present.
Additive boolean derived from score >= 1.0 (same rule as passed_tasks).
Lifecycle status stays completed/failed for compatibility; consumers no
longer treat a zero-score completed task as a pass.
Flip isolation invariants so own_runner task containers assert cap-drop
is absent while no-new-privileges, pids-limit, and tmpfs remain.
…ucceed
Deliberate isolation trade-off already running in prod as a hotpatch:
own_runner task guests need capabilities for apt/chmod inside verifiers.
Master compose cap_drop ALL is unchanged. Keep no-new-privileges, pids
limit, /tmp tmpfs, and writable workspace volume.
Narrow tmpfs options to dict[str, Any] before .get so union-attr is clean.
…action-passed-capdrop
fix(tb): scoring tmpfs, redaction, passed flag, cap-drop
…ate (#61)
* fix(proxy): allow miner env/launch routes under attested allowlist
Prod enables agent_challenge_attested_routes_enabled, which blocked
GET|PUT /env, POST /env/confirm-empty, and POST /launch with local 404.
Treat env/launch as signed routes flag-independently via the existing
_is_agent_challenge_env_route SSOT so the enabled-mode allowlist admits
them and miner X-Hotkey/X-Signature/X-Nonce/X-Timestamp headers are
preserved (otherwise signed PUT residual 401 after allowlisting).
* fix(agent-challenge): start submissions unconfirmed for miner env gate
_persist_submission hardcoded env_confirmed_empty=True, so analysis allow
always auto-enqueued credential-less evaluation and waiting_miner_env was
dead. New rows start unconfirmed; miners must PUT /env or POST
/env/confirm-empty before evaluation. Legacy backfill paths untouched.
* fix(agent-challenge): fail-closed evaluation enqueue status gate
_validate_evaluation_enqueue_status previously fell through for unknown
statuses (including accidental enqueue from non-ready states). Explicitly
allow in-flight TB statuses, waiting_miner_env when confirmed, legacy
analysis_allowed, and terminal re-eval labels; raise ValueError otherwise.
Host-local evaluation has egress to PyPI; --no-index caused instant
empty-index failures for real agent deps (e.g. litellm>=1.55.0).
Drop offline-only pip flags and use network-sane retries/timeouts.
…pypi-index
fix(agent-challenge): allow PyPI index for agent dependency install
Every Terminal-Bench evaluation scored 0 and every result.json carried
"model_name": null. The packaged agent fails closed without a concrete
model id:
agent run failed: A concrete model id is required: set LLM_MODEL
(the measured review harness supplies it under .rules).
Three independent layers dropped the model id, so no submission could
ever score:
1. the runner never set LLM_MODEL -- the platform only holds it as
CHALLENGE_LLM_MODEL (settings.llm_model), which the agent never reads;
2. sanitize_miner_env_for_job stripped a miner-supplied LLM_MODEL because
the name is not token/key shaped;
3. AGENT_ENV_ALLOWLIST admitted only LLM_COST_LIMIT and OPENROUTER_API_KEY.
The trial died in ~5s per task, before any LLM call, which is why the
whole leaderboard sat at zero while OPENROUTER_API_KEY was reaching the
agent correctly.
_terminal_bench_env now publishes settings.llm_model as LLM_MODEL, ahead
of the sanitized miner merge so a miner may override it; the model name
joins both the miner product allowlist and the agent sandbox allowlist.
A miner can therefore bring their own key and their own model. When an
operator blanks llm_model nothing is injected, so the agent still fails
loudly rather than running an unmeasured default.
Updated pins keep their intent: gateway/URL/host/proxy names stay
excluded, secret scrubbing still asserted, and the ex-gateway test now
asserts LLM_MODEL comes from settings and that no gateway base_url or
token leaks into the job env.
Verified: 7 new tests RED -> GREEN; full agent-challenge suite shows zero
regressions (26 pre-existing failures on main, 26 with this change,
identical sets; 2286 -> 2293 passing).
fix(agent-challenge): supply LLM_MODEL to the evaluated agent
…asted jobs
Four production causes made Terminal-Bench score 0 for infrastructure reasons rather than merit, and hid why.
- allow exec on the runner /tmp tmpfs: pip installs the miner agent into /tmp/.local under a read-only rootfs, and Docker mounts tmpfs noexec by default, so loading failed with 'failed to map segment from shared object'.
- surface per-trial failure diagnostics: the broker never bind-mounts the job directory back, so only runner stdout/stderr survives; the cause now travels on that channel.
- keep the real cause of agent construction failures: a constructor that shells out writes to inherited file descriptors, so CalledProcessError kept only argv plus exit status; fd 1 and 2 are captured and replayed.
- stop a job once the agent cannot be constructed: construction is task-independent, so a broken package no longer burns 30 tasks of wall clock and LLM budget to rediscover one packaging error.
The Swarm backend translated a docker-run tmpfs spec by keeping size= and
discarding everything else. The own_runner job container installs the miner
agent into /tmp/.local under a read-only rootfs, so its spec carries exec;
tmpfs is noexec unless told otherwise. Routing that workload through Swarm
therefore produced a noexec /tmp, where the agent loads no shared object and
fails with "failed to map segment from shared object" -- a symptom that took
hours to trace back to a dropped mount flag.
Swarm cannot express the flag at all: against Docker 29.2.1 a bare exec is
rejected as a non key=value field and tmpfs-options is an unknown option, so
only tmpfs-size and tmpfs-mode survive. Translate those two, accept the flags
Swarm already applies by default, and refuse anything else -- exec loudest of
all -- rather than dropping it and failing far from the cause.
@coderabbitai

Copy link
Copy Markdown

Warning

Review limit reached

@echobt, you've reached your PR review limit, so we couldn't start this review.

Next review available in:6 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a1074551-b672-4846-9614-9972546d7739

📥 Commits

Reviewing files that changed from the base of the PR and between 7b076d3 and 62e55f7.

📒 Files selected for processing (2)
  • src/base/master/swarm_backend.py
  • tests/unit/test_swarm_tmpfs_exec.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@echobt@alpha1122x