# Tenant App Hosting Transition Notes

Date: 2026-03-12

## Goal

Move tenant app hosting off the shared host Docker daemon and onto tenant-local
rootless `containerd` + `buildkitd` + `nerdctl` while keeping the host Docker
daemon for the NanoClaw agent pipeline itself.

## Target Architecture

- Agent containers continue to run via the existing host Docker daemon.
- Tenant-hosted apps run as the tenant Linux user via rootless `containerd`.
- Tenant app images, layers, containers, and runtime state live under the
  tenant home volume at `/home/<tenant>/...`.
- Public ingress remains platform-controlled via Caddy and tenant port ranges.
- NanoClaw keeps a thin IPC surface for app deploy/remove and Caddy updates.
- The runtime contract becomes platform-native rather than Docker Compose-first.

## Runtime Contract

### Workspace files

- Per-app directory: `/workspace/shared/apps/<app>/`
- App spec file: `/workspace/shared/apps/<app>/app.yml`

### App spec

Supported fields for v1:

- `services.<name>.image`
- `services.<name>.build.context`
- `services.<name>.build.dockerfile`
- `services.<name>.internal_port`
- `services.<name>.public`
- `services.<name>.environment`
- `services.<name>.volumes`
- `services.<name>.depends_on`

Behavior:

- Exactly zero or one service may be marked `public: true`.
- All services for an app run in one rootless `nerdctl compose` project.
- Services communicate over a tenant-local rootless bridge network by service
  name.
- The host-exposed port is controlled by the IPC payload and must be within the
  tenant's allocated range.
- Static apps remain file-served by Caddy and do not use `nerdctl`.

### IPC

New messages:

- `app-apply`
- `app-remove`

Legacy `compose-apply` and `compose-remove` may remain in code during migration,
but they are no longer the preferred contract.

## Implementation Steps

1. Enable rootless Podman declaratively in NixOS.
2. Ensure tenant users have lingering enabled so user systemd units survive
   without interactive logins.
3. Allow the `nanoclaw` service user to invoke tenant-local Podman/systemd-user
   operations via narrowly-scoped sudo rules.
4. Add a Podman app-hosting module in NanoClaw:
   - parse and validate `app.yml`
   - build tenant-local images with `podman build`
   - generate Quadlet `.pod` and `.container` units
   - start/stop services via `systemctl --user`
   - report status via `systemctl --user` and `podman ps`
5. Add IPC handlers for `app-apply` and `app-remove`.
6. Update hosting metadata and skill instructions to the new contract.
7. Update MC service inspection to understand Podman-backed tenant services.
8. Add unit tests for parsing, validation, quadlet generation, and command
   generation.
9. Rebuild NixOS, restart NanoClaw if required, provision a fresh tenant, and
   validate an end-to-end deployment.

## Acceptance Criteria

- Podman is installed and configured declaratively on the host.
- A tenant user can run rootless Podman with storage inside `/home/<tenant>`.
- NanoClaw can deploy a proxy app from `app.yml` via `app-apply`.
- A deployed tenant app is reachable through Caddy on
  `https://<subdomain>.<tenant>.devitltd.uk`.
- The host-exposed port is rejected if outside the tenant's allocated range.
- Static hosting still works.
- App removal via `app-remove` stops the app and removes the Caddy route.
- MC reports Podman-backed tenant app status without relying on Docker Compose.
- Tenant app images/layers do not accumulate under `/var/lib/docker`.
- Platform instructions and hosting metadata describe the new workflow.

## Validation Plan

### Automated

- Parse valid and invalid app specs.
- Validate one public service max, unique internal ports, safe volume paths, and
  valid build contexts.
- Generate stable Quadlet pod/container units.
- Generate correct tenant-local Podman/systemd commands.
- Preserve legacy Caddy generation and tenant service storage behavior.

### Manual / end-to-end

1. Provision a fresh non-Discord tenant for testing.
2. Create a minimal Node app and `app.yml` in the tenant workspace.
3. Send an `app-apply` IPC message from a registered test group.
4. Verify:
   - images/layers appear under the tenant home
   - no new tenant app image state appears under `/var/lib/docker`
   - user systemd units are active for the tenant app
   - Caddy routes the public hostname correctly
   - the app responds with expected content
5. Send `app-remove`.
6. Verify the Podman services stop and the public route is removed.

## Test Cases

- Single-service proxy app built from a Dockerfile.
- Single-service static app.
- Multi-service pod spec with one public service and one internal service.
- Rejection when two services are marked public.
- Rejection when a requested public host port is outside the tenant range.
- Rejection when a volume escapes the tenant home.
- Re-deploy of an existing app updates units cleanly.
