diff --git a/.gitignore b/.gitignore index 9558968..28570be 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ web/node_modules/ # Test coverage.out coverage.html +# hunkwise +.vscode/hunkwise/ diff --git a/CLAUDE.md b/CLAUDE.md index 7a88e2d..f910038 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,9 +63,15 @@ desktopus.yaml → resolve modules → check compatibility - **Version injection**: `main.go` has `var version, commit, buildTime` set via ldflags in `magefile.go`. - **Image tagging**: `desktopus build` reads `image` from `desktopus.yaml` only (`--tag` overrides). `desktopus run` reads `image` from `desktopus.runtime.yaml` only (CLI arg overrides). Each command is independent — fails if no image is provided. -## Plan Documents +## Specs & Docs -All planning and design docs are in `docs/plan/`. See `docs/plan/README.md` for the index. The project follows a 6-phase implementation plan. +This project uses [OpenSpec](https://github.com/Fission-AI/OpenSpec) for spec-driven development: + +- **What the system does** → `openspec/specs/` (source of truth, built incrementally). +- **Planned / in-flight work** → `openspec/changes/`. Start a change with `/opsx:propose`, implement with `/opsx:apply`, finalize with `/opsx:archive`. +- **Durable design rationale + base-image facts** → `docs/reference.md`. + +The original 6-phase plan under `docs/plan/` was retired on 2026-05-31 (priorities shifted); its history is in git. ## Git Workflow diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 87c6277..0000000 --- a/TODO.md +++ /dev/null @@ -1,57 +0,0 @@ -# Desktopus — TODO - -## Current State - -**Phase 1: Foundation** is complete. The core CLI works end-to-end: `init` → `build` → `run` → `stop` → `list` with the Chrome module. - -### What's done (Phase 1) - -- [x] Project scaffolding (go.mod, cmd/desktopus, magefile.go, .gitignore) -- [x] Config types: DesktopConfig, AppConfig, loader, validation -- [x] CLI commands: root, init, build, run, stop, list, version -- [x] Module system: types, registry (embed.FS), loader, Chrome module -- [x] Build pipeline: Dockerfile generation, tar context, playbook/ansible.cfg generation -- [x] Runtime manager: Docker SDK container lifecycle (create, start, stop, list) -- [x] Unit tests for config, build, module, and runtime packages - -## Up Next: Phase 2 — Module Ecosystem + Post-Run - -- [ ] Built-in modules: vscode, firefox, claude-code -- [ ] Custom module filesystem path resolution -- [ ] Module validation (`internal/module/validate.go`) -- [ ] CLI subcommands: `module list`, `module info`, `module validate` -- [ ] CLI commands: `validate`, `logs`, `rm` -- [ ] Post-run scripts: s6 `custom-cont-init.d` generation with `runas` support -- [ ] Runtime files: envsubst templating at container startup -- [ ] Env var validation at `desktopus run` - -## Phase 3: State + Runtime - -- [ ] SQLite store (modernc.org/sqlite, pure Go) -- [ ] GPU passthrough (DRI device mounting, DRINODE env) -- [ ] Resource limits (memory, CPU, SHM) -- [ ] Build history tracking -- [ ] Container state reconciliation - -## Phase 4: REST API - -- [ ] Chi HTTP server with middleware -- [ ] All endpoints under `/api/v1/` -- [ ] SSE streaming for build/container logs -- [ ] `desktopus serve` command - -## Phase 5: Web UI - -- [ ] React 19 + TypeScript + Vite + Tailwind + shadcn/ui -- [ ] Full management dashboard -- [ ] Embed in Go binary via `//go:embed` - -## Phase 6: Polish & Release - -- [ ] Shell completion (bash, zsh, fish) -- [ ] Colored CLI output (lipgloss/tablewriter) -- [ ] goreleaser multi-platform builds -- [ ] GitHub Actions CI/CD -- [ ] Integration tests (testcontainers-go) -- [ ] README quick start guide -- [ ] v0.1.0 release diff --git a/docs/plan/README.md b/docs/plan/README.md deleted file mode 100644 index 1a08e0d..0000000 --- a/docs/plan/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# Desktopus — Project Plan - -Linux desktop-as-code platform. Define desktops in YAML, build as Docker images (on top of [linuxserver/docker-webtop](https://github.com/linuxserver/docker-webtop)), and manage via CLI + web UI. - -## Documents - -| Document | Description | -|---|---| -| [decisions.md](decisions.md) | Technology choices and rationale | -| [architecture.md](architecture.md) | Project structure, config specs, CLI commands, build pipeline, API | -| [phases.md](phases.md) | 6-phase implementation plan with deliverables and verification | - -## Tech Stack - -- **Backend**: Go, Cobra (CLI), Chi (API), Koanf (config), Docker SDK -- **Frontend**: React 19 + TypeScript + Vite, embedded in Go binary -- **Modules**: Ansible playbooks (built-in embedded + custom from filesystem) -- **Base image**: `lscr.io/linuxserver/webtop:ubuntu-xfce` -- **Storage**: SQLite for state persistence -- **Build tool**: Taskfile.yml -- **Post-run**: s6-overlay `custom-cont-init.d`, envsubst for runtime files - -## Phase Summary - -| Phase | Goal | Key Output | -|---|---|---| -| **1: Foundation** | Minimal working slice | `init` + `build` + `run` + `stop` + `list` with chrome module | -| **2: Modules + Post-Run** | Full module ecosystem | vscode/firefox/claude-code, custom modules, post-run scripts | -| **3: State + Runtime** | Persistence + advanced features | SQLite store, GPU passthrough, resource limits | -| **4: REST API** | HTTP API for web UI | Chi server, all endpoints, SSE streaming | -| **5: Web UI** | React dashboard | Full management UI embedded in binary | -| **6: Polish** | Release v0.1.0 | CI/CD, goreleaser, tests, docs | diff --git a/docs/plan/architecture.md b/docs/plan/architecture.md deleted file mode 100644 index 32375b9..0000000 --- a/docs/plan/architecture.md +++ /dev/null @@ -1,420 +0,0 @@ -# Desktopus — Architecture - -## Go Module - -`github.com/desktopus-org/desktopus` - -## Project Structure - -``` -desktopus/ -├── cmd/desktopus/main.go # Entrypoint -├── internal/ -│ ├── cli/ # Cobra commands -│ │ ├── root.go # Root cmd, global flags, koanf loading -│ │ ├── init.go # desktopus init -│ │ ├── build.go # desktopus build -│ │ ├── run.go # desktopus run -│ │ ├── stop.go # desktopus stop -│ │ ├── rm.go # desktopus rm -│ │ ├── list.go # desktopus list -│ │ ├── logs.go # desktopus logs -│ │ ├── serve.go # desktopus serve (web UI) -│ │ ├── module.go # desktopus module list|info|validate -│ │ ├── validate.go # desktopus validate -│ │ └── version.go # desktopus version -│ ├── config/ # Config parsing & validation -│ │ ├── app.go # AppConfig (~/.desktopus/config.yaml) -│ │ ├── desktop.go # DesktopConfig (desktopus.yaml) -│ │ ├── loader.go # Koanf loading logic -│ │ └── validate.go # Validation -│ ├── build/ # Image build pipeline -│ │ ├── pipeline.go # Orchestrates full build -│ │ ├── dockerfile.go # Generates Dockerfile from template -│ │ ├── context.go # Assembles Docker build context (tar) -│ │ ├── ansible.go # Generates playbook.yml + ansible.cfg -│ │ └── templates/ # Embedded Go templates -│ │ ├── Dockerfile.tmpl -│ │ ├── ansible.cfg.tmpl -│ │ └── playbook.yml.tmpl -│ ├── runtime/ # Container lifecycle (Docker SDK) -│ │ ├── manager.go # Create/Start/Stop/Remove/List/Logs -│ │ └── options.go # RunOptions, ContainerInfo, etc. -│ ├── module/ # Module system -│ │ ├── types.go # Module, ModuleMeta structs -│ │ ├── registry.go # Discovery + resolution (builtin + custom) -│ │ ├── loader.go # Load from FS or embed.FS -│ │ └── validate.go # Module validation -│ ├── api/ # REST API (Chi) -│ │ ├── server.go # HTTP server setup -│ │ ├── router.go # Route registration -│ │ ├── handlers/ # desktops, containers, modules, builds, system -│ │ └── middleware/ # logging, errors -│ ├── store/ # State persistence (SQLite) -│ │ ├── store.go # Interface -│ │ ├── sqlite.go # Implementation -│ │ └── models.go # Desktop, Container, Build records -│ └── postrun/ # Post-run / s6 integration -│ ├── s6.go # Generate s6 init scripts -│ └── envsubst.go # Runtime file templating -├── modules/ # Built-in Ansible modules (embedded in binary) -│ ├── chrome/ -│ │ ├── module.yaml -│ │ └── tasks/main.yml -│ ├── vscode/ -│ │ ├── module.yaml -│ │ └── tasks/main.yml -│ ├── claude-code/ -│ │ ├── module.yaml -│ │ └── tasks/main.yml -│ └── firefox/ -│ ├── module.yaml -│ └── tasks/main.yml -├── web/ # React frontend (embedded in binary) -│ ├── package.json -│ ├── vite.config.ts -│ ├── tsconfig.json -│ ├── index.html -│ └── src/ -│ ├── main.tsx -│ ├── App.tsx -│ ├── api/ -│ │ ├── client.ts # Typed API client -│ │ └── types.ts # TypeScript types -│ ├── pages/ -│ │ ├── DashboardPage.tsx -│ │ ├── DesktopsListPage.tsx -│ │ ├── DesktopDetailPage.tsx -│ │ ├── ContainersPage.tsx -│ │ ├── BuildPage.tsx -│ │ ├── ModulesPage.tsx -│ │ └── SettingsPage.tsx -│ └── components/ -│ ├── Layout.tsx -│ ├── DesktopCard.tsx -│ ├── ContainerStatus.tsx -│ ├── LogViewer.tsx -│ └── BuildProgress.tsx -├── docs/plan/ # This documentation -├── Taskfile.yml -├── go.mod -├── go.sum -├── .gitignore -└── LICENSE -``` - ---- - -## Configuration Specs - -### Desktop Definition (`desktopus.yaml`) - -User-authored file that defines what gets built and how the container runs. -Minimal, docker-compose-inspired format — no apiVersion/kind/metadata nesting. - -```yaml -# desktopus.yaml - -name: my-dev-desktop # DNS-safe: [a-z0-9-] -description: "Dev desktop with VS Code and Chrome" - -base: - os: ubuntu # ubuntu | debian | fedora | arch | alpine - desktop: xfce # xfce | kde | i3 | mate - tag: "" # Optional: pin specific webtop image version - -modules: - - chrome # Simple string = built-in module, no vars - - name: vscode # Object form for modules with vars - vars: - extensions: - - ms-python.python - - golang.go - - name: ./my-modules/custom # Relative path = custom module - -env: # Map of env var declarations - GIT_USER_NAME: - required: true - GIT_USER_EMAIL: - required: true - description: "Git commit email" - FONT_SIZE: - default: "14" - -postrun: # Scripts that run at container startup (s6) - - name: configure-git - runas: abc # "root" or "abc" (default: abc) - script: | - git config --global user.name "${GIT_USER_NAME}" - git config --global user.email "${GIT_USER_EMAIL}" - -files: # Files provisioned at container startup (envsubst) - - path: /config/.config/settings.json - content: '{"fontSize": ${FONT_SIZE}}' - mode: "0644" - -runtime: # Container runtime configuration - hostname: dev-desktop - shm_size: 2g - ports: # Docker-compose style "host:container" - - "3000:3000" - - "3001:3001" - volumes: # Docker-compose style "host:container[:ro]" - - ~/projects:/config/projects - - ~/.ssh:/config/.ssh:ro - gpu: false # Simple boolean (or object for advanced config) - memory: 8g - cpus: 4 - restart: unless-stopped # no | always | unless-stopped - network: "" # Optional Docker network name - env: # Static env vars passed at runtime - TZ: Europe/Madrid - PUID: "1000" - PGID: "1000" -``` - -**Design notes:** -- `modules` supports both string shorthand (`- chrome`) and object form (`- name: vscode`) -- `env` is a map (not array) — key is the var name, value is options. Simpler to read and write. -- `postrun[].script` — no need for the `#!/usr/bin/with-contenv bash` shebang, desktopus adds it. -- `files` are always runtime (envsubst at startup). No `phase` field — simplifies the mental model. -- `runtime.ports` and `runtime.volumes` use docker-compose string notation. -- `runtime.gpu` is a simple boolean. Advanced GPU config (specific devices, driver node) can be added later if needed. -- `runtime.memory` and `runtime.cpus` are top-level under runtime (no `resources` nesting). -- `runtime.restart` and `runtime.network` added for common container needs. - -### App Config (`~/.desktopus/config.yaml`) - -Application-level configuration. Also minimal — no apiVersion/kind. - -```yaml -# ~/.desktopus/config.yaml - -docker: - host: "unix:///var/run/docker.sock" - -server: - listen: "127.0.0.1" - port: 7575 - -build: - cache_dir: "~/.desktopus/cache" - parallel: 2 - ansible_verbosity: 0 - -log: - level: "info" # debug | info | warn | error - format: "text" # text | json - -store: - path: "~/.desktopus/desktopus.db" -``` - ---- - -## CLI Commands - -``` -desktopus init [name] # Scaffold desktopus.yaml - --os string Base OS (default "ubuntu") - --desktop string Desktop environment (default "xfce") - --dir string Output directory (default ".") - -desktopus build [path] # Build image from desktopus.yaml - --tag string Override image tag - --no-cache Build without Docker cache - --progress string Progress output: auto, plain, tty - -desktopus run [name] # Run desktop container - -f, --file string Path to desktopus.yaml - -d, --detach Run in background - --gpu Enable GPU passthrough - --port stringArray Additional ports (host:container) - --volume stringArray Additional volumes (host:container) - --env stringArray Set env vars (KEY=VALUE) - --rm Remove on stop - -desktopus stop [name...] # Stop container(s) - -t, --timeout int Seconds before force kill (default 10) - --all Stop all desktopus containers - -desktopus rm [name...] # Remove container(s) - -f, --force Force remove running containers - --all Remove all desktopus containers - -desktopus list # List desktops + containers - --images List built images only - --containers List containers only - --all Include stopped containers - -o, --output Output format: table, json, yaml - -desktopus logs [name] # Stream container logs - -f, --follow Follow output - --tail string Lines from end (default "100") - --since string Show logs since timestamp - --timestamps Show timestamps - -desktopus serve # Start API + web UI - --listen string Listen address - --port int Listen port - --no-open Don't auto-open browser - -desktopus module list # List available modules -desktopus module info [name] # Module details -desktopus module validate [path] # Validate custom module - -desktopus validate [path] # Validate desktopus.yaml -desktopus version # Version info -desktopus completion [bash|zsh|fish] # Shell completions - -# Global flags - --config string Config file (default "~/.desktopus/config.yaml") - --docker-host string Override Docker host - --log-level string Override log level - --no-color Disable colored output -``` - ---- - -## Image Build Pipeline - -``` -desktopus.yaml - │ - ▼ -[1. Parse & Validate Config] - │ - ▼ -[2. Resolve Modules] ◄── Registry lookup (builtin embed.FS or filesystem path) - │ - ▼ -[3. Generate Build Context] - ├── Dockerfile (from Dockerfile.tmpl + config) - ├── modules/ (resolved module directories) - ├── playbook.yml (generated: includes each module's tasks) - ├── ansible.cfg (generated) - ├── files/ (runtime file templates from files) - └── postrun/ (s6 init scripts from postrun) - │ - ▼ -[4. Docker SDK ImageBuild()] ◄── Tar archive of build context - │ - ▼ -[5. Stream build output to terminal/SSE] - │ - ▼ -[6. Tag: desktopus/:latest] - │ - ▼ -[7. Record build in SQLite store] -``` - -### Generated Dockerfile Layers - -1. `FROM lscr.io/linuxserver/webtop:ubuntu-xfce` -2. Install Ansible + system packages -3. Copy modules + playbook → `ansible-playbook --connection=local` -4. Remove Ansible (reduce image size) -5. Copy post-run scripts to `/custom-cont-init.d/` -6. Copy runtime file templates + envsubst provisioner script -7. Add desktopus labels - ---- - -## Module System - -### Module Directory Structure - -``` -module-name/ -├── module.yaml # Required: metadata, vars, compatibility -├── tasks/ -│ └── main.yml # Required: Ansible tasks -├── files/ # Optional: static files to copy -├── templates/ # Optional: Jinja2 templates -├── handlers/ # Optional: Ansible handlers -└── defaults/ # Optional: default variable values -``` - -### module.yaml Spec - -```yaml -# module.yaml — also minimal, no apiVersion/kind - -name: chrome -description: "Google Chrome browser" -version: "1.0.0" -author: desktopus -tags: [browser, web] - -compatibility: - os: [ubuntu, debian] - desktop: [] # Empty = all desktops - arch: [amd64] - -vars: - chrome_version: - default: "latest" - description: "Chrome version to install" - -dependencies: [] -system_packages: [wget, gnupg] -``` - -### Resolution - -- Name starts with `./`, `/`, or `../` → filesystem path (relative to desktopus.yaml) -- Otherwise → lookup in built-in registry (embedded via `embed.FS`) - ---- - -## REST API - -All endpoints under `/api/v1/`. Consistent response envelope: `{ data, error, meta }`. - -| Method | Path | Description | -|---|---|---| -| GET | /health | Health check | -| GET | /version | Version info | -| GET | /system/info | Docker info, system stats | -| GET | /desktops | List desktops | -| POST | /desktops | Register desktop | -| GET | /desktops/{name} | Desktop details | -| PUT | /desktops/{name} | Update desktop | -| DELETE | /desktops/{name} | Remove desktop | -| POST | /desktops/{name}/build | Trigger build | -| GET | /desktops/{name}/builds | List builds | -| POST | /desktops/{name}/run | Run container | -| GET | /containers | List containers | -| GET | /containers/{id} | Container details | -| POST | /containers/{id}/start | Start | -| POST | /containers/{id}/stop | Stop | -| DELETE | /containers/{id} | Remove | -| GET | /containers/{id}/logs | Stream logs (SSE) | -| GET | /builds/{id} | Build status | -| GET | /builds/{id}/logs | Stream build logs (SSE) | -| GET | /modules | List modules | -| GET | /modules/{name} | Module details | -| GET | /images | List images | -| DELETE | /images/{id} | Remove image | - ---- - -## Post-Run & s6 Integration - -### Post-run scripts -- `postrun` entries → files in `/custom-cont-init.d/` baked into image at build time -- Numbered 50-89 for ordering: `50-desktopus-.sh` -- `runas: abc` uses `s6-setuidgid abc` to drop privileges -- Desktopus auto-adds `#!/usr/bin/with-contenv bash` shebang — users just write the script body - -### Runtime files -- `files` entries → templates stored at `/tmp/desktopus-runtime-files/` in image -- All files are processed at container startup by `90-desktopus-files.sh` using `envsubst` -- Supports `${VAR}` substitution from container env vars - -### Environment variable flow -1. `env` defaults + `runtime.env` → merged at `desktopus run` -2. Passed as `container.Config.Env` to Docker SDK -3. Available inside container via s6 `with-contenv` -4. Required vars validated before container creation diff --git a/docs/plan/decisions.md b/docs/plan/decisions.md deleted file mode 100644 index 8976f4b..0000000 --- a/docs/plan/decisions.md +++ /dev/null @@ -1,54 +0,0 @@ -# Desktopus — Decision Log - -## Technology Choices - -| Decision | Choice | Rationale | -|---|---|---| -| Language | Go | Fast, single binary, strong Docker SDK, good for CLI tools | -| CLI framework | Cobra | Industry standard (used by Docker, K8s), nested commands, Viper integration | -| Config format | YAML | Human-friendly, DevOps-standard (docker-compose, k8s, Ansible) | -| Config library | Koanf | Lighter than Viper, respects YAML spec, modular design | -| API framework | Chi | Minimal, stdlib-compatible, zero external deps, idiomatic Go | -| Frontend | React 19 + TypeScript + Vite | Large ecosystem, type safety, fast builds | -| Web architecture | Embedded in Go binary (embed.FS) | Single binary deployment, no CORS issues | -| Container mgmt | Docker SDK for Go (`github.com/docker/docker/client`) | Official, full control, no docker-compose dependency | -| Build tool | Taskfile.yml | YAML syntax, cross-platform, coordinates frontend + backend builds | -| Module system | Ansible everywhere | Consistent for built-in and custom modules. Runs inside Docker build (users don't need Ansible locally) | -| Base image | `lscr.io/linuxserver/webtop:ubuntu-xfce` | Selkies-based web streaming, s6-overlay, well-maintained | -| Post-run params | Environment variables | Docker-native, simple, Ansible-readable via env lookup | -| Post-run mechanism | s6 `custom-cont-init.d` | Official linuxserver extension point | -| Runtime file templating | envsubst | Lightweight, POSIX, already in base image | -| State persistence | SQLite | Zero-dependency, single-file, local-first | -| Licensing | Open source | Fully open source, all phases | -| CLI name | `desktopus` | Full name as command | - -## Architectural Decisions - -### Minimal config format — no apiVersion/kind/metadata/spec nesting -Kubernetes-style `apiVersion`/`kind` is overkill when there are only two config file types and no multi-resource manifests. Flat structure with docker-compose-inspired shorthand (string ports/volumes, map-based env vars, string module references) is faster to write and easier to read. Schema versioning can be added later with a simple `version: 2` field if breaking changes are needed. - -### Ansible runs inside Docker build, not on the host -Users don't need Ansible installed locally. Provisioning runs in the exact target environment, and builds are reproducible regardless of host OS. - -### s6 `custom-cont-init.d` for post-run scripts -Official linuxserver extension point. Scripts run after container init but before desktop services start — correct time for user config provisioning. - -### envsubst for runtime file templating -Lightweight POSIX tool already present in base image. Only supports `${VAR}` substitution, but that's sufficient for configuration files. Avoids shipping custom binaries inside the container. - -### SQLite for state persistence -Single-file database, zero external dependencies. Suitable for local-first usage. Stores desktop definitions, build history, and container records. - -### Single binary with embedded web UI -`desktopus serve` is one command with zero dependencies beyond Docker. Eliminates deployment complexity. - -## Docker-Webtop Key Facts - -- Based on baseimage-selkies (web desktop streaming via WebSocket + Canvas + VideoDecoder) -- Uses s6-overlay v3 for process supervision -- Container user: `abc`, home dir: `/config` -- Ports: 3000 (HTTP), 3001 (HTTPS), 8082 (Selkies WebSocket data) -- Env vars: PUID, PGID, TZ, DRINODE, PIXELFLUX_WAYLAND -- GPU: mount `/dev/dri`, set DRINODE env var -- Extend via `FROM lscr.io/linuxserver/webtop:...` -- NOT traditional VNC — uses modern Selkies protocol diff --git a/docs/plan/phases.md b/docs/plan/phases.md deleted file mode 100644 index bfcda50..0000000 --- a/docs/plan/phases.md +++ /dev/null @@ -1,239 +0,0 @@ -# Desktopus — Implementation Phases - -## Phase 1: Foundation (Minimal Working Slice) - -**Status**: Complete -**Goal**: `desktopus init` + `desktopus build` + `desktopus run` + `desktopus stop` + `desktopus list` working end-to-end. - -### Deliverables - -1. **Project scaffolding**: `go.mod`, `cmd/desktopus/main.go`, `Taskfile.yml`, `.gitignore` -2. **Config types** (`internal/config/`): - - `desktop.go` — DesktopConfig, DesktopSpec, BaseSpec, ModuleRef, RuntimeSpec structs - - `app.go` — AppConfig struct - - `loader.go` — Koanf-based loading from YAML + env var overrides - - `validate.go` — Basic validation (required fields, valid OS/desktop combos) -3. **CLI commands** (`internal/cli/`): - - `root.go` — Root cobra command, global flags (--config, --log-level, --no-color) - - `init.go` — Generate starter desktopus.yaml - - `build.go` — Build image from config - - `run.go` — Create + start container - - `stop.go` — Stop container - - `list.go` — List desktops/containers - - `version.go` — Print version -4. **Module system** (`internal/module/`): - - `types.go` — Module, ModuleMetadata, ModuleSpec structs - - `registry.go` — Built-in module discovery from embed.FS - - `loader.go` — Load and parse module.yaml - - One built-in module: `modules/chrome/` -5. **Build pipeline** (`internal/build/`): - - `pipeline.go` — Orchestrate: parse config → resolve modules → generate Dockerfile → build - - `dockerfile.go` — Go text/template Dockerfile generation - - `context.go` — Assemble tar build context in memory - - `ansible.go` — Generate playbook.yml and ansible.cfg - - `templates/Dockerfile.tmpl`, `templates/playbook.yml.tmpl`, `templates/ansible.cfg.tmpl` -6. **Runtime manager** (`internal/runtime/`): - - `manager.go` — Docker SDK: Create, Start, Stop, List containers (filtered by desktopus label) - - `options.go` — RunOptions, ContainerInfo types - -### Verification - -```bash -mkdir test-desktop && cd test-desktop -desktopus init my-desktop -# desktopus.yaml created with chrome module -desktopus build . -# Image built: desktopus/my-desktop:latest -desktopus run my-desktop -# Container running, desktop at http://localhost:3000 -desktopus list -# Shows running container -desktopus stop my-desktop -``` - ---- - -## Phase 2: Module Ecosystem + Post-Run - -**Status**: Not started -**Goal**: Full module system, custom modules, post-run scripts, runtime files. - -### Deliverables - -1. **Additional built-in modules**: - - `modules/vscode/` — VS Code with extension support - - `modules/firefox/` — Firefox browser - - `modules/claude-code/` — Claude Code CLI -2. **Custom module support**: filesystem path resolution in `internal/module/loader.go` -3. **Module validation**: `internal/module/validate.go` -4. **CLI subcommands**: - - `desktopus module list` — List available modules (built-in + discovered) - - `desktopus module info [name]` — Show module details, vars, compatibility - - `desktopus module validate [path]` — Validate custom module structure - - `desktopus validate [path]` — Validate desktopus.yaml - - `desktopus logs [name]` — Stream container logs - - `desktopus rm [name...]` — Remove stopped containers -5. **Post-run scripts** (`internal/postrun/`): - - `s6.go` — Generate `/custom-cont-init.d/` scripts from `spec.postrun` - - Handle `runas` (root vs abc via `s6-setuidgid`) - - Numbered script ordering (50-89) -6. **Runtime files**: - - `envsubst.go` — Generate startup script that processes runtime-phase files - - Generate `90-desktopus-files.sh` init script -7. **Env var validation** at `desktopus run` — check required vars are provided - -### Verification - -```bash -desktopus module list -# chrome, vscode, firefox, claude-code -desktopus build . --no-cache -# Build with custom module + post-run scripts -desktopus run my-desktop --env GIT_USER_NAME=Carlos --env GIT_USER_EMAIL=carlos@example.com -# Post-run scripts execute, runtime files templated -desktopus logs my-desktop -f -# Streaming logs -``` - ---- - -## Phase 3: State Persistence + Advanced Runtime - -**Status**: Not started -**Goal**: SQLite store, GPU passthrough, resource limits, build history. - -### Deliverables - -1. **SQLite store** (`internal/store/`): - - `store.go` — Store interface: SaveDesktop, ListDesktops, SaveBuild, etc. - - `sqlite.go` — SQLite implementation using `modernc.org/sqlite` (pure Go, no CGO) - - `models.go` — DesktopRecord, BuildRecord, ContainerRecord - - Auto-initialize `~/.desktopus/desktopus.db` on first use -2. **GPU passthrough**: - - DRI device mounting in runtime manager - - DRINODE + DRI_NODE env var injection - - `--gpu` flag on `desktopus run` -3. **Resource limits**: - - Memory limit via Docker SDK `container.Resources.Memory` - - CPU limit via `container.Resources.NanoCPUs` - - SHM size via `HostConfig.ShmSize` -4. **Build history**: - - Record each build (timestamp, status, duration, image tag, logs) in store - - `desktopus list --images` shows build history -5. **Container tracking**: - - Record container state changes in store - - Reconcile store state with Docker on startup - -### Verification - -```bash -desktopus build . -desktopus build . # Second build -desktopus list --images -# Shows both builds with timestamps -desktopus run my-desktop --gpu --env TZ=Europe/Madrid -# GPU devices mounted, resource limits applied -desktopus list -# Shows container with GPU badge, memory/CPU info -``` - ---- - -## Phase 4: REST API - -**Status**: Not started -**Goal**: Full REST API serving all functionality over HTTP. - -### Deliverables - -1. **HTTP server** (`internal/api/server.go`): - - Chi router with middleware (RequestID, Logger, Recoverer, CORS, Compress) - - Graceful shutdown - - Configurable listen address + port from `~/.desktopus/config.yaml` -2. **Router** (`internal/api/router.go`): - - All routes under `/api/v1/` - - SPA fallback: serve `index.html` for non-API, non-static routes -3. **Handlers** (`internal/api/handlers/`): - - `desktops.go` — CRUD + build trigger + run trigger - - `containers.go` — List, get, start, stop, remove - - `builds.go` — Get build status, stream build logs (SSE) - - `modules.go` — List, get module details - - `system.go` — Health, version, Docker info -4. **SSE streaming**: - - Build logs: `GET /api/v1/builds/{id}/logs` (text/event-stream) - - Container logs: `GET /api/v1/containers/{id}/logs` (text/event-stream) -5. **Consistent response envelope**: `{ data, error, meta: { timestamp, request_id } }` -6. **CLI command**: `desktopus serve` — starts server, auto-opens browser - -### Verification - -```bash -desktopus serve -# Server started on http://127.0.0.1:7575 -curl http://localhost:7575/api/v1/health -# {"data":{"status":"ok"},...} -curl http://localhost:7575/api/v1/desktops -curl -X POST http://localhost:7575/api/v1/desktops/my-desktop/build -# SSE: curl -N http://localhost:7575/api/v1/builds/{id}/logs -``` - ---- - -## Phase 5: Web UI - -**Status**: Not started -**Goal**: React SPA with full desktop management. - -### Deliverables - -1. **Project setup** (`web/`): - - Vite + React 19 + TypeScript - - Tailwind CSS v4 + shadcn/ui - - TanStack Query + TanStack Router -2. **API client** (`web/src/api/`): - - Typed fetch wrapper matching all API endpoints - - SSE helper for log streaming -3. **Pages**: - - `DashboardPage` — Overview: running containers, recent builds, quick actions - - `DesktopsListPage` — All desktop definitions with status badges - - `DesktopDetailPage` — Config view, build history, running instances - - `ContainersPage` — All containers with status, actions (stop/start/remove) - - `BuildPage` — Build progress with streaming log viewer - - `ModulesPage` — Browse modules with search/filter - - `SettingsPage` — App config, Docker connection status -4. **Components**: - - `Layout` — Sidebar nav + top bar with server status - - `DesktopCard` — Name, OS/DE badge, modules, actions - - `ContainerStatus` — Real-time status badge with uptime - - `LogViewer` — Terminal-like SSE log viewer (xterm.js or ansi-to-html) - - `BuildProgress` — Step indicator + streaming output -5. **Embed in Go binary**: - - `//go:embed all:web/dist` in Go source - - Taskfile: `build:web` runs before `build:go` - - SPA routing: return index.html for 404s on non-API paths - -### Verification - -```bash -task build # Builds web + Go -desktopus serve # Opens browser -# Full workflow in browser: create desktop, build, run, view logs, stop -``` - ---- - -## Phase 6: Polish & Release - -**Status**: Not started -**Goal**: Production readiness for v0.1.0. - -### Deliverables - -1. Shell completion: `desktopus completion bash|zsh|fish` -2. Colored CLI output: tables (lipgloss/tablewriter), progress bars, status badges -3. `goreleaser` config for multi-platform builds (linux/amd64, linux/arm64, darwin/amd64, darwin/arm64) -4. GitHub Actions CI/CD: lint (golangci-lint), test, build, release -5. Integration tests using testcontainers-go -6. README with quick start guide -7. `~/.desktopus/` auto-initialization on first run -8. Error messages with actionable suggestions diff --git a/docs/reference.md b/docs/reference.md new file mode 100644 index 0000000..0317a81 --- /dev/null +++ b/docs/reference.md @@ -0,0 +1,66 @@ +# Desktopus — Reference + +Durable design rationale and external facts that aren't captured elsewhere. + +> **Scope of this file.** This is *not* a roadmap or an architecture spec. +> - **What the system does** → `openspec/specs/` (source of truth, built incrementally). +> - **What work is planned / in flight** → `openspec/changes/` (`/opsx:propose`). +> - **Code structure & build commands** → `CLAUDE.md`. +> +> This file keeps only the *why* behind durable decisions and reference facts about +> the base image that would otherwise be rediscovered the hard way. The original +> 6-phase plan (`docs/plan/`) was retired on 2026-05-31 because priorities and goals +> shifted; its history lives in git. + +## Design rationale (still in force) + +| Decision | Choice | Why | +|---|---|---| +| Language | Go | Single static binary, strong Docker SDK, good for CLI tools | +| CLI framework | Cobra | Nested commands, industry standard (Docker, k8s) | +| Config format | YAML | DevOps-standard (compose, k8s, Ansible), human-friendly | +| Config loading | `gopkg.in/yaml.v3` directly | Simple; Koanf was considered but not adopted | +| Container mgmt | Docker SDK behind a multi-provider runtime abstraction | Full control; not locked to a single backend | +| Build tool | Mage (`magefile.go`) | Go-native build automation | +| Module system | Ansible everywhere | Same mechanism for built-in and custom modules | +| Base image | `lscr.io/linuxserver/webtop` family | Selkies web streaming + s6-overlay, well-maintained | +| Post-run params | Environment variables | Docker-native, Ansible-readable | +| Post-run mechanism | s6 `custom-cont-init.d` | Official linuxserver extension point | +| Runtime file templating | envsubst | POSIX, already present in the base image | +| State / persistence | Docker volumes | Volume-based persistence (the earlier SQLite-store idea was dropped) | +| CLI name | `desktopus` | Full name as command | + +### Minimal config format — no apiVersion/kind/metadata/spec nesting +Kubernetes-style `apiVersion`/`kind` is overkill with only a couple of config file +types and no multi-resource manifests. A flat, docker-compose-inspired shape (string +ports/volumes, map-based env vars, string module references) is faster to write and +read. Schema versioning can be added later with a simple `version:` field if a +breaking change is ever needed. + +### Ansible runs inside the Docker build, not on the host +Users don't need Ansible installed locally. Provisioning runs in the exact target +environment, so builds are reproducible regardless of host OS. + +### s6 `custom-cont-init.d` for post-run scripts +Official linuxserver extension point. Scripts run after container init but before the +desktop services start — the correct moment for user-config provisioning. `runas: abc` +drops privileges via `s6-setuidgid abc`; desktopus auto-adds the +`#!/usr/bin/with-contenv bash` shebang. + +### envsubst for runtime file templating +Lightweight POSIX tool already in the base image. Only `${VAR}` substitution, but +that's enough for config files, and it avoids shipping extra binaries in the container. + +## Docker-Webtop key facts + +External reference about the base image — worth keeping because it's easy to forget +and hard to find: + +- Based on **baseimage-selkies** (web desktop streaming via WebSocket + Canvas + VideoDecoder) +- Uses **s6-overlay v3** for process supervision +- Container user: **`abc`**, home dir: **`/config`** +- Ports: **3000** (HTTP), **3001** (HTTPS), **8082** (Selkies WebSocket data) +- Env vars: `PUID`, `PGID`, `TZ`, `DRINODE`, `PIXELFLUX_WAYLAND` +- GPU: mount `/dev/dri`, set `DRINODE` env var +- Extend via `FROM lscr.io/linuxserver/webtop:...` +- **Not** traditional VNC — uses the modern Selkies protocol diff --git a/openspec/changes/archive/.gitkeep b/openspec/changes/archive/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/openspec/changes/archive/2026-05-31-baseline-current-specs/design.md b/openspec/changes/archive/2026-05-31-baseline-current-specs/design.md new file mode 100644 index 0000000..671c1a8 --- /dev/null +++ b/openspec/changes/archive/2026-05-31-baseline-current-specs/design.md @@ -0,0 +1,30 @@ +## Context + +Desktopus already ships a working CLI (build → run → connect, plus module and volume management). OpenSpec was just initialized on this branch and `openspec/specs/` is empty. This change reverse-documents the shipped behavior into capability specs so they can serve as the source of truth going forward. + +## Goals / Non-Goals + +**Goals:** +- Capture the currently-shipped contract as source-of-truth specs. +- Carve the system into coherent capabilities aligned to the `internal/` package layout, so specs live conceptually close to the code. +- Provide a paper trail (this change) for where the initial specs came from. + +**Non-Goals:** +- No behavior changes — documentation only. +- Not documenting unimplemented or reserved surface (the future server API, the state store DB) as behavior; these are noted as reserved configuration only. +- Not exhaustively enumerating every flag default — specs stay at the contract level. + +## Decisions + +- **Seven capabilities**, mapped to the internal package split: + `desktop-authoring`, `module-system`, `image-build`, `container-runtime`, `volume-management`, `desktop-viewer`, `app-configuration`. +- **`desktopus.runtime.yaml` folds into `container-runtime`** rather than being its own capability, because it is the input contract to `run` and has no independent behavior. +- **Global flags, `version`, and `~/.desktopus/config.yaml` fold into `app-configuration`** — they are cross-cutting CLI/runtime configuration rather than a feature. +- **Specs describe behavior at the contract level** (WHEN/THEN), citing concrete values only where the value *is* the contract: the OS×desktop compatibility matrix, container ports 3000/3001, the `org.desktopus.*` label keys, and the default user (`desktopus`) / home (`/home/` or `/config` for `abc`). +- **Present-only facts are stated as the current set, not the requirement.** Example: "ships built-in modules" is the durable requirement; "currently only `chrome`" is noted as today's set so the spec stays honest without freezing the catalog. + +## Risks / Trade-offs + +- **Spec drift**: reverse-documented specs can fall out of sync with code. Mitigated by tasks that verify each spec against its source package during apply. +- **Reality vs intent**: chose to document what exists (e.g. only `chrome` ships) while noting the extensible contract, rather than speccing aspirational features. +- **Altitude**: keeping specs behavioral risks omitting a detail that later turns out contractual; concrete values were pulled in only where they are externally observable guarantees. diff --git a/openspec/changes/archive/2026-05-31-baseline-current-specs/proposal.md b/openspec/changes/archive/2026-05-31-baseline-current-specs/proposal.md new file mode 100644 index 0000000..b74cdba --- /dev/null +++ b/openspec/changes/archive/2026-05-31-baseline-current-specs/proposal.md @@ -0,0 +1,26 @@ +## Why + +Desktopus is adopting OpenSpec for spec-driven development, but `openspec/specs/` is empty — there is no documented source of truth for what the system already does. Before proposing new behavior, we need a baseline that captures the current contract so future changes have something to diff against. + +## What Changes + +This change is documentation-only. It introduces specs describing the **existing, shipped** behavior of Desktopus — no source code changes. Each capability spec records what the system guarantees today: the CLI commands, config schemas and validation, the build pipeline, module resolution, container lifecycle, volume management, and the viewer client. + +## Capabilities + +### New Capabilities +- `desktop-authoring`: the `desktopus.yaml` schema, validation rules, and `init` scaffolding +- `module-system`: module resolution (built-in vs filesystem path), `module.yaml` format, compatibility, and built-in modules +- `image-build`: the `build` command and the Docker image build pipeline (Dockerfile + Ansible generation) +- `container-runtime`: the `run`/`stop`/`list` lifecycle, `desktopus.runtime.yaml` input, container labels, GPU passthrough, web ports, and persistence +- `volume-management`: `volume ls` / `volume rm` over desktopus-managed Docker volumes +- `desktop-viewer`: the `connect` command and the hardened Electron viewer client +- `app-configuration`: global `~/.desktopus/config.yaml`, global CLI flags, Docker host, and `version` + +### Modified Capabilities + + +## Impact + +- Affected: `openspec/specs/` (populated on archive). No application code changes. +- Establishes the contract that subsequent changes will modify via spec deltas. diff --git a/openspec/changes/archive/2026-05-31-baseline-current-specs/specs/app-configuration/spec.md b/openspec/changes/archive/2026-05-31-baseline-current-specs/specs/app-configuration/spec.md new file mode 100644 index 0000000..27d160a --- /dev/null +++ b/openspec/changes/archive/2026-05-31-baseline-current-specs/specs/app-configuration/spec.md @@ -0,0 +1,29 @@ +## ADDED Requirements + +### Requirement: Global app config +The system SHALL load global configuration from `~/.desktopus/config.yaml` (overridable via `--config`), covering Docker connection, build settings, and logging. Reserved `server` and `store` sections MAY be present for future use without affecting current behavior. + +#### Scenario: Default docker host +- **WHEN** no Docker host is configured +- **THEN** it defaults to `unix:///var/run/docker.sock` + +### Requirement: Docker connection +The Docker endpoint SHALL be configurable via `docker.host` and used for all build and runtime operations. + +#### Scenario: Custom docker host +- **WHEN** `docker.host` is set to a remote endpoint +- **THEN** build and run operate against that endpoint + +### Requirement: Global CLI flags +The CLI SHALL expose global flags: `--config` (config file path), `--log-level` (`debug` | `info` | `warn` | `error`), and `--no-color`. + +#### Scenario: Log level override +- **WHEN** `--log-level debug` is passed +- **THEN** debug logging is enabled regardless of the level set in the config file + +### Requirement: Version reporting +The `version` command SHALL print the build version, commit hash, and build timestamp injected at compile time. + +#### Scenario: Version output +- **WHEN** `version` is invoked +- **THEN** the version, commit hash, and build time are printed diff --git a/openspec/changes/archive/2026-05-31-baseline-current-specs/specs/container-runtime/spec.md b/openspec/changes/archive/2026-05-31-baseline-current-specs/specs/container-runtime/spec.md new file mode 100644 index 0000000..8225c3b --- /dev/null +++ b/openspec/changes/archive/2026-05-31-baseline-current-specs/specs/container-runtime/spec.md @@ -0,0 +1,76 @@ +## ADDED Requirements + +### Requirement: Runtime configuration file +Container runtime options SHALL be read from `desktopus.runtime.yaml` (located alongside the desktop definition, or via `--file`). The file is optional and defaults to empty. It MAY declare `name`, `image` override, `hostname`, `shm_size`, `ports`, `volumes`, `gpu`, `memory`, `cpus`, `restart`, `network`, `env`, `provider`, `persistence_home`, and `web` port bindings. + +#### Scenario: Optional runtime config +- **WHEN** no `desktopus.runtime.yaml` exists +- **THEN** `run` proceeds with default runtime settings + +### Requirement: Image resolution for run +The `run` command SHALL resolve the image tag in priority order: the CLI argument, else the `image` field of `desktopus.runtime.yaml`, else an error. (This is independent of `build`, which reads `image` only from `desktopus.yaml`.) + +#### Scenario: No image for run +- **WHEN** `run` is invoked with no image argument and `desktopus.runtime.yaml` has no `image` +- **THEN** the command fails asking for an image + +### Requirement: Runtime validation +The runtime config SHALL validate `restart` (`no` | `always` | `unless-stopped` | `on-failure`), `provider` (`docker` only), and `gpu` (`intel` | `amd` | `nvidia`). + +#### Scenario: Invalid restart policy +- **WHEN** `restart` is not one of the allowed values +- **THEN** validation fails + +### Requirement: Run command +The `run` command SHALL create and start a container from a built image, detaching by default, applying ports, volumes, env, GPU, name, and `--rm` from both the runtime config and CLI flags, and SHALL launch the viewer client unless `--no-client` is set. + +#### Scenario: Detached run +- **WHEN** `run` is invoked without overriding detach +- **THEN** the container runs in the background and its ID is returned + +#### Scenario: Skipping the client +- **WHEN** `--no-client` is set +- **THEN** the container starts but the viewer is not launched + +### Requirement: Web port mapping +The container's web interface SHALL be exposed by mapping container port 3000 (HTTP) and optionally 3001 (HTTPS) to host ports. A port value of `0` means a random host port (HTTP) or disabled (HTTPS). The resolved host HTTP port SHALL be discoverable so the viewer can connect. + +#### Scenario: Random HTTP port +- **WHEN** the HTTP host port is `0` +- **THEN** Docker assigns a random host port and the runtime resolves it for the client + +### Requirement: GPU passthrough +The runtime SHALL support GPU passthrough: `intel`/`amd` mount `/dev/dri` and set the relevant rendering env vars; `nvidia` uses Docker device requests with compute/video/graphics capabilities. Incompatible OS/GPU combinations SHALL be rejected. + +#### Scenario: nvidia on alpine +- **WHEN** GPU is `nvidia` and the image's base OS is `alpine` +- **THEN** the run is rejected as incompatible + +### Requirement: Container labels +Created containers SHALL be labeled `org.desktopus.managed-by=desktopus` and `org.desktopus.desktop=`, so desktopus can filter the containers it manages. Base OS and user are NOT set as container labels; they are recorded as image labels (`org.desktopus.base-os` / `org.desktopus.user`) at build time and read back via image inspection at runtime (e.g. for GPU compatibility checks and persistence path resolution). + +#### Scenario: Listing only managed containers +- **WHEN** `list` is invoked +- **THEN** only containers labeled `org.desktopus.managed-by=desktopus` are shown + +#### Scenario: Base OS resolved from image +- **WHEN** the runtime needs the base OS (e.g. to validate GPU compatibility) +- **THEN** it reads the `org.desktopus.base-os` label from the image, not from the container + +### Requirement: Persistent home volume +When `persistence_home` names a Docker volume, the runtime SHALL mount it at the desktop's effective home directory so user data survives container removal, labeling it as a desktopus home volume. + +#### Scenario: Persistence configured +- **WHEN** `persistence_home` is set +- **THEN** the named volume is mounted at the effective home and tagged as a desktopus home volume + +### Requirement: Lifecycle commands +The system SHALL support stopping containers (`stop`, with a timeout before force-kill and `--all` to stop every managed container) and listing them (`list`/`ls`, with `--all` to include stopped containers), scoped to desktopus-managed containers. + +#### Scenario: Stop all +- **WHEN** `stop --all` is invoked +- **THEN** all desktopus-managed containers are stopped + +#### Scenario: List including stopped +- **WHEN** `list --all` is invoked +- **THEN** stopped managed containers are included in the output diff --git a/openspec/changes/archive/2026-05-31-baseline-current-specs/specs/desktop-authoring/spec.md b/openspec/changes/archive/2026-05-31-baseline-current-specs/specs/desktop-authoring/spec.md new file mode 100644 index 0000000..1a93e32 --- /dev/null +++ b/openspec/changes/archive/2026-05-31-baseline-current-specs/specs/desktop-authoring/spec.md @@ -0,0 +1,67 @@ +## ADDED Requirements + +### Requirement: Desktop definition file +The system SHALL read a desktop definition from `desktopus.yaml`, describing the desktop's identity, base image, Linux user, modules, environment variables, startup scripts, and provisioned files. + +#### Scenario: Locating the config +- **WHEN** a command needs the desktop definition and is given a directory (or none, defaulting to the current directory) +- **THEN** the system locates `desktopus.yaml` within that directory and loads it + +### Requirement: Name validation +The desktop `name` SHALL be required and DNS-safe: lowercase alphanumeric and hyphens, not starting or ending with a hyphen. + +#### Scenario: Invalid name rejected +- **WHEN** `name` is empty, contains characters outside `[a-z0-9-]`, or starts/ends with a hyphen +- **THEN** validation fails with an error identifying the name + +### Requirement: Base OS and desktop compatibility +`base.os` and `base.desktop` SHALL both be required, and the pair SHALL be validated against the supported compatibility matrix: +- `ubuntu`, `debian`, `fedora`, `arch`, `alpine` → `i3`, `kde`, `mate`, `xfce` +- `el` → `i3`, `mate`, `xfce` (no `kde`) + +#### Scenario: Unsupported OS +- **WHEN** `base.os` is not one of `ubuntu`, `debian`, `fedora`, `el`, `arch`, `alpine` +- **THEN** validation fails, listing the supported OS values + +#### Scenario: Incompatible desktop for OS +- **WHEN** `base.desktop` is not available for the chosen `base.os` (e.g. `kde` on `el`) +- **THEN** validation fails, listing the valid desktops for that OS + +### Requirement: User resolution +The Linux `user` SHALL default to `desktopus` when unset, MUST NOT be `root`, and MUST be a valid Linux username (matching `[a-z_][a-z0-9_-]*`, at most 32 characters). The special value `abc` selects the built-in linuxserver/webtop user. + +#### Scenario: Root rejected +- **WHEN** `user` is `root` +- **THEN** validation fails + +#### Scenario: abc user +- **WHEN** `user` is `abc` +- **THEN** the effective home directory resolves to `/config` + +### Requirement: Home directory resolution +The effective home directory SHALL be `/config` when the user is `abc`, the explicit `home` value when set (which MUST be an absolute path), otherwise `/home/`. + +#### Scenario: Relative home rejected +- **WHEN** `home` is set to a non-absolute path +- **THEN** validation fails + +### Requirement: Module references +Each entry in `modules` SHALL accept either a string (the module name) or an object with `name` and optional `vars`. `name` is required. + +#### Scenario: String shorthand +- **WHEN** a module entry is the bare string `chrome` +- **THEN** it is treated as module `chrome` with no variable overrides + +### Requirement: Startup scripts and provisioned files +`postrun` scripts SHALL each require a `name` and a `script`, with `runas` restricted to `root` or the configured user (default: the configured user). `files` entries SHALL each require `path` and `content`, with an optional octal `mode` (default `0644`), and are provisioned at container startup via envsubst. + +#### Scenario: Invalid runas +- **WHEN** a `postrun` entry's `runas` is neither `root` nor the configured user +- **THEN** validation fails + +### Requirement: Scaffolding new desktops +The `init` command SHALL generate starter `desktopus.yaml` and `desktopus.runtime.yaml` files, defaulting OS to `ubuntu` and desktop to `xfce`, with a configurable desktop name, output directory, OS, and desktop. + +#### Scenario: Default init +- **WHEN** the user runs `init` with no flags +- **THEN** a `desktopus.yaml` (ubuntu/xfce) and a `desktopus.runtime.yaml` are written diff --git a/openspec/changes/archive/2026-05-31-baseline-current-specs/specs/desktop-viewer/spec.md b/openspec/changes/archive/2026-05-31-baseline-current-specs/specs/desktop-viewer/spec.md new file mode 100644 index 0000000..9cfaa5e --- /dev/null +++ b/openspec/changes/archive/2026-05-31-baseline-current-specs/specs/desktop-viewer/spec.md @@ -0,0 +1,33 @@ +## ADDED Requirements + +### Requirement: Connect command +The `connect` command SHALL open the desktopus viewer against a running desktop, resolving the target in priority order: an explicit `http(s)://` URL argument, then lookup by desktop or container name, then `desktopus.runtime.yaml`. + +#### Scenario: Connect by name +- **WHEN** `connect` is given a desktop or container name +- **THEN** the viewer opens against that container's resolved web URL + +#### Scenario: Connect by URL +- **WHEN** `connect` is given an `http(s)://` URL +- **THEN** the viewer opens that URL directly + +### Requirement: Viewer launch and resolution +The viewer binary SHALL be located in priority order: `$DESKTOPUS_VIEWER`, alongside the running desktopus binary, `$PATH`, `~/.desktopus/bin/`, then an embedded copy (when the binary is built with the embed tag). + +#### Scenario: Embedded extraction +- **WHEN** no external viewer binary is found and an embedded copy exists +- **THEN** it is extracted to cache (skipped if the cached copy's SHA256 already matches) and launched + +### Requirement: Keyboard capture +The viewer SHALL capture keyboard input fully while focused — locking via the Keyboard Lock API in fullscreen, suppressing its own browser shortcuts, and grabbing window-manager-level shortcuts (X11 `XGrabKeyboard` / Wayland inhibit) — and SHALL re-acquire the lock when the window regains focus. + +#### Scenario: Refocus re-locks +- **WHEN** the OS releases the keyboard grab (e.g. a VT switch) and the window later regains focus +- **THEN** the viewer re-acquires the keyboard lock + +### Requirement: Network and navigation hardening +The viewer SHALL restrict all HTTP and WebSocket traffic to the target host and port, allowing only the `devtools`, `chrome-extension`, `data:`, and `blob:` schemes, and SHALL prevent navigation away from the target URL. + +#### Scenario: Cross-host request blocked +- **WHEN** the page attempts a request to a host other than the target host:port +- **THEN** the request is blocked diff --git a/openspec/changes/archive/2026-05-31-baseline-current-specs/specs/image-build/spec.md b/openspec/changes/archive/2026-05-31-baseline-current-specs/specs/image-build/spec.md new file mode 100644 index 0000000..265f717 --- /dev/null +++ b/openspec/changes/archive/2026-05-31-baseline-current-specs/specs/image-build/spec.md @@ -0,0 +1,47 @@ +## ADDED Requirements + +### Requirement: Build command +The `build` command SHALL build a Docker image from a desktop definition, reading the image tag from the `image` field of `desktopus.yaml` unless overridden by `--tag`, and SHALL fail if no tag is available. + +#### Scenario: No tag available +- **WHEN** neither `image` is set in `desktopus.yaml` nor `--tag` is provided +- **THEN** the build fails with an error requesting an image tag + +#### Scenario: Tag override +- **WHEN** `--tag` is provided +- **THEN** it takes precedence over the `image` field in the config + +### Requirement: Build pipeline +The build SHALL resolve modules, generate a Dockerfile, an Ansible playbook, and an `ansible.cfg` from embedded templates, assemble an in-memory tar build context, and build the image via the Docker SDK while streaming build output. + +#### Scenario: Module provisioning +- **WHEN** the desktop lists modules +- **THEN** their tasks and variable overrides are included in the generated playbook and executed during the image build + +### Requirement: Base image and user setup +The generated image SHALL be based on the resolved `lscr.io/linuxserver/webtop` tag for the OS/desktop pair (or an explicit `base.tag` override), and SHALL create the configured Linux user, rewriting webtop's default `abc` user references unless the configured user is `abc`. + +#### Scenario: alpine-xfce tag +- **WHEN** the base is `alpine` + `xfce` +- **THEN** the webtop image tag resolves to `latest` (no `alpine-xfce` tag exists) + +### Requirement: Startup hooks and runtime files +The build SHALL install `postrun` scripts as s6 `custom-cont-init.d` hooks and stage `files` entries for envsubst-based provisioning at container startup. + +#### Scenario: Postrun included +- **WHEN** a `postrun` script is defined +- **THEN** it is added to the image as an s6 init hook that runs as the specified user + +### Requirement: Image labels +Built images SHALL be labeled with `org.desktopus.*` metadata (name, description, base OS, user) for later introspection. + +#### Scenario: Base OS label +- **WHEN** an image is built +- **THEN** it carries an `org.desktopus.base-os` label that is read at runtime for compatibility checks + +### Requirement: Build cache control +The build SHALL support disabling the Docker layer cache via `--no-cache`. + +#### Scenario: no-cache build +- **WHEN** `--no-cache` is set +- **THEN** the image is built without reusing cached layers diff --git a/openspec/changes/archive/2026-05-31-baseline-current-specs/specs/module-system/spec.md b/openspec/changes/archive/2026-05-31-baseline-current-specs/specs/module-system/spec.md new file mode 100644 index 0000000..9b4508f --- /dev/null +++ b/openspec/changes/archive/2026-05-31-baseline-current-specs/specs/module-system/spec.md @@ -0,0 +1,48 @@ +## ADDED Requirements + +### Requirement: Module resolution by name or path +The system SHALL resolve a module reference as a built-in module when it is a bare name, and as a custom filesystem module when it is a path (starting with `./`, `../`, or `/`). + +#### Scenario: Built-in lookup +- **WHEN** a module is referenced by the bare name `chrome` +- **THEN** the system loads the built-in `chrome` module from the embedded module set + +#### Scenario: Custom path lookup +- **WHEN** a module is referenced by a path such as `./my-module` +- **THEN** the system loads the module from that filesystem location + +### Requirement: Module metadata +Each module SHALL provide a `module.yaml` declaring at least its `name`, and optionally description, version, author, tags, compatibility (OS / desktop / architecture lists), variables with defaults, dependencies, system packages, and tests. + +#### Scenario: Variable defaults and overrides +- **WHEN** a module declares a variable with a default and the desktop config overrides it via `vars` +- **THEN** the override value is used during provisioning; otherwise the default is used + +### Requirement: Module compatibility enforcement +When a module declares OS or desktop compatibility constraints, the build SHALL verify the target OS and desktop are supported before provisioning. Empty constraints mean the module supports all targets. An `architecture` constraint MAY be declared in `module.yaml` but is NOT currently enforced at resolution time. + +#### Scenario: Incompatible OS +- **WHEN** a module declares it supports only `ubuntu` and the build targets `alpine` +- **THEN** module resolution fails with a compatibility error + +#### Scenario: Architecture not enforced +- **WHEN** a module declares amd64-only compatibility and the build targets a different architecture +- **THEN** resolution does not fail on that basis (the constraint is recorded but unenforced; provisioning may still fail later) + +### Requirement: Module task layout +A module SHALL provide its Ansible tasks either as `tasks/main.yml` or as per-OS files `tasks/.yml` for any of `alpine`, `arch`, `debian`, `el`, `fedora`, `ubuntu`. `tasks/main.yml` is required only when the module declares no OS compatibility list; when an OS list is declared, per-OS task files alone are sufficient. When both `tasks/main.yml` and a matching `tasks/.yml` exist, the per-OS file takes precedence. Optional `handlers/`, `files/`, and `templates/` directories MAY be present. + +#### Scenario: OS-specific tasks take precedence +- **WHEN** a module has both `tasks/main.yml` and `tasks/alpine.yml` and the build targets alpine +- **THEN** the alpine-specific tasks are used for that build + +#### Scenario: Per-OS tasks without main.yml +- **WHEN** a module declares an OS compatibility list and ships only per-OS task files (no `tasks/main.yml`) +- **THEN** the module loads successfully and the matching per-OS task file is used + +### Requirement: Built-in modules +The system SHALL ship a set of built-in modules embedded in the binary, available by name. The current built-in set is `chrome` (Google Chrome; declares compatibility with `ubuntu`, `debian`, `fedora`, `el`, `arch`, and architecture `amd64`, and ships per-OS task files with no `tasks/main.yml`). + +#### Scenario: Chrome module +- **WHEN** the desktop config lists module `chrome` and the target is one of its supported OSes +- **THEN** Chrome is installed during the image build diff --git a/openspec/changes/archive/2026-05-31-baseline-current-specs/specs/volume-management/spec.md b/openspec/changes/archive/2026-05-31-baseline-current-specs/specs/volume-management/spec.md new file mode 100644 index 0000000..86837af --- /dev/null +++ b/openspec/changes/archive/2026-05-31-baseline-current-specs/specs/volume-management/spec.md @@ -0,0 +1,19 @@ +## ADDED Requirements + +### Requirement: List volumes +The `volume ls` command SHALL list Docker volumes managed by desktopus, showing the volume name, the owning desktop, and the volume type. + +#### Scenario: Listing managed volumes +- **WHEN** `volume ls` is invoked +- **THEN** only volumes labeled `org.desktopus.managed-by=desktopus` are listed, with name, desktop, and type + +### Requirement: Remove volumes +The `volume rm` command SHALL remove named desktopus-managed volumes, supporting `--force`. + +#### Scenario: Remove in-use volume +- **WHEN** `volume rm` targets a volume that is in use and `--force` is not set +- **THEN** removal fails + +#### Scenario: Force remove +- **WHEN** `volume rm --force` is invoked for a managed volume +- **THEN** the volume is removed diff --git a/openspec/changes/archive/2026-05-31-baseline-current-specs/tasks.md b/openspec/changes/archive/2026-05-31-baseline-current-specs/tasks.md new file mode 100644 index 0000000..64818e1 --- /dev/null +++ b/openspec/changes/archive/2026-05-31-baseline-current-specs/tasks.md @@ -0,0 +1,14 @@ +## 1. Verify capability specs against code + +- [x] 1.1 `desktop-authoring`: confirm schema, validation, and `init` defaults match `internal/config/` and `internal/cli/init.go` — confirmed exact +- [x] 1.2 `module-system`: confirm resolution, `module.yaml`, compatibility, and built-ins match `internal/module/` and `modules/` — corrected (arch not enforced; main.yml optional when OS list declared) +- [x] 1.3 `image-build`: confirm pipeline and `build` flags match `internal/build/` and `internal/cli/build.go` — confirmed exact +- [x] 1.4 `container-runtime`: confirm run/stop/list, labels, GPU, ports, and persistence match `internal/runtime/` and `internal/cli/{run,stop,list}.go` — corrected (base-os/user are image labels, not container labels) +- [x] 1.5 `volume-management`: confirm `volume ls`/`rm` match `internal/cli/volume.go` and `internal/runtime/` — confirmed exact +- [x] 1.6 `desktop-viewer`: confirm `connect` and viewer behavior match `internal/viewer/` and `viewer/` — confirmed exact +- [x] 1.7 `app-configuration`: confirm global config, flags, and `version` match `internal/config/app.go` and `internal/cli/root.go` — confirmed exact + +## 2. Validate and archive + +- [x] 2.1 `openspec validate baseline-current-specs --strict` passes +- [ ] 2.2 Archive the change to populate `openspec/specs/` diff --git a/openspec/specs/.gitkeep b/openspec/specs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/openspec/specs/app-configuration/spec.md b/openspec/specs/app-configuration/spec.md new file mode 100644 index 0000000..cc3dfa8 --- /dev/null +++ b/openspec/specs/app-configuration/spec.md @@ -0,0 +1,33 @@ +# app-configuration Specification + +## Purpose +TBD - created by archiving change baseline-current-specs. Update Purpose after archive. +## Requirements +### Requirement: Global app config +The system SHALL load global configuration from `~/.desktopus/config.yaml` (overridable via `--config`), covering Docker connection, build settings, and logging. Reserved `server` and `store` sections MAY be present for future use without affecting current behavior. + +#### Scenario: Default docker host +- **WHEN** no Docker host is configured +- **THEN** it defaults to `unix:///var/run/docker.sock` + +### Requirement: Docker connection +The Docker endpoint SHALL be configurable via `docker.host` and used for all build and runtime operations. + +#### Scenario: Custom docker host +- **WHEN** `docker.host` is set to a remote endpoint +- **THEN** build and run operate against that endpoint + +### Requirement: Global CLI flags +The CLI SHALL expose global flags: `--config` (config file path), `--log-level` (`debug` | `info` | `warn` | `error`), and `--no-color`. + +#### Scenario: Log level override +- **WHEN** `--log-level debug` is passed +- **THEN** debug logging is enabled regardless of the level set in the config file + +### Requirement: Version reporting +The `version` command SHALL print the build version, commit hash, and build timestamp injected at compile time. + +#### Scenario: Version output +- **WHEN** `version` is invoked +- **THEN** the version, commit hash, and build time are printed + diff --git a/openspec/specs/container-runtime/spec.md b/openspec/specs/container-runtime/spec.md new file mode 100644 index 0000000..c9d363e --- /dev/null +++ b/openspec/specs/container-runtime/spec.md @@ -0,0 +1,80 @@ +# container-runtime Specification + +## Purpose +TBD - created by archiving change baseline-current-specs. Update Purpose after archive. +## Requirements +### Requirement: Runtime configuration file +Container runtime options SHALL be read from `desktopus.runtime.yaml` (located alongside the desktop definition, or via `--file`). The file is optional and defaults to empty. It MAY declare `name`, `image` override, `hostname`, `shm_size`, `ports`, `volumes`, `gpu`, `memory`, `cpus`, `restart`, `network`, `env`, `provider`, `persistence_home`, and `web` port bindings. + +#### Scenario: Optional runtime config +- **WHEN** no `desktopus.runtime.yaml` exists +- **THEN** `run` proceeds with default runtime settings + +### Requirement: Image resolution for run +The `run` command SHALL resolve the image tag in priority order: the CLI argument, else the `image` field of `desktopus.runtime.yaml`, else an error. (This is independent of `build`, which reads `image` only from `desktopus.yaml`.) + +#### Scenario: No image for run +- **WHEN** `run` is invoked with no image argument and `desktopus.runtime.yaml` has no `image` +- **THEN** the command fails asking for an image + +### Requirement: Runtime validation +The runtime config SHALL validate `restart` (`no` | `always` | `unless-stopped` | `on-failure`), `provider` (`docker` only), and `gpu` (`intel` | `amd` | `nvidia`). + +#### Scenario: Invalid restart policy +- **WHEN** `restart` is not one of the allowed values +- **THEN** validation fails + +### Requirement: Run command +The `run` command SHALL create and start a container from a built image, detaching by default, applying ports, volumes, env, GPU, name, and `--rm` from both the runtime config and CLI flags, and SHALL launch the viewer client unless `--no-client` is set. + +#### Scenario: Detached run +- **WHEN** `run` is invoked without overriding detach +- **THEN** the container runs in the background and its ID is returned + +#### Scenario: Skipping the client +- **WHEN** `--no-client` is set +- **THEN** the container starts but the viewer is not launched + +### Requirement: Web port mapping +The container's web interface SHALL be exposed by mapping container port 3000 (HTTP) and optionally 3001 (HTTPS) to host ports. A port value of `0` means a random host port (HTTP) or disabled (HTTPS). The resolved host HTTP port SHALL be discoverable so the viewer can connect. + +#### Scenario: Random HTTP port +- **WHEN** the HTTP host port is `0` +- **THEN** Docker assigns a random host port and the runtime resolves it for the client + +### Requirement: GPU passthrough +The runtime SHALL support GPU passthrough: `intel`/`amd` mount `/dev/dri` and set the relevant rendering env vars; `nvidia` uses Docker device requests with compute/video/graphics capabilities. Incompatible OS/GPU combinations SHALL be rejected. + +#### Scenario: nvidia on alpine +- **WHEN** GPU is `nvidia` and the image's base OS is `alpine` +- **THEN** the run is rejected as incompatible + +### Requirement: Container labels +Created containers SHALL be labeled `org.desktopus.managed-by=desktopus` and `org.desktopus.desktop=`, so desktopus can filter the containers it manages. Base OS and user are NOT set as container labels; they are recorded as image labels (`org.desktopus.base-os` / `org.desktopus.user`) at build time and read back via image inspection at runtime (e.g. for GPU compatibility checks and persistence path resolution). + +#### Scenario: Listing only managed containers +- **WHEN** `list` is invoked +- **THEN** only containers labeled `org.desktopus.managed-by=desktopus` are shown + +#### Scenario: Base OS resolved from image +- **WHEN** the runtime needs the base OS (e.g. to validate GPU compatibility) +- **THEN** it reads the `org.desktopus.base-os` label from the image, not from the container + +### Requirement: Persistent home volume +When `persistence_home` names a Docker volume, the runtime SHALL mount it at the desktop's effective home directory so user data survives container removal, labeling it as a desktopus home volume. + +#### Scenario: Persistence configured +- **WHEN** `persistence_home` is set +- **THEN** the named volume is mounted at the effective home and tagged as a desktopus home volume + +### Requirement: Lifecycle commands +The system SHALL support stopping containers (`stop`, with a timeout before force-kill and `--all` to stop every managed container) and listing them (`list`/`ls`, with `--all` to include stopped containers), scoped to desktopus-managed containers. + +#### Scenario: Stop all +- **WHEN** `stop --all` is invoked +- **THEN** all desktopus-managed containers are stopped + +#### Scenario: List including stopped +- **WHEN** `list --all` is invoked +- **THEN** stopped managed containers are included in the output + diff --git a/openspec/specs/desktop-authoring/spec.md b/openspec/specs/desktop-authoring/spec.md new file mode 100644 index 0000000..2afca5a --- /dev/null +++ b/openspec/specs/desktop-authoring/spec.md @@ -0,0 +1,71 @@ +# desktop-authoring Specification + +## Purpose +TBD - created by archiving change baseline-current-specs. Update Purpose after archive. +## Requirements +### Requirement: Desktop definition file +The system SHALL read a desktop definition from `desktopus.yaml`, describing the desktop's identity, base image, Linux user, modules, environment variables, startup scripts, and provisioned files. + +#### Scenario: Locating the config +- **WHEN** a command needs the desktop definition and is given a directory (or none, defaulting to the current directory) +- **THEN** the system locates `desktopus.yaml` within that directory and loads it + +### Requirement: Name validation +The desktop `name` SHALL be required and DNS-safe: lowercase alphanumeric and hyphens, not starting or ending with a hyphen. + +#### Scenario: Invalid name rejected +- **WHEN** `name` is empty, contains characters outside `[a-z0-9-]`, or starts/ends with a hyphen +- **THEN** validation fails with an error identifying the name + +### Requirement: Base OS and desktop compatibility +`base.os` and `base.desktop` SHALL both be required, and the pair SHALL be validated against the supported compatibility matrix: +- `ubuntu`, `debian`, `fedora`, `arch`, `alpine` → `i3`, `kde`, `mate`, `xfce` +- `el` → `i3`, `mate`, `xfce` (no `kde`) + +#### Scenario: Unsupported OS +- **WHEN** `base.os` is not one of `ubuntu`, `debian`, `fedora`, `el`, `arch`, `alpine` +- **THEN** validation fails, listing the supported OS values + +#### Scenario: Incompatible desktop for OS +- **WHEN** `base.desktop` is not available for the chosen `base.os` (e.g. `kde` on `el`) +- **THEN** validation fails, listing the valid desktops for that OS + +### Requirement: User resolution +The Linux `user` SHALL default to `desktopus` when unset, MUST NOT be `root`, and MUST be a valid Linux username (matching `[a-z_][a-z0-9_-]*`, at most 32 characters). The special value `abc` selects the built-in linuxserver/webtop user. + +#### Scenario: Root rejected +- **WHEN** `user` is `root` +- **THEN** validation fails + +#### Scenario: abc user +- **WHEN** `user` is `abc` +- **THEN** the effective home directory resolves to `/config` + +### Requirement: Home directory resolution +The effective home directory SHALL be `/config` when the user is `abc`, the explicit `home` value when set (which MUST be an absolute path), otherwise `/home/`. + +#### Scenario: Relative home rejected +- **WHEN** `home` is set to a non-absolute path +- **THEN** validation fails + +### Requirement: Module references +Each entry in `modules` SHALL accept either a string (the module name) or an object with `name` and optional `vars`. `name` is required. + +#### Scenario: String shorthand +- **WHEN** a module entry is the bare string `chrome` +- **THEN** it is treated as module `chrome` with no variable overrides + +### Requirement: Startup scripts and provisioned files +`postrun` scripts SHALL each require a `name` and a `script`, with `runas` restricted to `root` or the configured user (default: the configured user). `files` entries SHALL each require `path` and `content`, with an optional octal `mode` (default `0644`), and are provisioned at container startup via envsubst. + +#### Scenario: Invalid runas +- **WHEN** a `postrun` entry's `runas` is neither `root` nor the configured user +- **THEN** validation fails + +### Requirement: Scaffolding new desktops +The `init` command SHALL generate starter `desktopus.yaml` and `desktopus.runtime.yaml` files, defaulting OS to `ubuntu` and desktop to `xfce`, with a configurable desktop name, output directory, OS, and desktop. + +#### Scenario: Default init +- **WHEN** the user runs `init` with no flags +- **THEN** a `desktopus.yaml` (ubuntu/xfce) and a `desktopus.runtime.yaml` are written + diff --git a/openspec/specs/desktop-viewer/spec.md b/openspec/specs/desktop-viewer/spec.md new file mode 100644 index 0000000..1a3bbc5 --- /dev/null +++ b/openspec/specs/desktop-viewer/spec.md @@ -0,0 +1,37 @@ +# desktop-viewer Specification + +## Purpose +TBD - created by archiving change baseline-current-specs. Update Purpose after archive. +## Requirements +### Requirement: Connect command +The `connect` command SHALL open the desktopus viewer against a running desktop, resolving the target in priority order: an explicit `http(s)://` URL argument, then lookup by desktop or container name, then `desktopus.runtime.yaml`. + +#### Scenario: Connect by name +- **WHEN** `connect` is given a desktop or container name +- **THEN** the viewer opens against that container's resolved web URL + +#### Scenario: Connect by URL +- **WHEN** `connect` is given an `http(s)://` URL +- **THEN** the viewer opens that URL directly + +### Requirement: Viewer launch and resolution +The viewer binary SHALL be located in priority order: `$DESKTOPUS_VIEWER`, alongside the running desktopus binary, `$PATH`, `~/.desktopus/bin/`, then an embedded copy (when the binary is built with the embed tag). + +#### Scenario: Embedded extraction +- **WHEN** no external viewer binary is found and an embedded copy exists +- **THEN** it is extracted to cache (skipped if the cached copy's SHA256 already matches) and launched + +### Requirement: Keyboard capture +The viewer SHALL capture keyboard input fully while focused — locking via the Keyboard Lock API in fullscreen, suppressing its own browser shortcuts, and grabbing window-manager-level shortcuts (X11 `XGrabKeyboard` / Wayland inhibit) — and SHALL re-acquire the lock when the window regains focus. + +#### Scenario: Refocus re-locks +- **WHEN** the OS releases the keyboard grab (e.g. a VT switch) and the window later regains focus +- **THEN** the viewer re-acquires the keyboard lock + +### Requirement: Network and navigation hardening +The viewer SHALL restrict all HTTP and WebSocket traffic to the target host and port, allowing only the `devtools`, `chrome-extension`, `data:`, and `blob:` schemes, and SHALL prevent navigation away from the target URL. + +#### Scenario: Cross-host request blocked +- **WHEN** the page attempts a request to a host other than the target host:port +- **THEN** the request is blocked + diff --git a/openspec/specs/image-build/spec.md b/openspec/specs/image-build/spec.md new file mode 100644 index 0000000..3e30a27 --- /dev/null +++ b/openspec/specs/image-build/spec.md @@ -0,0 +1,51 @@ +# image-build Specification + +## Purpose +TBD - created by archiving change baseline-current-specs. Update Purpose after archive. +## Requirements +### Requirement: Build command +The `build` command SHALL build a Docker image from a desktop definition, reading the image tag from the `image` field of `desktopus.yaml` unless overridden by `--tag`, and SHALL fail if no tag is available. + +#### Scenario: No tag available +- **WHEN** neither `image` is set in `desktopus.yaml` nor `--tag` is provided +- **THEN** the build fails with an error requesting an image tag + +#### Scenario: Tag override +- **WHEN** `--tag` is provided +- **THEN** it takes precedence over the `image` field in the config + +### Requirement: Build pipeline +The build SHALL resolve modules, generate a Dockerfile, an Ansible playbook, and an `ansible.cfg` from embedded templates, assemble an in-memory tar build context, and build the image via the Docker SDK while streaming build output. + +#### Scenario: Module provisioning +- **WHEN** the desktop lists modules +- **THEN** their tasks and variable overrides are included in the generated playbook and executed during the image build + +### Requirement: Base image and user setup +The generated image SHALL be based on the resolved `lscr.io/linuxserver/webtop` tag for the OS/desktop pair (or an explicit `base.tag` override), and SHALL create the configured Linux user, rewriting webtop's default `abc` user references unless the configured user is `abc`. + +#### Scenario: alpine-xfce tag +- **WHEN** the base is `alpine` + `xfce` +- **THEN** the webtop image tag resolves to `latest` (no `alpine-xfce` tag exists) + +### Requirement: Startup hooks and runtime files +The build SHALL install `postrun` scripts as s6 `custom-cont-init.d` hooks and stage `files` entries for envsubst-based provisioning at container startup. + +#### Scenario: Postrun included +- **WHEN** a `postrun` script is defined +- **THEN** it is added to the image as an s6 init hook that runs as the specified user + +### Requirement: Image labels +Built images SHALL be labeled with `org.desktopus.*` metadata (name, description, base OS, user) for later introspection. + +#### Scenario: Base OS label +- **WHEN** an image is built +- **THEN** it carries an `org.desktopus.base-os` label that is read at runtime for compatibility checks + +### Requirement: Build cache control +The build SHALL support disabling the Docker layer cache via `--no-cache`. + +#### Scenario: no-cache build +- **WHEN** `--no-cache` is set +- **THEN** the image is built without reusing cached layers + diff --git a/openspec/specs/module-system/spec.md b/openspec/specs/module-system/spec.md new file mode 100644 index 0000000..40ee4f5 --- /dev/null +++ b/openspec/specs/module-system/spec.md @@ -0,0 +1,52 @@ +# module-system Specification + +## Purpose +TBD - created by archiving change baseline-current-specs. Update Purpose after archive. +## Requirements +### Requirement: Module resolution by name or path +The system SHALL resolve a module reference as a built-in module when it is a bare name, and as a custom filesystem module when it is a path (starting with `./`, `../`, or `/`). + +#### Scenario: Built-in lookup +- **WHEN** a module is referenced by the bare name `chrome` +- **THEN** the system loads the built-in `chrome` module from the embedded module set + +#### Scenario: Custom path lookup +- **WHEN** a module is referenced by a path such as `./my-module` +- **THEN** the system loads the module from that filesystem location + +### Requirement: Module metadata +Each module SHALL provide a `module.yaml` declaring at least its `name`, and optionally description, version, author, tags, compatibility (OS / desktop / architecture lists), variables with defaults, dependencies, system packages, and tests. + +#### Scenario: Variable defaults and overrides +- **WHEN** a module declares a variable with a default and the desktop config overrides it via `vars` +- **THEN** the override value is used during provisioning; otherwise the default is used + +### Requirement: Module compatibility enforcement +When a module declares OS or desktop compatibility constraints, the build SHALL verify the target OS and desktop are supported before provisioning. Empty constraints mean the module supports all targets. An `architecture` constraint MAY be declared in `module.yaml` but is NOT currently enforced at resolution time. + +#### Scenario: Incompatible OS +- **WHEN** a module declares it supports only `ubuntu` and the build targets `alpine` +- **THEN** module resolution fails with a compatibility error + +#### Scenario: Architecture not enforced +- **WHEN** a module declares amd64-only compatibility and the build targets a different architecture +- **THEN** resolution does not fail on that basis (the constraint is recorded but unenforced; provisioning may still fail later) + +### Requirement: Module task layout +A module SHALL provide its Ansible tasks either as `tasks/main.yml` or as per-OS files `tasks/.yml` for any of `alpine`, `arch`, `debian`, `el`, `fedora`, `ubuntu`. `tasks/main.yml` is required only when the module declares no OS compatibility list; when an OS list is declared, per-OS task files alone are sufficient. When both `tasks/main.yml` and a matching `tasks/.yml` exist, the per-OS file takes precedence. Optional `handlers/`, `files/`, and `templates/` directories MAY be present. + +#### Scenario: OS-specific tasks take precedence +- **WHEN** a module has both `tasks/main.yml` and `tasks/alpine.yml` and the build targets alpine +- **THEN** the alpine-specific tasks are used for that build + +#### Scenario: Per-OS tasks without main.yml +- **WHEN** a module declares an OS compatibility list and ships only per-OS task files (no `tasks/main.yml`) +- **THEN** the module loads successfully and the matching per-OS task file is used + +### Requirement: Built-in modules +The system SHALL ship a set of built-in modules embedded in the binary, available by name. The current built-in set is `chrome` (Google Chrome; declares compatibility with `ubuntu`, `debian`, `fedora`, `el`, `arch`, and architecture `amd64`, and ships per-OS task files with no `tasks/main.yml`). + +#### Scenario: Chrome module +- **WHEN** the desktop config lists module `chrome` and the target is one of its supported OSes +- **THEN** Chrome is installed during the image build + diff --git a/openspec/specs/volume-management/spec.md b/openspec/specs/volume-management/spec.md new file mode 100644 index 0000000..751e10a --- /dev/null +++ b/openspec/specs/volume-management/spec.md @@ -0,0 +1,23 @@ +# volume-management Specification + +## Purpose +TBD - created by archiving change baseline-current-specs. Update Purpose after archive. +## Requirements +### Requirement: List volumes +The `volume ls` command SHALL list Docker volumes managed by desktopus, showing the volume name, the owning desktop, and the volume type. + +#### Scenario: Listing managed volumes +- **WHEN** `volume ls` is invoked +- **THEN** only volumes labeled `org.desktopus.managed-by=desktopus` are listed, with name, desktop, and type + +### Requirement: Remove volumes +The `volume rm` command SHALL remove named desktopus-managed volumes, supporting `--force`. + +#### Scenario: Remove in-use volume +- **WHEN** `volume rm` targets a volume that is in use and `--force` is not set +- **THEN** removal fails + +#### Scenario: Force remove +- **WHEN** `volume rm --force` is invoked for a managed volume +- **THEN** the volume is removed +