mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 11:33:14 +00:00
feat(deploy): add Sea Haven self-hosted deployment capture
Captures the stock-LangGraph deployment of this fork at Sea Haven: - systemd/open-swe.service: langgraph dev (:2024) + store seed ExecStartPost - seed_store.sh: re-seeds team_settings + user_mappings (in-memory store resets on restart); env-parameterized, no secrets - nginx/openswe.conf: dashboard SPA + scoped /dashboard/api proxy (security boundary; agent API not exposed) - aegra/: deferred self-hosted-runtime alternative (not active on stock) - DEPLOYMENT.md: full runbook (models, build, ingress, OAuth callback) Secrets and internal infra identifiers are intentionally excluded (public fork); real values live in private IT docs.
This commit is contained in:
parent
f6c215fff7
commit
cf57ba7aa5
6 changed files with 252 additions and 0 deletions
99
deploy/seahaven/DEPLOYMENT.md
Normal file
99
deploy/seahaven/DEPLOYMENT.md
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# Sea Haven — Open SWE self-hosted deployment
|
||||
|
||||
How this fork is deployed at Sea Haven. The runtime is the **stock LangGraph dev
|
||||
server** (not the Aegra path — see [Aegra](#aegra-deferred)). Internal addresses,
|
||||
ARNs, and account IDs are shown as `<PLACEHOLDERS>`; the real values live in the
|
||||
private IT docs (Confluence "AWS Architecture Map") — **do not commit them to this
|
||||
public fork.**
|
||||
|
||||
## Topology
|
||||
|
||||
```
|
||||
GitHub / Slack ──▶ hooks.seahavenind.com ──┐
|
||||
│ (public ALB :443, host+path rule)
|
||||
Browser ─────────▶ openswe.seahavenind.com ─┤
|
||||
▼
|
||||
AWS ALB ──(Site-to-Site VPN)──▶ on-prem VM
|
||||
├─ nginx :80 (dashboard SPA + /dashboard/api proxy)
|
||||
└─ langgraph dev :2024 (3+ graphs + FastAPI webapp)
|
||||
└─▶ LangSmith cloud sandbox (build/git/PR)
|
||||
```
|
||||
|
||||
- The VM is **internet-closed**; all inbound rides the existing ALB over the VPN.
|
||||
- **Webhooks** (`hooks.seahavenind.com`) → ALB listener rule scoped to `/webhooks/*`
|
||||
only → VM `:2024`. The unauthenticated LangGraph API (`/threads`, `/runs`,
|
||||
`/assistants`, `/store`) is never path-forwarded.
|
||||
- **Dashboard** (`openswe.seahavenind.com`) → ALB → VM `:80` (nginx). nginx is the
|
||||
security boundary: it serves the static SPA and proxies **only** `/dashboard/api/`
|
||||
to `:2024`; the agent API is not reachable through it.
|
||||
|
||||
## VM components
|
||||
|
||||
| Component | What |
|
||||
|---|---|
|
||||
| `langgraph dev` | systemd `open-swe.service` — `--host 0.0.0.0 --port 2024 --no-browser --no-reload`. In-memory runtime. |
|
||||
| Store seeding | `seed_store.sh` as `ExecStartPost` (re-seeds team settings + user mappings, which the in-memory store loses on restart). |
|
||||
| nginx | `nginx/openswe.conf` — SPA from `/var/www/openswe`, proxy `/dashboard/api/` → `:2024`. |
|
||||
| Postgres | present (was for the Aegra path); unused by the stock in-memory runtime. |
|
||||
| swap | 8 GB swapfile — **required**: the dashboard (`ui/`) Nitro build OOMs on an 8 GB box without it. |
|
||||
|
||||
### Models
|
||||
Model selection is **store-driven**, not env. `LLM_MODEL_ID` is effectively dead for
|
||||
runtime selection; the `team_settings/default` store doc wins (then per-user profile,
|
||||
then per-thread). Defaults seeded by `seed_store.sh`:
|
||||
- builder: `anthropic:claude-opus-4-8` (effort `high`)
|
||||
- reviewer (cross-family): `openai:gpt-5.5` (effort `high`) — `openai:gpt-4.1` is **not**
|
||||
in this fork's `SUPPORTED_MODELS` (`agent/dashboard/options.py`); a raw value is
|
||||
silently rewritten to gpt-5.5. Add it to `SUPPORTED_MODELS` first if you need 4.1.
|
||||
- `analyzer` graph is hardcoded to the code default and ignores team settings.
|
||||
|
||||
## Build & deploy the dashboard (`ui/`)
|
||||
|
||||
`ui/` is a **TanStack Start + Nitro** app (build with `bun`, not plain Vite):
|
||||
|
||||
```bash
|
||||
cd ui
|
||||
export PATH="$HOME/.bun/bin:$PATH"
|
||||
export NODE_OPTIONS=--max-old-space-size=6144 # + the 8 GB swapfile, or the build OOMs
|
||||
bun install
|
||||
bun run build # -> .output/public (static SPA, _shell.html)
|
||||
sudo cp -r .output/public/. /var/www/openswe/ # served by nginx
|
||||
```
|
||||
|
||||
Served as a static SPA (per `ui/vercel.json`); the Nitro `.output/server` is unused.
|
||||
|
||||
## Install / wire-up checklist
|
||||
|
||||
1. App config in `.env` (gitignored — never commit): LLM keys, GitHub App creds,
|
||||
`LANGSMITH_API_KEY*` + `DEFAULT_SANDBOX_SNAPSHOT_ID` (`SANDBOX_TYPE=langsmith` — the
|
||||
only sandbox provider with working in-sandbox git/gh auth), `LANGGRAPH_URL=http://127.0.0.1:2024`,
|
||||
dashboard vars (`DASHBOARD_JWT_SECRET`, `CONFIGURED_ADMINS`, `DASHBOARD_*_URL=https://openswe.seahavenind.com`).
|
||||
2. `systemd/open-swe.service` → `/etc/systemd/system/`, `seed_store.sh` on the VM with
|
||||
`OPENSWE_*` env exported (owner login/email, default repo, model ids).
|
||||
3. `nginx/openswe.conf` → `/etc/nginx/sites-available/openswe`, symlink into
|
||||
`sites-enabled`, remove the default site, `nginx -t && systemctl reload nginx`.
|
||||
4. AWS (real IDs in Confluence): IP target groups → `<VM_LAN_IP>:2024` and `:80`;
|
||||
ALB SG **egress** rules to those ports (the ALB SG is allow-listed — health checks
|
||||
time out without them); `:443` listener rules for the two hostnames; Route53 ALIAS
|
||||
records → ALB. Webhook rule must stay path-scoped to `/webhooks/*`.
|
||||
5. GitHub App: webhook URL `https://hooks.seahavenind.com/webhooks/github`; subscribe to
|
||||
the events the install guide lists (Issue comment, PR review×2, check_run/suite,
|
||||
workflow_run, status) — add **Issues** too if you want issue-title/body triggers.
|
||||
6. **GitHub App OAuth callback (manual, UI-only — not API-settable):**
|
||||
`https://openswe.seahavenind.com/dashboard/api/auth/callback` — without it, dashboard
|
||||
login fails with a `redirect_uri` mismatch.
|
||||
|
||||
## Triggering
|
||||
|
||||
Start a task by mentioning **`@openswe`** in a GitHub issue comment (the documented
|
||||
intake — the `open-swe` *label* path needs the `Issues` event subscription). The
|
||||
commenter must have a `user_mappings` entry or the run is skipped.
|
||||
|
||||
## Aegra (deferred)
|
||||
|
||||
`aegra/aegra.json` + `aegra/aegra_entry.py` are the self-hosted-runtime alternative
|
||||
(Apache-2.0, avoids the LangGraph-Platform Elastic license). Not active on the stock
|
||||
deployment. To use: place both at the repo root, run `aegra serve` (:2026), and point
|
||||
`LANGGRAPH_URL` at `:2026`. Aegra gives a Postgres-backed durable store/checkpointer,
|
||||
which removes the need for `seed_store.sh` and survives restarts (paused HITL
|
||||
interrupts persist).
|
||||
11
deploy/seahaven/aegra/aegra.json
Normal file
11
deploy/seahaven/aegra/aegra.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"agent": "./aegra_entry.py:agent_graph",
|
||||
"reviewer": "./aegra_entry.py:reviewer_graph",
|
||||
"analyzer": "./aegra_entry.py:analyzer_graph"
|
||||
},
|
||||
"http": {
|
||||
"app": "./aegra_entry.py:webapp_app"
|
||||
}
|
||||
}
|
||||
19
deploy/seahaven/aegra/aegra_entry.py
Normal file
19
deploy/seahaven/aegra/aegra_entry.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
"""Aegra entrypoint for Open SWE graphs.
|
||||
|
||||
Aegra loads graph files standalone via importlib.spec_from_file_location, which
|
||||
gives them a synthetic module name and breaks agent/*.py's package-relative
|
||||
imports (e.g. `from .dashboard.admin import ...`). Re-exporting the graphs here
|
||||
through the installed ``agent`` package (absolute imports) restores correct
|
||||
``__package__`` resolution, so the relative imports inside the agent modules work.
|
||||
"""
|
||||
|
||||
from agent.server import traced_agent as agent_graph
|
||||
from agent.reviewer import traced_reviewer_agent as reviewer_graph
|
||||
from agent.analyzer import traced_analyzer as analyzer_graph
|
||||
|
||||
# Open SWE's FastAPI webapp (GitHub/Slack webhooks + dashboard API), mounted by
|
||||
# Aegra via the "http" key in aegra.json. Absolute import for the same reason as
|
||||
# the graphs above (relative imports break under Aegra's standalone file loader).
|
||||
from agent.webapp import app as webapp_app
|
||||
|
||||
__all__ = ["agent_graph", "reviewer_graph", "analyzer_graph", "webapp_app"]
|
||||
32
deploy/seahaven/nginx/openswe.conf
Normal file
32
deploy/seahaven/nginx/openswe.conf
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Open SWE dashboard frontend (TanStack Start SPA) + scoped API proxy.
|
||||
# nginx is the security boundary: ONLY /dashboard/api/* reaches the backend;
|
||||
# the unauthenticated LangGraph agent API (/threads,/runs,/assistants,/store) is NOT proxied.
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name openswe.seahavenind.com;
|
||||
|
||||
root /var/www/openswe;
|
||||
index _shell.html;
|
||||
|
||||
# ALB health check
|
||||
location = /healthz { default_type text/plain; return 200 "ok\n"; }
|
||||
|
||||
# Dashboard API + OAuth callback -> backend webapp on :2024 (the ONLY proxied path)
|
||||
location /dashboard/api/ {
|
||||
proxy_pass http://127.0.0.1:2024;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
|
||||
# Static assets + SPA shell fallback (client-side routing)
|
||||
location / {
|
||||
try_files $uri $uri/ /_shell.html;
|
||||
}
|
||||
}
|
||||
74
deploy/seahaven/seed_store.sh
Executable file
74
deploy/seahaven/seed_store.sh
Executable file
|
|
@ -0,0 +1,74 @@
|
|||
#!/usr/bin/env bash
|
||||
# Seed the LangGraph store after a (re)start.
|
||||
#
|
||||
# The stock `langgraph dev` server uses an IN-MEMORY store, so anything written
|
||||
# to it (team model settings, user mappings) is lost on every restart. This
|
||||
# script idempotently re-PUTs that state and is wired as a systemd
|
||||
# ExecStartPost on the open-swe.service unit so it runs after each start.
|
||||
#
|
||||
# Replace this with Postgres-backed durability (Aegra / `langgraph up`) to make
|
||||
# the store survive restarts and drop this script.
|
||||
#
|
||||
# Configuration comes from the environment (set these in the service env or a
|
||||
# sourced file alongside the app .env) — no real values are committed here:
|
||||
# OPENSWE_BASE_URL default http://127.0.0.1:2024
|
||||
# OPENSWE_AGENT_MODEL default anthropic:claude-opus-4-8
|
||||
# OPENSWE_AGENT_EFFORT default high
|
||||
# OPENSWE_REVIEWER_MODEL default openai:gpt-5.5
|
||||
# OPENSWE_REVIEWER_EFFORT default high
|
||||
# OPENSWE_DEFAULT_REPO e.g. your-org/your-pilot-repo (required)
|
||||
# OPENSWE_OWNER_LOGIN GitHub login of the triggering owner (required)
|
||||
# OPENSWE_OWNER_EMAIL work email mapped to that login (required)
|
||||
set -euo pipefail
|
||||
|
||||
BASE="${OPENSWE_BASE_URL:-http://127.0.0.1:2024}"
|
||||
AGENT_MODEL="${OPENSWE_AGENT_MODEL:-anthropic:claude-opus-4-8}"
|
||||
AGENT_EFFORT="${OPENSWE_AGENT_EFFORT:-high}"
|
||||
REVIEWER_MODEL="${OPENSWE_REVIEWER_MODEL:-openai:gpt-5.5}"
|
||||
REVIEWER_EFFORT="${OPENSWE_REVIEWER_EFFORT:-high}"
|
||||
DEFAULT_REPO="${OPENSWE_DEFAULT_REPO:?set OPENSWE_DEFAULT_REPO=owner/repo}"
|
||||
OWNER_LOGIN="${OPENSWE_OWNER_LOGIN:?set OPENSWE_OWNER_LOGIN=github-login}"
|
||||
OWNER_EMAIL="${OPENSWE_OWNER_EMAIL:?set OPENSWE_OWNER_EMAIL=work-email}"
|
||||
NOW="$(date -u +%Y-%m-%dT%H:%M:%S+00:00)"
|
||||
|
||||
# Wait for the server to accept requests (up to ~60s).
|
||||
for _ in $(seq 1 30); do
|
||||
[ "$(curl -s -o /dev/null -w '%{http_code}' "$BASE/ok" || true)" = "200" ] && break
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# 1) team_settings/default — builder + reviewer models (NOT read from env by the
|
||||
# app; the store value wins over LLM_MODEL_ID). gpt-4.1 is NOT in this fork's
|
||||
# SUPPORTED_MODELS, so the reviewer uses gpt-5.5 (cross-family vs the builder).
|
||||
curl -s -X PUT "$BASE/store/items" -H "Content-Type: application/json" -d @- <<JSON
|
||||
{"namespace":["team_settings"],"key":"default","value":{
|
||||
"review_draft_prs": false,
|
||||
"pr_summaries": true,
|
||||
"review_trace_links": true,
|
||||
"org_guidelines": null,
|
||||
"default_agent_model": "$AGENT_MODEL",
|
||||
"default_agent_reasoning_effort": "$AGENT_EFFORT",
|
||||
"default_agent_subagent_model": "$AGENT_MODEL",
|
||||
"default_agent_subagent_reasoning_effort": "$AGENT_EFFORT",
|
||||
"default_repo": "$DEFAULT_REPO",
|
||||
"default_reviewer_model": "$REVIEWER_MODEL",
|
||||
"default_reviewer_reasoning_effort": "$REVIEWER_EFFORT",
|
||||
"default_reviewer_subagent_model": "$REVIEWER_MODEL",
|
||||
"default_reviewer_subagent_reasoning_effort": "$REVIEWER_EFFORT",
|
||||
"default_grouping_model": null,
|
||||
"default_grouping_reasoning_effort": null,
|
||||
"default_chat_model": null,
|
||||
"default_chat_reasoning_effort": null,
|
||||
"updated_at": "$NOW"
|
||||
}}
|
||||
JSON
|
||||
|
||||
# 2) user_mappings/<login> — required, or the @openswe trigger ignores the commenter.
|
||||
curl -s -X PUT "$BASE/store/items" -H "Content-Type: application/json" -d @- <<JSON
|
||||
{"namespace":["user_mappings"],"key":"$OWNER_LOGIN","value":{
|
||||
"github_login":"$OWNER_LOGIN","work_email":"$OWNER_EMAIL","slack_user_id":null,
|
||||
"source":"slack_oauth","status":"active","created_at":"$NOW","updated_at":"$NOW"
|
||||
}}
|
||||
JSON
|
||||
|
||||
echo "seed_store: done at $NOW"
|
||||
17
deploy/seahaven/systemd/open-swe.service
Normal file
17
deploy/seahaven/systemd/open-swe.service
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[Unit]
|
||||
Description=Open SWE stock LangGraph dev server (graphs + webapp, :2024)
|
||||
After=network-online.target postgresql.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=adam
|
||||
WorkingDirectory=/home/adam/open-swe
|
||||
ExecStart=/home/adam/open-swe/.venv/bin/langgraph dev --host 0.0.0.0 --port 2024 --no-browser --no-reload
|
||||
ExecStartPost=/home/adam/open-swe/seed_store.sh
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
TimeoutStartSec=120
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Loading…
Add table
Reference in a new issue