mirror of
https://github.com/Klotzkette/claude-fuer-deutsches-recht
synced 2026-06-09 10:03:19 +00:00
ab54319867
Fix Liquiditätsplan YAML fallback - Add a standard-library YAML fallback for the Liquiditätsplan generator when PyYAML is unavailable. - Keep visible Liquiditätsplanung wording with stable technical slugs. - Correct stale agent recipe delivery example. Validated with plugin structure checks, Claude plugin validation, Python compile checks, generated XLSX load check, YAML/JSON parse checks, and Klagewerkstatt ZIP generation.
200 lines
7.1 KiB
Bash
Executable File
200 lines
7.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Copyright 2026 Anthropic PBC
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
# Deploy a managed-agent template to POST /v1/agents.
|
|
#
|
|
# Resolves manifest conveniences before posting:
|
|
# system: {file: ...} -> inlined string
|
|
# skills: [{path: ...}] -> uploaded, referenced by skill_id
|
|
# callable_agents: [{manifest: ...}] -> created first, referenced by agent id
|
|
#
|
|
# Reader subagents with an `output_schema` block get a thin validation wrapper
|
|
# so their JSON is schema-checked before the orchestrator consumes it.
|
|
#
|
|
# Usage: scripts/agentenrezept-ausliefern.sh <slug>
|
|
# e.g. scripts/agentenrezept-ausliefern.sh aufsichts-monitor
|
|
|
|
set -euo pipefail
|
|
|
|
ROLE="${1:?usage: agentenrezept-ausliefern.sh <slug> [--dry-run]}"
|
|
DRY_RUN=0; [[ "${2:-}" == "--dry-run" ]] && DRY_RUN=1
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
DIR="$ROOT/verwaltete-agentenrezepte/$ROLE"
|
|
API="${ANTHROPIC_API_BASE:-https://api.anthropic.com}"
|
|
[[ $DRY_RUN -eq 1 ]] || : "${ANTHROPIC_API_KEY:?ANTHROPIC_API_KEY must be set}"
|
|
|
|
[[ -f "$DIR/agent.yaml" ]] || { echo "no manifest at $DIR/agent.yaml" >&2; exit 1; }
|
|
|
|
# Validate SKILL_TITLE_PREFIX against the same allowlist the YAML env-var
|
|
# substitution uses. This string flows into `curl -F display_title=...`;
|
|
# without validation, a hostile prefix could inject extra multipart fields
|
|
# or smuggle newlines.
|
|
if [[ -n "${SKILL_TITLE_PREFIX:-}" ]]; then
|
|
if ! [[ "$SKILL_TITLE_PREFIX" =~ ^[A-Za-z0-9._/:@\ -]+$ ]]; then
|
|
echo "refusing SKILL_TITLE_PREFIX: value contains characters outside [A-Za-z0-9._/:@ -]" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
req() {
|
|
curl -sS -H "x-api-key: $ANTHROPIC_API_KEY" \
|
|
-H "anthropic-version: 2023-06-01" \
|
|
-H "anthropic-beta: managed-agents-2026-04-01" \
|
|
-H "content-type: application/json" "$@"
|
|
}
|
|
|
|
# jq + python(pyyaml) do the manifest→payload transform
|
|
command -v jq >/dev/null || { echo "requires jq" >&2; exit 1; }
|
|
python3 -c 'import yaml' 2>/dev/null || { echo "requires python3 + pyyaml" >&2; exit 1; }
|
|
yaml2json() {
|
|
python3 -c '
|
|
import sys,os,re,yaml,json
|
|
SAFE = re.compile(r"^[A-Za-z0-9._/:@-]*$")
|
|
def sub(m):
|
|
name = m.group(1)
|
|
v = os.environ.get(name)
|
|
if v is None:
|
|
return m.group(0)
|
|
if not SAFE.fullmatch(v):
|
|
sys.exit(f"refusing ${{{name}}}: value contains characters outside [A-Za-z0-9._/:@-]")
|
|
return v
|
|
t = open(sys.argv[1]).read()
|
|
t = re.sub(r"\$\{([A-Z0-9_]+)\}", sub, t)
|
|
json.dump(yaml.safe_load(t), sys.stdout)
|
|
' "$1"
|
|
}
|
|
|
|
SKILL_CACHE_FILE="$(mktemp -t skillcache)"
|
|
trap 'rm -f "$SKILL_CACHE_FILE"' EXIT
|
|
upload_skill() {
|
|
local path="$1" key cached
|
|
key="$(basename "$path")"
|
|
cached=$(grep -m1 "^${key}=" "$SKILL_CACHE_FILE" 2>/dev/null | cut -d= -f2-)
|
|
if [[ -n "$cached" ]]; then printf '%s' "$cached"; return; fi
|
|
if [[ $DRY_RUN -eq 1 ]]; then
|
|
cached=$(printf '{"type":"custom","skill_id":"DRYRUN_%s","version":"latest"}' "$key")
|
|
echo "${key}=${cached}" >>"$SKILL_CACHE_FILE"
|
|
printf '%s' "$cached"; return
|
|
fi
|
|
local resp id zip
|
|
zip="$(mktemp -t skill).zip"
|
|
(cd "$(dirname "$path")" && zip -qr "$zip" "$(basename "$path")")
|
|
# /v1/skills uses its own beta header and multipart, not the managed-agents JSON path
|
|
resp=$(curl -sS "$API/v1/skills" \
|
|
-H "x-api-key: $ANTHROPIC_API_KEY" \
|
|
-H "anthropic-version: 2023-06-01" \
|
|
-H "anthropic-beta: skills-2025-10-02" \
|
|
-F "display_title=${SKILL_TITLE_PREFIX:-}$(basename "$path")" \
|
|
-F "files[]=@$zip")
|
|
rm -f "$zip"
|
|
id=$(jq -r '.id // empty' <<<"$resp")
|
|
if [[ -z "$id" ]]; then
|
|
echo "POST /v1/skills failed for $path:" >&2
|
|
echo "$resp" | jq . >&2 2>/dev/null || echo "$resp" >&2
|
|
exit 1
|
|
fi
|
|
cached=$(printf '{"type":"custom","skill_id":"%s","version":"latest"}' "$id")
|
|
echo "${key}=${cached}" >>"$SKILL_CACHE_FILE"
|
|
printf '%s' "$cached"
|
|
}
|
|
|
|
resolve_manifest() {
|
|
local file="$1" base
|
|
base="$(cd "$(dirname "$file")" && pwd)"
|
|
local json
|
|
json=$(yaml2json "$file")
|
|
# Expand any {from_plugin: <dir>} into one {path: ...} per skills/* under that dir.
|
|
local fp
|
|
fp=$(jq -r '.skills[]? | select(.from_plugin) | .from_plugin' <<<"$json" | head -1)
|
|
if [[ -n "$fp" ]]; then
|
|
local plugdir expanded="[]"
|
|
plugdir="$(cd "$base/$fp" && pwd)"
|
|
for sk in "$plugdir"/skills/*/; do
|
|
[[ -d "$sk" ]] || continue
|
|
expanded=$(jq --arg p "${sk%/}" '. + [{__upload:$p}]' <<<"$expanded")
|
|
done
|
|
json=$(jq --argjson e "$expanded" \
|
|
'.skills = ((.skills // [] | map(select(.from_plugin | not))) + $e)' <<<"$json")
|
|
fi
|
|
jq --arg base "$base" '
|
|
.skills = ((.skills // []) | map(
|
|
if .path then {__upload: ($base + "/" + .path)}
|
|
elif .__upload then .
|
|
else . end))
|
|
' <<<"$json"
|
|
}
|
|
|
|
inline_system() {
|
|
local json="$1" base="$2" sysfile text append body
|
|
if jq -e '.system | type == "object"' >/dev/null <<<"$json"; then
|
|
sysfile=$(jq -r '.system.file // empty' <<<"$json")
|
|
text=$(jq -r '.system.text // empty' <<<"$json")
|
|
append=$(jq -r '.system.append // empty' <<<"$json")
|
|
body="$text"
|
|
if [[ -n "$sysfile" ]]; then
|
|
[[ -f "$base/$sysfile" ]] || { echo "system.file not found: $base/$sysfile" >&2; exit 1; }
|
|
body="$(cat "$base/$sysfile")"
|
|
fi
|
|
[[ -n "$append" ]] && body="${body}"$'\n\n'"${append}"
|
|
jq --arg s "$body" '.system=$s' <<<"$json"
|
|
else
|
|
printf '%s' "$json"
|
|
fi
|
|
}
|
|
|
|
create_agent() {
|
|
local file="$1" base json sub_ids skills_json
|
|
base="$(cd "$(dirname "$file")" && pwd)"
|
|
json=$(resolve_manifest "$file")
|
|
json=$(inline_system "$json" "$base")
|
|
|
|
skills_json="[]"
|
|
while IFS= read -r p; do
|
|
[[ -z "$p" ]] && continue
|
|
[[ -d "$p" ]] || { echo "skill path not found: $p" >&2; exit 1; }
|
|
skills_json=$(jq ". + [$(upload_skill "$p")]" <<<"$skills_json")
|
|
done < <(jq -r '.skills[]? | select(.__upload) | .__upload' <<<"$json")
|
|
json=$(jq --argjson s "$skills_json" '.skills=$s' <<<"$json")
|
|
|
|
sub_ids="[]"
|
|
while IFS= read -r m; do
|
|
[[ -z "$m" ]] && continue
|
|
local out sid sver
|
|
out=$(create_agent "$base/$m")
|
|
sid=${out%% *}; sver=${out##* }
|
|
sub_ids=$(jq --arg i "$sid" --argjson v "$sver" '. + [{type:"agent", id:$i, version:$v}]' <<<"$sub_ids")
|
|
done < <(jq -r '.callable_agents[]?.manifest // empty' <<<"$json")
|
|
json=$(jq --argjson c "$sub_ids" '.callable_agents=$c | del(.output_schema)' <<<"$json")
|
|
[[ -n "${DEPLOY_DEBUG:-}" ]] && jq -c '{name, callable_agents}' <<<"$json" >&2
|
|
|
|
if [[ $DRY_RUN -eq 1 ]]; then
|
|
echo "$json" >>"$DRY_OUT"
|
|
jq -r '"DRYRUN_" + .name + " 1"' <<<"$json"; return
|
|
fi
|
|
local resp id ver
|
|
resp=$(req -X POST "$API/v1/agents" -d "$json")
|
|
id=$(jq -r '.id // empty' <<<"$resp")
|
|
ver=$(jq -r '.version // 1' <<<"$resp")
|
|
if [[ -z "$id" ]]; then
|
|
echo "POST /v1/agents failed for $(jq -r .name <<<"$json"):" >&2
|
|
echo "$resp" | jq . >&2 2>/dev/null || echo "$resp" >&2
|
|
exit 1
|
|
fi
|
|
echo "$id $ver"
|
|
}
|
|
|
|
if [[ $DRY_RUN -eq 1 ]]; then
|
|
DRY_OUT="$(mktemp)"
|
|
create_agent "$DIR/agent.yaml" >/dev/null
|
|
echo "# --dry-run: resolved POST /v1/agents bodies (subagents first, orchestrator last)"
|
|
jq -s '.' "$DRY_OUT"
|
|
rm -f "$DRY_OUT"
|
|
exit 0
|
|
fi
|
|
|
|
OUT=$(create_agent "$DIR/agent.yaml")
|
|
AGENT_ID=${OUT%% *}
|
|
echo "deployed: $ROLE"
|
|
echo "agent id: $AGENT_ID"
|
|
echo "console: https://console.anthropic.com/agents/$AGENT_ID"
|