mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 07:31:32 +02:00
Model transport provider sources
This commit is contained in:
@@ -1634,6 +1634,7 @@ def _print_transport_status(status: TransportStatus) -> None:
|
||||
f"supervisor={status.supervisor} "
|
||||
f"adapter={status.adapter} "
|
||||
f"backend={status.backend} "
|
||||
f"provider_source={status.provider_source} "
|
||||
f"installed={_bool(status.installed)} "
|
||||
f"running={_bool(status.running)} "
|
||||
f"adoptable={_bool(status.adoptable)} "
|
||||
|
||||
@@ -73,6 +73,10 @@ def _daemon_adapters_doc() -> str:
|
||||
sections.append(f"- Protocols: {', '.join(adapter.protocols)}")
|
||||
sections.append(f"- Default policy: `{adapter.default_policy}`")
|
||||
sections.append(f"- Generated artifacts: {', '.join(adapter.generated_artifacts)}")
|
||||
sections.append(
|
||||
"- Provider sources: "
|
||||
+ (", ".join(f"`{source}`" for source in adapter.provider_sources) or "-")
|
||||
)
|
||||
sections.append("")
|
||||
sections.append("Notes:")
|
||||
for note in adapter.notes:
|
||||
|
||||
@@ -31,6 +31,7 @@ class DaemonAdapter:
|
||||
default_policy: str
|
||||
generated_artifacts: tuple[str, ...]
|
||||
notes: tuple[str, ...]
|
||||
provider_sources: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -512,6 +513,7 @@ DAEMON_ADAPTERS = (
|
||||
"Clearnet defaults to adopt because TLS and public vhost policy are operator-owned.",
|
||||
"Tor/I2P HTTP targets can use loopback-only server blocks.",
|
||||
),
|
||||
provider_sources=("configured", "system-adopted", "system-managed", "platform-package"),
|
||||
),
|
||||
DaemonAdapter(
|
||||
daemon=DEFAULT_DAEMONS["tor"],
|
||||
@@ -522,6 +524,7 @@ DAEMON_ADAPTERS = (
|
||||
"AMPG should preserve existing HiddenServiceDir material when adopting.",
|
||||
"Managed hidden-service keys belong under AMPG state.",
|
||||
),
|
||||
provider_sources=("configured", "bundled-sidecar", "system-adopted", "system-managed", "platform-package"),
|
||||
),
|
||||
DaemonAdapter(
|
||||
daemon=DEFAULT_DAEMONS["i2p"],
|
||||
@@ -532,6 +535,7 @@ DAEMON_ADAPTERS = (
|
||||
"Web tunnels should use web-specific key files.",
|
||||
"Existing RPC/P2P tunnel keys are not reused for web publishing.",
|
||||
),
|
||||
provider_sources=("configured", "bundled-sidecar", "system-adopted", "system-managed", "platform-package"),
|
||||
),
|
||||
DaemonAdapter(
|
||||
daemon=DEFAULT_DAEMONS["gemini"],
|
||||
@@ -542,6 +546,7 @@ DAEMON_ADAPTERS = (
|
||||
"Gemini serves generated Gemtext output directly.",
|
||||
"Certificate and key paths are plan values until install/apply support exists.",
|
||||
),
|
||||
provider_sources=("configured", "bundled-sidecar", "system-adopted", "system-managed", "platform-package"),
|
||||
),
|
||||
DaemonAdapter(
|
||||
daemon=DEFAULT_DAEMONS["ipfs"],
|
||||
@@ -552,6 +557,7 @@ DAEMON_ADAPTERS = (
|
||||
"IPFS output is static web content for a local gateway or later pinning.",
|
||||
"IPFS is content-addressed distribution, not an anonymity layer.",
|
||||
),
|
||||
provider_sources=("configured", "bundled-sidecar", "system-adopted", "system-managed", "platform-package"),
|
||||
),
|
||||
DaemonAdapter(
|
||||
daemon=DEFAULT_DAEMONS["reticulum"],
|
||||
@@ -563,5 +569,13 @@ DAEMON_ADAPTERS = (
|
||||
"Reticulum is resilient/private routing, not an anonymity layer.",
|
||||
"Physical interfaces may require explicit operator setup.",
|
||||
),
|
||||
provider_sources=(
|
||||
"configured",
|
||||
"bundled-sidecar",
|
||||
"system-adopted",
|
||||
"system-managed",
|
||||
"platform-package",
|
||||
"operator-interface",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -40,6 +40,7 @@ class TransportStatus:
|
||||
supervisor: str
|
||||
adapter: str
|
||||
backend: str
|
||||
provider_source: str
|
||||
installed: bool
|
||||
running: bool
|
||||
adoptable: bool
|
||||
@@ -265,6 +266,11 @@ def _transport_status(
|
||||
supervisor=provider.process_supervisor,
|
||||
adapter=f"{adapter.protocol}/{adapter.daemon}",
|
||||
backend=adapter.backend,
|
||||
provider_source=_provider_source_for_status(
|
||||
action=action,
|
||||
installed=daemon.installed,
|
||||
executable_path=daemon.executable_path,
|
||||
),
|
||||
installed=daemon.installed,
|
||||
running=daemon.running,
|
||||
adoptable=adoptable,
|
||||
@@ -276,6 +282,18 @@ def _transport_status(
|
||||
)
|
||||
|
||||
|
||||
def _provider_source_for_status(*, action: str, installed: bool, executable_path: str | None) -> str:
|
||||
if action == "adopt-existing":
|
||||
return "system-adopted"
|
||||
if action == "manage-owned":
|
||||
if installed or executable_path:
|
||||
return "system-managed"
|
||||
return "platform-package"
|
||||
if action == "render-only":
|
||||
return "external"
|
||||
return "unavailable"
|
||||
|
||||
|
||||
def _process_running(executable: str) -> bool:
|
||||
pgrep = shutil.which("pgrep")
|
||||
if not pgrep:
|
||||
|
||||
@@ -3,6 +3,14 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProviderSource:
|
||||
kind: str
|
||||
discovery: str
|
||||
lifecycle: str
|
||||
notes: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TransportAdapter:
|
||||
protocol: str
|
||||
@@ -13,9 +21,48 @@ class TransportAdapter:
|
||||
managed_supported: bool
|
||||
default_ports: tuple[int, ...] = ()
|
||||
generated_artifacts: tuple[str, ...] = ()
|
||||
provider_sources: tuple[ProviderSource, ...] = ()
|
||||
notes: tuple[str, ...] = ()
|
||||
|
||||
|
||||
COMMON_SERVICE_PROVIDER_SOURCES = (
|
||||
ProviderSource(
|
||||
kind="configured",
|
||||
discovery="explicit daemon config and approved generated snippets",
|
||||
lifecycle="operator-owned unless daemon_policy selects manage",
|
||||
notes=("Explicit config always wins over automatic discovery.",),
|
||||
),
|
||||
ProviderSource(
|
||||
kind="system-adopted",
|
||||
discovery="running system daemon, service, or listen endpoint",
|
||||
lifecycle="operator-owned; AMPG may generate snippets but does not own the service",
|
||||
notes=("Used when daemon_policy is adopt or auto with a healthy daemon.",),
|
||||
),
|
||||
ProviderSource(
|
||||
kind="system-managed",
|
||||
discovery="installed daemon binary on the selected platform",
|
||||
lifecycle="AMPG-owned state and AMPG-named supervisor entry",
|
||||
notes=("Used when daemon_policy is manage or auto and the platform can supervise it.",),
|
||||
),
|
||||
ProviderSource(
|
||||
kind="platform-package",
|
||||
discovery="allowlisted package backend such as apt, brew, or pkg",
|
||||
lifecycle="installed only through reviewed deploy apply package stage",
|
||||
notes=("Package install is separate from state, supervisor, start, address, and health stages.",),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
USERSPACE_PROVIDER_SOURCES = COMMON_SERVICE_PROVIDER_SOURCES + (
|
||||
ProviderSource(
|
||||
kind="bundled-sidecar",
|
||||
discovery="AMPG package or app resource provider directory",
|
||||
lifecycle="AMPG-owned process and state",
|
||||
notes=("Useful for old laptops, single-purpose boxes, and mobile-server shells.",),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
TRANSPORT_ADAPTERS = (
|
||||
TransportAdapter(
|
||||
protocol="clearnet",
|
||||
@@ -26,6 +73,7 @@ TRANSPORT_ADAPTERS = (
|
||||
managed_supported=True,
|
||||
default_ports=(80, 443),
|
||||
generated_artifacts=("nginx server block snippet",),
|
||||
provider_sources=COMMON_SERVICE_PROVIDER_SOURCES,
|
||||
notes=(
|
||||
"Clearnet ingress is usually adopted because TLS and public vhost policy are operator-owned.",
|
||||
),
|
||||
@@ -39,6 +87,7 @@ TRANSPORT_ADAPTERS = (
|
||||
managed_supported=True,
|
||||
default_ports=(18080,),
|
||||
generated_artifacts=("torrc hidden-service snippet", "loopback HTTP server block"),
|
||||
provider_sources=USERSPACE_PROVIDER_SOURCES,
|
||||
notes=(
|
||||
"Adopted Tor keeps existing HiddenServiceDir material.",
|
||||
"Managed Tor stores onion service keys under AMPG state.",
|
||||
@@ -53,6 +102,18 @@ TRANSPORT_ADAPTERS = (
|
||||
managed_supported=False,
|
||||
default_ports=(18080,),
|
||||
generated_artifacts=("future Arti onion-service config",),
|
||||
provider_sources=(
|
||||
ProviderSource(
|
||||
kind="bundled-sidecar",
|
||||
discovery="AMPG package or app resource provider directory",
|
||||
lifecycle="future AMPG-owned Arti onion-service process",
|
||||
),
|
||||
ProviderSource(
|
||||
kind="configured",
|
||||
discovery="explicit arti binary path and onion-service config",
|
||||
lifecycle="future AMPG-owned or operator-owned by policy",
|
||||
),
|
||||
),
|
||||
notes=("Modeled as a future backend until the AMPG Arti adapter is implemented.",),
|
||||
),
|
||||
TransportAdapter(
|
||||
@@ -64,6 +125,7 @@ TRANSPORT_ADAPTERS = (
|
||||
managed_supported=True,
|
||||
default_ports=(18081,),
|
||||
generated_artifacts=("i2pd server tunnel snippet", "loopback HTTP server block"),
|
||||
provider_sources=USERSPACE_PROVIDER_SOURCES,
|
||||
notes=("Destination keys are transport identity material and must be preserved.",),
|
||||
),
|
||||
TransportAdapter(
|
||||
@@ -75,6 +137,7 @@ TRANSPORT_ADAPTERS = (
|
||||
managed_supported=True,
|
||||
default_ports=(1965,),
|
||||
generated_artifacts=("Agate plan values",),
|
||||
provider_sources=USERSPACE_PROVIDER_SOURCES,
|
||||
notes=("Gemini serves generated Gemtext output directly.",),
|
||||
),
|
||||
TransportAdapter(
|
||||
@@ -86,6 +149,7 @@ TRANSPORT_ADAPTERS = (
|
||||
managed_supported=True,
|
||||
default_ports=(5001, 8080),
|
||||
generated_artifacts=("pin/add plan",),
|
||||
provider_sources=USERSPACE_PROVIDER_SOURCES,
|
||||
notes=("IPFS is content-addressed distribution, not an anonymity layer.",),
|
||||
),
|
||||
TransportAdapter(
|
||||
@@ -97,6 +161,15 @@ TRANSPORT_ADAPTERS = (
|
||||
managed_supported=True,
|
||||
default_ports=(),
|
||||
generated_artifacts=("Reticulum page-service plan",),
|
||||
provider_sources=USERSPACE_PROVIDER_SOURCES
|
||||
+ (
|
||||
ProviderSource(
|
||||
kind="operator-interface",
|
||||
discovery="explicit Reticulum interface configuration",
|
||||
lifecycle="operator-owned physical or link-layer interface",
|
||||
notes=("AMPG can manage page serving while interface setup may remain manual.",),
|
||||
),
|
||||
),
|
||||
notes=(
|
||||
"Reticulum is resilient/private routing, not an anonymity layer.",
|
||||
"Physical interfaces may require operator setup outside AMPG.",
|
||||
|
||||
@@ -22,17 +22,36 @@ Managed daemons:
|
||||
- use AMPG-owned ports, sockets, hidden-service dirs, key files, and output roots.
|
||||
- can be stopped, restarted, or removed by AMPG.
|
||||
|
||||
## Provider model
|
||||
|
||||
A transport provider is the implementation AMPG uses to publish one protocol target.
|
||||
Every provider must resolve to a local endpoint, generated config, managed state, and a
|
||||
health check.
|
||||
|
||||
Provider sources:
|
||||
|
||||
- `configured`: explicit config from `gateway.toml` or approved generated snippets.
|
||||
- `bundled-sidecar`: a provider shipped with AMPG or an app bundle and run from AMPG
|
||||
state.
|
||||
- `system-adopted`: a healthy daemon that AMPG did not start and does not own.
|
||||
- `system-managed`: an installed daemon binary supervised through AMPG-owned state.
|
||||
- `platform-package`: a daemon package installed by an allowlisted apply stage.
|
||||
- `operator-interface`: physical or link-layer setup that remains operator-owned.
|
||||
- `external`: render outputs and review artifacts only.
|
||||
|
||||
## Discovery order
|
||||
|
||||
For each enabled protocol, AMPG checks:
|
||||
|
||||
1. Explicit config in `gateway.toml`.
|
||||
2. AMPG-owned state from previous runs.
|
||||
3. Common system services and ports.
|
||||
4. Known config paths for supported daemons.
|
||||
2. Bundled AMPG providers for the current platform.
|
||||
3. AMPG-owned state from previous runs.
|
||||
4. Common system services and ports.
|
||||
5. Known config paths for supported daemons.
|
||||
6. Platform package backends for managed install stages.
|
||||
|
||||
The result is shown in `ampg status`. `ampg plan` shows rendered outputs and reviewable
|
||||
config artifacts.
|
||||
The result is shown in `ampg status` as `provider_source`. `ampg plan` shows rendered
|
||||
outputs and reviewable config artifacts.
|
||||
|
||||
## Preflight commands
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ This file is generated from code. Do not edit it by hand.
|
||||
- Protocols: clearnet, tor, i2p
|
||||
- Default policy: `adopt`
|
||||
- Generated artifacts: server block snippets
|
||||
- Provider sources: `configured`, `system-adopted`, `system-managed`, `platform-package`
|
||||
|
||||
Notes:
|
||||
- Clearnet defaults to adopt because TLS and public vhost policy are operator-owned.
|
||||
@@ -20,6 +21,7 @@ Notes:
|
||||
- Protocols: tor
|
||||
- Default policy: `auto`
|
||||
- Generated artifacts: torrc hidden-service snippet
|
||||
- Provider sources: `configured`, `bundled-sidecar`, `system-adopted`, `system-managed`, `platform-package`
|
||||
|
||||
Notes:
|
||||
- AMPG should preserve existing HiddenServiceDir material when adopting.
|
||||
@@ -30,6 +32,7 @@ Notes:
|
||||
- Protocols: i2p
|
||||
- Default policy: `auto`
|
||||
- Generated artifacts: i2pd server tunnel snippet
|
||||
- Provider sources: `configured`, `bundled-sidecar`, `system-adopted`, `system-managed`, `platform-package`
|
||||
|
||||
Notes:
|
||||
- Web tunnels should use web-specific key files.
|
||||
@@ -40,6 +43,7 @@ Notes:
|
||||
- Protocols: gemini
|
||||
- Default policy: `auto`
|
||||
- Generated artifacts: Agate plan values
|
||||
- Provider sources: `configured`, `bundled-sidecar`, `system-adopted`, `system-managed`, `platform-package`
|
||||
|
||||
Notes:
|
||||
- Gemini serves generated Gemtext output directly.
|
||||
@@ -50,6 +54,7 @@ Notes:
|
||||
- Protocols: ipfs
|
||||
- Default policy: `auto`
|
||||
- Generated artifacts: fixture manifest route expectations
|
||||
- Provider sources: `configured`, `bundled-sidecar`, `system-adopted`, `system-managed`, `platform-package`
|
||||
|
||||
Notes:
|
||||
- IPFS output is static web content for a local gateway or later pinning.
|
||||
@@ -60,6 +65,7 @@ Notes:
|
||||
- Protocols: reticulum
|
||||
- Default policy: `auto`
|
||||
- Generated artifacts: Reticulum page-service plan
|
||||
- Provider sources: `configured`, `bundled-sidecar`, `system-adopted`, `system-managed`, `platform-package`, `operator-interface`
|
||||
|
||||
Notes:
|
||||
- Reticulum output is planned as small page-service content.
|
||||
|
||||
@@ -3,6 +3,7 @@ import io
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from ampg.approvals import ApprovalInput, approve_artifacts
|
||||
from ampg.cli import main
|
||||
@@ -170,7 +171,7 @@ daemon_policy = "auto"
|
||||
)
|
||||
|
||||
stdout = io.StringIO()
|
||||
with contextlib.redirect_stdout(stdout):
|
||||
with contextlib.redirect_stdout(stdout), patch("ampg.status.shutil.which", return_value=None):
|
||||
status = main(
|
||||
[
|
||||
"--config",
|
||||
|
||||
@@ -3,6 +3,7 @@ import io
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from ampg.cli import main
|
||||
from ampg.config import load_config
|
||||
@@ -149,7 +150,7 @@ class ProtocolSelectionTest(unittest.TestCase):
|
||||
(output / ".ampg-output").write_text("generated by AMPG\n", encoding="utf-8")
|
||||
|
||||
stdout = io.StringIO()
|
||||
with contextlib.redirect_stdout(stdout):
|
||||
with contextlib.redirect_stdout(stdout), patch("ampg.status.shutil.which", return_value=None):
|
||||
status = main(
|
||||
[
|
||||
"--config",
|
||||
@@ -179,7 +180,7 @@ class ProtocolSelectionTest(unittest.TestCase):
|
||||
(output / ".ampg-output").write_text("generated by AMPG\n", encoding="utf-8")
|
||||
|
||||
stdout = io.StringIO()
|
||||
with contextlib.redirect_stdout(stdout):
|
||||
with contextlib.redirect_stdout(stdout), patch("ampg.status.shutil.which", return_value=None):
|
||||
status = main(
|
||||
[
|
||||
"--config",
|
||||
|
||||
@@ -74,6 +74,7 @@ daemon_policy = "auto"
|
||||
self.assertEqual(1, len(statuses))
|
||||
self.assertEqual("ok", statuses[0].status)
|
||||
self.assertEqual("adopt-existing", statuses[0].action)
|
||||
self.assertEqual("system-adopted", statuses[0].provider_source)
|
||||
self.assertTrue(statuses[0].adoptable)
|
||||
|
||||
def test_auto_policy_manages_missing_daemon_on_manageable_platform(self):
|
||||
@@ -98,6 +99,7 @@ daemon_policy = "auto"
|
||||
|
||||
self.assertEqual("ok", statuses[0].status)
|
||||
self.assertEqual("manage-owned", statuses[0].action)
|
||||
self.assertEqual("platform-package", statuses[0].provider_source)
|
||||
self.assertTrue(statuses[0].manageable)
|
||||
|
||||
def test_adopt_policy_errors_when_daemon_is_missing(self):
|
||||
@@ -122,6 +124,7 @@ daemon_policy = "adopt"
|
||||
|
||||
self.assertEqual("error", statuses[0].status)
|
||||
self.assertEqual("unavailable", statuses[0].action)
|
||||
self.assertEqual("unavailable", statuses[0].provider_source)
|
||||
self.assertIn("nginx is not installed", statuses[0].message)
|
||||
|
||||
def test_unknown_adapter_errors_unless_external(self):
|
||||
|
||||
Reference in New Issue
Block a user