diff --git a/docker/edge/dispatcher.sh b/docker/edge/dispatcher.sh index da33c7a..cd10ff2 100755 --- a/docker/edge/dispatcher.sh +++ b/docker/edge/dispatcher.sh @@ -437,13 +437,40 @@ launch_runner() { log "Action ${action_id} has no secrets declared — runner will execute without extra env vars" 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 cmd+=(-v "${OPS_REPO_ROOT}:/home/agent/ops:ro") # Service name and action-id argument 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 local log_file diff --git a/lib/release.sh b/lib/release.sh index 1f993ec..8217f5e 100644 --- a/lib/release.sh +++ b/lib/release.sh @@ -96,7 +96,8 @@ disinto_release() { id = "${id}" formula = "release" context = "Release ${version}" -secrets = ["GITHUB_TOKEN", "CODEBERG_TOKEN"] +secrets = [] +mounts = ["ssh"] EOF echo "Created vault item: ${vault_toml}" diff --git a/vault/SCHEMA.md b/vault/SCHEMA.md index cb7bc00..adab177 100644 --- a/vault/SCHEMA.md +++ b/vault/SCHEMA.md @@ -14,9 +14,12 @@ id = "publish-skill-20260331" formula = "clawhub-publish" context = "SKILL.md bumped to 0.3.0" -# Required secrets to inject +# Required secrets to inject (env vars) secrets = ["CLAWHUB_TOKEN"] +# Optional file-based credential mounts +mounts = ["ssh"] + # Optional model = "sonnet" tools = ["clawhub"] @@ -39,6 +42,7 @@ blast_radius = "low" # optional: overrides policy.toml tier ("low"|"medium | 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 | | `tools` | array of strings | `[]` | MCP tools to enable during execution | | `timeout_minutes` | integer | `60` | Maximum execution time in minutes | @@ -53,6 +57,16 @@ Common secret names: - `GITHUB_TOKEN` - GitHub API token for repository operations - `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 1. **Required fields**: `id`, `formula`, `context`, and `secrets` must be present diff --git a/vault/examples/release.toml b/vault/examples/release.toml index 0f1ce66..8e3952f 100644 --- a/vault/examples/release.toml +++ b/vault/examples/release.toml @@ -12,7 +12,8 @@ # id = "release-v120" # formula = "release" # context = "Release v1.2.0" -# secrets = ["GITHUB_TOKEN", "CODEBERG_TOKEN"] +# secrets = [] +# mounts = ["ssh"] # # Steps executed by the release formula: # 1. preflight - Validate prerequisites (version, FORGE_TOKEN, Docker) @@ -26,7 +27,8 @@ id = "release-v120" formula = "release" context = "Release v1.2.0 — includes vault redesign, .profile system, architect agent" -secrets = ["GITHUB_TOKEN", "CODEBERG_TOKEN"] +secrets = [] +mounts = ["ssh"] # Optional: specify a larger model for complex release logic # model = "sonnet" diff --git a/vault/validate.sh b/vault/validate.sh index f01ea63..6c0bf43 100755 --- a/vault/validate.sh +++ b/vault/validate.sh @@ -39,6 +39,7 @@ if validate_vault_action "$TOML_FILE"; then echo " Formula: $VAULT_ACTION_FORMULA" echo " Context: $VAULT_ACTION_CONTEXT" echo " Secrets: $VAULT_ACTION_SECRETS" + echo " Mounts: ${VAULT_ACTION_MOUNTS:-none}" exit 0 else echo "INVALID: $TOML_FILE" >&2 diff --git a/vault/vault-env.sh b/vault/vault-env.sh index d9a17db..4234774 100644 --- a/vault/vault-env.sh +++ b/vault/vault-env.sh @@ -31,6 +31,9 @@ fi # 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" +# Allowed mount aliases — well-known file-based credential directories +VAULT_ALLOWED_MOUNTS="ssh gpg sops" + # Validate a vault action TOML file # Usage: validate_vault_action # 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_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) 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|dispatch_mode|blast_radius) ;; + id|formula|context|secrets|mounts|model|tools|timeout_minutes|dispatch_mode|blast_radius) ;; *) echo "$field" ;; esac done) @@ -122,6 +130,19 @@ validate_vault_action() { fi 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 # model if echo "$toml_content" | grep -qE '^model\s*='; then @@ -158,10 +179,12 @@ validate_vault_action() { export VAULT_ACTION_FORMULA="$formula" export VAULT_ACTION_CONTEXT="$context" export VAULT_ACTION_SECRETS="$secrets_array" + export VAULT_ACTION_MOUNTS="${mounts_array:-}" log "VAULT_ACTION_ID=$VAULT_ACTION_ID" log "VAULT_ACTION_FORMULA=$VAULT_ACTION_FORMULA" log "VAULT_ACTION_SECRETS=$VAULT_ACTION_SECRETS" + log "VAULT_ACTION_MOUNTS=${VAULT_ACTION_MOUNTS:-none}" return 0 }