fix(pi): prepare runtime state and agent workdir
This commit is contained in:
@@ -26,6 +26,7 @@ class DockerBottle(Bottle):
|
||||
agent_provider_template: str = "claude",
|
||||
terminal_title: str = "",
|
||||
terminal_color: str = "",
|
||||
agent_workdir: str = "/home/node",
|
||||
):
|
||||
self.name = container
|
||||
self._teardown = teardown
|
||||
@@ -35,6 +36,7 @@ class DockerBottle(Bottle):
|
||||
self.terminal_title = terminal_title
|
||||
self.terminal_color = terminal_color
|
||||
self.agent_provider_template = agent_provider_template
|
||||
self.agent_workdir = agent_workdir
|
||||
self._closed = False
|
||||
|
||||
def agent_argv(
|
||||
@@ -47,6 +49,8 @@ class DockerBottle(Bottle):
|
||||
cmd = ["docker", "exec"]
|
||||
if tty:
|
||||
cmd.append("-it")
|
||||
if self.agent_workdir and self.agent_workdir != "/home/node":
|
||||
cmd.extend(["-w", self.agent_workdir])
|
||||
cmd.extend([self.name, self.agent_command, *full_argv])
|
||||
return cmd
|
||||
|
||||
|
||||
@@ -178,6 +178,7 @@ def launch(
|
||||
agent_provider_template=plan.agent_provider_template,
|
||||
terminal_title=plan.spec.label or plan.spec.agent_name,
|
||||
terminal_color=plan.spec.color,
|
||||
agent_workdir=plan.workspace_plan.workdir,
|
||||
)
|
||||
bottle.prompt_path = provision(plan, bottle)
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from __future__ import annotations
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import shlex
|
||||
from typing import Mapping, cast
|
||||
|
||||
from ...agent_provider import PromptMode, prompt_args
|
||||
@@ -72,6 +73,7 @@ class SmolmachinesBottle(Bottle):
|
||||
agent_provider_template: str = "claude",
|
||||
terminal_title: str = "",
|
||||
terminal_color: str = "",
|
||||
agent_workdir: str = "/home/node",
|
||||
) -> None:
|
||||
self.name = machine_name
|
||||
# In-VM path to the agent's prompt file. None when the
|
||||
@@ -88,6 +90,7 @@ class SmolmachinesBottle(Bottle):
|
||||
self.terminal_title = terminal_title
|
||||
self.terminal_color = terminal_color
|
||||
self.agent_provider_template = agent_provider_template
|
||||
self.agent_workdir = agent_workdir
|
||||
|
||||
def agent_argv(
|
||||
self, argv: list[str], *, tty: bool = True,
|
||||
@@ -95,8 +98,14 @@ class SmolmachinesBottle(Bottle):
|
||||
flags = ["smolvm", "machine", "exec", "--name", self.name]
|
||||
if tty:
|
||||
flags += ["-i", "-t"]
|
||||
agent_tail = ["env", *_env_assignments_for("node", self._guest_env),
|
||||
self.agent_command]
|
||||
agent_tail = ["env", *_env_assignments_for("node", self._guest_env)]
|
||||
if self.agent_workdir and self.agent_workdir != _HOME_FOR["node"]:
|
||||
agent_tail += [
|
||||
"sh", "-lc",
|
||||
f"cd {shlex.quote(self.agent_workdir)} && exec \"$@\"",
|
||||
"bot-bottle-agent",
|
||||
]
|
||||
agent_tail.append(self.agent_command)
|
||||
provider_prompt_args = prompt_args(
|
||||
cast(PromptMode, self._agent_prompt_mode), self.prompt_path, argv=argv,
|
||||
)
|
||||
|
||||
@@ -106,6 +106,7 @@ def launch(
|
||||
agent_provider_template=plan.agent_provider_template,
|
||||
terminal_title=plan.spec.label or plan.spec.agent_name,
|
||||
terminal_color=plan.spec.color,
|
||||
agent_workdir=plan.workspace_plan.workdir,
|
||||
)
|
||||
bottle.prompt_path = provision(plan, bottle)
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ FROM node:22-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
git \
|
||||
ca-certificates \
|
||||
curl \
|
||||
fd-find \
|
||||
ripgrep \
|
||||
git \
|
||||
ca-certificates \
|
||||
curl \
|
||||
fd-find \
|
||||
ripgrep \
|
||||
&& ln -s /usr/bin/fdfind /usr/local/bin/fd \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -21,9 +21,21 @@ RUN apt-get update \
|
||||
RUN npm install -g --ignore-scripts --no-fund --no-audit @earendil-works/pi-coding-agent \
|
||||
&& npm cache clean --force
|
||||
|
||||
RUN mkdir -p /home/node/.pi/agent \
|
||||
/home/node/.pi/context-mode/sessions \
|
||||
/tmp/pi-subagents-uid-1000 \
|
||||
&& chown -R node:node /home/node/.pi /tmp \
|
||||
&& chmod -R u+rwX /tmp \
|
||||
&& chown root:root /tmp /var/tmp \
|
||||
&& chmod 1777 /tmp /var/tmp
|
||||
|
||||
USER node
|
||||
WORKDIR /home/node
|
||||
|
||||
RUN mkdir -p /home/node/.pi/agent
|
||||
RUN pi install npm:@harms-haus/pi-cwd \
|
||||
&& pi install npm:pi-web-access \
|
||||
&& pi install npm:context-mode \
|
||||
&& pi install npm:pi-subagents \
|
||||
&& pi install npm:pi-mcp-adapter
|
||||
|
||||
CMD ["pi"]
|
||||
|
||||
@@ -33,6 +33,8 @@ if TYPE_CHECKING:
|
||||
_DEFAULT_BASE_URL = "http://ollama:11434/v1"
|
||||
_DEFAULT_MODEL = "qwen2.5-coder:7b"
|
||||
_DEFAULT_PROVIDER_NAME = "ollama"
|
||||
_DEFAULT_CONTEXT_WINDOW = 4096
|
||||
_DEFAULT_MAX_TOKENS = 1024
|
||||
|
||||
|
||||
def _skills_dir(guest_home: str) -> str:
|
||||
@@ -43,10 +45,29 @@ def _prompt_path(guest_home: str) -> str:
|
||||
return f"{guest_home}/.bot-bottle-prompt.txt"
|
||||
|
||||
|
||||
def _append_system_path(guest_home: str) -> str:
|
||||
return f"{guest_home}/.pi/agent/APPEND_SYSTEM.md"
|
||||
|
||||
|
||||
def _models_path(guest_home: str) -> str:
|
||||
return f"{guest_home}/.pi/agent/models.json"
|
||||
|
||||
|
||||
def _runtime_state_repair_script(guest_home: str) -> str:
|
||||
home = shlex.quote(guest_home)
|
||||
pi_home = shlex.quote(f"{guest_home}/.pi")
|
||||
context_sessions = shlex.quote(f"{guest_home}/.pi/context-mode/sessions")
|
||||
return (
|
||||
f"mkdir -p {context_sessions} /tmp/pi-subagents-uid-1000 && "
|
||||
f"chown node:node {home} && "
|
||||
f"chown -R node:node {pi_home} /tmp && "
|
||||
"chmod -R u+rwX /tmp && "
|
||||
f"chmod 755 {home} && "
|
||||
"chown root:root /tmp /var/tmp && "
|
||||
"chmod 1777 /tmp /var/tmp"
|
||||
)
|
||||
|
||||
|
||||
def _settings_value(
|
||||
settings: dict[str, object],
|
||||
key: str,
|
||||
@@ -74,14 +95,33 @@ def _pi_models_json(
|
||||
supports_reasoning_effort = bool(
|
||||
_settings_value(settings, "supports_reasoning_effort", False)
|
||||
)
|
||||
max_tokens_field = str(
|
||||
_settings_value(settings, "max_tokens_field", "max_tokens")
|
||||
)
|
||||
context_window = int(_settings_value(
|
||||
settings, "context_window", _DEFAULT_CONTEXT_WINDOW,
|
||||
))
|
||||
max_tokens = int(_settings_value(
|
||||
settings, "max_tokens", _DEFAULT_MAX_TOKENS,
|
||||
))
|
||||
input_context_window = max(1, context_window - max_tokens)
|
||||
provider: dict[str, object] = {
|
||||
"baseUrl": base_url,
|
||||
"api": api,
|
||||
"compat": {
|
||||
"supportsDeveloperRole": supports_developer_role,
|
||||
"supportsReasoningEffort": supports_reasoning_effort,
|
||||
"maxTokensField": max_tokens_field,
|
||||
},
|
||||
"models": [{"id": model} for model in models],
|
||||
"models": [
|
||||
{
|
||||
"id": model,
|
||||
"name": model,
|
||||
"contextWindow": input_context_window,
|
||||
"maxTokens": max_tokens,
|
||||
}
|
||||
for model in models
|
||||
],
|
||||
}
|
||||
if api_key is not None:
|
||||
provider["apiKey"] = str(api_key)
|
||||
@@ -201,16 +241,28 @@ class PiAgentProvider(AgentProvider):
|
||||
|
||||
def provision_prompt(self, plan: "BottlePlan", bottle: "Bottle") -> str | None:
|
||||
prompt_path = _prompt_path(plan.guest_home)
|
||||
append_system_path = _append_system_path(plan.guest_home)
|
||||
bottle.cp_in(str(plan.prompt_file), prompt_path) # type: ignore
|
||||
bottle.exec(
|
||||
f"chown node:node {prompt_path} && chmod 600 {prompt_path}",
|
||||
f"mkdir -p {shlex.quote(plan.guest_home)}/.pi/agent && "
|
||||
f"cp {shlex.quote(prompt_path)} {shlex.quote(append_system_path)} && "
|
||||
f"chown node:node {shlex.quote(prompt_path)} "
|
||||
f"{shlex.quote(append_system_path)} && "
|
||||
f"chmod 600 {shlex.quote(prompt_path)} "
|
||||
f"{shlex.quote(append_system_path)}",
|
||||
user="root",
|
||||
)
|
||||
agent = plan.spec.manifest.agents[plan.spec.agent_name]
|
||||
return prompt_path if plan.agent_provision.has_prompt or agent.prompt else None
|
||||
# Pi's `--append-system-prompt` takes literal text, not a file path.
|
||||
# Use its documented APPEND_SYSTEM.md discovery path instead.
|
||||
return None
|
||||
|
||||
def provision(self, plan: "BottlePlan", bottle: "Bottle") -> None:
|
||||
provision = plan.agent_provision
|
||||
_exec(
|
||||
bottle,
|
||||
_runtime_state_repair_script(plan.guest_home),
|
||||
"could not prepare pi runtime state",
|
||||
)
|
||||
for d in provision.dirs:
|
||||
path = shlex.quote(d.guest_path)
|
||||
_exec(bottle, f"mkdir -p {path}", f"could not create {d.guest_path}")
|
||||
|
||||
@@ -212,6 +212,9 @@ def _parse_provider_settings(
|
||||
"api_key",
|
||||
"api_key_env",
|
||||
"models",
|
||||
"context_window",
|
||||
"max_tokens_field",
|
||||
"max_tokens",
|
||||
"supports_developer_role",
|
||||
"supports_reasoning_effort",
|
||||
}
|
||||
@@ -228,6 +231,14 @@ def _parse_provider_settings(
|
||||
f"bottle '{bottle_name}' agent_provider.settings.{key} must "
|
||||
"be a non-empty string"
|
||||
)
|
||||
max_tokens_field = settings.get("max_tokens_field")
|
||||
if max_tokens_field is not None and max_tokens_field not in (
|
||||
"max_tokens", "max_completion_tokens",
|
||||
):
|
||||
raise ManifestError(
|
||||
f"bottle '{bottle_name}' agent_provider.settings.max_tokens_field "
|
||||
"must be 'max_tokens' or 'max_completion_tokens'"
|
||||
)
|
||||
if settings.get("api_key") is not None and settings.get("api_key_env") is not None:
|
||||
raise ManifestError(
|
||||
f"bottle '{bottle_name}' agent_provider.settings may set either "
|
||||
@@ -253,4 +264,13 @@ def _parse_provider_settings(
|
||||
f"bottle '{bottle_name}' agent_provider.settings.{key} must "
|
||||
f"be a boolean (was {type(value).__name__})"
|
||||
)
|
||||
for key in ("context_window", "max_tokens"):
|
||||
value = settings.get(key)
|
||||
if value is not None and (
|
||||
not isinstance(value, int) or isinstance(value, bool) or value <= 0
|
||||
):
|
||||
raise ManifestError(
|
||||
f"bottle '{bottle_name}' agent_provider.settings.{key} must "
|
||||
f"be a positive integer (was {type(value).__name__})"
|
||||
)
|
||||
return dict(settings)
|
||||
|
||||
@@ -66,12 +66,20 @@ Supported keys:
|
||||
- `api_key`: string, defaults to `ollama`
|
||||
- `api_key_env`: string, optional host env var name for egress auth injection
|
||||
- `models`: non-empty array of strings, defaults to `["qwen2.5-coder:7b"]`
|
||||
- `context_window`: positive integer, defaults to `4096`; this is the Ollama
|
||||
runtime context, and bot-bottle subtracts `max_tokens` before writing Pi's
|
||||
`contextWindow` so output space is reserved
|
||||
- `max_tokens`: positive integer, defaults to `1024`
|
||||
- `max_tokens_field`: `max_tokens` or `max_completion_tokens`, defaults to
|
||||
`max_tokens`
|
||||
- `supports_developer_role`: boolean, defaults to `false`
|
||||
- `supports_reasoning_effort`: boolean, defaults to `false`
|
||||
|
||||
The snake-case manifest keys are converted into Pi's JSON field names:
|
||||
`baseUrl`, `apiKey`, `supportsDeveloperRole`, and
|
||||
`supportsReasoningEffort`.
|
||||
`baseUrl`, `apiKey`, `contextWindow`, `maxTokens`,
|
||||
`supportsDeveloperRole`, and `supportsReasoningEffort`. `context_window`
|
||||
describes the server's total context; Pi's `contextWindow` receives
|
||||
`context_window - max_tokens` because Pi uses it as an input compaction target.
|
||||
|
||||
`api_key` and `api_key_env` are mutually exclusive. When targeting a hosted
|
||||
provider through bot-bottle's egress sidecar, omit `api_key` and set
|
||||
|
||||
@@ -289,7 +289,16 @@ class TestAgentProviderRuntime(unittest.TestCase):
|
||||
self.assertEqual("http://ollama:11434/v1", provider["baseUrl"])
|
||||
self.assertEqual("openai-completions", provider["api"])
|
||||
self.assertEqual("ollama", provider["apiKey"])
|
||||
self.assertEqual([{"id": "qwen2.5-coder:7b"}], provider["models"])
|
||||
self.assertEqual("max_tokens", provider["compat"]["maxTokensField"])
|
||||
self.assertEqual(
|
||||
[{
|
||||
"id": "qwen2.5-coder:7b",
|
||||
"name": "qwen2.5-coder:7b",
|
||||
"contextWindow": 3072,
|
||||
"maxTokens": 1024,
|
||||
}],
|
||||
provider["models"],
|
||||
)
|
||||
self.assertEqual("ollama", plan.egress_routes[0].host)
|
||||
self.assertEqual("", plan.egress_routes[0].auth_scheme)
|
||||
self.assertEqual("", plan.egress_routes[0].token_ref)
|
||||
@@ -307,6 +316,9 @@ class TestAgentProviderRuntime(unittest.TestCase):
|
||||
"api": "openai-responses",
|
||||
"api_key": "local",
|
||||
"models": ["gpt-oss:20b", "qwen3:14b"],
|
||||
"context_window": 65536,
|
||||
"max_tokens_field": "max_completion_tokens",
|
||||
"max_tokens": 12000,
|
||||
"supports_developer_role": True,
|
||||
"supports_reasoning_effort": True,
|
||||
},
|
||||
@@ -317,11 +329,28 @@ class TestAgentProviderRuntime(unittest.TestCase):
|
||||
self.assertEqual("openai-responses", provider["api"])
|
||||
self.assertEqual("local", provider["apiKey"])
|
||||
self.assertEqual(
|
||||
[{"id": "gpt-oss:20b"}, {"id": "qwen3:14b"}],
|
||||
[
|
||||
{
|
||||
"id": "gpt-oss:20b",
|
||||
"name": "gpt-oss:20b",
|
||||
"contextWindow": 53536,
|
||||
"maxTokens": 12000,
|
||||
},
|
||||
{
|
||||
"id": "qwen3:14b",
|
||||
"name": "qwen3:14b",
|
||||
"contextWindow": 53536,
|
||||
"maxTokens": 12000,
|
||||
},
|
||||
],
|
||||
provider["models"],
|
||||
)
|
||||
self.assertTrue(provider["compat"]["supportsDeveloperRole"])
|
||||
self.assertTrue(provider["compat"]["supportsReasoningEffort"])
|
||||
self.assertEqual(
|
||||
"max_completion_tokens",
|
||||
provider["compat"]["maxTokensField"],
|
||||
)
|
||||
|
||||
def test_pi_plan_can_target_openrouter_with_egress_injected_api_key(self):
|
||||
with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp:
|
||||
@@ -345,8 +374,14 @@ class TestAgentProviderRuntime(unittest.TestCase):
|
||||
self.assertEqual("https://openrouter.ai/api/v1", provider["baseUrl"])
|
||||
self.assertEqual("openai-completions", provider["api"])
|
||||
self.assertEqual("egress-placeholder", provider["apiKey"])
|
||||
self.assertEqual("max_tokens", provider["compat"]["maxTokensField"])
|
||||
self.assertEqual(
|
||||
[{"id": "google/gemma-4-26b-a4b-it:free"}],
|
||||
[{
|
||||
"id": "google/gemma-4-26b-a4b-it:free",
|
||||
"name": "google/gemma-4-26b-a4b-it:free",
|
||||
"contextWindow": 3072,
|
||||
"maxTokens": 1024,
|
||||
}],
|
||||
provider["models"],
|
||||
)
|
||||
self.assertEqual(
|
||||
|
||||
@@ -20,6 +20,7 @@ from bot_bottle.manifest import Manifest
|
||||
|
||||
|
||||
_URL = "http://supervise:9100/"
|
||||
_PI_DOCKERFILE = Path(__file__).resolve().parents[2] / "bot_bottle/contrib/pi/Dockerfile"
|
||||
|
||||
|
||||
def _make_bottle(exec_result: ExecResult | None = None) -> MagicMock:
|
||||
@@ -93,7 +94,7 @@ class TestPiProvisionPrompt(unittest.TestCase):
|
||||
result = PiAgentProvider().provision_prompt(
|
||||
_plan(agent_prompt="hello"), bottle,
|
||||
)
|
||||
self.assertEqual("/home/node/.bot-bottle-prompt.txt", result)
|
||||
self.assertIsNone(result)
|
||||
bottle.cp_in.assert_called_once_with(
|
||||
"/tmp/state/demo-abc12/agent/prompt.txt",
|
||||
"/home/node/.bot-bottle-prompt.txt",
|
||||
@@ -102,6 +103,12 @@ class TestPiProvisionPrompt(unittest.TestCase):
|
||||
self.assertTrue(
|
||||
any("chown node:node" in s
|
||||
and "/home/node/.bot-bottle-prompt.txt" in s
|
||||
and "/home/node/.pi/agent/APPEND_SYSTEM.md" in s
|
||||
for s in scripts)
|
||||
)
|
||||
self.assertTrue(
|
||||
any("cp /home/node/.bot-bottle-prompt.txt" in s
|
||||
and "/home/node/.pi/agent/APPEND_SYSTEM.md" in s
|
||||
for s in scripts)
|
||||
)
|
||||
|
||||
@@ -165,6 +172,14 @@ class TestPiProvision(unittest.TestCase):
|
||||
self.assertTrue(
|
||||
any("mkdir -p" in s and "/home/node/.pi/agent" in s for s in scripts)
|
||||
)
|
||||
self.assertTrue(
|
||||
any("/home/node/.pi/context-mode/sessions" in s
|
||||
and "/tmp/pi-subagents-uid-1000" in s
|
||||
and "chown node:node /home/node" in s
|
||||
and "chown -R node:node /home/node/.pi /tmp" in s
|
||||
and "chmod 755 /home/node" in s
|
||||
for s in scripts)
|
||||
)
|
||||
self.assertTrue(
|
||||
any("chown" in s and "/home/node/.pi/agent/models.json" in s
|
||||
for s in scripts)
|
||||
@@ -191,5 +206,20 @@ class TestPiSuperviseMcp(unittest.TestCase):
|
||||
bottle.exec.assert_not_called()
|
||||
|
||||
|
||||
class TestPiDockerfile(unittest.TestCase):
|
||||
def test_installs_pi_cwd_at_build_time(self):
|
||||
dockerfile = _PI_DOCKERFILE.read_text()
|
||||
self.assertIn("pi install npm:@harms-haus/pi-cwd", dockerfile)
|
||||
|
||||
def test_prepares_pi_extension_state_dirs_and_tmp_for_node(self):
|
||||
dockerfile = _PI_DOCKERFILE.read_text()
|
||||
self.assertIn("/home/node/.pi/context-mode/sessions", dockerfile)
|
||||
self.assertIn("/tmp/pi-subagents-uid-1000", dockerfile)
|
||||
self.assertIn("chown -R node:node /home/node/.pi /tmp", dockerfile)
|
||||
self.assertIn("chmod -R u+rwX /tmp", dockerfile)
|
||||
self.assertIn("chown root:root /tmp /var/tmp", dockerfile)
|
||||
self.assertIn("chmod 1777 /tmp /var/tmp", dockerfile)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -41,6 +41,15 @@ def _pi_bottle(prompt_path: str | None = None) -> DockerBottle:
|
||||
)
|
||||
|
||||
|
||||
def _workspace_bottle() -> DockerBottle:
|
||||
return DockerBottle(
|
||||
container="bot-bottle-dev-abc",
|
||||
teardown=lambda: None,
|
||||
prompt_path_in_container=None,
|
||||
agent_workdir="/home/node/workspace",
|
||||
)
|
||||
|
||||
|
||||
class TestClaudeArgv(unittest.TestCase):
|
||||
def test_minimal_argv_no_prompt(self):
|
||||
argv = _bottle().agent_argv([])
|
||||
@@ -89,6 +98,16 @@ class TestClaudeArgv(unittest.TestCase):
|
||||
argv,
|
||||
)
|
||||
|
||||
def test_workspace_workdir_is_used_when_set(self):
|
||||
argv = _workspace_bottle().agent_argv([])
|
||||
self.assertEqual(
|
||||
[
|
||||
"docker", "exec", "-it", "-w", "/home/node/workspace",
|
||||
"bot-bottle-dev-abc", "claude",
|
||||
],
|
||||
argv,
|
||||
)
|
||||
|
||||
def test_caller_argv_not_mutated(self):
|
||||
# `agent_argv` builds `full_argv` from a copy, so a
|
||||
# caller passing a long-lived list (e.g., the dashboard's
|
||||
|
||||
@@ -120,6 +120,9 @@ class TestAgentProviderHostCredentials(unittest.TestCase):
|
||||
"api": "openai-completions",
|
||||
"api_key": "ollama",
|
||||
"models": ["qwen2.5-coder:7b"],
|
||||
"context_window": 65536,
|
||||
"max_tokens_field": "max_tokens",
|
||||
"max_tokens": 12000,
|
||||
"supports_developer_role": False,
|
||||
"supports_reasoning_effort": False,
|
||||
},
|
||||
@@ -131,6 +134,9 @@ class TestAgentProviderHostCredentials(unittest.TestCase):
|
||||
"api": "openai-completions",
|
||||
"api_key": "ollama",
|
||||
"models": ["qwen2.5-coder:7b"],
|
||||
"context_window": 65536,
|
||||
"max_tokens_field": "max_tokens",
|
||||
"max_tokens": 12000,
|
||||
"supports_developer_role": False,
|
||||
"supports_reasoning_effort": False,
|
||||
},
|
||||
|
||||
@@ -37,6 +37,14 @@ def _pi_bottle(prompt_path: str | None = None) -> SmolmachinesBottle:
|
||||
)
|
||||
|
||||
|
||||
def _workspace_bottle() -> SmolmachinesBottle:
|
||||
return SmolmachinesBottle(
|
||||
"bot-bottle-dev-abc",
|
||||
prompt_path=None,
|
||||
agent_workdir="/home/node/workspace",
|
||||
)
|
||||
|
||||
|
||||
def _unwrap(argv: list[str]) -> list[str]:
|
||||
"""Strip the pty_resize wrapper from the front of a TTY-mode
|
||||
argv, return the inner smolvm argv. Mirrors what the kernel
|
||||
@@ -141,6 +149,19 @@ class TestClaudeArgvWrapped(unittest.TestCase):
|
||||
)
|
||||
self.assertNotIn("-p", argv)
|
||||
|
||||
def test_workspace_workdir_wraps_agent_command(self):
|
||||
argv = _unwrap(_workspace_bottle().agent_argv([]))
|
||||
agent_idx = argv.index("claude")
|
||||
self.assertEqual(
|
||||
[
|
||||
"sh", "-lc",
|
||||
"cd /home/node/workspace && exec \"$@\"",
|
||||
"bot-bottle-agent",
|
||||
"claude",
|
||||
],
|
||||
argv[agent_idx - 4:agent_idx + 1],
|
||||
)
|
||||
|
||||
|
||||
class TestClaudeArgvNoTTY(unittest.TestCase):
|
||||
"""`tty=False` paths skip the pty_resize wrapper — there's no
|
||||
|
||||
Reference in New Issue
Block a user