Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ node_modules
**/node_modules
ui/.vite
electron/dist
# The server image never builds or ships the Electron desktop workspace. Keep
# it out of the post-install COPY so pnpm does not discover a new eighth
# workspace and auto-install Electron after the cacheable dependency layer.
apps/desktop

# Editor / IDE
.vscode
Expand Down
93 changes: 93 additions & 0 deletions .github/workflows/docker-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: Docker Smoke

on:
workflow_dispatch:
pull_request:
branches: [dev, master]
paths:
- ".github/workflows/docker-smoke.yml"
- "Dockerfile"
- ".dockerignore"
- "docker-compose.yml"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "turbo.json"
- "scripts/docker-runtime-smoke*.mjs"
- "scripts/guardian/**"
- "src/core/paths.ts"
- "src/core/runtime-profile.ts"
- "src/main.ts"
- "src/server/**"
- "src/webui/**"
- "src/workspaces/**"
- "services/uta/**"
- "packages/**"
- "default/**"
push:
# Dev-targeted PRs provide integration coverage. Keep direct push smoke on
# master so routine serial merges do not duplicate the expensive image job.
branches: [master]
paths:
- ".github/workflows/docker-smoke.yml"
- "Dockerfile"
- ".dockerignore"
- "docker-compose.yml"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "turbo.json"
- "scripts/docker-runtime-smoke*.mjs"
- "scripts/guardian/**"
- "src/core/paths.ts"
- "src/core/runtime-profile.ts"
- "src/main.ts"
- "src/server/**"
- "src/webui/**"
- "src/workspaces/**"
- "services/uta/**"
- "packages/**"
- "default/**"

permissions:
contents: read

concurrency:
group: docker-smoke-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
smoke:
runs-on: ubuntu-latest
timeout-minutes: 35
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22

- uses: docker/setup-buildx-action@v3

- name: Build server image
uses: docker/build-push-action@v6
with:
context: .
load: true
tags: openalice:ci
cache-from: type=gha,scope=openalice-docker
cache-to: type=gha,mode=max,scope=openalice-docker

- name: Exercise Guardian, Workspace PTY, and CLI gateway
env:
OPENALICE_DOCKER_SMOKE_LOG_FILE: artifacts/docker-smoke.log
run: node scripts/docker-runtime-smoke.mjs --skip-build --image openalice:ci

- name: Upload failure diagnostics
if: failure()
uses: actions/upload-artifact@v4
with:
name: docker-smoke-logs
path: artifacts/docker-smoke.log
if-no-files-found: ignore
retention-days: 7
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ Add checks according to the touched surface:
| Workspace issues, schedules, headless dispatch | Follow [Workspace issues and scheduling](docs/workspace-issues-and-scheduling.md) |
| Guardian locks, process ownership, takeover | `pnpm test:guardian-recovery`; exercise the real launcher path |
| Desktop, IPC, PTY, managed Pi, shell, packaging | Follow [Managed Workspace runtime](docs/managed-workspace-runtime.md) and run the matching Electron/package smoke |
| Docker/server image, Compose, remote deployment | Follow [Docker deployment](docs/docker-deployment.md) and run `pnpm docker:smoke` |
| Persisted data shape | Add an idempotent migration + spec, register it, then run `pnpm build:migration-index` |
| Onboarding/first run/auth | Use isolated data; exercise dev and packaged onboarding paths where relevant |

Expand Down Expand Up @@ -148,6 +149,8 @@ Read the relevant guide before editing its subsystem:
delivery modes, promotions, external contributions, and risk gates.
- [[docs/managed-workspace-runtime.md]] — [Managed Workspace runtime](docs/managed-workspace-runtime.md): Electron
packaging, managed Pi, PortableGit/Bash, runtime profiles, and Workspace PATH.
- [[docs/docker-deployment.md]] — [Docker deployment](docs/docker-deployment.md): server image topology,
remote-host safety, persistence, health, and container acceptance.
- [[docs/workspace-agent-guidance.md]] — [Workspace agent guidance](docs/workspace-agent-guidance.md): prompt
layers, skill ownership, live CLI authority, and guidance versioning.
- [[docs/workspace-lifecycle.md]] — [Workspace and Session lifecycle](docs/workspace-lifecycle.md): offboarding,
Expand Down
47 changes: 31 additions & 16 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ WORKDIR /src

# pnpm via corepack (ships with Node 22). Pin the version we develop with
# so the install plan is reproducible.
RUN corepack enable && corepack prepare pnpm@10.29.2 --activate
RUN corepack enable && corepack prepare pnpm@11.7.0 --activate

# Cache-friendly: copy only manifests first so the dep-resolution layer
# stays warm across source-only changes. `scripts/` joins this layer
# stays warm across source-only changes. The postinstall helper joins this layer
# because the root postinstall hook (`fix-pty-perms.mjs`) runs at the end
# of `pnpm install` and must already exist on disk.
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./
COPY scripts ./scripts
COPY scripts/fix-pty-perms.mjs ./scripts/fix-pty-perms.mjs
COPY packages/guardian-runtime/package.json packages/guardian-runtime/
COPY packages/ibkr/package.json packages/ibkr/
COPY packages/opentypebb/package.json packages/opentypebb/
Expand All @@ -31,16 +31,12 @@ COPY ui/package.json ui/

RUN pnpm install --frozen-lockfile

# Source + build. Mirrors root `pnpm build` (turbo: workspace packages +
# UI Vite build + services/uta, then `tsup` bundles the Alice backend
# into `dist/main.js` — UTA ends up at `services/uta/dist/uta.js`), but
# EXCLUDES the Electron desktop shell: `apps/desktop` matches the
# `apps/*` workspace glob, yet its package.json is deliberately absent
# from the manifest layer above, so its deps (electron, ~500MB) are
# never installed — an unfiltered `turbo run build` would die on TS2307
# "Cannot find module 'electron'". The server image never needs it.
# Source + build. Mirrors root `pnpm build` (turbo: workspace packages + UI
# Vite build + services/uta, then `tsup` bundles Alice into `dist/main.js`).
# `.dockerignore` removes `apps/desktop`, so Electron is not a discovered
# workspace and cannot trigger a late dependency install in this server build.
COPY . .
RUN pnpm exec turbo run build --filter='!@traderalice/desktop' \
RUN pnpm exec turbo run build \
&& pnpm exec tsup src/main.ts --format esm --dts

# Strip dev deps (typescript, turbo, vitest, vite, …) before the runtime
Expand Down Expand Up @@ -71,11 +67,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
# Two agent CLIs installed globally so they're on PATH for the PTY
# sessions OpenAlice spawns. Both come from npm (codex's npm package is
# a thin wrapper that pulls down the Rust binary on install).
# Smoke-checking versions at build time fails the build loud if either
# package broke.
# Keep these explicit: an unchanged Dockerfile layer must resolve to the same
# runtime instead of silently changing when an upstream `latest` tag moves.
ARG CLAUDE_CODE_VERSION=2.1.202
ARG CODEX_VERSION=0.144.1
RUN npm install -g \
@anthropic-ai/claude-code \
@openai/codex \
"@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
"@openai/codex@${CODEX_VERSION}" \
&& claude --version \
&& codex --version \
&& npm cache clean --force
Expand All @@ -88,6 +86,18 @@ COPY --from=build /src/services/uta/dist ./services/uta/dist
COPY --from=build /src/ui/dist ./ui/dist
COPY --from=build /src/default ./default
COPY --from=build /src/src/workspaces/templates ./src/workspaces/templates
# Workspace CLI launchers and their sibling payload are runtime resources just
# like templates. Keep the image-owned directory intact for `cliBinPath()`, and
# install the same self-contained launcher set into /usr/local/bin. Debian login
# shells reset PATH, so ENV alone would make these commands disappear from the
# actual Workspace terminal. Installing only the launchers would also break
# their sibling `openalice-cli.cjs` lookup.
COPY --from=build /src/src/workspaces/cli/bin ./src/workspaces/cli/bin
RUN install -m 0755 /app/src/workspaces/cli/bin/alice /usr/local/bin/alice \
&& install -m 0755 /app/src/workspaces/cli/bin/alice-uta /usr/local/bin/alice-uta \
&& install -m 0755 /app/src/workspaces/cli/bin/alice-workspace /usr/local/bin/alice-workspace \
&& install -m 0755 /app/src/workspaces/cli/bin/traderhub /usr/local/bin/traderhub \
&& install -m 0644 /app/src/workspaces/cli/bin/openalice-cli.cjs /usr/local/bin/openalice-cli.cjs
# tsup bundles backend deps into the entry files where possible, but
# native modules (node-pty, longbridge, etc.) stay as runtime requires.
COPY --from=build /src/node_modules ./node_modules
Expand Down Expand Up @@ -126,6 +136,11 @@ ENV OPENALICE_APP_HOME=/app \
VOLUME ["/data"]
EXPOSE 47331

# Compose and remote orchestrators can distinguish "container process exists"
# from "Alice HTTP surface is ready" without requiring curl in the slim image.
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD ["node", "-e", "const p=process.env.OPENALICE_WEB_PORT||'47331';fetch('http://127.0.0.1:'+p+'/api/version').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]

# tini handles signal forwarding + zombie reaping; Guardian then spawns
# UTA → Alice and supervises the lifecycle (see scripts/guardian/prod.mjs).
ENTRYPOINT ["/usr/bin/tini", "--"]
Expand Down
8 changes: 8 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ services:
image: openalice:local
container_name: openalice
restart: unless-stopped
stop_grace_period: 30s
ports:
# Web UI. The MCP server (47332) is intentionally NOT exposed — it's
# only consumed by the CLIs running inside this container.
Expand All @@ -32,6 +33,13 @@ services:
# the interactive OAuth flow.
stdin_open: true
tty: true
# Bound Docker's json-file growth on long-running remote hosts. Product
# state remains in /data; these are only container stdout/stderr logs.
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"

volumes:
openalice-data:
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ GitHub navigation.
| [[docs/project-structure.md]] | [Project structure](project-structure.md) | Process boundaries, source ownership, state roots, architectural entry points |
| [[docs/development-workflow.md]] | [Development workflow](development-workflow.md) | Branches, delivery modes, PRs, promotions, external review, risk gates |
| [[docs/managed-workspace-runtime.md]] | [Managed Workspace runtime](managed-workspace-runtime.md) | Electron packaging, managed Pi, PortableGit/Bash, runtime profile, Workspace PATH |
| [[docs/docker-deployment.md]] | [Docker deployment](docker-deployment.md) | Server image topology, remote-host safety, persistence, health, and container acceptance |
| [[docs/workspace-agent-guidance.md]] | [Workspace agent guidance](workspace-agent-guidance.md) | Always-loaded prompt contract, skill ownership, live CLI authority, guidance versioning |
| [[docs/workspace-lifecycle.md]] | [Workspace and Session lifecycle](workspace-lifecycle.md) | Offboarding, departed directories, handoff, restore/purge, Session retirement |
| [[docs/workspace-issues-and-scheduling.md]] | [Workspace issues and scheduling](workspace-issues-and-scheduling.md) | Markdown issue contract, global board, schedule scanner, headless execution, Inbox delivery |
Expand Down
115 changes: 115 additions & 0 deletions docs/docker-deployment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Docker Deployment

This guide owns the OpenAlice server-image contract, Docker Compose lifecycle,
remote-host safety boundary, and container smoke requirements. It complements
[[docs/project-structure.md]] and [[docs/managed-workspace-runtime.md]].

## Topology

The image is the non-Electron production topology:

```text
tini (PID 1)
└── scripts/guardian/prod.mjs
├── Alice HTTP + Workspace process
└── optional UTA process

/app immutable image resources
/data persistent operator state and Workspaces
```

Only Alice's web port `47331` is published. The CLI/MCP gateway and UTA stay on
container loopback. Workspace agents reach Alice through the injected
`alice`, `alice-workspace`, `alice-uta`, and `traderhub` CLI launchers; remote
clients must not expose the internal tool gateway as a replacement API.

The server image installs pinned Claude Code and Codex runtimes. Unlike the
desktop package, it does not currently bundle managed Pi, and it does not
install opencode. Version changes are deliberate Dockerfile changes so a
cached/rebuilt image cannot silently acquire a different native runtime.

## Start and Authenticate

```bash
docker compose up -d --build
docker compose ps
docker compose logs openalice
```

The first boot prints a one-time admin token. Store it in a password manager
and use it on the web login screen. The token hash, sessions, Workspaces,
credentials, reports, and trading state persist in the `openalice-data`
volume. Authenticate the agent runtime you intend to use:

```bash
docker exec -it openalice claude
docker exec -it openalice codex login
```

Never set `OPENALICE_DISABLE_AUTH=1` on a remote deployment. That switch exists
for isolated automated smokes only. Expose port `47331` through HTTPS (for
example Caddy, nginx, Tailscale, or a private tunnel) rather than publishing an
unencrypted public endpoint. Configure `OPENALICE_TRUSTED_PROXIES` only with
the actual proxy peer addresses; an overly broad trusted-proxy range weakens
the localhost/auth boundary.

## Health and Lifecycle

The image healthcheck calls the public `/api/version` route from container
loopback. `docker compose ps` should report `healthy` after Alice is ready.
`stop_grace_period: 30s` gives Guardian time to stop PTYs and UTA before Docker
forces termination. Compose also rotates stdout/stderr logs (`10m`, three
files) so an always-on host does not grow an unbounded Docker json log.

Useful operations:

```bash
docker compose logs --tail=200 -f openalice
docker compose restart openalice
docker compose down
docker compose up -d --build
```

`docker compose down` preserves the named volume. `docker compose down -v` is
a factory reset and permanently removes user data.

## Backup and Restore

Stop the container before taking a filesystem-consistent volume snapshot:

```bash
docker compose stop openalice
docker run --rm \
-v openalice_openalice-data:/data:ro \
-v "$PWD":/backup \
alpine tar -czf /backup/openalice-data.tgz -C /data .
docker compose start openalice
```

Compose derives the volume prefix from the project directory; confirm the real
name with `docker volume ls` before backup. Restore into an empty volume while
OpenAlice is stopped. Treat the archive as sensitive: it can contain sealed
broker credentials, the local sealing key, agent logins, reports, and private
Workspace history.

## Runtime Acceptance

`pnpm docker:smoke` is the local definition of a usable server image. It:

1. builds an isolated, uniquely tagged image;
2. starts it in lite mode with a temporary Docker volume and random host port;
3. waits for Alice HTTP readiness;
4. creates a real Chat Workspace with the shell adapter;
5. opens the real Workspace PTY WebSocket;
6. runs `alice` inside that PTY and requires a live CLI manifest response;
7. offboards the Workspace and removes its container, volume, and owned image.

The smoke uses no AI credential and no broker. It deliberately checks an
observable CLI round trip rather than only asserting that files exist. Docker
build cache is shared infrastructure and is retained; only resources owned by
the smoke are deleted. Use `--keep` or `--keep-image` for investigation.

CI builds with BuildKit's GitHub cache, reuses that caller-owned image with
`--skip-build --image openalice:ci`, and uploads redacted container diagnostics
on failure. The Docker workflow runs for deployment/runtime surfaces on PRs to
`dev` or `master`, and again for matching direct changes on `master`.
1 change: 1 addition & 0 deletions docs/project-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ and persistent-state layout. Update it when a top-level subsystem moves or a
new long-lived process, package, or state root is introduced.

Related guides: [[docs/managed-workspace-runtime.md]],
[[docs/docker-deployment.md]],
[[docs/workspace-lifecycle.md]],
[[docs/workspace-issues-and-scheduling.md]],
[[docs/conversation-provenance.md]], and [[docs/market-data-architecture.md]].
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"scripts": {
"dev": "tsx scripts/guardian/dev.ts",
"dev:onboarding": "tsx scripts/onboarding-test-dev.ts",
"docker:build": "docker build --tag openalice:local .",
"docker:smoke": "node scripts/docker-runtime-smoke.mjs",
"prebuild": "tsx scripts/build-migration-index.ts",
"build": "turbo run build && tsup src/main.ts --format esm --dts",
"build:migration-index": "tsx scripts/build-migration-index.ts",
Expand Down
Loading
Loading