diff --git a/README.md b/README.md index 84c9c90..3a8965d 100644 --- a/README.md +++ b/README.md @@ -402,6 +402,7 @@ jedem Chat-Turn im Hot-Memory-Block auf: - **oauth-reauth-reflex** — bei 401: ZUERST `oauth_get_token` (Auto-Refresh), nur bei dessen Fehler `oauth_authorize` - **no-skill-drift** — kein Drift vom Skill zu Ad-hoc-Bash-Befehlen. Skill kaputt? `skill_logs` + `skill_update`. Niemals nur SAGEN „ich baue dir einen Skill", wenn `skill_create` nicht wirklich gefeuert wird - **runtime-topology** (architektur) — ARIA laeuft als `claude`-CLI-Subprocess IM aria-proxy Container (alpine — kein python3/jq), NICHT im aria-brain. `/data/skills/` und `BRAIN_INTERNAL_URL` existieren dort nicht. Brain-Resources via Brain-Tools (`oauth_get_token`, `memory_search`, `run_` …), nicht via Bash. SSH zur VM-Host via `ssh aria@host` (Key liegt im Proxy) +- **PreToolUse-Hard-Block** *(claude-CLI Hook)*: `proxy-patches/pre-tool-bash-block.js` ist als PreToolUse-Hook fuer das Bash-Tool im aria-proxy-Container registriert (via `/etc/claude-code/managed-settings.json`). Vor JEDEM Bash-Tool-Call wird Brain-Endpoint `/skills/can-bash-host` gefragt — wenn die URL gegen einen Host laeuft fuer den bereits ein matching Skill existiert, exit 2 + Stderr → claude-CLI lehnt den Tool-Call ab und gibt ARIA einen echten Tool-Error zurueck *„BLOCKED — nutze run_X stattdessen"*. Im Gegensatz zu seed_rules ist das echter Zwang, kein Hinweis den sie ignorieren kann. Fail-open: bei Brain-Timeout/Fehler greift der Block nicht (kein Lockout). - **scaffold-reflex** — Brain trackt cross-session welche externen Hosts via Bash-curl wiederholt (≥3× in 24h) ohne passenden Skill aufgerufen wurden. Ergebnis landet als `## API-Heuristik`-Block im System-Prompt. **Auto-Scaffold**: bei bekannten Hosts (Spotify, GitHub, OpenAI etc.) legt Brain den Skill automatisch an — ARIA findet ihn beim nächsten Turn vor (author=`aria-auto`) und nutzt `run_` statt curlen. Toggle via ENV `BRAIN_AUTO_SCAFFOLD=false`. **Bypass-Lehre**: wenn ARIA trotz vorhandenem Skill weiter curlt (Skill-Bypass), erkennt Brain das im agent_stream und (1) injiziert einen drastischen `🚨 SKILL-BYPASS`-Hint im aktuellen System-Prompt und (2) speichert ein pinned `type=rule, source=auto-feedback` Memory mit Skill+Host (idempotent via migration_key `auto/skill-bypass/`) — damit lernt sie es cross-session, nicht nur in der aktuellen Konversation. Data-Source: `agent_stream.jsonl`, Cache 5 min - **external-api-auth-strategy** — OAuth2 → `oauth_get_token`, sonst `config_schema`, NIEMALS hardcoden diff --git a/aria-brain/main.py b/aria-brain/main.py index 4240c2e..ea78352 100644 --- a/aria-brain/main.py +++ b/aria-brain/main.py @@ -805,6 +805,52 @@ class SkillScaffold(BaseModel): author: str = "stefan" +class SkillCanBashHostIn(BaseModel): + command: str + + +@app.post("/skills/can-bash-host") +def skills_can_bash_host(body: SkillCanBashHostIn): + """Prueft ob ein Bash-Command gegen einen Host laufen will fuer den + bereits ein matching Skill existiert. Wird vom claude-CLI PreToolUse- + Hook im aria-proxy gefragt — wenn block=True, weist der Hook den + Bash-Call mit Fehlermeldung zurueck und ARIA muss `run_` nehmen. + + Antwort: {block: bool, host?: str, skill?: str, safe_tool?: str} + """ + import re as _re + cmd = (body.command or "").strip() + if not cmd: + return {"block": False} + skills = skills_mod.list_skills(active_only=False) + if not skills: + return {"block": False} + + # Stem-Map: jeder Skill-Name als potentieller Hostname-Match + # (yt_dlp_download → 'yt_dlp_download', 'spotify' → 'spotify' etc.) + stem_to_skill = {} + for s in skills: + sname = (s.get("name") or "").lower() + if sname: + stem_to_skill[sname] = sname + # Underscore-Variante auch als Stem akzeptieren + stem_to_skill[sname.replace("_", "-")] = sname + + # Alle https-URLs im Command einsammeln + matchen + for url_host in _re.findall(r'https?://([a-zA-Z0-9.\-]+)', cmd): + host_lower = url_host.lower() + for stem, skill_name in stem_to_skill.items(): + if stem and stem in host_lower: + safe_tool = "run_" + _re.sub(r"[^a-zA-Z0-9_]", "_", skill_name) + return { + "block": True, + "host": url_host, + "skill": skill_name, + "safe_tool": safe_tool, + } + return {"block": False} + + @app.get("/skills/templates") def skills_templates_list(): """Liste der verfuegbaren Templates — fuer UI und Dokumentation.""" diff --git a/docker-compose.yml b/docker-compose.yml index 7b514ae..e3ae226 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,8 @@ services: cp /proxy-patches/openai-to-cli.js $$DIST/adapter/openai-to-cli.js && cp /proxy-patches/cli-to-openai.js $$DIST/adapter/cli-to-openai.js && cp /proxy-patches/routes.js $$DIST/server/routes.js && + mkdir -p /etc/claude-code && + cp /proxy-patches/managed-settings.json /etc/claude-code/managed-settings.json && claude-max-api" volumes: - ~/.claude:/root/.claude # Claude CLI Auth (Credentials in /root/.claude/.credentials.json) diff --git a/proxy-patches/managed-settings.json b/proxy-patches/managed-settings.json new file mode 100644 index 0000000..1f3b321 --- /dev/null +++ b/proxy-patches/managed-settings.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "node /proxy-patches/pre-tool-bash-block.js" + } + ] + } + ] + } +} diff --git a/proxy-patches/pre-tool-bash-block.js b/proxy-patches/pre-tool-bash-block.js new file mode 100644 index 0000000..371a497 --- /dev/null +++ b/proxy-patches/pre-tool-bash-block.js @@ -0,0 +1,95 @@ +#!/usr/bin/env node +/** + * ARIA claude-CLI PreToolUse-Hook: blockiert Bash-Calls gegen externe APIs + * fuer die bereits ein matching Skill im Brain existiert. + * + * Wird von claude-CLI PRO Tool-Use vor der Ausfuehrung mit dem Tool-Use- + * JSON via stdin aufgerufen. Wenn wir exit 2 mit Stderr returnen, lehnt + * claude-CLI den Tool-Call ab und gibt die Stderr als tool_use_error + * an das LLM zurueck — ARIA bekommt also eine echte Fehlermeldung und + * MUSS umdenken (nicht nur Prompt-Anweisung die sie ignorieren kann). + * + * Fail-open: bei jeder Art von Fehler (Brain nicht erreichbar, kaputtes + * JSON etc.) exit 0 — wir blockieren Stefan's eigentliche Arbeit nicht + * nur weil der Block-Mechanismus selber haengt. + */ + +const http = require("http"); + +const BRAIN_URL = process.env.BRAIN_INTERNAL_URL || "http://aria-brain:8080"; +const BRAIN_TIMEOUT_MS = 3000; + +function fail_open(reason) { + if (process.env.HOOK_DEBUG) console.error(`hook-skip: ${reason}`); + process.exit(0); +} + +function block(message) { + // exit 2 = block in claude-CLI PreToolUse hook contract + process.stderr.write(message); + process.exit(2); +} + +let stdinBuf = ""; +process.stdin.on("data", chunk => stdinBuf += chunk); +process.stdin.on("end", () => { + let payload; + try { + payload = JSON.parse(stdinBuf || "{}"); + } catch (_) { + return fail_open("stdin not json"); + } + // claude-CLI Hook-Format kann je nach Version variieren — + // wir akzeptieren tool_name oder hook_event_name in Kombination + const toolName = payload.tool_name || payload.tool || ""; + if (toolName !== "Bash") return fail_open("tool != Bash"); + const command = (payload.tool_input && payload.tool_input.command) || + payload.command || ""; + if (!command) return fail_open("no command"); + // Schnellfilter: nur wenn ueberhaupt eine URL drin ist + if (!/https?:\/\//i.test(command)) return fail_open("no url"); + + // Brain fragen ob ein matching Skill existiert + const body = JSON.stringify({ command }); + const req = http.request( + BRAIN_URL + "/skills/can-bash-host", + { + method: "POST", + headers: { + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(body), + }, + timeout: BRAIN_TIMEOUT_MS, + }, + (res) => { + let chunks = ""; + res.on("data", d => chunks += d); + res.on("end", () => { + let r; + try { r = JSON.parse(chunks); } catch (_) { return fail_open("brain bad json"); } + if (!r || !r.block) return fail_open("brain says ok"); + const skill = r.skill || "?"; + const host = r.host || "?"; + const safeTool = r.safe_tool || `run_${skill}`; + const msg = + `🚨 BASH GEGEN ${host} BLOCKIERT.\n\n` + + `Es existiert bereits ein Skill '${skill}' fuer diesen Host. ` + + `Stefan hat das System so eingerichtet dass Skills via ` + + `\`${safeTool}\` direkt aufgerufen werden — das ist 5-10x ` + + `schneller als der Bash-Curl-Wrapper.\n\n` + + `Konkret: nutze JETZT \`${safeTool}\` mit den passenden ` + + `Parametern (method/path/body) statt curl. Wenn der Skill ` + + `nicht das liefert was Du brauchst: skill_update mit Fix, ` + + `nicht zurueck zu Bash.`; + block(msg); + }); + } + ); + req.on("error", () => fail_open("brain network error")); + req.on("timeout", () => { req.destroy(); fail_open("brain timeout"); }); + req.write(body); + req.end(); +}); + +// Falls stdin nie ein 'end' triggert — Timeout damit wir nicht haengen +setTimeout(() => fail_open("stdin timeout"), 4000);