refactor(supervise): split the supervise plane by tier; per-service store managers
test / integration-docker (pull_request) Successful in 13s
tracker-policy-pr / check-pr (pull_request) Successful in 14s
test / unit (pull_request) Successful in 42s
lint / lint (push) Failing after 54s
test / integration-firecracker (pull_request) Successful in 3m17s
test / coverage (pull_request) Successful in 17s
test / publish-infra (pull_request) Has been skipped
test / integration-docker (pull_request) Successful in 13s
tracker-policy-pr / check-pr (pull_request) Successful in 14s
test / unit (pull_request) Successful in 42s
lint / lint (push) Failing after 54s
test / integration-firecracker (pull_request) Successful in 3m17s
test / coverage (pull_request) Successful in 17s
test / publish-infra (pull_request) Has been skipped
Give each service its own store package + manager, and cut the supervise module
along the control/data-plane boundary so nothing in the shared layer reaches up
into the orchestrator.
Stores, by owner:
- bot_bottle/store/ keeps only the shared base (DbStore, migrations) and the
concrete stores that aren't service-owned (audit_store, config_store).
- bot_bottle/orchestrator/store/ now houses the orchestrator-owned stores —
queue_store (supervise queue), secret_store, config_store — plus a new
orchestrator store_manager that migrates them (composing audit/config
downward from the base). The old shared store_manager is gone.
Supervise plane, by tier:
- bot_bottle/supervisor/ (NEUTRAL, importable by every tier including the
gateway): types.py (the Proposal/Response/AuditEntry wire types + the tool/
status/poll constants + the shared daemon constants moved out of
supervise.py) and plan.py (SupervisePlan, a pure DTO).
- bot_bottle/orchestrator/supervisor/ (orchestrator-only): queue.py (the
queue/audit I/O wrappers + render_diff + sha256_hex) and supervise.py (the
Supervise lifecycle that stages the DB via the store manager). Its __init__
re-exports the neutral vocabulary so orchestrator-side callers import from
one place.
The gateway now imports only bot_bottle.supervisor.types (never
bot_bottle.supervise), so the data plane holds no code dependency on the
orchestrator — it reaches the queue over the control-plane RPC. This removes
the circular import that moving queue_store under orchestrator introduced
(supervise -> orchestrator -> service -> supervise).
supervise_types.py -> supervisor/types.py; supervise.py deleted (split). Full
unit suite green (2251).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -48,7 +48,7 @@ from ..git_gate import GitGatePlan
|
|||||||
from ..log import die, info, warn
|
from ..log import die, info, warn
|
||||||
from ..util import read_tty_line
|
from ..util import read_tty_line
|
||||||
from ..manifest import Manifest, ManifestIndex
|
from ..manifest import Manifest, ManifestIndex
|
||||||
from ..supervise import SupervisePlan
|
from ..supervisor.plan import SupervisePlan
|
||||||
from ..util import expand_tilde
|
from ..util import expand_tilde
|
||||||
from ..env import resolve_env, ResolvedEnv
|
from ..env import resolve_env, ResolvedEnv
|
||||||
from ..workspace import WorkspacePlan, workspace_plan
|
from ..workspace import WorkspacePlan, workspace_plan
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from ..egress import EgressPlan
|
|||||||
from ..git_gate import GitGatePlan
|
from ..git_gate import GitGatePlan
|
||||||
from ..orchestrator.client import OrchestratorClient, RegisteredBottle
|
from ..orchestrator.client import OrchestratorClient, RegisteredBottle
|
||||||
from ..orchestrator.registration import registration_inputs
|
from ..orchestrator.registration import registration_inputs
|
||||||
from ..orchestrator.secret_store import new_env_var_secret
|
from ..orchestrator.store.secret_store import new_env_var_secret
|
||||||
from .docker.gateway_provision import GatewayTransport, deprovision_git_gate, provision_git_gate
|
from .docker.gateway_provision import GatewayTransport, deprovision_git_gate, provision_git_gate
|
||||||
|
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ def teardown_consolidated(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Deregister the bottle and remove its git-gate state. Both steps are
|
"""Deregister the bottle and remove its git-gate state. Both steps are
|
||||||
idempotent so this is safe from a cleanup trap."""
|
idempotent so this is safe from a cleanup trap."""
|
||||||
from ..orchestrator.config_store import DEFAULT_TEARDOWN_TIMEOUT_SECONDS
|
from ..orchestrator.store.config_store import DEFAULT_TEARDOWN_TIMEOUT_SECONDS
|
||||||
OrchestratorClient(
|
OrchestratorClient(
|
||||||
orchestrator_url,
|
orchestrator_url,
|
||||||
timeout=timeout if timeout is not None else DEFAULT_TEARDOWN_TIMEOUT_SECONDS,
|
timeout=timeout if timeout is not None else DEFAULT_TEARDOWN_TIMEOUT_SECONDS,
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ from contextlib import contextmanager
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Generator, Sequence
|
from typing import Generator, Sequence
|
||||||
|
|
||||||
from ...supervise import SUPERVISE_HOSTNAME, SUPERVISE_PORT
|
from ...supervisor.types import SUPERVISE_HOSTNAME, SUPERVISE_PORT
|
||||||
from ...agent_provider import AgentProvisionPlan
|
from ...agent_provider import AgentProvisionPlan
|
||||||
from ...egress import EgressPlan
|
from ...egress import EgressPlan
|
||||||
from ...env import ResolvedEnv
|
from ...env import ResolvedEnv
|
||||||
from ...git_gate import GitGatePlan
|
from ...git_gate import GitGatePlan
|
||||||
from ...supervise import SupervisePlan
|
from ...supervisor.plan import SupervisePlan
|
||||||
from ...manifest import Manifest
|
from ...manifest import Manifest
|
||||||
from .. import ActiveAgent, BottleBackend, BottleImages, BottleSpec
|
from .. import ActiveAgent, BottleBackend, BottleImages, BottleSpec
|
||||||
from . import cleanup as _cleanup
|
from . import cleanup as _cleanup
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from __future__ import annotations
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from ...egress import egress_agent_env_entries
|
from ...egress import egress_agent_env_entries
|
||||||
from ...orchestrator.secret_store import ENV_VAR_SECRET_NAME
|
from ...orchestrator.store.secret_store import ENV_VAR_SECRET_NAME
|
||||||
from ..util import AGENT_CA_BUNDLE, AGENT_CA_PATH
|
from ..util import AGENT_CA_BUNDLE, AGENT_CA_PATH
|
||||||
from .bottle_plan import DockerBottlePlan
|
from .bottle_plan import DockerBottlePlan
|
||||||
from .egress import EGRESS_PORT
|
from .egress import EGRESS_PORT
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from ...git_gate import GitGatePlan
|
|||||||
from ...orchestrator.client import OrchestratorClient
|
from ...orchestrator.client import OrchestratorClient
|
||||||
from ...gateway import GATEWAY_NETWORK
|
from ...gateway import GATEWAY_NETWORK
|
||||||
from ...orchestrator.lifecycle import INFRA_NAME, OrchestratorService
|
from ...orchestrator.lifecycle import INFRA_NAME, OrchestratorService
|
||||||
from ...orchestrator.secret_store import ENV_VAR_SECRET_NAME
|
from ...orchestrator.store.secret_store import ENV_VAR_SECRET_NAME
|
||||||
from ...orchestrator.reprovision import reprovision_bottles
|
from ...orchestrator.reprovision import reprovision_bottles
|
||||||
from ..consolidated_util import provision_bottle
|
from ..consolidated_util import provision_bottle
|
||||||
from ..consolidated_util import teardown_consolidated as _teardown_util
|
from ..consolidated_util import teardown_consolidated as _teardown_util
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ from .compose import (
|
|||||||
write_compose_file,
|
write_compose_file,
|
||||||
)
|
)
|
||||||
from .consolidated_compose import consolidated_agent_compose
|
from .consolidated_compose import consolidated_agent_compose
|
||||||
from ...orchestrator.config_store import resolve_teardown_timeout
|
from ...orchestrator.store.config_store import resolve_teardown_timeout
|
||||||
from .consolidated_launch import launch_consolidated, teardown_consolidated
|
from .consolidated_launch import launch_consolidated, teardown_consolidated
|
||||||
from ...orchestrator.lifecycle import INFRA_NAME
|
from ...orchestrator.lifecycle import INFRA_NAME
|
||||||
from .gateway import DockerGateway
|
from .gateway import DockerGateway
|
||||||
@@ -207,7 +207,7 @@ def launch(
|
|||||||
# spec, value only in the subprocess env so it is never written to disk.
|
# spec, value only in the subprocess env so it is never written to disk.
|
||||||
compose_env: dict[str, str] = {**os.environ, **plan.forwarded_env}
|
compose_env: dict[str, str] = {**os.environ, **plan.forwarded_env}
|
||||||
if plan.env_var_secret:
|
if plan.env_var_secret:
|
||||||
from ...orchestrator.secret_store import ENV_VAR_SECRET_NAME
|
from ...orchestrator.store.secret_store import ENV_VAR_SECRET_NAME
|
||||||
compose_env[ENV_VAR_SECRET_NAME] = plan.env_var_secret
|
compose_env[ENV_VAR_SECRET_NAME] = plan.env_var_secret
|
||||||
info(
|
info(
|
||||||
f"docker compose up -d (project {project}, agent on shared "
|
f"docker compose up -d (project {project}, agent on shared "
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from ...env import ResolvedEnv
|
|||||||
from ...agent_provider import AgentProvisionPlan
|
from ...agent_provider import AgentProvisionPlan
|
||||||
from ...egress import EgressPlan
|
from ...egress import EgressPlan
|
||||||
from ...manifest import Manifest
|
from ...manifest import Manifest
|
||||||
from ...supervise import SupervisePlan
|
from ...supervisor.plan import SupervisePlan
|
||||||
from ...git_gate import GitGatePlan
|
from ...git_gate import GitGatePlan
|
||||||
|
|
||||||
def preflight() -> None:
|
def preflight() -> None:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from ...egress import EgressPlan
|
|||||||
from ...env import ResolvedEnv
|
from ...env import ResolvedEnv
|
||||||
from ...git_gate import GitGatePlan
|
from ...git_gate import GitGatePlan
|
||||||
from ...manifest import Manifest
|
from ...manifest import Manifest
|
||||||
from ...supervise import SupervisePlan
|
from ...supervisor.plan import SupervisePlan
|
||||||
from .. import ActiveAgent, BottleBackend, BottleImages, BottleSpec
|
from .. import ActiveAgent, BottleBackend, BottleImages, BottleSpec
|
||||||
from . import cleanup as _cleanup
|
from . import cleanup as _cleanup
|
||||||
from . import enumerate as _enumerate
|
from . import enumerate as _enumerate
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ from ...orchestrator.lifecycle import (
|
|||||||
OrchestratorStartError, # re-exported so callers can catch it
|
OrchestratorStartError, # re-exported so callers can catch it
|
||||||
)
|
)
|
||||||
from ...orchestrator.reprovision import reprovision_bottles
|
from ...orchestrator.reprovision import reprovision_bottles
|
||||||
from ...orchestrator.secret_store import ENV_VAR_SECRET_NAME
|
from ...orchestrator.store.secret_store import ENV_VAR_SECRET_NAME
|
||||||
from ..consolidated_util import (
|
from ..consolidated_util import (
|
||||||
provision_bottle,
|
provision_bottle,
|
||||||
teardown_consolidated as _teardown_util,
|
teardown_consolidated as _teardown_util,
|
||||||
|
|||||||
@@ -48,14 +48,14 @@ from ...git_gate import (
|
|||||||
)
|
)
|
||||||
from ...image_cache import check_stale_path
|
from ...image_cache import check_stale_path
|
||||||
from ...log import die, info, warn
|
from ...log import die, info, warn
|
||||||
from ...supervise import SUPERVISE_PORT
|
from ...supervisor.types import SUPERVISE_PORT
|
||||||
from ..docker.egress import EGRESS_PORT
|
from ..docker.egress import EGRESS_PORT
|
||||||
from ..util import AGENT_CA_BUNDLE, AGENT_CA_PATH
|
from ..util import AGENT_CA_BUNDLE, AGENT_CA_PATH
|
||||||
from . import firecracker_vm, image_builder, isolation_probe, netpool, util
|
from . import firecracker_vm, image_builder, isolation_probe, netpool, util
|
||||||
from .bottle import FirecrackerBottle
|
from .bottle import FirecrackerBottle
|
||||||
from .bottle_plan import FirecrackerBottlePlan
|
from .bottle_plan import FirecrackerBottlePlan
|
||||||
from ...orchestrator.config_store import resolve_teardown_timeout
|
from ...orchestrator.store.config_store import resolve_teardown_timeout
|
||||||
from ...orchestrator.secret_store import ENV_VAR_SECRET_NAME
|
from ...orchestrator.store.secret_store import ENV_VAR_SECRET_NAME
|
||||||
from .consolidated_launch import (
|
from .consolidated_launch import (
|
||||||
launch_consolidated,
|
launch_consolidated,
|
||||||
persist_env_var_secret,
|
persist_env_var_secret,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from ...egress import EgressPlan
|
|||||||
from ...env import ResolvedEnv
|
from ...env import ResolvedEnv
|
||||||
from ...git_gate import GitGatePlan
|
from ...git_gate import GitGatePlan
|
||||||
from ...manifest import Manifest
|
from ...manifest import Manifest
|
||||||
from ...supervise import SupervisePlan
|
from ...supervisor.plan import SupervisePlan
|
||||||
from .. import BottleSpec
|
from .. import BottleSpec
|
||||||
from . import util
|
from . import util
|
||||||
from .bottle_plan import FirecrackerBottlePlan
|
from .bottle_plan import FirecrackerBottlePlan
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from ...agent_provider import AgentProvisionPlan
|
|||||||
from ...egress import EgressPlan
|
from ...egress import EgressPlan
|
||||||
from ...env import ResolvedEnv
|
from ...env import ResolvedEnv
|
||||||
from ...git_gate import GitGatePlan
|
from ...git_gate import GitGatePlan
|
||||||
from ...supervise import SupervisePlan
|
from ...supervisor.plan import SupervisePlan
|
||||||
from ...manifest import Manifest
|
from ...manifest import Manifest
|
||||||
from .. import ActiveAgent, BottleBackend, BottleImages, BottleSpec
|
from .. import ActiveAgent, BottleBackend, BottleImages, BottleSpec
|
||||||
from . import cleanup as _cleanup
|
from . import cleanup as _cleanup
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ from ...git_gate import GitGatePlan
|
|||||||
from ...log import info
|
from ...log import info
|
||||||
from ...orchestrator.client import OrchestratorClient, OrchestratorClientError
|
from ...orchestrator.client import OrchestratorClient, OrchestratorClientError
|
||||||
from ...orchestrator.reprovision import reprovision_bottles
|
from ...orchestrator.reprovision import reprovision_bottles
|
||||||
from ...orchestrator.secret_store import ENV_VAR_SECRET_NAME
|
from ...orchestrator.store.secret_store import ENV_VAR_SECRET_NAME
|
||||||
from ..consolidated_util import (
|
from ..consolidated_util import (
|
||||||
provision_bottle,
|
provision_bottle,
|
||||||
teardown_consolidated as _teardown_util,
|
teardown_consolidated as _teardown_util,
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ from ...gateway.git_http_backend import DEFAULT_PORT as _GIT_HTTP_PORT
|
|||||||
from ...image_cache import check_stale
|
from ...image_cache import check_stale
|
||||||
from ...log import die, info, warn
|
from ...log import die, info, warn
|
||||||
from .. import BottleImages
|
from .. import BottleImages
|
||||||
from ...supervise import SUPERVISE_PORT
|
from ...supervisor.types import SUPERVISE_PORT
|
||||||
from ..docker.egress import EGRESS_PORT
|
from ..docker.egress import EGRESS_PORT
|
||||||
from ..util import AGENT_CA_BUNDLE, AGENT_CA_PATH
|
from ..util import AGENT_CA_BUNDLE, AGENT_CA_PATH
|
||||||
from . import util as container_mod
|
from . import util as container_mod
|
||||||
@@ -68,8 +68,8 @@ from .gateway_hosts import (
|
|||||||
)
|
)
|
||||||
from . import nested_containers as nested_containers_mod
|
from . import nested_containers as nested_containers_mod
|
||||||
from .bottle_plan import MacosContainerBottlePlan
|
from .bottle_plan import MacosContainerBottlePlan
|
||||||
from ...orchestrator.config_store import resolve_teardown_timeout
|
from ...orchestrator.store.config_store import resolve_teardown_timeout
|
||||||
from ...orchestrator.secret_store import ENV_VAR_SECRET_NAME, new_env_var_secret
|
from ...orchestrator.store.secret_store import ENV_VAR_SECRET_NAME, new_env_var_secret
|
||||||
from .consolidated_launch import (
|
from .consolidated_launch import (
|
||||||
GatewayEndpoint,
|
GatewayEndpoint,
|
||||||
ensure_gateway,
|
ensure_gateway,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from ...agent_provider import AgentProvisionPlan
|
|||||||
from ...egress import EgressPlan
|
from ...egress import EgressPlan
|
||||||
from ...env import ResolvedEnv
|
from ...env import ResolvedEnv
|
||||||
from ...git_gate import GitGatePlan
|
from ...git_gate import GitGatePlan
|
||||||
from ...supervise import SupervisePlan
|
from ...supervisor.plan import SupervisePlan
|
||||||
from ...manifest import Manifest
|
from ...manifest import Manifest
|
||||||
from .. import BottleSpec
|
from .. import BottleSpec
|
||||||
from . import util as container_mod
|
from . import util as container_mod
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ from ..egress import Egress, EgressPlan
|
|||||||
from ..git_gate import GitGate, GitGatePlan
|
from ..git_gate import GitGate, GitGatePlan
|
||||||
from ..log import die
|
from ..log import die
|
||||||
from ..manifest import Manifest, ManifestBottle
|
from ..manifest import Manifest, ManifestBottle
|
||||||
from ..supervise import Supervise, SupervisePlan
|
from ..supervisor.plan import SupervisePlan
|
||||||
|
from ..orchestrator.supervisor.supervise import Supervise
|
||||||
from . import BottleSpec
|
from . import BottleSpec
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import sys
|
|||||||
from ..errors import MissingEnvVarError
|
from ..errors import MissingEnvVarError
|
||||||
from ..log import Die, die, error
|
from ..log import Die, die, error
|
||||||
from ..manifest import ManifestError
|
from ..manifest import ManifestError
|
||||||
from ..store.store_manager import StoreManager
|
from ..orchestrator.store.store_manager import StoreManager
|
||||||
from ._common import PROG
|
from ._common import PROG
|
||||||
from . import list as _list_mod
|
from . import list as _list_mod
|
||||||
from .backend import cmd_backend
|
from .backend import cmd_backend
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from ..orchestrator.client import (
|
|||||||
discover_orchestrator_url,
|
discover_orchestrator_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..supervise import (
|
from ..supervisor.types import (
|
||||||
Proposal,
|
Proposal,
|
||||||
TOOL_EGRESS_ALLOW,
|
TOOL_EGRESS_ALLOW,
|
||||||
TOOL_EGRESS_BLOCK,
|
TOOL_EGRESS_BLOCK,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ from bot_bottle.gateway.egress_addon_core import (
|
|||||||
scan_outbound,
|
scan_outbound,
|
||||||
)
|
)
|
||||||
from bot_bottle.gateway.policy_resolver import PolicyResolveError, PolicyResolver
|
from bot_bottle.gateway.policy_resolver import PolicyResolveError, PolicyResolver
|
||||||
from bot_bottle.supervise_types import (
|
from bot_bottle.supervisor.types import (
|
||||||
STATUS_APPROVED,
|
STATUS_APPROVED,
|
||||||
STATUS_MODIFIED,
|
STATUS_MODIFIED,
|
||||||
STATUSES,
|
STATUSES,
|
||||||
|
|||||||
@@ -283,10 +283,10 @@ from pathlib import Path
|
|||||||
# calling bottle exactly as a direct write once did.
|
# calling bottle exactly as a direct write once did.
|
||||||
try:
|
try:
|
||||||
from bot_bottle.gateway.policy_resolver import PolicyResolver, PolicyResolveError
|
from bot_bottle.gateway.policy_resolver import PolicyResolver, PolicyResolveError
|
||||||
from bot_bottle.supervise_types import TOOL_GITLEAKS_ALLOW
|
from bot_bottle.supervisor.types import TOOL_GITLEAKS_ALLOW
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from policy_resolver import PolicyResolver, PolicyResolveError
|
from policy_resolver import PolicyResolver, PolicyResolveError
|
||||||
from supervise_types import TOOL_GITLEAKS_ALLOW
|
from supervisor.types import TOOL_GITLEAKS_ALLOW
|
||||||
|
|
||||||
report_path = Path(sys.argv[1])
|
report_path = Path(sys.argv[1])
|
||||||
source_ip = os.environ.get("SUPERVISE_SOURCE_IP", "")
|
source_ip = os.environ.get("SUPERVISE_SOURCE_IP", "")
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ from bot_bottle.gateway.egress_addon_core import (
|
|||||||
LOG_OFF, load_config, resolve_client_context, route_to_yaml_dict,
|
LOG_OFF, load_config, resolve_client_context, route_to_yaml_dict,
|
||||||
)
|
)
|
||||||
from bot_bottle.gateway.policy_resolver import PolicyResolveError, PolicyResolver
|
from bot_bottle.gateway.policy_resolver import PolicyResolveError, PolicyResolver
|
||||||
from bot_bottle import supervise as _sv
|
from bot_bottle.supervisor import types as _sv
|
||||||
|
|
||||||
|
|
||||||
# --- JSON-RPC / MCP plumbing ----------------------------------------------
|
# --- JSON-RPC / MCP plumbing ----------------------------------------------
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import secrets
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .. import log
|
from .. import log
|
||||||
from ..store.store_manager import StoreManager
|
from .store.store_manager import StoreManager
|
||||||
from .broker import LaunchBroker, StubBroker
|
from .broker import LaunchBroker, StubBroker
|
||||||
from .control_plane import make_server
|
from .control_plane import make_server
|
||||||
from .docker_broker import DockerBroker
|
from .docker_broker import DockerBroker
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ from urllib.parse import urlsplit
|
|||||||
|
|
||||||
from ..control_auth import ROLE_CLI, ROLES, verify
|
from ..control_auth import ROLE_CLI, ROLES, verify
|
||||||
from ..paths import CONTROL_PLANE_TOKEN_ENV
|
from ..paths import CONTROL_PLANE_TOKEN_ENV
|
||||||
from ..supervise_types import TOOLS
|
from ..supervisor.types import TOOLS
|
||||||
from .service import Orchestrator
|
from .service import Orchestrator
|
||||||
|
|
||||||
# JSON body payload type (parsed request / rendered response).
|
# JSON body payload type (parsed request / rendered response).
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from datetime import datetime, timezone
|
|||||||
|
|
||||||
from .broker import LaunchBroker, LaunchRequest, sign_request
|
from .broker import LaunchBroker, LaunchRequest, sign_request
|
||||||
from .registry import DEFAULT_REAP_GRACE_SECONDS, BottleRecord, RegistryStore
|
from .registry import DEFAULT_REAP_GRACE_SECONDS, BottleRecord, RegistryStore
|
||||||
from ..supervise import (
|
from .supervisor import (
|
||||||
AuditEntry,
|
AuditEntry,
|
||||||
COMPONENT_FOR_TOOL,
|
COMPONENT_FOR_TOOL,
|
||||||
POLL_STATUS_PENDING,
|
POLL_STATUS_PENDING,
|
||||||
@@ -104,7 +104,7 @@ class Orchestrator:
|
|||||||
if tokens:
|
if tokens:
|
||||||
self._tokens[rec.bottle_id] = dict(tokens)
|
self._tokens[rec.bottle_id] = dict(tokens)
|
||||||
if env_var_secret:
|
if env_var_secret:
|
||||||
from .secret_store import encrypt_value
|
from .store.secret_store import encrypt_value
|
||||||
encrypted = {k: encrypt_value(env_var_secret, v) for k, v in tokens.items()}
|
encrypted = {k: encrypt_value(env_var_secret, v) for k, v in tokens.items()}
|
||||||
self.registry.store_agent_secrets(rec.bottle_id, encrypted)
|
self.registry.store_agent_secrets(rec.bottle_id, encrypted)
|
||||||
req = LaunchRequest(
|
req = LaunchRequest(
|
||||||
@@ -367,7 +367,7 @@ class Orchestrator:
|
|||||||
value with *env_var_secret*, and restores ``_tokens[bottle_id]``.
|
value with *env_var_secret*, and restores ``_tokens[bottle_id]``.
|
||||||
Returns True on success, False when no stored secrets exist for this
|
Returns True on success, False when no stored secrets exist for this
|
||||||
bottle or decryption fails (wrong key / corrupt data)."""
|
bottle or decryption fails (wrong key / corrupt data)."""
|
||||||
from .secret_store import decrypt_value
|
from .store.secret_store import decrypt_value
|
||||||
encrypted = self.registry.get_agent_secrets(bottle_id)
|
encrypted = self.registry.get_agent_secrets(bottle_id)
|
||||||
if not encrypted:
|
if not encrypted:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
"""Orchestrator-owned SQLite stores.
|
||||||
|
|
||||||
|
The stores whose tables only the orchestrator (control plane) opens: the
|
||||||
|
supervise proposal/response `queue_store`, the agent-secret `secret_store`, and
|
||||||
|
the orchestrator `config_store`. They build on the shared `DbStore` /
|
||||||
|
`migrations` base in `bot_bottle.store`.
|
||||||
|
|
||||||
|
Callers import the concrete module directly, e.g.
|
||||||
|
`from bot_bottle.orchestrator.store.queue_store import QueueStore`.
|
||||||
|
"""
|
||||||
+3
-3
@@ -11,9 +11,9 @@ import os
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ..store.db_store import DbStore
|
from ...store.db_store import DbStore
|
||||||
from ..store.migrations import TableMigrations
|
from ...store.migrations import TableMigrations
|
||||||
from ..paths import host_db_path
|
from ...paths import host_db_path
|
||||||
|
|
||||||
TEARDOWN_TIMEOUT_ENV = "BOT_BOTTLE_ORCHESTRATOR_TEARDOWN_TIMEOUT_SECONDS"
|
TEARDOWN_TIMEOUT_ENV = "BOT_BOTTLE_ORCHESTRATOR_TEARDOWN_TIMEOUT_SECONDS"
|
||||||
DEFAULT_TEARDOWN_TIMEOUT_SECONDS = 30.0
|
DEFAULT_TEARDOWN_TIMEOUT_SECONDS = 30.0
|
||||||
@@ -7,12 +7,12 @@ import sqlite3
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ..supervise_types import Proposal, Response
|
from ...supervisor.types import Proposal, Response
|
||||||
from ..paths import host_db_path
|
from ...paths import host_db_path
|
||||||
from .db_store import DbStore
|
from ...store.db_store import DbStore
|
||||||
from .migrations import TableMigrations
|
from ...store.migrations import TableMigrations
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from supervise_types import Proposal, Response # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
from supervisor.types import Proposal, Response # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
||||||
from paths import host_db_path # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
from paths import host_db_path # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
||||||
from db_store import DbStore # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
from db_store import DbStore # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
||||||
from migrations import TableMigrations # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
from migrations import TableMigrations # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
||||||
+16
-16
@@ -1,34 +1,34 @@
|
|||||||
"""Singleton manager for all bot-bottle SQLite stores (PRD 0013)."""
|
"""The orchestrator's store manager (PRD 0013 / 0070).
|
||||||
|
|
||||||
|
One store manager per service. Post-PRD-0070 the orchestrator is the sole
|
||||||
|
opener of `bot-bottle.db`, so it owns migrating every table in it: the supervise
|
||||||
|
`queue_store` (local to `orchestrator.store`) plus the `audit_store` and
|
||||||
|
`config_store` from the shared `bot_bottle.store` base. The data plane never
|
||||||
|
touches these — it reaches state over the control-plane RPC.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
try:
|
from .queue_store import QueueStore
|
||||||
from .audit_store import AuditStore
|
from ...store.audit_store import AuditStore
|
||||||
from .config_store import ConfigStore
|
from ...store.config_store import ConfigStore
|
||||||
from .queue_store import QueueStore
|
|
||||||
except ImportError:
|
|
||||||
from audit_store import AuditStore # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
|
||||||
from config_store import ConfigStore # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
|
||||||
from queue_store import QueueStore # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
|
||||||
|
|
||||||
_instance: StoreManager | None = None
|
_instance: StoreManager | None = None
|
||||||
|
|
||||||
|
|
||||||
class StoreManager:
|
class StoreManager:
|
||||||
"""Owns db_path and delegates migrate/is_migrated across all stores.
|
"""Owns db_path and delegates migrate/is_migrated across the orchestrator's
|
||||||
|
stores.
|
||||||
|
|
||||||
Use instance() for normal access. Call reset(db_path) in tests to swap
|
Use instance() for normal access. Call reset(db_path) in tests to swap the
|
||||||
the singleton to a temp path, then reset() with no args to restore the
|
singleton to a temp path, then reset() with no args to restore the
|
||||||
default."""
|
default."""
|
||||||
|
|
||||||
def __init__(self, db_path: Path | None = None) -> None:
|
def __init__(self, db_path: Path | None = None) -> None:
|
||||||
if db_path is None:
|
if db_path is None:
|
||||||
try:
|
from ...paths import host_db_path
|
||||||
from ..paths import host_db_path
|
|
||||||
except ImportError:
|
|
||||||
from paths import host_db_path # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
|
||||||
db_path = host_db_path()
|
db_path = host_db_path()
|
||||||
self.db_path = db_path
|
self.db_path = db_path
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""The orchestrator's view of the supervise plane.
|
||||||
|
|
||||||
|
Bundles the orchestrator-owned supervise surface: the queue/audit I/O and diff
|
||||||
|
rendering (`queue`) and the `Supervise` lifecycle (`supervise`). For
|
||||||
|
convenience it re-exports the neutral vocabulary (`bot_bottle.supervisor`'s
|
||||||
|
types + `SupervisePlan`) so orchestrator-side callers — service, CLI, tests —
|
||||||
|
import from one place.
|
||||||
|
|
||||||
|
The data plane must NOT import this package; it imports `bot_bottle.supervisor`
|
||||||
|
(neutral) directly and reaches the queue over the control-plane RPC.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ...supervisor.types import * # noqa: F401,F403 — re-export the neutral vocabulary
|
||||||
|
from ...supervisor.plan import SupervisePlan
|
||||||
|
from .queue import (
|
||||||
|
archive_all_proposals,
|
||||||
|
archive_proposal,
|
||||||
|
audit_dir,
|
||||||
|
audit_log_path,
|
||||||
|
list_all_pending_proposals,
|
||||||
|
list_pending_proposals,
|
||||||
|
read_audit_entries,
|
||||||
|
read_proposal,
|
||||||
|
read_response,
|
||||||
|
render_diff,
|
||||||
|
sha256_hex,
|
||||||
|
wait_for_response,
|
||||||
|
write_audit_entry,
|
||||||
|
write_proposal,
|
||||||
|
write_response,
|
||||||
|
)
|
||||||
|
from .supervise import Supervise
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
"""Orchestrator-side supervise queue + audit I/O and diff rendering.
|
||||||
|
|
||||||
|
The host-side library the orchestrator uses to drive the supervise flow: read /
|
||||||
|
write proposals and responses against the queue store, append audit entries,
|
||||||
|
and render operator diffs. Only the orchestrator opens `bot-bottle.db` (PRD
|
||||||
|
0070), so this lives under `orchestrator/` — the data plane reaches it over the
|
||||||
|
control-plane RPC, never by importing this module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import difflib
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ...supervisor.types import (
|
||||||
|
AuditEntry,
|
||||||
|
DEFAULT_POLL_INTERVAL_SEC,
|
||||||
|
Proposal,
|
||||||
|
Response,
|
||||||
|
)
|
||||||
|
from ...paths import bot_bottle_root
|
||||||
|
from ..store.queue_store import QueueStore
|
||||||
|
from ...store.audit_store import AuditStore
|
||||||
|
|
||||||
|
|
||||||
|
# --- Paths -----------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def audit_dir() -> Path:
|
||||||
|
return bot_bottle_root() / "audit"
|
||||||
|
|
||||||
|
|
||||||
|
def audit_log_path(component: str, slug: str) -> Path:
|
||||||
|
return audit_dir() / f"{component}-{slug}.log"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Queue I/O -------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def write_proposal(proposal: Proposal) -> Path:
|
||||||
|
"""Persist `proposal` in the queue database, mode 0o600.
|
||||||
|
Directory is created if missing."""
|
||||||
|
return QueueStore(proposal.bottle_slug).write_proposal(proposal)
|
||||||
|
|
||||||
|
|
||||||
|
def read_proposal(bottle_slug: str, proposal_id: str) -> Proposal:
|
||||||
|
return QueueStore(bottle_slug).read_proposal(proposal_id)
|
||||||
|
|
||||||
|
|
||||||
|
def list_pending_proposals(bottle_slug: str) -> list[Proposal]:
|
||||||
|
"""All proposals for `bottle_slug` that do not yet have a matching
|
||||||
|
response. Sorted by `arrival_timestamp` so the operator sees the queue
|
||||||
|
FIFO."""
|
||||||
|
return QueueStore(bottle_slug).list_pending_proposals()
|
||||||
|
|
||||||
|
|
||||||
|
def list_all_pending_proposals() -> list[Proposal]:
|
||||||
|
"""All pending proposals across bottles, sorted FIFO."""
|
||||||
|
return QueueStore("").list_all_pending_proposals()
|
||||||
|
|
||||||
|
|
||||||
|
def write_response(bottle_slug: str, response: Response) -> Path:
|
||||||
|
return QueueStore(bottle_slug).write_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def read_response(bottle_slug: str, proposal_id: str) -> Response:
|
||||||
|
return QueueStore(bottle_slug).read_response(proposal_id)
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_response(
|
||||||
|
bottle_slug: str,
|
||||||
|
proposal_id: str,
|
||||||
|
*,
|
||||||
|
poll_interval: float = DEFAULT_POLL_INTERVAL_SEC,
|
||||||
|
deadline: float | None = None,
|
||||||
|
) -> Response:
|
||||||
|
"""Block until a response file appears for `proposal_id`, then return it.
|
||||||
|
`deadline` is an absolute time.monotonic() value after which the wait raises
|
||||||
|
TimeoutError. None waits forever — the natural shape, since the operator's
|
||||||
|
response time is unbounded.
|
||||||
|
|
||||||
|
Polls SQLite so the implementation stays portable and stdlib-only."""
|
||||||
|
store = QueueStore(bottle_slug)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
return store.read_response(proposal_id)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
if deadline is not None and time.monotonic() >= deadline:
|
||||||
|
raise TimeoutError(f"no response for proposal {proposal_id!r}")
|
||||||
|
time.sleep(poll_interval)
|
||||||
|
|
||||||
|
|
||||||
|
def archive_proposal(bottle_slug: str, proposal_id: str) -> None:
|
||||||
|
"""Mark both proposal and response rows processed.
|
||||||
|
Idempotent — missing rows are silently skipped."""
|
||||||
|
QueueStore(bottle_slug).archive_proposal(proposal_id)
|
||||||
|
|
||||||
|
|
||||||
|
def archive_all_proposals(bottle_slug: str) -> None:
|
||||||
|
"""Archive every proposal + response for `bottle_slug` (bottle teardown /
|
||||||
|
reconcile cleanup). Idempotent."""
|
||||||
|
QueueStore(bottle_slug).archive_all()
|
||||||
|
|
||||||
|
|
||||||
|
# --- Audit log -------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def write_audit_entry(entry: AuditEntry) -> Path:
|
||||||
|
"""Append `entry` to the host supervise audit table."""
|
||||||
|
return AuditStore().write_audit_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
|
def read_audit_entries(component: str, slug: str) -> list[AuditEntry]:
|
||||||
|
"""Load all audit entries for the given component+slug."""
|
||||||
|
return AuditStore().read_audit_entries(component, slug)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Diff rendering --------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def render_diff(before: str, after: str, *, label: str = "config") -> str:
|
||||||
|
"""Unified diff suitable for the audit log + TUI. Empty diff (no changes)
|
||||||
|
renders as the empty string."""
|
||||||
|
diff = difflib.unified_diff(
|
||||||
|
before.splitlines(keepends=True),
|
||||||
|
after.splitlines(keepends=True),
|
||||||
|
fromfile=f"{label} (current)",
|
||||||
|
tofile=f"{label} (proposed)",
|
||||||
|
lineterm="",
|
||||||
|
)
|
||||||
|
parts = list(diff)
|
||||||
|
if not parts:
|
||||||
|
return ""
|
||||||
|
return "".join(p if p.endswith("\n") else p + "\n" for p in parts).rstrip("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def sha256_hex(content: str) -> str:
|
||||||
|
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"""The `Supervise` lifecycle — host-side database staging (PRD 0013 / 0070).
|
||||||
|
|
||||||
|
`Supervise.prepare` migrates the orchestrator's `bot-bottle.db` and returns the
|
||||||
|
neutral `SupervisePlan`. It touches the orchestrator store manager, so it lives
|
||||||
|
here rather than in the neutral `bot_bottle.supervisor` package; the backend
|
||||||
|
(which drives launch) may import it — backend → orchestrator is an allowed
|
||||||
|
direction, unlike gateway → orchestrator.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import ABC
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ..store.store_manager import StoreManager
|
||||||
|
from ...supervisor.plan import SupervisePlan
|
||||||
|
|
||||||
|
|
||||||
|
class Supervise(ABC):
|
||||||
|
"""Per-bottle supervise daemon. Encapsulates host-side database staging;
|
||||||
|
the gateway's start/stop lifecycle is backend-specific."""
|
||||||
|
|
||||||
|
def prepare(
|
||||||
|
self,
|
||||||
|
slug: str,
|
||||||
|
stage_dir: Path,
|
||||||
|
) -> SupervisePlan:
|
||||||
|
"""Stage the host database. Returns the plan; `internal_network` must be
|
||||||
|
set by the launch step before .start runs."""
|
||||||
|
del stage_dir
|
||||||
|
mgr = StoreManager.instance()
|
||||||
|
mgr.migrate()
|
||||||
|
return SupervisePlan(
|
||||||
|
slug=slug,
|
||||||
|
db_path=mgr.db_path,
|
||||||
|
)
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
"""SQLite-backed persistence for bot-bottle.
|
"""SQLite-backed persistence base for bot-bottle.
|
||||||
|
|
||||||
The store family: a shared `DbStore` base (versioned `migrations`) and the
|
The shared store base: the `DbStore` class (versioned `migrations`) and the
|
||||||
concrete stores built on it — `ConfigStore`, `AuditStore`, `QueueStore` — plus
|
concrete stores built on it that aren't owned by a single service —
|
||||||
`StoreManager`, which migrates them together against the one per-host DB.
|
`ConfigStore` and `AuditStore`.
|
||||||
|
|
||||||
|
Service-owned stores live under their service: the supervise `queue_store`, the
|
||||||
|
`secret_store`, the orchestrator `config_store`, and the orchestrator
|
||||||
|
`store_manager` are under `bot_bottle.orchestrator.store`.
|
||||||
|
|
||||||
Callers import the concrete module they need directly, e.g.
|
Callers import the concrete module they need directly, e.g.
|
||||||
`from bot_bottle.store.store_manager import StoreManager`.
|
`from bot_bottle.store.db_store import DbStore`.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import sqlite3
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ..supervise_types import AuditEntry
|
from ..supervisor.types import AuditEntry
|
||||||
from ..paths import host_db_path
|
from ..paths import host_db_path
|
||||||
from .db_store import DbStore
|
from .db_store import DbStore
|
||||||
from .migrations import TableMigrations
|
from .migrations import TableMigrations
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from supervise_types import AuditEntry # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
from supervisor.types import AuditEntry # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
||||||
from paths import host_db_path # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
from paths import host_db_path # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
||||||
from db_store import DbStore # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
from db_store import DbStore # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
||||||
from migrations import TableMigrations # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
from migrations import TableMigrations # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
||||||
|
|||||||
@@ -1,298 +0,0 @@
|
|||||||
"""Per-bottle supervise plane (PRD 0013).
|
|
||||||
|
|
||||||
The supervise plane is the per-bottle MCP daemon plus its host-side
|
|
||||||
queue/audit support. The daemon (bot_bottle.gateway.supervise_server)
|
|
||||||
sits on the bottle's internal network and exposes MCP tools the agent
|
|
||||||
calls when it needs an operator-reviewed egress change:
|
|
||||||
|
|
||||||
* egress-block / allow — agent proposes a new routes.yaml
|
|
||||||
|
|
||||||
Each tool call: the agent passes the full proposed file plus a
|
|
||||||
justification text. The gateway validates the proposal syntactically,
|
|
||||||
writes it to the host SQLite queue table, and holds the tool-call
|
|
||||||
connection open. The operator's supervise TUI
|
|
||||||
(bot_bottle.cli.supervise) sees the proposal, accepts
|
|
||||||
approve / modify / reject, and writes a response row. The gateway sees
|
|
||||||
the response and returns `{status, notes}` to the agent.
|
|
||||||
|
|
||||||
This module defines the host-side library: dataclasses for the queue
|
|
||||||
record shapes, queue read/write helpers, the audit log writer, and the
|
|
||||||
diff renderer. The in-gateway daemon lives in
|
|
||||||
bot_bottle/gateway/supervise_server.py; the supervise daemon's container
|
|
||||||
lifecycle is owned by the gateway (PRD 0024).
|
|
||||||
|
|
||||||
For 0013 the supervisor's approval handlers are deliberately no-ops:
|
|
||||||
on approval the audit log is written and the response file is
|
|
||||||
delivered to the agent, but no host-side config change happens. The
|
|
||||||
remediation engines that wire real config changes land in PRDs 0014,
|
|
||||||
0015, and 0016.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import difflib
|
|
||||||
import hashlib
|
|
||||||
import time
|
|
||||||
from abc import ABC
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from .supervise_types import (
|
|
||||||
ACTION_OPERATOR_EDIT,
|
|
||||||
AuditEntry,
|
|
||||||
POLL_STATUS_PENDING,
|
|
||||||
POLL_STATUS_UNKNOWN,
|
|
||||||
Proposal,
|
|
||||||
Response,
|
|
||||||
STATUSES,
|
|
||||||
STATUS_APPROVED,
|
|
||||||
STATUS_MODIFIED,
|
|
||||||
STATUS_REJECTED,
|
|
||||||
TOOLS,
|
|
||||||
TOOL_CHECK_PROPOSAL,
|
|
||||||
TOOL_EGRESS_ALLOW,
|
|
||||||
TOOL_EGRESS_BLOCK,
|
|
||||||
TOOL_EGRESS_TOKEN_ALLOW,
|
|
||||||
TOOL_GITLEAKS_ALLOW,
|
|
||||||
TOOL_LIST_EGRESS_ROUTES,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
from .paths import bot_bottle_root
|
|
||||||
except ImportError: # flat imports inside the gateway
|
|
||||||
from paths import bot_bottle_root # type: ignore[import-not-found,no-redef] # pylint: disable=import-error,no-name-in-module
|
|
||||||
|
|
||||||
|
|
||||||
SUPERVISE_HOSTNAME = "supervise"
|
|
||||||
SUPERVISE_PORT = 9100
|
|
||||||
|
|
||||||
# The supervise daemon uses these to query egress's
|
|
||||||
# introspection endpoint for the `list-egress-routes` MCP
|
|
||||||
# tool. The hostname + port match egress's docker network
|
|
||||||
# listen port (see backend.docker.egress.EGRESS_PORT). The supervise
|
|
||||||
# daemon runs inside the gateway alongside egress, so loopback
|
|
||||||
# is the stable address across docker, firecracker, and Apple
|
|
||||||
# Container backends.
|
|
||||||
EGRESS_FORWARD_PROXY = "http://127.0.0.1:9099"
|
|
||||||
EGRESS_INTROSPECT_URL = "http://_egress.local/allowlist"
|
|
||||||
|
|
||||||
COMPONENT_FOR_TOOL: dict[str, str] = {
|
|
||||||
TOOL_EGRESS_ALLOW: "egress",
|
|
||||||
TOOL_EGRESS_BLOCK: "egress",
|
|
||||||
}
|
|
||||||
|
|
||||||
DB_PATH_IN_CONTAINER = "/run/supervise/bot-bottle.db"
|
|
||||||
DEFAULT_POLL_INTERVAL_SEC = 0.5
|
|
||||||
|
|
||||||
|
|
||||||
# --- Paths -----------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def audit_dir() -> Path:
|
|
||||||
return bot_bottle_root() / "audit"
|
|
||||||
|
|
||||||
|
|
||||||
def audit_log_path(component: str, slug: str) -> Path:
|
|
||||||
return audit_dir() / f"{component}-{slug}.log"
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
from .store.queue_store import QueueStore
|
|
||||||
from .store.audit_store import AuditStore
|
|
||||||
from .store.store_manager import StoreManager
|
|
||||||
except ImportError:
|
|
||||||
# Gateway: files are flat-copied under /app, not a package.
|
|
||||||
from queue_store import QueueStore # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
|
||||||
from audit_store import AuditStore # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
|
||||||
from store_manager import StoreManager # type: ignore[import-not-found] # pylint: disable=import-error,no-name-in-module
|
|
||||||
|
|
||||||
|
|
||||||
# --- Queue I/O -------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def write_proposal(proposal: Proposal) -> Path:
|
|
||||||
"""Persist `proposal` in the queue database, mode 0o600.
|
|
||||||
Directory is created if missing."""
|
|
||||||
return QueueStore(proposal.bottle_slug).write_proposal(proposal)
|
|
||||||
|
|
||||||
|
|
||||||
def read_proposal(bottle_slug: str, proposal_id: str) -> Proposal:
|
|
||||||
return QueueStore(bottle_slug).read_proposal(proposal_id)
|
|
||||||
|
|
||||||
|
|
||||||
def list_pending_proposals(bottle_slug: str) -> list[Proposal]:
|
|
||||||
"""All proposals for `bottle_slug` that do not yet have a matching
|
|
||||||
response. Sorted by `arrival_timestamp` so the operator
|
|
||||||
sees the queue FIFO."""
|
|
||||||
return QueueStore(bottle_slug).list_pending_proposals()
|
|
||||||
|
|
||||||
|
|
||||||
def list_all_pending_proposals() -> list[Proposal]:
|
|
||||||
"""All pending proposals across bottles, sorted FIFO."""
|
|
||||||
return QueueStore("").list_all_pending_proposals()
|
|
||||||
|
|
||||||
|
|
||||||
def write_response(bottle_slug: str, response: Response) -> Path:
|
|
||||||
return QueueStore(bottle_slug).write_response(response)
|
|
||||||
|
|
||||||
|
|
||||||
def read_response(bottle_slug: str, proposal_id: str) -> Response:
|
|
||||||
return QueueStore(bottle_slug).read_response(proposal_id)
|
|
||||||
|
|
||||||
|
|
||||||
def wait_for_response(
|
|
||||||
bottle_slug: str,
|
|
||||||
proposal_id: str,
|
|
||||||
*,
|
|
||||||
poll_interval: float = DEFAULT_POLL_INTERVAL_SEC,
|
|
||||||
deadline: float | None = None,
|
|
||||||
) -> Response:
|
|
||||||
"""Block until a response file appears for `proposal_id`, then
|
|
||||||
return it. `deadline` is an absolute time.monotonic() value after
|
|
||||||
which the wait raises TimeoutError. None waits forever — the
|
|
||||||
natural shape, since the operator's response time is unbounded.
|
|
||||||
|
|
||||||
Polls SQLite so the implementation stays portable and stdlib-only."""
|
|
||||||
store = QueueStore(bottle_slug)
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
return store.read_response(proposal_id)
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
if deadline is not None and time.monotonic() >= deadline:
|
|
||||||
raise TimeoutError(f"no response for proposal {proposal_id!r}")
|
|
||||||
time.sleep(poll_interval)
|
|
||||||
|
|
||||||
|
|
||||||
def archive_proposal(bottle_slug: str, proposal_id: str) -> None:
|
|
||||||
"""Mark both proposal and response rows processed.
|
|
||||||
Idempotent — missing rows are silently skipped."""
|
|
||||||
QueueStore(bottle_slug).archive_proposal(proposal_id)
|
|
||||||
|
|
||||||
|
|
||||||
def archive_all_proposals(bottle_slug: str) -> None:
|
|
||||||
"""Archive every proposal + response for `bottle_slug` (bottle teardown /
|
|
||||||
reconcile cleanup). Idempotent."""
|
|
||||||
QueueStore(bottle_slug).archive_all()
|
|
||||||
|
|
||||||
|
|
||||||
# --- Audit log -------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def write_audit_entry(entry: AuditEntry) -> Path:
|
|
||||||
"""Append `entry` to the host supervise audit table."""
|
|
||||||
return AuditStore().write_audit_entry(entry)
|
|
||||||
|
|
||||||
|
|
||||||
def read_audit_entries(component: str, slug: str) -> list[AuditEntry]:
|
|
||||||
"""Load all audit entries for the given component+slug."""
|
|
||||||
return AuditStore().read_audit_entries(component, slug)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Diff rendering --------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def render_diff(before: str, after: str, *, label: str = "config") -> str:
|
|
||||||
"""Unified diff suitable for the audit log + TUI. Empty diff (no
|
|
||||||
changes) renders as the empty string."""
|
|
||||||
diff = difflib.unified_diff(
|
|
||||||
before.splitlines(keepends=True),
|
|
||||||
after.splitlines(keepends=True),
|
|
||||||
fromfile=f"{label} (current)",
|
|
||||||
tofile=f"{label} (proposed)",
|
|
||||||
lineterm="",
|
|
||||||
)
|
|
||||||
parts = list(diff)
|
|
||||||
if not parts:
|
|
||||||
return ""
|
|
||||||
return "".join(p if p.endswith("\n") else p + "\n" for p in parts).rstrip("\n")
|
|
||||||
|
|
||||||
|
|
||||||
def sha256_hex(content: str) -> str:
|
|
||||||
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
# --- Gateway plan + abstract lifecycle -------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class SupervisePlan:
|
|
||||||
"""Output of Supervise.prepare; consumed by .start.
|
|
||||||
|
|
||||||
`db_path` is the host database bind-mounted into the gateway at
|
|
||||||
/run/supervise/bot-bottle.db. `internal_network` is empty at
|
|
||||||
prepare time; the backend's launch step fills it via
|
|
||||||
dataclasses.replace before calling .start."""
|
|
||||||
|
|
||||||
slug: str
|
|
||||||
db_path: Path
|
|
||||||
internal_network: str = ""
|
|
||||||
|
|
||||||
|
|
||||||
class Supervise(ABC):
|
|
||||||
"""Per-bottle supervise daemon. Encapsulates host-side database
|
|
||||||
staging; the gateway's start/stop lifecycle is backend-specific."""
|
|
||||||
|
|
||||||
def prepare(
|
|
||||||
self,
|
|
||||||
slug: str,
|
|
||||||
stage_dir: Path,
|
|
||||||
) -> SupervisePlan:
|
|
||||||
"""Stage the host database. Returns the plan; `internal_network`
|
|
||||||
must be set by the launch step before .start runs."""
|
|
||||||
del stage_dir
|
|
||||||
mgr = StoreManager.instance()
|
|
||||||
mgr.migrate()
|
|
||||||
return SupervisePlan(
|
|
||||||
slug=slug,
|
|
||||||
db_path=mgr.db_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"ACTION_OPERATOR_EDIT",
|
|
||||||
"AuditEntry",
|
|
||||||
"AuditStore",
|
|
||||||
"COMPONENT_FOR_TOOL",
|
|
||||||
"DEFAULT_POLL_INTERVAL_SEC",
|
|
||||||
"DB_PATH_IN_CONTAINER",
|
|
||||||
"Proposal",
|
|
||||||
"QueueStore",
|
|
||||||
"Response",
|
|
||||||
"StoreManager",
|
|
||||||
"STATUSES",
|
|
||||||
"STATUS_APPROVED",
|
|
||||||
"STATUS_MODIFIED",
|
|
||||||
"STATUS_REJECTED",
|
|
||||||
"POLL_STATUS_PENDING",
|
|
||||||
"POLL_STATUS_UNKNOWN",
|
|
||||||
"SUPERVISE_HOSTNAME",
|
|
||||||
"SUPERVISE_PORT",
|
|
||||||
"Supervise",
|
|
||||||
"SupervisePlan",
|
|
||||||
"TOOLS",
|
|
||||||
"EGRESS_FORWARD_PROXY",
|
|
||||||
"EGRESS_INTROSPECT_URL",
|
|
||||||
"TOOL_CHECK_PROPOSAL",
|
|
||||||
"TOOL_EGRESS_ALLOW",
|
|
||||||
"TOOL_EGRESS_BLOCK",
|
|
||||||
"TOOL_GITLEAKS_ALLOW",
|
|
||||||
"TOOL_EGRESS_TOKEN_ALLOW",
|
|
||||||
"TOOL_LIST_EGRESS_ROUTES",
|
|
||||||
"archive_proposal",
|
|
||||||
"archive_all_proposals",
|
|
||||||
"audit_dir",
|
|
||||||
"audit_log_path",
|
|
||||||
"list_pending_proposals",
|
|
||||||
"list_all_pending_proposals",
|
|
||||||
"read_audit_entries",
|
|
||||||
"read_proposal",
|
|
||||||
"read_response",
|
|
||||||
"render_diff",
|
|
||||||
"sha256_hex",
|
|
||||||
"wait_for_response",
|
|
||||||
"write_audit_entry",
|
|
||||||
"write_proposal",
|
|
||||||
"write_response",
|
|
||||||
]
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"""Neutral supervise vocabulary — importable by every tier.
|
||||||
|
|
||||||
|
The supervise plane spans the control plane (orchestrator) and the data plane
|
||||||
|
(gateway `supervise_server`): the gateway proposes/polls over RPC, the
|
||||||
|
orchestrator queues and applies operator decisions. Both sides must agree on
|
||||||
|
the wire types and constants, so those live here — a neutral package neither
|
||||||
|
tier's private code, importable by `bot_bottle.gateway` **and**
|
||||||
|
`bot_bottle.orchestrator` without either depending on the other.
|
||||||
|
|
||||||
|
* `types` — the `Proposal`/`Response`/`AuditEntry` dataclasses, the tool /
|
||||||
|
status / poll-status constants, and the shared daemon constants.
|
||||||
|
* `plan` — `SupervisePlan`, the launch-time staging DTO.
|
||||||
|
|
||||||
|
The orchestrator-only half (queue I/O, diff rendering, the `Supervise`
|
||||||
|
lifecycle) lives under `bot_bottle.orchestrator.supervisor`.
|
||||||
|
"""
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"""`SupervisePlan` — the launch-time supervise staging DTO.
|
||||||
|
|
||||||
|
A pure value type (no store access), so it lives in the neutral package: the
|
||||||
|
backend builds it at launch and the orchestrator's `Supervise.prepare` returns
|
||||||
|
it. The behaviour that fills it in — staging the host database — is the
|
||||||
|
orchestrator's, in `bot_bottle.orchestrator.supervisor.supervise`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SupervisePlan:
|
||||||
|
"""Output of `Supervise.prepare`; consumed by `.start`.
|
||||||
|
|
||||||
|
`db_path` is the host database bind-mounted into the gateway at
|
||||||
|
/run/supervise/bot-bottle.db. `internal_network` is empty at prepare time;
|
||||||
|
the backend's launch step fills it via `dataclasses.replace` before calling
|
||||||
|
`.start`."""
|
||||||
|
|
||||||
|
slug: str
|
||||||
|
db_path: Path
|
||||||
|
internal_network: str = ""
|
||||||
@@ -48,6 +48,24 @@ POLL_STATUS_UNKNOWN = "unknown"
|
|||||||
|
|
||||||
ACTION_OPERATOR_EDIT = "operator-edit"
|
ACTION_OPERATOR_EDIT = "operator-edit"
|
||||||
|
|
||||||
|
# Shared supervise-daemon constants. The gateway `supervise_server` and the
|
||||||
|
# backend read these, so they live with the wire types in the neutral package
|
||||||
|
# (never in the orchestrator's private code the data plane can't import).
|
||||||
|
SUPERVISE_HOSTNAME = "supervise"
|
||||||
|
SUPERVISE_PORT = 9100
|
||||||
|
# The supervise daemon queries egress's introspection endpoint for the
|
||||||
|
# `list-egress-routes` tool over loopback (egress runs alongside it in the
|
||||||
|
# gateway), stable across docker / firecracker / Apple Container backends.
|
||||||
|
EGRESS_FORWARD_PROXY = "http://127.0.0.1:9099"
|
||||||
|
EGRESS_INTROSPECT_URL = "http://_egress.local/allowlist"
|
||||||
|
DB_PATH_IN_CONTAINER = "/run/supervise/bot-bottle.db"
|
||||||
|
DEFAULT_POLL_INTERVAL_SEC = 0.5
|
||||||
|
|
||||||
|
COMPONENT_FOR_TOOL: dict[str, str] = {
|
||||||
|
TOOL_EGRESS_ALLOW: "egress",
|
||||||
|
TOOL_EGRESS_BLOCK: "egress",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _require_str(raw: dict[str, object], key: str) -> str:
|
def _require_str(raw: dict[str, object], key: str) -> str:
|
||||||
value = raw.get(key)
|
value = raw.get(key)
|
||||||
@@ -175,4 +193,11 @@ __all__ = [
|
|||||||
"TOOL_EGRESS_TOKEN_ALLOW",
|
"TOOL_EGRESS_TOKEN_ALLOW",
|
||||||
"TOOL_GITLEAKS_ALLOW",
|
"TOOL_GITLEAKS_ALLOW",
|
||||||
"TOOL_LIST_EGRESS_ROUTES",
|
"TOOL_LIST_EGRESS_ROUTES",
|
||||||
|
"SUPERVISE_HOSTNAME",
|
||||||
|
"SUPERVISE_PORT",
|
||||||
|
"EGRESS_FORWARD_PROXY",
|
||||||
|
"EGRESS_INTROSPECT_URL",
|
||||||
|
"DB_PATH_IN_CONTAINER",
|
||||||
|
"DEFAULT_POLL_INTERVAL_SEC",
|
||||||
|
"COMPONENT_FOR_TOOL",
|
||||||
]
|
]
|
||||||
@@ -91,7 +91,7 @@ class TestGatewayImage(unittest.TestCase):
|
|||||||
# Probe that the package imports resolve inside the image.
|
# Probe that the package imports resolve inside the image.
|
||||||
rc, out = self._run_in_image(
|
rc, out = self._run_in_image(
|
||||||
"python3", "-c",
|
"python3", "-c",
|
||||||
"from bot_bottle import supervise; from bot_bottle.gateway import supervise_server; print('ok')",
|
"from bot_bottle.supervisor import types; from bot_bottle.gateway import supervise_server; print('ok')",
|
||||||
)
|
)
|
||||||
self.assertEqual(0, rc, msg=out)
|
self.assertEqual(0, rc, msg=out)
|
||||||
self.assertIn("ok", out)
|
self.assertIn("ok", out)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan
|
|||||||
from bot_bottle.egress import EgressPlan, EgressRoute
|
from bot_bottle.egress import EgressPlan, EgressRoute
|
||||||
from bot_bottle.git_gate import GitGatePlan, GitGateUpstream
|
from bot_bottle.git_gate import GitGatePlan, GitGateUpstream
|
||||||
from bot_bottle.manifest import ManifestIndex
|
from bot_bottle.manifest import ManifestIndex
|
||||||
from bot_bottle.supervise import SupervisePlan
|
from bot_bottle.supervisor.plan import SupervisePlan
|
||||||
|
|
||||||
|
|
||||||
SLUG = "demo-abc12"
|
SLUG = "demo-abc12"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from bot_bottle.cli import main
|
|||||||
from bot_bottle.store.db_store import DbStore
|
from bot_bottle.store.db_store import DbStore
|
||||||
from bot_bottle.log import Die
|
from bot_bottle.log import Die
|
||||||
from bot_bottle.manifest import ManifestError
|
from bot_bottle.manifest import ManifestError
|
||||||
from bot_bottle.store.store_manager import StoreManager
|
from bot_bottle.orchestrator.store.store_manager import StoreManager
|
||||||
|
|
||||||
|
|
||||||
class TestMainDispatch(unittest.TestCase):
|
class TestMainDispatch(unittest.TestCase):
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from bot_bottle.store.config_store import (
|
|||||||
DEFAULT_CACHED_IMAGE_STALE_WARNING_DAYS,
|
DEFAULT_CACHED_IMAGE_STALE_WARNING_DAYS,
|
||||||
ConfigStore,
|
ConfigStore,
|
||||||
)
|
)
|
||||||
from bot_bottle.store.store_manager import StoreManager
|
from bot_bottle.orchestrator.store.store_manager import StoreManager
|
||||||
|
|
||||||
|
|
||||||
class TestConfigStore(unittest.TestCase):
|
class TestConfigStore(unittest.TestCase):
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from bot_bottle.contrib.claude.agent_provider import ClaudeAgentProvider
|
|||||||
from bot_bottle.egress import EgressPlan
|
from bot_bottle.egress import EgressPlan
|
||||||
from bot_bottle.git_gate import GitGatePlan
|
from bot_bottle.git_gate import GitGatePlan
|
||||||
from bot_bottle.manifest import ManifestIndex
|
from bot_bottle.manifest import ManifestIndex
|
||||||
from bot_bottle.supervise import SupervisePlan
|
from bot_bottle.supervisor.plan import SupervisePlan
|
||||||
|
|
||||||
|
|
||||||
_URL = "http://supervise:9100/"
|
_URL = "http://supervise:9100/"
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from bot_bottle.contrib.codex.agent_provider import CodexAgentProvider
|
|||||||
from bot_bottle.egress import EgressPlan
|
from bot_bottle.egress import EgressPlan
|
||||||
from bot_bottle.git_gate import GitGatePlan
|
from bot_bottle.git_gate import GitGatePlan
|
||||||
from bot_bottle.manifest import ManifestIndex
|
from bot_bottle.manifest import ManifestIndex
|
||||||
from bot_bottle.supervise import SupervisePlan
|
from bot_bottle.supervisor.plan import SupervisePlan
|
||||||
|
|
||||||
|
|
||||||
_URL = "http://supervise:9100/"
|
_URL = "http://supervise:9100/"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import unittest
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
|
|
||||||
from bot_bottle.orchestrator.config_store import (
|
from bot_bottle.orchestrator.store.config_store import (
|
||||||
DEFAULT_TEARDOWN_TIMEOUT_SECONDS,
|
DEFAULT_TEARDOWN_TIMEOUT_SECONDS,
|
||||||
TEARDOWN_TIMEOUT_ENV,
|
TEARDOWN_TIMEOUT_ENV,
|
||||||
OrchestratorConfigStore,
|
OrchestratorConfigStore,
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ from bot_bottle.orchestrator.broker import StubBroker
|
|||||||
from bot_bottle.orchestrator.control_plane import dispatch, make_server
|
from bot_bottle.orchestrator.control_plane import dispatch, make_server
|
||||||
from bot_bottle.orchestrator.registry import BottleRecord, RegistryStore
|
from bot_bottle.orchestrator.registry import BottleRecord, RegistryStore
|
||||||
from bot_bottle.orchestrator.service import Orchestrator
|
from bot_bottle.orchestrator.service import Orchestrator
|
||||||
from bot_bottle.store.store_manager import StoreManager
|
from bot_bottle.orchestrator.store.store_manager import StoreManager
|
||||||
from bot_bottle.supervise import (
|
from bot_bottle.orchestrator.supervisor import (
|
||||||
Proposal,
|
Proposal,
|
||||||
TOOL_EGRESS_ALLOW,
|
TOOL_EGRESS_ALLOW,
|
||||||
sha256_hex,
|
sha256_hex,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from bot_bottle.orchestrator.secret_store import (
|
from bot_bottle.orchestrator.store.secret_store import (
|
||||||
ENV_VAR_SECRET_NAME,
|
ENV_VAR_SECRET_NAME,
|
||||||
decrypt_value,
|
decrypt_value,
|
||||||
encrypt_value,
|
encrypt_value,
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ from unittest.mock import patch
|
|||||||
from bot_bottle.orchestrator.broker import LaunchBroker, LaunchRequest, StubBroker
|
from bot_bottle.orchestrator.broker import LaunchBroker, LaunchRequest, StubBroker
|
||||||
from bot_bottle.orchestrator.registry import RegistryStore
|
from bot_bottle.orchestrator.registry import RegistryStore
|
||||||
from bot_bottle.orchestrator.service import Orchestrator
|
from bot_bottle.orchestrator.service import Orchestrator
|
||||||
from bot_bottle.orchestrator.secret_store import new_env_var_secret
|
from bot_bottle.orchestrator.store.secret_store import new_env_var_secret
|
||||||
from bot_bottle.store.store_manager import StoreManager
|
from bot_bottle.orchestrator.store.store_manager import StoreManager
|
||||||
from bot_bottle.supervise import (
|
from bot_bottle.orchestrator.supervisor import (
|
||||||
Proposal,
|
Proposal,
|
||||||
STATUS_APPROVED,
|
STATUS_APPROVED,
|
||||||
TOOL_EGRESS_ALLOW,
|
TOOL_EGRESS_ALLOW,
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import unittest
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from bot_bottle import supervise
|
from bot_bottle.orchestrator import supervisor as supervise
|
||||||
from bot_bottle.paths import host_db_path
|
from bot_bottle.paths import host_db_path
|
||||||
from tests.unit import use_bottle_root
|
from tests.unit import use_bottle_root
|
||||||
from bot_bottle.store.audit_store import AuditStore
|
from bot_bottle.store.audit_store import AuditStore
|
||||||
from bot_bottle.store.queue_store import QueueStore
|
from bot_bottle.orchestrator.store.queue_store import QueueStore
|
||||||
from bot_bottle.supervise import (
|
from bot_bottle.orchestrator.supervisor import (
|
||||||
AuditEntry,
|
AuditEntry,
|
||||||
Proposal,
|
Proposal,
|
||||||
Response,
|
Response,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from datetime import datetime, timezone
|
|||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from bot_bottle.cli import supervise as supervise_cli
|
from bot_bottle.cli import supervise as supervise_cli
|
||||||
from bot_bottle.supervise import (
|
from bot_bottle.orchestrator.supervisor import (
|
||||||
Proposal,
|
Proposal,
|
||||||
TOOL_EGRESS_ALLOW,
|
TOOL_EGRESS_ALLOW,
|
||||||
TOOL_EGRESS_BLOCK,
|
TOOL_EGRESS_BLOCK,
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import unittest
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from bot_bottle import supervise
|
from bot_bottle.orchestrator import supervisor as supervise
|
||||||
from bot_bottle.store.audit_store import AuditStore
|
from bot_bottle.store.audit_store import AuditStore
|
||||||
from bot_bottle.paths import bot_bottle_root
|
from bot_bottle.paths import bot_bottle_root
|
||||||
from bot_bottle.store.queue_store import QueueStore
|
from bot_bottle.orchestrator.store.queue_store import QueueStore
|
||||||
from bot_bottle.supervise import (
|
from bot_bottle.orchestrator.supervisor import (
|
||||||
AuditEntry,
|
AuditEntry,
|
||||||
Proposal,
|
Proposal,
|
||||||
STATUS_APPROVED,
|
STATUS_APPROVED,
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ from pathlib import Path
|
|||||||
|
|
||||||
from tests.unit import use_bottle_root
|
from tests.unit import use_bottle_root
|
||||||
|
|
||||||
from bot_bottle import supervise as _sv
|
from bot_bottle.orchestrator import supervisor as _sv
|
||||||
from bot_bottle.store import queue_store as _qs
|
from bot_bottle.orchestrator.store import queue_store as _qs
|
||||||
from bot_bottle.store import audit_store as _as
|
from bot_bottle.store import audit_store as _as
|
||||||
|
|
||||||
from bot_bottle.gateway import supervise_server # noqa: E402
|
from bot_bottle.gateway import supervise_server # noqa: E402
|
||||||
|
|||||||
Reference in New Issue
Block a user