Draft PRD for prompting operators for a custom label and optional
ANSI color at agent launch time, storing both in metadata.json, and
surfacing the label (in color) in the dashboard's active-agents pane.
Closes#171
- 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
- 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
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
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
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.
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
- 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.
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.
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.
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
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>
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).
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.
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.