diff --git a/pyproject.toml b/pyproject.toml index 5483d049..2c487234 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,9 +37,9 @@ dev = ["ruff>=0.8.0", "ty>=0.0.1a1"] [project.scripts] seismic-deploy = "deploy_gcp.seismic_deploy.cli:cli" # Operator-facing: act on your own node. Cloud-agnostic, never wraps Pulumi. -seismic-tee = "tee.cli:app" +seismic-tee = "tee.cli.node.app:app" # Seismic-internal: bootstrap a network (provision cohort + genesis ceremony). -seismic-tee-bootstrap = "tee.bootstrap_cli:app" +seismic-tee-bootstrap = "tee.cli.network.app:app" [tool.setuptools.packages.find] include = ["deploy_gcp*", "tee*"] diff --git a/tee/README.md b/tee/README.md index ad5b0b14..7cf2a45d 100644 --- a/tee/README.md +++ b/tee/README.md @@ -73,7 +73,7 @@ only consumes one to reach an already-running node. Only `seismic-tee` is cloud-agnostic and **never wraps Pulumi**; `seismic-tee-bootstrap` is the one allowed to. A **node descriptor** is a small JSON (`public_ip`/`fqdn`) describing one provisioned node — see -`tee/descriptor.py`. It's what `seismic-tee-bootstrap up` emits +`tee/cli/common/descriptor.py`. It's what `seismic-tee-bootstrap up` emits (just `{public_ip, fqdn}`); a BYO-infra operator can hand-write it (or use `pulumi stack output --json` — only those two keys are read). @@ -95,16 +95,17 @@ the same audience line: — founding a *network* (orchestrating a cohort, the genesis ceremony), which only Seismic does. -The current layout already anticipates this: the operator path -(`cli.py` → `configure.py` → `descriptor.py`) has no dependency on the -founding side and never imports Pulumi, so it lifts out cleanly, and the -dependency direction is one-way (private builds on public). The seam is -the node descriptor. At extraction the shared bits (`descriptor.py`, -`cli_common.py`) go to the public side and this repo depends on them. +The package layout mirrors this split: `cli/node/` + `cli/common/` + +`pulumi/seismic_node/` are the public half, `cli/network/` + +`pulumi/playground/` the private half. The dependency direction is +one-way (private builds on public): `cli/common/` imports neither side, +`cli/node/` imports only `cli/common/`, and only `cli/network/` may +import both. The seam is the node descriptor. At extraction the public +half lifts out wholesale and this repo depends on it. -Not done yet — **don't pre-split within this repo.** The rule to keep -honoring until then: the operator CLI stays cloud-agnostic and never wraps -Pulumi, and a new file goes on whichever side it would ultimately land. +The rules to keep honoring until then: the operator CLI stays +cloud-agnostic and never wraps Pulumi, the import direction above holds, +and a new file goes on whichever side it would ultimately land. ### Joining an existing network (the common case) @@ -284,7 +285,7 @@ only gates against a second POST within the same boot. today — it only POSTs the config. The `--measurements` flag is gated off and errors if passed, rather than pretending to verify. -Why: the old path (`proxy.py`, Flashbots' `cvm-reverse-proxy` +Why: the old path (`cli/node/proxy.py`, Flashbots' `cvm-reverse-proxy` `proxy-client` against `:7936`) is non-functional — no seismic node serves that RA-TLS endpoint, and cvm-reverse-proxy's pre-handshake aTLS is a different protocol than the `flashbots/attested-tls` the enclave diff --git a/tee/cli/__init__.py b/tee/cli/__init__.py new file mode 100644 index 00000000..20a7f1ff --- /dev/null +++ b/tee/cli/__init__.py @@ -0,0 +1,8 @@ +"""The two TEE CLIs and their shared code. + +Import direction — it keeps the eventual public/private repo split +(see tee/README.md) mechanical: common/ imports nothing else here, +node/ imports only common/, network/ may import both. node/ and +common/ are slated for the public operator repo, so nothing they +import may come from network/. +""" diff --git a/tee/cli/common/__init__.py b/tee/cli/common/__init__.py new file mode 100644 index 00000000..4059c674 --- /dev/null +++ b/tee/cli/common/__init__.py @@ -0,0 +1 @@ +"""Code shared by both CLIs. Must not import from ..node or ..network.""" diff --git a/tee/descriptor.py b/tee/cli/common/descriptor.py similarity index 100% rename from tee/descriptor.py rename to tee/cli/common/descriptor.py diff --git a/tee/utils/logging_setup.py b/tee/cli/common/logging_setup.py similarity index 100% rename from tee/utils/logging_setup.py rename to tee/cli/common/logging_setup.py diff --git a/tee/manifest.py b/tee/cli/common/manifest.py similarity index 98% rename from tee/manifest.py rename to tee/cli/common/manifest.py index b324903d..9d6a3973 100644 --- a/tee/manifest.py +++ b/tee/cli/common/manifest.py @@ -21,13 +21,13 @@ only command that takes loose files — then `assemble`/`validate` operate on the directory): - uv run python -m tee.manifest init tee/networks/seismic-devnet-3 \ + uv run python -m tee.cli.common.manifest init tee/networks/seismic-devnet-3 \ --reth-genesis dev.json \ --measurements ../seismic-images/build/measurements.json \ --measurement-id seismic_2026-06-11.abc123.vhd # edit tee/networks/seismic-devnet-3/summit-template.toml, then: - uv run python -m tee.manifest assemble tee/networks/seismic-devnet-3 - uv run python -m tee.manifest validate tee/networks/seismic-devnet-3 + uv run python -m tee.cli.common.manifest assemble tee/networks/seismic-devnet-3 + uv run python -m tee.cli.common.manifest validate tee/networks/seismic-devnet-3 """ import argparse @@ -45,7 +45,7 @@ from pathlib import Path from typing import Any -from tee.utils.logging_setup import setup_logging +from tee.cli.common.logging_setup import setup_logging logger = logging.getLogger(__name__) @@ -777,7 +777,9 @@ def validate_reth_genesis_matches( def _parse_args(argv: list[str] | None = None) -> argparse.Namespace: - parser = argparse.ArgumentParser(prog="python -m tee.manifest", description=__doc__) + parser = argparse.ArgumentParser( + prog="python -m tee.cli.common.manifest", description=__doc__ + ) sub = parser.add_subparsers(dest="command", required=True) def add_reth_bin(p: argparse.ArgumentParser) -> None: diff --git a/tee/cli_common.py b/tee/cli/common/plumbing.py similarity index 100% rename from tee/cli_common.py rename to tee/cli/common/plumbing.py diff --git a/tee/tests/__init__.py b/tee/cli/common/tests/__init__.py similarity index 100% rename from tee/tests/__init__.py rename to tee/cli/common/tests/__init__.py diff --git a/tee/tests/test_manifest.py b/tee/cli/common/tests/test_manifest.py similarity index 99% rename from tee/tests/test_manifest.py rename to tee/cli/common/tests/test_manifest.py index 0e568349..0c3d1103 100644 --- a/tee/tests/test_manifest.py +++ b/tee/cli/common/tests/test_manifest.py @@ -12,8 +12,8 @@ import urllib.request from pathlib import Path -from tee import manifest as manifest_mod -from tee.manifest import ( +from tee.cli.common import manifest as manifest_mod +from tee.cli.common.manifest import ( AssembledManifest, GateContext, GateError, diff --git a/tee/cli/network/__init__.py b/tee/cli/network/__init__.py new file mode 100644 index 00000000..5443a906 --- /dev/null +++ b/tee/cli/network/__init__.py @@ -0,0 +1,4 @@ +"""`seismic-tee-bootstrap` — internal CLI to found a network. + +May import ..node and ..common. +""" diff --git a/tee/bootstrap_cli.py b/tee/cli/network/app.py similarity index 84% rename from tee/bootstrap_cli.py rename to tee/cli/network/app.py index a17e08c9..4fd088be 100644 --- a/tee/bootstrap_cli.py +++ b/tee/cli/network/app.py @@ -6,16 +6,16 @@ Pulumi — `up` / `down` drive the seismic_node Automation-API orchestrator. The operator CLI (`seismic-tee`) deliberately is not; the boundary is the node -descriptor file (see tee/descriptor.py), which this CLI produces +descriptor file (see tee/cli/common/descriptor.py), which this CLI produces (via provisioning) and consumes (during the genesis ceremony). Wired via [project.scripts] in pyproject.toml. Each leaf forwards its argv -to that module's argparse `main()`; see tee/cli_common.py. +to that module's argparse `main()`; see tee/cli/common/plumbing.py. """ import click -from tee.cli_common import PASSTHROUGH, forward +from tee.cli.common.plumbing import PASSTHROUGH, forward @click.group() @@ -27,7 +27,7 @@ def app() -> None: @click.argument("argv", nargs=-1, type=click.UNPROCESSED) def up(argv: tuple[str, ...]) -> None: """Provision a cohort of TDX nodes (one independent Pulumi stack each).""" - from tee import orchestrator + from tee.cli.network import orchestrator forward(orchestrator.up_main, "seismic-tee-bootstrap up", argv) @@ -36,7 +36,7 @@ def up(argv: tuple[str, ...]) -> None: @click.argument("argv", nargs=-1, type=click.UNPROCESSED) def down(argv: tuple[str, ...]) -> None: """Tear down cohort node(s); each stack destroys independently.""" - from tee import orchestrator + from tee.cli.network import orchestrator forward(orchestrator.down_main, "seismic-tee-bootstrap down", argv) @@ -45,7 +45,7 @@ def down(argv: tuple[str, ...]) -> None: @click.argument("argv", nargs=-1, type=click.UNPROCESSED) def configure(argv: tuple[str, ...]) -> None: """Configure a cohort in parallel: one genesis + N joiners, one command.""" - from tee import cohort_configure + from tee.cli.network import cohort_configure forward(cohort_configure.main, "seismic-tee-bootstrap configure", argv) @@ -56,7 +56,7 @@ def configure(argv: tuple[str, ...]) -> None: @click.argument("argv", nargs=-1, type=click.UNPROCESSED) def genesis_ceremony(argv: tuple[str, ...]) -> None: """One-shot genesis ceremony: build genesis.toml from the cohort, fan it out.""" - from tee import genesis as genesis_mod + from tee.cli.network import genesis as genesis_mod forward(genesis_mod.main, "seismic-tee-bootstrap genesis-ceremony", argv) @@ -65,7 +65,7 @@ def genesis_ceremony(argv: tuple[str, ...]) -> None: @click.argument("argv", nargs=-1, type=click.UNPROCESSED) def manifest(argv: tuple[str, ...]) -> None: """Scaffold (init) / assemble / validate a network artifact-set dir.""" - from tee import manifest as manifest_mod + from tee.cli.common import manifest as manifest_mod forward(manifest_mod.main, "seismic-tee-bootstrap manifest", argv) diff --git a/tee/cohort_configure.py b/tee/cli/network/cohort_configure.py similarity index 97% rename from tee/cohort_configure.py rename to tee/cli/network/cohort_configure.py index 71cf0779..4f0a45ec 100644 --- a/tee/cohort_configure.py +++ b/tee/cli/network/cohort_configure.py @@ -25,17 +25,17 @@ from dataclasses import dataclass, field from pathlib import Path -from tee import manifest as manifest_mod -from tee.configure import ( +from tee.cli.common import manifest as manifest_mod +from tee.cli.common.descriptor import load_descriptor, require +from tee.cli.common.logging_setup import setup_logging +from tee.cli.node.configure import ( ENCLAVE_PEER_PORT, TDX_INIT_PORT, build_config, post_config_to_tdx_init, resolve_reth_genesis, ) -from tee.descriptor import load_descriptor, require -from tee.status import poll_provisioning -from tee.utils.logging_setup import setup_logging +from tee.cli.node.status import poll_provisioning logger = logging.getLogger(__name__) diff --git a/tee/genesis.py b/tee/cli/network/genesis.py similarity index 97% rename from tee/genesis.py rename to tee/cli/network/genesis.py index ae7fd93c..0aebbf82 100644 --- a/tee/genesis.py +++ b/tee/cli/network/genesis.py @@ -18,7 +18,7 @@ open → summit keygen, and both are polled until every node responds — so it can run straight after `configure` without hand-timing the boot tail. -Each node is located by a descriptor file (see tee/descriptor.py), +Each node is located by a descriptor file (see tee/cli/common/descriptor.py), so this never calls Pulumi. """ @@ -33,9 +33,9 @@ import requests -from tee import manifest as manifest_mod -from tee.descriptor import load_descriptor, require -from tee.utils.summit_client import PublicKeys, SummitClient +from tee.cli.common import manifest as manifest_mod +from tee.cli.common.descriptor import load_descriptor, require +from tee.cli.network.summit_client import PublicKeys, SummitClient # Summit's consensus (BLS) port. Each validator entry in the generated # genesis.toml pins ":". @@ -171,7 +171,7 @@ def _get_pubkeys( """Gather each cohort node's summit pubkeys from its descriptor file. Resolves every node's fqdn/public_ip from its descriptor (see - tee/descriptor.py), so the summit endpoints come from whatever + tee/cli/common/descriptor.py), so the summit endpoints come from whatever infra tool produced the descriptor — this script never calls Pulumi. Returns the validator entries and the per-node summit clients (reused for the genesis.toml fanout). diff --git a/tee/orchestrator.py b/tee/cli/network/orchestrator.py similarity index 99% rename from tee/orchestrator.py rename to tee/cli/network/orchestrator.py index fc14aedf..2071cfbe 100644 --- a/tee/orchestrator.py +++ b/tee/cli/network/orchestrator.py @@ -5,7 +5,7 @@ of the single-node `seismic_node` program via the Pulumi Automation API (local-program workspace), and writes one node descriptor per node (the same `{public_ip, fqdn, …}` shape `pulumi stack output --json` emits — see -tee/descriptor.py). +tee/cli/common/descriptor.py). Shared settings are inherited from an existing stack config file (`Pulumi.dev.yaml` by default): the orchestrator reads its `config:` block @@ -49,12 +49,12 @@ import yaml from pulumi import automation as auto -from tee import manifest as manifest_mod +from tee.cli.common import manifest as manifest_mod # The single-node program this orchestrator fans out over. A local-program # workspace points at this dir, so the project name / runtime / venv all # come from its Pulumi.yaml — identical to running `pulumi` in that dir. -SEISMIC_NODE_DIR = Path(__file__).parent / "pulumi" / "seismic_node" +SEISMIC_NODE_DIR = Path(__file__).parents[2] / "pulumi" / "seismic_node" # Shared settings are inherited from this stack config unless --config overrides. DEFAULT_CONFIG = SEISMIC_NODE_DIR / "Pulumi.dev.yaml" diff --git a/tee/utils/summit_client.py b/tee/cli/network/summit_client.py similarity index 100% rename from tee/utils/summit_client.py rename to tee/cli/network/summit_client.py diff --git a/tee/cli/network/tests/__init__.py b/tee/cli/network/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tee/tests/test_cohort_configure.py b/tee/cli/network/tests/test_cohort_configure.py similarity index 95% rename from tee/tests/test_cohort_configure.py rename to tee/cli/network/tests/test_cohort_configure.py index 27f9d15d..6bd30bd0 100644 --- a/tee/tests/test_cohort_configure.py +++ b/tee/cli/network/tests/test_cohort_configure.py @@ -13,8 +13,8 @@ import unittest from pathlib import Path -from tee.cohort_configure import build_cohort -from tee.configure import ENCLAVE_PEER_PORT +from tee.cli.network.cohort_configure import build_cohort +from tee.cli.node.configure import ENCLAVE_PEER_PORT def _descriptor(name: str, public_ip: str, fqdn: str) -> Path: diff --git a/tee/tests/test_genesis.py b/tee/cli/network/tests/test_genesis.py similarity index 99% rename from tee/tests/test_genesis.py rename to tee/cli/network/tests/test_genesis.py index 0c46cd94..fa4c9e87 100644 --- a/tee/tests/test_genesis.py +++ b/tee/cli/network/tests/test_genesis.py @@ -11,8 +11,8 @@ from pathlib import Path from unittest import mock -from tee import genesis -from tee.utils.summit_client import PublicKeys +from tee.cli.network import genesis +from tee.cli.network.summit_client import PublicKeys class ParseArgsTests(unittest.TestCase): diff --git a/tee/tests/test_orchestrator.py b/tee/cli/network/tests/test_orchestrator.py similarity index 97% rename from tee/tests/test_orchestrator.py rename to tee/cli/network/tests/test_orchestrator.py index c822f5ad..35b7f86e 100644 --- a/tee/tests/test_orchestrator.py +++ b/tee/cli/network/tests/test_orchestrator.py @@ -10,8 +10,8 @@ import unittest from pathlib import Path -from tee.manifest import render_manifest -from tee.orchestrator import ( +from tee.cli.common.manifest import render_manifest +from tee.cli.network.orchestrator import ( DEFAULT_OUT_DIR, _check_vhd_matches_network, _resolve_out_dir, diff --git a/tee/tests/test_summit_client.py b/tee/cli/network/tests/test_summit_client.py similarity index 90% rename from tee/tests/test_summit_client.py rename to tee/cli/network/tests/test_summit_client.py index cecc10dd..665cd5f5 100644 --- a/tee/tests/test_summit_client.py +++ b/tee/cli/network/tests/test_summit_client.py @@ -1,4 +1,4 @@ -"""Tests for tee.utils.summit_client (stdlib unittest; no test deps). +"""Tests for tee.cli.network.summit_client (stdlib unittest; no test deps). Run with: uv run python -m unittest discover -s tee/tests -v @@ -7,8 +7,8 @@ import unittest from unittest import mock -from tee.utils import summit_client -from tee.utils.summit_client import SummitClient +from tee.cli.network import summit_client +from tee.cli.network.summit_client import SummitClient class SummitClientTests(unittest.TestCase): diff --git a/tee/cli/node/__init__.py b/tee/cli/node/__init__.py new file mode 100644 index 00000000..5f543b28 --- /dev/null +++ b/tee/cli/node/__init__.py @@ -0,0 +1 @@ +"""`seismic-tee` — operator CLI for a single node. Imports only ..common.""" diff --git a/tee/cli.py b/tee/cli/node/app.py similarity index 84% rename from tee/cli.py rename to tee/cli/node/app.py index 12ddba12..31536341 100644 --- a/tee/cli.py +++ b/tee/cli/node/app.py @@ -7,12 +7,12 @@ lives in the separate `seismic-tee-bootstrap` CLI. Wired via [project.scripts] in pyproject.toml. Each leaf forwards its argv -to that module's argparse `main()`; see tee/cli_common.py. +to that module's argparse `main()`; see tee/cli/common/plumbing.py. """ import click -from tee.cli_common import PASSTHROUGH, forward +from tee.cli.common.plumbing import PASSTHROUGH, forward @click.group() @@ -24,7 +24,7 @@ def app() -> None: @click.argument("argv", nargs=-1, type=click.UNPROCESSED) def configure(argv: tuple[str, ...]) -> None: """Configure a node to join a network: assemble + POST config to tdx-init.""" - from tee import configure as configure_mod + from tee.cli.node import configure as configure_mod forward(configure_mod.main, "seismic-tee configure", argv) @@ -33,7 +33,7 @@ def configure(argv: tuple[str, ...]) -> None: @click.argument("argv", nargs=-1, type=click.UNPROCESSED) def status(argv: tuple[str, ...]) -> None: """Watch a node's first-boot LUKS provisioning progress.""" - from tee import status as status_mod + from tee.cli.node import status as status_mod forward(status_mod.main, "seismic-tee status", argv) diff --git a/tee/configure.py b/tee/cli/node/configure.py similarity index 97% rename from tee/configure.py rename to tee/cli/node/configure.py index c0fe38a2..e71eb69d 100644 --- a/tee/configure.py +++ b/tee/cli/node/configure.py @@ -17,7 +17,7 @@ from flags, `[domain]` from the descriptor fqdn + `--email`, and `[network]` from `--manifest` + `--reth-genesis`. Those network-wide artifacts stay standalone files, merged only at POST time. The node address is *brought by -the operator* via a descriptor file (see tee/descriptor.py), typically +the operator* via a descriptor file (see tee/cli/common/descriptor.py), typically `pulumi stack output --json`. The CLI never provisions infrastructure (Pulumi's job). @@ -34,11 +34,11 @@ import requests -from tee import manifest as manifest_mod -from tee.descriptor import load_descriptor, require -from tee.proxy import ProxyClient -from tee.status import watch_luks_provisioning -from tee.utils.logging_setup import setup_logging +from tee.cli.common import manifest as manifest_mod +from tee.cli.common.descriptor import load_descriptor, require +from tee.cli.common.logging_setup import setup_logging +from tee.cli.node.proxy import ProxyClient +from tee.cli.node.status import watch_luks_provisioning logger = logging.getLogger(__name__) diff --git a/tee/proxy.py b/tee/cli/node/proxy.py similarity index 100% rename from tee/proxy.py rename to tee/cli/node/proxy.py diff --git a/tee/status.py b/tee/cli/node/status.py similarity index 98% rename from tee/status.py rename to tee/cli/node/status.py index 38a7dc77..b93445c2 100644 --- a/tee/status.py +++ b/tee/cli/node/status.py @@ -29,8 +29,8 @@ import requests -from tee.descriptor import load_descriptor, require -from tee.utils.logging_setup import setup_logging +from tee.cli.common.descriptor import load_descriptor, require +from tee.cli.common.logging_setup import setup_logging ENCLAVE_PORT = 7878 POLL_INTERVAL_SECONDS = 5 @@ -292,7 +292,8 @@ def parse_args() -> argparse.Namespace: type=Path, required=True, metavar="DESCRIPTOR", - help="Node descriptor JSON (provides public_ip); see tee/descriptor.py.", + help="Node descriptor JSON (provides public_ip); " + "see tee/cli/common/descriptor.py.", ) parser.add_argument( "--once", diff --git a/tee/cli/node/tests/__init__.py b/tee/cli/node/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tee/tests/test_configure.py b/tee/cli/node/tests/test_configure.py similarity index 97% rename from tee/tests/test_configure.py rename to tee/cli/node/tests/test_configure.py index d7596632..68cb65bd 100644 --- a/tee/tests/test_configure.py +++ b/tee/cli/node/tests/test_configure.py @@ -10,17 +10,17 @@ import unittest from pathlib import Path -from tee.configure import ( +from tee.cli.common.manifest import render_manifest + +# Reuse the canonical valid manifest from the manifest tests rather than +# duplicate the schema here; build_config validates it before merging. +from tee.cli.common.tests.test_manifest import FIXTURE_MANIFEST +from tee.cli.node.configure import ( ENCLAVE_PEER_PORT, build_config, resolve_peer, resolve_reth_genesis, ) -from tee.manifest import render_manifest - -# Reuse the canonical valid manifest from the manifest tests rather than -# duplicate the schema here; build_config validates it before merging. -from tee.tests.test_manifest import FIXTURE_MANIFEST FQDN = "node1.example.com" EMAIL = "ops@example.com" diff --git a/tee/tests/test_status.py b/tee/cli/node/tests/test_status.py similarity index 99% rename from tee/tests/test_status.py rename to tee/cli/node/tests/test_status.py index 59e806e5..e3a73a95 100644 --- a/tee/tests/test_status.py +++ b/tee/cli/node/tests/test_status.py @@ -8,7 +8,7 @@ import unittest from unittest import mock -from tee import status +from tee.cli.node import status class FormatTests(unittest.TestCase): diff --git a/tee/utils/__init__.py b/tee/utils/__init__.py deleted file mode 100644 index 90bb49fe..00000000 --- a/tee/utils/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Utility module for common helper functions. - -Import directly from submodules when needed, e.g. -`from tee.utils.summit_client import SummitClient`. -""" - -__all__ = [ - "logging_setup", - "summit_client", -]