14 KiB
Chat-Driven Project Bootstrap
Design overview for going from "I want a new project" to a running, container-isolated, editor-accessible huskies project in one chat command.
Goal
A user can say to Timmy in chat:
new project myapp --stack rust
new project legacy-rails --git git@github.com:me/legacy-rails.git
and end up with:
- A fresh docker container running the project's huskies node.
- The project's source code bind-mounted from the host so the user can edit it in any editor.
- SSH into the container so editors can run LSPs, builds, and tests inside the container — never on the host.
- Optional git remote configured for push to GitHub or Gitea.
- The new sled registered with the gateway, so Timmy can drive coders / mergemaster / etc. on the project via existing chat commands.
Manual repo creation on GitHub/Gitea remains the user's job. Everything downstream of that is orchestrated.
Architecture at a Glance
┌──────────────────────┐
│ Browser / Matrix │───┐
└──────────────────────┘ │
▼
┌───────────────────────┐
│ Gateway (huskies-gw) │
│ • chat dispatcher │
│ • new-project │
│ • routing │
└─────────┬─────────────┘
│
┌─────────┴───────────────────────────────────┐
│ docker engine (host) │
│ ┌────────────┐ ┌────────────┐ ┌─────────┐ │
│ │ project-A │ │ project-B │ │ ... │ │
│ │ sled + │ │ sled + │ │ │ │
│ │ sshd + │ │ sshd + │ │ │ │
│ │ LSPs │ │ LSPs │ │ │ │
│ └─────┬──────┘ └─────┬──────┘ └─────────┘ │
└────────┼──────────────┼─────────────────────┘
│ │
bind mount │ │ bind mount
┌────────┴───┐ ┌─────┴──────┐
│ ~/code/A │ │ ~/code/B │ ◄── host
└────────────┘ └────────────┘ editor opens
these paths
- One container per project. The container runs the project's huskies binary (sled), an SSH server, and the stack-appropriate LSP(s).
- Source lives on the host (e.g.
~/code/<project>), bind-mounted into the container at a known path. Host can git-diff, back up, or edit. - The gateway is editor-agnostic and project-agnostic — it talks to each sled via the existing rendezvous / CRDT-sync protocol.
Three Personas
| Persona | What they do | What they need |
|---|---|---|
| Chat-only user | Drives everything via Matrix/web chat | Installed huskies binary; chat client |
| Editor-using technical user | Same + edits source in their editor | SSH config to the container + editor-specific remote-dev setup |
| Multi-project user | Several projects running in parallel | Gateway-listed projects, all routable from one chat |
Chat-only users never touch SSH. Editor users go through a one-time "copy this SSH command into your editor's remote settings" handoff at project creation time.
The Bootstrap Chat Command
new project <name> [--stack <stack>] [--git <url>] [--path <host-path>]
Flow:
- Validate: name unique among existing projects; host path doesn't already exist; stack (if declared) is one of the supported overlays.
- Allocate a fresh per-project port range (gateway picks).
- Create host directory at
--path(default~/huskies/<name>/). - If
--gitprovided,git cloneinto that directory; elsegit init. - Detect stack from cloned content if not declared:
Cargo.toml→rustpackage.json→nodego.mod→gopyproject.toml/requirements.txt/setup.py→pythonGemfile→rubypom.xml/build.gradle→jvm- Multiple → pick the dominant, warn.
- None → minimal base image, user can install tooling later.
- Compose the container from
huskies-project-base+ the stack overlay (Dockerfile fragments underdocker/stacks/<stack>/). - Launch the container with bind mount + port forwards + an auto-generated SSH key.
- Seed
.huskies/project.tomlwith sensible defaults. - Register the project with the gateway (
gateway_projectsLWW-map). - Reply in chat with: project name, host path, SSH command, and
a
huskies status <name>invocation to verify.
Container Template
Layered:
-
huskies-project-base: debian-slim + git + huskies binary + sshd- sudo + a
huskiesuser with the SSH pubkey installed.
- sudo + a
-
huskies-project-<stack>: per-stack additions, pre-built byscript/build-project-images. E.g. rust getsrustup+rust-analyzer+cargo-nextest; node getsnode@22+typescript-language-server; etc. Stack fragments live indocker/stacks/<stack>/Dockerfile.fragment. -
huskies-project-local-<name>(optional): built on the fly at container launch time when the project contains.huskies/Dockerfile.fragment. This file is appended after the stack overlay (FROM huskies-project-<stack>) so agents can extend their own image without editing shared stack files. Because the fragment lives inside the bind-mounted/workspace/.huskies/, changes survive container recreation and are committed alongside the project source. Theproject-rebuildcommand picks up the fragment automatically when rebuilding.Example
.huskies/Dockerfile.fragmentthat addsjq:RUN apt-get update && apt-get install -y jq -
Project layer: the bind-mounted
/workspaceis the project source, written by the host's editor, read by the in-container tooling.
The container's SSH server is bound to a host-local port (not exposed externally). Auth is the per-project keypair generated at bootstrap; the public key sits inside the container, the private key on host.
Build Sandbox Model
The threat: editing code in a host-side editor causes the editor (or its
LSP plugin) to run cargo check / npm install / pip install /
similar, which executes arbitrary code from project dependencies —
build.rs, proc-macros, npm postinstall, Python setup.py, Ruby
native-extension build scripts, etc. A malicious dependency compromises
the host.
The mitigation: all build / type-check / dependency-install commands
execute inside the project container. The host's editor connects to
the container over SSH; rust-analyzer (or equivalent) runs inside the
container; the host process never execs untrusted build scripts.
Container isolation is the docker default plus:
- No
--privileged. - No host bind mounts beyond the project source and the SSH key.
- No host network beyond the gateway's CRDT sync port.
--cap-drop=ALLplus the minimum caps needed (probably none).
This isn't a hardened sandbox in the gvisor / Firecracker sense — a docker-escape exploit on a compromised container still escalates to host. For most consumer threat models (malicious crate from crates.io / npm), docker's default isolation is sufficient. Tighter sandboxing (gvisor) is a separate future spike if needed.
Editor Connection — Editor-Agnostic SSH
| Editor | Connection mechanism |
|---|---|
| VSCode | Remote-SSH extension |
| JetBrains (IntelliJ/Rover) | JetBrains Gateway (SSH) |
| Zed | Built-in SSH remoting (mac/linux only today) |
| Vim/Neovim | SSH terminal session, or local nvim + LSP-over-SSH |
| Emacs | TRAMP + remote LSP via lsp-mode |
All converge on: ssh huskies@127.0.0.1 -p <project-port> -i ~/.huskies/<name>/id_ed25519.
That string is emitted in the bootstrap chat reply.
Git Integration
- Initial setup is
git initorgit cloneinside the container. - For push: user's existing GitHub / Gitea SSH key is bind-mounted
read-only into the container at
~/.ssh/id_*, OR the user supplies a push token viahuskies secrets set GIT_TOKEN=...(stored as a Fly secret equivalent — for now, a chmod 600 file in the container). - The container's
gitconfig getsuser.name/user.emailfrom the gateway-level user identity.
Decisions
| Decision | Choice | Alternative |
|---|---|---|
| Container per project | One container per project | One container many projects: simpler but breaks isolation, breaks per-project deps |
| Editor model | SSH-remote (any editor) | VSCode Dev Containers only: simpler config but locks out everyone else |
| Source location | Bind mount from host | Inside container only: breaks "I can also edit on my laptop" requirement |
| Stack detection | Auto from project files, override with --stack |
Always declared: more friction at bootstrap |
| Push secrets | Bind-mounted host SSH key OR per-project token | Gateway holds tokens: bigger blast radius |
Open Questions
- Per-project resource limits. Should each container have a hard CPU / RAM cap so a runaway agent doesn't starve the host?
- Lifecycle / cleanup. If the user deletes a project from chat, what gets removed? Container yes; host source no (data loss); git remotes yes? Need a confirm step.
- Multi-tenant. Out of scope for this design (that's huskies.dev territory). This doc assumes single-user local-only.
- Windows specifics. Bind mounts work but line-ending / permission edge cases. Probably document "use WSL2 for best experience" rather than fight Windows native paths.
- Gateway-on-host vs gateway-in-container. The gateway today runs in its own container. New per-project containers connect via docker network. Need to confirm the network plumbing works for arbitrary per-project containers, not just the manually-configured ones.
Phasing
The work breaks naturally into:
- Phase 0 (now): this design doc.
- Phase 1: chat command exists and provisions a bare project container (no stack overlay, no SSH, no git clone — just "start a container, register with gateway"). Validates the orchestration shell.
- Phase 2: stack-aware container template — base image + overlays; detection from project files.
- Phase 3: SSH-remote editor access — sshd in the container, per-project keypair, chat-reply emits the connection string.
- Phase 4: git integration —
--git <url>clones, host SSH key mount, push verification. - Phase 5: per-project resource limits + cleanup chat commands.
- Phase 6:
--adopt <dir>wraps a container around an existing checkout. No clone or init — bind-mount only. - Phase 7 (story 1137): First-run init flow — config summary and chat-driven overrides (see below).
Each phase ships independently and is usable on its own. Phase 1 alone gives chat-only users a working project; later phases add the editor and git polish.
First-Run Init Flow (Story 1137)
After a successful new project ... --adopt (or any new-project
bootstrap), the bot appends a Default configuration block to the
adoption success reply. This block lists every scaffolded agent with
its model, budget cap, and turn limit, and provides ready-to-send
override commands.
Example reply tail
**Default configuration** (3 agents):
- coder-1 (coder): model=`sonnet`, budget=$5.00, max_turns=50
- qa (qa): model=`sonnet`, budget=$4.00, max_turns=40
- mergemaster (mergemaster): model=`sonnet`, budget=$5.00, max_turns=30
Override via chat: `huskies config myapp coder.model=opus`
Project settings: `huskies config myapp default_qa=human`
Accept all defaults silently: add `--skip-config` to the bootstrap command.
Config override command
huskies config <project> <key>=<value>
The gateway resolves the project's host_path from projects.toml,
then writes the setting to .huskies/agents.toml or
.huskies/project.toml on the host.
Agent fields (<stage_or_name>.<field>=<value>):
| Key | Target | Supported values |
|---|---|---|
coder.model |
agents.toml, coder stage | sonnet, opus, any model string |
qa.model |
agents.toml, qa stage | same |
mergemaster.model |
agents.toml, mergemaster stage | same |
coder.max_turns |
agents.toml, coder stage | integer |
coder.max_budget |
agents.toml, coder stage | decimal (USD) |
Project keys (bare <key>=<value>):
| Key | Notes |
|---|---|
default_qa |
"server", "agent", or "human" |
max_retries |
integer |
max_coders |
integer |
base_branch |
branch name string |
timezone |
IANA timezone (e.g. "Europe/London") |
default_coder_model |
model string |
Skip path
Pass --skip-config to suppress the config block entirely:
new project myapp --adopt /path/to/checkout --skip-config
The success reply is identical to pre-1137 output — only the SSH command and registration summary, no agent listing.