Commit Graph

35 Commits

Author SHA1 Message Date
didericis ecaae708f7 feat(provider): support startup args settings
test / unit (pull_request) Successful in 41s
test / integration (pull_request) Successful in 26s
lint / lint (push) Successful in 2m12s
test / unit (push) Successful in 41s
test / integration (push) Successful in 26s
Update Quality Badges / update-badges (push) Successful in 2m9s
2026-06-24 22:51:27 -04:00
didericis-claude 2e790268b0 fix(deploy-key): raise DeployKeyCollisionError on 422 key conflicts
lint / lint (push) Successful in 2m7s
test / unit (push) Successful in 46s
test / integration (push) Successful in 25s
Update Quality Badges / update-badges (push) Successful in 2m7s
Gitea returns HTTP 422 when a deploy key title or public key content
already exists on the repo. The provisioner previously surfaced this
as a generic RuntimeError with the raw status code. Introduce
DeployKeyCollisionError (a RuntimeError subclass) in the base module
and detect 422 in GiteaDeployKeyProvisioner.create so callers can
catch collisions explicitly and the error message names the repo and
title involved.
2026-06-25 02:23:12 +00:00
didericis-codex 3ea35ba5d2 fix: update codex supervise mcp registration
lint / lint (push) Successful in 1m54s
test / unit (pull_request) Successful in 38s
test / integration (pull_request) Successful in 22s
2026-06-23 04:06:21 +00:00
didericis-claude da42740156 refactor(types): move loaded manifest from BottleSpec to BottlePlan
test / integration (pull_request) Successful in 21s
test / unit (pull_request) Successful in 49s
lint / lint (push) Successful in 2m15s
test / unit (push) Successful in 56s
test / integration (push) Successful in 27s
Update Quality Badges / update-badges (push) Successful in 2m37s
BottleSpec.manifest was ManifestIndex | Manifest — a union encoding
two lifecycle stages in one field. The union was unjustifiable:
it forced a type-narrowing workaround (loaded_manifest property)
on every consumer.

Clean split:
- BottleSpec.manifest: ManifestIndex (always; CLI-supplied intent)
- BottlePlan.manifest: Manifest (always; loaded by _validate())

_validate() returns the loaded Manifest directly. prepare() passes
it to _resolve_plan(), which stores it on the plan. All provisioner
code now reads plan.manifest.agent / plan.manifest.bottle — no
union, no asserts, no type: ignore.
2026-06-22 23:54:02 -04:00
didericis-claude 56ef71060a fix(types): add BottleSpec.loaded_manifest to satisfy pyright on union type
BottleSpec.manifest is ManifestIndex | Manifest (pre/post _validate()).
Downstream code always runs post-validate so it needs Manifest, but
pyright flagged every .agent/.bottle access. The new loaded_manifest
property asserts isinstance and returns Manifest, giving pyright a
narrowed type without scattering type: ignore everywhere.

Also remove unused Manifest imports from test files and annotate the
_index() helper in test_manifest_agent_git_user.
2026-06-22 23:54:02 -04:00
didericis-claude 294a6ed023 refactor(manifest): split Manifest into ManifestIndex + Manifest single-value type
Manifest now holds exactly one agent and one effective bottle (with
git_user overlay already applied). The old multi-agent/bottle
collection is renamed ManifestIndex. BottleSpec.manifest starts as
ManifestIndex from the CLI and becomes Manifest after _validate()
calls load_for_agent(); all provisioning code downstream reads
spec.manifest.agent / spec.manifest.bottle instead of indexing by name.
2026-06-22 23:54:02 -04:00
didericis-claude 6e6890ebd9 feat: remove cyan and white from color palette
lint / lint (push) Successful in 1m41s
test / unit (push) Successful in 33s
test / integration (push) Successful in 18s
Update Quality Badges / update-badges (push) Successful in 1m16s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 19:09:22 +00:00
didericis-claude 609b3ed090 feat: drop dim colors, keep only bright variants renamed to base names
test / unit (pull_request) Successful in 38s
test / integration (pull_request) Successful in 21s
lint / lint (push) Successful in 1m40s
test / unit (push) Successful in 34s
test / integration (push) Successful in 19s
Update Quality Badges / update-badges (push) Successful in 1m19s
Remove the 8 non-bright and 1 bright-black colors from all color maps.
Rename the remaining 7 bright-* colors to their base names (e.g.
bright-green → green) so the palette is smaller and always vibrant.

Update _init_color_pairs in tui.py to always apply A_BOLD (all palette
entries are now bright variants), and fix all tests to match.
2026-06-22 18:59:51 +00:00
didericis-claude c1c225aa05 docs(gitea-provisioner): document required GITEA_DEPLOY_TOKEN permissions
lint / lint (push) Successful in 1m46s
test / unit (push) Successful in 34s
test / integration (push) Successful in 20s
Update Quality Badges / update-badges (push) Successful in 1m21s
2026-06-11 03:43:13 +00:00
didericis bb69af31f8 chore(claude): bump claude-code to 2.1.170
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 22:44:46 -04:00
didericis-codex 019efab804 fix: narrow pi numeric settings types
test / unit (pull_request) Successful in 49s
test / integration (pull_request) Successful in 27s
lint / lint (push) Successful in 1m37s
test / unit (push) Successful in 31s
test / integration (push) Successful in 19s
Update Quality Badges / update-badges (push) Successful in 1m20s
2026-06-10 01:13:00 -04:00
didericis 504144eb9c fix(pi): prepare runtime state and agent workdir
lint / lint (push) Failing after 1m58s
test / unit (push) Successful in 41s
test / integration (push) Successful in 24s
Update Quality Badges / update-badges (push) Successful in 1m27s
2026-06-10 00:02:28 -04:00
didericis 86374ab293 fix(pi): select configured startup models
test / unit (pull_request) Successful in 30s
test / integration (pull_request) Successful in 17s
lint / lint (push) Successful in 1m37s
test / unit (push) Successful in 33s
test / integration (push) Successful in 17s
Update Quality Badges / update-badges (push) Successful in 1m6s
2026-06-09 06:57:33 -04:00
didericis 199edb228c feat(pi): add fd and ripgrep to image
test / unit (pull_request) Successful in 35s
test / integration (pull_request) Successful in 18s
2026-06-09 06:26:19 -04:00
didericis 598a20a3f0 fix(pi): keep interactive sessions open
lint / lint (push) Successful in 1m29s
test / unit (pull_request) Successful in 30s
test / integration (pull_request) Successful in 17s
2026-06-09 06:00:40 -04:00
didericis c8b5ba3812 feat(pi): support egress injected api keys
lint / lint (push) Successful in 1m38s
test / unit (pull_request) Successful in 31s
test / integration (pull_request) Successful in 17s
2026-06-09 05:56:39 -04:00
didericis-codex 4f7cfc0418 feat: add pi agent provider 2026-06-09 08:31:48 +00:00
didericis b1551045dc feat(terminal): tint terminal background per agent color
test / unit (pull_request) Successful in 29s
test / integration (pull_request) Successful in 17s
Add backend-agnostic terminal color support via OSC escape sequences:
- New backend/terminal.py with palette_printf() and exec_shell_script()
  shared by both Docker and smolmachines bottle backends
- Emits OSC 4 (indexed palette) + OSC 11 (default background tint)
  before launching; resets both on agent exit via OSC 104/111
- OSC 11 background tint is visible even when the TUI uses true/24-bit
  colors (which bypass the palette), as Codex does for its chrome
- Fix Codex [tui] config: status_line=["model-with-reasoning"],
  theme="ansi" (dark-ansi and cwd/directory were invalid identifiers)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 00:47:55 -04:00
didericis d02226aab9 feat: forward agent style via native CLI config and terminal title
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>
2026-06-09 00:47:55 -04:00
didericis-codex 39811c9b32 feat: forward agent display identity to prompts 2026-06-09 00:47:55 -04:00
didericis-claude a64e3170cd refactor: make AgentProvisionPlan the source of truth for instance_name, prompt_file, image, dockerfile, guest_home
Drop the parallel fields passed through prepare() → _resolve_plan and
read everything from agent_provision instead. The provider plugin now
declares its own guest_home (so the backend stops hardcoding
"/home/node") and the wrapper that builds the provision plan accepts
instance_name and prompt_file, which providers store on the plan.

DockerBottlePlan and SmolmachinesBottlePlan expose container_name /
machine_name, image / agent_image, dockerfile_path /
agent_dockerfile_path, and prompt_file as properties that delegate to
agent_provision so existing call sites keep working unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 23:05:14 -04:00
didericis-claude f23b2b9683 refactor: move guest_home onto AgentProvisionPlan as source of truth
guest_home is now a field on AgentProvisionPlan (set by each provider's
provision_plan() method). BottlePlan.guest_home becomes a read-only
property delegating to agent_provision.guest_home so existing callers
(provision_git, provision_skills, provision_prompt) are unchanged.

Both resolve_plan.py files drop guest_home from the plan constructor
call; the local variable still exists as an intermediary for the
workspace_plan call that precedes agent_provision_plan.
2026-06-08 23:05:14 -04:00
didericis-claude ee0607f022 refactor: replace runtime.dockerfile with AgentProvider.dockerfile property
Drop the `dockerfile` field from `AgentProviderRuntime` and replace it
with a convention-based `dockerfile` property on `AgentProvider`: the
base class looks for a `Dockerfile` file next to the provider's own
`agent_provider.py` module (via `inspect.getfile`), returning its path
or None. Built-in providers inherit the default automatically; custom
user providers work the same way by dropping a Dockerfile next to their
plugin file; any provider needing a non-standard path can override.

All callers (`docker/prepare.py`, `smolmachines/prepare.py`,
`capability_apply.py`) now resolve the provider object once and call
`.dockerfile` directly instead of reading `runtime.dockerfile`.
2026-06-08 23:05:14 -04:00
didericis-claude afe5d43a9a refactor: move agent Dockerfiles into their contrib directories
Dockerfile.claude and Dockerfile.codex move from the repo root into
bot_bottle/contrib/claude/Dockerfile and bot_bottle/contrib/codex/Dockerfile
respectively, so all per-provider assets live alongside the provider code.

Closes #215
2026-06-08 23:05:14 -04:00
didericis 04d7ca2e6a feat(agents): named and labelled agents with optional ANSI color
test / unit (pull_request) Successful in 32s
test / integration (pull_request) Successful in 43s
lint / lint (push) Successful in 1m32s
prd-number / assign-numbers (push) Successful in 17s
test / unit (push) Successful in 29s
Update Quality Badges / update-badges (push) Successful in 1m18s
test / integration (push) Successful in 45s
Chunk 1 (schema + storage): BottleSpec, ActiveAgent, and BottleMetadata
gain label and color fields. Both docker and smolmachines backends
persist them to metadata.json on prepare and surface them in
enumerate_active_agents(). AgentProvider.provision_plan() passes
label/color through to the Claude provider, which injects them into
claude.json so claude-code displays the session name and color in its
header. Codex provider accepts and ignores the knobs.

Chunk 2 (curses modal + display): cmd_start presents a two-step curses
modal — first edit the label (first keystroke replaces the pre-fill),
then optionally pick a color. cli list active renders label with ANSI
escape codes when the terminal supports it, falling back to agent_name
when no label is set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 12:12:32 -04:00
didericis-claude d528f578aa fix: correct broken imports and fileno() guard after rebase
codex_auth.py was moved into contrib/codex/ but still used `.log`/
`.util` relative imports that resolved to the parent bot_bottle
package before the move — update to `...log` / `...util`.

_read_winsize() called sys.stdin.fileno() outside the OSError guard;
pytest's redirected stdin raises UnsupportedOperation (an OSError
subclass) there, breaking test_returns_first_tty_size. Move fileno()
inside the try block so any non-TTY stream is skipped cleanly.
2026-06-07 11:35:35 -04:00
didericis-claude 74d6b25183 refactor: move codex_auth into contrib/codex
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 11:35:35 -04:00
didericis-claude 05b12b41b6 fix: remove remaining pipelock references missed in prior pass
lint / lint (push) Failing after 1m20s
test / unit (pull_request) Successful in 33s
test / integration (pull_request) Successful in 46s
- test_supervise.py: drop TOOL_PIPELOCK_BLOCK import; update TOOLS
  assertion to match the 3-item tuple (egress, capability, list-egress)
- test_supervise_server.py: remove pipelock from tools-list assertion,
  fix test_rejected_response_sets_isError to use capability-block
- contrib/claude and contrib/codex: remove tls_passthrough=True from
  EgressRoute constructors (field removed with pipelock)
- test_egress.py: drop tls_passthrough parameter from _provider_route,
  remove tls_passthrough-only tests, fix EgressRoute constructions
- test_agent_provider.py: drop route.tls_passthrough assertions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 21:58:36 +00:00
didericis a5078daf1c fix: resolve all 22 remaining pylint warnings
Lint and Type Check / lint (push) Has been cancelled
test / unit (pull_request) Has been cancelled
test / integration (pull_request) Has been cancelled
Fixed issues across bot_bottle/:

1. Unspecified encoding in open() - 6 files:
   - Added encoding='utf-8' to Path.read_text() and open() calls
   - Files: env.py, pipelock_apply.py, prepare.py, loopback_alias.py, _common.py, supervise.py

2. Exception chaining (raise-missing-from) - 5 files:
   - Added 'from e' to raise statements for proper traceback chaining
   - Files: manifest_loader.py (2x), manifest_egress.py

3. Redefining built-in 'format' - 2 files:
   - Added # noqa: A002 comments to override methods
   - Files: supervise_server.py, git_http_backend.py

4. Unused function arguments - 5 files:
   - Added # noqa: F841 comments for interface-required unused params
   - Files: manifest_loader.py, supervise.py, loopback_alias.py, cli/supervise.py

5. Broad exception catching - 6 files:
   - Added # noqa: broad-exception-caught comments with explanations
   - Files: supervise_server.py, docker/launch.py, smolmachines/launch.py, tui.py, supervise.py, deploy_key_provisioner.py

6. Unreachable code - 3 files:
   - Removed unreachable return statements after die() calls
   - Files: loopback_alias.py, sidecar_bundle.py, local_registry.py

7. Unnecessary ellipsis in Protocol - 2 files:
   - Reverted pass back to ... (more idiomatic for Protocols)
   - Files: workspace.py, backend/__init__.py

8. Platform-specific function redeclaration:
   - Added type: ignore[reportRedeclaration] for Unix/Windows variants
   - File: supervise.py (_try_flock, _try_funlock)

Final scores:
 Pylint: 9.95/10 (0 E/W violations)
 Pyright: 0 errors (100% type safe)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-04 11:42:40 -04:00
didericis a430bac1bf fix: resolve remaining pyright errors across the codebase
Lint and Type Check / lint (push) Failing after 6m54s
test / unit (pull_request) Successful in 34s
test / integration (pull_request) Failing after 44s
Main code fixes:
- Remove unused Iterator import from local_registry.py
- Fix signal handler signature in pty_resize.py (correct parameters for signal.signal)
- Add type annotations for screen parameters in tui.py (use Any for curses types)
- Fix missing tty_fd type annotation in tui.py
- Remove unused old_term variable in tui.py
- Fix tty_fd FileIO wrapping for TextIOWrapper initialization
- Add type: ignore for curses._CursesWindow attributes in supervise.py
- Add type: ignore for BaseServer attributes in git_http_backend.py
- Fix HTTPRequestHandler.log_message parameter name mismatch
- Cast _agent_prompt_mode to PromptMode in bottle.py files
- Fix Popen[bytes] generic type annotations in sidecar_init.py
- Add type: ignore for dynamic prompt_file attribute access in agent_provider.py

Configuration:
- pyrightconfig.json now suppresses third-party library unknowns
- Remaining test errors are mostly in test suites

Fixes 23 errors in main code, reduces total from 985 → 240 (75% reduction from initial ~1,200)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-03 23:53:04 -04:00
didericis-claude ea66f63d45 refactor(backend): hoist guest_home to BottlePlan base
test / unit (push) Successful in 37s
test / integration (push) Successful in 54s
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.
2026-06-03 21:38:13 -04:00
didericis-claude 83db7336c8 refactor(agent_provider): drop GUEST_HOME default, backend drives guest_home
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.
2026-06-03 21:38:13 -04:00
didericis-claude bcdffc8400 refactor(contrib): inline provision steps per-provider, drop shared apply module
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).
2026-06-03 21:38:13 -04:00
didericis-claude 44365ecf68 refactor(agent_provider): introduce AgentProvider ABC + contrib plugins
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.
2026-06-03 21:38:13 -04:00
didericis-claude 0b5d59cf9e feat(prd-0048): implement SSH deploy-key provisioning with contrib/gitea
- 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
2026-06-03 11:58:36 -04:00