Commit Graph

625 Commits

Author SHA1 Message Date
didericis 8a09e32fcc docs(prd): add PRD 0050 -- strip dashboard to supervisor tui
test / unit (pull_request) Successful in 51s
test / integration (pull_request) Successful in 54s
2026-06-03 13:15:05 -04:00
didericis-claude 83463f1cc8 docs(prd): activate PRD 0048 — SSH deploy-key provisioning
test / unit (push) Successful in 40s
test / integration (push) Successful in 41s
2026-06-03 11:58:36 -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
didericis-claude 464012d97c docs(prd): address review feedback on PRD 0048
- Rename deploy_key → provisioned_key throughout (manifest key,
  dataclass names, internal field names, test descriptions)
- Revocation failure at teardown now halts cleanup and propagates
  loudly; a stranded key is a security concern that must surface
2026-06-03 11:58:36 -04:00
didericis-claude b5f8a27c47 docs(prd): add SSH deploy-key provisioning plan (PRD 0048)
Introduces the design for short-lived deploy keys provisioned at spin-up
and revoked at teardown, plus the contrib package structure for
platform-specific provisioner implementations. First contrib provider
targets the Gitea deploy-key API.

Closes #169
2026-06-03 11:58:36 -04:00
didericis-claude f0ca4e3527 refactor: extract dashboard state/model layer into dashboard_model.py
test / unit (pull_request) Successful in 41s
test / integration (pull_request) Successful in 47s
test / unit (push) Successful in 35s
test / integration (push) Successful in 44s
Splits the 2103-line dashboard.py into two modules. Pure data
structures (QueuedProposal), discovery helpers (discover_pending,
discover_active_agents), derived-value helpers (_is_recent,
_approval_status, _format_agent_row, _detail_lines, etc.), and
argv-builder helpers (_build_split_pane_argv, _build_respawn_pane_argv,
_build_resume_argv_with_fallback, _agent_runtime_args) all move to
dashboard_model.py. The curses TUI, $EDITOR integration, tmux
subprocess flows, and action handlers (approve, reject,
operator_edit_routes, operator_edit_allowlist) remain in dashboard.py,
which re-imports everything from dashboard_model so existing callers and
tests are unaffected.

Adds tests/unit/test_dashboard_model.py covering _approval_status,
_proposed_payload_label, and _suffix_for_tool — three helpers that had
no prior coverage. All 894 unit tests pass.

Closes #158
2026-06-03 15:52:27 +00:00
didericis-claude ca6d257f30 test(git-gate): add shell-escaping regression tests (issue #159)
test / unit (pull_request) Successful in 36s
test / integration (pull_request) Successful in 44s
test / unit (push) Successful in 35s
test / integration (push) Successful in 42s
Cover all six pathological character classes (single-quote,
double-quote, space, semicolon, newline, backtick) in both
upstream URL and name positions.  Each case validates rendered
output via `sh -n` and asserts the original value is preserved
verbatim after shlex.quote encoding.  Also add `sh -n` smoke
tests for the static pre-receive and access-hook scripts.
2026-06-03 14:51:23 +00:00
didericis-claude cc0c952d0b fix(security): harden git_gate.py shell rendering with shlex.quote and name validation
test / unit (pull_request) Successful in 35s
test / integration (pull_request) Successful in 44s
test / unit (push) Successful in 32s
test / integration (push) Successful in 41s
Use shlex.quote() on name and upstream_url in git_gate_render_entrypoint()
so special characters (single quotes, spaces, semicolons) cannot break or
inject into the generated sh script.

Add _GIT_NAME_RE validation in GitEntry.from_repos_entry() to restrict
repo names to [A-Za-z0-9._-]+, making the manifest the first line of
defence and shlex.quote() the belt-and-suspenders backstop.

Closes #155
2026-06-03 04:40:21 +00:00
didericis-claude 8c9d4fbc46 refactor: address PR review feedback — de-privatize helpers and rename modules
test / unit (pull_request) Successful in 34s
test / integration (pull_request) Successful in 43s
test / unit (push) Successful in 34s
test / integration (push) Successful in 43s
- Rename _manifest_util.py → manifest_util.py (module isn't private)
- Rename _as_json_object → as_json_object, _parse_git_upstream → parse_git_upstream,
  _parse_git_gate_config → parse_git_gate_config,
  _validate_unique_git_names → validate_unique_git_names,
  _validate_egress_routes → validate_egress_routes (none are private at
  module boundary — underscore prefix was a carry-over from the old
  monolithic manifest.py where everything lived in one namespace)
- Move _is_ip_literal → util.is_ip_literal (generic, belongs in the
  top-level util module)
- Update all import sites across manifest_*.py, manifest_extends.py,
  manifest_schema.py; existing callers of manifest.py are unaffected

All 867 unit tests pass.
2026-06-03 00:33:02 -04:00
didericis-claude b9ab1263c2 refactor: split manifest.py into domain-specific modules
Closes #157. Distributes the 1,026-line manifest.py across four
focused modules:

- _manifest_util.py: ManifestError + _as_json_object (shared base)
- manifest_git.py: GitEntry, GitUser, git-gate config helpers
- manifest_egress.py: EgressRoute, EgressConfig, PipelockRoutePolicy
- manifest_agent.py: AgentProvider, Agent

manifest.py is now the residual orchestration layer: Bottle, Manifest,
and re-exports of all public names so existing callers are unaffected.
All 867 unit tests pass.
2026-06-03 00:33:02 -04:00
didericis-claude 9282bceaf8 fix: emit WARNING when Docker teardown ExitStack raises (issue #156)
test / unit (pull_request) Successful in 37s
test / integration (pull_request) Successful in 40s
test / unit (push) Successful in 32s
test / integration (push) Successful in 43s
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.
2026-06-03 04:13:53 +00:00
didericis-claude 3e50079bcc docs(prd): activate git-gate manifest redesign
test / unit (pull_request) Successful in 41s
test / integration (pull_request) Successful in 1m10s
test / unit (push) Successful in 39s
test / integration (push) Successful in 54s
PRD 0047 is now shipped to main.
2026-06-02 23:59:34 -04:00
didericis-claude cf9aaf68e7 chore: update demo manifest and example agent to git-gate (PRD 0047)
bot-bottle.demo.json: git array → git-gate.repos with url/identity/host_key
examples/agents/implementer.md: git.user → git-gate.user
2026-06-02 23:59:34 -04:00
didericis-claude 4cf2cfc55d test: update test suite for git-gate manifest redesign (PRD 0047)
- fixtures.py: fixture_with_git_dict uses git-gate.repos + url/identity/host_key
- test_manifest_git: rewrite to use git-gate.repos; replace duplicate-name
  test (names = dict keys, always unique) with two-repos-different-hosts test
- test_manifest_git_user: _manifest → git-gate.user; update error message assertions
- test_manifest_agent_git_user: git → git-gate throughout; repos rejection test
- test_manifest_extends: git.remotes/git.user → git-gate.repos/git-gate.user
- test_provision_git: IP test updated — no host alias, single insteadOf
- test_compose: git.remotes → git-gate.repos + new field names
- test_docker_provision_git_user: git.user → git-gate.user
- test_git_gate: inline manifest dict updated to git-gate.repos
- test_smolmachines_provision: git_json → git_gate_json; remove _remote_host
2026-06-02 23:59:34 -04:00
didericis-claude 7c285fde7a feat(manifest): replace git key with git-gate (PRD 0047)
- BOTTLE_KEYS and AGENT_KEYS_OPTIONAL: "git" → "git-gate"
- GitEntry: remove from_dict/from_remote_dict; add from_repos_entry
  parsing url/identity/host_key with repo name as the dict key
- GitUser.from_dict: error messages updated to git-gate.user
- _parse_git_config → _parse_git_gate_config; repos/user subkeys
- Bottle.from_dict: reads git-gate key; "git" key raises a migration error
- Agent.from_dict: reads git-gate key; repos rejected at agent level
- manifest_extends: _child_declares_git_remotes → _child_declares_git_gate_repos
- manifest_loader: threads git-gate frontmatter key into agent_dict
2026-06-02 23:59:34 -04:00
didericis-claude 64ac204c05 docs(prd): consolidate git.user into git-gate per review
Move git.user under git-gate and remove git as a top-level key
entirely, so all git configuration lives under a single section.
2026-06-02 23:59:34 -04:00
didericis-claude 59fd132b9d docs(prd): add git-gate manifest redesign plan
PRD 0047 proposes replacing git.remotes with a top-level git-gate.repos
section and snake_case field names to make clear the config is
specifically for git-gate routing, not generic git or SSH config.

Closes #160
2026-06-02 23:59:34 -04:00
didericis f427d35e72 fix(git-http): log access-hook denial detail to stdout
test / unit (pull_request) Successful in 33s
test / integration (pull_request) Successful in 39s
test / unit (push) Successful in 43s
test / integration (push) Successful in 59s
Previously when the access-hook returned non-zero, git-http would pipe
the hook's stderr into the 403 body sent back to the agent's git
client but never log it locally, so docker logs just showed
`"GET ... 403 -"` with no explanation. Operators had to shell into
the sidecar and re-run the hook by hand to find out why a clone was
being refused (e.g. upstream SSH unreachable, missing credentials).

Route the hook's stderr/stdout through the existing log_message
channel before sending the 403, one log line per output line so the
default request-log format stays readable. When the hook exits
non-zero with no output, log the exit code so the line is still
informative.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 23:29:39 -04:00
didericis-codex 1105d9a269 chore(skills): add quality evaluation skill
test / unit (push) Successful in 48s
test / integration (push) Successful in 56s
2026-06-02 18:42:48 +00:00
didericis-codex 46e596d0b1 docs(prd): renumber host override removal to 0046
test / unit (push) Successful in 46s
test / integration (push) Successful in 56s
2026-06-02 18:32:55 +00:00
didericis-codex a3a8a01b09 docs(prd): activate git remote host override removal
test / unit (pull_request) Successful in 33s
test / integration (pull_request) Successful in 44s
test / unit (push) Successful in 36s
test / integration (push) Successful in 52s
2026-06-02 18:17:29 +00:00
didericis-codex 941f316462 feat(git-gate): remove git remote host override plumbing 2026-06-02 18:17:24 +00:00
didericis-codex be3defe5d8 docs(prd): add git remote host override removal plan 2026-06-02 18:16:24 +00:00
didericis 3885e2f5ad fix(workspace): include hidden cwd files in docker layer
test / unit (pull_request) Successful in 32s
test / integration (pull_request) Successful in 42s
test / unit (push) Successful in 33s
test / integration (push) Successful in 40s
2026-06-02 13:12:40 -04:00
didericis-codex a08829573d docs(prd): activate workspace porting plan
test / unit (pull_request) Successful in 33s
test / integration (pull_request) Successful in 40s
2026-06-02 17:01:53 +00:00
didericis-codex d5fcbe53ef feat(workspace): port cwd across backends 2026-06-02 17:01:19 +00:00
didericis-codex 6150497b47 feat(workspace): trust resolved project path 2026-06-02 16:57:52 +00:00
didericis-codex 5308d53288 feat(workspace): add shared workspace plan 2026-06-02 16:56:57 +00:00
didericis-codex d01f4b6613 docs(prd): add workspace porting plan 2026-06-02 16:54:50 +00:00
didericis 44273be9eb fix(dashboard): stop agents in dashboard from moving during selection
test / unit (push) Successful in 38s
test / integration (push) Successful in 58s
2026-06-02 12:47:00 -04:00
didericis 096c7b8196 fix(codex): update cli image version
test / unit (push) Successful in 37s
test / integration (push) Successful in 57s
2026-06-02 12:42:09 -04:00
didericis 0432a5d3ff fix(codex): keep dummy auth refresh timestamp valid
test / unit (push) Successful in 49s
test / integration (push) Successful in 1m0s
2026-06-02 12:40:14 -04:00
didericis-claude fcd1b34e49 docs: mark PRD 0044 Active
test / unit (pull_request) Successful in 34s
test / integration (pull_request) Successful in 41s
test / unit (push) Successful in 34s
test / integration (push) Successful in 43s
2026-06-02 12:12:08 -04:00
didericis-claude a0762ac2d3 test: add cross-backend print parity tests (PRD 0044)
Shared fixtures build DockerBottlePlan and SmolmachinesBottlePlan from
identical git_gate_plan and egress_plan inputs and assert that both
backends render the same git gate lines (name → host:port) and egress
lines (host [auth:scheme] when authenticated, host alone otherwise).
2026-06-02 12:12:08 -04:00
didericis-claude 53219a55e1 refactor: hoist plan fields and print to BottlePlan base class (PRD 0044)
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.
2026-06-02 12:12:08 -04:00
didericis-claude 71ac555f25 docs(prd): add PRD 0044 — print parity across backends 2026-06-02 12:12:08 -04:00
didericis-claude f25fa589fe fix(git-http): extract peer variable to clarify access hook call convention
test / unit (push) Successful in 31s
test / integration (push) Successful in 43s
Both remote-addr and peer-addr args to the access hook are the same
TCP peer in this non-proxied stack. Extract a `peer` variable so the
intentional repetition is visible. Closes #148.
2026-06-02 16:08:15 +00:00
didericis-claude 4fdf354b4f docs: mark PRD 0043 Active
test / unit (pull_request) Successful in 34s
test / integration (pull_request) Successful in 43s
test / unit (push) Successful in 34s
test / integration (push) Successful in 41s
2026-06-02 11:48:24 -04:00
didericis-claude 5a2011c48f fix: close child stdout pipes on restart and loop convergence (PRD 0043)
Closes #140. In restart_daemon, the old process's stdout pipe was never
explicitly closed after p.wait() returned, leaking the fd until the
supervisor object was GC'd. Similarly, when the watch loop converged
(all children dead), no pipe was closed. Both paths now call
p.stdout.close() immediately after the process is confirmed exited.
Tests enforce this with warnings.simplefilter("error", ResourceWarning)
in TestSupervisor.setUp.
2026-06-02 11:48:24 -04:00
didericis-claude 19ebcd52a1 docs: add PRD 0043 2026-06-02 11:48:24 -04:00
didericis-claude 2c061d9cd9 docs: mark PRD 0042 Active
test / unit (pull_request) Successful in 40s
test / integration (pull_request) Successful in 55s
test / unit (push) Successful in 40s
test / integration (push) Successful in 46s
2026-06-02 11:30:54 -04:00
didericis-claude cceb300d58 test: add cross-backend parity tests (PRD 0042)
Closes #139. Adds tests/unit/test_backend_parity.py which verifies that
DockerBottle and SmolmachinesBottle expose identical observable contracts
for agent_argv shape, env injection, exec user-switching, ExecResult
fields, and close() idempotency. All assertions use mock subprocess
layers — no live Docker daemon or VM required.
2026-06-02 11:30:54 -04:00
didericis-claude b63927368a docs: add PRD 0042 2026-06-02 11:30:54 -04:00
didericis 4319b4ef3b refactor(git-http): rename variable to indicate configurability
test / unit (pull_request) Successful in 38s
test / integration (pull_request) Successful in 54s
test / unit (push) Successful in 40s
test / integration (push) Successful in 57s
2026-06-02 11:24:54 -04:00
didericis-claude 71005d56e2 docs: mark PRD 0041 Active
test / unit (pull_request) Successful in 40s
test / integration (pull_request) Successful in 53s
2026-06-02 11:23:19 -04:00
didericis-claude 96b0c3f1fa fix(git-http): validate Content-Length and cap body size (PRD 0041)
Before this change, int() on a non-numeric Content-Length raised an
unhandled ValueError, crashing the request handler. There was also no
upper bound on how much memory a POST body could consume.

After this change:
- Non-numeric or missing Content-Length returns HTTP 400.
- Negative Content-Length returns HTTP 400.
- Bodies declared larger than 1 MiB (_MAX_BODY_BYTES) return HTTP 413,
  matching the cap already in supervise_server.py.

Closes #138
2026-06-02 11:23:19 -04:00
didericis-claude 3087a9aa8b docs: add PRD 0041 2026-06-02 11:23:19 -04:00
didericis-claude e43f75dd1b refactor: rename machine_name to instance_name in _bottle_for_slug
test / unit (pull_request) Successful in 32s
test / integration (pull_request) Successful in 41s
test / unit (push) Successful in 39s
test / integration (push) Successful in 1m0s
2026-06-02 11:16:17 -04:00
didericis-claude 4ad1ff3898 docs: mark PRD 0040 Active 2026-06-02 11:16:17 -04:00
didericis-claude a3d9ac9605 feat: persist backend in BottleMetadata; use it in resume and dashboard reattach (PRD 0040)
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
2026-06-02 11:16:17 -04:00