Merge pull request 'fix: feat: vault actions should support mount declarations for credentials like SSH keys (#528)' (#536) from fix/issue-528 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
31fde3d471
6 changed files with 72 additions and 4 deletions
|
|
@ -437,13 +437,40 @@ launch_runner() {
|
||||||
log "Action ${action_id} has no secrets declared — runner will execute without extra env vars"
|
log "Action ${action_id} has no secrets declared — runner will execute without extra env vars"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Add volume mounts for file-based credentials (if any declared)
|
||||||
|
local mounts_array
|
||||||
|
mounts_array="${VAULT_ACTION_MOUNTS:-}"
|
||||||
|
if [ -n "$mounts_array" ]; then
|
||||||
|
local runtime_home="${HOME:-/home/debian}"
|
||||||
|
for mount_alias in $mounts_array; do
|
||||||
|
mount_alias=$(echo "$mount_alias" | xargs)
|
||||||
|
[ -n "$mount_alias" ] || continue
|
||||||
|
case "$mount_alias" in
|
||||||
|
ssh)
|
||||||
|
cmd+=(-v "${runtime_home}/.ssh:/home/agent/.ssh:ro")
|
||||||
|
;;
|
||||||
|
gpg)
|
||||||
|
cmd+=(-v "${runtime_home}/.gnupg:/home/agent/.gnupg:ro")
|
||||||
|
;;
|
||||||
|
sops)
|
||||||
|
cmd+=(-v "${runtime_home}/.config/sops/age:/home/agent/.config/sops/age:ro")
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log "ERROR: Unknown mount alias '${mount_alias}' for action ${action_id}"
|
||||||
|
write_result "$action_id" 1 "Unknown mount alias: ${mount_alias}"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
# Mount the ops repo so the runner entrypoint can read the action TOML
|
# Mount the ops repo so the runner entrypoint can read the action TOML
|
||||||
cmd+=(-v "${OPS_REPO_ROOT}:/home/agent/ops:ro")
|
cmd+=(-v "${OPS_REPO_ROOT}:/home/agent/ops:ro")
|
||||||
|
|
||||||
# Service name and action-id argument
|
# Service name and action-id argument
|
||||||
cmd+=(runner "$action_id")
|
cmd+=(runner "$action_id")
|
||||||
|
|
||||||
log "Running: docker compose run --rm runner ${action_id} (secrets: ${secrets_array:-none})"
|
log "Running: docker compose run --rm runner ${action_id} (secrets: ${secrets_array:-none}, mounts: ${mounts_array:-none})"
|
||||||
|
|
||||||
# Create temp file for logs
|
# Create temp file for logs
|
||||||
local log_file
|
local log_file
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@ id = "${id}"
|
||||||
formula = "release"
|
formula = "release"
|
||||||
context = "Release ${version}"
|
context = "Release ${version}"
|
||||||
secrets = ["GITHUB_TOKEN", "CODEBERG_TOKEN"]
|
secrets = ["GITHUB_TOKEN", "CODEBERG_TOKEN"]
|
||||||
|
mounts = ["ssh"]
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "Created vault item: ${vault_toml}"
|
echo "Created vault item: ${vault_toml}"
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,12 @@ id = "publish-skill-20260331"
|
||||||
formula = "clawhub-publish"
|
formula = "clawhub-publish"
|
||||||
context = "SKILL.md bumped to 0.3.0"
|
context = "SKILL.md bumped to 0.3.0"
|
||||||
|
|
||||||
# Required secrets to inject
|
# Required secrets to inject (env vars)
|
||||||
secrets = ["CLAWHUB_TOKEN"]
|
secrets = ["CLAWHUB_TOKEN"]
|
||||||
|
|
||||||
|
# Optional file-based credential mounts
|
||||||
|
mounts = ["ssh"]
|
||||||
|
|
||||||
# Optional
|
# Optional
|
||||||
model = "sonnet"
|
model = "sonnet"
|
||||||
tools = ["clawhub"]
|
tools = ["clawhub"]
|
||||||
|
|
@ -39,6 +42,7 @@ blast_radius = "low" # optional: overrides policy.toml tier ("low"|"medium
|
||||||
|
|
||||||
| Field | Type | Default | Description |
|
| Field | Type | Default | Description |
|
||||||
|-------|------|---------|-------------|
|
|-------|------|---------|-------------|
|
||||||
|
| `mounts` | array of strings | `[]` | Well-known mount aliases for file-based credentials. The dispatcher maps each alias to a read-only volume flag |
|
||||||
| `model` | string | `sonnet` | Override the default Claude model for this action |
|
| `model` | string | `sonnet` | Override the default Claude model for this action |
|
||||||
| `tools` | array of strings | `[]` | MCP tools to enable during execution |
|
| `tools` | array of strings | `[]` | MCP tools to enable during execution |
|
||||||
| `timeout_minutes` | integer | `60` | Maximum execution time in minutes |
|
| `timeout_minutes` | integer | `60` | Maximum execution time in minutes |
|
||||||
|
|
@ -53,6 +57,16 @@ Common secret names:
|
||||||
- `GITHUB_TOKEN` - GitHub API token for repository operations
|
- `GITHUB_TOKEN` - GitHub API token for repository operations
|
||||||
- `DEPLOY_KEY` - Infrastructure deployment key
|
- `DEPLOY_KEY` - Infrastructure deployment key
|
||||||
|
|
||||||
|
## Mount Aliases
|
||||||
|
|
||||||
|
Mount aliases map to read-only volume flags passed to the runner container:
|
||||||
|
|
||||||
|
| Alias | Maps to |
|
||||||
|
|-------|---------|
|
||||||
|
| `ssh` | `-v ${HOME}/.ssh:/home/agent/.ssh:ro` |
|
||||||
|
| `gpg` | `-v ${HOME}/.gnupg:/home/agent/.gnupg:ro` |
|
||||||
|
| `sops` | `-v ${HOME}/.config/sops/age:/home/agent/.config/sops/age:ro` |
|
||||||
|
|
||||||
## Validation Rules
|
## Validation Rules
|
||||||
|
|
||||||
1. **Required fields**: `id`, `formula`, `context`, and `secrets` must be present
|
1. **Required fields**: `id`, `formula`, `context`, and `secrets` must be present
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@
|
||||||
# id = "release-v120"
|
# id = "release-v120"
|
||||||
# formula = "release"
|
# formula = "release"
|
||||||
# context = "Release v1.2.0"
|
# context = "Release v1.2.0"
|
||||||
# secrets = ["GITHUB_TOKEN", "CODEBERG_TOKEN"]
|
# secrets = []
|
||||||
|
# mounts = ["ssh"]
|
||||||
#
|
#
|
||||||
# Steps executed by the release formula:
|
# Steps executed by the release formula:
|
||||||
# 1. preflight - Validate prerequisites (version, FORGE_TOKEN, Docker)
|
# 1. preflight - Validate prerequisites (version, FORGE_TOKEN, Docker)
|
||||||
|
|
@ -27,6 +28,7 @@ id = "release-v120"
|
||||||
formula = "release"
|
formula = "release"
|
||||||
context = "Release v1.2.0 — includes vault redesign, .profile system, architect agent"
|
context = "Release v1.2.0 — includes vault redesign, .profile system, architect agent"
|
||||||
secrets = ["GITHUB_TOKEN", "CODEBERG_TOKEN"]
|
secrets = ["GITHUB_TOKEN", "CODEBERG_TOKEN"]
|
||||||
|
mounts = ["ssh"]
|
||||||
|
|
||||||
# Optional: specify a larger model for complex release logic
|
# Optional: specify a larger model for complex release logic
|
||||||
# model = "sonnet"
|
# model = "sonnet"
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ if validate_vault_action "$TOML_FILE"; then
|
||||||
echo " Formula: $VAULT_ACTION_FORMULA"
|
echo " Formula: $VAULT_ACTION_FORMULA"
|
||||||
echo " Context: $VAULT_ACTION_CONTEXT"
|
echo " Context: $VAULT_ACTION_CONTEXT"
|
||||||
echo " Secrets: $VAULT_ACTION_SECRETS"
|
echo " Secrets: $VAULT_ACTION_SECRETS"
|
||||||
|
echo " Mounts: ${VAULT_ACTION_MOUNTS:-none}"
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo "INVALID: $TOML_FILE" >&2
|
echo "INVALID: $TOML_FILE" >&2
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@ fi
|
||||||
# Allowed secret names - must match keys in .env.vault.enc
|
# Allowed secret names - must match keys in .env.vault.enc
|
||||||
VAULT_ALLOWED_SECRETS="CLAWHUB_TOKEN GITHUB_TOKEN CODEBERG_TOKEN DEPLOY_KEY NPM_TOKEN DOCKER_HUB_TOKEN"
|
VAULT_ALLOWED_SECRETS="CLAWHUB_TOKEN GITHUB_TOKEN CODEBERG_TOKEN DEPLOY_KEY NPM_TOKEN DOCKER_HUB_TOKEN"
|
||||||
|
|
||||||
|
# Allowed mount aliases — well-known file-based credential directories
|
||||||
|
VAULT_ALLOWED_MOUNTS="ssh gpg sops"
|
||||||
|
|
||||||
# Validate a vault action TOML file
|
# Validate a vault action TOML file
|
||||||
# Usage: validate_vault_action <path-to-toml>
|
# Usage: validate_vault_action <path-to-toml>
|
||||||
# Returns: 0 if valid, 1 if invalid
|
# Returns: 0 if valid, 1 if invalid
|
||||||
|
|
@ -69,11 +72,16 @@ validate_vault_action() {
|
||||||
secrets_line=$(echo "$toml_content" | grep -E '^secrets\s*=' | tr -d '\r')
|
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:]]*$//')
|
secrets_array=$(echo "$secrets_line" | sed -E 's/^secrets\s*=\s*\[(.*)\]/\1/' | tr -d '[]"' | tr ',' ' ' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||||
|
|
||||||
|
# Extract mounts array (optional)
|
||||||
|
local mounts_line mounts_array
|
||||||
|
mounts_line=$(echo "$toml_content" | grep -E '^mounts\s*=' | tr -d '\r') || true
|
||||||
|
mounts_array=$(echo "$mounts_line" | sed -E 's/^mounts\s*=\s*\[(.*)\]/\1/' | tr -d '[]"' | tr ',' ' ' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') || true
|
||||||
|
|
||||||
# Check for unknown fields (any top-level key not in allowed list)
|
# Check for unknown fields (any top-level key not in allowed list)
|
||||||
local unknown_fields
|
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
|
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
|
case "$field" in
|
||||||
id|formula|context|secrets|model|tools|timeout_minutes|dispatch_mode|blast_radius) ;;
|
id|formula|context|secrets|mounts|model|tools|timeout_minutes|dispatch_mode|blast_radius) ;;
|
||||||
*) echo "$field" ;;
|
*) echo "$field" ;;
|
||||||
esac
|
esac
|
||||||
done)
|
done)
|
||||||
|
|
@ -122,6 +130,19 @@ validate_vault_action() {
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Validate each mount alias is in the allowlist
|
||||||
|
if [ -n "$mounts_array" ]; then
|
||||||
|
for mount in $mounts_array; do
|
||||||
|
mount=$(echo "$mount" | tr -d '"' | xargs) # trim whitespace and quotes
|
||||||
|
if [ -n "$mount" ]; then
|
||||||
|
if ! echo " $VAULT_ALLOWED_MOUNTS " | grep -q " $mount "; then
|
||||||
|
echo "ERROR: Unknown mount alias (not in allowlist): $mount" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
# Validate optional fields if present
|
# Validate optional fields if present
|
||||||
# model
|
# model
|
||||||
if echo "$toml_content" | grep -qE '^model\s*='; then
|
if echo "$toml_content" | grep -qE '^model\s*='; then
|
||||||
|
|
@ -158,10 +179,12 @@ validate_vault_action() {
|
||||||
export VAULT_ACTION_FORMULA="$formula"
|
export VAULT_ACTION_FORMULA="$formula"
|
||||||
export VAULT_ACTION_CONTEXT="$context"
|
export VAULT_ACTION_CONTEXT="$context"
|
||||||
export VAULT_ACTION_SECRETS="$secrets_array"
|
export VAULT_ACTION_SECRETS="$secrets_array"
|
||||||
|
export VAULT_ACTION_MOUNTS="${mounts_array:-}"
|
||||||
|
|
||||||
log "VAULT_ACTION_ID=$VAULT_ACTION_ID"
|
log "VAULT_ACTION_ID=$VAULT_ACTION_ID"
|
||||||
log "VAULT_ACTION_FORMULA=$VAULT_ACTION_FORMULA"
|
log "VAULT_ACTION_FORMULA=$VAULT_ACTION_FORMULA"
|
||||||
log "VAULT_ACTION_SECRETS=$VAULT_ACTION_SECRETS"
|
log "VAULT_ACTION_SECRETS=$VAULT_ACTION_SECRETS"
|
||||||
|
log "VAULT_ACTION_MOUNTS=${VAULT_ACTION_MOUNTS:-none}"
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue