feat(ctx): universal subnet CLI + A→Z miner docs on the public gateway - #207
feat(ctx): universal subnet CLI + A→Z miner docs on the public gateway#207echobt wants to merge 4 commits into
Conversation
Co-authored-by: Mathis <echobt@users.noreply.github.com>
Co-authored-by: Mathis <echobt@users.noreply.github.com>
Co-authored-by: Mathis <echobt@users.noreply.github.com>
Co-authored-by: Mathis <echobt@users.noreply.github.com>
Greptile SummaryThis change adds the cross-platform Confidence Score: 0/5Not safe to merge until the release workflow is least-privilege and immutable, and Bounty session storage is made resistant to local path attacks. Focused executable checks reproduced the CLI exit-status failure, confirmed the write-authorized release artifact path, and exercised symlinked session storage through an authenticated report request. Files Needing Attention:
|
| if json_out { | ||
| println!("{}", reply.body); | ||
| return Ok(()); | ||
| } | ||
| if !reply.ok() { | ||
| return Err(explain(reply.status, &reply.message())); |
There was a problem hiding this comment.
ctx --json bounty show prints a non-2xx response body and returns Ok(()) before reply.ok() is checked, so automation receives exit code 0 for gateway failures. A local gateway returned 404 for bounty show, 503 for bounty status and weights, and 401 for image prompts; every JSON-mode command printed the error payload with an exit status of 0, while the equivalent non-JSON Bounty command exited 1. Check the HTTP status before rendering either output format so failed requests consistently return a nonzero status.
| permissions: | ||
| contents: write |
There was a problem hiding this comment.
Build jobs inherit repository write authority
The workflow-level contents: write permission is inherited by all five build-matrix jobs because build has no narrower permissions block. Those jobs execute dtolnay/rust-toolchain@master, then produce archives that the write-capable release job downloads and publishes. Restrict write permission to the release job and pin third-party actions to immutable commit SHAs, so a mutable action revision cannot use a build token or influence miner-installed release assets.
| let path = session_path(); | ||
| if let Some(parent) = path.parent() { | ||
| std::fs::create_dir_all(parent).map_err(|e| format!("create {}: {e}", parent.display()))?; | ||
| } | ||
| let record = json!({ | ||
| "gateway": gateway, | ||
| "account_id": account_id, | ||
| "miner_hotkey": ss58, | ||
| "session": reply.get("session"), | ||
| "session_id": reply.get("session_id"), | ||
| }); | ||
| std::fs::write(&path, format!("{record}\n")).map_err(|e| format!("write session: {e}"))?; | ||
| restrict(&path)?; | ||
| Ok(path) |
There was a problem hiding this comment.
Bounty session cache follows attacker-planted symlinks
store_session writes the bearer session claim through the deterministic cache path before applying 0600. A runtime check planted cortex/bounty-session.json as a symlink: the CLI wrote the claim to its target, the target was initially created with mode 0644, and the subsequently loaded redirected claim was sent to the report endpoint. Create the cache atomically with owner-only permissions and no-follow semantics, and reject non-regular cache entries before loading the credential.
Summary
Miners had no single entry point: the guides carried
<gateway>placeholders, Bounty told them to export an operator secret (BOUNTY_CHAT_COMMAND), and the only CLI (cortex-bounty) covered one third of one challenge. This PR ships one binary, one host, and rewritten guides that a miner can follow start to finish.New:
bins/ctx— the subnet CLI for the four live challenges.ctx challengesctx status/ctx weightscan_scoreper challenge, and whether the epoch is sealed or on the fail-closed burn vectorctx relearn|image|agent submit | show | status--wait) to a terminal statectx image promptsctx bounty pair | report | show | status~/.config/cortex/bounty-session.json(0600), then file reportsDefault gateway is
https://network.cortex.foundation;--gatewayoverrides it for a local stack.LIUM_API_KEYis forwarded asX-Lium-Api-Keyand never printed.The CLI refuses locally what the services refuse anyway, so a miner does not learn a gate by paying for a run: an empty manifest is rejected before the request (
contamination_evidence_missing), pairing prints the terms and will not proceed without--accept-terms, and a thin report is rejected before it burns a rate-limit window. Every HTTP failure is explained — a503says nothing was stored, nothing was rented, and the challenge cannot score right now.Release + install.
.github/workflows/release-ctx.ymlbuildsctxon tagsv*.*.*for linux-amd64/arm64 (static musl), darwin-amd64/arm64, and windows-amd64, then attaches the archives plusSHA256SUMS.txtto the release.scripts/install-ctx.shis the curl one-liner; it verifies the checksum and installs to~/.local/bin/ctx, and it aborts rather than installing something it could not verify.Docs.
docs/external-miner/is rewritten A→Z: installctx, what you need before submitting, pick a challenge, readcan_scorebefore spending anything, submit (CLI and the equivalentcurl), read the verdict. Every host is written out; thecurlroutes are real URLs.troubleshoot.mdis reorganised around what a miner actually sees (installer, connectivity, reject reasons, Bounty states) and no longer hands miners operator env names. Top-levelREADME.mdgained a Mine section pointing at the installer and the four challenges.cortex-bountystill works for operator scripts but prints a deprecation notice pointing atctx bounty pair.Gate.
xtask external-docs-checknow readsDEFAULT_GATEWAYout ofbins/ctx/src/api.rsand requires every miner page to name that exact host, so the docs and the binary cannot drift. It fails on a<gateway>/<host>placeholder anywhere underdocs/external-miner/(recursively, which caught therelearn-seedcopy), and fails if a miner page mentionsBOUNTY_CHAT_COMMAND,BOUNTY_BACKEND_PUBLIC_URL, orBASE_GATEWAY_*.validators.mdis exempt from the env rule — a validator does set its own gateway endpoint. The index no longer pins those env names, which is how they got into miner docs in the first place.Greptile
Every PR is reviewed by Greptile before merge. Config:
.greptile/.@greptileai reviewTest plan
cargo test --workspace— green (includes 33 newctxtests and a newexternal-docs-checktest that runs the whole gate against this repo)cargo fmt --all -- --check,cargo clippy --workspace --all-targets -- -D warningsxtask loc-cap(bins/ctx1190 / 1500),consensus-lint,spec-check,design-check,external-docs-check<gateway>inrelearn.mdfails with both the placeholder and the missing-host findingscargo build -p ctx --release --locked --target x86_64-unknown-linux-musl→ static-pie binary, 5.8 MB,ctx --versionrunsSHA256SUMS.txtboth abortstatus/weights/image prompts; Relearn and Image submit (happy path, plus the empty-manifest refusal) andshow; Bountypair(terms refusal, then a real sr25519 sign → session cached 0600),report,show,status, the thin-report refusal, and the fail-closed503on a host with no adjudication feedcargo deny check— cargo-deny is not installed on this runner; the lockfile delta is thectxpackage entry only, with no new third-party cratesdeploy/scripts/assert-compose-matrix.sh— needs docker, unavailable here; no compose file changedRisk
Docs and a new miner-facing binary; no challenge scoring, emission, model pin, or deploy path is touched. Two things need an operator before miners can use this:
network.cortex.foundationdoes not resolve yet (the legacychain.joinbase.ainame is still whatdeploy/compose/env-prod.ymlpins, and this PR deliberately leaves that pin alone), and the firstv*.*.*tag after merge is what publishes the release assets the install script downloads. Until both land, the install one-liner and the documented host will 404 / fail to resolve.ctx installand self-update are deliberately out of scope for this first cut.Follow-up outside this repo
The public miner repo
CortexLM/relearnstill shows the old submit snippet. Its in-repo seed copy (docs/external-miner/relearn-seed/) is updated here with thectxinstall line and the concrete gateway host; the same edit needs to land on the public repo, which this PR cannot reach.Naming
I did not rename
BASE_*environment variables, deployed host paths(
/opt/base,/run/base, …), GHCRbaseintelligence/basepackage names, orbase-*-v1cryptographic domain tags, unless this PR’s purpose is a coordinatedcutover documented in
docs/NAMING.md.