diff --git a/.huskies/specs/tech/CHAT_DRIVEN_PROJECT_BOOTSTRAP.md b/.huskies/specs/tech/CHAT_DRIVEN_PROJECT_BOOTSTRAP.md new file mode 100644 index 00000000..1e7e0e02 --- /dev/null +++ b/.huskies/specs/tech/CHAT_DRIVEN_PROJECT_BOOTSTRAP.md @@ -0,0 +1,221 @@ +# 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/`), 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 [--stack ] [--git ] [--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//`). +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//`). +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 ` 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-stack-`**: per-stack additions. E.g. rust gets + `rustup` + `rust-analyzer` + `cargo-nextest`; node gets `node@22` + + `typescript-language-server`; etc. +- **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 -i ~/.huskies//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 ` clones, host SSH key + mount, push verification. +- **Phase 5:** per-project resource limits + cleanup chat commands. + +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.