Merge pull request 'fix: feat: define vault action TOML schema for PR-based approval (#74)' (#80) from fix/issue-74 into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
This commit is contained in:
commit
4be719bcef
6 changed files with 332 additions and 0 deletions
81
vault/SCHEMA.md
Normal file
81
vault/SCHEMA.md
Normal file
|
|
@ -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/<action-id>.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: `<action-type>-<date>` (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/<action-id>.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
|
||||||
21
vault/examples/promote.toml
Normal file
21
vault/examples/promote.toml
Normal file
|
|
@ -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
|
||||||
21
vault/examples/publish.toml
Normal file
21
vault/examples/publish.toml
Normal file
|
|
@ -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
|
||||||
21
vault/examples/webhook-call.toml
Normal file
21
vault/examples/webhook-call.toml
Normal file
|
|
@ -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
|
||||||
46
vault/validate.sh
Executable file
46
vault/validate.sh
Executable file
|
|
@ -0,0 +1,46 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# vault/validate.sh — Validate vault action TOML files
|
||||||
|
#
|
||||||
|
# Usage: ./vault/validate.sh <path-to-toml>
|
||||||
|
#
|
||||||
|
# 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 <path-to-toml>" >&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
|
||||||
|
|
@ -10,3 +10,145 @@ FORGE_TOKEN="${FORGE_VAULT_TOKEN:-${FORGE_TOKEN}}"
|
||||||
|
|
||||||
# Vault redesign in progress (PR-based approval workflow)
|
# Vault redesign in progress (PR-based approval workflow)
|
||||||
# This file is kept for shared env setup; scripts being replaced by #73
|
# 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 <path-to-toml>
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue