Domains are infrastructure, and in v2 infrastructure is the platform owner's
alone (D9). A tenant gets one automatic demo host and nothing else.
- admin_domains.py (owner-only): list every route with its owning app, attach an
extra host, detach with the host typed back as confirmation (audited), re-sync
the router from the database, and read the suggested MCP gateway route.
- domain/platform/rules.py: validate_extra_host. Two shapes only — a single
label under the managed root, or a full custom FQDN when ALLOW_CUSTOM_DOMAINS
is on. A wildcard is refused in both (it would match another tenant's
subdomain), and so are 30 protected platform labels (git, mcp, panel, api...).
Enforced in the router *and* again in the adapter.
- Both router adapters gained routes_fingerprint / list_all_routes /
sync_routes. Re-sync writes and reloads only when the rendered state differs:
a reload drops every in-flight connection on the host, including live demos.
Adapter-level extras, not additions to the frozen DomainRouterPort.
- The member-facing preview-domain routes are owner-only now. They are kept
because the demo host is still created through them at app-creation time.
Tests: 473 API (+58); ruff and mypy clean.
Report: docs/plan/reports/agent-3-phase-4.md
The platform's side of D6: git runs on the host, in a gitdir that is never
mounted into a container, so repository credentials never reach the model.
- adapters/git_workspace.py: LocalGitWorkspaceAdapter implementing
RepoWorkspacePort. Bare shared gitdir + a dev work tree mounted at
/workspace + one linked worktree per task under work/.worktrees/<id>
(excluded via the gitdir's info/exclude, not the project's .gitignore).
Explicit --git-dir/--work-tree, core.hooksPath=/dev/null, fsmonitor off,
quotePath off, GIT_CONFIG_NOSYSTEM, a private HOME per project, prompts off,
one asyncio.Lock per project. Token via GIT_ASKPASS reading a 0600 file; SSH
key as a 0600 temp file with a pinned known_hosts. Everything deleted in a
finally; only a *path* ever travels through the environment.
- The bug this phase existed to find: --git-dir=<shared> does not address a
linked worktree, so `git commit` in a task worktree committed straight to
`dev`. Operations now address <gitdir>/worktrees/<task_id> explicitly — and
never read the worktree's .git file, which a repository can rewrite.
- domain/platform/rules.py: is_blocked_address (pure) + assert_remote_host_allowed
(resolves, refuses if any address is loopback/private/link-local/metadata).
Shared by the git remote test and, later, every deploy target.
- ProjectExternalGit domain model + repository; container_delivery wires the
real adapter behind GIT_WORKSPACE=local with a resolver that joins the
project, its repo and its (decrypted) credentials.
Tests: 394 API (+48) against real git and a local bare repo; ruff and mypy clean.
Report: docs/plan/reports/agent-3-phase-2.md
مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت.
تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.