docs(prd): expand user-provider-plugins to cover Dockerfile convention and provisioning methods
This commit is contained in:
@@ -12,10 +12,16 @@ again at launch. Users who want to run a different agent (Gemini, Aider, a custo
|
|||||||
local model wrapper) cannot add a provider without forking the package.
|
local model wrapper) cannot add a provider without forking the package.
|
||||||
|
|
||||||
This PRD opens the registry to user-defined plugins. A plugin placed at
|
This PRD opens the registry to user-defined plugins. A plugin placed at
|
||||||
`~/.bot-bottle/contrib/<name>/agent_provider.py` is discovered and loaded at launch
|
`~/.bot-bottle/contrib/<name>/` is discovered and loaded at launch time. The manifest
|
||||||
time. The manifest accepts any non-empty template string that names a built-in or
|
accepts any non-empty template string that names a built-in or resolves to a user
|
||||||
resolves to a user plugin at that path. No changes to the built-in providers or the
|
plugin at that path.
|
||||||
internal `bot_bottle/contrib/` layout.
|
|
||||||
|
Alongside discovery, this PRD moves CA and git provisioning out of the Docker backend
|
||||||
|
and into the `AgentProvider` ABC as overridable methods. The current standalone
|
||||||
|
`provision/ca.py` and `provision/git.py` files in the Docker backend are deleted;
|
||||||
|
their logic becomes the default implementations on the ABC. This lets exotic provider
|
||||||
|
images (different base OS, different user, non-standard trust mechanism) override
|
||||||
|
provisioning freely without the abstraction fighting them.
|
||||||
|
|
||||||
The preceding commit on this PR moves `codex_auth.py` from `bot_bottle/` into
|
The preceding commit on this PR moves `codex_auth.py` from `bot_bottle/` into
|
||||||
`bot_bottle/contrib/codex/` — a clean-up that fits naturally here since this PR
|
`bot_bottle/contrib/codex/` — a clean-up that fits naturally here since this PR
|
||||||
@@ -32,33 +38,49 @@ be "cheap to add" — but "cheap" today still means a pull request against the b
|
|||||||
repo, not a drop-in file in the user's home directory. The filesystem layout is already
|
repo, not a drop-in file in the user's home directory. The filesystem layout is already
|
||||||
the right shape; the discovery step is missing.
|
the right shape; the discovery step is missing.
|
||||||
|
|
||||||
|
Beyond discovery, the Docker backend's `provision_ca` and `provision_git` functions
|
||||||
|
bake in Debian-specific commands (`update-ca-certificates`) and a hardcoded container
|
||||||
|
user (`node`). A user plugin that runs as a different user, or on a different base OS,
|
||||||
|
silently gets the wrong provisioning with no way to correct it short of forking.
|
||||||
|
|
||||||
## Goals / Success Criteria
|
## Goals / Success Criteria
|
||||||
|
|
||||||
1. A user places `~/.bot-bottle/contrib/<name>/agent_provider.py` — a file that exports
|
1. A user places `~/.bot-bottle/contrib/<name>/agent_provider.py` — a file that exports
|
||||||
a class inheriting `AgentProvider` — sets `agent_provider.template: <name>` in a
|
a class inheriting `AgentProvider` — sets `agent_provider.template: <name>` in a
|
||||||
bottle's frontmatter, and launches a bottle using that provider with no changes to
|
bottle's frontmatter, and launches a bottle using that provider with no changes to
|
||||||
the bot-bottle source.
|
the bot-bottle source.
|
||||||
2. The manifest validator accepts any non-empty template string. Unknown templates that
|
2. The plugin directory may also contain a `Dockerfile` at
|
||||||
|
`~/.bot-bottle/contrib/<name>/Dockerfile`; the existing three-tier Dockerfile cascade
|
||||||
|
(per-bottle override → manifest `dockerfile:` field → provider default) uses this
|
||||||
|
path as the provider default for user plugins.
|
||||||
|
3. The manifest validator accepts any non-empty template string. Unknown templates that
|
||||||
resolve to no user plugin still raise a clear error, but at launch (via `get_provider`)
|
resolve to no user plugin still raise a clear error, but at launch (via `get_provider`)
|
||||||
rather than at manifest-load time.
|
rather than at manifest-load time.
|
||||||
3. Built-in provider knobs (`auth_token` → claude only; `forward_host_credentials` →
|
4. Built-in provider knobs (`auth_token` → claude only; `forward_host_credentials` →
|
||||||
codex only) are guarded to built-in template names. Bottles using a user provider
|
codex only) are guarded to built-in template names. Bottles using a user provider
|
||||||
may set neither knob.
|
may set neither knob.
|
||||||
4. `get_provider(template)` checks `~/.bot-bottle/contrib/<template>/agent_provider.py`
|
5. `get_provider(template)` checks `~/.bot-bottle/contrib/<template>/agent_provider.py`
|
||||||
before the built-ins, so a user can shadow a built-in for local testing.
|
before the built-ins, so a user can shadow a built-in for local testing.
|
||||||
5. A clear `ValueError` is raised if the user plugin file exists but contains no
|
6. A clear `ValueError` is raised if the user plugin file exists but contains no
|
||||||
`AgentProvider` subclass.
|
`AgentProvider` subclass.
|
||||||
|
7. `AgentProvider` gains `provision_ca(self, bottle, plan)` and
|
||||||
|
`provision_git(self, bottle, plan)` with default implementations that reproduce
|
||||||
|
current Docker/Debian/node behavior. Built-in providers inherit the defaults
|
||||||
|
unchanged. User plugins override either method when their image diverges.
|
||||||
|
8. `bot_bottle/backend/docker/provision/ca.py` and
|
||||||
|
`bot_bottle/backend/docker/provision/git.py` are deleted. The Docker backend base
|
||||||
|
class calls `provider.provision_ca(bottle, plan)` and
|
||||||
|
`provider.provision_git(bottle, plan)` directly.
|
||||||
|
|
||||||
## Non-goals
|
## Non-goals
|
||||||
|
|
||||||
- Packaging or distributing user plugins as installable Python packages.
|
- Packaging or distributing user plugins as installable Python packages.
|
||||||
- A plugin registry, index, or discovery beyond the filesystem path convention.
|
- A plugin registry, index, or discovery beyond the filesystem path convention.
|
||||||
- Adding a third built-in provider.
|
- Adding a third built-in provider.
|
||||||
- Changing the `AgentProvider` ABC contract — user plugins implement the same abstract
|
|
||||||
methods as `ClaudeAgentProvider` and `CodexAgentProvider`.
|
|
||||||
- Validating that user plugin images, Dockerfiles, or commands exist before launch
|
- Validating that user plugin images, Dockerfiles, or commands exist before launch
|
||||||
(same policy as built-ins).
|
(same policy as built-ins).
|
||||||
- Sandboxing user plugin code — plugins run with full Python interpreter access.
|
- Sandboxing user plugin code — plugins run with full Python interpreter access.
|
||||||
|
- Per-provider opt-out of the egress sidecar or network provisioning (follow-on).
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
@@ -70,26 +92,45 @@ the right shape; the discovery step is missing.
|
|||||||
- `_load_user_plugin` uses `importlib.util.spec_from_file_location` to load the module
|
- `_load_user_plugin` uses `importlib.util.spec_from_file_location` to load the module
|
||||||
and returns the first `AgentProvider` subclass found in its `__dict__`. Raises
|
and returns the first `AgentProvider` subclass found in its `__dict__`. Raises
|
||||||
`ValueError` if the file exists but exports no subclass.
|
`ValueError` if the file exists but exports no subclass.
|
||||||
|
- The Dockerfile cascade in the Docker backend's `resolve_plan()` uses
|
||||||
|
`~/.bot-bottle/contrib/<template>/Dockerfile` as the provider default for user
|
||||||
|
plugins (the same slot currently occupied by `Dockerfile.claude` / `Dockerfile.codex`
|
||||||
|
for built-ins).
|
||||||
- `manifest_agent.AgentProvider.from_dict`: the `template not in PROVIDER_TEMPLATES`
|
- `manifest_agent.AgentProvider.from_dict`: the `template not in PROVIDER_TEMPLATES`
|
||||||
check is removed; the two built-in-specific knob guards (`auth_token` → claude,
|
check is removed; the two built-in-specific knob guards (`auth_token` → claude,
|
||||||
`forward_host_credentials` → codex) are tightened to `template in PROVIDER_TEMPLATES`
|
`forward_host_credentials` → codex) are tightened to `template in PROVIDER_TEMPLATES`
|
||||||
so they are skipped for user-defined names.
|
so they are skipped for user-defined names.
|
||||||
- `PROVIDER_TEMPLATES` remains in `agent_provider.py` as the set of built-in names for
|
- `PROVIDER_TEMPLATES` remains in `agent_provider.py` as the set of built-in names for
|
||||||
use by tests and any enumeration callers.
|
use by tests and any enumeration callers.
|
||||||
|
- `AgentProvider` ABC gains:
|
||||||
|
```python
|
||||||
|
def provision_ca(self, bottle: Bottle, plan: BottlePlan) -> None: ...
|
||||||
|
def provision_git(self, bottle: Bottle, plan: BottlePlan) -> None: ...
|
||||||
|
```
|
||||||
|
Default implementations reproduce the current `provision/ca.py` and
|
||||||
|
`provision/git.py` logic exactly (Debian `update-ca-certificates`, `node` user,
|
||||||
|
`/home/node` home).
|
||||||
|
- `bot_bottle/backend/docker/provision/ca.py` and
|
||||||
|
`bot_bottle/backend/docker/provision/git.py` deleted. The Docker backend base
|
||||||
|
class substitutes direct calls to the provider methods.
|
||||||
- Unit tests for the discovery path:
|
- Unit tests for the discovery path:
|
||||||
- Plugin found and loaded → correct `AgentProvider` instance returned.
|
- Plugin found and loaded → correct `AgentProvider` instance returned.
|
||||||
- Plugin file exists but exports no subclass → `ValueError`.
|
- Plugin file exists but exports no subclass → `ValueError`.
|
||||||
- Unknown template with no user plugin → `ValueError` from `get_provider`.
|
- Unknown template with no user plugin → `ValueError` from `get_provider`.
|
||||||
- Built-in template name still works normally even when no user plugin exists.
|
- Built-in template name still works normally even when no user plugin exists.
|
||||||
|
- Unit tests for the provisioning delegation:
|
||||||
|
- A provider subclass that overrides `provision_ca` has its override called.
|
||||||
|
- A provider subclass that overrides `provision_git` has its override called.
|
||||||
- One paragraph added to `README.md` under a new "Custom providers" section describing
|
- One paragraph added to `README.md` under a new "Custom providers" section describing
|
||||||
the `~/.bot-bottle/contrib/<name>/agent_provider.py` convention and pointing at the
|
the `~/.bot-bottle/contrib/<name>/` convention (both `agent_provider.py` and
|
||||||
existing contrib providers as reference implementations.
|
`Dockerfile`), the `provision_ca` / `provision_git` override points, and pointing at
|
||||||
|
the existing contrib providers as reference implementations.
|
||||||
|
|
||||||
### Out of scope
|
### Out of scope
|
||||||
|
|
||||||
- Hot-reloading plugins during a running session.
|
- Hot-reloading plugins during a running session.
|
||||||
- Plugin versioning or dependency declaration.
|
- Plugin versioning or dependency declaration.
|
||||||
- Changes to smolmachines or Docker backend provisioning paths.
|
- Changes to the smolmachines backend provisioning path.
|
||||||
|
|
||||||
## Proposed design
|
## Proposed design
|
||||||
|
|
||||||
@@ -136,6 +177,49 @@ def _load_user_plugin(template: str) -> AgentProvider | None:
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Dockerfile convention for user plugins
|
||||||
|
|
||||||
|
`resolve_plan()` in the Docker backend already has a three-tier cascade. For user
|
||||||
|
plugins the provider-default slot is filled by:
|
||||||
|
|
||||||
|
```python
|
||||||
|
Path.home() / ".bot-bottle" / "contrib" / template / "Dockerfile"
|
||||||
|
```
|
||||||
|
|
||||||
|
Per-bottle overrides and manifest `dockerfile:` fields continue to take precedence.
|
||||||
|
|
||||||
|
### Provisioning methods on `AgentProvider`
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AgentProvider(ABC):
|
||||||
|
...
|
||||||
|
def provision_ca(self, bottle: Bottle, plan: BottlePlan) -> None:
|
||||||
|
"""Install the egress MITM CA into the agent container's trust store.
|
||||||
|
Override for non-Debian base images or non-standard trust mechanisms."""
|
||||||
|
cert_host_path, label = select_ca_cert(plan.egress_plan)
|
||||||
|
bottle.cp_in(str(cert_host_path), AGENT_CA_PATH)
|
||||||
|
bottle.exec(
|
||||||
|
f"chmod 644 {AGENT_CA_PATH} && update-ca-certificates",
|
||||||
|
user="root",
|
||||||
|
)
|
||||||
|
log_ca_fingerprint(cert_host_path, label)
|
||||||
|
|
||||||
|
def provision_git(self, bottle: Bottle, plan: BottlePlan) -> None:
|
||||||
|
"""Configure git inside the agent container.
|
||||||
|
Override for images that run as a different user or use a non-standard home."""
|
||||||
|
_provision_cwd_git(plan, bottle)
|
||||||
|
_provision_git_gate_config(plan, bottle)
|
||||||
|
_provision_git_user(plan, bottle)
|
||||||
|
```
|
||||||
|
|
||||||
|
The Docker backend base class replaces the direct calls to the old standalone
|
||||||
|
functions with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
provider.provision_ca(bottle, plan)
|
||||||
|
provider.provision_git(bottle, plan)
|
||||||
|
```
|
||||||
|
|
||||||
### Manifest validation change
|
### Manifest validation change
|
||||||
|
|
||||||
In `manifest_agent.AgentProvider.from_dict`, remove the hard rejection:
|
In `manifest_agent.AgentProvider.from_dict`, remove the hard rejection:
|
||||||
@@ -172,10 +256,7 @@ if forward_host_credentials and template not in PROVIDER_TEMPLATES:
|
|||||||
|
|
||||||
## Open questions
|
## Open questions
|
||||||
|
|
||||||
1. **Shadow order.** This PRD puts user plugins before built-ins, allowing local
|
1. **`BOT_BOTTLE_CONTRIB_DIR` env var.** Omitted for now — `~/.bot-bottle/contrib/`
|
||||||
overrides. If the preference is built-ins-first (to prevent accidental shadowing),
|
|
||||||
swap the order and document accordingly.
|
|
||||||
2. **`BOT_BOTTLE_CONTRIB_DIR` env var.** Omitted for now — `~/.bot-bottle/contrib/`
|
|
||||||
is consistent with the rest of the user config layout. Revisit if the need surfaces.
|
is consistent with the rest of the user config layout. Revisit if the need surfaces.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
@@ -184,3 +265,5 @@ if forward_host_credentials and template not in PROVIDER_TEMPLATES:
|
|||||||
- PRD 0048 — SSH deploy key provisioning (the `contrib/` convention)
|
- PRD 0048 — SSH deploy key provisioning (the `contrib/` convention)
|
||||||
- `bot_bottle/agent_provider.py` — `get_provider`, `PROVIDER_TEMPLATES`, `AgentProvider` ABC
|
- `bot_bottle/agent_provider.py` — `get_provider`, `PROVIDER_TEMPLATES`, `AgentProvider` ABC
|
||||||
- `bot_bottle/manifest_agent.py` — template validation that this PRD relaxes
|
- `bot_bottle/manifest_agent.py` — template validation that this PRD relaxes
|
||||||
|
- `bot_bottle/backend/docker/provision/ca.py` — current CA provisioner (to be deleted)
|
||||||
|
- `bot_bottle/backend/docker/provision/git.py` — current git provisioner (to be deleted)
|
||||||
|
|||||||
Reference in New Issue
Block a user