Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 25 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div align="center">
<h1>⛪ Cathedral CLI</h1>
<p><strong>Two ways to mine. One way to validate.</strong></p>
<p><strong>Two early miner paths. Validator control is quarantined.</strong></p>
<video controls width="800" src="https://github.com/user-attachments/assets/930814df-e648-40f2-8d1a-6adcaae3f3f4"></video>
<p><a href="https://www.youtube.com/watch?v=KQRz6r9HJAs">Watch on YouTube</a> · <code>EARLY BETA</code></p>
</div>
Expand All @@ -13,7 +13,7 @@ One interface for Cathedral miners and validators.
|---|---|
| Compute miner | Runs Intel TDX CPU work and returns measured evidence with receipts. |
| Distill miner | Finds exploit proofs for fixed vulnerabilities. Validators rerun both builds before scoring. |
| Validator | Verifies evidence and composes weights. Chain broadcasts stay off by default. |
| Validator | Reports a contract mismatch and launches no test, serving loop, or write operation. At reviewed upstream commit `d225e8758ca02627cced800b7de0c79464d89aee`, its entrypoint is a direct chain writer with no non-writing mode. |

## Quickstart

Expand All @@ -37,8 +37,26 @@ python3.11 ./cathedral agent-brief compute
python3.11 ./cathedral capabilities --json
```

Replace `compute` with `distill` or `validator`.

Everything above works on a fresh clone today.

`setup` and `test` additionally need a signed Cathedral release, which is not published yet — so they fail closed until it is. That is the design, not a fault: the CLI installs no engine it cannot verify. Rewards and chain writes stay off by default in early beta.
Replace `compute` with `distill`. `explain validator` and `capabilities` are
available for diagnostics, but Validator setup, config writes, tests, and starts
fail closed with `contract.engine_incompatible`.

The discovery and explanation commands above work on a fresh clone today.

Miner `setup` and `test` additionally need a signed Cathedral release, which is
not published yet, so they fail closed until it is. The Validator needs more
than a new pin: an owner must first choose a diagnostics-only interface or an
explicit chain-controller design. This CLI will not infer that authority.

Signed releases are node-wide. Miner setup or update may therefore install the
retained Validator package and verify its signed files. After wheel installation,
no runtime, import, client-entrypoint, or server-entrypoint probe intentionally
executes newly installed Validator code; all public Validator launch, test, setup,
and config-write paths remain quarantined.

Engine receipt v6 records `runtime_checked` or `static_quarantine`. A static
generation exposes no executable path, and an older v5 generation is not inferred
safe. The verifier binds that tier to the current adapter policy, so rewriting a
receipt cannot promote static evidence. Enabling a future Validator contract
therefore requires an owner-specified migration and a newly runtime-checked,
higher signed release; this draft intentionally does not invent that transition.
2 changes: 1 addition & 1 deletion cathedral
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Cathedral node — one command for Distill mining, Compute mining, and validation.
"""Cathedral node — Distill mining, Compute mining, and Validator diagnostics.

Zero dependencies beyond the standard library, on purpose: `git clone` then run
this file. Nothing to install before you can find out whether your machine
Expand Down
2 changes: 1 addition & 1 deletion cathedral.lock.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"schema": "cathedral.node.lock.v1",
"generated_at": "2026-07-30T23:00:00Z",
"note": "Pinned upstream engine revisions. A node installs exactly these commits. Changing a pin is a reviewed change: run `cathedral update --check` to see what a newer pin would bring, and `cathedral update --to <lockfile>` to apply one.",
"note": "Pinned upstream engine revisions. A node installs only releases that are signed and authorized by these exact pins. Changing a pin requires an owner-reviewed signed whole-node release: use `cathedral update --check` to inspect the configured release, and `cathedral update --release <bundle-dir> --yes` to apply an explicit verified bundle.",
"engines": {
"distill": {
"repository": "https://github.com/cathedralai/cathedral-distill.git",
Expand Down
49 changes: 30 additions & 19 deletions cathedral_node/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
templatable surface — ``cathedral test distill --json`` and
``cathedral test compute --json`` differ by one token.

Upstream command names are not preserved. ``cathedral-cybergym-agent --local``,
``cathedral worker serve``, and ``cathedral-validator serve --dry-run --offline``
are three unrelated spellings of "try this safely"; here they are all
``cathedral test <role>``.
Upstream command names are adapted only when their semantics still match. The
Distill and Compute engines expose safe local tests. At the content-addressed
Validator revision recorded by its adapter, the entry point is a direct chain
writer with no non-writing mode, so its historical test/start translation is
quarantined instead of being approximated with different argv.
"""

from __future__ import annotations
Expand Down Expand Up @@ -64,9 +65,9 @@ def error(self, message: str): # noqa: A003 - argparse's own name
cathedral status what is running, and what happened

validator
cathedral setup validator
cathedral test validator safe dry run against the signed feed
cathedral start validator begin validating (never broadcasts by default)
cathedral explain validator inspect the contract mismatch
cathedral test validator fail closed; launches nothing
cathedral start validator fail closed; launches nothing

agents
every command takes --json versioned envelope on stdout; diagnostics on stderr
Expand All @@ -79,7 +80,10 @@ def error(self, message: str): # noqa: A003 - argparse's own name
def build_parser() -> argparse.ArgumentParser:
parser = ContractParser(
prog="cathedral",
description="Cathedral node — one command for Distill mining, Compute mining, and validation.",
description=(
"Cathedral node — Distill mining, Compute mining, and quarantined "
"Validator diagnostics."
),
epilog=_EPILOG,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
Expand Down Expand Up @@ -118,15 +122,15 @@ def add_release_args(sub: argparse.ArgumentParser) -> None:
help="root-owned allowed_signers trust file (default: $CATHEDRAL_HOME/allowed_signers)")

# --- discovery and orientation ---------------------------------------------
quickstart_cmd = add("quickstart", "Guided path from a clean machine to a verified local test",
quickstart_cmd = add("quickstart", "Guided path for an available role to a verified local test",
role="optional")
add_release_args(quickstart_cmd)
add("doctor", "Check whether this machine and identity qualify")
add("capabilities", "What this node can do, and what needs an owner decision", role=None)
add("explain", "What a role does, what it needs, and what it pays", role="required")

# --- setup -------------------------------------------------------------------
setup = add("setup", "Install the signed release and write configuration", role="required")
setup = add("setup", "Install a signed release for an available role", role="required")
setup.add_argument("--force", action="store_true", help="reinstall even if already correct")
add_release_args(setup)

Expand Down Expand Up @@ -159,12 +163,13 @@ def add_release_args(sub: argparse.ArgumentParser) -> None:
secret_rm.add_argument("name")

# --- running -------------------------------------------------------------------
test_cmd = add("test", "Run the verified local test — pays nothing, touches no chain", role="required")
test_cmd = add("test", "Run an available engine's local test — pays nothing, touches no chain",
role="required")
test_cmd.add_argument("--timeout", type=float, default=0, help="seconds before giving up")

start = add("start", "Start mining or validating", role="required")
start = add("start", "Start an available miner engine", role="required")
start.add_argument("--broadcast", action="store_true",
help="validator only: allow chain writes. Requires --yes as well.")
help="validator-only refusal sentinel; chain writes are never enabled")
start.add_argument("--foreground", action="store_true", default=True,
help="run in this terminal (the default)")
start.add_argument("--once", action="store_true", help="one cycle, then exit")
Expand All @@ -181,7 +186,7 @@ def add_release_args(sub: argparse.ArgumentParser) -> None:
logs.add_argument("--lines", "-n", type=int, default=40)
logs.add_argument("--raw", action="store_true", help="engine output rather than node events")

resume = add("resume", "Continue an interrupted run", role=None)
resume = add("resume", "Continue an interrupted run when its current contract permits", role=None)
resume.add_argument("run", help="run id")

cancel = add("cancel", "Cancel a run, preserving its state", role=None)
Expand All @@ -200,13 +205,19 @@ def add_release_args(sub: argparse.ArgumentParser) -> None:

# --- updates -------------------------------------------------------------------
update = subparsers.add_parser("update", parents=[common],
help="Move to new pinned engine revisions, safely")
update.add_argument("--check", action="store_true", help="report what would change; change nothing")
help="Inspect or apply a signed node-wide release")
update.add_argument("--check", action="store_true",
help="inspect the signed node-wide release; change nothing")
update.add_argument("--to", metavar="LOCKFILE", default=None,
help="adopt pins from another lockfile. NOT verified: review it, and "
"the repositories it names, before using it")
help="refused compatibility sentinel; unsigned lockfile adoption is disabled")
add_release_args(update)
update.add_argument("role", nargs="?", choices=lockfile.ROLES, default=None)
update.add_argument(
"role",
nargs="?",
choices=lockfile.ROLES,
default=None,
help="refused compatibility sentinel; signed releases are node-wide",
)

# Rollback is node-wide (there is no per-role rollback): it undoes an interrupted
# transaction, or explains that a deliberate rollback is a newly-signed release.
Expand Down
97 changes: 80 additions & 17 deletions cathedral_node/commands/agent_brief.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,15 @@ def _brief(ctx: Context, lock, roles, states, group) -> Envelope:
for name in roles:
engine = engines.load(name, lock, group)
installed = states[name]
cfg = config.load(name)
static_blocker = engine.operation_blocker()
legacy_config_error: str | None = None
try:
cfg = config.load(name)
except config.ConfigError as exc:
if static_blocker is None:
raise
cfg = config.defaults(name)
legacy_config_error = str(exc)
qualification = engine.qualify(cfg)
facts[name] = {
"title": engine.title,
Expand All @@ -48,6 +56,7 @@ def _brief(ctx: Context, lock, roles, states, group) -> Envelope:
"can_local_test": qualification.can_local_test,
"can_operate": qualification.can_operate,
"blockers": qualification.blockers,
"legacy_config_error": legacy_config_error,
"owner_gated": [
{"capability": k, "detail": v.get("detail", "")}
for k, v in engine.capabilities().items()
Expand Down Expand Up @@ -88,6 +97,44 @@ def _markdown(facts: dict[str, Any], roles: list[str]) -> str:
]
blocked_block = "\n".join(blocked) if blocked else "- Nothing additional."

quarantined = [
name
for name in roles
if any(
blocker.get("code") == "contract.engine_incompatible"
for blocker in facts[name]["blockers"]
)
]
available = [name for name in roles if name not in quarantined]
operation_sections: list[str] = []
if available:
role_arg = available[0] if len(available) == 1 else "<miner-role>"
operation_sections.append(
f"""For available role(s): {', '.join(available)}.

```bash
cathedral capabilities --json # discovery; confirm the protocol MAJOR
cathedral doctor {role_arg} --json # qualification
cathedral setup {role_arg} --json # install the signed release
cathedral test {role_arg} --json # verified local test; no chain access
cathedral status --json # what is running and what happened
```"""
)
if quarantined:
operation_sections.append(
f"""For quarantined role(s): {', '.join(quarantined)}. Diagnostics only.

```bash
cathedral capabilities --json
cathedral explain validator --json
cathedral doctor validator --json # exits 32; owner action is required
```

Do not run setup, test, start, or config set for the Validator. Retrying or
installing the retired pin cannot create a non-writing contract."""
)
operation_block = "\n\n".join(operation_sections)

return f"""\
# Operating a Cathedral node

Expand All @@ -105,8 +152,8 @@ def _markdown(facts: dict[str, Any], roles: list[str]) -> str:
- `error.remediation.command`, when present, is runnable verbatim.
- `error.remediation.requires_operator: true` means **no command fixes this**. Stop and
report it. Do not retry, and do not look for a way around it.
- Every command is safe to re-run. `setup`, `config set`, `secret set`, `stop`, and
`cleanup` are idempotent.
- Every command is safe to re-run. For available roles, `setup` and `config set`
are idempotent; `secret set`, `stop`, and `cleanup` are also idempotent.
- Add `--yes` where a confirmation is required. A required confirmation is never hidden
inside an interactive prompt — you will always get exit `{int(Exit.USAGE)}` with the flag named.

Expand All @@ -121,13 +168,7 @@ def _markdown(facts: dict[str, Any], roles: list[str]) -> str:

## Order of operations

```bash
cathedral capabilities --json # discovery; confirm the protocol MAJOR
cathedral doctor --json # qualification; read .data.roles.<role>.can_local_test
cathedral setup <role> --json # idempotent install of the pinned engine
cathedral test <role> --json # verified local test; pays nothing, no chain access
cathedral status --json # what is running and what happened
```
{operation_block}

{role_sections}

Expand All @@ -150,23 +191,45 @@ def _markdown(facts: dict[str, Any], roles: list[str]) -> str:
## Reporting

Report the envelope's `run_id`, `status`, `exit_code`, and any identifiers under
`data.identifiers` — they are the exact challenge, receipt, vector, and submission ids the
operator needs. Include `error.code` verbatim when something failed.
`data.identifiers`. Include `error.code` verbatim when something failed. The
quarantined Validator creates no run and returns no vector or submission identifiers.
"""


def _role_section(role: str, facts: dict[str, Any]) -> str:
lines = [f"### {facts['title']} (`{role}`)", ""]
if facts["installed"]:
lines.append(f"- Engine installed at `{(facts['revision'] or '')[:12]}`.")
elif any(
blocker.get("code") == "contract.engine_incompatible"
for blocker in facts["blockers"]
):
lines.append("- Engine execution is quarantined. Installation is not a compatibility fix.")
else:
lines.append(f"- Engine not installed. Run `cathedral setup {role} --json` first.")
lines.append(
f"- Local test: {'available' if facts['can_local_test'] else 'blocked'} — `cathedral test {role} --json`"
)
lines.append(
f"- Live operation: {'available' if facts['can_operate'] else 'blocked'} — `cathedral start {role} --json`"
contract_quarantined = any(
blocker.get("code") == "contract.engine_incompatible"
for blocker in facts["blockers"]
)
if contract_quarantined:
lines.append("- Local test: quarantined; do not install or retry.")
lines.append("- Live operation: quarantined; this CLI launches no Validator operation.")
lines.append(
"- Node-wide miner setup/update may install and statically verify the retained package, "
"but no post-install runtime, import, or entrypoint probe executes its code."
)
if facts.get("legacy_config_error"):
lines.append(
"- Legacy config is unreadable but inert while quarantined: "
+ facts["legacy_config_error"]
)
else:
lines.append(
f"- Local test: {'available' if facts['can_local_test'] else 'blocked'} — `cathedral test {role} --json`"
)
lines.append(
f"- Live operation: {'available' if facts['can_operate'] else 'blocked'} — `cathedral start {role} --json`"
)
if facts["blockers"]:
lines.append("- Blockers:")
for blocker in facts["blockers"]:
Expand Down
Loading