roles.sh had been red for a while, hiding everything behind whatever failed first: - planner.md was bumped to 4.2.0 (board-triggered split mode) without updating the test's hardcoded 4.1.0 expectations. - reviewer.md had drifted two minor versions ahead of its catalog.json entry (1.6.0 registered vs. 1.8.0 actual) across two prior commits that never touched the catalog; catalog.json now matches. - researcher.json's generic Bash(*) grant is an intentional, already deployed capability (docs/intent/research-pipeline-researcher-shell.md) that the blanket "no self-close settings file may hold Bash(*)" loop didn't know about; researcher.json is now a documented exception with its own check that the required deny-prefix list stays in place. - fixer.md's Inspector/Fixer split reworded "erfolgreichen Review" to "erfolgreichem Review"; the substring assertion didn't follow. - the operator.json permission check had a jq and/or precedence bug (missing parens around length == N clauses) that made it error out rather than actually check anything. agents/tests/roles.sh, agents/tests/fixer-role.sh and agents/tests/inspector-role.sh all pass end to end now. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T7UVrKsttMP9a6FiGFna9b
1143 lines
58 KiB
Bash
1143 lines
58 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
|
agents_dir="$repo_root/agents"
|
|
catalog="$agents_dir/catalog.json"
|
|
loader="$agents_dir/AGENTS.md"
|
|
orga="$agents_dir/orga.md"
|
|
admin="$agents_dir/admin.md"
|
|
agent_manager="$agents_dir/agent-manager.md"
|
|
graphics="$agents_dir/graphics.md"
|
|
reviewer="$agents_dir/reviewer.md"
|
|
builder="$agents_dir/builder.md"
|
|
planner="$agents_dir/planner.md"
|
|
researcher="$agents_dir/researcher.md"
|
|
operator="$agents_dir/operator.md"
|
|
fixer="$agents_dir/fixer.md"
|
|
inspector="$agents_dir/inspector.md"
|
|
settings_dir="$agents_dir/claude-settings"
|
|
orchestration="$repo_root/docs/herdr-orchestration.md"
|
|
self_close_design="$agents_dir/design/herdr-self-close-entwurf.md"
|
|
conventions="$agents_dir/conventions.md"
|
|
graphify_skill="$repo_root/.agents/skills/graphify/SKILL.md"
|
|
graphify_version="$repo_root/.agents/skills/graphify/.graphify_version"
|
|
tick='`'
|
|
|
|
fail() {
|
|
printf 'FAIL: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
assert_contains() {
|
|
local file=$1
|
|
local text=$2
|
|
grep -Fq -- "$text" "$file" || fail "$file enthält nicht: $text"
|
|
}
|
|
|
|
assert_not_contains() {
|
|
local file=$1
|
|
local text=$2
|
|
if grep -Fq -- "$text" "$file"; then
|
|
fail "$file enthält unerlaubt: $text"
|
|
fi
|
|
}
|
|
|
|
assert_text_contains() {
|
|
local label=$1
|
|
local content=$2
|
|
local text=$3
|
|
[[ $content == *"$text"* ]] || fail "$label enthält nicht: $text"
|
|
}
|
|
|
|
assert_text_order() {
|
|
local label=$1
|
|
local content=$2
|
|
local before=$3
|
|
local after=$4
|
|
local remainder
|
|
|
|
assert_text_contains "$label" "$content" "$before"
|
|
assert_text_contains "$label" "$content" "$after"
|
|
remainder=${content#*"$before"}
|
|
[[ $remainder == *"$after"* ]] \
|
|
|| fail "$label: erwartete Reihenfolge fehlt: $before vor $after"
|
|
}
|
|
|
|
role_header_value() {
|
|
local file=$1
|
|
local key=$2
|
|
sed -n "2,/^---$/s/^${key}: //p" "$file"
|
|
}
|
|
|
|
assert_role_header_matches_catalog() {
|
|
local role_id=$1
|
|
local definition=$2
|
|
local expected_version=$3
|
|
local role_file="$agents_dir/$definition"
|
|
local catalog_definition catalog_status catalog_version
|
|
|
|
[[ $(sed -n '1p' "$role_file") == --- ]] \
|
|
|| fail "$definition: Rollenheader fehlt"
|
|
catalog_definition=$(jq -r --arg id "$role_id" \
|
|
'.roles[] | select(.id == $id) | .definition' "$catalog")
|
|
catalog_status=$(jq -r --arg id "$role_id" \
|
|
'.roles[] | select(.id == $id) | .status' "$catalog")
|
|
catalog_version=$(jq -r --arg id "$role_id" \
|
|
'.roles[] | select(.id == $id) | .version' "$catalog")
|
|
|
|
[[ $catalog_definition == "$definition" ]] \
|
|
|| fail "$role_id: Definitionsdatei weicht vom Katalog ab"
|
|
[[ $catalog_version == "$expected_version" ]] \
|
|
|| fail "$role_id: erwartete Katalogversion $expected_version"
|
|
[[ $(role_header_value "$role_file" role-id) == "$role_id" ]] \
|
|
|| fail "$definition: role-id weicht vom Katalog ab"
|
|
[[ $(role_header_value "$role_file" role-version) == "$catalog_version" ]] \
|
|
|| fail "$definition: role-version weicht vom Katalog ab"
|
|
[[ $(role_header_value "$role_file" status) == "$catalog_status" ]] \
|
|
|| fail "$definition: status weicht vom Katalog ab"
|
|
}
|
|
|
|
jq -e '.schema_version == 1' "$catalog" >/dev/null \
|
|
|| fail "unbekannte Katalogversion"
|
|
jq -e '.profile_policy.sol_high_requires_measured_quality == true and
|
|
.profile_policy.defaults_are_configurable == true and
|
|
.profile_policy.running_processes_are_unchanged == true' "$catalog" \
|
|
>/dev/null || fail "Modellprofil-Richtlinie ist nicht sicher konfiguriert"
|
|
|
|
duplicate_ids=$(jq -r '[.roles[].id] | length == (unique | length)' "$catalog")
|
|
[[ $duplicate_ids == true ]] || fail "doppelte Rollen-ID im Katalog"
|
|
|
|
profile_ids=$(jq -r '[.model_profiles[].id] | length == (unique | length)' "$catalog")
|
|
[[ $profile_ids == true ]] || fail "doppelte Modellprofil-ID im Katalog"
|
|
|
|
assert_profile() {
|
|
local id=$1
|
|
local model=$2
|
|
local effort=$3
|
|
local actual_model actual_effort
|
|
actual_model=$(jq -r --arg id "$id" \
|
|
'.model_profiles[] | select(.id == $id) | (.runtimes.codex.model // "null")' "$catalog")
|
|
actual_effort=$(jq -r --arg id "$id" \
|
|
'.model_profiles[] | select(.id == $id) | (.runtimes.codex.reasoning_effort // "null")' "$catalog")
|
|
[[ $actual_model == "$model" ]] || fail "$id: unerwarteter Modelldefault"
|
|
[[ $actual_effort == "$effort" ]] || fail "$id: unerwarteter Reasoning-Default"
|
|
}
|
|
|
|
assert_profile orchestrator gpt-5.6-terra medium
|
|
assert_profile administrator gpt-5.6-terra high
|
|
assert_profile agent-manager gpt-5.6-sol high
|
|
assert_profile reviewer gpt-5.6-sol high
|
|
assert_profile planner gpt-5.6-terra high
|
|
assert_profile researcher gpt-5.6-terra medium
|
|
assert_profile operator gpt-5.6-terra high
|
|
assert_profile grafiker gpt-5.6-terra medium
|
|
assert_profile orga gpt-5.6-terra medium
|
|
assert_profile finance gpt-5.6-luna medium
|
|
assert_profile dispatcher null null
|
|
|
|
jq -e '.model_profiles[] | select(.id == "dispatcher") | .execution == "no-llm"' \
|
|
"$catalog" >/dev/null || fail "Dispatcher ist nicht als no-llm markiert"
|
|
jq -e '.model_profiles[] | select(.id == "administrator") |
|
|
.runtimes.codex.fallback.model == "gpt-5.6-sol" and
|
|
.runtimes.codex.fallback.reasoning_effort == "high" and
|
|
.runtimes.codex.fallback.condition == "nur bei gemessener Qualitätsnotwendigkeit"' \
|
|
"$catalog" >/dev/null || fail "Administrator-Eskalation ist falsch"
|
|
jq -e '.model_profiles[] | select(.id == "researcher") |
|
|
.runtimes.codex.fallback.model == "gpt-5.6-luna" and
|
|
.runtimes.codex.fallback.reasoning_effort == "low" and .runtimes.codex.fallback.condition == "Routineinventur"' \
|
|
"$catalog" >/dev/null || fail "Researcher-Routineprofil ist falsch"
|
|
jq -e '.model_profiles[] | select(.id == "grafiker") |
|
|
.runtimes.codex.fallback.model == "gpt-5.6-luna" and
|
|
.runtimes.codex.fallback.reasoning_effort == "low" and .runtimes.codex.fallback.condition == "reines Resize"' \
|
|
"$catalog" >/dev/null || fail "Grafik-Resizeprofil ist falsch"
|
|
jq -e '.model_profiles[] | select(.id == "finance") |
|
|
.legacy_fallbacks == "preserve-existing-historical-fallbacks"' \
|
|
"$catalog" >/dev/null || fail "historische Finance-Fallbacks nicht geschützt"
|
|
|
|
while IFS= read -r profile_id; do
|
|
[[ $profile_id == dispatcher ]] && continue
|
|
jq -e --arg id "$profile_id" '
|
|
.model_profiles[] | select(.id == $id) |
|
|
(.runtimes | has("codex") and has("claude")) and
|
|
(.runtimes.codex.model | startswith("gpt-5.6-")) and
|
|
(.runtimes.claude.model | startswith("claude-")) and
|
|
(.runtimes.codex.reasoning_effort | IN("low", "medium", "high")) and
|
|
(.runtimes.claude.reasoning_effort | IN("low", "medium", "high"))' \
|
|
"$catalog" >/dev/null || fail "$profile_id: unvollständige oder ungültige Runtime"
|
|
done < <(jq -r '.model_profiles[].id' "$catalog")
|
|
jq -e '.model_profiles[] | select(.id == "dispatcher") |
|
|
(.runtimes | has("none")) and (.runtimes | has("codex") | not) and
|
|
(.runtimes | has("claude") | not) and .runtime_policy == "no-llm; kein Codex- oder Claude-Prozess"' \
|
|
"$catalog" >/dev/null || fail "Dispatcher-Ausnahme ist nicht dokumentiert"
|
|
jq -e '[.model_profiles[].runtimes.claude.fallback? // empty |
|
|
select(.model != null and .reasoning_effort != null)] | length >= 5' "$catalog" \
|
|
>/dev/null || fail "Claude-Routinefallbacks fehlen"
|
|
|
|
# Planner ist ab 4.0.0 die einzige, ausdrücklich dokumentierte Ausnahme von
|
|
# defaultMode dontAsk (Pilotwechsel auf auto, um AskUserQuestion nutzbar zu
|
|
# machen; siehe planner.md, Abschnitt "Direkter Dialog mit dem Nutzer").
|
|
while IFS=$'\t' read -r profile_id settings_file; do
|
|
[[ -n $settings_file ]] || fail "$profile_id: Claude settings_file fehlt"
|
|
[[ -f "$repo_root/$settings_file" ]] \
|
|
|| fail "$profile_id: Claude settings_file fehlt auf dem Dateisystem"
|
|
expected_default_mode=dontAsk
|
|
[[ $profile_id == planner ]] && expected_default_mode=auto
|
|
jq -e --arg mode "$expected_default_mode" '.permissions.defaultMode == $mode and
|
|
(.permissions.allow | type == "array") and
|
|
(.permissions.deny | type == "array")' "$repo_root/$settings_file" \
|
|
>/dev/null \
|
|
|| fail "$profile_id: ungültige Claude-Settings-Struktur (defaultMode erwartet: $expected_default_mode)"
|
|
while IFS= read -r grant; do
|
|
case "$grant" in
|
|
*nixos-rebuild\ switch*|*git\ push*|*agenix\ -d*|*sudo*)
|
|
fail "$profile_id: gefährlicher Befehl als Claude-Allowlist-Grant" ;;
|
|
esac
|
|
done < <(jq -r '.permissions.allow[]' "$repo_root/$settings_file")
|
|
assert_contains "$repo_root/$settings_file" "defaultMode"
|
|
if grep -Fq -- ".claude/settings.local.json" "$repo_root/$settings_file"; then
|
|
fail "$profile_id: persönliche settings.local.json referenziert"
|
|
fi
|
|
done < <(jq -r '.model_profiles[] | select(.runtimes.claude.settings_file?) |
|
|
[.id, .runtimes.claude.settings_file] | @tsv' "$catalog")
|
|
|
|
jq -e '.model_profiles[] | select(.id == "dispatcher") |
|
|
(.runtimes.claude.settings_file? == null)' "$catalog" >/dev/null \
|
|
|| fail "Dispatcher darf keine Claude-Settings voraussetzen"
|
|
|
|
self_close_grant='Bash(/etc/scripts/close-my-pane.sh)'
|
|
self_close_settings=(
|
|
administrator.json
|
|
agent-manager.json
|
|
grafiker.json
|
|
orga.json
|
|
planner.json
|
|
researcher.json
|
|
)
|
|
self_close_roles=(
|
|
"$admin"
|
|
"$agent_manager"
|
|
"$graphics"
|
|
"$orga"
|
|
"$researcher"
|
|
)
|
|
|
|
assert_role_header_matches_catalog graphics graphics.md 2.0.0
|
|
assert_role_header_matches_catalog administrator admin.md 3.1.0
|
|
assert_role_header_matches_catalog agent-manager agent-manager.md 2.3.0
|
|
assert_role_header_matches_catalog orga orga.md 2.1.1
|
|
assert_role_header_matches_catalog planner planner.md 4.2.0
|
|
assert_role_header_matches_catalog researcher researcher.md 1.0.0
|
|
assert_role_header_matches_catalog operator operator.md 1.0.0
|
|
assert_role_header_matches_catalog fixer fixer.md 2.0.0
|
|
assert_role_header_matches_catalog inspector inspector.md 1.0.0
|
|
|
|
for settings_name in "${self_close_settings[@]}"; do
|
|
settings_file="$settings_dir/$settings_name"
|
|
grant_count=$(jq --arg grant "$self_close_grant" \
|
|
'[.permissions.allow[] | select(. == $grant)] | length' "$settings_file")
|
|
[[ $grant_count -eq 1 ]] \
|
|
|| fail "$settings_name: Self-Close-Wrapper muss exakt einmal erlaubt sein"
|
|
close_grant_count=$(jq \
|
|
'[.permissions.allow[] | select(contains("close-my-pane"))] | length' \
|
|
"$settings_file")
|
|
[[ $close_grant_count -eq 1 ]] \
|
|
|| fail "$settings_name: nur der exakte Self-Close-Wrapper darf erlaubt sein"
|
|
if [[ $settings_name != researcher.json ]]; then
|
|
broad_bash_grants=$(jq \
|
|
'[.permissions.allow[] | select(. == "Bash(*)")] | length' \
|
|
"$settings_file")
|
|
[[ $broad_bash_grants -eq 0 ]] \
|
|
|| fail "$settings_name: Self-Close darf kein pauschales Bash-Recht erhalten"
|
|
fi
|
|
done
|
|
|
|
# researcher.json ist die dokumentierte Ausnahme von der pauschalen
|
|
# Bash(*)-Sperre oben (docs/intent/research-pipeline-researcher-shell.md,
|
|
# Claude-Pfad, ausschließlich blunix): generischer Bash-Zugriff zur reinen
|
|
# Informationsbeschaffung, abgesichert über eine Deny-Präfixliste statt
|
|
# einer Befehls-Allowlist. Diese Deny-Liste bleibt hier verpflichtend.
|
|
researcher_broad_bash_grants=$(jq \
|
|
'[.permissions.allow[] | select(. == "Bash(*)")] | length' \
|
|
"$settings_dir/researcher.json")
|
|
[[ $researcher_broad_bash_grants -eq 1 ]] \
|
|
|| fail "researcher.json: erwarteter generischer Bash-Zugriff fehlt"
|
|
researcher_required_denies=(
|
|
"Bash(sudo *)" "Bash(rm *)" "Bash(mv *)" "Bash(chmod *)" "Bash(chown *)"
|
|
"Bash(kill *)" "Bash(systemctl start *)" "Bash(systemctl stop *)"
|
|
"Bash(systemctl restart *)" "Bash(systemctl enable *)"
|
|
"Bash(systemctl disable *)" "Bash(nixos-rebuild switch *)"
|
|
"Bash(nixos-rebuild test *)" "Bash(reboot*)" "Bash(shutdown *)"
|
|
"Bash(ssh *)" "Bash(agenix *)"
|
|
)
|
|
for deny_entry in "${researcher_required_denies[@]}"; do
|
|
deny_count=$(jq --arg deny "$deny_entry" \
|
|
'[.permissions.deny[] | select(. == $deny)] | length' \
|
|
"$settings_dir/researcher.json")
|
|
[[ $deny_count -eq 1 ]] \
|
|
|| fail "researcher.json: fehlender Deny-Eintrag $deny_entry für den generischen Bash-Zugriff"
|
|
done
|
|
|
|
for settings_file in "$settings_dir"/*.json; do
|
|
settings_name=${settings_file##*/}
|
|
# Seit 03.09.2026 (Nutzerentscheidung, siehe docs/herdr-orchestration.md,
|
|
# Abschnitt "Bestätigungsfreier Spezialisten-Start durch den Orchestrator")
|
|
# führt ausschließlich orchestrator.json ein breites Bash(herdr *)-Recht;
|
|
# die vorher engeren Bash(herdr agent *)/Bash(herdr pane *)-Rechte sind
|
|
# dadurch obsolet und für jede Settings-Datei unzulässig.
|
|
narrow_herdr_grants=$(jq \
|
|
'[.permissions.allow[] |
|
|
select(contains("herdr pane") or contains("herdr agent") or
|
|
contains("herdr-pane-close"))] | length' \
|
|
"$settings_file")
|
|
[[ $narrow_herdr_grants -eq 0 ]] \
|
|
|| fail "$settings_name: enges Herdr-Agent-/Pane-Recht ist obsolet, nur Bash(herdr *) auf orchestrator.json ist vorgesehen"
|
|
broad_herdr_grants=$(jq \
|
|
'[.permissions.allow[] | select(. == "Bash(herdr *)")] | length' \
|
|
"$settings_file")
|
|
if [[ $settings_name == orchestrator.json ]]; then
|
|
[[ $broad_herdr_grants -eq 1 ]] \
|
|
|| fail "Orchestrator braucht genau sein breites Herdr-Recht (Bash(herdr *))"
|
|
else
|
|
[[ $broad_herdr_grants -eq 0 ]] \
|
|
|| fail "$settings_name: freies Herdr-Recht (Bash(herdr *)) ist ausschließlich dem Orchestrator vorbehalten"
|
|
fi
|
|
|
|
wrapper_grants=$(jq --arg grant "$self_close_grant" \
|
|
'[.permissions.allow[] | select(. == $grant)] | length' "$settings_file")
|
|
case " ${self_close_settings[*]} " in
|
|
*" $settings_name "*)
|
|
[[ $wrapper_grants -eq 1 ]] \
|
|
|| fail "$settings_name: erwarteter Self-Close-Wrapper fehlt"
|
|
;;
|
|
*)
|
|
[[ $wrapper_grants -eq 0 ]] \
|
|
|| fail "$settings_name: unerwartetes Self-Close-Recht"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
for role_file in "${self_close_roles[@]}"; do
|
|
assert_contains "$role_file" 'Session-Abschluss (Self-Close)'
|
|
assert_contains "$role_file" '/etc/scripts/close-my-pane.sh'
|
|
assert_contains "$role_file" '$HERDR_PANE_ID'
|
|
assert_contains "$role_file" 'ignoriert jedes Argument'
|
|
assert_contains "$role_file" '**eigene**'
|
|
assert_contains "$role_file" 'niemals eine fremde'
|
|
assert_contains "$role_file" 'COMPLETE'
|
|
assert_contains "$role_file" 'BLOCKED: …'
|
|
assert_contains "$role_file" 'nachweislich gelesen'
|
|
assert_contains "$role_file" 'ausdrücklich zum Schließen freigegeben'
|
|
assert_contains "$role_file" 'bleibst idle'
|
|
assert_contains "$role_file" 'Fehlt eine Bedingung'
|
|
assert_contains "$role_file" '**Codex 0.145:**'
|
|
assert_contains "$role_file" '`sandbox_permissions=require_escalated`'
|
|
assert_contains "$role_file" '`workspace-write` wird blockiert'
|
|
assert_contains "$role_file" 'Keine Argumente, kein'
|
|
assert_contains "$role_file" '**Claude:**'
|
|
assert_contains "$role_file" 'wegen des Sessionlimits noch aus'
|
|
done
|
|
|
|
# Nur den Repository-Vertrag prüfen. Externe Home- und System-Rules sind keine
|
|
# Harness-Voraussetzung und werden hier weder gelesen noch auf Existenz geprüft.
|
|
assert_contains "$orchestration" 'Testpane `w5:pQ`'
|
|
assert_contains "$orchestration" '`sandbox_permissions=require_escalated`'
|
|
assert_contains "$orchestration" '`~/.codex/rules/default.rules`'
|
|
assert_contains "$orchestration" '`/etc/codex/rules`'
|
|
assert_contains "$orchestration" 'symlinked User-Rules'
|
|
assert_contains "$orchestration" 'Claude-End-to-End-Test bleibt wegen des Sessionlimits ungetestet'
|
|
assert_contains "$self_close_design" 'rohen Herdr-Shell'
|
|
assert_contains "$self_close_design" 'Testpane `w5:pQ`'
|
|
assert_contains "$self_close_design" '`sandbox_permissions=require_escalated`'
|
|
assert_contains "$self_close_design" 'setzt keine Datei im'
|
|
assert_contains "$self_close_design" 'Home-Verzeichnis voraus'
|
|
|
|
assert_not_contains "$reviewer" 'Session-Abschluss (Self-Close)'
|
|
assert_not_contains "$reviewer" '/etc/scripts/close-my-pane.sh'
|
|
jq -e --arg grant "$self_close_grant" \
|
|
'[.permissions.allow[] | select(. == $grant)] | length == 0' \
|
|
"$settings_dir/reviewer.json" >/dev/null \
|
|
|| fail "Reviewer darf kein Self-Close-Recht erhalten"
|
|
|
|
assert_catalog_role_version() {
|
|
local role_id=$1
|
|
local expected_version=$2
|
|
local actual_version
|
|
actual_version=$(jq -r --arg id "$role_id" \
|
|
'.roles[] | select(.id == $id) | .version' "$catalog")
|
|
[[ $actual_version == "$expected_version" ]] \
|
|
|| fail "$role_id: erwartete Rollenvertragsversion $expected_version"
|
|
}
|
|
|
|
# Self-Close öffnet eine neue Vertrauensgrenze und ist daher ein Major-Bump.
|
|
assert_catalog_role_version graphics 2.0.0
|
|
# administrator 3.1.0: neuer Punkt im Ergebnisformat (Eskalationsanlass in
|
|
# der Chat-Übergabe) - rückwärtskompatibles neues Ausgabeformat, Minor-Bump.
|
|
assert_catalog_role_version administrator 3.1.0
|
|
# Eskalationsannahme (Fixer) bzw. Rückfrage-Kanal-Pilot sind rückwärts-
|
|
# kompatible neue Aufgaben, daher Minor-Bump statt Major. 2.2.0: zusätzlicher
|
|
# Ergebnisformat-Punkt (Eskalationsanlass), ebenfalls Minor. 2.3.0 bereinigt
|
|
# dessen Anforderungsgrundlage wortsinngleich zum Rollenvertrag.
|
|
assert_catalog_role_version agent-manager 2.3.0
|
|
assert_catalog_role_version orga 2.1.1
|
|
# reviewer 1.2.0: optionale Kennzahlen-Momentaufnahme und Eskalationsanlass
|
|
# im Systemaudit-Bericht - neues, rückwärtskompatibles Ausgabeformat, Minor.
|
|
# 1.3.0: nichtpersistierende Graphify-Übergabe für erlaubte read-only Queries;
|
|
# die bestehende Save-/Reflect-Sperre bleibt erhalten, daher ebenfalls Minor.
|
|
# 1.4.0 (docs/intent/git-intent-board-feedback.md, Abschnitt "Reviewer: keine
|
|
# neue Aufgabenart"): Zusatzabsatz für den board-getriggerten Review - bleibt
|
|
# dieselbe Diff-Review-Aufgabe mit festem Commit-Scope statt einer dritten
|
|
# Aufgabenart, nutzt die bestehende docs/review/*.md-Schreibausnahme ohne
|
|
# neue Rechte, headless-Start gilt ausschließlich für diesen Trigger. Kein
|
|
# Trust-Boundary-Wechsel, daher Minor statt Major.
|
|
# 1.5.0 (2026-09-07): Backend-Wechsel Codex -> Claude Code (`claude -p`)
|
|
# für den board-getriggerten Review, ausgelöst durch dasselbe erschöpfte
|
|
# Codex-Wochenkontingent, das schon am Vortag den Builder betroffen hat
|
|
# (siehe assert_catalog_role_version builder oben). Effektive Rechte
|
|
# unverändert (read-only gegenüber Code/Konfiguration, keine neue
|
|
# Schreiberweiterung) -- Durchsetzung jetzt über `--disallowed-tools
|
|
# Edit,Write,MultiEdit,NotebookEdit` plus reviewer.json-Deny-Liste statt
|
|
# Codex' `--sandbox read-only`, daher Minor statt Major, analog zum
|
|
# Builder-Präzedenzfall.
|
|
# 1.6.0: Board-Jobs können Codex oder Claude als strikt validierten,
|
|
# über Build und Review unveränderten Runtime-Snapshot tragen. Keine neue
|
|
# Aufgabenart oder Schreiberweiterung, daher Minor.
|
|
# 1.7.0: Prüftiefe und Berichtslänge skalieren am Diff-Risiko (riskant vs.
|
|
# nicht-riskant) statt immer der vollen Struktur - der verpflichtende
|
|
# git-grep-Mindestcheck bleibt in beiden Fällen bestehen, keine neue
|
|
# Schreiberweiterung, daher Minor.
|
|
# 1.8.0 (docs/intent/board-automatic-review-correction-loop.md): bei
|
|
# "zurück zur Planung" liefert der Reviewer zusätzlich zum Freitext einen
|
|
# maschinenlesbaren CORRECTION-ITEMS-Block, damit eine Board-Automatisierung
|
|
# außerhalb dieses Rollenvertrags bis zu zwei automatische
|
|
# Builder-Korrekturläufe anstoßen kann. Eigene Prüftiefe/-methode und die
|
|
# Schreibgrenze auf docs/review/<slug>.md bleiben unverändert, daher Minor.
|
|
assert_catalog_role_version reviewer 1.8.0
|
|
# builder 1.1.0 (docs/intent/builder-iterationslimit.md): atomare Sessions
|
|
# statt Wiederaufnahme sowie ein konfigurierbares Iterationslimit (Start: 20)
|
|
# zusätzlich zum unveränderten Wall-Clock-Timeout. Reine Verhaltensänderung -
|
|
# keine neuen Schreibrechte, keine Sandbox-Änderung (workspace-write bleibt),
|
|
# daher Minor statt Major.
|
|
# 1.2.0 (2026-09-06): Backend-Wechsel Codex -> Claude Code (`claude -p`),
|
|
# ausgelöst durch erschöpftes Codex-Wochenkontingent. Die effektiven Rechte
|
|
# bleiben identisch (kein Commit/Push/Deploy/Flake-Check/Build, Schreibzugriff
|
|
# nur in den bekannten Verzeichnissen) -- nur der Durchsetzungsmechanismus
|
|
# wechselt von Codex' Kernel-Sandbox auf eine settings.json-Deny-Liste
|
|
# (agents/claude-settings/builder.json), daher Minor statt Major. Katalogfeld
|
|
# "sandbox" wechselt entsprechend von "workspace-write" auf "scoped-write".
|
|
# 2.0.0 (2026-09-06): echter Vertrauensgrenzenwechsel, kein reines
|
|
# Backend-Detail mehr -- der Builder darf jetzt nach einem
|
|
# nixos-config-Commit selbst `nixos-rebuild switch` auf blunix ausführen
|
|
# (deploy_blunix() in builder.py, wrapper-ausgeführt, nie ein
|
|
# Bash-Aufruf des Modells selbst), ohne separate menschliche Freigabe
|
|
# und ohne vorherige nix eval/build-Verifizierung. Bisher war "kein
|
|
# Deploy, unter keinen Umständen" die durchgängig verankerte Grenze
|
|
# (builder.md, Prompt, Settings-Deny-Liste) -- deren Aufhebung ist per
|
|
# Definition Major, nicht Minor, auch wenn Claude selbst weiterhin
|
|
# keinen sudo/nixos-rebuild-Zugriff bekommt. Bewusste
|
|
# Nutzerentscheidung für die aktuelle Entwicklungsphase (Geschwindigkeit
|
|
# vor Sicherheit, Rückbau über git revert/Generation-Wechsel), begrenzt
|
|
# auf blunix lokal -- kein SSH, kein tnix/pinix.
|
|
# 2.1.0: Runtime-Snapshot codex/claude für Board-Pipelines; Sicherheits-
|
|
# und Deployment-Grenzen bleiben unverändert, daher Minor.
|
|
assert_catalog_role_version builder 2.1.0
|
|
# Planner: direkter Nutzerdialog ersetzt Orchestrator-Relay und
|
|
# Orchestrator-Schließfreigabe durch direkte Nutzerfreigabe im Pane -
|
|
# geänderte Vertrauensgrenze, daher Major-Bump (3.0.0). 3.0.1 korrigiert nur
|
|
# den wirkungslosen AskUserQuestion-Grant weg (dontAsk blockt das Tool
|
|
# strukturell); reine Korrektur ohne Verhaltens- oder Grenzänderung = Patch.
|
|
# 4.0.0: Pilotwechsel von dontAsk auf auto, ausschließlich für diese Rolle,
|
|
# um AskUserQuestion nutzbar zu machen. Erneuter Major-Bump: auto ersetzt
|
|
# die feste Deny-by-default-Garantie für ungelistete Aufrufe durch einen
|
|
# Klassifikator - eine echte Vertrauensgrenzen-Verschiebung, kein reiner
|
|
# Fähigkeitszugewinn (Einschätzung des Agent-Managers, abweichend vom im
|
|
# Auftrag vorgeschlagenen Minor-Bump).
|
|
# 4.1.0: bedingte Design-Zwischenstufe (Abgrenzungs-/Schnittstellenreview)
|
|
# vor dem finalen Intent-Schreiben - rückwärtskompatibler neuer Dialog-
|
|
# Schritt ohne neue Schreib-/Freigabegrenze, daher Minor-Bump.
|
|
# 4.2.0: zusätzlicher Board-getriggerter Split-Modus (headless, read-only,
|
|
# eigener fester Auftrag statt Nutzerdialog) - rein additiv, keine neue
|
|
# Schreib-/Freigabegrenze, daher ebenfalls Minor-Bump.
|
|
assert_catalog_role_version planner 4.2.0
|
|
assert_catalog_role_version researcher 1.0.0
|
|
assert_catalog_role_version operator 1.0.0
|
|
|
|
while IFS=$'\t' read -r role_id version definition start; do
|
|
[[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \
|
|
|| fail "$role_id hat keine semantische Version"
|
|
[[ -f "$agents_dir/$definition" ]] || fail "$definition fehlt"
|
|
# Der ausschließlich Board-headless gestartete Fixer wird durch Katalog und
|
|
# expliziten Startauftrag geladen; agents/AGENTS.md liegt nicht im
|
|
# exklusiven Dateibesitz dieses Auftrags.
|
|
if [[ $role_id != fixer && $role_id != inspector ]]; then
|
|
assert_contains "$loader" "\`$definition\`"
|
|
fi
|
|
done < <(jq -r '.roles[] | [.id, .version, .definition, .start] | @tsv' "$catalog")
|
|
|
|
assert_contains "$loader" "nur ein neutraler Loader"
|
|
assert_contains "$loader" "explizite Auftrag"
|
|
|
|
# Fixer 2.0.0: brechend neuer FIX_RESULT-Vertrag und auf echte Defekte oder
|
|
# negative Reviews verengte fachliche Startgrenze. Commit/Push gelten nur im
|
|
# isolierten Worktree; Deployment bleibt ein Wrapper-/Runner-Schritt nach
|
|
# positivem Review desselben Commits und ausschließlich auf blunix.
|
|
assert_catalog_role_version fixer 2.0.0
|
|
fixer_catalog_start=$(jq -r '.roles[] | select(.id == "fixer") | .start' "$catalog")
|
|
[[ $fixer_catalog_start == board-headless ]] \
|
|
|| fail "Fixer ist nicht als board-headless katalogisiert"
|
|
fixer_catalog_sandbox=$(jq -r '.roles[] | select(.id == "fixer") | .sandbox' "$catalog")
|
|
[[ $fixer_catalog_sandbox == isolated-worktree-write ]] \
|
|
|| fail "Fixer ist nicht auf einen isolierten Worktree begrenzt"
|
|
assert_contains "$fixer" "echten Defekt oder einen negativen Review"
|
|
assert_contains "$fixer" "isolierten Git-Worktree"
|
|
assert_contains "$fixer" "committen"
|
|
assert_contains "$fixer" "pushen"
|
|
assert_contains "$fixer" "erfolgreichem Review"
|
|
assert_contains "$fixer" "kontrollierte"
|
|
assert_contains "$fixer" "blunix-Deploy-Runner"
|
|
assert_contains "$fixer" 'niemals selbst `sudo`, `nixos-rebuild`'
|
|
assert_contains "$fixer" "tnix und pinix sind"
|
|
assert_contains "$fixer" "unvertrauenswürdige Daten"
|
|
assert_contains "$fixer" "persistiert diese Angaben an der Karte"
|
|
assert_contains "$fixer" "keine dauerhafte"
|
|
assert_contains "$fixer" '===FIX_RESULT==='
|
|
assert_contains "$fixer" '===END_FIX_RESULT==='
|
|
for outcome in repaired needs_input not_applicable failed; do
|
|
assert_contains "$fixer" "$outcome"
|
|
done
|
|
for field in schema_version outcome summary commit_hash pushed_branch tests \
|
|
question required_action review_required failure; do
|
|
assert_contains "$fixer" "\"$field\""
|
|
done
|
|
assert_not_contains "$fixer" 'Session-Abschluss (Self-Close)'
|
|
assert_not_contains "$fixer" '/etc/scripts/close-my-pane.sh'
|
|
|
|
# Inspector: zwei strikt getrennte, read-only Board-Modi. Der Wrapper liefert
|
|
# und validiert allowed_actions und bewegt allein den Board-State.
|
|
assert_catalog_role_version inspector 1.0.0
|
|
inspector_catalog_start=$(jq -r '.roles[] | select(.id == "inspector") | .start' "$catalog")
|
|
[[ $inspector_catalog_start == board-headless ]] \
|
|
|| fail "Inspector ist nicht als board-headless katalogisiert"
|
|
inspector_catalog_sandbox=$(jq -r '.roles[] | select(.id == "inspector") | .sandbox' "$catalog")
|
|
[[ $inspector_catalog_sandbox == read-only ]] \
|
|
|| fail "Inspector ist nicht read-only"
|
|
for text in implementation_status route_next present partial missing \
|
|
unverifiable deployed not_deployed unknown allowed_actions \
|
|
recommended_action '===INSPECTION_RESULT===' '===ROUTING_RESULT===' \
|
|
"persistiert" "neue atomare Inspector-Session" "unvertrauenswürdige Daten"; do
|
|
assert_contains "$inspector" "$text"
|
|
done
|
|
assert_not_contains "$inspector" '"TARGET_PHASE"'
|
|
assert_not_contains "$inspector" 'Session-Abschluss (Self-Close)'
|
|
assert_not_contains "$inspector" '/etc/scripts/close-my-pane.sh'
|
|
|
|
[[ -f "$graphify_skill" ]] || fail "projektlokaler Graphify-Skill fehlt"
|
|
[[ -f "$graphify_version" ]] || fail "Graphify-Versionsstempel fehlt"
|
|
[[ $(<"$graphify_version") =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \
|
|
|| fail "ungültiger Graphify-Versionsstempel"
|
|
for reference in \
|
|
add-watch.md exports.md extraction-spec.md github-and-merge.md hooks.md \
|
|
query.md transcribe.md update.md; do
|
|
[[ -f "$repo_root/.agents/skills/graphify/references/$reference" ]] \
|
|
|| fail "Graphify-Referenz fehlt: $reference"
|
|
done
|
|
assert_contains "$graphify_skill" "name: graphify"
|
|
assert_contains "$graphify_skill" 'spawn_agent'
|
|
assert_contains "$graphify_skill" 'wait_agent'
|
|
assert_contains "$graphify_skill" 'generic `Task` tool'
|
|
assert_contains "$graphify_skill" "Do not call both interfaces for the same chunk"
|
|
assert_contains "$graphify_skill" "This skill never expands the active role"
|
|
assert_contains "$graphify_skill" "Never use Graphify-generated"
|
|
assert_contains "$graphify_skill" "Role-aware query feedback"
|
|
assert_contains "$graphify_skill" 'must not run `graphify save-result` or `graphify reflect`'
|
|
assert_contains "$graphify_skill" "explicitly assigned write path for derived Graphify"
|
|
assert_contains "$conventions" 'graphify install --project --platform agents'
|
|
assert_contains "$conventions" 'graphify install --project --platform codex'
|
|
assert_contains "$conventions" '`.agents/skills/graphify/`'
|
|
assert_contains "$conventions" 'eigenen, exklusiven Dateibesitz'
|
|
assert_contains "$conventions" 'Ein Skill-Aufruf erweitert niemals Netzwerk-, Schreib-, Deployment- oder'
|
|
assert_contains "$conventions" 'Read-only Rollen dürfen dort erlaubte Query-/Path-/Explain-Aufrufe'
|
|
assert_contains "$conventions" 'Dieses Feedback bleibt nichtpersistierend'
|
|
assert_contains "$conventions" 'explizit zugewiesenen Schreibpfad'
|
|
assert_contains "$conventions" '`graphify reflect` nach dem bestehenden Ablauf'
|
|
|
|
if [[ ${1:-} == --graphify-only ]]; then
|
|
printf 'OK: projektlokaler Graphify-Skill und Laufzeitgrenzen\n'
|
|
exit 0
|
|
fi
|
|
|
|
reviewer_catalog_status=$(jq -r '.roles[] | select(.id == "reviewer") | .status' "$catalog")
|
|
[[ $reviewer_catalog_status == active ]] \
|
|
|| fail "Reviewer ist nicht als aktiv katalogisiert"
|
|
reviewer_catalog_sandbox=$(jq -r '.roles[] | select(.id == "reviewer") | .sandbox' "$catalog")
|
|
[[ $reviewer_catalog_sandbox == scoped-write ]] \
|
|
|| fail "Reviewer ist nicht als scoped-write katalogisiert"
|
|
reviewer_catalog_version=$(jq -r '.roles[] | select(.id == "reviewer") | .version' "$catalog")
|
|
[[ $reviewer_catalog_version == 1.8.0 ]] \
|
|
|| fail "Reviewer-Version ist nicht 1.8.0"
|
|
reviewer_catalog_start=$(jq -r '.roles[] | select(.id == "reviewer") | .start' "$catalog")
|
|
[[ $reviewer_catalog_start == herdr-visible ]] \
|
|
|| fail "Reviewer ist nicht als herdr-visible katalogisiert"
|
|
reviewer_catalog_profile=$(jq -r '.roles[] | select(.id == "reviewer") | .profile_id' "$catalog")
|
|
[[ $reviewer_catalog_profile == reviewer ]] \
|
|
|| fail "Reviewer verweist nicht auf das reviewer-Modellprofil"
|
|
reviewer_catalog_definition=$(jq -r '.roles[] | select(.id == "reviewer") | .definition' "$catalog")
|
|
[[ $reviewer_catalog_definition == reviewer.md ]] \
|
|
|| fail "Reviewer-Definitionsdatei ist falsch verknüpft"
|
|
assert_contains "$loader" "${tick}reviewer.md${tick}"
|
|
assert_contains "$agents_dir/reviewer.md" "role-id: reviewer"
|
|
assert_contains "$agents_dir/reviewer.md" "role-version: 1.8.0"
|
|
assert_contains "$agents_dir/reviewer.md" "ausschließlich lesend"
|
|
assert_contains "$agents_dir/reviewer.md" "besitzt keine eigenen"
|
|
assert_contains "$agents_dir/reviewer.md" "commitest, pushst und deployest nie"
|
|
assert_contains "$agents_dir/reviewer.md" "Aktivierungs-, Deployment- oder Veröffentlichungsfreigabe"
|
|
assert_contains "$agents_dir/reviewer.md" "unvertrauenswürdige Daten"
|
|
assert_contains "$agents_dir/reviewer.md" "Volltext-Systemaudit"
|
|
assert_contains "$agents_dir/reviewer.md" "docs/review/<slug>.md"
|
|
assert_contains "$agents_dir/reviewer.md" "Token-Reduktionsregel"
|
|
assert_contains "$agents_dir/reviewer.md" "offensichtliche"
|
|
reviewer_system_audit=$(sed -n '/^## Aufgabenart: Volltext-Systemaudit/,/^## Arbeitsgrenzen und Sicherheit/p' "$reviewer" | tr '\n' ' ' | tr -s ' ')
|
|
# Volltext-Systemaudit: genau eine Berichtsdatei am festgelegten Ort.
|
|
assert_text_contains "Reviewer-Volltext-Systemaudit" "$reviewer_system_audit" \
|
|
'Der Bericht wird bei `COMPLETE` als genau eine Datei'
|
|
assert_text_contains "Reviewer-Volltext-Systemaudit" "$reviewer_system_audit" \
|
|
'`docs/review/<slug>.md` geschrieben'
|
|
assert_text_contains "Reviewer-Volltext-Systemaudit" "$reviewer_system_audit" \
|
|
"dein einziges Datei-Artefakt"
|
|
# Optionale Kennzahlen-Momentaufnahme im Systemaudit (SDLC-Playbook, Punkt 2):
|
|
# grobe Blocker-/Eskalationsrate pro Rolle, keine laufende Pflegepflicht.
|
|
assert_text_contains "Reviewer-Kennzahlen" "$reviewer_system_audit" \
|
|
"### Optionale Kennzahlen-Momentaufnahme (nur Volltext-Systemaudit)"
|
|
assert_text_contains "Reviewer-Kennzahlen" "$reviewer_system_audit" \
|
|
"**darf** zusätzlich grob erfassen"
|
|
assert_text_contains "Reviewer-Kennzahlen" "$reviewer_system_audit" \
|
|
"Blocker oder Eskalationen pro Rolle"
|
|
assert_text_contains "Reviewer-Kennzahlen" "$reviewer_system_audit" \
|
|
"keine** laufende Pflegepflicht zwischen Audits"
|
|
assert_text_contains "Reviewer-Kennzahlen" "$reviewer_system_audit" \
|
|
"statt Zahlen zu erfinden"
|
|
# Eskalationsanlass dokumentieren (SDLC-Playbook, Punkt 3).
|
|
assert_contains "$agents_dir/reviewer.md" "Eskalationsanlass"
|
|
assert_contains "$agents_dir/reviewer.md" "nur bei"
|
|
[[ -f "$repo_root/docs/review/.gitkeep" ]] \
|
|
|| fail "Review-Berichtsverzeichnis fehlt"
|
|
jq -e '[.permissions.allow[] | select(startswith("Edit("))] == ["Edit(docs/review/*.md)"]' \
|
|
"$settings_dir/reviewer.json" >/dev/null \
|
|
|| fail "Reviewer darf nur Systemaudit-Berichte editieren"
|
|
jq -e '[.permissions.deny[] | select(. == "Edit(**)" or . == "Edit(*)")] | length == 0' \
|
|
"$settings_dir/reviewer.json" >/dev/null \
|
|
|| fail "Reviewer-Settings dürfen keinen pauschalen Edit-Deny enthalten"
|
|
jq -e '[.permissions.allow[] + .permissions.deny[] | select(startswith("Write("))] | length == 0' \
|
|
"$settings_dir/reviewer.json" >/dev/null \
|
|
|| fail "Reviewer-Settings dürfen keine Write-Regel enthalten"
|
|
# Read-only Reviewer: Graphify-Abfragen bleiben erlaubt, persistierende
|
|
# Feedback-Befehle bleiben technisch gesperrt und die Übergabe bleibt im Pane.
|
|
jq -e '[.permissions.allow[] | select(. == "Bash(graphify query *)" or . == "Bash(graphify path *)" or . == "Bash(graphify explain *)")] | length == 3' \
|
|
"$settings_dir/reviewer.json" >/dev/null \
|
|
|| fail "Reviewer braucht genau die drei read-only Graphify-Abfragen"
|
|
jq -e '[.permissions.deny[] | select(. == "Bash(graphify save-result *)" or . == "Bash(graphify reflect *)")] | length == 2' \
|
|
"$settings_dir/reviewer.json" >/dev/null \
|
|
|| fail "Reviewer muss save-result und reflect weiterhin sperren"
|
|
reviewer_graphify_handoff=$(sed -n '/Graphify-Recherche/,/^## Review-Methode und Ergebnisformat/p' "$reviewer" | tr '\n' ' ' | tr -s ' ')
|
|
for field in 'Query-Typ' 'useful' 'dead_end' 'corrected' 'zitierte Knoten/Quellen' 'die Korrektur' 'nichtpersistierend'; do
|
|
assert_text_contains "Reviewer-Graphify-Übergabe" "$reviewer_graphify_handoff" "$field"
|
|
done
|
|
|
|
# Board-getriggerter Review (docs/intent/git-intent-board-feedback.md,
|
|
# Abschnitt "Reviewer: keine neue Aufgabenart") - Zusatzabsatz zur bestehenden
|
|
# freien Diff-Review, keine dritte Aufgabenart, headless-Ausnahme gilt nur
|
|
# für diesen Trigger.
|
|
reviewer_board_trigger=$(sed -n '/^## Board-getriggerter Review/,/^## Arbeitsgrenzen und Sicherheit/p' "$reviewer" | tr '\n' ' ' | tr -s ' ')
|
|
for field in \
|
|
'keine neue Aufgabenart' 'festen Commit-Scope' 'docs/review/<slug>.md' \
|
|
'`bereit für Deployment`' '`zurück zur Planung`' \
|
|
'**keine** automatische neue Planner-Session' \
|
|
'headless statt herdr-visible' 'auf diesen Board-Trigger begrenzte Ausnahme'; do
|
|
assert_text_contains "Reviewer-Board-Trigger" "$reviewer_board_trigger" "$field"
|
|
done
|
|
jq -e '[.permissions.allow[] | select(startswith("Edit("))] == ["Edit(docs/review/*.md)"]' \
|
|
"$settings_dir/reviewer.json" >/dev/null \
|
|
|| fail "Board-getriggerter Review darf keine zusätzlichen Schreibrechte in reviewer.json bekommen"
|
|
|
|
# Iterationslimit für atomare Builder-Aufträge
|
|
# (docs/intent/builder-iterationslimit.md).
|
|
builder_catalog_status=$(jq -r '.roles[] | select(.id == "builder") | .status' "$catalog")
|
|
[[ $builder_catalog_status == active ]] || fail "Builder ist nicht als aktiv katalogisiert"
|
|
# "scoped-write" seit 1.2.0 (Backend-Wechsel Codex -> Claude Code,
|
|
# 2026-09-06, siehe assert_catalog_role_version builder oben) --
|
|
# vorher "workspace-write" (Codex-Sandbox-Begriff). Die effektiven Rechte
|
|
# sind unverändert, nur der Durchsetzungsmechanismus (settings.json statt
|
|
# Kernel-Sandbox) hat einen anderen Namen.
|
|
builder_catalog_sandbox=$(jq -r '.roles[] | select(.id == "builder") | .sandbox' "$catalog")
|
|
[[ $builder_catalog_sandbox == scoped-write ]] \
|
|
|| fail "Builder-Sandbox-Feld stimmt nicht mit dem Claude-Backend-Wechsel überein"
|
|
builder_catalog_start=$(jq -r '.roles[] | select(.id == "builder") | .start' "$catalog")
|
|
[[ $builder_catalog_start == xmpp-worker ]] \
|
|
|| fail "Builder-Startweise darf sich durch das Iterationslimit nicht ändern"
|
|
builder_catalog_version=$(jq -r '.roles[] | select(.id == "builder") | .version' "$catalog")
|
|
[[ $builder_catalog_version == 2.1.0 ]] || fail "Builder-Version ist nicht 2.1.0"
|
|
builder_session_limit=$(sed -n '/^## Atomare Sessions und Iterationslimit/,/^## Direkter Dialog und Freigabe/p' "$builder" | tr '\n' ' ' | tr -s ' ')
|
|
for field in \
|
|
'neue, eigenständige Session' 'keine automatische Fortsetzung' \
|
|
'**20**' 'konfigurierbar' 'ohne neue Intent-Klärung' \
|
|
'günstiges Profil' 'bekannten, konfigurierten Profilen' \
|
|
'Iterationslimit gilt unabhängig' 'globale Obergrenze nie überschreiten' \
|
|
'**Kein** Prüf-Commit' '**kein** Reviewer-Lauf' '**kein** Push' '**kein** Deployment' \
|
|
'Zwischenstandsbericht' 'als Kontext für die neue,'; do
|
|
assert_text_contains "Builder-Iterationslimit" "$builder_session_limit" "$field"
|
|
done
|
|
assert_contains "$orga" "role-id: orga"
|
|
orga_catalog_version=$(
|
|
jq -r '.roles[] | select(.id == "orga") | .version' "$catalog"
|
|
)
|
|
[[ $orga_catalog_version == 2.1.1 ]] \
|
|
|| fail "unerwartete Orga-Version im Katalog"
|
|
|
|
# Eskalationsannahme (Agent-Manager als Fixer für Rollenvertrags-Blocker)
|
|
assert_contains "$agent_manager" "Eskalationsannahme (Fixer für Rollenvertrags-Blocker)"
|
|
assert_contains "$agent_manager" "wörtlich, nicht paraphrasiert"
|
|
assert_contains "$agent_manager" "bleiben Sache des"
|
|
assert_contains "$agent_manager" "Administrators, nicht deine"
|
|
# Eskalationsanlass im Ergebnisformat (SDLC-Playbook, Punkt 3): Anforderung
|
|
# und Rollenvertrag sind wortsinngleich auf die Chat-Übergabe festgelegt.
|
|
agent_manager_escalation=$(sed -n '/^## Ergebnisformat/,$p' "$agent_manager" | tr '\n' ' ' | tr -s ' ')
|
|
sdlc_intent_escalation=$(sed -n '/^3\. \*\*Eskalationsanlass dokumentieren\*\*/,/^## Explizit ausgeschlossen/p' \
|
|
"$repo_root/docs/intent/sdlc-playbook-prozessergaenzungen.md" | tr '\n' ' ' | tr -s ' ')
|
|
agent_manager_escalation_contract='Der Agent-Manager führt kein Datei-Artefakt; der Eskalationsanlass steht ausschließlich als zusätzlicher Punkt in der Chat-Übergabe an den Orchestrator.'
|
|
assert_text_contains "Agent-Manager-Ergebnisformat" "$agent_manager_escalation" \
|
|
"$agent_manager_escalation_contract"
|
|
assert_text_contains "SDLC-Intent" "$sdlc_intent_escalation" \
|
|
"$agent_manager_escalation_contract"
|
|
assert_text_contains "Agent-Manager-Ergebnisformat" "$agent_manager_escalation" \
|
|
"gemessenem Qualitätsbedarf"
|
|
agent_manager_catalog_version=$(
|
|
jq -r '.roles[] | select(.id == "agent-manager") | .version' "$catalog"
|
|
)
|
|
[[ $agent_manager_catalog_version == 2.3.0 ]] \
|
|
|| fail "unerwartete Agent-Manager-Version im Katalog"
|
|
|
|
# Rückfrage-Kanal-Pilot (ausschließlich Orga, promptbasierte Grenze)
|
|
assert_contains "$orga" "Rückfrage-Kanal (Pilot)"
|
|
assert_contains "$orga" "blockiert mit Rückfrage, Turn bleibt offen"
|
|
assert_contains "$orga" "herdr pane report-agent"
|
|
assert_contains "$orga" "state blocked"
|
|
assert_contains "$orga" "keine technische"
|
|
assert_contains "$orga" "als Capability-Handoff behandeln"
|
|
assert_not_contains "$admin" "Rückfrage-Kanal (Pilot)"
|
|
assert_not_contains "$reviewer" "Rückfrage-Kanal (Pilot)"
|
|
jq -e '[.permissions.allow[] | select(. == "AskUserQuestion")] | length == 0' \
|
|
"$settings_dir/orga.json" >/dev/null \
|
|
|| fail "Orga darf den Nutzer nicht direkt per AskUserQuestion befragen"
|
|
|
|
assert_contains "$orchestration" "Reviewer-Einbindung, Eskalationskette und Rückfrage-Kanal"
|
|
assert_contains "$orchestration" "Eigenständiger Volltext-Systemaudit"
|
|
assert_contains "$orchestration" "docs/review/<slug>.md"
|
|
assert_contains "$orchestration" "ausdrücklich ausgenommen"
|
|
assert_contains "$orchestration" "Reviewer als Standardschritt ab Risikoschwelle"
|
|
assert_contains "$orchestration" "ausschließlich für die Rolle \`orga\` freigegeben"
|
|
assert_contains "$orchestration" "bewusst noch nicht gebaut"
|
|
|
|
# Planner: eng pfadbeschränkte Discovery-Rolle, direkter Nutzerdialog im
|
|
# eigenen Pane statt Orchestrator-Relay - seit 4.0.0 echter interaktiver
|
|
# Dialog inkl. AskUserQuestion (Modus auto statt dontAsk, siehe unten).
|
|
assert_contains "$planner" "role-id: planner"
|
|
assert_contains "$planner" "role-version: 4.2.0"
|
|
planner_catalog_version=$(
|
|
jq -r '.roles[] | select(.id == "planner") | .version' "$catalog"
|
|
)
|
|
[[ $planner_catalog_version == 4.2.0 ]] || fail "unerwartete Planner-Version im Katalog"
|
|
planner_catalog_sandbox=$(jq -r '.roles[] | select(.id == "planner") | .sandbox' "$catalog")
|
|
[[ $planner_catalog_sandbox == scoped-write ]] || fail "Planner ist nicht als scoped-write katalogisiert"
|
|
assert_contains "$planner" "implementierst nichts selbst"
|
|
assert_contains "$planner" "Intent-Dokument"
|
|
assert_contains "$planner" "docs/intent/"
|
|
assert_contains "$planner" "direkt und persönlich mit dem Nutzer"
|
|
assert_contains "$planner" "Rohfassung"
|
|
assert_contains "$planner" "direkt im Pane ausdrücklich bestätigt"
|
|
# Bedingte Design-Zwischenstufe (SDLC-Playbook, Punkt 1): beide positiven
|
|
# Trigger, der negative Erwähnungsfall sowie Nutzerbestätigung vor dem finalen
|
|
# Intent-Schreiben sind ein zusammenhängender Textvertrag.
|
|
planner_design_stage=$(sed -n '/^### Bedingte Design-Zwischenstufe/,/^## Session-Abschluss/p' "$planner" | tr '\n' ' ' | tr -s ' ')
|
|
assert_text_contains "Planner-Design-Zwischenstufe" "$planner_design_stage" \
|
|
"Das Vorhaben berührt **mehrere Hosts**"
|
|
assert_text_contains "Planner-Design-Zwischenstufe" "$planner_design_stage" \
|
|
"Das Vorhaben berührt die **Rollen-/Katalog-Grenze**"
|
|
assert_text_contains "Planner-Design-Zwischenstufe" "$planner_design_stage" \
|
|
'`agents/catalog.json`, an einer `agents/*.md`-Rollendatei oder an `agents/claude-settings/*.json`'
|
|
assert_text_order "Planner-Design-Zwischenstufe" "$planner_design_stage" \
|
|
"Die bloße Erwähnung einer Rolle im Fließtext" "löst den Schritt **nicht** aus"
|
|
assert_text_order "Planner-Design-Zwischenstufe" "$planner_design_stage" \
|
|
"Trifft ein Auslöser zu, legst du dem Nutzer im Chat" \
|
|
"Diese Zusammenfassung lässt du ausdrücklich vom Nutzer bestätigen"
|
|
assert_text_order "Planner-Design-Zwischenstufe" "$planner_design_stage" \
|
|
"Diese Zusammenfassung lässt du ausdrücklich vom Nutzer bestätigen" \
|
|
"bevor du zum finalen Intent-Dokument übergehst"
|
|
assert_text_contains "Planner-Design-Zwischenstufe" "$planner_design_stage" \
|
|
"Es entsteht dabei **kein neues Artefakt**"
|
|
assert_not_contains "$planner" "nachweislich gelesen"
|
|
assert_not_contains "$planner" "BLOCKED: Rückfragen"
|
|
|
|
# Planner ist als einzige Rolle im direkten Dialog: gleiche Self-Close-
|
|
# Mechanik wie die übrigen Rollen, aber Freigabe kommt vom Nutzer im Pane
|
|
# statt vom Orchestrator.
|
|
assert_contains "$planner" 'Session-Abschluss (Self-Close)'
|
|
assert_contains "$planner" '/etc/scripts/close-my-pane.sh'
|
|
assert_contains "$planner" '$HERDR_PANE_ID'
|
|
assert_contains "$planner" 'ignoriert jedes Argument'
|
|
assert_contains "$planner" '**eigene**'
|
|
assert_contains "$planner" 'niemals eine fremde'
|
|
assert_contains "$planner" 'bleibst idle'
|
|
assert_contains "$planner" 'Fehlt eine Bedingung'
|
|
assert_contains "$planner" '**Codex 0.145:**'
|
|
assert_contains "$planner" '`sandbox_permissions=require_escalated`'
|
|
assert_contains "$planner" '`workspace-write` wird blockiert'
|
|
assert_contains "$planner" 'Keine Argumente, kein'
|
|
assert_contains "$planner" '**Claude:**'
|
|
assert_contains "$planner" 'wegen des Sessionlimits noch aus'
|
|
|
|
wrapper_grants_planner=$(jq --arg grant "$self_close_grant" \
|
|
'[.permissions.allow[] | select(. == $grant)] | length' \
|
|
"$settings_dir/planner.json")
|
|
[[ $wrapper_grants_planner -eq 1 ]] \
|
|
|| fail "planner.json: erwarteter Self-Close-Wrapper fehlt"
|
|
# Seit 4.0.0 läuft der Planner im Modus auto statt dontAsk (dontAsk blockt
|
|
# AskUserQuestion strukturell, unabhängig von der Allow-Liste; live
|
|
# beobachtet, siehe Version 3.0.1). auto erlaubt das Tool, verlässt sich für
|
|
# ungelistete Aufrufe aber auf einen Klassifikator statt auf eine feste
|
|
# Liste - ein bewusst auf diese Rolle begrenzter Pilotwechsel, kein Rollout.
|
|
# Der Grant muss deshalb jetzt genau einmal vorhanden sein.
|
|
jq -e '[.permissions.allow[] | select(. == "AskUserQuestion")] | length == 1' \
|
|
"$settings_dir/planner.json" >/dev/null \
|
|
|| fail "Planner braucht den AskUserQuestion-Grant (Modus auto seit 4.0.0)"
|
|
assert_contains "$planner" 'echter interaktiver'
|
|
assert_contains "$planner" 'Klassifikator'
|
|
assert_contains "$planner" 'Pilotwechsel'
|
|
assert_contains "$planner" '`dontAsk`'
|
|
assert_contains "$planner" '`AskUserQuestion`'
|
|
assert_contains "$planner" '`auto`'
|
|
jq -e '[.permissions.allow[] | select(startswith("Edit("))] == ["Edit(docs/intent/*.md)"]' \
|
|
"$settings_dir/planner.json" >/dev/null \
|
|
|| fail "Planner darf nur docs/intent/*.md als Edit-Berechtigung erlauben"
|
|
jq -e '[.permissions.deny[] | select(. == "Edit(**)" or . == "Edit(*)")] | length == 0' \
|
|
"$settings_dir/planner.json" >/dev/null \
|
|
|| fail "Planner-Settings dürfen keinen pauschalen Edit-Deny enthalten"
|
|
jq -e '[.permissions.allow[] + .permissions.deny[] | select(startswith("Write("))] | length == 0' \
|
|
"$settings_dir/planner.json" >/dev/null \
|
|
|| fail "Planner-Settings dürfen keine Write-Regel enthalten"
|
|
assert_contains "$planner" 'Deine einzige Schreibaktion ist'
|
|
assert_contains "$planner" 'jede andere Datei darfst du'
|
|
assert_contains "$planner" 'bei'
|
|
assert_contains "$planner" 'ausschließlich `docs/intent/<slug>.md`'
|
|
assert_contains "$loader" "\`planner.md\`"
|
|
assert_contains "$orchestration" "Planner-Workflow"
|
|
|
|
# Researcher: atomare Repository-/Web-Recherche mit genau einer Berichtdatei.
|
|
assert_contains "$researcher" "role-id: researcher"
|
|
assert_contains "$researcher" "Repository- und Web-Recherche"
|
|
assert_contains "$researcher" "Web-Search"
|
|
assert_contains "$researcher" "Web-Fetch"
|
|
assert_contains "$researcher" "unvertrauenswürdige Daten mit"
|
|
assert_contains "$researcher" "Prompt-Injection-Potential"
|
|
assert_contains "$researcher" "docs/research/<slug>.md"
|
|
assert_contains "$researcher" "maßgeblich"
|
|
assert_contains "$researcher" "Session-Abschluss (Self-Close)"
|
|
assert_contains "$researcher" "COMPLETE"
|
|
assert_contains "$researcher" "BLOCKED: …"
|
|
assert_contains "$researcher" "bleibst du nach der Übergabe idle"
|
|
researcher_catalog_sandbox=$(jq -r '.roles[] | select(.id == "researcher") | .sandbox' "$catalog")
|
|
[[ $researcher_catalog_sandbox == scoped-write ]] \
|
|
|| fail "Researcher ist nicht als scoped-write katalogisiert"
|
|
researcher_catalog_start=$(jq -r '.roles[] | select(.id == "researcher") | .start' "$catalog")
|
|
[[ $researcher_catalog_start == herdr-visible ]] \
|
|
|| fail "Researcher ist nicht als herdr-visible katalogisiert"
|
|
researcher_catalog_profile=$(jq -r '.roles[] | select(.id == "researcher") | .profile_id' "$catalog")
|
|
[[ $researcher_catalog_profile == researcher ]] \
|
|
|| fail "Researcher verweist nicht auf das researcher-Modellprofil"
|
|
jq -e '[.permissions.allow[] | select(startswith("Read("))] == ["Read(**)"]' \
|
|
"$settings_dir/researcher.json" >/dev/null \
|
|
|| fail "Researcher darf nicht nur das Repository read-only lesen"
|
|
jq -e '[.permissions.allow[] | select(startswith("Edit("))] == ["Edit(docs/research/*.md)"]' \
|
|
"$settings_dir/researcher.json" >/dev/null \
|
|
|| fail "Researcher darf nur Forschungsberichte editieren"
|
|
jq -e '[.permissions.allow[] | select(. == "WebSearch")] | length == 1' \
|
|
"$settings_dir/researcher.json" >/dev/null \
|
|
|| fail "Researcher WebSearch-Allowlist fehlt"
|
|
jq -e '[.permissions.allow[] | select(. == "WebFetch")] | length == 1' \
|
|
"$settings_dir/researcher.json" >/dev/null \
|
|
|| fail "Researcher WebFetch-Allowlist fehlt"
|
|
jq -e '[.permissions.allow[] + .permissions.deny[] | select(startswith("Write("))] | length == 0' \
|
|
"$settings_dir/researcher.json" >/dev/null \
|
|
|| fail "Researcher-Settings dürfen keine Write-Regel enthalten"
|
|
assert_contains "$loader" "\`researcher.md\`"
|
|
|
|
# Operator: ausschließlich headless vom Board gestartete, strikt read-only
|
|
# Entscheidungsrolle. Der Wrapper validiert und mutiert atomar; das Modell
|
|
# liefert nur den maschinenlesbaren Marker-Block.
|
|
assert_contains "$operator" "role-id: operator"
|
|
assert_contains "$operator" "role-version: 1.0.0"
|
|
operator_catalog_sandbox=$(jq -r '.roles[] | select(.id == "operator") | .sandbox' "$catalog")
|
|
[[ $operator_catalog_sandbox == read-only ]] \
|
|
|| fail "Operator ist nicht als read-only katalogisiert"
|
|
operator_catalog_start=$(jq -r '.roles[] | select(.id == "operator") | .start' "$catalog")
|
|
[[ $operator_catalog_start == board-headless ]] \
|
|
|| fail "Operator ist nicht als board-headless katalogisiert"
|
|
operator_catalog_profile=$(jq -r '.roles[] | select(.id == "operator") | .profile_id' "$catalog")
|
|
[[ $operator_catalog_profile == operator ]] \
|
|
|| fail "Operator verweist nicht auf das operator-Modellprofil"
|
|
assert_contains "$operator" "strikt read-only"
|
|
assert_contains "$operator" "Ein vertrauenswürdiger Board-Wrapper"
|
|
assert_contains "$operator" '===OPERATOR-DECISION==='
|
|
assert_contains "$operator" '===END-OPERATOR-DECISION==='
|
|
for decision in direct_builder split needs_human; do
|
|
assert_contains "$operator" "$decision"
|
|
done
|
|
for field in \
|
|
schema_version decision reason recommended_lane runtime model \
|
|
reasoning_effort human_action children slug title wave blocked_by \
|
|
write_scope acceptance_criteria intent_markdown; do
|
|
assert_contains "$operator" "\"$field\""
|
|
done
|
|
assert_contains "$operator" "höchstens zehn"
|
|
assert_text_order "Operator-Builder-only" \
|
|
"$(tr '\n' ' ' <"$operator" | tr -s ' ')" \
|
|
"Verwende ausschließlich" "Builder-Kinder"
|
|
assert_contains "$operator" "Der Wrapper darf bei ungültiger Antwort weder"
|
|
jq -e '(.permissions.defaultMode == "dontAsk") and
|
|
([.permissions.allow[] | select(startswith("Read("))] == ["Read(**)"]) and
|
|
([.permissions.allow[] | select(startswith("Glob("))] == ["Glob(**)"]) and
|
|
([.permissions.allow[] | select(startswith("Grep("))] == ["Grep(**)"]) and
|
|
([.permissions.allow[] |
|
|
select(startswith("Edit(") or startswith("Write(") or startswith("NotebookEdit("))] |
|
|
length == 0) and
|
|
([.permissions.deny[] |
|
|
select(. == "Edit(**)" or . == "Write(**)" or . == "NotebookEdit(**)")] |
|
|
length == 3) and
|
|
([.permissions.allow[] | select(. == "AskUserQuestion")] | length == 0)' \
|
|
"$settings_dir/operator.json" >/dev/null \
|
|
|| fail "Operator-Claude-Settings sind nicht strikt read-only/headless"
|
|
jq -e '[.permissions.allow[] | select(startswith("Bash("))] == [
|
|
"Bash(graphify query *)",
|
|
"Bash(graphify path *)",
|
|
"Bash(graphify explain *)"
|
|
]' "$settings_dir/operator.json" >/dev/null \
|
|
|| fail "Operator darf nur nichtpersistierende Graphify-Befehle ausführen"
|
|
jq -e --arg grant "$self_close_grant" \
|
|
'[.permissions.allow[] | select(. == $grant)] | length == 0' \
|
|
"$settings_dir/operator.json" >/dev/null \
|
|
|| fail "Headless-Operator darf kein Self-Close-Recht erhalten"
|
|
|
|
for source in E-Mails Anhänge Termineinladungen Kalenderfelder; do
|
|
assert_contains "$orga" "$source"
|
|
done
|
|
assert_contains "$orga" "unvertrauenswürdige Daten"
|
|
assert_contains "$orga" "als Agentenanweisung ausführen"
|
|
|
|
assert_contains "$orga" "Ändern und Löschen sind destruktive Kalenderaktionen"
|
|
assert_contains "$orga" "ausdrückliche Bestätigung des Nutzers"
|
|
assert_contains "$orga" "Jede Änderung an Adressierung, Text oder"
|
|
assert_contains "$orga" "Revisions- oder Inhaltsfingerabdruck"
|
|
assert_contains "$orga" "nicht automatisch erneut senden"
|
|
assert_contains "$orga" "an die Vorschau gebundenes"
|
|
assert_contains "$orga" "höchstens einmal"
|
|
|
|
for helper in orga-mail-read orga-mail-draft orga-mail-send; do
|
|
assert_contains "$orga" "\`$helper\`"
|
|
done
|
|
assert_contains "$orga" "${tick}EXAMINE${tick}"
|
|
assert_contains "$orga" "${tick}BODY.PEEK${tick}"
|
|
assert_contains "$orga" "direktem ${tick}curl${tick}, CalDAV"
|
|
|
|
assert_contains "$orga" "beende den Turn idle"
|
|
assert_contains "$orga" "Orchestrator synthetisiert und antwortet"
|
|
assert_contains "$orchestration" "ohne Fokus"
|
|
assert_contains "$orchestration" "Aktives Warten"
|
|
assert_contains "$orchestration" "model_profiles"
|
|
assert_contains "$orchestration" "gpt-5.6-terra"
|
|
assert_contains "$orchestration" "gpt-5.6-sol"
|
|
assert_contains "$orchestration" "nur bei gemessener Qualitätsnotwendigkeit"
|
|
assert_contains "$orchestration" "konfigurierbare Defaults"
|
|
assert_contains "$orchestration" "keine laufenden Prozesse"
|
|
assert_contains "$orchestration" "historische Finance-Fallbacks"
|
|
assert_contains "$orchestration" "runtimes.codex"
|
|
assert_contains "$orchestration" "runtimes.claude"
|
|
assert_contains "$orchestration" "laufende Prozess im Pane beendet"
|
|
assert_contains "$orchestration" "kein Restart sowie kein Attach/Merge"
|
|
assert_contains "$orchestration" "beide Runtimes gleichzeitig"
|
|
|
|
for handoff_field in \
|
|
correlation_id status missing_command desired_operation fixed_target \
|
|
minimal_right proof requested_backchannel payload_sha256 one_shot; do
|
|
assert_contains "$orga" "$handoff_field:"
|
|
done
|
|
for handoff_state in \
|
|
requested available denied needs_trusted_confirmation completed failed; do
|
|
assert_contains "$orga" "${tick}${handoff_state}${tick}"
|
|
done
|
|
assert_contains "$orga" "technische Anforderung und niemals"
|
|
assert_contains "$orga" "Capability-Handoff"
|
|
assert_contains "$orga" "orga-cal-20260901-hdm-online-plenum-v1"
|
|
assert_contains "$orga" "capabilities|events"
|
|
assert_contains "$orga" "dieselbe ${tick}correlation_id${tick}"
|
|
assert_contains "$orga" "öffentliche Ziel ${tick}calendar/public${tick}"
|
|
assert_contains "$orga" "direktem ${tick}curl${tick}, CalDAV, IMAP, SMTP"
|
|
|
|
handoff_field() {
|
|
local payload=$1
|
|
local field=$2
|
|
sed -n "s/^${field}: //p" <<<"$payload"
|
|
}
|
|
|
|
validate_capability_handoff() {
|
|
local payload=$1
|
|
local field value status fixed_target minimal_right backchannel
|
|
local required_fields=(
|
|
correlation_id status missing_command desired_operation fixed_target
|
|
minimal_right proof requested_backchannel payload_sha256 one_shot
|
|
)
|
|
|
|
for field in "${required_fields[@]}"; do
|
|
[[ $(grep -Ec "^${field}: .+" <<<"$payload") -eq 1 ]] || return 1
|
|
done
|
|
|
|
status=$(handoff_field "$payload" status)
|
|
case "$status" in
|
|
requested|available|denied|needs_trusted_confirmation|completed|failed) ;;
|
|
*) return 1 ;;
|
|
esac
|
|
|
|
fixed_target=$(handoff_field "$payload" fixed_target)
|
|
[[ $fixed_target != *calendar/public* ]] || return 1
|
|
|
|
minimal_right=$(handoff_field "$payload" minimal_right)
|
|
[[ $minimal_right =~ ^[a-z0-9._-]+:one-shot$ ]] || return 1
|
|
case "$minimal_right" in
|
|
all:one-shot|any:one-shot|rw:one-shot|write:one-shot) return 1 ;;
|
|
esac
|
|
|
|
backchannel=$(handoff_field "$payload" requested_backchannel)
|
|
[[ $backchannel =~ ^herdr:[a-zA-Z0-9._-]+$ ]] || return 1
|
|
|
|
value=$(handoff_field "$payload" payload_sha256)
|
|
[[ $value =~ ^[a-f0-9]{64}$ ]] || return 1
|
|
[[ $(handoff_field "$payload" one_shot) == true ]] || return 1
|
|
}
|
|
|
|
capability_allows_execution() {
|
|
local payload=$1
|
|
local expected_correlation=$2
|
|
local expected_hash=$3
|
|
local expected_target=$4
|
|
local expected_backchannel=$5
|
|
validate_capability_handoff "$payload" || return 1
|
|
[[ $(handoff_field "$payload" status) == available ]] || return 1
|
|
[[ $(handoff_field "$payload" correlation_id) == "$expected_correlation" ]] \
|
|
|| return 1
|
|
[[ $(handoff_field "$payload" payload_sha256) == "$expected_hash" ]] || return 1
|
|
[[ $(handoff_field "$payload" fixed_target) == "$expected_target" ]] || return 1
|
|
[[ $(handoff_field "$payload" requested_backchannel) == "$expected_backchannel" ]] \
|
|
|| return 1
|
|
[[ $(handoff_field "$payload" proof) == *capability_check=unambiguous* ]] \
|
|
|| return 1
|
|
[[ $(handoff_field "$payload" proof) =~ resolved_target_id=calendar-id:private:[a-zA-Z0-9._-]+ ]] \
|
|
|| return 1
|
|
}
|
|
|
|
valid_handoff='correlation_id: orga-cal-test-v1
|
|
status: available
|
|
missing_command: orga-calendar-create
|
|
desired_operation: calendar.event.create
|
|
fixed_target: private-calendar-ref:orga-owner
|
|
minimal_right: calendar.event.create:one-shot
|
|
proof: capability_check=unambiguous; resolved_target_id=calendar-id:private:abc123; command=create
|
|
requested_backchannel: herdr:orga
|
|
payload_sha256: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
|
one_shot: true'
|
|
|
|
expected_correlation='orga-cal-test-v1'
|
|
expected_hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
|
expected_target=private-calendar-ref:orga-owner
|
|
expected_backchannel=herdr:orga
|
|
|
|
validate_capability_handoff "$valid_handoff" \
|
|
|| fail "gültiger Capability-Handoff wurde abgelehnt"
|
|
capability_allows_execution \
|
|
"$valid_handoff" "$expected_correlation" "$expected_hash" \
|
|
"$expected_target" "$expected_backchannel" \
|
|
|| fail "eindeutig verfügbarer Capability-Handoff wurde abgelehnt"
|
|
|
|
assert_handoff_rejected() {
|
|
local name=$1
|
|
local payload=$2
|
|
if validate_capability_handoff "$payload"; then
|
|
fail "$name wurde nicht abgelehnt"
|
|
fi
|
|
}
|
|
|
|
assert_execution_rejected() {
|
|
local name=$1
|
|
local payload=$2
|
|
if capability_allows_execution \
|
|
"$payload" "$expected_correlation" "$expected_hash" \
|
|
"$expected_target" "$expected_backchannel"; then
|
|
fail "Ausführung für $name wurde nicht abgelehnt"
|
|
fi
|
|
}
|
|
|
|
assert_handoff_rejected "pauschale Rechteforderung" \
|
|
"${valid_handoff/calendar.event.create:one-shot/rw}"
|
|
assert_handoff_rejected "calendar/public als Zielersatz" \
|
|
"${valid_handoff/private-calendar-ref:orga-owner/calendar\/public}"
|
|
assert_handoff_rejected "fehlende correlation_id" \
|
|
"$(sed '/^correlation_id:/d' <<<"$valid_handoff")"
|
|
assert_handoff_rejected "fehlender status" \
|
|
"$(sed '/^status:/d' <<<"$valid_handoff")"
|
|
assert_handoff_rejected "fehlender Backchannel" \
|
|
"$(sed '/^requested_backchannel:/d' <<<"$valid_handoff")"
|
|
|
|
ambiguous_handoff=${valid_handoff/capability_check=unambiguous/capability_check=ambiguous}
|
|
failed_handoff=${valid_handoff/status: available/status: failed}
|
|
assert_execution_rejected "mehrdeutige Capability-Prüfung" "$ambiguous_handoff"
|
|
assert_execution_rejected "fehlgeschlagene Capability-Prüfung" "$failed_handoff"
|
|
assert_execution_rejected "abweichende correlation_id" \
|
|
"${valid_handoff/orga-cal-test-v1/orga-cal-other-v1}"
|
|
assert_execution_rejected "abweichenden Payload-Hash" \
|
|
"${valid_handoff/$expected_hash/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb}"
|
|
assert_execution_rejected "abweichendes festes Ziel" \
|
|
"${valid_handoff/private-calendar-ref:orga-owner/private-calendar-ref:other}"
|
|
|
|
assert_contains "$admin" "role-id: administrator"
|
|
admin_catalog_version=$(
|
|
jq -r '.roles[] | select(.id == "administrator") | .version' "$catalog"
|
|
)
|
|
[[ $admin_catalog_version == 3.1.0 ]] \
|
|
|| fail "unerwartete Administrator-Version im Katalog"
|
|
# Eskalationsanlass im Ergebnisformat (SDLC-Playbook, Punkt 3): Administrator
|
|
# hat kein Datei-Artefakt, der Anlass steht in der Chat-Übergabe.
|
|
assert_contains "$admin" "Eskalationsanlass"
|
|
assert_contains "$admin" "gemessener Qualitätsnotwendigkeit"
|
|
assert_contains "$admin" "kein Datei-Artefakt"
|
|
|
|
assert_contains "$admin" "/home/drone/nixos-config"
|
|
assert_contains "$admin" "maßgebliche"
|
|
assert_contains "$admin" "Source of Truth"
|
|
for managed_area in Paketen Diensten Benutzern Netzwerk Berechtigungen Deployment; do
|
|
assert_contains "$admin" "$managed_area"
|
|
done
|
|
assert_contains "$admin" "NixOS-, Home-Manager- oder Flake-Konfiguration"
|
|
assert_contains "$admin" "deklarativ ändern, risikogerecht prüfen"
|
|
assert_contains "$admin" "Freigabe aktivieren"
|
|
|
|
assert_contains "$admin" "read-only Diagnose"
|
|
assert_contains "$admin" "zeitlich und sachlich begrenzten"
|
|
assert_contains "$admin" "Notfalländerung"
|
|
assert_contains "$admin" "als Konfigurationsdrift"
|
|
assert_contains "$admin" "zurückgenommen oder in der deklarativen Konfiguration"
|
|
assert_contains "$admin" "keinen undokumentierten Konfigurationsdrift"
|
|
assert_contains "$admin" "Keine bekannte Konfigurationsdrift"
|
|
|
|
assert_contains "$admin" "Dateibesitz muss exklusiv sein"
|
|
assert_contains "$admin" "Keine Klartext-Secrets"
|
|
assert_contains "$admin" "Keine destruktiven Git-Befehle"
|
|
assert_contains "$admin" "im Auftrag ausdrücklich"
|
|
|
|
printf 'OK: Rollenkatalog und Sicherheitsgrenzen\n'
|