303 lines
9.9 KiB
Markdown
303 lines
9.9 KiB
Markdown
# Container Packaging and Portable Storage Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Run ThothII from exactly two application images with runtime configuration and host-independent persistent roots.
|
|
|
|
**Architecture:** Build a multi-runtime core image for Fastify, Pi, and `tht`, plus a static frontend image. Resolve workspace paths beneath mounted logical roots and provide a Compose base for external dependencies.
|
|
|
|
**Tech Stack:** Docker BuildKit, Docker Compose v2, Node 22, Python 3.11, Fastify, React/Vite, nginx or Caddy.
|
|
|
|
## Global Constraints
|
|
|
|
- Plan 1 is complete and adapter factories are the only integration construction path.
|
|
- Do not bake secrets or customer workspace content into images.
|
|
- Containers run as non-root and write only beneath mounted data roots.
|
|
- `pi`, `tht`, CA certificates, and native dependencies must work on every advertised architecture.
|
|
- Frontend backend URL is runtime-configurable.
|
|
|
|
---
|
|
|
|
### Task 1: Add logical workspace roots and migration diagnostics
|
|
|
|
**Files:**
|
|
- Create: `harness/tht/paths.py`
|
|
- Create: `harness/tht/cli/doctor_cmd.py`
|
|
- Modify: `harness/tht/config.py`
|
|
- Modify: `harness/tht/cli/__init__.py`
|
|
- Test: `harness/tests/test_portable_paths.py`
|
|
- Test: `harness/tests/test_doctor_cli.py`
|
|
|
|
**Interfaces:**
|
|
- Produces: `resolve_workspace_paths(config_path, cfg, data_root) -> ResolvedPaths` and `tht doctor --json`.
|
|
|
|
- [ ] **Step 1: Test relative, absolute-legacy, and escape rejection**
|
|
|
|
```python
|
|
def test_relative_paths_resolve_under_workspace_root(tmp_path):
|
|
resolved = resolve_workspace_paths(cfg_path, cfg, tmp_path / "data")
|
|
assert resolved.sessions == tmp_path / "data/workspaces/demo/sessions"
|
|
|
|
def test_path_escape_is_rejected(tmp_path):
|
|
with pytest.raises(ConfigError, match="outside workspace root"):
|
|
resolve_workspace_paths(cfg_path, config_with_sessions("../../private"), tmp_path)
|
|
```
|
|
|
|
- [ ] **Step 2: Verify failure**
|
|
|
|
Run: `cd harness && .venv/bin/pytest tests/test_portable_paths.py tests/test_doctor_cli.py -q`
|
|
Expected: FAIL.
|
|
|
|
- [ ] **Step 3: Implement resolution and JSON diagnostics**
|
|
|
|
```python
|
|
@dataclass(frozen=True)
|
|
class ResolvedPaths:
|
|
workspace: Path
|
|
sessions: Path
|
|
artifacts: Path
|
|
indexes: Path
|
|
corpus: Path
|
|
```
|
|
|
|
`doctor --json` returns component statuses without printing warnings to stdout.
|
|
|
|
- [ ] **Step 4: Run tests**
|
|
|
|
Run: `cd harness && .venv/bin/pytest tests/test_portable_paths.py tests/test_doctor_cli.py tests/test_workspace.py -q`
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add harness/tht/paths.py harness/tht/cli/doctor_cmd.py harness/tht/config.py harness/tht/cli/__init__.py harness/tests/test_portable_paths.py harness/tests/test_doctor_cli.py
|
|
git commit -m "feat(storage): resolve portable workspace roots"
|
|
```
|
|
|
|
### Task 2: Make backend process paths and listening address container-safe
|
|
|
|
**Files:**
|
|
- Modify: `backend/src/config.ts`
|
|
- Modify: `backend/src/server.ts`
|
|
- Modify: `backend/src/pi/pi-process-manager.ts`
|
|
- Modify: `backend/src/tht/tht-runner.ts`
|
|
- Test: `backend/test/config.test.ts`
|
|
- Test: `backend/test/pi-process-manager.test.ts`
|
|
|
|
**Interfaces:**
|
|
- Produces env contract: `HOST`, `PORT`, `THT_HARNESS_DIR`, `THT_BIN`, `PI_BIN`, `SETTINGS_FILE`, `THT_DATA_ROOT`.
|
|
|
|
- [ ] **Step 1: Add tests for explicit binaries and `0.0.0.0` listening**
|
|
|
|
```typescript
|
|
expect(loadConfig({ HOST: "0.0.0.0", THT_BIN: "/opt/venv/bin/tht" })).toMatchObject({
|
|
host: "0.0.0.0", thtBin: "/opt/venv/bin/tht"
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Verify failure**
|
|
|
|
Run: `cd backend && npx vitest run test/config.test.ts test/pi-process-manager.test.ts`
|
|
Expected: FAIL for missing host/data-root behavior.
|
|
|
|
- [ ] **Step 3: Remove the `.venv/bin` PATH assumption**
|
|
|
|
```typescript
|
|
const env = { ...process.env, THT_DATA_ROOT: cfg.dataRoot };
|
|
const child = spawn(cfg.piBin, ["--mode", "rpc"], { cwd: cfg.harnessDir, env });
|
|
```
|
|
|
|
- [ ] **Step 4: Run backend tests and typecheck**
|
|
|
|
Run: `cd backend && npx vitest run && npx tsc --noEmit -p .`
|
|
Expected: PASS.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add backend/src backend/test
|
|
git commit -m "feat(backend): support container runtime paths"
|
|
```
|
|
|
|
### Task 3: Build the core image
|
|
|
|
**Files:**
|
|
- Create: `docker/core.Dockerfile`
|
|
- Create: `docker/core-entrypoint.sh`
|
|
- Create: `.dockerignore`
|
|
- Create: `docker/smoke/core-smoke.sh`
|
|
|
|
**Interfaces:**
|
|
- Produces image entrypoints: `server`, `doctor`, `preprocess`, and arbitrary `tht ...`.
|
|
|
|
- [ ] **Step 1: Add a smoke script that asserts binaries and health**
|
|
|
|
```sh
|
|
test "$(id -u)" != "0"
|
|
node --version
|
|
python --version
|
|
tht --help >/dev/null
|
|
pi --version >/dev/null
|
|
curl --fail http://127.0.0.1:8787/health
|
|
```
|
|
|
|
- [ ] **Step 2: Build and observe the initial failure**
|
|
|
|
Run: `docker build -f docker/core.Dockerfile -t thothii-core:test .`
|
|
Expected: FAIL because the Dockerfile is not present before implementation.
|
|
|
|
- [ ] **Step 3: Implement a multi-stage core build**
|
|
|
|
```dockerfile
|
|
FROM node:22-bookworm AS backend-build
|
|
WORKDIR /src/backend
|
|
COPY backend/package*.json ./
|
|
RUN npm ci
|
|
COPY backend/ ./
|
|
RUN npm run build
|
|
|
|
FROM python:3.11-slim-bookworm AS runtime
|
|
RUN useradd --create-home --uid 10001 thoth
|
|
WORKDIR /app
|
|
COPY harness/ /app/harness/
|
|
RUN python -m venv /opt/venv && /opt/venv/bin/pip install --no-cache-dir /app/harness
|
|
COPY --from=backend-build /src/backend/dist /app/backend/dist
|
|
COPY --from=backend-build /src/backend/node_modules /app/backend/node_modules
|
|
ENV PATH="/opt/venv/bin:$PATH" HOST=0.0.0.0 PORT=8787
|
|
USER thoth
|
|
ENTRYPOINT ["/app/docker/core-entrypoint.sh"]
|
|
CMD ["server"]
|
|
```
|
|
|
|
Add Pi installation only from its pinned, redistributable source after the Phase 0 license/runtime gate; fail the build if `pi --version` is unavailable.
|
|
|
|
- [ ] **Step 4: Build and run smoke test**
|
|
|
|
Run: `docker build -f docker/core.Dockerfile -t thothii-core:test .`
|
|
Expected: success.
|
|
Run: `docker run --rm thothii-core:test doctor`
|
|
Expected: process starts and reports missing external configuration without traceback.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add docker/core.Dockerfile docker/core-entrypoint.sh docker/smoke/core-smoke.sh .dockerignore
|
|
git commit -m "build(docker): add core application image"
|
|
```
|
|
|
|
### Task 4: Add runtime frontend configuration and image
|
|
|
|
**Files:**
|
|
- Create: `frontend/public/config.js`
|
|
- Create: `frontend/src/api/runtime-config.ts`
|
|
- Modify: `frontend/src/api/client.ts`
|
|
- Modify: `frontend/index.html`
|
|
- Create: `docker/frontend.Dockerfile`
|
|
- Create: `docker/frontend-entrypoint.sh`
|
|
- Create: `docker/nginx.conf.template`
|
|
- Test: `frontend/src/api/runtime-config.test.ts`
|
|
|
|
**Interfaces:**
|
|
- Produces browser contract: `window.__THOTHII_CONFIG__.backendBaseUrl`.
|
|
|
|
- [ ] **Step 1: Write fallback and injected-config tests**
|
|
|
|
```typescript
|
|
expect(resolveBackendUrl({ backendBaseUrl: "/api" })).toBe("/api");
|
|
expect(resolveBackendUrl(undefined)).toBe(import.meta.env.VITE_BACKEND_URL ?? "");
|
|
```
|
|
|
|
- [ ] **Step 2: Verify failure**
|
|
|
|
Run: `cd frontend && npx vitest run src/api/runtime-config.test.ts`
|
|
Expected: FAIL.
|
|
|
|
- [ ] **Step 3: Implement runtime config generation and reverse proxy**
|
|
|
|
```sh
|
|
sed "s|__BACKEND_BASE_URL__|${BACKEND_BASE_URL:-/api}|g" \
|
|
/usr/share/nginx/html/config.template.js > /usr/share/nginx/html/config.js
|
|
exec nginx -g 'daemon off;'
|
|
```
|
|
|
|
- [ ] **Step 4: Test and build**
|
|
|
|
Run: `cd frontend && npx vitest run && npx tsc -b && npm run build`
|
|
Expected: PASS.
|
|
Run: `docker build -f docker/frontend.Dockerfile -t thothii-frontend:test .`
|
|
Expected: success.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add frontend docker/frontend.Dockerfile docker/frontend-entrypoint.sh docker/nginx.conf.template
|
|
git commit -m "build(docker): add runtime-configured frontend image"
|
|
```
|
|
|
|
### Task 5: Compose external profile and end-to-end smoke gate
|
|
|
|
> **Final-review security amendment (2026-07-12):** the frontend port binds to `127.0.0.1` by
|
|
> default. Public deployment uses an authenticated upstream proxy with `AUTH_MODE=upstream`;
|
|
> `THOTH_PUBLIC_EXPOSURE=true` plus `AUTH_MODE=none` is invalid. Local env files are development
|
|
> only; production uses read-only Compose secrets. Image gates pin exact tags and multi-platform
|
|
> digests and verify both linux/amd64 and linux/arm64 using the shared container verification
|
|
> script.
|
|
|
|
**Files:**
|
|
- Create: `compose.yaml`
|
|
- Create: `deploy/env.example`
|
|
- Create: `deploy/workspaces/example.yaml`
|
|
- Create: `scripts/docker-smoke.sh`
|
|
- Modify: `README.md`
|
|
- Test: `backend/test/health.test.ts`
|
|
|
|
**Interfaces:**
|
|
- Produces services `core` and `frontend`; persistent volume/mount contract beneath `/data`.
|
|
|
|
- [ ] **Step 1: Add a smoke script for Compose config and HTTP health**
|
|
|
|
```sh
|
|
docker compose config --quiet
|
|
docker compose up --build --wait core frontend
|
|
curl --fail http://localhost:8080/health
|
|
docker compose down
|
|
```
|
|
|
|
- [ ] **Step 2: Verify Compose is initially absent**
|
|
|
|
Run: `docker compose config --quiet`
|
|
Expected: FAIL before `compose.yaml` is implemented.
|
|
|
|
- [ ] **Step 3: Define the base deployment**
|
|
|
|
```yaml
|
|
services:
|
|
core:
|
|
build: { context: ., dockerfile: docker/core.Dockerfile }
|
|
environment:
|
|
THT_DATA_ROOT: /data
|
|
SETTINGS_FILE: /data/settings/settings.json
|
|
volumes: ["./deploy/workspaces:/data/workspaces:ro", "thoth_data:/data"]
|
|
frontend:
|
|
build: { context: ., dockerfile: docker/frontend.Dockerfile }
|
|
environment: { BACKEND_BASE_URL: /api }
|
|
ports: ["8080:8080"]
|
|
volumes: { thoth_data: {} }
|
|
```
|
|
|
|
- [ ] **Step 4: Run Compose smoke and full layer gates**
|
|
|
|
Run: `./scripts/docker-smoke.sh`
|
|
Expected: both health checks PASS.
|
|
Run: `cd harness && .venv/bin/pytest -q`
|
|
Run: `cd backend && npx vitest run && npx tsc --noEmit -p .`
|
|
Run: `cd frontend && npx vitest run && npx tsc -b`
|
|
Expected: all PASS.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add compose.yaml deploy scripts/docker-smoke.sh README.md
|
|
git commit -m "feat(deploy): add portable external-service stack"
|
|
```
|