# resilience-agents-platform

Single-container deployment of the coding-agent platform:

- **usher** — web router over Claude Code / Codex sessions
- **usher-lark** — Lark/Feishu plugin (mirrors sessions into a group chat)
- **code-server** — VS Code in the browser, sharing the same filesystem

## What runs on it

- [`alert-triage-adapter/`](alert-triage-adapter/) — turns an alert dispatched
  by alertstore into a usher session, and lets the agent report its verdict
  back. Not in the image yet: it is built as a static binary and run off the
  persistent `$HOME` while the prompt is still being tuned.

## Build

The Docker build clones usher from upstream (`github.com/nexustar/usher`).
Extra `docker build` flags go through `MAKE_VARS`.

```bash
make docker-build
# pin a usher version:
make docker-build IMG=rap:v1 MAKE_VARS="--build-arg USHER_REF=v0.1.0"
# bake a different default timezone:
make docker-build MAKE_VARS="--build-arg TZ=Asia/Shanghai"
```

| Build arg | Default | Description |
|---|---|---|
| `USHER_REPO` | upstream URL | usher source repository |
| `USHER_REF` | `main` | branch/tag/commit to build |
| `TZ` | `UTC` | baked into `/etc/localtime`; overridable at runtime |
| `NODE_VERSION` | `24.19.0` | Node LTS installed to `/usr/local` |
| `NODE_SHA256` | — | checksum of the matching `linux-x64` tarball |
| `S6_OVERLAY_VERSION` | `3.2.0.2` | s6-overlay release |

Bumping Node means bumping both args together; get the checksum from
`https://nodejs.org/dist/v<version>/SHASUMS256.txt`.

## Node.js

Node comes from the official tarball, not Debian's `nodejs` package (trixie
ships 20.19, EOL since 2026-04-30). `corepack enable` is run at build time, so
`pnpm` and `yarn` are on `PATH` alongside `npm`.

There is intentionally no `nvm`/`fnm`: usher is launched by s6 from `/bin/sh`
with no login shell, and finds the agent CLIs via `exec.LookPath`. A version
manager that injects `PATH` from a shell hook would be invisible to the one
process that has to see it. If per-project versions are ever needed, use a
shim-based manager (`mise`), not a shell-hook one.

Note that `npm i -g` writes to `/usr/local/lib/node_modules`, which is in the
image, not on the `$HOME` volume — global installs do not survive a pod
restart.

## Environment variables

| Variable | Required | Description |
|---|---|---|
| `LARK_APP_ID` | no | enables usher-lark when set with the two below |
| `LARK_APP_SECRET` | no | Lark app secret |
| `LARK_CHAT_ID` | no | Lark group chat to mirror sessions into |
| `USHER_ARGS` | no | extra flags appended to `usher serve` |
| `USHER_LARK_ARGS` | no | extra flags appended to `usher-lark` |
| `TZ` | no | overrides the baked-in timezone (glibc reads it directly) |
| `LANG` | no | defaults to `en_US.UTF-8`; `zh_CN.UTF-8` is also generated |

`usher-lark` defaults `--default-cwd` to `/tmp`, which is rarely what you want
on a box with a persistent `$HOME`. Point it somewhere real:

```
USHER_LARK_ARGS=--default-cwd=/home/dev/work --default-agent=claude
```

`ANTHROPIC_API_KEY` and other credentials are managed on the persistent
`$HOME` volume — place them in `~/.config/` or shell profile manually.

## Container runtime

The image ships `podman` (rootless, with `uidmap`/`fuse-overlayfs`/`passt`)
and `bubblewrap` for sandboxing agent-run commands. Both need unprivileged
user namespaces, which the pod must actually permit:

- the `dev` user's subuid/subgid ranges are baked in at `/etc/subuid`
- `/etc/containers/storage.conf` forces `fuse-overlayfs`, since nested kernel
  overlay needs privileges the pod does not have
- the pod's seccomp/AppArmor profile must allow `unshare(CLONE_NEWUSER)`;
  on a default-restricted cluster this needs a securityContext exception

Rootless podman's graphroot lives under `$HOME`, so pulled images persist
across pod restarts along with the rest of the volume.
