feat: structured __renderer envelopes for MCP tools
All 19 oikos tools now return JSON envelopes with __renderer hints
(terminal, service_status, ping, path_report, etc.) carrying structured
data alongside the model-facing prose in a `message` field — agent
behavior is unchanged while the UI renders native cards.
Fixes: ping_service SQL (json||text precedence), timestamptz scan,
run:{...} action prefix parsing, get_execution_status target slug join.
Also: deploy.sh step 5.5 restarts dsh web after API deploy; watchdog
LaunchAgent detects API recovery and kickstarts dsh web for stale MCP
sessions.
This commit is contained in:
179
scripts/deploy-plugins.sh
Normal file → Executable file
179
scripts/deploy-plugins.sh
Normal file → Executable file
@@ -1,57 +1,166 @@
|
||||
#!/bin/sh
|
||||
# Oikos-plugins deploy script — triggered by Gitea webhook on push to dtoro/oikos-plugins.
|
||||
# Runs on mac-mini via launchd unit running cmd/webhook (route: /deploy-plugins).
|
||||
# Runs on mac-mini via launchd unit network.hubris.oikos-deploy-webhook (route: /deploy-plugins).
|
||||
#
|
||||
# Deployment vehicle is the in-tree clone at $DSH_DIR/packages/oikos: the web
|
||||
# profile symlinks its packages, and their @deepseek-ai peer deps resolve
|
||||
# through the harness workspace root node_modules. UI and node halves ship as
|
||||
# committed lib/ artifacts, so no build step runs here. On failure the script
|
||||
# restores the previous SHAs and notifies via the oikos API (OIKOS_API_TOKEN)
|
||||
# and Matrix (MATRIX_WEBHOOK_URL) when configured.
|
||||
set -e
|
||||
|
||||
REPO_DIR="${REPO_DIR:-$HOME/Projects/oikos}"
|
||||
PLUGIN_DIR="${PLUGIN_DIR:-$HOME/oikos-plugins}"
|
||||
DSH_DIR="${DSH_DIR:-$HOME/Projects/deepseek-harness}"
|
||||
PLUGIN_DIR="${PLUGIN_DIR:-$DSH_DIR/packages/oikos}"
|
||||
PROFILE_DIR="${PROFILE_DIR:-$HOME/.dsh/profiles/web}"
|
||||
PORT="${PORT:-3080}"
|
||||
LOCKDIR="${LOCKDIR:-/tmp/oikos-plugins-deploy.lock}"
|
||||
DSH_BRANCH="${DSH_BRANCH:-master}"
|
||||
HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:$PORT/}"
|
||||
RETRIES=${RETRIES:-90}
|
||||
ROLLBACK_RETRIES=${ROLLBACK_RETRIES:-30}
|
||||
SLEEP=${SLEEP:-2}
|
||||
AGENT_LABEL="network.hubris.dsh-web"
|
||||
UID_N=$(id -u)
|
||||
|
||||
acquire_lock() {
|
||||
if mkdir "$LOCKDIR" 2>/dev/null; then
|
||||
trap 'rm -rf "$LOCKDIR"' EXIT
|
||||
return 0
|
||||
notify_deploy_failure() {
|
||||
reason="$1"
|
||||
echo "NOTIFY: deploy failed — $reason"
|
||||
if [ -n "${OIKOS_API_TOKEN:-}" ]; then
|
||||
curl -sf -X POST "http://localhost:8090/api/v1/events" \
|
||||
-H "Authorization: Bearer $OIKOS_API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"type\":\"deploy.failed\",\"severity\":\"critical\",\"source\":\"webhook\",\"data\":{\"repo\":\"oikos-plugins\",\"reason\":\"$reason\"}}" \
|
||||
>/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -n "${MATRIX_WEBHOOK_URL:-}" ]; then
|
||||
curl -sf -X POST "$MATRIX_WEBHOOK_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"msgtype\":\"m.text\",\"body\":\"🚨 oikos-plugins deploy failed: $reason\"}" \
|
||||
>/dev/null 2>&1 || true
|
||||
fi
|
||||
echo "deploy already running, skipping"
|
||||
exit 0
|
||||
}
|
||||
|
||||
acquire_lock
|
||||
# Serialize deploys. mkdir is atomic on POSIX (macOS lacks flock); the
|
||||
# stale-pid check recovers a lock left by a SIGKILLed or rebooted deploy.
|
||||
if ! mkdir "$LOCKDIR" 2>/dev/null; then
|
||||
oldpid=$(cat "$LOCKDIR/pid" 2>/dev/null || echo "")
|
||||
if [ -n "$oldpid" ] && kill -0 "$oldpid" 2>/dev/null; then
|
||||
echo "deploy already in progress (pid $oldpid) — exiting"
|
||||
exit 0
|
||||
fi
|
||||
echo "removing stale deploy lock (pid ${oldpid:-?} not running)"
|
||||
rm -rf "$LOCKDIR"
|
||||
mkdir "$LOCKDIR"
|
||||
fi
|
||||
echo $$ > "$LOCKDIR/pid"
|
||||
trap 'rc=$?; rm -rf "$LOCKDIR" 2>/dev/null || true; if [ "$_ok" != "1" ] && [ "$_notified" != "1" ]; then notify_deploy_failure "deploy aborted (exit $rc)"; fi' EXIT
|
||||
_ok=0
|
||||
_notified=0
|
||||
|
||||
# Any HTTP response counts as healthy: this probes liveness (is the port
|
||||
# serving), not a specific route.
|
||||
wait_healthy() {
|
||||
tries="$1"
|
||||
i=1
|
||||
while [ "$i" -le "$tries" ]; do
|
||||
if curl -s -o /dev/null --max-time 2 "$HEALTH_URL"; then
|
||||
echo "healthy after $((i * SLEEP))s"
|
||||
return 0
|
||||
fi
|
||||
sleep "$SLEEP"
|
||||
i=$((i + 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
restart_dsh() {
|
||||
if ! launchctl print "gui/$UID_N/$AGENT_LABEL" >/dev/null 2>&1; then
|
||||
launchctl bootstrap "gui/$UID_N" "$HOME/Library/LaunchAgents/$AGENT_LABEL.plist"
|
||||
sleep 1
|
||||
fi
|
||||
launchctl kickstart -k "gui/$UID_N/$AGENT_LABEL"
|
||||
}
|
||||
|
||||
echo "=== oikos-plugins deploy started ==="
|
||||
|
||||
# 1. Pull latest
|
||||
if [ ! -d "$PLUGIN_DIR" ]; then
|
||||
git clone gitea@git-ssh.hubris.network:dtoro/oikos-plugins.git "$PLUGIN_DIR"
|
||||
fi
|
||||
cd "$PLUGIN_DIR"
|
||||
git fetch origin master
|
||||
git reset --hard origin/master
|
||||
[ -d "$PLUGIN_DIR/.git" ] || {
|
||||
echo "ERROR: $PLUGIN_DIR is not a git clone of dtoro/oikos-plugins — refusing"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 2. Symlink packages into dsh profile
|
||||
PLUGIN_OLD=$(git -C "$PLUGIN_DIR" rev-parse HEAD 2>/dev/null || echo "")
|
||||
DSH_OLD=$(git -C "$DSH_DIR" rev-parse HEAD 2>/dev/null || echo "")
|
||||
echo "pre-deploy: plugins=${PLUGIN_OLD:-none} dsh=${DSH_OLD:-none}"
|
||||
|
||||
rollback() {
|
||||
_notified=1
|
||||
echo "=== rolling back ==="
|
||||
if [ -n "$PLUGIN_OLD" ] && [ "$(git -C "$PLUGIN_DIR" rev-parse HEAD)" != "$PLUGIN_OLD" ]; then
|
||||
git -C "$PLUGIN_DIR" reset --hard "$PLUGIN_OLD"
|
||||
fi
|
||||
if [ -n "$DSH_OLD" ] && [ "$(git -C "$DSH_DIR" rev-parse HEAD)" != "$DSH_OLD" ]; then
|
||||
git -C "$DSH_DIR" reset --hard "$DSH_OLD"
|
||||
pnpm -C "$DSH_DIR" install --no-frozen-lockfile >/dev/null 2>&1 || true
|
||||
git -C "$DSH_DIR" checkout -- pnpm-lock.yaml 2>/dev/null || true
|
||||
fi
|
||||
restart_dsh
|
||||
if wait_healthy "$ROLLBACK_RETRIES"; then
|
||||
notify_deploy_failure "deploy failed; rolled back to plugins@${PLUGIN_OLD:-?} dsh@${DSH_OLD:-?}"
|
||||
else
|
||||
notify_deploy_failure "deploy failed and rollback unhealthy — dsh web DOWN on port $PORT"
|
||||
fi
|
||||
}
|
||||
|
||||
# 1. Plugins: advance the in-tree clone to the pushed state. Uncommitted local
|
||||
# edits mean someone is developing here — never destroy them; deploy the dirty
|
||||
# tree as-is and say so.
|
||||
git -C "$PLUGIN_DIR" fetch origin master
|
||||
if git -C "$PLUGIN_DIR" diff --quiet && git -C "$PLUGIN_DIR" diff --cached --quiet; then
|
||||
git -C "$PLUGIN_DIR" reset --hard origin/master
|
||||
echo "plugins at $(git -C "$PLUGIN_DIR" rev-parse --short HEAD)"
|
||||
else
|
||||
echo "WARN: $PLUGIN_DIR has uncommitted changes — deploying the dirty tree as-is"
|
||||
fi
|
||||
|
||||
# 2. Harness: advance by fast-forward only (never discards local commits;
|
||||
# refuses when diverged). The untracked packages/oikos member is invisible to
|
||||
# origin/master's lockfile, so install non-frozen and restore the lockfile
|
||||
# afterwards: node_modules keeps the resolution, the tree stays clean.
|
||||
git -C "$DSH_DIR" fetch origin "$DSH_BRANCH"
|
||||
git -C "$DSH_DIR" pull --ff-only origin "$DSH_BRANCH"
|
||||
pnpm -C "$DSH_DIR" install --no-frozen-lockfile
|
||||
git -C "$DSH_DIR" checkout -- pnpm-lock.yaml
|
||||
echo "dsh at $(git -C "$DSH_DIR" rev-parse --short HEAD)"
|
||||
|
||||
# 2b. Rebuild the frontend dist: the web-app bundle serves the gitignored
|
||||
# apps/web dist through workspace exports, so a harness advance without a
|
||||
# rebuild keeps serving the previous UI.
|
||||
pnpm -C "$DSH_DIR" --filter @deepseek-ai/dsh-web-frontend run build >/dev/null
|
||||
echo "frontend dist rebuilt"
|
||||
|
||||
# 3. Symlink packages into dsh profile
|
||||
for pkg in ui mcp-scope session-summary bundle evals; do
|
||||
name=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$pkg/package.json')).name)")
|
||||
name=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$PLUGIN_DIR/$pkg/package.json')).name)")
|
||||
ln -sf "$PLUGIN_DIR/$pkg" "$PROFILE_DIR/node_modules/$name"
|
||||
echo "linked $name"
|
||||
done
|
||||
|
||||
# 3. Build UI client bundle (needs dsh workspace for tsdown)
|
||||
cd "$DSH_DIR"
|
||||
pnpm install --filter @deepseek-ai/dsh-oikos-ui --frozen-lockfile 2>&1
|
||||
cd "$PLUGIN_DIR/ui"
|
||||
DSH_BUILD_FACE=client npx tsdown --config tsdown.config.ts 2>&1
|
||||
echo "UI bundle built"
|
||||
|
||||
# 4. Restart dsh
|
||||
DASHBOARD_PID=$(pgrep -f 'dsh.*--port.*3080' 2>/dev/null || true)
|
||||
if [ -n "$DASHBOARD_PID" ]; then
|
||||
kill "$DASHBOARD_PID" 2>/dev/null || true
|
||||
sleep 2
|
||||
# 4. Patch migration: the oikos overlay used to live only in /tmp (wiped on
|
||||
# reboot). If the profile patch layer is still empty and the /tmp copy exists,
|
||||
# move it into the profile so launchd boots need no --patch flag.
|
||||
if [ -f /tmp/oikos-mcp-patch.yml ] && ! grep -q 'id:' "$PROFILE_DIR/cordis.patch.yml" 2>/dev/null; then
|
||||
cp /tmp/oikos-mcp-patch.yml "$PROFILE_DIR/cordis.patch.yml"
|
||||
echo "migrated oikos patch into $PROFILE_DIR/cordis.patch.yml"
|
||||
fi
|
||||
cd "$DSH_DIR"
|
||||
nohup pnpm dsh --profile web --patch /tmp/oikos-mcp-patch.yml --port "$PORT" > /tmp/dsh-web.log 2>&1 &
|
||||
echo "dsh restarted (pid $!)"
|
||||
|
||||
echo "=== oikos-plugins deploy complete ==="
|
||||
# 5. Restart under launchd and health-check
|
||||
restart_dsh
|
||||
if ! wait_healthy "$RETRIES"; then
|
||||
echo "ERROR: health check failed after $((RETRIES * SLEEP))s"
|
||||
rollback
|
||||
exit 1
|
||||
fi
|
||||
|
||||
_ok=1
|
||||
echo "=== oikos-plugins deploy complete ==="
|
||||
|
||||
@@ -230,6 +230,8 @@ DOCKER_BUILDKIT=1 docker compose --profile "$PROFILE" build \
|
||||
# ── 5. Rolling restart ────────────────────────────────────────────────────
|
||||
echo "[5/8] docker compose up -d"
|
||||
docker compose --profile "$PROFILE" up -d --remove-orphans
|
||||
echo "[5.5/8] restarting dsh web (stale MCP session after api restart)"
|
||||
launchctl kickstart -k "gui/$(id -u)/network.hubris.dsh-web" 2>/dev/null || true
|
||||
|
||||
# ── 6. Prune old image tags — keep the 3 newest per service so rollback ────
|
||||
# (OIKOS_VERSION=v0.x.y docker compose up) stays available. The repo list
|
||||
|
||||
36
scripts/network.hubris.dsh-web.plist
Normal file
36
scripts/network.hubris.dsh-web.plist
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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>network.hubris.dsh-web</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/opt/homebrew/bin/pnpm</string>
|
||||
<string>dsh</string>
|
||||
<string>--profile</string>
|
||||
<string>web</string>
|
||||
<string>--port</string>
|
||||
<string>3080</string>
|
||||
</array>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/Users/dtoro/Projects/deepseek-harness</string>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>HOME</key>
|
||||
<string>/Users/dtoro</string>
|
||||
<key>PATH</key>
|
||||
<string>/Users/dtoro/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
||||
</dict>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>ThrottleInterval</key>
|
||||
<integer>10</integer>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/dtoro/Library/Logs/dsh-web.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/dtoro/Library/Logs/dsh-web.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
20
scripts/network.hubris.oikos-api-watchdog.plist
Normal file
20
scripts/network.hubris.oikos-api-watchdog.plist
Normal file
@@ -0,0 +1,20 @@
|
||||
<?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>network.hubris.oikos-api-watchdog</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/Users/dtoro/Projects/deepseek-harness/packages/oikos/scripts/oikos-api-health-watchdog.sh</string>
|
||||
</array>
|
||||
<key>StartInterval</key>
|
||||
<integer>30</integer>
|
||||
<key>KeepAlive</key>
|
||||
<false/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/dtoro/Library/Logs/oikos-api-watchdog.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/dtoro/Library/Logs/oikos-api-watchdog.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
22
scripts/oikos-api-health-watchdog.sh
Executable file
22
scripts/oikos-api-health-watchdog.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
# Oikos API health watchdog — if the oikos API restarts while dsh web holds a
|
||||
# stale MCP session, every tool call fails with "session not found" until dsh
|
||||
# web itself restarts. This monitor detects the transition and triggers a
|
||||
# launchd kickstart so the connection recovers automatically.
|
||||
#
|
||||
# Runs every 30s via LaunchAgent network.hubris.oikos-api-watchdog.
|
||||
|
||||
set -e
|
||||
|
||||
API_URL="${API_URL:-http://localhost:8090/healthz}"
|
||||
MARKER="${MARKER:-/tmp/.oikos-api-down}"
|
||||
|
||||
if curl -sf -o /dev/null --max-time 3 "$API_URL"; then
|
||||
if [ -f "$MARKER" ]; then
|
||||
rm -f "$MARKER"
|
||||
echo "oikos API recovered — restarting dsh web"
|
||||
launchctl kickstart -k "gui/$(id -u)/network.hubris.dsh-web" 2>/dev/null || true
|
||||
fi
|
||||
else
|
||||
touch "$MARKER"
|
||||
fi
|
||||
Reference in New Issue
Block a user