feat(deploy): trigger service deploys to the redroid box (systemd), not plum (launchd)
Per the corrected .infra.yaml, the trigger service is a PROD unit that runs ON the redroid box (next to the Android container → local adb), with plum as dev-only. Replace the (wrong) macOS LaunchAgent with the box-native pattern, mirroring @redroid's deploy-droplet.sh / mrnumber-ocr.service: - deploy/mr-number-service.service: systemd unit (multi-user.target, EnvironmentFile for tokens, MR_NUMBER_DEVICE=localhost:5555, __BUN__ resolved at deploy time). - deploy/deploy-service.sh: prereq-checks the box (bun/python3/redroid_client/adb), scp's service+client to /opt/mr-number-service, installs the unit, seeds a 0600 env template, enables only once tokens are filled (no crashloop). Does NOT mint tokens. - service/run: drop launchd installer; plain dev launcher (loads cocotte-secrets). - install.sh: plum = dev setup only; points prod at deploy-service.sh. - CLAUDE.md/README: box-deploy ownership split (@redroid owns the box; we own our unit). Syntax-checked; box SSH (:22) unreachable from this env so not yet run against the box. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
afe2917a52
commit
c5c124faa1
6 changed files with 142 additions and 85 deletions
12
CLAUDE.md
12
CLAUDE.md
|
|
@ -44,9 +44,17 @@ Use "We…" / "The collective…". Never "I'll…" / "Let me…" / "You're absol
|
||||||
service that exposes this lives **here** (see README "Trigger service"). The persons
|
service that exposes this lives **here** (see README "Trigger service"). The persons
|
||||||
DB / signal model lives in cocotte (`@platforms/cocottetech`); the prospect gate
|
DB / signal model lives in cocotte (`@platforms/cocottetech`); the prospect gate
|
||||||
lives in **Prospector** (`@applications/prospector`). Never POST to quinn.api.
|
lives in **Prospector** (`@applications/prospector`). Never POST to quinn.api.
|
||||||
|
- **Deploy target = the redroid box (prod), plum = dev only.** Per `.infra.yaml`, the
|
||||||
|
deployable unit is the **trigger service** (`service/`), which runs as the systemd unit
|
||||||
|
`mr-number-service` ON the box (next to the Android container, so adb is local).
|
||||||
|
Deploy it with `deploy/deploy-service.sh` (scp to `/opt/mr-number-service` + install the
|
||||||
|
unit). The plum/fennel side (`client/` lookup + console-tray + stdio MCP) is dev/setup
|
||||||
|
tooling, NOT a deploy target.
|
||||||
- **IaC / box ownership:** the redroid droplet's canonical Terraform lives in
|
- **IaC / box ownership:** the redroid droplet's canonical Terraform lives in
|
||||||
`~/Code/@projects/uvlava/terraform/do/`; the box-side services + deploy live in the
|
`~/Code/@projects/uvlava/terraform/do/`; the **box itself** + the shared on-box services
|
||||||
**`@redroid`** app. NOT here. Never `terraform apply` or deploy the box from this repo.
|
(adb-keyboard, ocr-service, ws-scrcpy, the Android emulator) are owned by the **`@redroid`**
|
||||||
|
app. This repo owns only its own `mr-number-service` unit on that box. Never `terraform
|
||||||
|
apply` the box or deploy @redroid's services from here.
|
||||||
- **Secrets:** flat 0600 files on plum — `~/.config/cocotte-secrets/people-service.token`
|
- **Secrets:** flat 0600 files on plum — `~/.config/cocotte-secrets/people-service.token`
|
||||||
(people signal recording) and `~/.config/cocotte-secrets/mr-number.service-token`
|
(people signal recording) and `~/.config/cocotte-secrets/mr-number.service-token`
|
||||||
(inbound trigger auth). SSH key for the droplet console: `~/.ssh/id_ed25519_1984`.
|
(inbound trigger auth). SSH key for the droplet console: `~/.ssh/id_ed25519_1984`.
|
||||||
|
|
|
||||||
13
README.md
13
README.md
|
|
@ -100,10 +100,17 @@ The inbound HTTP surface Prospector calls. Durable SQLite queue + a single seria
|
||||||
worker (one Android box) that drains by invoking `mr_lookup.py`; the verdict lands as
|
worker (one Android box) that drains by invoking `mr_lookup.py`; the verdict lands as
|
||||||
the people-service signal.
|
the people-service signal.
|
||||||
|
|
||||||
|
**Prod: runs ON the redroid box** (systemd unit `mr-number-service`, next to the Android
|
||||||
|
container so adb is local), bound `0.0.0.0:8787`. Deploy from here:
|
||||||
```bash
|
```bash
|
||||||
export MRNUMBER_SERVICE_TOKEN="$(cat ~/.config/cocotte-secrets/mr-number.service-token)"
|
deploy/deploy-service.sh # scp service+client to /opt, install unit, restart
|
||||||
export PEOPLE_BASE_URL=... PEOPLE_SERVICE_TOKEN=... MR_NUMBER_DEVICE=45.55.191.82:5555
|
# then on the box fill /etc/mr-number-service.env (tokens), and point Prospector at:
|
||||||
cd service && ./run # binds 0.0.0.0:8787 (mesh-reachable); bun test / bun run typecheck
|
# MRNUMBER_BASE_URL=http://10.20.0.4:8787 (box VPC)
|
||||||
|
```
|
||||||
|
**Local dev (plum):**
|
||||||
|
```bash
|
||||||
|
cd service && ./run # loads ~/.config/cocotte-secrets/*; binds 0.0.0.0:8787
|
||||||
|
bun test && bun run typecheck # host-free
|
||||||
```
|
```
|
||||||
|
|
||||||
| Method + path | Auth | Body / result |
|
| Method + path | Auth | Body / result |
|
||||||
|
|
|
||||||
72
deploy/deploy-service.sh
Executable file
72
deploy/deploy-service.sh
Executable file
|
|
@ -0,0 +1,72 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deploy the mr-number TRIGGER SERVICE onto the redroid box (DigitalOcean
|
||||||
|
# lilith-store-redroid). The service runs NEXT TO the redroid Android container so adb
|
||||||
|
# is local. The box itself is provisioned by uvlava IaC + the @redroid app — this only
|
||||||
|
# installs/(re)starts the `mr-number-service` systemd unit and its code under /opt.
|
||||||
|
#
|
||||||
|
# Prereqs ON THE BOX (this script verifies them and fails loudly if missing):
|
||||||
|
# bun, python3 + redroid_client (pip: lilith-redroid-client from cocotte-forge),
|
||||||
|
# the claude-code-batch-sdk (for vision), a reachable people service, and a filled
|
||||||
|
# /etc/mr-number-service.env (tokens). It does NOT mint tokens.
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
HOST="${MRNUMBER_DEPLOY_HOST:-root@45.55.191.82}"
|
||||||
|
KEY="${MRNUMBER_DEPLOY_KEY:-$HOME/.ssh/id_ed25519_1984}"
|
||||||
|
SSH=(ssh -i "$KEY" -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 "$HOST")
|
||||||
|
SCP=(scp -i "$KEY" -o StrictHostKeyChecking=accept-new -o ConnectTimeout=15)
|
||||||
|
DEST=/opt/mr-number-service
|
||||||
|
|
||||||
|
echo "[mr-number] verifying box prerequisites…"
|
||||||
|
BUN_PATH="$("${SSH[@]}" 'command -v bun || true')"
|
||||||
|
[ -n "$BUN_PATH" ] || { echo "FATAL: bun not on the box. Install it first (curl -fsSL https://bun.sh/install | bash), then re-run." >&2; exit 1; }
|
||||||
|
"${SSH[@]}" '
|
||||||
|
set -e
|
||||||
|
command -v python3 >/dev/null || { echo "FATAL: python3 missing on box" >&2; exit 1; }
|
||||||
|
python3 -c "import redroid_client" 2>/dev/null || { echo "FATAL: redroid_client not installed on box (pip install lilith-redroid-client from cocotte-forge)" >&2; exit 1; }
|
||||||
|
adb devices 2>/dev/null | grep -qE "device$" || { echo "FATAL: no adb device on box (redroid container down?)" >&2; exit 1; }
|
||||||
|
'
|
||||||
|
echo "[mr-number] box ok (bun=$BUN_PATH)."
|
||||||
|
|
||||||
|
echo "[mr-number] copying service + client to $DEST …"
|
||||||
|
"${SSH[@]}" "mkdir -p $DEST/service $DEST/client"
|
||||||
|
"${SCP[@]}" \
|
||||||
|
"$ROOT/service/index.ts" "$ROOT/service/config.ts" "$ROOT/service/queue.ts" \
|
||||||
|
"$ROOT/service/worker.ts" "$ROOT/service/validate.ts" "$ROOT/service/package.json" \
|
||||||
|
"$ROOT/service/tsconfig.json" "$HOST:$DEST/"
|
||||||
|
"${SCP[@]}" "$ROOT/client/mr_lookup.py" "$HOST:$DEST/client/mr_lookup.py"
|
||||||
|
|
||||||
|
echo "[mr-number] installing systemd unit (resolved bun=$BUN_PATH)…"
|
||||||
|
TMP_UNIT="$(mktemp)"; trap 'rm -f "$TMP_UNIT"' EXIT
|
||||||
|
sed "s#__BUN__#$BUN_PATH#" "$ROOT/deploy/mr-number-service.service" > "$TMP_UNIT"
|
||||||
|
"${SCP[@]}" "$TMP_UNIT" "$HOST:/etc/systemd/system/mr-number-service.service"
|
||||||
|
|
||||||
|
echo "[mr-number] ensuring /etc/mr-number-service.env (0600) exists…"
|
||||||
|
"${SSH[@]}" '
|
||||||
|
if [ ! -f /etc/mr-number-service.env ]; then
|
||||||
|
umask 077
|
||||||
|
cat > /etc/mr-number-service.env <<ENV
|
||||||
|
# 0600 — fill these in, then: systemctl restart mr-number-service
|
||||||
|
MRNUMBER_SERVICE_TOKEN=
|
||||||
|
PEOPLE_SERVICE_TOKEN=
|
||||||
|
PEOPLE_BASE_URL=http://10.9.0.5:3061
|
||||||
|
CLAUDE_CODE_BATCH_SDK_PATH=
|
||||||
|
ENV
|
||||||
|
echo " created /etc/mr-number-service.env TEMPLATE — fill the two tokens + SDK path before it will start."
|
||||||
|
else
|
||||||
|
echo " /etc/mr-number-service.env already present (left untouched)."
|
||||||
|
fi
|
||||||
|
'
|
||||||
|
|
||||||
|
echo "[mr-number] (re)starting mr-number-service…"
|
||||||
|
"${SSH[@]}" '
|
||||||
|
systemctl daemon-reload
|
||||||
|
if grep -q "^MRNUMBER_SERVICE_TOKEN=.\+" /etc/mr-number-service.env && grep -q "^PEOPLE_SERVICE_TOKEN=.\+" /etc/mr-number-service.env; then
|
||||||
|
systemctl enable --now mr-number-service
|
||||||
|
sleep 2
|
||||||
|
printf " status: "; systemctl is-active mr-number-service || true
|
||||||
|
curl -sf -m5 http://127.0.0.1:8787/health && echo || echo " WARN: /health not responding yet — check: journalctl -u mr-number-service -n50"
|
||||||
|
else
|
||||||
|
echo " tokens not filled in /etc/mr-number-service.env — NOT enabling (would crashloop). Fill them, then: systemctl enable --now mr-number-service"
|
||||||
|
fi
|
||||||
|
'
|
||||||
|
echo "[mr-number] done. Prospector → MRNUMBER_BASE_URL=http://10.20.0.4:8787 (box VPC) or http://45.55.191.82:8787."
|
||||||
|
|
@ -75,17 +75,16 @@ cat <<EOC
|
||||||
|
|
||||||
EOC
|
EOC
|
||||||
|
|
||||||
# --- 5. trigger service (the inbound HTTP surface Prospector calls) ---
|
# --- 5. trigger service ---
|
||||||
echo "[mr-number] installing trigger-service LaunchAgent (autostart; KeepAlive)…"
|
# This install.sh sets up the PLUM DEV side only (client/ + mcp/ + console). The trigger
|
||||||
if [ -f "$HOME/.config/cocotte-secrets/mr-number.service-token" ]; then
|
# service is a PROD unit that runs ON THE REDROID BOX (systemd, next to the Android
|
||||||
"$ROOT/service/run" install-launchagent || true
|
# container) — deploy it from here with deploy/deploy-service.sh, not as a plum agent.
|
||||||
else
|
cat <<EOS
|
||||||
cat <<EOS
|
|
||||||
[mr-number] SKIPPED trigger-service LaunchAgent — secret not found.
|
[mr-number] trigger service (prod) deploys to the redroid box, not plum:
|
||||||
Provision it first (0600), then run: $ROOT/service/run install-launchagent
|
• fill tokens on the box: /etc/mr-number-service.env (MRNUMBER_SERVICE_TOKEN, PEOPLE_SERVICE_TOKEN)
|
||||||
• ~/.config/cocotte-secrets/mr-number.service-token (inbound auth; Prospector's MRNUMBER_SERVICE_TOKEN)
|
• deploy: $ROOT/deploy/deploy-service.sh
|
||||||
• ~/.config/cocotte-secrets/people-service.token (recording auth; PEOPLE_SERVICE_TOKEN)
|
• Prospector → MRNUMBER_BASE_URL=http://10.20.0.4:8787 (box VPC)
|
||||||
Then point Prospector at it: MRNUMBER_BASE_URL=http://10.9.0.3:8787
|
Local dev on plum: cd $ROOT/service && ./run (loads ~/.config/cocotte-secrets/*)
|
||||||
EOS
|
EOS
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
|
||||||
23
deploy/mr-number-service.service
Normal file
23
deploy/mr-number-service.service
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
[Unit]
|
||||||
|
Description=mr-number trigger service — POST /api/screening/requests, drives local redroid + records screening_mrnumber people signal
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=/opt/mr-number-service
|
||||||
|
# Secrets + env live in a 0600 file on the box (NOT in this unit / NOT in git):
|
||||||
|
# MRNUMBER_SERVICE_TOKEN, PEOPLE_SERVICE_TOKEN, PEOPLE_BASE_URL, MR_NUMBER_DEVICE,
|
||||||
|
# CLAUDE_CODE_BATCH_SDK_PATH (see deploy/deploy-service.sh for the template)
|
||||||
|
EnvironmentFile=/etc/mr-number-service.env
|
||||||
|
# On the box adb is local; mr_lookup.py talks to the redroid container on loopback.
|
||||||
|
Environment=MR_NUMBER_DEVICE=localhost:5555
|
||||||
|
Environment=MRNUMBER_PORT=8787
|
||||||
|
Environment=MR_NUMBER_LOOKUP_SCRIPT=/opt/mr-number-service/client/mr_lookup.py
|
||||||
|
# __BUN__ is substituted with the box's resolved bun path by deploy-service.sh.
|
||||||
|
ExecStart=__BUN__ run /opt/mr-number-service/index.ts
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=3
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
82
service/run
82
service/run
|
|
@ -1,76 +1,24 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# mr-number trigger service launcher + LaunchAgent installer.
|
# mr-number trigger service launcher (dev/manual). In PROD the service runs on the
|
||||||
|
# redroid box as the systemd unit `mr-number-service` (see deploy/deploy-service.sh);
|
||||||
|
# that unit gets its env from /etc/mr-number-service.env, not this script.
|
||||||
#
|
#
|
||||||
# ./run launch the service (loads secrets + env, then bun run index.ts)
|
# Use this for local dev/testing on plum (fennel). Secrets are 0600 files under
|
||||||
# ./run install-launchagent install the plum LaunchAgent (autostart on login/boot)
|
# ~/.config/cocotte-secrets/ (env wins if already set):
|
||||||
#
|
# mr-number.service-token inbound auth Prospector presents (MRNUMBER_SERVICE_TOKEN)
|
||||||
# Secrets are 0600 files under ~/.config/cocotte-secrets/ (env wins if already set):
|
|
||||||
# mr-number.service-token inbound auth Prospector must present (MRNUMBER_SERVICE_TOKEN)
|
|
||||||
# people-service.token recording auth for mr_lookup.py (PEOPLE_SERVICE_TOKEN)
|
# people-service.token recording auth for mr_lookup.py (PEOPLE_SERVICE_TOKEN)
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
SECRETS="$HOME/.config/cocotte-secrets"
|
SECRETS="$HOME/.config/cocotte-secrets"
|
||||||
PLIST_LABEL="com.lilith.redroid-mrnumber-service"
|
|
||||||
PLIST="$HOME/Library/LaunchAgents/$PLIST_LABEL.plist"
|
|
||||||
LOG_DIR="$HOME/Library/Logs/redroid-mrnumber-service"
|
|
||||||
|
|
||||||
load_env() {
|
# File → env, only if the env var isn't already set. Tokens stay out of argv.
|
||||||
# File → env, only if the env var isn't already set. Tokens stay out of argv.
|
[ -n "${MRNUMBER_SERVICE_TOKEN:-}" ] || { [ -f "$SECRETS/mr-number.service-token" ] && export MRNUMBER_SERVICE_TOKEN="$(cat "$SECRETS/mr-number.service-token")"; }
|
||||||
[ -n "${MRNUMBER_SERVICE_TOKEN:-}" ] || { [ -f "$SECRETS/mr-number.service-token" ] && export MRNUMBER_SERVICE_TOKEN="$(cat "$SECRETS/mr-number.service-token")"; }
|
[ -n "${PEOPLE_SERVICE_TOKEN:-}" ] || { [ -f "$SECRETS/people-service.token" ] && export PEOPLE_SERVICE_TOKEN="$(cat "$SECRETS/people-service.token")"; }
|
||||||
[ -n "${PEOPLE_SERVICE_TOKEN:-}" ] || { [ -f "$SECRETS/people-service.token" ] && export PEOPLE_SERVICE_TOKEN="$(cat "$SECRETS/people-service.token")"; }
|
export PEOPLE_BASE_URL="${PEOPLE_BASE_URL:-http://10.9.0.5:3061}"
|
||||||
export PEOPLE_BASE_URL="${PEOPLE_BASE_URL:-http://10.9.0.5:3061}"
|
export MR_NUMBER_DEVICE="${MR_NUMBER_DEVICE:-45.55.191.82:5555}" # plum dev drives the box remotely; on-box prod uses localhost:5555
|
||||||
export MR_NUMBER_DEVICE="${MR_NUMBER_DEVICE:-45.55.191.82:5555}"
|
export MRNUMBER_PORT="${MRNUMBER_PORT:-8787}"
|
||||||
export MRNUMBER_PORT="${MRNUMBER_PORT:-8787}"
|
[ -n "${CLAUDE_CODE_BATCH_SDK_PATH:-}" ] || export CLAUDE_CODE_BATCH_SDK_PATH="$HOME/Code/@quinn/@applications/ml/@packages/@py/claude-code-batch-sdk/src"
|
||||||
[ -n "${CLAUDE_CODE_BATCH_SDK_PATH:-}" ] || export CLAUDE_CODE_BATCH_SDK_PATH="$HOME/Code/@quinn/@applications/ml/@packages/@py/claude-code-batch-sdk/src"
|
|
||||||
}
|
|
||||||
|
|
||||||
launch() {
|
cd "$HERE"
|
||||||
load_env
|
exec bun run index.ts
|
||||||
cd "$HERE"
|
|
||||||
exec bun run index.ts
|
|
||||||
}
|
|
||||||
|
|
||||||
install_launchagent() {
|
|
||||||
mkdir -p "$(dirname "$PLIST")" "$LOG_DIR"
|
|
||||||
cat > "$PLIST" <<PLIST
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>Label</key>
|
|
||||||
<string>$PLIST_LABEL</string>
|
|
||||||
<key>ProgramArguments</key>
|
|
||||||
<array>
|
|
||||||
<string>/bin/bash</string>
|
|
||||||
<string>-l</string>
|
|
||||||
<string>-c</string>
|
|
||||||
<string>$HERE/run</string>
|
|
||||||
</array>
|
|
||||||
<key>RunAtLoad</key>
|
|
||||||
<true/>
|
|
||||||
<key>KeepAlive</key>
|
|
||||||
<true/>
|
|
||||||
<key>StandardOutPath</key>
|
|
||||||
<string>$LOG_DIR/launchd.stdout.log</string>
|
|
||||||
<key>StandardErrorPath</key>
|
|
||||||
<string>$LOG_DIR/launchd.stderr.log</string>
|
|
||||||
<key>EnvironmentVariables</key>
|
|
||||||
<dict>
|
|
||||||
<key>PATH</key>
|
|
||||||
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
PLIST
|
|
||||||
launchctl bootout "gui/$(id -u)/$PLIST_LABEL" 2>/dev/null || true
|
|
||||||
launchctl bootstrap "gui/$(id -u)" "$PLIST" 2>/dev/null || true
|
|
||||||
echo "LaunchAgent $PLIST_LABEL installed (autostart; KeepAlive). Logs in $LOG_DIR"
|
|
||||||
echo "Listening on 0.0.0.0:${MRNUMBER_PORT:-8787} — set Prospector MRNUMBER_BASE_URL=http://10.9.0.3:8787"
|
|
||||||
}
|
|
||||||
|
|
||||||
case "${1:-launch}" in
|
|
||||||
launch) launch ;;
|
|
||||||
install-launchagent) install_launchagent ;;
|
|
||||||
*) echo "usage: $0 [launch|install-launchagent]" >&2; exit 2 ;;
|
|
||||||
esac
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue