From 39811c9b32d8db691226c5c880e198e6777319c9 Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 9 Jun 2026 03:39:54 +0000 Subject: [PATCH] feat: forward agent display identity to prompts --- bot_bottle/agent_provider.py | 1 + bot_bottle/contrib/claude/agent_provider.py | 25 ++++++++++++- bot_bottle/contrib/codex/agent_provider.py | 27 ++++++++++++-- tests/unit/test_agent_provider.py | 40 +++++++++++++++++++++ tests/unit/test_contrib_claude_provider.py | 15 ++++++++ tests/unit/test_contrib_codex_provider.py | 16 +++++++++ 6 files changed, 121 insertions(+), 3 deletions(-) diff --git a/bot_bottle/agent_provider.py b/bot_bottle/agent_provider.py index 2bca283..c3d6ea9 100644 --- a/bot_bottle/agent_provider.py +++ b/bot_bottle/agent_provider.py @@ -107,6 +107,7 @@ class AgentProvisionPlan: instance_name: str prompt_file: Path guest_env: dict[str, str] + has_prompt: bool = False env_vars: dict[str, str] = field(default_factory=dict) dirs: tuple[AgentProvisionDir, ...] = () files: tuple[AgentProvisionFile, ...] = () diff --git a/bot_bottle/contrib/claude/agent_provider.py b/bot_bottle/contrib/claude/agent_provider.py index 1085176..9b1bd5d 100644 --- a/bot_bottle/contrib/claude/agent_provider.py +++ b/bot_bottle/contrib/claude/agent_provider.py @@ -38,6 +38,27 @@ def _skills_dir(guest_home: str) -> str: def _prompt_path(guest_home: str) -> str: return f"{guest_home}/.bot-bottle-prompt.txt" + +def _display_identity_prompt(label: str, color: str) -> str: + lines: list[str] = [] + if label: + lines.append(f"Name: {label}") + if color: + lines.append(f"Color: {color}") + if not lines: + return "" + return "Bot-bottle agent display identity:\n" + "\n".join(lines) + + +def _prepend_display_identity(prompt_file: Path, label: str, color: str) -> bool: + identity = _display_identity_prompt(label, color) + original = prompt_file.read_text() if prompt_file.exists() else "" + if not identity: + return bool(original) + prompt_file.write_text(f"{identity}\n\n{original}" if original else f"{identity}\n") + return True + + _RUNTIME = AgentProviderRuntime( template="claude", command="claude", @@ -106,6 +127,7 @@ class ClaudeAgentProvider(AgentProvider): env_vars["CLAUDE_CODE_OAUTH_TOKEN"] = "egress-placeholder" hidden_env_names = frozenset({"CLAUDE_CODE_OAUTH_TOKEN"}) + has_prompt = _prepend_display_identity(prompt_file, label, color) return AgentProvisionPlan( template=_RUNTIME.template, command=_RUNTIME.command, @@ -117,6 +139,7 @@ class ClaudeAgentProvider(AgentProvider): prompt_file=prompt_file, env_vars=env_vars, guest_env=resolved_guest_env, + has_prompt=has_prompt, files=files, egress_routes=egress_routes, hidden_env_names=hidden_env_names, @@ -158,7 +181,7 @@ class ClaudeAgentProvider(AgentProvider): user="root", ) agent = plan.spec.manifest.agents[plan.spec.agent_name] - return prompt_path if agent.prompt else None + return prompt_path if plan.agent_provision.has_prompt or agent.prompt else None def provision(self, plan: "BottlePlan", bottle: "Bottle") -> None: """Apply the claude-side declarative provision steps from diff --git a/bot_bottle/contrib/codex/agent_provider.py b/bot_bottle/contrib/codex/agent_provider.py index 4b2c99b..894e50a 100644 --- a/bot_bottle/contrib/codex/agent_provider.py +++ b/bot_bottle/contrib/codex/agent_provider.py @@ -46,6 +46,27 @@ def _skills_dir(guest_home: str) -> str: def _prompt_path(guest_home: str) -> str: return f"{guest_home}/.bot-bottle-prompt.txt" + +def _display_identity_prompt(label: str, color: str) -> str: + lines: list[str] = [] + if label: + lines.append(f"Name: {label}") + if color: + lines.append(f"Color: {color}") + if not lines: + return "" + return "Bot-bottle agent display identity:\n" + "\n".join(lines) + + +def _prepend_display_identity(prompt_file: Path, label: str, color: str) -> bool: + identity = _display_identity_prompt(label, color) + original = prompt_file.read_text() if prompt_file.exists() else "" + if not identity: + return bool(original) + prompt_file.write_text(f"{identity}\n\n{original}" if original else f"{identity}\n") + return True + + _RUNTIME = AgentProviderRuntime( template="codex", command="codex", @@ -77,7 +98,7 @@ class CodexAgentProvider(AgentProvider): label: str = "", color: str = "", ) -> AgentProvisionPlan: - del auth_token, label, color # Claude-only knobs + del auth_token # Claude-only knob resolved_guest_env = dict(guest_env or {}) guest_home = self.guest_home trusted_path = trusted_project_path or guest_home @@ -143,6 +164,7 @@ class CodexAgentProvider(AgentProvider): "guest, but Codex did not accept it" ))) + has_prompt = _prepend_display_identity(prompt_file, label, color) return AgentProvisionPlan( template=_RUNTIME.template, command=_RUNTIME.command, @@ -154,6 +176,7 @@ class CodexAgentProvider(AgentProvider): prompt_file=prompt_file, env_vars=env_vars, guest_env=resolved_guest_env, + has_prompt=has_prompt, dirs=tuple(dirs), files=tuple(files), pre_copy=tuple(pre_copy), @@ -198,7 +221,7 @@ class CodexAgentProvider(AgentProvider): user="root", ) agent = plan.spec.manifest.agents[plan.spec.agent_name] - return prompt_path if agent.prompt else None + return prompt_path if plan.agent_provision.has_prompt or agent.prompt else None def provision(self, plan: "BottlePlan", bottle: "Bottle") -> None: """Apply the codex-side declarative provision steps from diff --git a/tests/unit/test_agent_provider.py b/tests/unit/test_agent_provider.py index a23094b..6e05ac2 100644 --- a/tests/unit/test_agent_provider.py +++ b/tests/unit/test_agent_provider.py @@ -62,6 +62,26 @@ class TestAgentProviderRuntime(unittest.TestCase): config = Path(tmp, "codex-config.toml").read_text() self.assertIn('[projects."/home/node/workspace"]', config) + def test_codex_injects_name_and_color_into_prompt(self): + with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: + prompt_file = Path(tmp) / "prompt.txt" + prompt_file.write_text("Existing instructions.\n") + plan = build_agent_provision_plan( + template="codex", + dockerfile="", + state_dir=Path(tmp), + instance_name="bot-bottle-test", + prompt_file=prompt_file, + label="review-api", + color="bright-cyan", + ) + prompt = prompt_file.read_text() + self.assertTrue(plan.has_prompt) + self.assertIn("Bot-bottle agent display identity:", prompt) + self.assertIn("Name: review-api", prompt) + self.assertIn("Color: bright-cyan", prompt) + self.assertTrue(prompt.endswith("Existing instructions.\n")) + def test_codex_forward_host_credentials_adds_auth_and_verify(self): with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: home = Path(tmp) / "host-codex" @@ -126,6 +146,26 @@ class TestAgentProviderRuntime(unittest.TestCase): self.assertIn("/home/node", config["projects"]) self.assertIn("/home/node/workspace", config["projects"]) + def test_claude_injects_name_and_color_into_prompt(self): + with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: + prompt_file = Path(tmp) / "prompt.txt" + prompt_file.write_text("Existing instructions.\n") + plan = build_agent_provision_plan( + template="claude", + dockerfile="", + state_dir=Path(tmp), + instance_name="bot-bottle-test", + prompt_file=prompt_file, + label="research-ui", + color="green", + ) + prompt = prompt_file.read_text() + self.assertTrue(plan.has_prompt) + self.assertIn("Bot-bottle agent display identity:", prompt) + self.assertIn("Name: research-ui", prompt) + self.assertIn("Color: green", prompt) + self.assertTrue(prompt.endswith("Existing instructions.\n")) + def test_codex_forward_host_credentials_populates_egress_routes(self): with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: home = Path(tmp) / "host-codex" diff --git a/tests/unit/test_contrib_claude_provider.py b/tests/unit/test_contrib_claude_provider.py index df37fec..08a0813 100644 --- a/tests/unit/test_contrib_claude_provider.py +++ b/tests/unit/test_contrib_claude_provider.py @@ -127,6 +127,21 @@ class TestClaudeProvisionPrompt(unittest.TestCase): self.assertIsNone(r) bottle.cp_in.assert_called_once() + def test_returns_path_when_provider_prompt_has_identity(self): + bottle = _make_bottle() + provision = AgentProvisionPlan( + template="claude", command="claude", prompt_mode="append_file", + image="", dockerfile="", guest_home="/home/node", + instance_name="bot-bottle-demo-abc12", + prompt_file=Path("/tmp/prompt.txt"), + guest_env={}, + has_prompt=True, + ) + r = ClaudeAgentProvider().provision_prompt( + _plan(agent_prompt="", agent_provision=provision), bottle, + ) + self.assertEqual("/home/node/.bot-bottle-prompt.txt", r) + def test_chowns_to_node_after_copy(self): bottle = _make_bottle() ClaudeAgentProvider().provision_prompt(_plan(), bottle) diff --git a/tests/unit/test_contrib_codex_provider.py b/tests/unit/test_contrib_codex_provider.py index e0ab6fc..cf847bb 100644 --- a/tests/unit/test_contrib_codex_provider.py +++ b/tests/unit/test_contrib_codex_provider.py @@ -130,6 +130,22 @@ class TestCodexProvisionPrompt(unittest.TestCase): self.assertIsNone(r) bottle.cp_in.assert_called_once() + def test_returns_path_when_provider_prompt_has_identity(self): + bottle = _make_bottle() + provision = AgentProvisionPlan( + template="codex", command="codex", + prompt_mode="read_prompt_file", + image="", dockerfile="", guest_home="/home/node", + instance_name="bot-bottle-demo-abc12", + prompt_file=Path("/tmp/prompt.txt"), + guest_env={}, + has_prompt=True, + ) + r = CodexAgentProvider().provision_prompt( + _plan(agent_prompt="", agent_provision=provision), bottle, + ) + self.assertEqual("/home/node/.bot-bottle-prompt.txt", r) + class TestCodexProvisionSkills(unittest.TestCase): def test_noop_when_agent_has_no_skills(self):