From 0981712c6e314c72b62b25eb06a1dad1031a429d Mon Sep 17 00:00:00 2001 From: aeoess Date: Sat, 4 Apr 2026 15:46:30 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20/governance=20skill=20=E2=80=94=20crypt?= =?UTF-8?q?ographic=20accountability=20for=20agent=20sessions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ed25519-signed receipts for every destructive action. Scope enforcement via .gstack-scope.json. Hash-chained audit ledger for tamper detection. - Session identity: Ed25519 keypair generated at first use - Signed receipts: deploy, push, delete, db mutations get cryptographic proof - Scope enforcement: declare allowed/blocked actions in .gstack-scope.json - Hash chain: each receipt includes previous receipt hash (tamper-evident) - Composes with /careful, /ship, /review, /guard - Zero dependencies — works standalone with openssl - Optional: compatible with Agent Passport System for external verification --- governance/SKILL.md | 154 +++++++++++++++++++++++++++++ governance/bin/check-governance.sh | 104 +++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 governance/SKILL.md create mode 100755 governance/bin/check-governance.sh diff --git a/governance/SKILL.md b/governance/SKILL.md new file mode 100644 index 000000000..27f007e93 --- /dev/null +++ b/governance/SKILL.md @@ -0,0 +1,154 @@ +--- +name: governance +version: 0.1.0 +description: | + Cryptographic accountability for agent sessions. Every destructive action gets an + Ed25519-signed receipt. Scope enforcement prevents the agent from exceeding its + declared authority. Tamper-evident audit trail survives session restarts. + Use when asked to "govern this session", "accountability mode", "audit mode", + or before any high-stakes workflow (deploy, data migration, production changes). (gstack) +allowed-tools: + - Bash + - Read + - Write + - Edit + - AskUserQuestion +hooks: + PreToolUse: + - matcher: "Bash" + hooks: + - type: command + command: "bash ${CLAUDE_SKILL_DIR}/bin/check-governance.sh" + statusMessage: "Checking governance scope..." +--- + +# /governance — Cryptographic Accountability for Agent Sessions + +Governance mode is now **active**. This session has a cryptographic identity and +every significant action produces a signed, tamper-evident receipt. + +```bash +mkdir -p ~/.gstack/governance +mkdir -p ~/.gstack/analytics + +# Generate session identity (Ed25519 keypair) if not exists +if [ ! -f ~/.gstack/governance/session-key.pem ]; then + openssl genpkey -algorithm ed25519 -out ~/.gstack/governance/session-key.pem 2>/dev/null + openssl pkey -in ~/.gstack/governance/session-key.pem -pubout -out ~/.gstack/governance/session-key.pub 2>/dev/null + echo "Generated session Ed25519 keypair" +fi + +# Extract public key fingerprint +SESSION_PUB=$(openssl pkey -in ~/.gstack/governance/session-key.pub -pubin -text -noout 2>/dev/null | grep -A2 'pub:' | tail -1 | tr -d ' :') +echo "Session identity: ${SESSION_PUB:0:16}..." + +# Initialize audit ledger for this session +LEDGER=~/.gstack/governance/ledger-$(date -u +%Y%m%d-%H%M%S).jsonl +echo "{\"event\":\"session_start\",\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"pubkey\":\"$SESSION_PUB\",\"pid\":\"$$\"}" >> "$LEDGER" +echo "Audit ledger: $LEDGER" + +# Load scope (if declared) +if [ -f .gstack-scope.json ]; then + SCOPE=$(cat .gstack-scope.json) + echo "Scope loaded: $(echo $SCOPE | python3 -c 'import sys,json; d=json.load(sys.stdin); print(", ".join(d.get("allowed",[])))' 2>/dev/null || echo 'custom')" +else + echo "No scope file found (.gstack-scope.json). Running in audit-only mode." + echo "Create .gstack-scope.json to enforce scope boundaries." +fi + +echo '{"skill":"governance","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","repo":"'$(basename "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null || echo "unknown")'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true +``` + +## What this does + +| Feature | Description | +|---------|-------------| +| **Session identity** | Ed25519 keypair generated at first use. Persists across sessions. | +| **Signed receipts** | Every destructive action (deploy, push, delete, db mutation) gets a receipt signed with the session key. | +| **Scope enforcement** | If `.gstack-scope.json` exists, actions outside the declared scope are blocked. | +| **Audit ledger** | Append-only JSONL file. Every action logged with timestamp, command hash, and signature. | +| **Tamper detection** | Each receipt includes the hash of the previous receipt. Break the chain = evidence of tampering. | + +## Scope file format + + +Create `.gstack-scope.json` in your project root: + +```json +{ + "allowed": ["read", "write", "test", "review"], + "blocked": ["deploy", "db_migrate", "force_push"], + "spend_limit": null, + "expires_at": null, + "principal": "your-name" +} +``` + +- **allowed**: Actions the agent can perform. If empty, all actions are allowed (audit-only). +- **blocked**: Actions that are always denied, even if in `allowed`. +- **spend_limit**: Optional. Maximum dollar amount for purchase/deploy operations. +- **principal**: Who authorized this session. Logged in every receipt. + +Without a scope file, `/governance` runs in **audit-only mode** — everything is logged and signed, nothing is blocked. Add a scope file to enable enforcement. + +## How receipts work + +Every destructive command produces a receipt: + +```json +{ + "action": "git push origin main", + "action_hash": "sha256:a1b2c3...", + "timestamp": "2026-04-04T12:00:00Z", + "scope_check": "permit", + "previous_receipt_hash": "sha256:9f8e7d...", + "signature": "ed25519:..." +} +``` + +The `previous_receipt_hash` creates a hash chain — if any receipt is deleted or modified, +the chain breaks and the tampering is detectable. This is the same pattern used in +blockchain and certificate transparency logs. + +## Composing with other gstack skills + +`/governance` composes naturally with existing gstack skills: + +- **`/governance` + `/ship`**: Every deploy gets a signed receipt. If something breaks in prod, you can prove exactly what was deployed, when, and with what authorization. +- **`/governance` + `/careful`**: `/careful` warns before destructive commands. `/governance` signs a receipt if you proceed. Defense in depth. +- **`/governance` + `/review`**: Code review findings are logged in the audit ledger. A reviewer can prove they flagged an issue before it shipped. +- **`/governance` + `/guard`**: Maximum safety. Scope enforcement + destructive command warnings + signed audit trail. + +## Viewing the audit trail + +```bash +# View recent receipts +cat ~/.gstack/governance/ledger-*.jsonl | tail -20 | python3 -m json.tool + +# Verify chain integrity +cat ~/.gstack/governance/ledger-*.jsonl | python3 -c " +import sys, json, hashlib +prev = None +for line in sys.stdin: + r = json.loads(line) + if prev and r.get('previous_receipt_hash') != prev: + print(f'CHAIN BREAK at {r[\"ts\"]}') + prev = hashlib.sha256(line.encode()).hexdigest() +print(f'Chain intact. {sum(1 for _ in open(sys.argv[1]) if True)} receipts verified.' if prev else 'Empty ledger.') +" 2>/dev/null || echo "Use: cat ledger-*.jsonl | python3 verify_chain.py" +``` + +## External verification (optional) + +For teams that want external accountability, receipts can be verified against the +[Agent Passport System](https://github.com/aeoess/agent-passport-system) gateway: + +```bash +npm install agent-passport-system # optional — governance works without this +``` + +With APS, the session identity becomes a verifiable agent passport with delegation +chains, reputation scoring, and cascade revocation. The receipts produced by +`/governance` are compatible with APS receipt format. + +This is optional. `/governance` works standalone with zero dependencies. diff --git a/governance/bin/check-governance.sh b/governance/bin/check-governance.sh new file mode 100755 index 000000000..8d6b7bf58 --- /dev/null +++ b/governance/bin/check-governance.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# Governance hook for gstack — checks scope and signs receipts +# Called by PreToolUse hook on every Bash command + +set -euo pipefail + +GOVERNANCE_DIR="$HOME/.gstack/governance" +SCOPE_FILE=".gstack-scope.json" +LEDGER=$(ls -t "$GOVERNANCE_DIR"/ledger-*.jsonl 2>/dev/null | head -1) +KEY_FILE="$GOVERNANCE_DIR/session-key.pem" + +# Extract command from tool input (Claude Code passes JSON via stdin) +COMMAND=$(cat | python3 -c " +import sys, json +try: + data = json.load(sys.stdin) + print(data.get('command', data.get('input', ''))) +except: + print('') +" 2>/dev/null || echo "") + +if [ -z "$COMMAND" ]; then + exit 0 # No command to check +fi + +# Classify action type +ACTION_TYPE="read" +DESTRUCTIVE=false + +# Destructive patterns (same as /careful, extended) +if echo "$COMMAND" | grep -qiE 'rm\s+-r|drop\s+table|truncate|git\s+push.*(-f|--force)|git\s+reset\s+--hard|kubectl\s+delete|docker\s+rm|deploy|migrate'; then + ACTION_TYPE="destructive" + DESTRUCTIVE=true +elif echo "$COMMAND" | grep -qiE 'git\s+push|npm\s+publish|pip\s+upload|twine\s+upload'; then + ACTION_TYPE="deploy" + DESTRUCTIVE=true +elif echo "$COMMAND" | grep -qiE 'git\s+commit|git\s+add'; then + ACTION_TYPE="write" +elif echo "$COMMAND" | grep -qiE 'curl.*-X\s*(POST|PUT|DELETE|PATCH)|fetch.*method'; then + ACTION_TYPE="network_write" + DESTRUCTIVE=true +fi + +# Check scope if scope file exists +VERDICT="permit" +REASON="" + +if [ -f "$SCOPE_FILE" ]; then + BLOCKED=$(python3 -c " +import json, sys +with open('$SCOPE_FILE') as f: + scope = json.load(f) +blocked = scope.get('blocked', []) +action = '$ACTION_TYPE' +if action in blocked or ('$COMMAND' and any(b in '$COMMAND' for b in blocked)): + print('blocked') +else: + allowed = scope.get('allowed', []) + if allowed and action not in allowed and action != 'read': + print('not_in_scope') + else: + print('ok') +" 2>/dev/null || echo "ok") + + if [ "$BLOCKED" = "blocked" ]; then + VERDICT="deny" + REASON="Action type '$ACTION_TYPE' is in blocked list" + elif [ "$BLOCKED" = "not_in_scope" ]; then + VERDICT="deny" + REASON="Action type '$ACTION_TYPE' not in allowed scope" + fi +fi + +# Sign receipt for destructive actions +if [ "$DESTRUCTIVE" = true ] && [ -n "$LEDGER" ] && [ -f "$KEY_FILE" ]; then + # Get previous receipt hash for chain + PREV_HASH=$(tail -1 "$LEDGER" 2>/dev/null | shasum -a 256 | cut -d' ' -f1 || echo "genesis") + + # Create receipt + ACTION_HASH=$(echo -n "$COMMAND" | shasum -a 256 | cut -d' ' -f1) + TS=$(date -u +%Y-%m-%dT%H:%M:%SZ) + + RECEIPT="{\"event\":\"action\",\"action_type\":\"$ACTION_TYPE\",\"action_hash\":\"sha256:$ACTION_HASH\",\"verdict\":\"$VERDICT\",\"ts\":\"$TS\",\"previous_receipt_hash\":\"sha256:$PREV_HASH\"}" + + # Sign the receipt + SIG=$(echo -n "$RECEIPT" | openssl pkeyutl -sign -inkey "$KEY_FILE" 2>/dev/null | xxd -p | tr -d '\n' || echo "unsigned") + + # Append signed receipt to ledger + echo "$RECEIPT" | python3 -c " +import sys, json +r = json.loads(sys.stdin.read()) +r['signature'] = 'ed25519:${SIG:0:128}' +print(json.dumps(r)) +" >> "$LEDGER" 2>/dev/null || echo "$RECEIPT" >> "$LEDGER" +fi + +# Return verdict to Claude Code hook system +if [ "$VERDICT" = "deny" ]; then + echo "{\"permissionDecision\": \"ask\", \"message\": \"🛡️ Governance: $REASON. Action type: $ACTION_TYPE. Override requires explicit approval.\"}" +else + if [ "$DESTRUCTIVE" = true ]; then + echo "{\"permissionDecision\": \"allow\", \"message\": \"📋 Receipt signed for: $ACTION_TYPE\"}" >&2 + fi +fi