feat(workspace): add shared workspace plan
This commit is contained in:
@@ -46,6 +46,7 @@ from ..log import die, info
|
|||||||
from ..manifest import GitEntry, Manifest
|
from ..manifest import GitEntry, Manifest
|
||||||
from ..supervise import SupervisePlan
|
from ..supervise import SupervisePlan
|
||||||
from ..util import expand_tilde
|
from ..util import expand_tilde
|
||||||
|
from ..workspace import WorkspacePlan
|
||||||
from .print_util import print_multi, visible_agent_env_names
|
from .print_util import print_multi, visible_agent_env_names
|
||||||
from .util import host_skill_dir
|
from .util import host_skill_dir
|
||||||
|
|
||||||
@@ -79,6 +80,7 @@ class BottlePlan(ABC):
|
|||||||
egress_plan: EgressPlan
|
egress_plan: EgressPlan
|
||||||
supervise_plan: SupervisePlan | None
|
supervise_plan: SupervisePlan | None
|
||||||
agent_provision: AgentProvisionPlan
|
agent_provision: AgentProvisionPlan
|
||||||
|
workspace_plan: WorkspacePlan
|
||||||
|
|
||||||
def print(self, *, remote_control: bool) -> None:
|
def print(self, *, remote_control: bool) -> None:
|
||||||
"""Render the y/N preflight summary to stderr."""
|
"""Render the y/N preflight summary to stderr."""
|
||||||
@@ -320,7 +322,7 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
|
|||||||
decide whether to add provider-specific prompt args to the agent's
|
decide whether to add provider-specific prompt args to the agent's
|
||||||
argv.
|
argv.
|
||||||
|
|
||||||
Default orchestration: ca → prompt → skills → git →
|
Default orchestration: ca → prompt → skills → workspace → git →
|
||||||
supervise. CA install runs first so the agent's trust store
|
supervise. CA install runs first so the agent's trust store
|
||||||
is rebuilt before anything inside the agent makes a TLS call.
|
is rebuilt before anything inside the agent makes a TLS call.
|
||||||
Subclasses typically don't override this; they implement the
|
Subclasses typically don't override this; they implement the
|
||||||
@@ -335,6 +337,7 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
|
|||||||
prompt_path = self.provision_prompt(plan, target)
|
prompt_path = self.provision_prompt(plan, target)
|
||||||
self.provision_provider_auth(plan, target)
|
self.provision_provider_auth(plan, target)
|
||||||
self.provision_skills(plan, target)
|
self.provision_skills(plan, target)
|
||||||
|
self.provision_workspace(plan, target)
|
||||||
self.provision_git(plan, target)
|
self.provision_git(plan, target)
|
||||||
self.provision_supervise(plan, target)
|
self.provision_supervise(plan, target)
|
||||||
return prompt_path
|
return prompt_path
|
||||||
@@ -365,6 +368,11 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
|
|||||||
"""Copy the agent's named skills from the host into the
|
"""Copy the agent's named skills from the host into the
|
||||||
running bottle. No-op when the agent has no skills."""
|
running bottle. No-op when the agent has no skills."""
|
||||||
|
|
||||||
|
def provision_workspace(self, plan: PlanT, target: str) -> None:
|
||||||
|
"""Copy the operator workspace into the running bottle when
|
||||||
|
the backend cannot bake it into the agent image. Default is
|
||||||
|
no-op for backends like Docker that handle this before launch."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def provision_git(self, plan: PlanT, target: str) -> None:
|
def provision_git(self, plan: PlanT, target: str) -> None:
|
||||||
"""Copy the host's cwd `.git` directory into the running
|
"""Copy the host's cwd `.git` directory into the running
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from ...git_gate import GitGate
|
|||||||
from ...log import die
|
from ...log import die
|
||||||
from ...pipelock import PipelockProxy
|
from ...pipelock import PipelockProxy
|
||||||
from ...supervise import Supervise
|
from ...supervise import Supervise
|
||||||
|
from ...workspace import workspace_plan as resolve_workspace_plan
|
||||||
from .. import BottleSpec
|
from .. import BottleSpec
|
||||||
from . import util as docker_mod
|
from . import util as docker_mod
|
||||||
from .bottle_plan import DockerBottlePlan
|
from .bottle_plan import DockerBottlePlan
|
||||||
@@ -62,6 +63,8 @@ def resolve_plan(
|
|||||||
bottle = manifest.bottle_for(spec.agent_name)
|
bottle = manifest.bottle_for(spec.agent_name)
|
||||||
provider = bottle.agent_provider
|
provider = bottle.agent_provider
|
||||||
provider_runtime = runtime_for(provider.template)
|
provider_runtime = runtime_for(provider.template)
|
||||||
|
guest_home = os.environ.get("BOT_BOTTLE_CONTAINER_HOME", "/home/node")
|
||||||
|
workspace_plan = resolve_workspace_plan(spec, guest_home=guest_home)
|
||||||
|
|
||||||
# PRD 0016 follow-up: identity, not bare slug. A fresh `start`
|
# PRD 0016 follow-up: identity, not bare slug. A fresh `start`
|
||||||
# mints a random-suffixed identity (so parallel runs of the same
|
# mints a random-suffixed identity (so parallel runs of the same
|
||||||
@@ -177,7 +180,7 @@ def resolve_plan(
|
|||||||
template=provider.template,
|
template=provider.template,
|
||||||
dockerfile=dockerfile_path,
|
dockerfile=dockerfile_path,
|
||||||
state_dir=agent_dir,
|
state_dir=agent_dir,
|
||||||
guest_home=os.environ.get("BOT_BOTTLE_CONTAINER_HOME", "/home/node"),
|
guest_home=guest_home,
|
||||||
forward_host_credentials=provider.forward_host_credentials,
|
forward_host_credentials=provider.forward_host_credentials,
|
||||||
auth_token=provider.auth_token,
|
auth_token=provider.auth_token,
|
||||||
host_env=dict(os.environ),
|
host_env=dict(os.environ),
|
||||||
@@ -245,6 +248,7 @@ def resolve_plan(
|
|||||||
supervise_plan=supervise_plan,
|
supervise_plan=supervise_plan,
|
||||||
use_runsc=use_runsc,
|
use_runsc=use_runsc,
|
||||||
agent_provision=agent_provision,
|
agent_provision=agent_provision,
|
||||||
|
workspace_plan=workspace_plan,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from ...env import resolve_env
|
|||||||
from ...git_gate import GitGate
|
from ...git_gate import GitGate
|
||||||
from ...pipelock import PipelockProxy
|
from ...pipelock import PipelockProxy
|
||||||
from ...supervise import Supervise
|
from ...supervise import Supervise
|
||||||
|
from ...workspace import workspace_plan as resolve_workspace_plan
|
||||||
from .bottle_plan import SmolmachinesBottlePlan
|
from .bottle_plan import SmolmachinesBottlePlan
|
||||||
from .util import smolmachines_bundle_subnet, smolmachines_preflight
|
from .util import smolmachines_bundle_subnet, smolmachines_preflight
|
||||||
|
|
||||||
@@ -60,6 +61,8 @@ def resolve_plan(
|
|||||||
bottle = manifest.bottle_for(spec.agent_name)
|
bottle = manifest.bottle_for(spec.agent_name)
|
||||||
provider = bottle.agent_provider
|
provider = bottle.agent_provider
|
||||||
provider_runtime = runtime_for(provider.template)
|
provider_runtime = runtime_for(provider.template)
|
||||||
|
guest_home = os.environ.get("BOT_BOTTLE_GUEST_HOME", "/home/node")
|
||||||
|
workspace_plan = resolve_workspace_plan(spec, guest_home=guest_home)
|
||||||
|
|
||||||
slug = spec.identity or bottle_identity(spec.agent_name)
|
slug = spec.identity or bottle_identity(spec.agent_name)
|
||||||
|
|
||||||
@@ -130,7 +133,7 @@ def resolve_plan(
|
|||||||
template=provider.template,
|
template=provider.template,
|
||||||
dockerfile=agent_dockerfile_path,
|
dockerfile=agent_dockerfile_path,
|
||||||
state_dir=agent_dir,
|
state_dir=agent_dir,
|
||||||
guest_home=os.environ.get("BOT_BOTTLE_GUEST_HOME", "/home/node"),
|
guest_home=guest_home,
|
||||||
guest_env=guest_env,
|
guest_env=guest_env,
|
||||||
forward_host_credentials=provider.forward_host_credentials,
|
forward_host_credentials=provider.forward_host_credentials,
|
||||||
auth_token=provider.auth_token,
|
auth_token=provider.auth_token,
|
||||||
@@ -181,6 +184,7 @@ def resolve_plan(
|
|||||||
egress_plan=egress_plan,
|
egress_plan=egress_plan,
|
||||||
supervise_plan=supervise_plan,
|
supervise_plan=supervise_plan,
|
||||||
agent_provision=agent_provision,
|
agent_provision=agent_provision,
|
||||||
|
workspace_plan=workspace_plan,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"""Backend-neutral plan for porting the operator workspace."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
|
||||||
|
WORKSPACE_DIRNAME = "workspace"
|
||||||
|
DEFAULT_WORKSPACE_OWNER = "node:node"
|
||||||
|
DEFAULT_WORKSPACE_MODE = "755"
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceSpec(Protocol):
|
||||||
|
copy_cwd: bool
|
||||||
|
user_cwd: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class WorkspacePlan:
|
||||||
|
"""Resolved workspace contract shared by all bottle backends."""
|
||||||
|
|
||||||
|
enabled: bool
|
||||||
|
host_path: Path
|
||||||
|
guest_home: str
|
||||||
|
guest_path: str
|
||||||
|
workdir: str
|
||||||
|
owner: str = DEFAULT_WORKSPACE_OWNER
|
||||||
|
mode: str = DEFAULT_WORKSPACE_MODE
|
||||||
|
copy_contents: bool = True
|
||||||
|
copy_git: bool = True
|
||||||
|
has_host_git_dir: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
def workspace_plan(spec: WorkspaceSpec, *, guest_home: str) -> WorkspacePlan:
|
||||||
|
"""Resolve the in-bottle workspace path from CLI intent."""
|
||||||
|
host_path = Path(spec.user_cwd).expanduser()
|
||||||
|
if spec.copy_cwd:
|
||||||
|
guest_path = f"{guest_home.rstrip('/')}/{WORKSPACE_DIRNAME}"
|
||||||
|
workdir = guest_path
|
||||||
|
else:
|
||||||
|
guest_path = guest_home
|
||||||
|
workdir = guest_home
|
||||||
|
return WorkspacePlan(
|
||||||
|
enabled=spec.copy_cwd,
|
||||||
|
host_path=host_path,
|
||||||
|
guest_home=guest_home,
|
||||||
|
guest_path=guest_path,
|
||||||
|
workdir=workdir,
|
||||||
|
has_host_git_dir=(host_path / ".git").is_dir(),
|
||||||
|
)
|
||||||
@@ -33,6 +33,7 @@ from bot_bottle.git_gate import GitGatePlan, GitGateUpstream
|
|||||||
from bot_bottle.manifest import Manifest
|
from bot_bottle.manifest import Manifest
|
||||||
from bot_bottle.pipelock import PipelockProxyPlan
|
from bot_bottle.pipelock import PipelockProxyPlan
|
||||||
from bot_bottle.supervise import SupervisePlan
|
from bot_bottle.supervise import SupervisePlan
|
||||||
|
from bot_bottle.workspace import workspace_plan
|
||||||
|
|
||||||
|
|
||||||
SLUG = "demo-abc12"
|
SLUG = "demo-abc12"
|
||||||
@@ -163,8 +164,9 @@ def _plan(
|
|||||||
roles=(),
|
roles=(),
|
||||||
),)
|
),)
|
||||||
|
|
||||||
|
spec = _spec(supervise=supervise, with_git=with_git, with_egress=with_egress)
|
||||||
return DockerBottlePlan(
|
return DockerBottlePlan(
|
||||||
spec=_spec(supervise=supervise, with_git=with_git, with_egress=with_egress),
|
spec=spec,
|
||||||
stage_dir=STAGE,
|
stage_dir=STAGE,
|
||||||
slug=SLUG,
|
slug=SLUG,
|
||||||
container_name=f"bot-bottle-{SLUG}",
|
container_name=f"bot-bottle-{SLUG}",
|
||||||
@@ -189,6 +191,7 @@ def _plan(
|
|||||||
dockerfile="",
|
dockerfile="",
|
||||||
guest_env={},
|
guest_env={},
|
||||||
),
|
),
|
||||||
|
workspace_plan=workspace_plan(spec, guest_home="/home/node"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ 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 Manifest
|
from bot_bottle.manifest import Manifest
|
||||||
from bot_bottle.pipelock import PipelockProxyPlan
|
from bot_bottle.pipelock import PipelockProxyPlan
|
||||||
|
from bot_bottle.workspace import workspace_plan
|
||||||
|
|
||||||
|
|
||||||
def _plan(*, git_user: dict | None = None,
|
def _plan(*, git_user: dict | None = None,
|
||||||
@@ -75,6 +76,7 @@ def _plan(*, git_user: dict | None = None,
|
|||||||
dockerfile="",
|
dockerfile="",
|
||||||
guest_env={},
|
guest_env={},
|
||||||
),
|
),
|
||||||
|
workspace_plan=workspace_plan(spec, guest_home="/home/node"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ 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 Manifest
|
from bot_bottle.manifest import Manifest
|
||||||
from bot_bottle.pipelock import PipelockProxyPlan
|
from bot_bottle.pipelock import PipelockProxyPlan
|
||||||
|
from bot_bottle.workspace import workspace_plan
|
||||||
|
|
||||||
|
|
||||||
def _plan(
|
def _plan(
|
||||||
@@ -29,13 +30,14 @@ def _plan(
|
|||||||
"bottles": {"dev": {"agent_provider": {"template": "codex"}}},
|
"bottles": {"dev": {"agent_provider": {"template": "codex"}}},
|
||||||
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
|
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
|
||||||
})
|
})
|
||||||
|
spec = BottleSpec(
|
||||||
|
manifest=manifest,
|
||||||
|
agent_name="demo",
|
||||||
|
copy_cwd=False,
|
||||||
|
user_cwd="/tmp/x",
|
||||||
|
)
|
||||||
return DockerBottlePlan(
|
return DockerBottlePlan(
|
||||||
spec=BottleSpec(
|
spec=spec,
|
||||||
manifest=manifest,
|
|
||||||
agent_name="demo",
|
|
||||||
copy_cwd=False,
|
|
||||||
user_cwd="/tmp/x",
|
|
||||||
),
|
|
||||||
stage_dir=Path("/tmp/stage"),
|
stage_dir=Path("/tmp/stage"),
|
||||||
slug="demo-abc12",
|
slug="demo-abc12",
|
||||||
container_name="bot-bottle-demo-abc12",
|
container_name="bot-bottle-demo-abc12",
|
||||||
@@ -69,6 +71,7 @@ def _plan(
|
|||||||
agent_provision=_agent_provision(
|
agent_provision=_agent_provision(
|
||||||
agent_provider_template, codex_auth_file=codex_auth_file,
|
agent_provider_template, codex_auth_file=codex_auth_file,
|
||||||
),
|
),
|
||||||
|
workspace_plan=workspace_plan(spec, guest_home="/home/node"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ 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 Manifest
|
from bot_bottle.manifest import Manifest
|
||||||
from bot_bottle.pipelock import PipelockProxyPlan
|
from bot_bottle.pipelock import PipelockProxyPlan
|
||||||
|
from bot_bottle.workspace import workspace_plan
|
||||||
|
|
||||||
|
|
||||||
def _manifest() -> Manifest:
|
def _manifest() -> Manifest:
|
||||||
@@ -109,6 +110,7 @@ def _docker_plan(spec: BottleSpec, tmp: str) -> DockerBottlePlan:
|
|||||||
egress_plan=_egress_plan(tmp),
|
egress_plan=_egress_plan(tmp),
|
||||||
supervise_plan=None,
|
supervise_plan=None,
|
||||||
agent_provision=_agent_provision(),
|
agent_provision=_agent_provision(),
|
||||||
|
workspace_plan=workspace_plan(spec, guest_home="/home/node"),
|
||||||
slug="test-00001",
|
slug="test-00001",
|
||||||
container_name="bot-bottle-test-00001",
|
container_name="bot-bottle-test-00001",
|
||||||
container_name_pinned=False,
|
container_name_pinned=False,
|
||||||
@@ -133,6 +135,7 @@ def _smolmachines_plan(spec: BottleSpec, tmp: str) -> SmolmachinesBottlePlan:
|
|||||||
egress_plan=_egress_plan(tmp),
|
egress_plan=_egress_plan(tmp),
|
||||||
supervise_plan=None,
|
supervise_plan=None,
|
||||||
agent_provision=_agent_provision(),
|
agent_provision=_agent_provision(),
|
||||||
|
workspace_plan=workspace_plan(spec, guest_home="/home/node"),
|
||||||
slug="test-00001",
|
slug="test-00001",
|
||||||
bundle_subnet="10.99.0.0/24",
|
bundle_subnet="10.99.0.0/24",
|
||||||
bundle_gateway="10.99.0.1",
|
bundle_gateway="10.99.0.1",
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ from bot_bottle.git_gate import GitGatePlan, GitGateUpstream
|
|||||||
from bot_bottle.manifest import GitEntry, Manifest
|
from bot_bottle.manifest import GitEntry, Manifest
|
||||||
from bot_bottle.pipelock import PipelockProxyPlan
|
from bot_bottle.pipelock import PipelockProxyPlan
|
||||||
from bot_bottle.supervise import SupervisePlan
|
from bot_bottle.supervise import SupervisePlan
|
||||||
|
from bot_bottle.workspace import workspace_plan
|
||||||
|
|
||||||
|
|
||||||
def _remote_host(g: GitEntry) -> str:
|
def _remote_host(g: GitEntry) -> str:
|
||||||
@@ -144,6 +145,7 @@ def _plan(
|
|||||||
codex_auth_file=codex_auth_file,
|
codex_auth_file=codex_auth_file,
|
||||||
guest_env=dict(guest_env or {}),
|
guest_env=dict(guest_env or {}),
|
||||||
),
|
),
|
||||||
|
workspace_plan=workspace_plan(spec, guest_home="/home/node"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user