- 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.
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).
Closes#178.
The backend provision functions now receive a Bottle handle with
exec / cp_in methods instead of a raw target string. Provisioner
modules use bottle.exec and bottle.cp_in in place of inlined
subprocess.run(["docker", "exec"/"cp", ...]) and direct
_smolvm.machine_cp / machine_exec calls. This decouples the
provisioners from backend-specific runtime primitives so future
refactors (e.g. the supervise rework) can swap the bottle's exec
implementation without touching every provisioner.
Each launch.py constructs the Bottle handle before calling
provision so it can be passed in; provision_prompt's return value
is wired back onto the bottle's prompt path attribute after the
fact.
- manifest_git.py: add ProvisionedKeyConfig dataclass; extend GitEntry
with ProvisionedKey field (optional); make IdentityFile default to ""
so provisioned_key entries can be constructed without a static path;
add _parse_provisioned_key_config; update from_repos_entry to accept
provisioned_key as an alternative to identity (mutually exclusive,
parser rejects both-or-neither)
- deploy_key_provisioner.py (new): DeployKeyProvisioner ABC with create()
and delete() abstract methods; get_provisioner() factory with lazy
contrib import for gitea
- contrib/gitea/deploy_key_provisioner.py (new): GiteaDeployKeyProvisioner
generating ed25519 keypairs via ssh-keygen and managing them through
the Gitea deploy-key API (POST/DELETE); 404 on delete is success;
all other errors raise RuntimeError
- git_gate.py: add _provision_dynamic_key() called in GitGate.prepare()
for entries with ProvisionedKey — generates key, writes private key
and key ID files to stage_dir, patches GitGateUpstream.identity_file;
add revoke_git_gate_provisioned_keys() for teardown — raises on failure
- docker/launch.py: call revoke_git_gate_provisioned_keys() in teardown()
after stack.close() so revocation runs after containers stop and
failures propagate (not suppressed)
- smolmachines/launch.py: extract _teardown_smolmachines() helper that
catches stack.close() errors (warn + re-raise) then calls revocation;
same fatal-on-failure contract as docker backend
- test_manifest_git.py: 9 new cases for provisioned_key parsing
- test_deploy_key_provisioner.py (new): factory smoke tests
- test_contrib_gitea_deploy_key.py (new): create/delete/error/split tests
Closes#169
Replace the bare `except BaseException: pass` in the `teardown` closure
with a `warn()` call that includes the container name and operation type
("compose-down"), so cleanup failures are visible in the log rather than
silently discarded. Non-blocking: the exception is consumed and teardown
continues, preserving the original error-propagation contract.
Add test_docker_launch_teardown.py to lock the new behaviour: it injects
a RuntimeError via a mocked `compose_down` callback and asserts the
WARNING message contains the container name and operation label.
Move git_gate_plan, egress_plan, supervise_plan, and agent_provision
from DockerBottlePlan and SmolmachinesBottlePlan into BottlePlan.
Replace the abstract print method with a single concrete implementation
that renders git gate entries as "name → upstream_host:upstream_port"
and egress routes with conditional "[auth:scheme]" annotations.
BottleMetadata gains a backend field (default ""). Docker prepare writes
"docker"; smolmachines prepare writes "smolmachines". read_metadata
deserialises it with "" as the backward-compatible default.
resume now passes metadata.backend to _launch_bottle so a preserved
smolmachines bottle is resumed on the right backend without requiring
BOT_BOTTLE_BACKEND to be set manually.
_bottle_for_slug now reads metadata.backend and constructs a
SmolmachinesBottle for smolmachines slugs instead of always defaulting
to DockerBottle. No-metadata slugs still fall back to Docker.
Closes#137
Add `provisioned_env: dict[str, str]` to `AgentProvisionPlan`. When
`forward_host_credentials=True`, `agent_provision_plan` reads the host
Codex access token at prepare time and stores it under
`CODEX_HOST_CREDENTIAL_TOKEN_REF`. Both backends merge `provisioned_env`
over `os.environ` before calling `egress_resolve_token_values`, so the
token slot resolves like any other manifest-declared token ref.
Removes `egress_resolve_token_values_with_provider` and the sentinel
`continue` skip from `egress_resolve_token_values`. The function is now
fully generic — it neither knows nor cares about provider identity.
Extract egress_resolve_token_values_with_provider into bot_bottle/egress.py.
Both docker and smolmachines launch paths now call the shared function
instead of duplicating the forward_host_credentials / CODEX_HOST_CREDENTIAL_TOKEN_REF
resolution block.
Also fixes the host_env: object annotation on smolmachines._resolve_token_env
to the correct dict[str, str].
Closes#118.
Operators can now declare:
agent_provider:
template: claude
auth_token: BOT_BOTTLE_CLAUDE_OAUTH_TOKEN
and the provisioner injects a provider-owned api.anthropic.com egress
route (Bearer, tls_passthrough) rather than requiring a manually
declared route with the former claude_code_oauth role.
Changes:
- Add auth_token field to AgentProvider; validate claude-only.
- Remove claude_code_oauth from EGRESS_ROLES / PROVIDER_EGRESS_ROLES.
Manifests that declare the role now fail at parse time with "unknown
role" — the provisioner owns the route.
- agent_provision_plan: replace manifest_egress_routes/has_provider_auth
with auth_token; Claude branch injects the api.anthropic.com route,
placeholder env, and nonessential-traffic flags when auth_token is set.
- Add hidden_env_names: frozenset[str] to AgentProvisionPlan; Claude
branch populates it with CLAUDE_CODE_OAUTH_TOKEN.
- Remove auth_role from AgentProviderRuntime and placeholder_env_for().
- print_util.visible_agent_env_names: accept hidden_env_names from the
plan instead of dispatching on agent_provider_template.
- Both backends: drop manifest_egress_routes call, pass auth_token.
- PRD 0029 rescoped to cover both Codex and Claude provider auth.
Assisted-by: Claude Code
The has_provider_auth check and egress-placeholder injection were
duplicated in both backends. Move them into agent_provision_plan so
the provisioner owns that decision entirely:
- Replace has_provider_auth: bool param with manifest_egress_routes,
compute has_provider_auth internally from the route roles.
- Inject CLAUDE_CODE_OAUTH_TOKEN=egress-placeholder inside the plan
when has_provider_auth, alongside the existing nonessential-traffic
vars. Backends no longer touch the placeholder env.
- Remove placeholder_env from AgentProviderRuntime; expose
placeholder_env_for() for print_util's hide-from-summary logic.
Assisted-by: Claude Code
Remove provider-specific branching from egress.py and pipelock.py.
Previously, `egress_routes_for_bottle` and `pipelock_effective_tls_passthrough`
both contained `template == "codex"` checks — the same pattern the rest
of the PR moved out of the backends.
Root cause: `EgressRoute` had no `tls_passthrough` field, so pipelock
couldn't learn from the synthesised Codex routes that they needed
passthrough. Fix:
- Add `EgressRoute.tls_passthrough: bool`. `egress_manifest_routes` lifts
the existing `pipelock.tls_passthrough` manifest flag here; provider
routes set it directly.
- Add `AgentProvisionPlan.egress_routes`. `agent_provision_plan` populates
it for Codex + `forward_host_credentials`, including `tls_passthrough=True`.
- Replace Codex-specific `egress_routes_for_bottle` logic with a generic
`_merge_provider_route` helper. Backends call `egress_routes_for_bottle(bottle,
plan.egress_routes)`; no provider type checks inside egress or pipelock.
- Rewrite `pipelock_effective_tls_passthrough` to read `route.tls_passthrough`
from the merged route set instead of re-implementing the provider check.
- Both backends now call `agent_provision_plan` before `Egress.prepare` and
`PipelockProxy.prepare`, threading `plan.egress_routes` to both. `has_provider_auth`
is derived from `egress_manifest_routes` (manifest routes only — provider
routes carry no auth roles, so the result is identical).
Assisted-by: Claude Code
Agents may declare git.user (name/email); it overlays the referenced
bottle's git.user per-field at Manifest.bottle_for (agent wins on
non-empty), mirroring the extends: merge. git.remotes is rejected on
agents — it carries credentials and host trust and stays bottle-only.
The overlay lives at bottle_for, the single chokepoint both backends
use, so the docker/smolmachines git provisioners are unchanged. Adds
Manifest.git_identity_summary with per-field (agent)/(bottle)
provenance, printed in both preflights and `info`.
Refs #94
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The two Debian-family CA-layout constants lived in
docker/provision/ca.py, which forced the smolmachines backend to
import them cross-backend (smolmachines -> docker). Move them into
the shared backend/util.py next to select_ca_cert; docker, compose,
and smolmachines now all import from there. No behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both backends' provision_ca duplicated _select_ca_cert and the
SHA-256 fingerprint computation verbatim. Lift them into the shared
backend/util.py as select_ca_cert + log_ca_fingerprint; docker and
smolmachines now call the shared helpers. No behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>