refactor(start): show_plan now takes DockerBottleSpec
test / run tests/run_tests.py (pull_request) Successful in 15s
test / run tests/run_tests.py (pull_request) Successful in 15s
This commit is contained in:
+45
-69
@@ -10,7 +10,6 @@ import shutil
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Sequence
|
|
||||||
|
|
||||||
from .. import docker as docker_mod
|
from .. import docker as docker_mod
|
||||||
from .. import pipelock
|
from .. import pipelock
|
||||||
@@ -28,44 +27,44 @@ from ..manifest import Manifest
|
|||||||
from ._common import PROG, USER_CWD, read_tty_line
|
from ._common import PROG, USER_CWD, read_tty_line
|
||||||
|
|
||||||
|
|
||||||
def show_plan(
|
def show_plan(spec: DockerBottleSpec, *, remote_control: bool) -> None:
|
||||||
*,
|
"""Render the y/N preflight summary to stderr. Pure presentation;
|
||||||
agent_name: str,
|
reads manifest-backed fields off `spec` and probes the Docker
|
||||||
image: str,
|
runtime label. `remote_control` is the only field not already on
|
||||||
derived_image: str,
|
the spec — it's a claude CLI flag, not a bottle property."""
|
||||||
user_cwd: str,
|
manifest = spec.manifest
|
||||||
container: str,
|
agent = manifest.agents[spec.agent_name]
|
||||||
stage_dir: Path,
|
bottle = manifest.bottle_for(spec.agent_name)
|
||||||
env_names: Sequence[str],
|
|
||||||
skills: Sequence[str],
|
env_names = list(bottle.env.keys())
|
||||||
docker_runtime: str,
|
if spec.forward_oauth_token:
|
||||||
bottle_name: str,
|
env_names.append("CLAUDE_CODE_OAUTH_TOKEN")
|
||||||
ssh_hosts: Sequence[str],
|
|
||||||
allowlist_summary: str,
|
ssh_hosts = [e.Host for e in bottle.ssh]
|
||||||
prompt_content: str,
|
allowlist_summary = pipelock.pipelock_allowlist_summary(manifest, agent.bottle)
|
||||||
remote_control: bool,
|
prompt_first_line = agent.prompt.splitlines()[0] if agent.prompt else ""
|
||||||
) -> None:
|
|
||||||
"""Render the y/N preflight summary to stderr. Pure presentation; no
|
|
||||||
side effects beyond writing to stderr."""
|
|
||||||
prompt_first_line = prompt_content.splitlines()[0] if prompt_content else ""
|
|
||||||
print(file=sys.stderr)
|
print(file=sys.stderr)
|
||||||
info(f"agent : {agent_name}")
|
info(f"agent : {spec.agent_name}")
|
||||||
info(f"image : {image}")
|
info(f"image : {spec.image}")
|
||||||
if derived_image:
|
if spec.derived_image:
|
||||||
info(f"cwd : {user_cwd} -> /home/node/workspace (derived: {derived_image})")
|
info(
|
||||||
info(f"container : {container}")
|
f"cwd : {spec.user_cwd} -> /home/node/workspace "
|
||||||
info(f"stage dir : {stage_dir}")
|
f"(derived: {spec.derived_image})"
|
||||||
|
)
|
||||||
|
info(f"container : {spec.container_name}")
|
||||||
|
info(f"stage dir : {spec.stage_dir}")
|
||||||
info("env (names only): " + (", ".join(env_names) if env_names else "(none)"))
|
info("env (names only): " + (", ".join(env_names) if env_names else "(none)"))
|
||||||
info("skills : " + (" ".join(skills) if skills else "(none)"))
|
info("skills : " + (" ".join(agent.skills) if agent.skills else "(none)"))
|
||||||
info(f"docker runtime : {docker_runtime}")
|
info(f"docker runtime : {docker_runtime_label()}")
|
||||||
info(f"bottle : {bottle_name}")
|
info(f"bottle : {agent.bottle}")
|
||||||
if ssh_hosts:
|
if ssh_hosts:
|
||||||
info(f" ssh hosts : {', '.join(ssh_hosts)}")
|
info(f" ssh hosts : {', '.join(ssh_hosts)}")
|
||||||
else:
|
else:
|
||||||
info(" ssh hosts : (none)")
|
info(" ssh hosts : (none)")
|
||||||
info(f" egress : {allowlist_summary}")
|
info(f" egress : {allowlist_summary}")
|
||||||
info(
|
info(
|
||||||
f"prompt : {len(prompt_content)} chars; "
|
f"prompt : {len(agent.prompt)} chars; "
|
||||||
f"first line: {prompt_first_line or '(empty)'}"
|
f"first line: {prompt_first_line or '(empty)'}"
|
||||||
)
|
)
|
||||||
info("remote-control : " + ("enabled" if remote_control else "disabled"))
|
info("remote-control : " + ("enabled" if remote_control else "disabled"))
|
||||||
@@ -122,22 +121,15 @@ def cmd_start(argv: list[str]) -> int:
|
|||||||
f"'docker rm -f <name>'"
|
f"'docker rm -f <name>'"
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Plan resolution (host-only, no container yet) ---
|
|
||||||
env_names = list(bottle.env.keys())
|
|
||||||
|
|
||||||
# CLAUDE_BOTTLE_OAUTH_TOKEN → CLAUDE_CODE_OAUTH_TOKEN forwarding.
|
# CLAUDE_BOTTLE_OAUTH_TOKEN → CLAUDE_CODE_OAUTH_TOKEN forwarding.
|
||||||
# Host-side token is always forwarded so every container can authenticate.
|
# Host-side token is always forwarded so every container can authenticate.
|
||||||
forward_oauth_token = bool(os.environ.get("CLAUDE_BOTTLE_OAUTH_TOKEN"))
|
forward_oauth_token = bool(os.environ.get("CLAUDE_BOTTLE_OAUTH_TOKEN"))
|
||||||
display_env_names = list(env_names)
|
|
||||||
if forward_oauth_token:
|
|
||||||
display_env_names.append("CLAUDE_CODE_OAUTH_TOKEN")
|
|
||||||
|
|
||||||
if agent.skills:
|
if agent.skills:
|
||||||
skills_mod.skills_validate_all(list(agent.skills))
|
skills_mod.skills_validate_all(list(agent.skills))
|
||||||
|
|
||||||
ssh_entries = bottle.ssh
|
if bottle.ssh:
|
||||||
if ssh_entries:
|
ssh_mod.ssh_validate_entries(bottle.ssh)
|
||||||
ssh_mod.ssh_validate_entries(ssh_entries)
|
|
||||||
|
|
||||||
stage_dir = Path(tempfile.mkdtemp(prefix="claude-bottle-stage."))
|
stage_dir = Path(tempfile.mkdtemp(prefix="claude-bottle-stage."))
|
||||||
env_file = stage_dir / "agent.env"
|
env_file = stage_dir / "agent.env"
|
||||||
@@ -153,41 +145,12 @@ def cmd_start(argv: list[str]) -> int:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
pipelock.pipelock_write_yaml(manifest, bottle_name, pipelock_yaml)
|
pipelock.pipelock_write_yaml(manifest, bottle_name, pipelock_yaml)
|
||||||
allowlist_summary = pipelock.pipelock_allowlist_summary(manifest, bottle_name)
|
|
||||||
|
|
||||||
env_resolve(manifest, name, env_file, args_file)
|
env_resolve(manifest, name, env_file, args_file)
|
||||||
|
|
||||||
prompt_content = agent.prompt
|
prompt_content = agent.prompt
|
||||||
prompt_file.write_text(prompt_content)
|
prompt_file.write_text(prompt_content)
|
||||||
|
|
||||||
show_plan(
|
|
||||||
agent_name=name,
|
|
||||||
image=image,
|
|
||||||
derived_image=derived_image,
|
|
||||||
user_cwd=USER_CWD,
|
|
||||||
container=container,
|
|
||||||
stage_dir=stage_dir,
|
|
||||||
env_names=display_env_names,
|
|
||||||
skills=agent.skills,
|
|
||||||
docker_runtime=docker_runtime_label(),
|
|
||||||
bottle_name=bottle_name,
|
|
||||||
ssh_hosts=[e.Host for e in ssh_entries],
|
|
||||||
allowlist_summary=allowlist_summary,
|
|
||||||
prompt_content=prompt_content,
|
|
||||||
remote_control=args.remote_control,
|
|
||||||
)
|
|
||||||
|
|
||||||
if dry_run:
|
|
||||||
info("dry-run requested; not starting container.")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
sys.stderr.write("claude-bottle: launch this agent? [y/N] ")
|
|
||||||
sys.stderr.flush()
|
|
||||||
reply = read_tty_line()
|
|
||||||
if reply not in ("y", "Y", "yes", "YES"):
|
|
||||||
info("aborted by user")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
spec = DockerBottleSpec(
|
spec = DockerBottleSpec(
|
||||||
agent_name=name,
|
agent_name=name,
|
||||||
slug=slug,
|
slug=slug,
|
||||||
@@ -208,6 +171,19 @@ def cmd_start(argv: list[str]) -> int:
|
|||||||
forward_oauth_token=forward_oauth_token,
|
forward_oauth_token=forward_oauth_token,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
show_plan(spec, remote_control=args.remote_control)
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
info("dry-run requested; not starting container.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
sys.stderr.write("claude-bottle: launch this agent? [y/N] ")
|
||||||
|
sys.stderr.flush()
|
||||||
|
reply = read_tty_line()
|
||||||
|
if reply not in ("y", "Y", "yes", "YES"):
|
||||||
|
info("aborted by user")
|
||||||
|
return 0
|
||||||
|
|
||||||
factory = get_bottle_factory()
|
factory = get_bottle_factory()
|
||||||
with factory(spec) as bottle_handle:
|
with factory(spec) as bottle_handle:
|
||||||
info(
|
info(
|
||||||
|
|||||||
Reference in New Issue
Block a user