feat: forward agent style via native CLI config and terminal title
lint / lint (push) Successful in 1m33s
test / unit (pull_request) Successful in 34s
test / integration (pull_request) Successful in 21s

Replace prompt-injection for display identity with native UI wiring:
- Claude: writes a statusline shell script + custom theme JSON, wired up
  via settings.json so label/color show in the status bar and theme
- Codex: writes [tui] block into codex-config.toml (status_line,
  terminal_title, dark-ansi theme)
- Both backends set the terminal title via ANSI OSC 0 escape before
  exec-ing the agent when a label is present

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 00:00:19 -04:00
parent 0a564bb41e
commit 20cfafcc4d
9 changed files with 204 additions and 65 deletions
+105 -24
View File
@@ -17,9 +17,11 @@ from typing import TYPE_CHECKING
from ...agent_provider import (
AgentProvider,
AgentProviderRuntime,
AgentProvisionDir,
AgentProvisionFile,
AgentProvisionPlan,
)
from ...backend.docker import util as docker_mod
from ...egress import EgressRoute
from ...log import die, info, warn
@@ -39,24 +41,68 @@ 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)
_STATUS_LINE_COLORS = {
"black": "\033[30m",
"red": "\033[31m",
"green": "\033[32m",
"yellow": "\033[33m",
"blue": "\033[34m",
"magenta": "\033[35m",
"cyan": "\033[36m",
"white": "\033[37m",
"bright-black": "\033[90m",
"bright-red": "\033[91m",
"bright-green": "\033[92m",
"bright-yellow": "\033[93m",
"bright-blue": "\033[94m",
"bright-magenta": "\033[95m",
"bright-cyan": "\033[96m",
"bright-white": "\033[97m",
}
_CLAUDE_THEME_COLORS = {
"black": "black",
"red": "red",
"green": "green",
"yellow": "yellow",
"blue": "blue",
"magenta": "magenta",
"cyan": "cyan",
"white": "white",
"bright-black": "blackBright",
"bright-red": "redBright",
"bright-green": "greenBright",
"bright-yellow": "yellowBright",
"bright-blue": "blueBright",
"bright-magenta": "magentaBright",
"bright-cyan": "cyanBright",
"bright-white": "whiteBright",
}
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
def _status_line_script(label: str, color: str) -> str:
if not label:
return "#!/bin/sh\nprintf '\\n'\n"
label_q = shlex.quote(label)
if color and color in _STATUS_LINE_COLORS:
return (
"#!/bin/sh\n"
f"printf '%b%s%b\\n' '{_STATUS_LINE_COLORS[color]}' {label_q} '\\033[0m'\n"
)
return f"#!/bin/sh\nprintf '%s\\n' {label_q}\n"
def _custom_theme_payload(color: str) -> dict[str, object] | None:
theme_color = _CLAUDE_THEME_COLORS.get(color)
if not theme_color:
return None
return {
"name": f"Bot-bottle {color}",
"base": "dark",
"overrides": {
"claude": f"ansi:{theme_color}",
},
}
_RUNTIME = AgentProviderRuntime(
@@ -99,6 +145,10 @@ class ClaudeAgentProvider(AgentProvider):
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
"DISABLE_ERROR_REPORTING": "1",
}
dirs = (
AgentProvisionDir(f"{guest_home}/.claude"),
AgentProvisionDir(f"{guest_home}/.claude/themes"),
)
claude_config = state_dir / "claude.json"
claude_projects = {guest_home: {"hasTrustDialogAccepted": True}}
claude_projects[trusted_path] = {"hasTrustDialogAccepted": True}
@@ -108,15 +158,45 @@ class ClaudeAgentProvider(AgentProvider):
"bypassPermissionsModeAccepted": True,
"projects": claude_projects,
}
if label:
payload["name"] = label
if color:
payload["color"] = color
claude_config.write_text(json.dumps(payload, indent=2) + "\n")
claude_config.chmod(0o600)
files = (
files = [
AgentProvisionFile(claude_config, f"{guest_home}/.claude.json"),
)
]
claude_settings = state_dir / "claude-settings.json"
claude_settings_payload: dict[str, object] = {}
if label or color:
statusline_script = state_dir / "claude-statusline.sh"
statusline_script.write_text(_status_line_script(label, color))
statusline_script.chmod(0o755)
files.append(AgentProvisionFile(
statusline_script,
f"{guest_home}/.claude/statusline.sh",
mode="755",
))
claude_settings_payload["statusLine"] = {
"type": "command",
"command": "~/.claude/statusline.sh",
}
theme_payload = _custom_theme_payload(color)
if theme_payload is not None:
theme_name = f"bot-bottle-{docker_mod.slugify(label or color)}"
theme_file = state_dir / f"{theme_name}.json"
theme_file.write_text(json.dumps(theme_payload, indent=2) + "\n")
theme_file.chmod(0o644)
files.append(AgentProvisionFile(
theme_file,
f"{guest_home}/.claude/themes/{theme_name}.json",
))
claude_settings_payload["theme"] = f"custom:{theme_name}"
if claude_settings_payload:
claude_settings.write_text(json.dumps(claude_settings_payload, indent=2) + "\n")
claude_settings.chmod(0o600)
files.append(AgentProvisionFile(
claude_settings,
f"{guest_home}/.claude/settings.json",
))
egress_routes = (EgressRoute(
host="api.anthropic.com",
auth_scheme="Bearer" if auth_token else "",
@@ -127,7 +207,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)
has_prompt = prompt_file.exists() and bool(prompt_file.read_text())
return AgentProvisionPlan(
template=_RUNTIME.template,
command=_RUNTIME.command,
@@ -140,7 +220,8 @@ class ClaudeAgentProvider(AgentProvider):
env_vars=env_vars,
guest_env=resolved_guest_env,
has_prompt=has_prompt,
files=files,
dirs=dirs,
files=tuple(files),
egress_routes=egress_routes,
hidden_env_names=hidden_env_names,
)
+8 -23
View File
@@ -18,8 +18,8 @@ from ...agent_provider import (
CODEX_HOST_CREDENTIAL_HOSTS,
AgentProvider,
AgentProviderRuntime,
AgentProvisionCommand,
AgentProvisionDir,
AgentProvisionCommand,
AgentProvisionFile,
AgentProvisionPlan,
)
@@ -47,26 +47,6 @@ 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",
@@ -98,7 +78,7 @@ class CodexAgentProvider(AgentProvider):
label: str = "",
color: str = "",
) -> AgentProvisionPlan:
del auth_token # Claude-only knob
del auth_token, label, color # Claude-only / title-only knobs
resolved_guest_env = dict(guest_env or {})
guest_home = self.guest_home
trusted_path = trusted_project_path or guest_home
@@ -122,6 +102,11 @@ class CodexAgentProvider(AgentProvider):
config_file.write_text(
f'[projects."{toml_path}"]\n'
'trust_level = "trusted"\n'
"\n"
"[tui]\n"
'status_line = ["model", "cwd"]\n'
'terminal_title = ["spinner", "project"]\n'
'theme = "dark-ansi"\n'
)
config_file.chmod(0o600)
files.append(AgentProvisionFile(config_file, config_path))
@@ -164,7 +149,7 @@ class CodexAgentProvider(AgentProvider):
"guest, but Codex did not accept it"
)))
has_prompt = _prepend_display_identity(prompt_file, label, color)
has_prompt = prompt_file.exists() and bool(prompt_file.read_text())
return AgentProvisionPlan(
template=_RUNTIME.template,
command=_RUNTIME.command,