diff --git a/lib/manifest.sh b/lib/manifest.sh index 6dc496b..d232ad4 100644 --- a/lib/manifest.sh +++ b/lib/manifest.sh @@ -8,7 +8,8 @@ # "bottles": { # "": { # "env": { "": , ... }, -# "ssh": [ , ... ] +# "ssh": [ , ... ], +# "egress": { "allowlist": [ "", ... ] } # }, # ... # }, @@ -22,9 +23,17 @@ # } # } # -# A bottle groups shared infrastructure (SSH keys, known hosts) that multiple -# agents can reference by name. The "bottle" field is required on every agent; -# cli.sh start rejects agents that omit it. +# A bottle groups shared infrastructure (SSH keys, known hosts, egress +# allowlist) that multiple agents can reference by name. The "bottle" field +# is required on every agent; cli.sh start rejects agents that omit it. +# +# The "egress" object is added in PRD 0001. Today it carries one key: +# - allowlist: array of hostnames the agent is allowed to reach. The +# effective allowlist at launch is this list UNIONED with the +# baked-in defaults for Claude Code's required hosts (see +# lib/pipelock.sh). Bottles with no "egress" block use defaults +# only. Future keys (mode, dlp, data_budget, ...) are reserved +# under the same object; v1 ignores anything we don't recognize. # # An is a JSON string. Mode is selected by sentinel prefix: # "?" → prompt for the value at runtime, displaying @@ -225,6 +234,24 @@ manifest_bottle_ssh() { jq -c --arg b "$bottle_name" '.bottles[$b].ssh // [] | .[]' "$manifest_file" } +# manifest_bottle_egress_allowlist — prints one +# hostname per line on stdout for the entries in +# bottles[bottle_name].egress.allowlist. Prints nothing if the field is missing +# or the array is empty. Validates only that the field, when present, is an +# array; per-element string typing is checked at use-time in lib/pipelock.sh +# so the validation lives next to the YAML generator that consumes it. +manifest_bottle_egress_allowlist() { + local manifest_file="${1:?manifest_bottle_egress_allowlist: missing manifest file}" + local bottle_name="${2:?manifest_bottle_egress_allowlist: missing bottle name}" + local field_type + field_type="$(jq -r --arg b "$bottle_name" '.bottles[$b].egress.allowlist | type' "$manifest_file" 2>/dev/null || echo "null")" + case "$field_type" in + array|null) : ;; + *) die "bottle '${bottle_name}' egress.allowlist must be an array (was ${field_type})." ;; + esac + jq -r --arg b "$bottle_name" '.bottles[$b].egress.allowlist // [] | .[]' "$manifest_file" +} + # manifest_ssh — prints one compact JSON object per line # for each ssh entry associated with the agent. SSH entries are resolved via # the agent's "bottle" field: if set, entries come from bottles[bottle].ssh; if the