fix(supervise): remove queue directory from db-backed flow
lint / lint (push) Successful in 2m4s
test / unit (pull_request) Successful in 59s
test / integration (pull_request) Successful in 20s
test / coverage (pull_request) Successful in 1m10s

This commit is contained in:
2026-07-01 19:50:38 +00:00
parent 3067b067d2
commit 29904609da
23 changed files with 212 additions and 270 deletions
-9
View File
@@ -35,7 +35,6 @@ from ...git_gate import GIT_GATE_HOSTNAME
from ...log import die, warn
from ...supervise import (
DB_PATH_IN_CONTAINER,
QUEUE_DIR_IN_CONTAINER,
SUPERVISE_HOSTNAME,
SUPERVISE_PORT,
)
@@ -165,7 +164,6 @@ def _sidecar_bundle_service(plan: DockerBottlePlan) -> dict[str, Any]:
env += [
f"SUPERVISE_BOTTLE_SLUG={plan.slug}",
f"SUPERVISE_DB_PATH={DB_PATH_IN_CONTAINER}",
f"SUPERVISE_QUEUE_DIR={QUEUE_DIR_IN_CONTAINER}",
f"SUPERVISE_PORT={SUPERVISE_PORT}",
]
volumes.append({
@@ -174,13 +172,6 @@ def _sidecar_bundle_service(plan: DockerBottlePlan) -> dict[str, Any]:
"target": DB_PATH_IN_CONTAINER,
"read_only": False,
})
volumes.append({
"type": "bind",
"source": str(sp.queue_dir),
"target": QUEUE_DIR_IN_CONTAINER,
"read_only": False,
})
internal_aliases = [EGRESS_HOSTNAME]
if gp.upstreams:
internal_aliases.append(GIT_GATE_HOSTNAME)
+1 -3
View File
@@ -33,7 +33,7 @@ from ...git_gate import (
revoke_git_gate_provisioned_keys,
)
from ...log import die, info, warn
from ...supervise import DB_PATH_IN_CONTAINER, QUEUE_DIR_IN_CONTAINER, SUPERVISE_PORT
from ...supervise import DB_PATH_IN_CONTAINER, SUPERVISE_PORT
from ...util import expand_tilde
from ..docker.egress import EGRESS_CA_IN_CONTAINER, EGRESS_PORT
from ..docker.git_gate import (
@@ -380,7 +380,6 @@ def _sidecar_env_entries(plan: MacosContainerBottlePlan) -> tuple[str, ...]:
env += [
f"SUPERVISE_BOTTLE_SLUG={plan.slug}",
f"SUPERVISE_DB_PATH={DB_PATH_IN_CONTAINER}",
f"SUPERVISE_QUEUE_DIR={QUEUE_DIR_IN_CONTAINER}",
f"SUPERVISE_PORT={SUPERVISE_PORT}",
]
return tuple(env)
@@ -407,7 +406,6 @@ def _sidecar_mounts(
sp = plan.supervise_plan
if sp is not None:
mounts.append((str(sp.db_path), DB_PATH_IN_CONTAINER, False))
mounts.append((str(sp.queue_dir), QUEUE_DIR_IN_CONTAINER, False))
return tuple(mounts)
+1 -3
View File
@@ -27,7 +27,7 @@ from ...egress import (
egress_resolve_token_values,
egress_sidecar_env_entries,
)
from ...supervise import DB_PATH_IN_CONTAINER, QUEUE_DIR_IN_CONTAINER, SUPERVISE_PORT
from ...supervise import DB_PATH_IN_CONTAINER, SUPERVISE_PORT
from ...util import expand_tilde
from ..docker import util as docker_mod
from ..docker.egress import (
@@ -370,11 +370,9 @@ def _bundle_launch_spec(
env += [
f"SUPERVISE_BOTTLE_SLUG={plan.slug}",
f"SUPERVISE_DB_PATH={DB_PATH_IN_CONTAINER}",
f"SUPERVISE_QUEUE_DIR={QUEUE_DIR_IN_CONTAINER}",
f"SUPERVISE_PORT={SUPERVISE_PORT}",
]
volumes.append((str(sp.db_path), DB_PATH_IN_CONTAINER, False))
volumes.append((str(sp.queue_dir), QUEUE_DIR_IN_CONTAINER, False))
# Container ports the agent reaches from the smolvm guest —
# published on host loopback so the guest can dial via TSI +
+2 -3
View File
@@ -284,9 +284,8 @@ def git_gate_state_dir(identity: str) -> Path:
def supervise_state_dir(identity: str) -> Path:
"""State subdir reserved for supervise sidecar bind-mount sources.
The queue dir is intentionally NOT under here — it lives at
~/.bot-bottle/queue/<slug>/ alongside the audit logs, so it
survives state-dir cleanup."""
Runtime queue/audit rows live in the host-level bot-bottle SQLite
database, so they survive state-dir cleanup."""
return bottle_state_dir(identity) / _SUPERVISE_SUBDIR
+9 -16
View File
@@ -45,7 +45,7 @@ from ..supervise import (
TOOL_EGRESS_BLOCK,
TOOL_GITLEAKS_ALLOW,
TOOL_EGRESS_TOKEN_ALLOW,
list_pending_proposals,
list_all_pending_proposals,
render_diff,
write_audit_entry,
write_response,
@@ -63,10 +63,9 @@ _REPORT_ONLY_TOOLS: tuple[str, ...] = (TOOL_GITLEAKS_ALLOW, TOOL_EGRESS_TOKEN_AL
@dataclass(frozen=True)
class QueuedProposal:
"""A pending proposal plus the queue dir it was found in."""
"""A pending proposal from the supervise queue."""
proposal: Proposal
queue_dir: Path
# Errors any remediation engine may raise. Caught by the TUI key
@@ -86,16 +85,11 @@ def apply_routes_change(slug: str, content: str) -> tuple[str, str]:
def discover_pending() -> list[QueuedProposal]:
"""Walk ~/.bot-bottle/queue/* and collect pending proposals."""
queue_root = _supervise.bot_bottle_root() / "queue"
if not queue_root.is_dir():
return []
out: list[QueuedProposal] = []
for slug_dir in sorted(queue_root.iterdir()):
if not slug_dir.is_dir():
continue
for proposal in list_pending_proposals(slug_dir):
out.append(QueuedProposal(proposal=proposal, queue_dir=slug_dir))
"""Collect pending proposals across bottles."""
out = [
QueuedProposal(proposal=proposal)
for proposal in list_all_pending_proposals()
]
out.sort(key=lambda q: q.proposal.arrival_timestamp)
return out
@@ -118,7 +112,6 @@ def _detail_lines(
(f"tool: {p.tool}", 0),
(f"id: {p.id}", 0),
(f"arrived: {p.arrival_timestamp}", 0),
(f"queue: {qp.queue_dir}", 0),
("", 0),
("justification:", 0),
]
@@ -165,7 +158,7 @@ def approve(
notes=notes,
final_file=final_file,
)
write_response(qp.queue_dir, response)
write_response(qp.proposal.bottle_slug, response)
_write_audit(
qp, action=status, notes=notes,
diff_before=diff_before, diff_after=diff_after,
@@ -179,7 +172,7 @@ def reject(qp: QueuedProposal, *, reason: str) -> None:
notes=reason,
final_file=None,
)
write_response(qp.queue_dir, response)
write_response(qp.proposal.bottle_slug, response)
_write_audit(qp, action=STATUS_REJECTED, notes=reason, diff_before="", diff_after="")
+6 -9
View File
@@ -79,14 +79,13 @@ class EgressAddon:
# only — a restart re-prompts. Mutated only from the asyncio loop that
# runs the addon hooks, so no lock is needed.
self.safe_tokens: set[str] = set()
self._supervise_queue_dir = os.environ.get("SUPERVISE_QUEUE_DIR", "").strip()
self._supervise_slug = os.environ.get("SUPERVISE_BOTTLE_SLUG", "").strip()
self._token_allow_timeout = _token_allow_timeout_from_env(os.environ)
self._reload(initial=True)
self._install_sighup()
def _supervise_available(self) -> bool:
return bool(self._supervise_queue_dir and self._supervise_slug)
return bool(self._supervise_slug)
def _reload(self, *, initial: bool = False) -> None:
try:
@@ -393,9 +392,8 @@ class EgressAddon:
justification=_TOKEN_ALLOW_JUSTIFICATION,
current_file_hash=_sv.sha256_hex(payload),
)
queue_dir = Path(self._supervise_queue_dir)
try:
_sv.write_proposal(queue_dir, proposal)
_sv.write_proposal(proposal)
except OSError as e:
sys.stderr.write(
f"egress: could not queue token-allow proposal: {e}; "
@@ -411,8 +409,8 @@ class EgressAddon:
**self._req_ctx(flow),
}) + "\n")
response = await self._await_token_response(queue_dir, proposal.id)
_sv.archive_proposal(queue_dir, proposal.id)
response = await self._await_token_response(proposal.id)
_sv.archive_proposal(self._supervise_slug, proposal.id)
if response is not None and response.status in (
_sv.STATUS_APPROVED, _sv.STATUS_MODIFIED,
@@ -439,16 +437,15 @@ class EgressAddon:
async def _await_token_response(
self,
queue_dir: Path,
proposal_id: str,
) -> "_sv.Response | None":
"""Poll the queue dir for the operator's response without blocking the
"""Poll the DB for the operator's response without blocking the
proxy event loop. Returns the Response, or None on timeout."""
loop = asyncio.get_running_loop()
deadline = loop.time() + self._token_allow_timeout
while True:
try:
return _sv.read_response(queue_dir, proposal_id)
return _sv.read_response(self._supervise_slug, proposal_id)
except (OSError, ValueError, KeyError):
# Not written yet, or a partial/malformed write — retry until
# the deadline, then fail closed.
+8 -10
View File
@@ -239,9 +239,8 @@ from pathlib import Path
from bot_bottle import supervise as _sv
report_path = Path(sys.argv[1])
queue_dir = os.environ.get("SUPERVISE_QUEUE_DIR", "")
slug = os.environ.get("SUPERVISE_BOTTLE_SLUG", "")
if not queue_dir or not slug:
if not slug:
sys.exit(2)
try:
@@ -289,7 +288,7 @@ proposal = _sv.Proposal.new(
current_file_hash=hashlib.sha256(payload.encode("utf-8")).hexdigest(),
now=datetime.datetime.now(datetime.timezone.utc),
)
_sv.write_proposal(Path(queue_dir), proposal)
_sv.write_proposal(proposal)
print(proposal.id)
PY
)
@@ -303,7 +302,7 @@ PY
return 1
fi
queue_dir=${SUPERVISE_QUEUE_DIR:-}
slug=${SUPERVISE_BOTTLE_SLUG:-}
timeout=${SUPERVISE_GITLEAKS_ALLOW_TIMEOUT_SECONDS:-300}
case "$timeout" in
''|*[!0-9]*)
@@ -315,14 +314,14 @@ PY
echo "git-gate: approve with './cli.py supervise' to continue this push" >&2
waited=0
while [ "$waited" -lt "$timeout" ]; do
status=$(python3 - "$queue_dir" "$proposal_id" <<'PY'
status=$(python3 - "$slug" "$proposal_id" <<'PY'
import sys
from pathlib import Path
from bot_bottle import supervise as _sv
slug = sys.argv[1]
try:
response = _sv.read_response(Path(sys.argv[1]), sys.argv[2])
response = _sv.read_response(slug, sys.argv[2])
except FileNotFoundError:
sys.exit(2)
print(response.status)
@@ -337,13 +336,12 @@ PY
if [ -n "$status" ]; then
case "$status" in
approved|modified)
python3 - "$queue_dir" "$proposal_id" <<'PY' || true
python3 - "$slug" "$proposal_id" <<'PY' || true
import sys
from pathlib import Path
from bot_bottle import supervise as _sv
_sv.archive_proposal(Path(sys.argv[1]), sys.argv[2])
_sv.archive_proposal(sys.argv[1], sys.argv[2])
PY
echo "git-gate: supervisor approved # gitleaks:allow for $ref" >&2
return 0
+59 -58
View File
@@ -9,15 +9,14 @@ calls when it needs an operator-reviewed egress change:
Each tool call: the agent passes the full proposed file plus a
justification text. The sidecar validates the proposal syntactically,
writes it to the host's per-bottle queue dir, and holds the tool-call
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 file alongside the
proposal. The sidecar sees the response and returns `{status, notes}`
to the agent.
approve / modify / reject, and writes a response row. The sidecar sees
the response and returns `{status, notes}` to the agent.
This module defines the host-side library: dataclasses for the queue
file shapes, queue read/write helpers, the audit log writer, and the
record shapes, queue read/write helpers, the audit log writer, and the
diff renderer. The in-container sidecar lives in
bot_bottle/supervise_server.py; the supervise daemon's container
lifecycle is owned by the sidecar bundle (PRD 0024).
@@ -86,7 +85,6 @@ STATUSES: tuple[str, ...] = (STATUS_APPROVED, STATUS_MODIFIED, STATUS_REJECTED)
# `routes edit <bottle>` verb writes entries with this action.
ACTION_OPERATOR_EDIT = "operator-edit"
QUEUE_DIR_IN_CONTAINER = "/run/supervise/queue"
DB_PATH_IN_CONTAINER = "/run/supervise/bot-bottle.db"
DEFAULT_POLL_INTERVAL_SEC = 0.5
HOST_DB_FILENAME = "bot-bottle.db"
@@ -99,10 +97,6 @@ def bot_bottle_root() -> Path:
return Path.home() / ".bot-bottle"
def queue_dir_for_slug(slug: str) -> Path:
return bot_bottle_root() / "queue" / slug
def audit_dir() -> Path:
return bot_bottle_root() / "audit"
@@ -115,8 +109,7 @@ def host_db_path() -> Path:
return bot_bottle_root() / HOST_DB_FILENAME
def queue_db_path(queue_dir: Path) -> Path:
del queue_dir
def queue_db_path() -> Path:
env_path = os.environ.get("SUPERVISE_DB_PATH", "").strip()
return Path(env_path) if env_path else host_db_path()
@@ -126,9 +119,7 @@ def queue_db_path(queue_dir: Path) -> Path:
@dataclass(frozen=True)
class Proposal:
"""One pending tool-call from the agent. The sidecar writes one
of these to the queue dir on a tool call; the operator's TUI
reads them; the sidecar polls for a matching Response."""
"""One pending tool-call from the agent."""
id: str
bottle_slug: str
@@ -182,7 +173,7 @@ class Proposal:
@dataclass(frozen=True)
class Response:
"""The operator's decision on a proposal. The TUI writes one of
these to the queue dir; the sidecar reads it and returns the
these to the queue table; the sidecar reads it and returns the
`{status, notes}` pair to the agent's tool call.
`final_file` carries the file content the supervisor will
@@ -238,33 +229,38 @@ class AuditEntry:
# --- Queue I/O -------------------------------------------------------------
def write_proposal(queue_dir: Path, proposal: Proposal) -> Path:
def write_proposal(proposal: Proposal) -> Path:
"""Persist `proposal` in the queue database, mode 0o600.
Directory is created if missing."""
return _QueueStore(queue_dir).write_proposal(proposal)
return _QueueStore(proposal.bottle_slug).write_proposal(proposal)
def read_proposal(queue_dir: Path, proposal_id: str) -> Proposal:
return _QueueStore(queue_dir).read_proposal(proposal_id)
def read_proposal(bottle_slug: str, proposal_id: str) -> Proposal:
return _QueueStore(bottle_slug).read_proposal(proposal_id)
def list_pending_proposals(queue_dir: Path) -> list[Proposal]:
"""All proposals in `queue_dir` that do not yet have a matching
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(queue_dir).list_pending_proposals()
return _QueueStore(bottle_slug).list_pending_proposals()
def write_response(queue_dir: Path, response: Response) -> Path:
return _QueueStore(queue_dir).write_response(response)
def list_all_pending_proposals() -> list[Proposal]:
"""All pending proposals across bottles, sorted FIFO."""
return _QueueStore("").list_all_pending_proposals()
def read_response(queue_dir: Path, proposal_id: str) -> Response:
return _QueueStore(queue_dir).read_response(proposal_id)
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(
queue_dir: Path,
bottle_slug: str,
proposal_id: str,
*,
poll_interval: float = DEFAULT_POLL_INTERVAL_SEC,
@@ -276,7 +272,7 @@ def wait_for_response(
natural shape, since the operator's response time is unbounded.
Polls SQLite so the implementation stays portable and stdlib-only."""
store = _QueueStore(queue_dir)
store = _QueueStore(bottle_slug)
while True:
try:
return store.read_response(proposal_id)
@@ -287,10 +283,10 @@ def wait_for_response(
time.sleep(poll_interval)
def archive_proposal(queue_dir: Path, proposal_id: str) -> None:
def archive_proposal(bottle_slug: str, proposal_id: str) -> None:
"""Mark both proposal and response rows processed.
Idempotent missing rows are silently skipped."""
_QueueStore(queue_dir).archive_proposal(proposal_id)
_QueueStore(bottle_slug).archive_proposal(proposal_id)
# --- Audit log -------------------------------------------------------------
@@ -333,9 +329,9 @@ def sha256_hex(content: str) -> str:
class _QueueStore:
def __init__(self, queue_dir: Path) -> None:
self.queue_key = _queue_key(queue_dir)
self.db_path = queue_db_path(queue_dir)
def __init__(self, queue_key: str) -> None:
self.queue_key = queue_key
self.db_path = queue_db_path()
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self._init()
@@ -396,6 +392,25 @@ class _QueueStore:
).fetchall()
return [_proposal_from_row(row) for row in rows]
def list_all_pending_proposals(self) -> list[Proposal]:
if not self.db_path.is_file():
return []
with self._connect() as conn:
rows = conn.execute(
"""
SELECT p.* FROM supervise_proposals p
WHERE p.archived = 0
AND NOT EXISTS (
SELECT 1 FROM supervise_responses r
WHERE r.queue_key = p.queue_key
AND r.proposal_id = p.id
AND r.archived = 0
)
ORDER BY p.arrival_timestamp, p.id
"""
).fetchall()
return [_proposal_from_row(row) for row in rows]
def write_response(self, response: Response) -> Path:
with self._connect() as conn:
conn.execute(
@@ -597,13 +612,6 @@ def _audit_entry_from_row(row: sqlite3.Row) -> AuditEntry:
)
def _queue_key(queue_dir: Path) -> str:
env_slug = os.environ.get("SUPERVISE_BOTTLE_SLUG", "").strip()
if env_slug:
return env_slug
return queue_dir.name
# --- Sidecar plan + abstract lifecycle -------------------------------------
@@ -611,39 +619,33 @@ def _queue_key(queue_dir: Path) -> str:
class SupervisePlan:
"""Output of Supervise.prepare; consumed by .start.
`queue_dir` is the host directory bind-mounted into the sidecar
at /run/supervise/queue. `internal_network` is empty at prepare
time; the backend's launch step fills it via dataclasses.replace
before calling .start."""
`db_path` is the host database bind-mounted into the sidecar 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
queue_dir: Path
db_path: Path
internal_network: str = ""
class Supervise(ABC):
"""Per-bottle supervise sidecar. Encapsulates the host-side
prepare (queue dir staging); the sidecar's start/stop lifecycle
is backend-specific."""
"""Per-bottle supervise sidecar. Encapsulates host-side database
staging; the sidecar's start/stop lifecycle is backend-specific."""
def prepare(
self,
slug: str,
stage_dir: Path,
) -> SupervisePlan:
"""Stage the per-bottle queue dir on the host. Returns the
plan; `internal_network` must be set by the launch step before
.start runs."""
"""Stage the host database. Returns the plan; `internal_network`
must be set by the launch step before .start runs."""
del stage_dir
queue_dir = queue_dir_for_slug(slug)
queue_dir.mkdir(parents=True, exist_ok=True)
db_path = host_db_path()
_QueueStore(queue_dir)
_QueueStore(slug)
_AuditStore(db_path)
return SupervisePlan(
slug=slug,
queue_dir=queue_dir,
db_path=db_path,
)
@@ -664,7 +666,6 @@ __all__ = [
"DEFAULT_POLL_INTERVAL_SEC",
"DB_PATH_IN_CONTAINER",
"Proposal",
"QUEUE_DIR_IN_CONTAINER",
"Response",
"STATUSES",
"STATUS_APPROVED",
@@ -688,8 +689,8 @@ __all__ = [
"bot_bottle_root",
"host_db_path",
"list_pending_proposals",
"list_all_pending_proposals",
"queue_db_path",
"queue_dir_for_slug",
"read_audit_entries",
"read_proposal",
"read_response",
+9 -17
View File
@@ -7,14 +7,13 @@ config changes when stuck. The tools are `egress-allow`,
Each queued tool call:
1. Validates the proposed file syntactically.
2. Writes a Proposal to /run/supervise/queue/ (bind-mounted from
the host's ~/.bot-bottle/queue/<slug>/).
3. Blocks polling for a matching Response file.
2. Writes a Proposal to the host SQLite database.
3. Blocks polling for a matching Response row.
4. Returns the operator's `{status, notes}` to the agent.
The bottle slug arrives via SUPERVISE_BOTTLE_SLUG env (stamped at
container creation by the backend's start step). The queue dir comes
from SUPERVISE_QUEUE_DIR (default `/run/supervise/queue`).
container creation by the backend's start step). SUPERVISE_DB_PATH
points at the bind-mounted host database.
Speaks MCP over HTTP+JSON-RPC. Methods handled:
@@ -42,7 +41,6 @@ import typing
import urllib.error
import urllib.request
from dataclasses import dataclass
from pathlib import Path
try:
# Same-directory imports inside the bundle container; these files are
@@ -277,7 +275,6 @@ def validate_proposed_file(tool: str, content: str) -> None:
@dataclass(frozen=True)
class ServerConfig:
bottle_slug: str
queue_dir: Path
response_timeout_seconds: float = DEFAULT_RESPONSE_TIMEOUT_SECONDS
@@ -376,7 +373,7 @@ def handle_tools_call(
current_file_hash=_sv.sha256_hex(proposed_file),
)
try:
_sv.write_proposal(config.queue_dir, proposal)
_sv.write_proposal(proposal)
except OSError as e:
raise _RpcInternalError(f"failed to write proposal to queue: {e}") from e
sys.stderr.write(
@@ -387,7 +384,7 @@ def handle_tools_call(
deadline = time.monotonic() + config.response_timeout_seconds
try:
response = _sv.wait_for_response(
config.queue_dir,
config.bottle_slug,
proposal.id,
poll_interval=MIN_RESPONSE_POLL_INTERVAL_SECONDS,
deadline=deadline,
@@ -399,7 +396,7 @@ def handle_tools_call(
"isError": False,
}
try:
_sv.archive_proposal(config.queue_dir, proposal.id)
_sv.archive_proposal(config.bottle_slug, proposal.id)
except OSError as e:
raise _RpcInternalError(f"failed to archive proposal: {e}") from e
@@ -539,7 +536,7 @@ class MCPHandler(http.server.BaseHTTPRequestHandler):
class MCPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
allow_reuse_address = True
daemon_threads = True
config: ServerConfig = ServerConfig(bottle_slug="", queue_dir=Path())
config: ServerConfig = ServerConfig(bottle_slug="")
# --- Entry point -----------------------------------------------------------
@@ -548,21 +545,18 @@ class MCPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
def serve(
*,
bottle_slug: str,
queue_dir: Path,
port: int = _sv.SUPERVISE_PORT,
bind: str = "0.0.0.0",
response_timeout_seconds: float = DEFAULT_RESPONSE_TIMEOUT_SECONDS,
) -> typing.NoReturn:
queue_dir.mkdir(parents=True, exist_ok=True)
server = MCPServer((bind, port), MCPHandler)
server.config = ServerConfig(
bottle_slug=bottle_slug,
queue_dir=queue_dir,
response_timeout_seconds=response_timeout_seconds,
)
sys.stderr.write(
f"supervise listening on {bind}:{port}; "
f"slug={bottle_slug!r}; queue={queue_dir}; "
f"slug={bottle_slug!r}; "
f"tools: {', '.join(t['name'] for t in TOOL_DEFINITIONS)}\n" # type: ignore[arg-type]
)
sys.stderr.flush()
@@ -581,7 +575,6 @@ def main(argv: list[str]) -> int:
if not bottle_slug:
sys.stderr.write("supervise: SUPERVISE_BOTTLE_SLUG env is unset\n")
return 2
queue_dir = Path(os.environ.get("SUPERVISE_QUEUE_DIR", _sv.QUEUE_DIR_IN_CONTAINER))
port = int(os.environ.get("SUPERVISE_PORT", str(_sv.SUPERVISE_PORT)))
bind = os.environ.get("SUPERVISE_BIND", "0.0.0.0")
try:
@@ -591,7 +584,6 @@ def main(argv: list[str]) -> int:
return 2
serve(
bottle_slug=bottle_slug,
queue_dir=queue_dir,
port=port,
bind=bind,
response_timeout_seconds=response_timeout_seconds,