PRD 0050: Move provider-specific agent logic into contrib #180

Merged
didericis merged 7 commits from prd-0050-agent-provider-contrib into main 2026-06-03 21:38:14 -04:00
Collaborator

Closes #177.

PRD: docs/prds/0050-agent-provider-contrib.md

Summary

  • Reshapes bot_bottle/agent_provider.py so Claude- and Codex-specific behavior lives under bot_bottle/contrib/claude/ and bot_bottle/contrib/codex/, matching the contrib convention PRD 0048 established.
  • Moves provision_skills, provision_prompt, and provision_supervise (renamed provision_supervise_mcp) off BottleBackend and onto the provider plugin; provision_provider_auth is folded into the per-provider provision step.
  • Adds first-class MCP registration to the provider contract so Codex bottles finally get the supervise sidecar wired into ~/.codex/config.toml alongside Claude's claude mcp add.
Closes #177. PRD: [docs/prds/0050-agent-provider-contrib.md](https://gitea.dideric.is/didericis/bot-bottle/src/commit/5d9cd01b9249a34aa0fc88b6b3960176a025f135/docs/prds/0050-agent-provider-contrib.md) ## Summary - Reshapes `bot_bottle/agent_provider.py` so Claude- and Codex-specific behavior lives under `bot_bottle/contrib/claude/` and `bot_bottle/contrib/codex/`, matching the contrib convention PRD 0048 established. - Moves `provision_skills`, `provision_prompt`, and `provision_supervise` (renamed `provision_supervise_mcp`) off `BottleBackend` and onto the provider plugin; `provision_provider_auth` is folded into the per-provider provision step. - Adds first-class MCP registration to the provider contract so Codex bottles finally get the supervise sidecar wired into `~/.codex/config.toml` alongside Claude's `claude mcp add`.
Owner

RE questions:

  1. Keep it similar to claude. And yes, there's a codex mcp add command
  2. Keep the default methods
  3. Remove the knobs
RE questions: 1. Keep it similar to claude. And yes, there's a `codex mcp add` command 2. Keep the default methods 3. Remove the knobs
didericis reviewed 2026-06-03 20:55:58 -04:00
@@ -0,0 +27,4 @@
PROMPT_PATH = f"{GUEST_HOME}/.bot-bottle-prompt.txt"
def apply_skills(plan: "BottlePlan", bottle: "Bottle") -> None:
Owner

I don't think these different apply steps should be separated like this... it seems like the responsibility of the agent provider to actually execute these different apply steps. They're likely to diverge based on harness changes, and the abstraction that allows both codex and claude to use them is weak. Ideally I think this whole file can go, and what's in here can happen in the different specific provision steps for the specific providers.

I don't think these different `apply` steps should be separated like this... it seems like the responsibility of the agent provider to actually execute these different apply steps. They're likely to diverge based on harness changes, and the abstraction that allows both codex and claude to use them is weak. Ideally I think this whole file can go, and what's in here can happen in the different specific provision steps for the specific providers.
didericis-claude reviewed 2026-06-03 21:00:28 -04:00
Author
Collaborator

Agreed — pushed ceb8506559 which deletes _provision_apply.py entirely and inlines the skills / prompt / provision steps into each provider class. AgentProvider now declares all four methods (provision_skills, provision_prompt, provision, provision_supervise_mcp) as abstract; ClaudeAgentProvider and CodexAgentProvider each carry their own copy loop. Tests split into test_contrib_claude_provider.py and test_contrib_codex_provider.py.

Agreed — pushed ceb850655931dae87efe3c61dea16ef2cfa83372 which deletes `_provision_apply.py` entirely and inlines the skills / prompt / provision steps into each provider class. `AgentProvider` now declares all four methods (`provision_skills`, `provision_prompt`, `provision`, `provision_supervise_mcp`) as abstract; `ClaudeAgentProvider` and `CodexAgentProvider` each carry their own copy loop. Tests split into [`test_contrib_claude_provider.py`](https://gitea.dideric.is/didericis/bot-bottle/src/commit/ceb850655931dae87efe3c61dea16ef2cfa83372/tests/unit/test_contrib_claude_provider.py) and [`test_contrib_codex_provider.py`](https://gitea.dideric.is/didericis/bot-bottle/src/commit/ceb850655931dae87efe3c61dea16ef2cfa83372/tests/unit/test_contrib_codex_provider.py).
didericis reviewed 2026-06-03 21:08:29 -04:00
@@ -27,6 +41,8 @@ PROVIDER_TEMPLATES = frozenset({PROVIDER_CLAUDE, PROVIDER_CODEX})
CODEX_HOST_CREDENTIAL_HOSTS = ("api.openai.com", "chatgpt.com")
PromptMode = Literal["append_file", "read_prompt_file"]
GUEST_HOME = "/home/node"
Owner

Don't have a default for this/have this be driven by the backend

Don't have a default for this/have this be driven by the backend
didericis-claude reviewed 2026-06-03 21:12:22 -04:00
@@ -40,5 +40,5 @@
# (no TLS MITM) or its header DLP blocks the injected JWT.
CODEX_HOST_CREDENTIAL_HOSTS = ("api.openai.com", "chatgpt.com")
PromptMode = Literal["append_file", "read_prompt_file"]
Author
Collaborator

Done in b2dc5fd20bGUEST_HOME is gone from agent_provider.py, guest_home is a required kwarg on provision_plan (no default), and the _SKILLS_DIR / _PROMPT_PATH constants in both contrib providers are gone too. The providers now derive in-guest paths from plan.workspace_plan.guest_home at call time, which the backend's prepare step populates.

Done in b2dc5fd20bcbfbc5dcf0def096a3fd0cb447105c — `GUEST_HOME` is gone from `agent_provider.py`, `guest_home` is a required kwarg on `provision_plan` (no default), and the `_SKILLS_DIR` / `_PROMPT_PATH` constants in both contrib providers are gone too. The providers now derive in-guest paths from `plan.workspace_plan.guest_home` at call time, which the backend's `prepare` step populates.
didericis reviewed 2026-06-03 21:26:39 -04:00
@@ -60,3 +59,2 @@
return
container_home = os.environ.get("BOT_BOTTLE_CONTAINER_HOME", "/home/node")
container_gitconfig = f"{container_home}/.gitconfig"
container_gitconfig = "/home/node/.gitconfig"
Owner

/home/node should be coming from the bottle plan... isn't this already there somewhere/don't we already need this to create the workspace? Regardless, it should be somewhere higher up in the plan.

`/home/node` should be coming from the bottle plan... isn't this already there somewhere/don't we already need this to create the workspace? Regardless, it should be somewhere higher up in the plan.
didericis marked this conversation as resolved
didericis-claude reviewed 2026-06-03 21:30:38 -04:00
@@ -58,8 +57,7 @@ def _provision_git_gate_config(plan: DockerBottlePlan, bottle: Bottle) -> None:
manifest_bottle = plan.spec.manifest.bottle_for(plan.spec.agent_name)
if not manifest_bottle.git:
return
Author
Collaborator

You were right — workspace_plan was the only place guest_home lived, and reading it through plan.workspace_plan.guest_home was the wrong layer. Hoisted in 21a46b97d8: guest_home: str is now a field on the base BottlePlan dataclass, populated by each backend's prepare step. The contrib providers, this gitconfig path, and the smolmachines _guest_home() helper all read plan.guest_home directly.

You were right — workspace_plan was the only place `guest_home` lived, and reading it through `plan.workspace_plan.guest_home` was the wrong layer. Hoisted in 21a46b97d8f35734fa23165ee2b94d1268ddb8d3: `guest_home: str` is now a field on the base `BottlePlan` dataclass, populated by each backend's `prepare` step. The contrib providers, this gitconfig path, and the smolmachines `_guest_home()` helper all read `plan.guest_home` directly.
didericis marked this conversation as resolved
didericis approved these changes 2026-06-03 21:35:33 -04:00
didericis added 7 commits 2026-06-03 21:35:47 -04:00
Lift the provider-specific blocks of agent_provision_plan into
contrib/claude/agent_provider.py and contrib/codex/agent_provider.py,
behind a new AgentProvider ABC and a lazy get_provider() registry
(mirrors PRD 0048's contrib convention).

agent_provision_plan and runtime_for stay as thin shims so existing
callers in backend/{docker,smolmachines}/prepare.py and cli/start.py
keep working without per-call edits — the shipping diff in this commit
is purely 'who owns the producer'.

Adds bot_bottle/_provision_apply.py — the backend-agnostic
skills / prompt / declarative-plan apply loops the per-provider
default methods will dispatch through in the next commit.
BottleBackend.provision now resolves the provider plugin from the
plan and dispatches prompt / skills / declarative-apply /
supervise-mcp through it. The four hooks the docker + smolmachines
backends used to override (provision_skills, provision_prompt,
provision_provider_auth, provision_supervise) are gone — the
duplicated 50-line implementations under
backend/{docker,smolmachines}/provision/{skills,prompt,
provider_auth,supervise}.py are deleted.

Each backend gains a small supervise_mcp_url(plan) override so the
provider plugin can run `claude mcp add` / `codex mcp add`
against the right URL: docker returns
http://{SUPERVISE_HOSTNAME}:{SUPERVISE_PORT}/ on the compose
network alias; smolmachines returns plan.agent_supervise_url which
launch.py already pins to a host-loopback port.

Removes tests/unit/test_provision_supervise.py — the URL it
asserted on now lives on the backend, with no equivalent
standalone surface to test against (it's covered by the broader
plan / launch integration tests).
- tests/unit/test_provision_apply.py covers the new shared
  apply helpers (apply_skills / apply_prompt / apply_provision)
  that replace the per-backend modules deleted in the prior
  commit.
- tests/unit/test_contrib_supervise_mcp.py covers both providers'
  provision_supervise_mcp behavior — confirms the codex bottle
  now runs `codex mcp add` symmetrically with claude.
- tests/unit/test_smolmachines_provision.py drops the four test
  classes whose subjects moved (TestProvisionPrompt /
  TestProvisionProviderAuth / TestProvisionSkills /
  TestProvisionSupervise); the backend-side CA / git / workspace
  classes stay.
- tests/unit/test_docker_provision_provider_auth.py removed; its
  coverage now lives in tests/unit/test_provision_apply.py
  (apply_provision is backend-agnostic, one test file suffices).

Drops the BOT_BOTTLE_CONTAINER_HOME, BOT_BOTTLE_GUEST_HOME,
BOT_BOTTLE_CONTAINER_SKILLS_DIR, and BOT_BOTTLE_GUEST_SKILLS_DIR
env knobs the deleted provision modules used to read. /home/node
is hardcoded everywhere the knobs lived; the values were
effectively constants today and removing them keeps the PRD-0050
surface area honest.

Flips PRD 0050 Status: Draft → Active. Closes #177 on merge.
Each AgentProvider now owns its skills / prompt / provision /
supervise_mcp end-to-end. The base ABC declares all four as
abstract; ClaudeAgentProvider and CodexAgentProvider each carry
their own copy loop.

Per PR review feedback (review #128): the shared
_provision_apply.py abstraction was weak — Claude and Codex
harnesses already diverge (codex's dummy-auth + login-status
verify has no claude analogue) and forcing both onto one helper
just postpones the split. Duplication is intentional.

Deletes bot_bottle/_provision_apply.py and consolidates testing
under tests/unit/test_contrib_{claude,codex}_provider.py (one
file per provider, covering all four methods).
Per PR review feedback (review #130): the GUEST_HOME = '/home/node'
default in agent_provider.py was driving the wrong direction —
the agent provider shouldn't ship its own opinion about the guest
home, the backend should.

- Removes the GUEST_HOME constant.
- Makes guest_home a required kwarg on AgentProvider.provision_plan
  and the agent_provision_plan shim (no default).
- Drops module-level _SKILLS_DIR / _PROMPT_PATH constants from
  contrib/{claude,codex}/agent_provider.py; both providers now
  derive the in-guest paths from plan.workspace_plan.guest_home
  at call time, which the backend's prepare step populated.
- Updates tests/unit/test_agent_provider.py callers to pass
  guest_home explicitly. The backend prepare paths already pass
  it; no production-code call sites changed.
refactor(backend): hoist guest_home to BottlePlan base
test / unit (pull_request) Successful in 39s
test / integration (pull_request) Successful in 49s
266013095e
Per PR review feedback (review #132): guest_home shouldn't be
buried inside workspace_plan / read from a hardcoded literal in
each provision module. It's a cross-cutting bottle property — the
backend's prepare step knows it, and every downstream consumer
(contrib providers, git provisioning, gitconfig path) should
read it from one place.

- Adds guest_home: str to BottlePlan base dataclass.
- Both backends' prepare steps populate plan.guest_home.
- contrib/{claude,codex}/agent_provider.py read plan.guest_home
  (was plan.workspace_plan.guest_home).
- bot_bottle/backend/docker/provision/git.py reads plan.guest_home
  for the gitconfig destination (was hardcoded "/home/node").
- bot_bottle/backend/smolmachines/provision/git.py drops the
  _GUEST_HOME / _guest_home() helpers and reads plan.guest_home.
- Tests that construct BottlePlan subclasses directly pass
  guest_home="/home/node" explicitly.
didericis force-pushed prd-0050-agent-provider-contrib from 21a46b97d8 to 266013095e 2026-06-03 21:35:47 -04:00 Compare
didericis merged commit ea66f63d45 into main 2026-06-03 21:38:14 -04:00
didericis deleted branch prd-0050-agent-provider-contrib 2026-06-03 21:38:15 -04:00
Sign in to join this conversation.