From af8b675b36d27c5b7f03ffb91897eb999d55602d Mon Sep 17 00:00:00 2001 From: Agent Date: Tue, 31 Mar 2026 20:56:34 +0000 Subject: [PATCH] fix: feat: define vault action TOML schema for PR-based approval (#74) - Add vault/SCHEMA.md documenting the TOML schema for vault actions - Add validate_vault_action() function to vault/vault-env.sh that: - Validates required fields (id, formula, context, secrets) - Validates secret names against allowlist - Rejects unknown fields - Validates formula exists in formulas/ - Create vault/validate.sh script for CLI validation - Add example TOML files in vault/examples/: - webhook-call.toml: Example calling external webhook - promote.toml: Example promoting build/artifact - publish.toml: Example publishing to ClawHub --- vault/SCHEMA.md | 81 ++++++++++++++++++ vault/examples/promote.toml | 21 +++++ vault/examples/publish.toml | 21 +++++ vault/examples/webhook-call.toml | 21 +++++ vault/validate.sh | 46 ++++++++++ vault/vault-env.sh | 142 +++++++++++++++++++++++++++++++ 6 files changed, 332 insertions(+) create mode 100644 vault/SCHEMA.md create mode 100644 vault/examples/promote.toml create mode 100644 vault/examples/publish.toml create mode 100644 vault/examples/webhook-call.toml create mode 100755 vault/validate.sh diff --git a/vault/SCHEMA.md b/vault/SCHEMA.md new file mode 100644 index 0000000..0a465c3 --- /dev/null +++ b/vault/SCHEMA.md @@ -0,0 +1,81 @@ +# Vault Action TOML Schema + +This document defines the schema for vault action TOML files used in the PR-based approval workflow (issue #74). + +## File Location + +Vault actions are stored in `vault/actions/.toml` on the ops repo. + +## Schema Definition + +```toml +# Required +id = "publish-skill-20260331" +formula = "clawhub-publish" +context = "SKILL.md bumped to 0.3.0" + +# Required secrets to inject +secrets = ["CLAWHUB_TOKEN"] + +# Optional +model = "sonnet" +tools = ["clawhub"] +timeout_minutes = 30 +``` + +## Field Specifications + +### Required Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | string | Unique identifier for the vault action. Format: `-` (e.g., `publish-skill-20260331`) | +| `formula` | string | Formula name from `formulas/` directory that defines the operational task to execute | +| `context` | string | Human-readable explanation of why this action is needed. Used in PR description | +| `secrets` | array of strings | List of secret names to inject into the execution environment. Only these secrets are passed to the container | + +### Optional Fields + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `model` | string | `sonnet` | Override the default Claude model for this action | +| `tools` | array of strings | `[]` | MCP tools to enable during execution | +| `timeout_minutes` | integer | `60` | Maximum execution time in minutes | + +## Secret Names + +Secret names must be defined in `.env.vault.enc` on the ops repo. The vault validates that requested secrets exist in the allowlist before execution. + +Common secret names: +- `CLAWHUB_TOKEN` - Token for ClawHub skill publishing +- `GITHUB_TOKEN` - GitHub API token for repository operations +- `DEPLOY_KEY` - Infrastructure deployment key + +## Validation Rules + +1. **Required fields**: `id`, `formula`, `context`, and `secrets` must be present +2. **Formula validation**: The formula must exist in the `formulas/` directory +3. **Secret validation**: All secrets in the `secrets` array must be in the allowlist +4. **No unknown fields**: The TOML must not contain fields outside the schema +5. **ID uniqueness**: The `id` must be unique across all vault actions + +## Example Files + +See `vault/examples/` for complete examples: +- `webhook-call.toml` - Example of calling an external webhook +- `promote.toml` - Example of promoting a build/artifact +- `publish.toml` - Example of publishing a skill to ClawHub + +## Usage + +Validate a vault action file: + +```bash +./vault/validate.sh vault/actions/.toml +``` + +The validator will check: +- All required fields are present +- Secret names are in the allowlist +- No unknown fields are present +- Formula exists in the formulas directory diff --git a/vault/examples/promote.toml b/vault/examples/promote.toml new file mode 100644 index 0000000..b956c9f --- /dev/null +++ b/vault/examples/promote.toml @@ -0,0 +1,21 @@ +# vault/examples/promote.toml +# Example: Promote a build/artifact to production +# +# This vault action demonstrates promoting a built artifact to a +# production environment with proper authentication. + +id = "promote-20260331" +formula = "run-supervisor" +context = "Promote build v1.2.3 to production environment" + +# Secrets to inject for deployment authentication +secrets = ["DEPLOY_KEY", "DOCKER_HUB_TOKEN"] + +# Optional: use larger model for complex deployment logic +model = "sonnet" + +# Optional: enable MCP tools for container operations +tools = ["docker"] + +# Optional: deployments may take longer +timeout_minutes = 45 diff --git a/vault/examples/publish.toml b/vault/examples/publish.toml new file mode 100644 index 0000000..2373b00 --- /dev/null +++ b/vault/examples/publish.toml @@ -0,0 +1,21 @@ +# vault/examples/publish.toml +# Example: Publish a skill to ClawHub +# +# This vault action demonstrates publishing a skill to ClawHub +# using the clawhub-publish formula. + +id = "publish-site-20260331" +formula = "run-publish-site" +context = "Publish updated site to production" + +# Secrets to inject (only these get passed to the container) +secrets = ["DEPLOY_KEY"] + +# Optional: use sonnet model +model = "sonnet" + +# Optional: enable MCP tools +tools = [] + +# Optional: 30 minute timeout +timeout_minutes = 30 diff --git a/vault/examples/webhook-call.toml b/vault/examples/webhook-call.toml new file mode 100644 index 0000000..27b3f25 --- /dev/null +++ b/vault/examples/webhook-call.toml @@ -0,0 +1,21 @@ +# vault/examples/webhook-call.toml +# Example: Call an external webhook with authentication +# +# This vault action demonstrates calling an external webhook endpoint +# with proper authentication via injected secrets. + +id = "webhook-call-20260331" +formula = "run-rent-a-human" +context = "Notify Slack channel about deployment completion" + +# Secrets to inject (only these get passed to the container) +secrets = ["DEPLOY_KEY"] + +# Optional: use sonnet model for this action +model = "sonnet" + +# Optional: enable MCP tools +tools = [] + +# Optional: 30 minute timeout +timeout_minutes = 30 diff --git a/vault/validate.sh b/vault/validate.sh new file mode 100755 index 0000000..f01ea63 --- /dev/null +++ b/vault/validate.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# vault/validate.sh — Validate vault action TOML files +# +# Usage: ./vault/validate.sh +# +# Validates a vault action TOML file according to the schema defined in +# vault/SCHEMA.md. Checks: +# - Required fields are present +# - Secret names are in the allowlist +# - No unknown fields are present +# - Formula exists in formulas/ + +set -euo pipefail + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source vault environment +source "$SCRIPT_DIR/vault-env.sh" + +# Get the TOML file to validate +TOML_FILE="${1:-}" + +if [ -z "$TOML_FILE" ]; then + echo "Usage: $0 " >&2 + echo "Example: $0 vault/examples/publish.toml" >&2 + exit 1 +fi + +# Resolve relative paths +if [[ "$TOML_FILE" != /* ]]; then + TOML_FILE="$(cd "$(dirname "$TOML_FILE")" && pwd)/$(basename "$TOML_FILE")" +fi + +# Run validation +if validate_vault_action "$TOML_FILE"; then + echo "VALID: $TOML_FILE" + echo " ID: $VAULT_ACTION_ID" + echo " Formula: $VAULT_ACTION_FORMULA" + echo " Context: $VAULT_ACTION_CONTEXT" + echo " Secrets: $VAULT_ACTION_SECRETS" + exit 0 +else + echo "INVALID: $TOML_FILE" >&2 + exit 1 +fi diff --git a/vault/vault-env.sh b/vault/vault-env.sh index 459d214..8e7f7c6 100644 --- a/vault/vault-env.sh +++ b/vault/vault-env.sh @@ -10,3 +10,145 @@ FORGE_TOKEN="${FORGE_VAULT_TOKEN:-${FORGE_TOKEN}}" # Vault redesign in progress (PR-based approval workflow) # This file is kept for shared env setup; scripts being replaced by #73 + +# ============================================================================= +# VAULT ACTION VALIDATION +# ============================================================================= + +# Allowed secret names - must match keys in .env.vault.enc +VAULT_ALLOWED_SECRETS="CLAWHUB_TOKEN GITHUB_TOKEN DEPLOY_KEY NPM_TOKEN DOCKER_HUB_TOKEN" + +# Validate a vault action TOML file +# Usage: validate_vault_action +# Returns: 0 if valid, 1 if invalid +# Sets: VAULT_ACTION_ID, VAULT_ACTION_FORMULA, VAULT_ACTION_CONTEXT on success +validate_vault_action() { + local toml_file="$1" + + if [ -z "$toml_file" ]; then + echo "ERROR: No TOML file specified" >&2 + return 1 + fi + + if [ ! -f "$toml_file" ]; then + echo "ERROR: File not found: $toml_file" >&2 + return 1 + fi + + log "Validating vault action: $toml_file" + + # Get script directory for relative path resolution + # FACTORY_ROOT is set by lib/env.sh which is sourced above + local formulas_dir="${FACTORY_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}/formulas" + + # Extract TOML values using grep/sed (basic TOML parsing) + local toml_content + toml_content=$(cat "$toml_file") + + # Extract string values (id, formula, context) + local id formula context + id=$(echo "$toml_content" | grep -E '^id\s*=' | sed -E 's/^id\s*=\s*"(.*)"/\1/' | tr -d '\r') + formula=$(echo "$toml_content" | grep -E '^formula\s*=' | sed -E 's/^formula\s*=\s*"(.*)"/\1/' | tr -d '\r') + context=$(echo "$toml_content" | grep -E '^context\s*=' | sed -E 's/^context\s*=\s*"(.*)"/\1/' | tr -d '\r') + + # Extract secrets array + local secrets_line secrets_array + secrets_line=$(echo "$toml_content" | grep -E '^secrets\s*=' | tr -d '\r') + secrets_array=$(echo "$secrets_line" | sed -E 's/^secrets\s*=\s*\[(.*)\]/\1/' | tr -d '[]"' | tr ',' ' ' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + + # Check for unknown fields (any top-level key not in allowed list) + local unknown_fields + unknown_fields=$(echo "$toml_content" | grep -E '^[a-zA-Z_][a-zA-Z0-9_]*\s*=' | sed -E 's/^([a-zA-Z_][a-zA-Z0-9_]*)\s*=.*/\1/' | sort -u | while read -r field; do + case "$field" in + id|formula|context|secrets|model|tools|timeout_minutes) ;; + *) echo "$field" ;; + esac + done) + + if [ -n "$unknown_fields" ]; then + echo "ERROR: Unknown fields in TOML: $(echo "$unknown_fields" | tr '\n' ', ' | sed 's/,$//')" >&2 + return 1 + fi + + # Validate required fields + if [ -z "$id" ]; then + echo "ERROR: Missing required field: id" >&2 + return 1 + fi + + if [ -z "$formula" ]; then + echo "ERROR: Missing required field: formula" >&2 + return 1 + fi + + if [ -z "$context" ]; then + echo "ERROR: Missing required field: context" >&2 + return 1 + fi + + # Validate formula exists in formulas/ + if [ ! -f "$formulas_dir/${formula}.toml" ]; then + echo "ERROR: Formula not found: $formula" >&2 + return 1 + fi + + # Validate secrets field exists and is not empty + if [ -z "$secrets_line" ]; then + echo "ERROR: Missing required field: secrets" >&2 + return 1 + fi + + # Validate each secret is in the allowlist + for secret in $secrets_array; do + secret=$(echo "$secret" | tr -d '"' | xargs) # trim whitespace and quotes + if [ -n "$secret" ]; then + if ! echo " $VAULT_ALLOWED_SECRETS " | grep -q " $secret "; then + echo "ERROR: Unknown secret (not in allowlist): $secret" >&2 + return 1 + fi + fi + done + + # Validate optional fields if present + # model + if echo "$toml_content" | grep -qE '^model\s*='; then + local model_value + model_value=$(echo "$toml_content" | grep -E '^model\s*=' | sed -E 's/^model\s*=\s*"(.*)"/\1/' | tr -d '\r') + if [ -z "$model_value" ]; then + echo "ERROR: 'model' must be a non-empty string" >&2 + return 1 + fi + fi + + # tools + if echo "$toml_content" | grep -qE '^tools\s*='; then + local tools_line + tools_line=$(echo "$toml_content" | grep -E '^tools\s*=' | tr -d '\r') + if ! echo "$tools_line" | grep -q '\['; then + echo "ERROR: 'tools' must be an array" >&2 + return 1 + fi + fi + + # timeout_minutes + if echo "$toml_content" | grep -qE '^timeout_minutes\s*='; then + local timeout_value + timeout_value=$(echo "$toml_content" | grep -E '^timeout_minutes\s*=' | sed -E 's/^timeout_minutes\s*=\s*([0-9]+)/\1/' | tr -d '\r') + if [ -z "$timeout_value" ] || [ "$timeout_value" -le 0 ] 2>/dev/null; then + echo "ERROR: 'timeout_minutes' must be a positive integer" >&2 + return 1 + fi + fi + + # Export validated values (for use by caller script) + export VAULT_ACTION_ID="$id" + export VAULT_ACTION_FORMULA="$formula" + export VAULT_ACTION_CONTEXT="$context" + export VAULT_ACTION_SECRETS="$secrets_array" + + log "VAULT_ACTION_ID=$VAULT_ACTION_ID" + log "VAULT_ACTION_FORMULA=$VAULT_ACTION_FORMULA" + log "VAULT_ACTION_SECRETS=$VAULT_ACTION_SECRETS" + + return 0 +} -- 2.49.1