Gap-closing on top of the merged A3-3..A3-6 work:
- ProjectService.create_preview_domain now refuses a label that is a reserved
platform name (git, mcp, api, panel, ...) via PROTECTED_HOST_LABELS. The rules
set existed but only the owner-facing validate_extra_host consulted it, so the
tenant path could reserve one.
- process.py: signal.SIGKILL is resolved through getattr with a SIGTERM
fallback, so the module imports and type-checks on Windows where SIGKILL does
not exist.
- Three more Windows test repairs: a git-created .git must be unlinked and
recreated rather than overwritten in place; POSIX-only chmod and symlink
assertions are gated on os.name (the security property they assert is checked
everywhere, the platform-specific mechanics only where they apply).
- docs/plan/reports/agent-3-final.md: the integration hand-off — host
requirements, every setting, every endpoint added or changed, the tables, the
open CHANGE REQUESTS, a 12-step manual QA script and the known limitations.
Checks: ruff clean, mypy clean (136 files), 521 API tests pass, 83 web tests
pass, tsc clean, next lint clean.
# Agent 3 — Final report: Delivery & Infra (phases 1–6)
Date: 2026-10-14 · Branch: `dev` · Supersedes and summarises
`agent-3-phase-1.md` … `agent-3-phase-6.md`.
This file is the integration hand-off: what exists, what it needs on the host,
every setting and endpoint it added, the tables it owns, the change requests still
open, the manual QA script, and the things deliberately not done.
The golden property, stated once because every design decision serves it: **the AI
never touches the host, the network, or a credential.** Git runs as the platform
user with a gitdir that is never mounted into a container; code runs inside
hardened containers; user-supplied hosts pass an SSRF check before a byte leaves;
and nothing reaches production without a human click.
---
## 1. What exists
| Phase | Delivered |
|---|---|
| 1 | `DockerSandboxAdapter` (hardened workspace + demo containers), `process.py` (the single hardened subprocess runner), orphan reaping, infra hidden from non-owners |
| 2 | `LocalGitWorkspaceAdapter` — the platform's own git: per-project gitdir, worktrees per task, per-repo lock, hooks disabled, credentials only ever as a *path* |
| 3 | Live CI/CD: pipeline runs/steps/releases/runtimes, build containers, blue/green demo deploy, live batched logs, remote polling |
| 4 | Owner domain console: list/attach/detach/resync routes, custom FQDNs behind a setting, MCP gateway system route |
| 5 | Production deploy: `asyncssh` + `aioftp` adapters with host-key TOFU, atomic symlink switch, FTP backup-first, `production_deploys` ledger |
| 6 | The Application page (four tabs + live build console + production panel), `/admin/domains`, `/admin/sandbox`, `/projects* → /app` redirect, typed client + contracts |
Phase 5's adapters are **lazy imports**. `asyncssh` and `aioftp` are dependencies,
but importing them costs real start-up time and neither is needed unless somebody
configures a deploy target. `deploy_target.py` therefore resolves them inside
`_connect()`, which is also what keeps `mypy` honest about an optional import.
---
## 2. Host requirements
| Requirement | Why | Notes |
|---|---|---|
| `docker`**or**`podman` on `PATH` (`SANDBOX_DOCKER_BIN`) | the only sanctioned way tenant code runs | the flags used are the common subset; the workspace/demo/build containers are `--read-only`, non-root, `--cap-drop ALL`, `no-new-privileges`, cpu/mem/pids capped |
| `git` ≥ 2.20 on `PATH` | the platform runs git itself (D6) | worktrees + `--git-dir`/`--work-tree` |
| `caddy` on `PATH`**and**`import /etc/caddy/agy-preview/*.caddy` in the Caddyfile (`DOMAIN_ROUTER=caddy`) | preview + custom domains | the adapter **refuses to run** if the import line is missing — silently writing a file Caddy never reads would report a success that is false |
| Caddy reload permission | saving a route reloads the live proxy | `caddy reload` needs to signal the caddy service; on a systemd host that is a polkit/sudoers entry, **not** root. The command used is `caddy reload --config <file>`, so granting the service user passwordless `caddy reload` is sufficient. |
| Writable, disk-rich dirs under `SANDBOX_WORKSPACES_ROOT` / `ARTIFACTS_ROOT` | work trees, per-run copies, artifacts, tarballs | default `./.data/...` (gitignored). Per-run copies mean disk grows with project size × runs — see Known limitations |
| `ftps` targets | explicit TLS | a plain `ftp` target is accepted and documented as unencrypted |
Today the server is the boundary and the UI renders optimistically.
4. **Reserved-label enforcement now lives in two places** and they must stay in
step. The tenant path is guarded in `ProjectService.create_preview_domain`
(`label in PROTECTED_HOST_LABELS` → `PermissionDeniedError`); the owner path is
guarded inside `rules.validate_extra_host`. A future third entry point that
builds a host should call one of them, not re-derive the rule.
---
## 8. Known limitations (deliberate, documented)
- **Build containers cannot write into the AI's work tree.** Each run gets a
*copy* of the tree under `<root>/…/runs/<run_id>/` (`.git`/`node_modules`
excluded). `RepoWorkspacePort` is frozen and has no checkout primitive, and a
build writing `dist/` into `work/` would show up in the next task's diff as if
an employee had changed it.
- **Disk per run.** Nothing prunes `runs/`, `artifacts/` or `ftp-backup-*` yet.
- **The healthcheck is a plain GET** with 20 × 3 s retries, no readiness protocol,
no auth on the probe path. Any 2xx/3xx counts as healthy.
- **`snapshot` does not capture the work tree** (bind mount) — image state only.
- **FTP has no atomic switch and no host key.** The deploy is backup-first so
there is always something to undo, but the window is not zero, and the plain
`ftp` kind is unencrypted by definition.
- **No container-level egress policy for build containers** beyond the default
bridge. A hermetic build would need an explicit deny and a package mirror.
- **`routes_fingerprint` compares host:port pairs.** Changing a *label* on a demo
host is a remove+add, which the digest does catch; a change that keeps both
host and port but edits the site block body would not.
- **The AI never commits, merges, pushes or inspects git** — that is the point
(D6), but it also means Agent 2's engine must go through `RepoWorkspacePort`
for all of it, and a git failure surfaces to the customer as a Persian message
rather than a git exit code.
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.