feat: vault — publishing gate for external-facing agent actions (#19)
Implements the vault subsystem: a JSONL queue and gate agent that sits between agent output and irreversible external actions (emails, posts, API calls, charges). New files: - vault/vault-poll.sh: cron entry (*/30), three phases: retry approved, timeout escalations (48h), invoke vault-agent for new pending actions - vault/vault-agent.sh: claude -p wrapper that classifies and routes actions based on risk × reversibility routing table - vault/vault-fire.sh: two-phase dispatcher (pending→approved→fired) with per-action locking and webhook-call handler - vault/vault-reject.sh: moves actions to rejected/ with reason + timestamp - vault/PROMPT.md: vault-agent system prompt with routing table Modified: - lib/matrix_listener.sh: new vault dispatch branch for APPROVE/REJECT replies to escalation threads Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5aa0b42481
commit
e503273fba
11 changed files with 555 additions and 0 deletions
91
vault/vault-agent.sh
Executable file
91
vault/vault-agent.sh
Executable file
|
|
@ -0,0 +1,91 @@
|
|||
#!/usr/bin/env bash
|
||||
# vault-agent.sh — Invoke claude -p to classify and route pending vault actions
|
||||
#
|
||||
# Called by vault-poll.sh when pending actions exist. Reads all pending/*.json,
|
||||
# builds a prompt with action summaries, and lets the LLM decide routing.
|
||||
#
|
||||
# The LLM can call vault-fire.sh (auto-approve) or vault-reject.sh (reject)
|
||||
# directly. For escalations, it sends a Matrix message and marks the action
|
||||
# as "escalated" in pending/ so vault-poll skips it on future runs.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "${SCRIPT_DIR}/../lib/env.sh"
|
||||
|
||||
VAULT_DIR="${FACTORY_ROOT}/vault"
|
||||
PROMPT_FILE="${VAULT_DIR}/PROMPT.md"
|
||||
LOGFILE="${VAULT_DIR}/vault.log"
|
||||
CLAUDE_TIMEOUT="${CLAUDE_TIMEOUT:-3600}"
|
||||
|
||||
log() {
|
||||
printf '[%s] vault-agent: %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*" >> "$LOGFILE"
|
||||
}
|
||||
|
||||
# Collect all pending actions (skip already-escalated)
|
||||
ACTIONS_BATCH=""
|
||||
ACTION_COUNT=0
|
||||
|
||||
for action_file in "${VAULT_DIR}/pending/"*.json; do
|
||||
[ -f "$action_file" ] || continue
|
||||
|
||||
ACTION_STATUS=$(jq -r '.status // ""' < "$action_file" 2>/dev/null)
|
||||
[ "$ACTION_STATUS" = "escalated" ] && continue
|
||||
|
||||
# Validate JSON
|
||||
if ! jq empty < "$action_file" 2>/dev/null; then
|
||||
ACTION_ID=$(basename "$action_file" .json)
|
||||
log "malformed JSON: $action_file — rejecting"
|
||||
bash "${VAULT_DIR}/vault-reject.sh" "$ACTION_ID" "malformed JSON" 2>/dev/null || true
|
||||
continue
|
||||
fi
|
||||
|
||||
ACTION_JSON=$(cat "$action_file")
|
||||
ACTIONS_BATCH="${ACTIONS_BATCH}
|
||||
--- ACTION ---
|
||||
$(echo "$ACTION_JSON" | jq '.')
|
||||
--- END ACTION ---
|
||||
"
|
||||
ACTION_COUNT=$((ACTION_COUNT + 1))
|
||||
done
|
||||
|
||||
if [ "$ACTION_COUNT" -eq 0 ]; then
|
||||
log "no actionable pending items"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "processing $ACTION_COUNT pending action(s) via claude -p"
|
||||
|
||||
# Build the prompt
|
||||
SYSTEM_PROMPT=$(cat "$PROMPT_FILE" 2>/dev/null || echo "You are a vault agent. Classify and route actions.")
|
||||
|
||||
PROMPT="${SYSTEM_PROMPT}
|
||||
|
||||
## Pending Actions (${ACTION_COUNT} total)
|
||||
${ACTIONS_BATCH}
|
||||
|
||||
## Environment
|
||||
- FACTORY_ROOT=${FACTORY_ROOT}
|
||||
- Vault directory: ${VAULT_DIR}
|
||||
- vault-fire.sh: bash ${VAULT_DIR}/vault-fire.sh <action-id>
|
||||
- vault-reject.sh: bash ${VAULT_DIR}/vault-reject.sh <action-id> \"<reason>\"
|
||||
- matrix_send is available after: source ${FACTORY_ROOT}/lib/env.sh
|
||||
|
||||
Process each action now. For auto-approve, fire immediately. For escalate,
|
||||
send Matrix message and mark as escalated. For reject, call vault-reject.sh."
|
||||
|
||||
CLAUDE_OUTPUT=$(timeout "$CLAUDE_TIMEOUT" claude -p "$PROMPT" \
|
||||
--model sonnet \
|
||||
--dangerously-skip-permissions \
|
||||
--max-turns 20 \
|
||||
2>/dev/null) || true
|
||||
|
||||
log "claude finished ($(echo "$CLAUDE_OUTPUT" | wc -c) bytes)"
|
||||
|
||||
# Log routing decisions
|
||||
ROUTES=$(echo "$CLAUDE_OUTPUT" | grep "^ROUTE:" || true)
|
||||
if [ -n "$ROUTES" ]; then
|
||||
echo "$ROUTES" | while read -r line; do
|
||||
log " $line"
|
||||
done
|
||||
fi
|
||||
Loading…
Add table
Add a link
Reference in a new issue