diff --git a/README.md b/README.md index 275b80f..18c80b8 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ python3 -m ampg --config gateway.toml apply --dry-run --profile vps-full python3 -m ampg --config gateway.toml deploy apply --stage state --dry-run --profile vps-full python3 -m ampg --config gateway.toml deploy apply --stage supervisor --dry-run --profile vps-full python3 -m ampg --config gateway.toml deploy apply --stage start --dry-run --profile vps-full +python3 -m ampg --config gateway.toml deploy apply --stage addresses --dry-run --profile vps-full ``` `init site` writes a readable `gateway.toml` with selected transports enabled and common @@ -71,7 +72,8 @@ reachability hints. `apply --dry-run` prints the activation sequence and a prefl verdict without changing services. `deploy apply --stage state` copies approved managed-daemon config into AMPG-owned state. `deploy apply --stage supervisor` installs approved supervisor files after state exists. `deploy apply --stage start` starts -AMPG-owned services after state and supervisor files are present. +AMPG-owned services after state and supervisor files are present. `deploy apply --stage +addresses` captures daemon-written public addresses into the address registry. ## Interaction tiers @@ -141,6 +143,8 @@ python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage supe python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage supervisor --profile mobile-i2p --yes python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage start --dry-run --profile mobile-i2p python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage start --profile mobile-i2p --yes +python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage addresses --dry-run --profile mobile-i2p +python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage addresses --profile mobile-i2p --yes python3 -m ampg --config examples/wownero.gateway.toml build python3 -m ampg --config examples/wownero.gateway.toml build --profile tor-i2p python3 -m ampg --config examples/wownero.gateway.toml build --protocol tor --protocol i2p @@ -174,8 +178,9 @@ platform supervisor files, but are not installed or started. Managed daemon conf runtime state at `gateway.state_dir`. Captured transport addresses are written under `gateway.state_dir` by -`addresses capture` or `addresses set`. Fixture manifests and health plans use explicit -config first, then captured addresses, then deterministic placeholders. +`deploy apply --stage addresses`, `addresses capture`, or `addresses set`. Fixture +manifests and health plans use explicit config first, then captured addresses, then +deterministic placeholders. `approvals list` reports whether generated artifact digests are missing, stale, unapproved, or approved. `approvals approve` records reviewed digests under @@ -204,6 +209,10 @@ requires `--yes`; it runs only structured platform commands for AMPG-named servi does not install packages, rewrite config, delete files, capture addresses, or run health checks. +`deploy apply --stage addresses --dry-run` shows daemon-written public addresses that can +be recorded. The live form requires `--yes`; it writes captured addresses to +`gateway.state_dir` and leaves health verification for the next stage. + `dns plan --free-domain-hints` prints optional community subdomain services that may help new users get a clearnet name without buying a domain. These are third-party registries; operators still need to review current terms, availability, content rules, and DNS @@ -232,4 +241,5 @@ Explicit `--protocol` and `--platform` flags override the profile when present. - [ ] Run `ampg deploy apply --stage state --dry-run`; confirm approved state copies. - [ ] Run `ampg deploy apply --stage supervisor --dry-run`; confirm approved service files. - [ ] Run `ampg deploy apply --stage start --dry-run`; confirm service-manager commands. +- [ ] Run `ampg deploy apply --stage addresses --dry-run`; confirm captured transport addresses. - [ ] Run live apply after the preflight gate is ready. diff --git a/ampg/addresses.py b/ampg/addresses.py index 6ba929b..a454716 100644 --- a/ampg/addresses.py +++ b/ampg/addresses.py @@ -185,27 +185,41 @@ def _protocol_specific_configured_url(protocol: ProtocolConfig) -> Any: return None -def capture_addresses(config: GatewayConfig) -> list[AddressCaptureResult]: - records = load_address_registry(config) +def capture_address_results(config: GatewayConfig) -> list[AddressCaptureResult]: results: list[AddressCaptureResult] = [] - changed = False for site in config.sites: for protocol in site.protocols.values(): if not protocol.enabled: continue - result = _capture_protocol_address(config, site, protocol) - results.append(result) - if result.status == "captured": - records[(site.id, protocol.name)] = AddressRecord( - site_id=site.id, - protocol=protocol.name, - url=result.url, - address_status="captured", - source=result.source, - ) - changed = True + results.append(_capture_protocol_address(config, site, protocol)) + return results + + +def write_captured_address_results( + config: GatewayConfig, + results: list[AddressCaptureResult], +) -> Path | None: + records = load_address_registry(config) + changed = False + for result in results: + if result.status != "captured": + continue + records[(result.site_id, result.protocol)] = AddressRecord( + site_id=result.site_id, + protocol=result.protocol, + url=result.url, + address_status="captured", + source=result.source, + ) + changed = True if changed: - write_address_registry(config, records) + return write_address_registry(config, records) + return None + + +def capture_addresses(config: GatewayConfig) -> list[AddressCaptureResult]: + results = capture_address_results(config) + write_captured_address_results(config, results) return results diff --git a/ampg/apply.py b/ampg/apply.py index 4517b88..b81e5c6 100644 --- a/ampg/apply.py +++ b/ampg/apply.py @@ -8,6 +8,11 @@ import shutil import shlex import subprocess +from .addresses import ( + AddressCaptureResult, + capture_address_results, + write_captured_address_results, +) from .config import GatewayConfig from .install_plan import ( InstallStateCopy, @@ -70,6 +75,18 @@ class StartApplyResult: message: str +@dataclass(frozen=True) +class AddressApplyResult: + site_id: str + protocol: str + status: str + mode: str + url: str + source: str + path: Path | None + message: str + + CommandRunner = Callable[[tuple[str, ...]], CommandRunResult] @@ -253,6 +270,17 @@ def apply_start( return results +def apply_addresses( + config: GatewayConfig, + *, + dry_run: bool, +) -> list[AddressApplyResult]: + captures = capture_address_results(config) + if not dry_run: + write_captured_address_results(config, captures) + return [_address_result(result, dry_run=dry_run) for result in captures] + + def _validate_state_copy( config: GatewayConfig, copy: InstallStateCopy, @@ -452,6 +480,36 @@ def _validate_start_action( ) +def _address_result( + result: AddressCaptureResult, + *, + dry_run: bool, +) -> AddressApplyResult: + if result.status == "captured": + status = "planned" if dry_run else "written" + message = ( + "would record captured public transport address" + if dry_run + else "recorded captured public transport address" + ) + elif result.status == "missing": + status = "blocked" + message = result.message + else: + status = "skipped" + message = result.message + return AddressApplyResult( + site_id=result.site_id, + protocol=result.protocol, + status=status, + mode="dry-run" if dry_run else "live", + url=result.url, + source=result.source, + path=result.path, + message=message, + ) + + def _result( copy: InstallStateCopy, *, diff --git a/ampg/cli.py b/ampg/cli.py index 5ef7791..a1407d7 100644 --- a/ampg/cli.py +++ b/ampg/cli.py @@ -22,9 +22,11 @@ from .addresses import ( set_address, ) from .apply import ( + AddressApplyResult, StateApplyResult, StartApplyResult, SupervisorApplyResult, + apply_addresses, apply_state, apply_start, apply_supervisor, @@ -160,7 +162,7 @@ def main(argv: list[str] | None = None) -> int: _add_target_selection(deploy_apply_parser) deploy_apply_parser.add_argument( "--stage", - choices=("state", "supervisor", "start"), + choices=("state", "supervisor", "start", "addresses"), required=True, help="Deployment stage to apply.", ) @@ -642,6 +644,26 @@ def _cmd_deploy(config, args) -> int: f"blocked={len(blocked)}" ) return 1 if blocked else 0 + if args.stage == "addresses": + results = apply_addresses(config, dry_run=args.dry_run) + for result in results: + _print_address_apply_result(result) + blocked = [result for result in results if result.status == "blocked"] + written = [result for result in results if result.status == "written"] + planned = [result for result in results if result.status == "planned"] + skipped = [result for result in results if result.status == "skipped"] + print( + "AMPG_DEPLOY_APPLY_SUMMARY " + f"stage=addresses " + f"mode={'dry-run' if args.dry_run else 'live'} " + f"results={len(results)} " + f"planned={len(planned)} " + f"written={len(written)} " + f"skipped={len(skipped)} " + f"blocked={len(blocked)} " + f"registry=\"{_quote(str(address_registry_path(config)))}\"" + ) + return 1 if blocked else 0 return 1 @@ -1165,6 +1187,21 @@ def _print_start_apply_result(result: StartApplyResult) -> None: ) +def _print_address_apply_result(result: AddressApplyResult) -> None: + path = str(result.path) if result.path else "-" + print( + "AMPG_DEPLOY_ADDRESS " + f"site={result.site_id} " + f"protocol={result.protocol} " + f"mode={result.mode} " + f"status={result.status} " + f"url=\"{_quote(result.url)}\" " + f"source=\"{_quote(result.source)}\" " + f"path=\"{_quote(path)}\" " + f"message=\"{_quote(result.message)}\"" + ) + + def _print_dns_record(record: DNSRecordPlan) -> None: print( "AMPG_DNS_RECORD " diff --git a/ampg/deploy.py b/ampg/deploy.py index 0ef1df5..698c694 100644 --- a/ampg/deploy.py +++ b/ampg/deploy.py @@ -82,7 +82,6 @@ def deploy_plan( daemon_probe=daemon_probe, ), _artifact_step(config, command_context, platform_provider=platform_provider), - _address_step(config, command_context), _apply_preflight_step( config, command_context, @@ -95,6 +94,7 @@ def deploy_plan( platform_provider=platform_provider, daemon_probe=daemon_probe, ), + _address_step(config, command_context), ) status = _overall_status(steps) return DeployPlan( @@ -273,7 +273,7 @@ def _address_step(config: GatewayConfig, commands: _CommandContext) -> DeploySte return DeployStep( "addresses", "review", - commands.command("addresses capture"), + commands.command("deploy apply --stage addresses --dry-run"), f"capture or set generated transport address(es): {names}", ) return DeployStep("addresses", "ready", commands.command("addresses list"), "addresses are known") diff --git a/docs/architecture.md b/docs/architecture.md index 205da36..42cb5f8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -142,6 +142,10 @@ after the state files they reference exist. It does not start or reload services services after state and supervisor files exist. Address capture and health verification remain separate stages. +`ampg deploy apply --stage addresses` records daemon-written public addresses after the +transport has produced them. It writes only the AMPG address registry; health +verification remains a separate stage. + The dry-run command is intentionally stricter than `doctor`: missing generated output is blocked during activation, because AMPG must not point a transport at an unbuilt output root. diff --git a/docs/daemon-management.md b/docs/daemon-management.md index f27bead..e4236f8 100644 --- a/docs/daemon-management.md +++ b/docs/daemon-management.md @@ -62,6 +62,8 @@ python3 -m ampg --config gateway.toml deploy apply --stage supervisor --dry-run python3 -m ampg --config gateway.toml deploy apply --stage supervisor --profile mobile-i2p --yes python3 -m ampg --config gateway.toml deploy apply --stage start --dry-run --profile mobile-i2p python3 -m ampg --config gateway.toml deploy apply --stage start --profile mobile-i2p --yes +python3 -m ampg --config gateway.toml deploy apply --stage addresses --dry-run --profile mobile-i2p +python3 -m ampg --config gateway.toml deploy apply --stage addresses --profile mobile-i2p --yes python3 -m ampg --config gateway.toml doctor --platform android-termux ``` @@ -99,9 +101,9 @@ daemon-written addresses under `gateway.state_dir`. which paths are AMPG-owned, daemon-written, required, or sensitive. `addresses list` prints the effective public address for each enabled protocol: -configured, captured, derived, or placeholder. `addresses capture` reads daemon-written -address files from AMPG state or a protocol `address_file` override, then updates the -address registry under `state_dir`. +configured, captured, derived, or placeholder. `deploy apply --stage addresses` and +`addresses capture` read daemon-written address files from AMPG state or a protocol +`address_file` override, then update the address registry under `state_dir`. `health-plan` prints fixture-based checks to run after services start. Published checks target real transport URLs; preview checks target local loopback preview endpoints. @@ -139,6 +141,11 @@ requires both state and supervisor files to exist, and runs only structured plat commands for AMPG-named services. It does not install packages, rewrite config, remove files, capture addresses, or run health checks. +`deploy apply --stage addresses` is the fourth live apply stage. Dry-run mode prints +`AMPG_DEPLOY_ADDRESS` rows for configured, skipped, missing, and capturable addresses. +Live mode requires `--yes` and writes captured transport addresses under +`gateway.state_dir`. It does not rewrite daemon config or run health checks. + `--protocol` scopes operational commands to selected enabled protocols. A clearnet `adopt` failure will block a full activation run, but it will not block `apply --dry-run --protocol i2p` because clearnet is outside that selected run. diff --git a/docs/fixture-manifests.md b/docs/fixture-manifests.md index 6bddf38..0239472 100644 --- a/docs/fixture-manifests.md +++ b/docs/fixture-manifests.md @@ -33,6 +33,7 @@ or deployment notes. python3 -m ampg --config examples/wownero.gateway.toml build python3 -m ampg --config examples/wownero.gateway.toml manifest python3 -m ampg --config examples/wownero.gateway.toml addresses list +python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage addresses --dry-run --profile mobile-i2p python3 -m ampg --config examples/wownero.gateway.toml addresses capture --profile mobile-i2p python3 -m ampg --config examples/wownero.gateway.toml preview manifest python3 -m ampg --config examples/wownero.gateway.toml health-plan @@ -62,10 +63,11 @@ Some transports may not have final public addresses when a fixture is generated. cases AMPG uses route-valid placeholders such as `http://wownero.onion/` so AMPB can still verify transport selection without contacting the network. -When a managed or adopted daemon writes an actual address, run `addresses capture`. -Captured addresses are stored under `gateway.state_dir` and replace placeholders in -fixture manifests and published health plans. Operators can also use `addresses set` to -store an address manually. +When a managed or adopted daemon writes an actual address, run +`deploy apply --stage addresses --dry-run` before recording it. Captured addresses are +stored under `gateway.state_dir` and replace placeholders in fixture manifests and +published health plans. Operators can also use `addresses capture` for a direct write or +`addresses set` to store an address manually. ## Interaction Policy diff --git a/docs/state-contract.md b/docs/state-contract.md index 6ae50d9..776d102 100644 --- a/docs/state-contract.md +++ b/docs/state-contract.md @@ -61,3 +61,7 @@ explicit stage. `deploy apply --stage start` runs the service-manager start commands for AMPG-named services only after required state and supervisor files exist. It does not write or delete state files. + +`deploy apply --stage addresses` reads daemon-written address files from the state +contract and writes captured public addresses to `addresses.json` under +`gateway.state_dir`. diff --git a/docs/wownero-integration.md b/docs/wownero-integration.md index d593177..cac6cff 100644 --- a/docs/wownero-integration.md +++ b/docs/wownero-integration.md @@ -141,8 +141,8 @@ daemon_policy = "auto" - `ampg build` writes a fixture manifest that AMPB can check. - `ampg preview manifest` writes loopback fixture URLs that AMPB can check before live transport daemons are installed or adopted. -- `ampg deploy apply --stage state`, `--stage supervisor`, and `--stage start` stage - Wownero transport services without mixing config writes and service start. +- `ampg deploy apply --stage state`, `--stage supervisor`, `--stage start`, and + `--stage addresses` stage Wownero transport publication without mixing concerns. - Tor output contains no `