H-337: add local Captain acceptance fixture (#63)

* H-337: add local Captain acceptance fixture

* fix(H-337): close acceptance fixture review gaps

* fix(H-337): reject non-string skill metadata

---------

Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
Rogee
2026-08-20 20:05:40 +08:00
committed by GitHub
co-authored by rogee
parent 60ad320e8d
commit abb1bed424
12 changed files with 818 additions and 19 deletions
+99
View File
@@ -0,0 +1,99 @@
#!/bin/sh
set -eu
root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
tmp=$(mktemp -d "${TMPDIR:-/tmp}/gochat-acceptance-scripts.XXXXXX")
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
fail() {
echo "acceptance scripts test failed: $1" >&2
exit 1
}
make_source() {
mkdir -p "$1"
cat > "$1/SKILL.md" <<'EOF'
---
name: fixture
description: Local fixture
---
Use the local fixture.
EOF
printf '%s\n' 'Local assistant instructions.' > "$1/SOUL.md"
}
expect_staging_failure() {
if GANBING_SOURCE=$1 "$root/scripts/stage_captain_assets.sh" "$tmp/output" > "$tmp/staging.log" 2>&1; then
fail "$2 was accepted"
fi
if grep -qx 'skill_status=active' "$tmp/output/manifest.txt" 2>/dev/null; then
fail "$2 generated an active manifest"
fi
rm -rf "$tmp/output"
}
mkdir "$tmp/bin"
cat > "$tmp/bin/docker" <<'EOF'
#!/bin/sh
case " $* " in
*" config --quiet "*) ;;
*" ps --status running --services "*)
printf '%s\n' captain-fixture gochat
;;
*" exec -T postgres "*)
if [ -n "${MOCK_PROVIDER_CONFIG:-}" ]; then printf '%s\n' 1; else printf '%s\n' 0; fi
;;
*" exec -T gochat curl "*|*" exec -T gochat sh "*) ;;
*) echo "unexpected docker invocation" >&2; exit 2 ;;
esac
EOF
chmod 700 "$tmp/bin/docker"
make_source "$tmp/clean"
clean_before=$(find "$tmp/clean" -type f -exec sha256sum {} +)
GANBING_SOURCE=$tmp/clean PATH=$tmp/bin:$PATH "$root/scripts/preflight_acceptance.sh" > "$tmp/preflight-clean.log" 2>&1 || fail "clean preflight failed"
[ "$clean_before" = "$(find "$tmp/clean" -type f -exec sha256sum {} +)" ] || fail "staging modified its source"
provider_config='{"base_url":"http://captain-fixture:8080/v1","api_key":"must-not-leak","extra":true}'
if GANBING_SOURCE=$tmp/clean MOCK_PROVIDER_CONFIG=$provider_config PATH=$tmp/bin:$PATH "$root/scripts/preflight_acceptance.sh" > "$tmp/preflight-override.log" 2>&1; then
fail "nonempty database provider override was accepted"
fi
grep -F "$provider_config" "$tmp/preflight-override.log" >/dev/null && fail "provider override leaked to logs"
make_source "$tmp/malformed"
cat > "$tmp/malformed/SKILL.md" <<'EOF'
---
name: fixture
---
description: This is body text, not frontmatter.
EOF
expect_staging_failure "$tmp/malformed" "missing frontmatter description"
case_number=0
for field in name description; do
for value in null Null NULL '~' '""' "''" '[]' '{}' true 123 .5 '[fixture]'; do
case_number=$((case_number + 1))
source="$tmp/frontmatter-$case_number"
make_source "$source"
sed -i "s/^$field:.*/$field: $value/" "$source/SKILL.md"
expect_staging_failure "$source" "$field value $value"
done
done
for fixture in json yaml markdown; do
source="$tmp/credential-$fixture"
make_source "$source"
case "$fixture" in
json) printf '%s\n' '{"api_key":"fixture-secret"}' > "$source/config.json" ;;
yaml) printf '%s\n' 'access_token: fixture-token' > "$source/reference.md" ;;
markdown) printf '%s\n' '**password** = `fixture-password`' > "$source/reference.md" ;;
esac
expect_staging_failure "$source" "$fixture credential assignment"
done
GANBING_SOURCE=$tmp/clean "$root/scripts/stage_captain_assets.sh" "$tmp/output" >/dev/null
[ "$clean_before" = "$(find "$tmp/clean" -type f -exec sha256sum {} +)" ] || fail "staging modified its source"
grep -qx 'skill_status=active' "$tmp/output/manifest.txt" || fail "valid string frontmatter did not generate an active manifest"
find "$tmp/output" -type f -exec stat -c '%a' {} + | grep -vx 600 >/dev/null && fail "staged file permissions are not 0600"
echo "acceptance scripts tests passed"
+55
View File
@@ -0,0 +1,55 @@
#!/bin/sh
set -eu
root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)
stage=$(mktemp -d "${TMPDIR:-/tmp}/gochat-captain-stage.XXXXXX")
trap 'rm -rf "$stage"' EXIT HUP INT TERM
"$root/scripts/stage_captain_assets.sh" "$stage"
compose() {
docker compose -f "$root/compose.yaml" -f "$root/compose.acceptance.yaml" "$@"
}
compose config --quiet
compose ps --status running --services | grep -qx captain-fixture || {
echo "preflight failed: captain-fixture is not running" >&2
exit 1
}
compose ps --status running --services | grep -qx gochat || {
echo "preflight failed: gochat is not running" >&2
exit 1
}
compose exec -T gochat curl -fsS --retry 30 --retry-delay 1 --retry-connrefused --max-time 2 http://127.0.0.1:3000/health >/dev/null
overrides=$(compose exec -T postgres sh -eu -c '
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc "
SELECT count(*) FROM installation_configs
WHERE deleted_at IS NULL AND (
(name = '\''COPILOT_PROVIDER_CONFIG'\'' AND COALESCE(value, '\'''\'') <> '\'''\'') OR
(name = '\''COPILOT_CHAT_API_KEY'\'' AND value <> '\''acceptance-fixture-not-a-secret'\'') OR
(name = '\''COPILOT_EMBEDDING_API_KEY'\'' AND value <> '\'''\'')
)"
')
[ "$overrides" = "0" ] || {
echo "preflight failed: database Copilot settings override the local fixture" >&2
exit 1
}
compose exec -T gochat sh -eu -c '
test -n "$GOCHAT_COPILOT_PROVIDER_CONFIG"
test "$GOCHAT_COPILOT_CHAT_API_KEY" = acceptance-fixture-not-a-secret
case "$GOCHAT_COPILOT_PROVIDER_CONFIG" in
*http://captain-fixture:8080/v1*) ;;
*) exit 1 ;;
esac
knowledge=$(curl -fsS http://captain-fixture:8080/knowledge)
test -n "$knowledge"
case "$knowledge" in *KBASE-LOCAL-2026*) ;; *) exit 1 ;; esac
chat=$(curl -fsS -H "Content-Type: application/json" -d "{\"model\":\"gpt-5.6-luna\",\"messages\":[{\"role\":\"user\",\"content\":\"preflight\"}]}" http://captain-fixture:8080/v1/chat/completions)
case "$chat" in *chat.completion*) ;; *) exit 1 ;; esac
embedding=$(curl -fsS -H "Content-Type: application/json" -d "{\"model\":\"fixture-embedding\",\"input\":[\"preflight\"]}" http://captain-fixture:8080/v1/embeddings)
case "$embedding" in *\"object\":\"list\"*) ;; *) exit 1 ;; esac
'
echo "acceptance preflight passed: provider=injected fixture=reachable knowledge=nonempty captain_assets=staged"
+115
View File
@@ -0,0 +1,115 @@
#!/bin/sh
set -eu
src=${GANBING_SOURCE:-}
dst=${1:-}
fail() {
echo "captain asset staging failed: $1" >&2
exit 1
}
[ -n "$src" ] || fail "set GANBING_SOURCE to the read-only source directory"
[ -d "$src" ] || fail "source directory does not exist"
[ -n "$dst" ] || fail "usage: GANBING_SOURCE=/path $0 DESTINATION"
if [ -e "$dst" ]; then
[ -d "$dst" ] && [ -z "$(find "$dst" -mindepth 1 -print -quit)" ] || fail "destination must be absent or empty"
fi
src=$(realpath "$src")
case "$(realpath -m "$dst")/" in
"$src"/*) fail "destination must be outside the source directory" ;;
esac
[ "$(find "$src" -type l | wc -l | tr -d ' ')" -eq 0 ] || fail "symbolic links are not allowed"
[ "$(find "$src" ! -type d ! -type f | wc -l | tr -d ' ')" -eq 0 ] || fail "special files are not allowed"
[ "$(find "$src" -type f -name SKILL.md | wc -l | tr -d ' ')" -eq 1 ] || fail "exactly one SKILL.md is required"
[ "$(find "$src" -type f -name SOUL.md | wc -l | tr -d ' ')" -eq 1 ] || fail "exactly one SOUL.md is required"
skill=$(find "$src" -type f -name SKILL.md -print -quit)
soul=$(find "$src" -type f -name SOUL.md -print -quit)
awk '
function present(line, first, lower) {
sub(/^[^:]*:[[:space:]]*/, "", line)
sub(/[[:space:]]+#.*$/, "", line)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", line)
first = substr(line, 1, 1)
if (first == "\"" || first == "\047")
return length(line) > 2 && substr(line, length(line), 1) == first && substr(line, 2, length(line) - 2) !~ /^[[:space:]]*$/
lower = tolower(line)
if (line == "" || line ~ /^#/ || lower ~ /^(~|null|true|false|yes|no|on|off|[+-]?\.(inf|nan))$/)
return 0
if (first == "[" || first == "{" || first == "|" || first == ">" || first == "!" || first == "&" || first == "*")
return 0
if (lower ~ /^[+-]?0[xob][0-9a-f_]+$/ || lower ~ /^[+-]?([0-9][0-9_]*(\.[0-9_]*)?|\.[0-9_]+)(e[+-]?[0-9_]+)?$/ || lower ~ /^[0-9][0-9_.:tz+-]*$/)
return 0
return 1
}
NR == 1 { if ($0 != "---") exit 1; next }
$0 == "---" { closed = 1; exit }
/^name:[[:space:]]*/ { if (present($0)) name = 1 }
/^description:[[:space:]]*/ { if (present($0)) description = 1 }
END { if (!closed || !name || !description) exit 1 }
' "$skill" || fail "SKILL.md frontmatter must contain name and description"
[ "$(wc -c < "$soul" | tr -d ' ')" -le 20000 ] || fail "SOUL.md exceeds the Assistant instructions UI limit"
[ "$(find "$src" -type f ! -name '*.md' ! -name '*.json' | wc -l | tr -d ' ')" -eq 0 ] || fail "only Markdown and JSON source files are allowed"
find "$src" -type f -exec sh -c '
for file do
case "$(file -b --mime-encoding "$file")" in
utf-8|us-ascii) ;;
*) exit 1 ;;
esac
done
' sh {} + || fail "all staged files must be UTF-8"
credential_scan=0
credential_pattern="['\"\`*_]?([[:alnum:]_.-]*[_-])?(api[ _-]?key|token|secret|password|authorization)['\"\`*_]*[[:space:]]*[:=][[:space:]]*['\"\`]?[^[:space:]'\"\`\$<{][^[:space:]]*"
grep -ERqi "$credential_pattern" "$src" || credential_scan=$?
case "$credential_scan" in
0) fail "a possible plaintext credential was detected" ;;
1) ;;
*) fail "credential scan could not verify the source" ;;
esac
mkdir -m 700 -p "$dst/captain-skill/references" "$dst/quarantine"
instructions="$dst/captain-skill/instructions.md"
awk 'NR == 1 && $0 == "---" { frontmatter=1; next } frontmatter && $0 == "---" { frontmatter=0; body=1; next } body { print }' "$skill" > "$instructions"
chmod 600 "$instructions"
[ -s "$instructions" ] || fail "SKILL.md body is empty"
[ "$(wc -c < "$instructions" | tr -d ' ')" -le 32768 ] || fail "SKILL.md body exceeds the Captain Skill limit"
install -m 600 "$soul" "$dst/assistant-instructions.md"
instruction_bytes=$(wc -c < "$instructions" | tr -d ' ')
export dst instruction_bytes
find "$src" -type f -name '*.md' ! -name SKILL.md ! -name SOUL.md -exec sh -eu -c '
for reference do
bytes=$(wc -c < "$reference" | tr -d " ")
[ "$bytes" -le 65536 ] || exit 1
key=$(printf "%s" "$reference" | sha256sum | cut -c1-12)
# Conservative allowance for JSON/tool wrappers and the complete reference-key list.
if [ $((instruction_bytes + bytes + 1200)) -le 8000 ]; then
install -m 600 "$reference" "$dst/captain-skill/references/reference-$key.md"
else
install -m 600 "$reference" "$dst/quarantine/reference-$key.md"
fi
done
' sh {} + || fail "a reference exceeds the Captain reference limit"
safe=$(find "$dst/captain-skill/references" -type f | wc -l | tr -d ' ')
quarantined=$(find "$dst/quarantine" -type f | wc -l | tr -d ' ')
total=$((safe + quarantined))
[ "$total" -le 20 ] || fail "Captain supports at most 20 references"
cat > "$dst/manifest.txt" <<EOF
skill_name=ganbing-local-acceptance
skill_description=Isolated local acceptance skill
skill_status=active
reference_count=$total
reference_importable=$safe
reference_quarantined=$quarantined
source_modified=false
EOF
chmod 600 "$dst/manifest.txt"
echo "captain assets staged: core=ready references=$safe quarantined=$quarantined"