Files
huskies/.huskies/specs/tech/CHAT_DRIVEN_PROJECT_BOOTSTRAP.md
T

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:

  1. A fresh docker container running the project's huskies node.
  2. The project's source code bind-mounted from the host so the user can edit it in any editor.
  3. SSH into the container so editors can run LSPs, builds, and tests inside the container — never on the host.
  4. Optional git remote configured for push to GitHub or Gitea.
  5. 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:

  1. Validate: name unique among existing projects; host path doesn't already exist; stack (if declared) is one of the supported overlays.
  2. Allocate a fresh per-project port range (gateway picks).
  3. Create host directory at --path (default ~/huskies/<name>/).
  4. If --git provided, git clone into that directory; else git init.
  5. Detect stack from cloned content if not declared:
    • Cargo.tomlrust
    • package.jsonnode
    • go.modgo
    • pyproject.toml / requirements.txt / setup.pypython
    • Gemfileruby
    • pom.xml / build.gradlejvm
    • Multiple → pick the dominant, warn.
    • None → minimal base image, user can install tooling later.
  6. Compose the container from huskies-project-base + the stack overlay (Dockerfile fragments under docker/stacks/<stack>/).
  7. Launch the container with bind mount + port forwards + an auto-generated SSH key.
  8. Seed .huskies/project.toml with sensible defaults.
  9. Register the project with the gateway (gateway_projects LWW-map).
  10. 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 huskies user with the SSH pubkey installed.
  • huskies-project-<stack>: per-stack additions, pre-built by script/build-project-images. E.g. rust gets rustup + rust-analyzer + cargo-nextest; node gets node@22 + typescript-language-server; etc. Stack fragments live in docker/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. The project-rebuild command picks up the fragment automatically when rebuilding.

    Example .huskies/Dockerfile.fragment that adds jq:

    RUN apt-get update && apt-get install -y jq
    
  • Project layer: the bind-mounted /workspace is 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=ALL plus 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 init or git clone inside 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 via huskies secrets set GIT_TOKEN=... (stored as a Fly secret equivalent — for now, a chmod 600 file in the container).
  • The container's git config gets user.name / user.email from 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

  1. Per-project resource limits. Should each container have a hard CPU / RAM cap so a runaway agent doesn't starve the host?
  2. 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.
  3. Multi-tenant. Out of scope for this design (that's huskies.dev territory). This doc assumes single-user local-only.
  4. Windows specifics. Bind mounts work but line-ending / permission edge cases. Probably document "use WSL2 for best experience" rather than fight Windows native paths.
  5. 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.