Merge remote-tracking branch 'gitea-ai/gsd/phase-13-aiui-functional-conversational-node-control-and-content-surf'
Demo images / Build & push demo images (push) Successful in 3m14s

This commit is contained in:
archipelago
2026-08-09 08:17:22 -04:00
481 changed files with 90667 additions and 210 deletions
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env bash
# aiui-proxy-closed.sh — S-15 deployed-surface check for 13-02-PLAN.md.
#
# T-13-08..T-13-12: `/aiui/api/claude/` and `/aiui/api/ollama/` used to proxy
# to an unauthenticated Python sidecar (port 3142) holding its own API key,
# and `/aiui/api/openrouter/` was a plain unauthenticated relay to a paid
# third-party API — anyone who could reach the node's web port could spend
# the owner's budget. A green `cargo test` on model_proxy.rs proves the Rust
# handler's own logic is correct in isolation; it proves NOTHING about which
# target nginx is actually pointed at on a deployed node, whether the old
# sidecar is still listening, or whether the OpenRouter relay still exists.
# This script is that proof, against a real node (13-AI-SPEC.md S-15 — "not
# a unit test and must not be treated as one").
#
# Usage: ./aiui-proxy-closed.sh <node-host> [ssh-user]
# HTTP checks always run, with NO session cookie, and must never see 200.
# SSH-based infra checks (systemd unit gone, port 3142 dark, single key
# ledger) run only when `sshpass` is installed AND $AIUI_TEST_SSH_PASS is
# set in the environment — otherwise they report SKIP, not FAIL. Never
# hardcode a password in this file (CLAUDE.md: never commit/push secrets).
#
# Exit 0 = every HTTP assertion passes AND every SSH assertion that ran passed.
set -uo pipefail
HOST="${1:?usage: aiui-proxy-closed.sh <node-host> [ssh-user]}"
SSH_USER="${2:-archipelago}"
BASE="http://${HOST}"
PASS=0; FAIL=0; SKIP=0
say() { printf '%s\n' "$*"; }
ok() { PASS=$((PASS+1)); say " PASS: $1"; }
bad() { FAIL=$((FAIL+1)); say " FAIL: $1"; }
skip() { SKIP=$((SKIP+1)); say " SKIP: $1"; }
# $1=method $2=path $3=optional body
status_of() {
if [ -n "${3:-}" ]; then
curl -s -m 10 -o /dev/null -w '%{http_code}' -X "$1" "${BASE}${2}" -d "$3" 2>/dev/null
else
curl -s -m 10 -o /dev/null -w '%{http_code}' -X "$1" "${BASE}${2}" 2>/dev/null
fi
}
# $1=label $2=observed status — closed means 401/403/404; 200 is the exposure.
assert_closed() {
case "$2" in
401|403|404) ok "$1 -> $2 (closed)" ;;
200) bad "$1 -> 200 (OPEN — an unauthenticated caller reached the backend)" ;;
*) bad "$1 -> '$2' (unexpected — want 401/403/404, and it is not 200 either)" ;;
esac
}
say "== S-15 AIUI model-proxy closure — node ${HOST} =="
# 1) /aiui/api/claude/v1/messages — no session cookie must never reach Anthropic.
S=$(status_of POST /aiui/api/claude/v1/messages '{"model":"claude-3-5-sonnet-20241022","max_tokens":1,"messages":[]}')
assert_closed "POST /aiui/api/claude/v1/messages (no session)" "$S"
# 2) /aiui/api/ollama/api/tags — no session cookie must never reach local Ollama.
S=$(status_of GET /aiui/api/ollama/api/tags)
assert_closed "GET /aiui/api/ollama/api/tags (no session)" "$S"
# 3) /aiui/api/openrouter/ — must be entirely GONE, not merely gated: 404 specifically.
S=$(status_of GET /aiui/api/openrouter/)
if [ "$S" = "404" ]; then
ok "GET /aiui/api/openrouter/ -> 404 (relay deleted)"
else
bad "GET /aiui/api/openrouter/ -> $S (want 404 — the relay must not exist at all, not just be gated)"
fi
say ""
say "-- SSH-based infra checks (${SSH_USER}@${HOST}) --"
if ! command -v sshpass >/dev/null 2>&1 || [ -z "${AIUI_TEST_SSH_PASS:-}" ]; then
skip "claude-api-proxy systemd unit (no sshpass or AIUI_TEST_SSH_PASS unset)"
skip "port 3142 listener (no sshpass or AIUI_TEST_SSH_PASS unset)"
skip "single key ledger (no sshpass or AIUI_TEST_SSH_PASS unset)"
else
ssh_run() {
sshpass -p "${AIUI_TEST_SSH_PASS}" ssh -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o LogLevel=ERROR \
"${SSH_USER}@${HOST}" "$1" 2>/dev/null
}
UNIT_STATE=$(ssh_run 'systemctl is-active claude-api-proxy 2>&1')
case "$UNIT_STATE" in
inactive|unknown) ok "claude-api-proxy unit is '$UNIT_STATE'" ;;
*"could not be found"*) ok "claude-api-proxy unit is gone (could not be found)" ;;
active) bad "claude-api-proxy unit is still ACTIVE — sidecar not torn down" ;;
*) bad "claude-api-proxy unit state unexpected: '$UNIT_STATE'" ;;
esac
PORT_COUNT=$(ssh_run "ss -ltn 2>/dev/null | grep -c ':3142 '")
[ "${PORT_COUNT:-1}" = "0" ] && ok "nothing listening on :3142" || bad "port 3142 still has a listener (count=${PORT_COUNT:-?})"
# claude-api-key's PRESENCE depends on whether an operator has configured a
# key on this node at all (via system.settings.set claude_api_key) — a
# freshly provisioned/dev node with no key set is expected to have neither
# file, and that is not a defect in this fix. The security-relevant
# invariant this plan makes is narrower and unconditional: the SECOND
# ledger (claude-api-proxy.env) must never exist, whether or not the first
# one does. Presence of claude-api-key is reported for visibility only.
LEDGER=$(ssh_run 'sudo ls /var/lib/archipelago/secrets/ 2>/dev/null')
if grep -qx 'claude-api-key' <<<"$LEDGER"; then
say " INFO: claude-api-key ledger present (a key is configured on this node)"
else
say " INFO: claude-api-key ledger absent (no key configured on this node yet — not a defect)"
fi
if grep -q 'claude-api-proxy.env' <<<"$LEDGER"; then
bad "claude-api-proxy.env still present — second key ledger not deleted"
else
ok "claude-api-proxy.env absent (single ledger enforced)"
fi
fi
say ""
say "== ${HOST}: ${PASS} passed, ${FAIL} failed, ${SKIP} skipped =="
[ "$FAIL" -eq 0 ]
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env bash
# deploy-guard-same-host.sh — regression pin for assert_safe_same_host_deploy
# (scripts/lib/common.sh), widened in 13-09 from containment-only to any
# resolved-path mismatch on the same host.
#
# The 2026-07-31 incident: a same-host `rsync --delete` deploy whose source
# was INSIDE the destination mirrored the source onto the destination and
# deleted ~1810 tracked files, a running dev server, and two sessions'
# uncommitted work. The original fix refused only containment (source-in-
# destination or destination-in-source). It missed SIBLING directories that
# share a parent but neither contains the other — e.g. this session's own
# worktree topology, archy-phase13 (source) vs archy (the main checkout,
# TARGET_DIR's resolved symlink target) — which is the identical rsync
# --delete hazard through a shape the old two-case guard let through.
#
# No SSH, no rsync, no real deploy — pure fixture strings against the
# function. Usage: ./deploy-guard-same-host.sh (takes no host argument)
# Exit 0 = all assertions pass.
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
# shellcheck source=../../scripts/lib/common.sh
source "$PROJECT_DIR/scripts/lib/common.sh"
PASS=0; FAIL=0
say() { printf '%s\n' "$*"; }
ok() { PASS=$((PASS+1)); say " PASS: $1"; }
bad() { FAIL=$((FAIL+1)); say " FAIL: $1"; }
# Helper: run assert_safe_same_host_deploy and check its exit code against
# an expectation ("allow" or "refuse"), silencing its stderr message so the
# test output stays readable.
check() {
local desc="$1" src="$2" dst="$3" expect="$4"
local rc
assert_safe_same_host_deploy "$src" "$dst" >/dev/null 2>&1
rc=$?
if [ "$expect" = "allow" ]; then
[ "$rc" -eq 0 ] && ok "$desc" || bad "$desc (expected allow/exit 0, got exit $rc)"
else
[ "$rc" -ne 0 ] && ok "$desc" || bad "$desc (expected refuse/non-zero, got exit $rc)"
fi
}
say "== assert_safe_same_host_deploy — fixture matrix =="
# 1) Identical resolved source and destination: allowed. The normal
# in-place deploy from the main checkout onto its own symlinked
# destination.
check "identical resolved paths are allowed" \
"/home/archipelago/Projects/archy" \
"/home/archipelago/Projects/archy" \
"allow"
# 2) Source is inside (a subdirectory of) the destination: refused. The
# original 2026-07-31 containment case.
check "source-inside-destination is refused" \
"/home/archipelago/Projects/archy/.claude/worktrees/some-agent" \
"/home/archipelago/Projects/archy" \
"refuse"
# 3) Destination is inside the source: refused. The mirror-image
# containment case.
check "destination-inside-source is refused" \
"/home/archipelago/Projects/archy" \
"/home/archipelago/Projects/archy/.claude/worktrees/some-agent" \
"refuse"
# 4) Sibling-directory regression pin — the exact shape this session's own
# worktree topology exhibits, and the gap the old two-case guard let
# through: archy-phase13 (this worktree) as source, archy (the main
# checkout, TARGET_DIR's resolved symlink target) as destination. Share
# a parent (/home/archipelago/Projects); neither contains the other.
check "sibling directories (archy-phase13 vs archy) are refused [SIBLING REGRESSION PIN]" \
"/home/archipelago/Projects/archy-phase13" \
"/home/archipelago/Projects/archy" \
"refuse"
# 5) Two completely unrelated same-host paths with no shared parent at
# all: refused. Same-host plus any mismatch is refused, not just the
# two containment shapes.
check "unrelated paths with no shared parent are refused" \
"/home/archipelago/Projects/archy" \
"/opt/archipelago/web-ui" \
"refuse"
# 6) A refused case names both resolved paths and the 2026-07-31 incident
# on stderr, so a future operator understands why rather than
# reflexively retrying with a force flag.
MSG="$(assert_safe_same_host_deploy "/home/archipelago/Projects/archy-phase13" "/home/archipelago/Projects/archy" 2>&1 >/dev/null)"
if echo "$MSG" | grep -q '/home/archipelago/Projects/archy-phase13' \
&& echo "$MSG" | grep -q '/home/archipelago/Projects/archy' \
&& echo "$MSG" | grep -q '2026-07-31'; then
ok "refusal message names both resolved paths and the 2026-07-31 incident"
else
bad "refusal message missing an expected component: $MSG"
fi
say ""
say "== ${PASS} passed, ${FAIL} failed =="
[ "$FAIL" -eq 0 ]