# 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.toml` → `rust`
   - `package.json` → `node`
   - `go.mod` → `go`
   - `pyproject.toml` / `requirements.txt` / `setup.py` → `python`
   - `Gemfile` → `ruby`
   - `pom.xml` / `build.gradle` → `jvm`
   - 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`:

  ```dockerfile
  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 `exec`s 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.
