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
43 changes: 36 additions & 7 deletions .github/workflows/parsar-server-release.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
# Build + push the parsar-server image to GitHub Container Registry.
# Build the parsar-server image in CI and push releases to GHCR.
#
# This is the Phase 1 deliverable's "one command" enabler: docker-compose.local.yml
# (and the Phase 2 self-host compose) pull ghcr.io/<owner>/parsar-server by
# default. Same image, two compose profiles — profile not fork.
#
# Triggers:
# - Pull request touching server/web/build inputs -> build only, no push
# - Push to main touching server/web/build inputs -> :latest + :<sha>
# - Tag push matching `parsar-server-v*` -> :<release-tag>
# - Manual workflow_dispatch
# - Manual workflow_dispatch -> build + push
#
# Output: ghcr.io/${{ github.repository_owner }}/parsar-server:<tag>
# Output: ghcr.io/<owner-lowercase>/parsar-server:<tag>
# - latest on default-branch pushes
# - <short-sha> on every build
# - <release-tag> on tag pushes (e.g. parsar-server-v0.1.0)
Expand All @@ -18,7 +19,7 @@
# `images:` field, or just override PARSAR_SERVER_IMAGE in the compose .env
# and never run this workflow.

name: parsar-server image release
name: parsar-server image

on:
push:
Expand All @@ -36,11 +37,32 @@ on:
- "go.mod"
- "go.sum"
- "go.work"
- "package.json"
- "pnpm-workspace.yaml"
- "pnpm-lock.yaml"
- "docs/openapi/openapi.yaml"
- ".dockerignore"
- "Dockerfile"
- ".github/workflows/parsar-server-release.yml"
tags:
- "parsar-server-v*"
pull_request:
paths:
- "server/**"
- "internal/**"
- "apps/web/**"
- "apps/parsar-daemon/**"
- "packages/**"
- "go.mod"
- "go.sum"
- "go.work"
- "package.json"
- "pnpm-workspace.yaml"
- "pnpm-lock.yaml"
- "docs/openapi/openapi.yaml"
- ".dockerignore"
- "Dockerfile"
- ".github/workflows/parsar-server-release.yml"
workflow_dispatch:

permissions:
Expand All @@ -60,7 +82,13 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Compute lowercase GHCR image name
id: image
shell: bash
run: echo "name=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/parsar-server" >> "$GITHUB_OUTPUT"

- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v4
with:
registry: ghcr.io
Expand All @@ -71,20 +99,21 @@ jobs:
id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository_owner }}/parsar-server
images: ${{ steps.image.outputs.name }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=ref,event=tag
type=sha,format=short
type=raw,value=latest,enable={{is_default_branch}}

- name: Build and push
- name: Build image
uses: docker/build-push-action@v7
with:
context: .
file: Dockerfile
platforms: linux/amd64,linux/arm64
push: true
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
Expand Down
9 changes: 7 additions & 2 deletions .github/workflows/sandbox-image-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# - Tag push matching `sandbox-v*` (release tags)
# - Manual workflow_dispatch
#
# Output: ghcr.io/${{ github.repository_owner }}/parsar-sandbox:<tag>
# Output: ghcr.io/<owner-lowercase>/parsar-sandbox:<tag>
# - `latest` on main pushes
# - `<sha>` short-SHA tag on every build
# - `<release-tag>` on tag pushes (e.g. `sandbox-v0.0.1`)
Expand Down Expand Up @@ -62,6 +62,11 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Compute lowercase GHCR image name
id: image
shell: bash
run: echo "name=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/parsar-sandbox" >> "$GITHUB_OUTPUT"

- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
Expand All @@ -73,7 +78,7 @@ jobs:
id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository_owner }}/parsar-sandbox
images: ${{ steps.image.outputs.name }}
# latest on main, sha for every push, release tag on tag push
tags: |
type=ref,event=branch
Expand Down
14 changes: 8 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ ARG NODE_VERSION=22-alpine
ARG GO_VERSION=1.25-bookworm
ARG RUNTIME_BASE=debian:bookworm-slim

FROM node:${NODE_VERSION} AS web-builder
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION} AS web-builder
ENV PNPM_HOME=/pnpm
ENV PATH=/pnpm:$PATH
RUN corepack enable && corepack prepare pnpm@10.30.3 --activate
Expand Down Expand Up @@ -74,7 +74,9 @@ RUN pnpm --filter @parsar/web build
# minimal runtime. trimpath strips build-host file paths from the
# binary (defence-in-depth against operator info leaks).
###############################################################################
FROM golang:${GO_VERSION} AS go-builder
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS go-builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /src

# Module graph first, source second — keeps `go mod download` cacheable.
Expand All @@ -92,11 +94,11 @@ COPY server ./server
# combined they shave ~25% off binary size with no runtime cost.
RUN cd server \
&& mkdir -p /out \
&& CGO_ENABLED=0 GOFLAGS=-trimpath go build -ldflags="-s -w" \
&& CGO_ENABLED=0 GOOS="$TARGETOS" GOARCH="$TARGETARCH" GOFLAGS=-trimpath go build -ldflags="-s -w" \
-o /out/parsar-server ./cmd/server \
&& CGO_ENABLED=0 GOFLAGS=-trimpath go build -ldflags="-s -w" \
&& CGO_ENABLED=0 GOOS="$TARGETOS" GOARCH="$TARGETARCH" GOFLAGS=-trimpath go build -ldflags="-s -w" \
-o /out/parsar-migrate ./cmd/migrate \
&& CGO_ENABLED=0 GOFLAGS=-trimpath go build -ldflags="-s -w" \
&& CGO_ENABLED=0 GOOS="$TARGETOS" GOARCH="$TARGETARCH" GOFLAGS=-trimpath go build -ldflags="-s -w" \
-o /out/parsar-bootstrap ./cmd/parsar-bootstrap

# parsar-daemon source (root module, no separate go.mod). Copied after the
Expand Down Expand Up @@ -127,7 +129,7 @@ RUN mkdir -p /out/daemon \
# - The opencode local runner may shell out (rg, basic core utils);
# keeping a real userland avoids surprises.
###############################################################################
FROM ${RUNTIME_BASE} AS runtime
FROM --platform=$TARGETPLATFORM ${RUNTIME_BASE} AS runtime

ARG PARSAR_USER=parsar
ARG PARSAR_UID=10001
Expand Down
177 changes: 52 additions & 125 deletions INSTALL.md
Original file line number Diff line number Diff line change
@@ -1,158 +1,85 @@
# INSTALL Parsar Local Quickstart
# INSTALL - Parsar Local Quickstart

> **Audience:** developers and AI coding agents (Claude Code, Cursor, Codex, etc.) who want to run Parsar on their own machine or evaluate it.
> **What you get:** a working Parsar in a few minutes (mock login, no Feishu or secrets required), with your local Claude Code / OpenCode / Codex paired in as an **online device** — the entire flow is "copy one command from the browser → paste in a terminal → device online".
> **Not covered here:** self-hosting / production deploys (real Feishu OIDC, custom ports and secrets, bootstrap tokens) live on a separate path — see `deploy/compose/compose.selfhost.yml` and `docs/deploy/`.
## One-command install

---

## Overview

```
clone → (pull/build image) → docker compose up → log in via browser → pair device → device online
```

The local stack is all-in-one: `postgres` + a one-shot `init` (migrate + first owner) + `parsar-server` (mock login). **Profile not fork:** it uses the **same image / migrations / SPA / install.sh** as the self-host edition; only the compose file and env values differ.

---

## 0 · Prerequisites
Parsar can be installed without cloning the repository. The installer writes
all generated config, secrets, database files, and runtime state under
`~/.parsar/`.

```bash
docker compose version # v2+ required
claude --version || opencode --version || codex --version # at least one, and logged in
curl -fsSL https://raw.githubusercontent.com/MiniMax-AI-Dev/parsar/main/install.sh | bash
```

- **Docker** installed and running (on macOS open Docker Desktop first so `docker info` shows the server section).
- At least one Agent CLI (Claude Code / OpenCode / Codex) installed and **logged in** on this machine. The daemon does not embed any model login of its own; it reuses this CLI's login state and subscription, and it can see the real repositories on your host.
When it finishes, open:

---

## 1 · Clone the repo

```bash
git clone <your-repo-url> parsar
cd parsar
# The local-edition deliverable currently lives on feature/deliverables-design;
# skip this step once it lands on the main branch and use the default branch.
git checkout feature/deliverables-design
```text
http://127.0.0.1:18080
```

---
Create the first owner account in the web setup flow. The first registered
user is the administrator.

## 2 · Pick an image and set env vars
## Requirements

By default the next step's `docker compose` pulls a prebuilt image from GHCR — no manual pull needed:
- Docker Engine with Docker Compose v2.
- Linux for Docker-managed agent sandboxes. Non-Linux hosts can still start
the web control plane, but managed Docker sandboxes are disabled by default.

```bash
# Default ports are 18080 / 15432; change these if they clash (e.g. 18088 / 15488)
export PARSAR_LOCAL_PORT=18080
export PARSAR_PG_PORT=15432
```
## What The Installer Does

> **Current status (remove this note once the image is published to GHCR):**
> The key that makes device pairing zero-config offline — the **four platform daemon binaries baked into the image** — has not yet been published alongside the image on GHCR. Until it is, pulling the GHCR image directly gives you an image **without** the embedded daemon, so **step 4 will fail (404) when it tries to download the daemon**. In the meantime, build locally as a fallback:
> ```bash
> make docker-build PARSAR_IMAGE=parsar PARSAR_IMAGE_TAG=local # first build takes ~5–10 min
> export PARSAR_SERVER_IMAGE=parsar:local
> ```
> Verify the built image really contains the daemon:
> ```bash
> docker run --rm --entrypoint ls parsar:local /usr/local/share/parsar/daemon
> # Expected: four files parsar-daemon-{darwin,linux}-{amd64,arm64}
> ```
> Once the image is published to GHCR this note (and the local-build step above) go away — just skip to step 3 and use the default image.
The script:

---
- creates `~/.parsar/compose.yml`
- creates `~/.parsar/.env`
- generates and persists `PARSAR_MASTER_KEY`
- generates and persists the Postgres password
- starts Postgres, runs migrations, and starts `parsar-server`
- leaves Feishu, Slack, and Discord integrations disabled unless explicitly
enabled in `~/.parsar/.env`

## 3 · Bring the local stack up with one command
It does not write runtime state into the repository or the current working
directory.

```bash
docker compose -f docker-compose.local.yml up -d
```
## Common Options

The first run pulls / uses the `parsar-server` image, runs the database migrations, and provisions the first workspace automatically (owner = the mock identity `admin@example.com`).
```bash
# Use a different web port.
curl -fsSL https://raw.githubusercontent.com/MiniMax-AI-Dev/parsar/main/install.sh \
| bash -s -- --port 18088

**Expected:** three containers; `parsar-local-server` and `parsar-local-postgres` become healthy within ~15 seconds; `parsar-local-init` exits after it finishes.
# Use a pinned image.
curl -fsSL https://raw.githubusercontent.com/MiniMax-AI-Dev/parsar/main/install.sh \
| bash -s -- --image ghcr.io/minimax-ai-dev/parsar-server:v0.1.0

**Verify:**
```bash
docker compose -f docker-compose.local.yml ps
docker inspect parsar-local-init --format 'init exit={{.State.ExitCode}}'
# Expected: 0; a repeated `up` still shows 0 (parsar-bootstrap exits 2 when
# already bootstrapped, and compose collapses that back to 0 via `|| [ $? -eq 2 ]`)
# Start without Docker-managed agent sandboxes.
curl -fsSL https://raw.githubusercontent.com/MiniMax-AI-Dev/parsar/main/install.sh \
| bash -s -- --no-sandbox
```

Quick end-to-end sanity check from the CLI (30 seconds before you touch the browser):
For local development from a checkout:

```bash
B="http://127.0.0.1:${PARSAR_LOCAL_PORT}"
for p in /healthz /api/v1/health / ; do
printf '%-18s -> %s\n' "$p" "$(curl -fsS -o /dev/null -w '%{http_code}' "$B$p")"
done
curl -fsS "$B/api/v1/bootstrap/status"; echo
```
**Expected:**
make docker-build
./install.sh --image parsar:dev
```
/healthz -> 200
/api/v1/health -> 200
/ -> 200
{"needed":false,"has_owners":true,"owner_count":1, ...}
```

---

## 4 · Browser E2E acceptance (north star: pair your device)

1. Open **http://127.0.0.1:18080** in your browser (use your own port if you changed it).
2. Click **Log in** (the button may read "Feishu login"; mock mode needs no username / password, it goes straight through) — identity `admin@example.com`, landing on `Local Workspace`.
3. Go to **Devices / runtime management** → click **"Pair new device"** → enter a device name → click **"Generate connect command"**.
4. Copy the **single** command in the dialog → paste it into **another terminal on the same host**. The command looks like:
```bash
curl -fsSL http://127.0.0.1:18080/api/v1/parsar-daemon/install.sh \
| PARSAR_DAEMON_CONNECT_URL=http://127.0.0.1:18080 \
PARSAR_DAEMON_CONNECT_TOKEN=<one-shot token> \
PARSAR_DAEMON_CONNECT_DEVICE_NAME=<device name> bash
```
- The server URL is filled in automatically from `PARSAR_PUBLIC_URL`, **do not edit it by hand**.
- The script: downloads the platform-appropriate daemon from your server → `chmod` → runs `connect` in the background; the token is passed via env vars rather than argv, so it never appears in `ps` output or access logs. You **never touch** the binary, its path, or the token.
5. Within a few seconds the device flips from `pending_pairing` → **"online"**. **E2E passed.**
If you want to confirm the device can actually do work: create an Agent and run through one issue.

---

## 5 · Stop / clean up
## Manage The Stack

```bash
docker compose -f docker-compose.local.yml down # stop, keep the data volume
docker compose -f docker-compose.local.yml down -v # also drop the Postgres data
docker compose -f ~/.parsar/compose.yml --env-file ~/.parsar/.env ps
docker compose -f ~/.parsar/compose.yml --env-file ~/.parsar/.env logs -f parsar-server
docker compose -f ~/.parsar/compose.yml --env-file ~/.parsar/.env down
```

---

## Troubleshooting
To remove all local data, stop the stack first and then delete `~/.parsar/`.

| Symptom | Cause | Fix |
|---|---|---|
| Device pairing downloads daemon and hits **404** | The GHCR image you pulled does not contain the embedded daemon (not published yet) | Use the local build from step 2: `export PARSAR_SERVER_IMAGE=parsar:local` then `up -d --force-recreate` |
| Port reports `address already in use` | 18080 / 15432 is taken | Change `PARSAR_LOCAL_PORT` / `PARSAR_PG_PORT` and `up -d` again |
| `server` crash-loops with `agent_daemon owner URL not resolvable` | Missing `PARSAR_AGENT_DAEMON_OWNER_URL` (the local compose already defaults it to `http://parsar-server:8080`) | If you edited / removed that env, restore it |
| `server` stays unhealthy but is actually reachable | The image's built-in HEALTHCHECK uses HEAD, but `/healthz` only accepts GET (the local compose overrides the probe to GET) | If you edited compose, confirm the healthcheck uses GET |
| Device is online but no `kind` is available when creating an Agent | No Agent CLI is installed / logged in on this host | Log into `claude` / `opencode` / `codex` on this machine — the daemon picks it up automatically |
| macOS `exec format error` | Built on x86 but running on Apple Silicon | Re-run `make docker-build` locally to build for the native architecture |
## Advanced Configuration

Logs: `docker logs -f parsar-local-server`

---

## TL;DR
Edit `~/.parsar/.env` and rerun:

```bash
git clone <repo> parsar && cd parsar && git checkout feature/deliverables-design
export PARSAR_LOCAL_PORT=18080 PARSAR_PG_PORT=15432
# Current phase (GHCR has no image with the embedded daemon yet) → build locally;
# once published these two lines can go and the default image works.
make docker-build PARSAR_IMAGE=parsar PARSAR_IMAGE_TAG=local
export PARSAR_SERVER_IMAGE=parsar:local
docker compose -f docker-compose.local.yml up -d
# Browser http://127.0.0.1:18080 → log in → Pair new device → copy command → run in another terminal → device online
curl -fsSL https://raw.githubusercontent.com/MiniMax-AI-Dev/parsar/main/install.sh | bash
```

The installer preserves generated secrets and refreshes non-secret settings
such as ports, image names, and paths.
Loading