diff --git a/integrations/isolation/acq-kits/README.md b/integrations/isolation/acq-kits/README.md index 048f6765..811aa526 100644 --- a/integrations/isolation/acq-kits/README.md +++ b/integrations/isolation/acq-kits/README.md @@ -20,6 +20,7 @@ not agent behavior. (Behavioral patterns live in `skills/`, `prompts/`, etc.) |-----|---------| | [`usai-provider/`](usai-provider/) | Configure the agent to use the GSA USAi model provider (OpenCode today), with egress allow-listed. | | [`agentic-coding-playbook/`](agentic-coding-playbook/) | Clone the GSA agentic-coding-playbook at startup and link its `AGENTS.md` + skills into each agent's search paths. | +| [`cloud-gov/`](cloud-gov/) | Install the Cloud Foundry CLI and allow-list cloud.gov platform, dashboard, and docs domains, with auth supplied through acq secrets. | | [`zscaler-ca-certificate/`](zscaler-ca-certificate/) | Trust the public Zscaler Root CA in the sandbox (msb: native `--trust-host-cas`; sbx: file-drop + `update-ca-certificates`). | | [`git-ssh-sign/`](git-ssh-sign/) | Sign git commits and tags with the SSH key forwarded from the host agent (vendored from sbx-kits-contrib). | | [`openchamber/`](openchamber/) | Run OpenChamber, a browser UI for OpenCode, inside the sandbox alongside the terminal TUI. Opt-in (see its parity note). | diff --git a/integrations/isolation/acq-kits/cloud-gov/README.md b/integrations/isolation/acq-kits/cloud-gov/README.md new file mode 100644 index 00000000..ddfd645c --- /dev/null +++ b/integrations/isolation/acq-kits/cloud-gov/README.md @@ -0,0 +1,160 @@ +# cloud-gov (acq mixin kit, `hybrid/v1`) + +A neutral [`acq`](https://github.com/GSA-TTS/agentic-coding-quickstart) **mixin +kit** for working with [cloud.gov](https://cloud.gov/) from an isolated coding +sandbox. + +> **Neutral (backend-agnostic) kit.** This is the `schemaVersion: "hybrid/v1"` +> form consumed by `acq`, which abstracts the isolation backend. See +> [backend parity](#backend-parity) and +> [`../../docs/decisions/0001-neutral-hybrid-v1-acq-kits.md`](../../docs/decisions/0001-neutral-hybrid-v1-acq-kits.md). + +## What It Does + +- **Cloud Foundry CLI** - uses an existing `cf` binary when the base image already + has one. If `cf` is missing, installs Cloud Foundry CLI v8.19.0 from pinned + upstream release tarballs with committed SHA-256 checks. +- **cloud.gov egress** - allow-lists exact cloud.gov control-plane and + documentation hosts: the Cloud Foundry API, login/UAA, log-stream hosts, and + user-facing cloud.gov documentation. Public app routes and custom domains stay + project-specific instead of using wildcard egress. +- **Cloud Foundry docs egress** - allow-lists upstream Cloud Foundry CLI and docs + hosts so agents can look up command behavior and platform guidance. +- **Secret-safe authorization** - documents the `acq secret set` binding for a + host-side `cf oauth-token`. The sandbox receives only the backend placeholder + needed for direct Cloud Foundry API calls; the real token stays in the host-side + secret store / backend proxy. + +## Backend Parity + +The kit declares `sbx` and `msb` support. Both backends consume the neutral kit +fields: `caps.network.allow`, file drops, and create-time/startup commands. There +is no backend shortcut. + +The Cloud Foundry token is not a kit field. The user stores it in the acq secret +store and binds it to `api.fr.cloud.gov`; the active backend maps that to its +own proxy or secret-substitution mechanism. The kit validates that any injected +`CF_OAUTH_TOKEN` value is a backend placeholder, not raw token material. + +The network allow-list avoids wildcard semantics. Both sbx and msb receive the +same exact host list, which keeps the shared kit least-privilege and makes app +route/custom-domain access an explicit project-specific decision. + +## Usage + +Apply the kit alongside an agent sandbox: + +```bash +acq run --kit opencode /path/to/project +``` + +The kit is a `mixin`, so it composes with the other acq-kits. + +## CF CLI Installation Pin + +If the base image already has `cf`, the install step exits successfully without +network package installation. If `cf` is missing, the kit installs Cloud Foundry +CLI v8.19.0 from the upstream GitHub release tarball for `linux_x86-64` or +`linux_arm64`, then verifies the archive SHA-256 before installing it. Unsupported +architectures fail closed. + +To update the CLI, change the version and both SHA-256 constants in +`files/home/cloud-gov-install-cf-cli.sh` together after reviewing the upstream +release metadata. + +## Prerequisites + +From a host shell, log in to cloud.gov with the normal human-controlled flow: + +```bash +cf login -a https://api.fr.cloud.gov --sso +``` + +Then store the short-lived Cloud Foundry OAuth token string in acq. `cf +oauth-token` returns the full authorization value, including the `bearer` prefix; +this command passes that value over stdin to the host-side secret store. Do not +paste it into chat or write it to a file: + +```bash +cf oauth-token | acq secret set -g cloud-gov --host api.fr.cloud.gov --env CF_OAUTH_TOKEN +``` + +For a sandbox-specific token, scope the secret to that sandbox: + +```bash +cf oauth-token | acq secret set cloud-gov --host api.fr.cloud.gov --env CF_OAUTH_TOKEN +``` + +The `CF_OAUTH_TOKEN` name is a placeholder binding for backend proxy/secret +substitution. At startup the kit targets `https://api.fr.cloud.gov` and, when +that value is present, refuses to continue unless it matches a recognized +backend-placeholder prefix, including the documented msb guest-secret placeholder +prefix. The startup guard rejects bearer/JWT-like raw token material before +anything can be written to the workspace. The stock CF CLI +may parse `AccessToken` locally, so the kit does not write the placeholder into +CF CLI config or claim authenticated `cf` commands work without live +verification. + +## Network Allow-List + +The kit allow-lists: + +| Host | Purpose | +|------|---------| +| `api.fr.cloud.gov` | Cloud Foundry API | +| `uaa.fr.cloud.gov`, `login.fr.cloud.gov` | cloud.gov UAA and browser-login control plane | +| `doppler.fr.cloud.gov`, `log-cache.fr.cloud.gov` | Cloud Foundry log streaming/control-plane endpoints | +| `cloud.gov`, `www.cloud.gov`, `docs.cloud.gov` | cloud.gov public documentation | +| `docs.cloudfoundry.org`, `cli.cloudfoundry.org` | Cloud Foundry documentation | +| `github.com`, `objects.githubusercontent.com` | Pinned Cloud Foundry CLI release tarball download during kit install; if runtime access to these hosts is not acceptable for a deployment context, use a base image with `cf` preinstalled or a project-specific vetted artifact source. | + +The kit intentionally does not allow `*.cloud.gov`. If the project needs a +specific app route, route service, or custom domain, add that exact host in a +project-specific kit or sandbox policy after review. + +## Using `cf` + +Inside the sandbox, use `cf` for unauthenticated operations such as targeting the +cloud.gov API: + +```bash +cf api https://api.fr.cloud.gov +cf target +``` + +For authenticated checks, use direct HTTPS calls to the Cloud Foundry API so the +backend can rewrite the placeholder on the wire: + +```bash +curl -H "Authorization: $CF_OAUTH_TOKEN" https://api.fr.cloud.gov/v3/organizations +``` + +If authenticated API calls fail, refresh the host-side secret with `cf oauth-token +| acq secret set ...` and restart or recreate the sandbox so the placeholder is +injected. Do not paste the token into chat or commit it to the workspace. + +## Troubleshooting + +Failure-mode notes are in [`TROUBLESHOOTING.md`](TROUBLESHOOTING.md). + +## Design Decisions + +- [`docs/decisions/cloud-gov-cli-and-auth.md`](docs/decisions/cloud-gov-cli-and-auth.md) + - why this kit installs `cf`, uses exact cloud.gov egress, and relies on acq + placeholder-based auth. + +## Verifying + +Run the bundled check: + +```bash +./scripts/verify +``` + +By default it runs offline checks only: repository kit validation, shell syntax +for kit scripts, auth-script guard tests with synthetic placeholders, and exact +egress allow-list assertions. Set `RUN_ACQ=1` to create a throwaway sandbox and +verify that `cf` is installed, cloud.gov/docs endpoints are reachable, and the +injected placeholder reaches the CF API through a direct HTTPS request. +`RUN_ACQ=1` requires a global `cloud-gov` acq secret. Set `KEEP=1` with +`RUN_ACQ=1` to keep the sandbox for inspection. diff --git a/integrations/isolation/acq-kits/cloud-gov/TROUBLESHOOTING.md b/integrations/isolation/acq-kits/cloud-gov/TROUBLESHOOTING.md new file mode 100644 index 00000000..9eb048b7 --- /dev/null +++ b/integrations/isolation/acq-kits/cloud-gov/TROUBLESHOOTING.md @@ -0,0 +1,95 @@ +# Troubleshooting - cloud-gov kit + +These are failure modes specific to the cloud-gov acq mixin kit. + +## `cf` Is Not Installed + +**Symptoms:** `cf version` fails inside the sandbox. + +**Causes and fixes:** + +- **Install egress blocked.** Check the sandbox policy log and confirm the CF CLI + package hosts in `spec.yaml` are allowed. +- **Unsupported architecture.** The install script supports upstream Linux + x86_64/amd64 and arm64/aarch64 tarballs. For another architecture, install the + CF CLI in the base image or extend `files/home/cloud-gov-install-cf-cli.sh` + with a pinned artifact and SHA-256. +- **Release artifact changed.** The install script verifies the downloaded + tarball SHA-256. A mismatch fails closed; review the upstream release metadata + before changing the pinned version or hash. + +## `cf api https://api.fr.cloud.gov` Fails + +**Symptoms:** `cf api` times out or cannot resolve/reach the API host. + +**Causes and fixes:** + +- **Kit not applied.** Recreate the sandbox with this kit applied. +- **Egress blocked by org policy.** Check `acq policy log ` or the + active backend's policy log for `api.fr.cloud.gov`. +- **TLS trust issue.** If your network uses TLS inspection, compose this kit with + `zscaler-ca-certificate` or the local equivalent CA-trust kit. + +## CF Authentication Fails + +**Symptoms:** cloud.gov is reachable, but `cf` reports unauthorized, forbidden, +or no current user. + +**Causes and fixes:** + +- **Missing or expired host-side token.** Cloud Foundry OAuth tokens are + short-lived. If `scripts/verify` reports `authenticated /v3/organizations -> + 401` or `403`, refresh the acq secret from a host shell where you are logged in + to cloud.gov: + + ```bash + cf oauth-token | acq secret set -g cloud-gov --host api.fr.cloud.gov --env CF_OAUTH_TOKEN + ``` + +- **Wrong scope.** If the sandbox has a sandbox-scoped `cloud-gov` secret, update + that scoped secret instead of the global one: + + ```bash + cf oauth-token | acq secret set cloud-gov --host api.fr.cloud.gov --env CF_OAUTH_TOKEN + ``` + +- **Unexpected token value in the sandbox.** The startup script only accepts + recognized backend placeholders in `CF_OAUTH_TOKEN`, including sbx/acq forms + and the documented msb guest-secret placeholder prefix. + If a raw `bearer ...` token, a JWT-looking value, or a value with control + characters appears in the sandbox, startup fails closed before writing CF CLI + auth state. Refresh the acq secret from the host and recreate the sandbox. Do + not paste the token into chat or commit it to the workspace. +- **CF CLI wants a local login.** The kit does not write the placeholder into + `~/.cf/config.json` because stock `cf` may parse cached tokens locally before + sending a request. Use direct Cloud Foundry API calls with the placeholder auth + header for authenticated probes unless a live-tested backend path for + authenticated `cf` commands is added later. + +## A cloud.gov App Route Is Blocked + +**Symptoms:** `curl https://.app.cloud.gov` or a browser/API request to a +cloud.gov app route fails because egress is denied. + +**Causes and fixes:** + +- **Expected shared-kit limit.** The kit intentionally allow-lists exact + cloud.gov control-plane and documentation hosts only. It does not include a + `*.cloud.gov` wildcard, because wildcard semantics differ by backend and widen + egress beyond least privilege. Add the specific app route, route service, or + custom domain as a project-specific network allow-list entry after review. +- **Backend parity check.** Confirm the generated backend policy from `acq` + contains the exact host you added; both sbx and msb should receive concrete + host allow rules, not a suffix wildcard. + +## Docs Are Not Reachable + +**Symptoms:** Requests to cloud.gov or Cloud Foundry docs fail. + +**Causes and fixes:** + +- Confirm the requested host is one of `cloud.gov`, `www.cloud.gov`, + `docs.cloud.gov`, `docs.cloudfoundry.org`, `cli.cloudfoundry.org`, or an exact + cloud.gov control-plane host listed in `spec.yaml`. +- If docs pull assets from another host and the backend blocks them, add the + specific observed host after review instead of adding a broad wildcard. diff --git a/integrations/isolation/acq-kits/cloud-gov/docs/decisions/cloud-gov-cli-and-auth.md b/integrations/isolation/acq-kits/cloud-gov/docs/decisions/cloud-gov-cli-and-auth.md new file mode 100644 index 00000000..6e47e994 --- /dev/null +++ b/integrations/isolation/acq-kits/cloud-gov/docs/decisions/cloud-gov-cli-and-auth.md @@ -0,0 +1,71 @@ +# Decision: install CF CLI and use acq secret placeholder auth + +**Status:** accepted + +## Context + +cloud.gov operations use the Cloud Foundry CLI (`cf`) against the cloud.gov +Cloud Foundry API. A sandbox used for this work needs the CLI, egress to exact +cloud.gov control-plane hosts, and access to cloud.gov and Cloud Foundry +documentation. + +Authentication is sensitive. A token from `cf oauth-token` must not be committed, +printed in chat, or written into agent-readable workspace files. At the same +time, stock CF CLI auth paths may parse cached access tokens locally before +sending a request, so a non-JWT backend placeholder is not enough to prove +interactive `cf` authentication works. + +## Decision + +Create a neutral `cloud-gov` acq mixin kit for sbx and msb that: + +1. Installs `cf` at create time from pinned Cloud Foundry CLI release tarballs + with committed SHA-256 values. +2. Allow-lists exact cloud.gov control-plane/documentation hosts and Cloud + Foundry docs domains, without wildcard semantics. +3. Uses acq's custom secret binding for a host-side token captured with + `cf oauth-token`: + + ```sh + cf oauth-token | acq secret set -g cloud-gov --host api.fr.cloud.gov --env CF_OAUTH_TOKEN + ``` + +4. Validates any injected `CF_OAUTH_TOKEN` value as a recognized backend + placeholder, including the documented msb guest-secret placeholder prefix, + and refuses raw token-looking values. The placeholder can be used for direct + HTTPS Cloud Foundry API calls that the backend proxy rewrites on the way to + `api.fr.cloud.gov`. + +The kit does not store the real Cloud Foundry OAuth token in `spec.yaml`, files, +or repository docs. The token remains in the host-side acq secret store and the +active backend's secret/proxy mechanism. The kit does not write a placeholder to +`~/.cf/config.json`; authenticated `cf` command support requires a separately +live-tested path because the stock CLI may parse cached tokens locally. + +## Consequences + +- Agents can use `cf` for Cloud Foundry targeting and unauthenticated inspection. + Authenticated operations should use direct HTTPS Cloud Foundry API calls until a + live-tested path for authenticated stock `cf` commands exists. +- The real token remains outside the sandbox; the sandbox may contain only a + backend-generated placeholder. +- The install currently supports Linux x86_64/amd64 and arm64/aarch64 release + tarballs. Other architectures should either preinstall `cf` or add a pinned, + integrity-checked install path before being supported. +- Egress is exact-host only for shared-kit backend parity and least privilege. + The kit includes the Cloud Foundry API, UAA/login, log streaming, cloud.gov + documentation, Cloud Foundry documentation, and pinned CLI download hosts. + The GitHub hosts are needed only when the base image lacks `cf`; if runtime + access to those hosts is unacceptable, use a base image with `cf` preinstalled + or a project-specific vetted artifact source. Public app routes, route + services, and custom domains are intentionally not included because they are + project-specific and should be added as exact host rules after review. +- Live verification with `RUN_ACQ=1` requires a configured `cloud-gov` secret and + must exercise raw authenticated CF API access through the backend placeholder. + Offline verification remains useful for schema/shell checks and auth guard + tests, but it is not evidence that cloud.gov auth wiring works end to end. + +## Links + +- `../../README.md` - operator usage and troubleshooting entry points. +- `../../spec.yaml` - authoritative kit behavior. diff --git a/integrations/isolation/acq-kits/cloud-gov/files/home/cloud-gov-configure-cf-auth.sh b/integrations/isolation/acq-kits/cloud-gov/files/home/cloud-gov-configure-cf-auth.sh new file mode 100755 index 00000000..16c64c44 --- /dev/null +++ b/integrations/isolation/acq-kits/cloud-gov/files/home/cloud-gov-configure-cf-auth.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env sh +set -eu + +# Configure the CF CLI to use cloud.gov. If acq injected a backend placeholder in +# CF_OAUTH_TOKEN, validate that it is not raw token material. The stock CF CLI +# parses AccessToken locally, so this script deliberately does not write a +# placeholder into ~/.cf/config.json and overclaim authenticated cf CLI support. + +log() { printf '%s\n' "cloud-gov: $*" >&2; } + +mkdir -p "$HOME/.cf" +chmod 0700 "$HOME/.cf" + +if [ -n "${CF_OAUTH_TOKEN:-}" ]; then + msb_prefix='$'MSB_ + case "$CF_OAUTH_TOKEN" in + *[![:print:]]*|*[[:space:]]*) + log "CF_OAUTH_TOKEN placeholder contains an unsafe character; refusing startup" + exit 1 + ;; + esac + + case "$CF_OAUTH_TOKEN" in + bearer*|Bearer*|BEARER*) + log "CF_OAUTH_TOKEN looks like raw token material; refusing startup" + exit 1 + ;; + acq_placeholder_?*|sbx-cs-?*|"$msb_prefix"?*) + : + ;; + *.*.*) + log "CF_OAUTH_TOKEN looks like raw token material; refusing startup" + exit 1 + ;; + *) + log "CF_OAUTH_TOKEN is not a recognized backend placeholder; refusing startup" + exit 1 + ;; + esac + + log "validated CF_OAUTH_TOKEN as a backend placeholder" +else + log "no CF_OAUTH_TOKEN placeholder present; cf is targeted but unauthenticated" +fi + +if command -v cf >/dev/null 2>&1; then + env CF_OAUTH_TOKEN= cf api https://api.fr.cloud.gov >/dev/null 2>&1 || log "warning: cf api target failed; check egress/TLS" +fi diff --git a/integrations/isolation/acq-kits/cloud-gov/files/home/cloud-gov-install-cf-cli.sh b/integrations/isolation/acq-kits/cloud-gov/files/home/cloud-gov-install-cf-cli.sh new file mode 100755 index 00000000..871075b3 --- /dev/null +++ b/integrations/isolation/acq-kits/cloud-gov/files/home/cloud-gov-install-cf-cli.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env sh +set -eu + +# Install the Cloud Foundry CLI from a pinned upstream release tarball. The exact +# version and SHA-256 values are committed in this script so the install is +# reproducible and fails closed on upstream drift. + +CF_CLI_VERSION="8.19.0" +CF_CLI_SHA256_X86_64="98268ab3134bb3a1c97ffce797b4e6d35590a82e006cd098ad7a29f0a5cae7d8" +CF_CLI_SHA256_ARM64="454c29a44a51c8edc9696678403e2e40808357a397033af5a018e6ca8ee32117" + +log() { printf '%s\n' "cloud-gov: $*" >&2; } +sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | cut -d' ' -f1 + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | cut -d' ' -f1 + else + return 1 + fi +} + +if command -v cf >/dev/null 2>&1; then + log "cf CLI already installed: $(cf version 2>/dev/null || printf 'version unavailable')" + exit 0 +fi + +if ! command -v curl >/dev/null 2>&1; then + log "curl is required to fetch the pinned CF CLI release; refusing install" + exit 1 +fi +if ! command -v tar >/dev/null 2>&1; then + log "tar is required to unpack the pinned CF CLI release; refusing install" + exit 1 +fi +if ! command -v gzip >/dev/null 2>&1; then + log "gzip is required to unpack the pinned CF CLI release; refusing install" + exit 1 +fi +if ! command -v sha256sum >/dev/null 2>&1 && ! command -v shasum >/dev/null 2>&1; then + log "sha256sum or shasum is required to verify the pinned CF CLI release; refusing install" + exit 1 +fi + +arch="$(uname -m)" +case "$arch" in + x86_64|amd64) + asset="cf8-cli_${CF_CLI_VERSION}_linux_x86-64.tgz" + want_sha="$CF_CLI_SHA256_X86_64" + ;; + aarch64|arm64) + asset="cf8-cli_${CF_CLI_VERSION}_linux_arm64.tgz" + want_sha="$CF_CLI_SHA256_ARM64" + ;; + *) + log "unsupported architecture for pinned CF CLI release: $arch" + exit 1 + ;; +esac + +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT INT TERM +url="https://github.com/cloudfoundry/cli/releases/download/v${CF_CLI_VERSION}/${asset}" +curl -fsSL "$url" -o "$work/cf-cli.tgz" +got_sha="$(sha256_of "$work/cf-cli.tgz" || true)" +if [ -z "$got_sha" ] || [ "$got_sha" != "$want_sha" ]; then + log "CF CLI release SHA-256 mismatch (got ${got_sha:-none}, want $want_sha); refusing install" + exit 1 +fi + +tar -xzf "$work/cf-cli.tgz" -C "$work" +if [ -x "$work/cf8" ]; then + install -m 0755 "$work/cf8" /usr/local/bin/cf8 +elif [ -x "$work/cf" ]; then + install -m 0755 "$work/cf" /usr/local/bin/cf8 +else + log "pinned CF CLI archive did not contain cf8 or cf executable" + exit 1 +fi +ln -sf /usr/local/bin/cf8 /usr/local/bin/cf + +if command -v cf >/dev/null 2>&1; then + log "installed $(cf version 2>/dev/null || printf 'cf CLI')" +else + log "cf CLI install completed but cf is not on PATH" + exit 1 +fi diff --git a/integrations/isolation/acq-kits/cloud-gov/scripts/verify b/integrations/isolation/acq-kits/cloud-gov/scripts/verify new file mode 100755 index 00000000..d46ffbb4 --- /dev/null +++ b/integrations/isolation/acq-kits/cloud-gov/scripts/verify @@ -0,0 +1,261 @@ +#!/usr/bin/env bash +# +# verify - host-side validation for the cloud-gov acq mixin kit. +# +# By default this runs offline checks only. Set RUN_ACQ=1 to create a throwaway +# sandbox and verify the CF CLI plus exact cloud.gov/docs network reachability. + +set -uo pipefail + +KIT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +REPO_ROOT="$(cd "$KIT_DIR/../../../.." && pwd)" +SBX_NAME="cloud-gov-verify-$$" +WORK="$(mktemp -d "${TMPDIR:-/tmp}/cloud-gov-verify.XXXXXX")" +CREATE_LOG="$WORK/create.log" +KEEP="${KEEP:-0}" +RUN_ACQ="${RUN_ACQ:-0}" + +pass=0 +fail=0 +ok() { printf ' \033[32mPASS\033[0m %s\n' "$1"; pass=$((pass + 1)); } +bad() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; fail=$((fail + 1)); } +skip() { printf ' \033[33mSKIP\033[0m %s\n' "$1"; } +info() { printf '\n\033[1m%s\033[0m\n' "$1"; } + +cleanup() { + if [ "$KEEP" = "1" ]; then + printf '\nKEEP=1 set; leaving sandbox %s and %s in place.\n' "$SBX_NAME" "$WORK" >&2 + return + fi + acq rm "$SBX_NAME" >/dev/null 2>&1 || true + rm -rf "$WORK" >/dev/null 2>&1 || true +} +trap cleanup EXIT INT TERM + +in_acq() { acq exec "$SBX_NAME" -- sh -c "$1" /dev/null || true; } + +info "0. Offline preconditions" +if [ -f "$KIT_DIR/spec.yaml" ]; then + ok "kit spec.yaml present" +else + bad "spec.yaml missing" + exit 1 +fi +if [ -f "$KIT_DIR/files/home/cloud-gov-install-cf-cli.sh" ]; then + ok "install script present" +else + bad "install script missing" +fi +if [ -f "$KIT_DIR/files/home/cloud-gov-configure-cf-auth.sh" ]; then + ok "auth script present" +else + bad "auth script missing" +fi + +if command -v bash >/dev/null 2>&1; then + for script in \ + "$KIT_DIR/files/home/cloud-gov-install-cf-cli.sh" \ + "$KIT_DIR/files/home/cloud-gov-configure-cf-auth.sh"; do + if bash -n "$script"; then + ok "$(basename "$script") parses" + else + bad "$(basename "$script") has a shell syntax error" + fi + done +else + skip "bash not on PATH - skipped shell parse checks" +fi + +info "1. Auth placeholder guard" +AUTH_SCRIPT="$KIT_DIR/files/home/cloud-gov-configure-cf-auth.sh" +AUTH_PATH="$(mktemp -d "${TMPDIR:-/tmp}/cloud-gov-auth-path.XXXXXX")" +for bin in sh mkdir chmod; do + ln -s "$(command -v "$bin")" "$AUTH_PATH/$bin" +done + +AUTH_WORK="$(mktemp -d "${TMPDIR:-/tmp}/cloud-gov-auth.XXXXXX")" +if HOME="$AUTH_WORK" PATH="$AUTH_PATH" CF_OAUTH_TOKEN="acq_placeholder_abcdefghijklmnopqrstuvwxyz012345" sh "$AUTH_SCRIPT" >/dev/null 2>&1; then + ok "auth script accepts acq placeholder" +else + bad "auth script rejected acq placeholder" +fi +rm -rf "$AUTH_WORK" >/dev/null 2>&1 || true + +AUTH_WORK="$(mktemp -d "${TMPDIR:-/tmp}/cloud-gov-auth.XXXXXX")" +MSB_PLACEHOLDER='$'MSB_SECRET_PLACEHOLDER +if HOME="$AUTH_WORK" PATH="$AUTH_PATH" CF_OAUTH_TOKEN="$MSB_PLACEHOLDER" sh "$AUTH_SCRIPT" >/dev/null 2>&1; then + ok "auth script accepts msb guest secret placeholder" +else + bad "auth script rejected msb guest secret placeholder" +fi +rm -rf "$AUTH_WORK" >/dev/null 2>&1 || true + +AUTH_WORK="$(mktemp -d "${TMPDIR:-/tmp}/cloud-gov-auth.XXXXXX")" +if HOME="$AUTH_WORK" PATH="$AUTH_PATH" CF_OAUTH_TOKEN="bearer TOKEN_HEADER.TOKEN_BODY.TOKEN_SIGNATURE" sh "$AUTH_SCRIPT" >/dev/null 2>&1; then + bad "auth script accepted bearer/JWT token material" +else + ok "auth script rejects bearer/JWT token material" +fi +rm -rf "$AUTH_WORK" >/dev/null 2>&1 || true + +AUTH_WORK="$(mktemp -d "${TMPDIR:-/tmp}/cloud-gov-auth.XXXXXX")" +if HOME="$AUTH_WORK" PATH="$AUTH_PATH" CF_OAUTH_TOKEN="TOKEN_HEADER.TOKEN_BODY.TOKEN_SIGNATURE" sh "$AUTH_SCRIPT" >/dev/null 2>&1; then + bad "auth script accepted JWT-like token material" +else + ok "auth script rejects JWT-like token material" +fi +rm -rf "$AUTH_WORK" "$AUTH_PATH" >/dev/null 2>&1 || true + +info "2. Network allow-list assumptions" +if grep -q '\*' "$KIT_DIR/spec.yaml"; then + bad "spec.yaml contains wildcard egress" +else + ok "spec.yaml avoids wildcard egress" +fi + +EXPECTED_HOSTS=$(mktemp "${TMPDIR:-/tmp}/cloud-gov-expected-hosts.XXXXXX") +ACTUAL_HOSTS=$(mktemp "${TMPDIR:-/tmp}/cloud-gov-actual-hosts.XXXXXX") +cat >"$EXPECTED_HOSTS" <<'HOSTS' +api.fr.cloud.gov:443 +uaa.fr.cloud.gov:443 +login.fr.cloud.gov:443 +doppler.fr.cloud.gov:443 +log-cache.fr.cloud.gov:443 +cloud.gov:443 +www.cloud.gov:443 +docs.cloud.gov:443 +docs.cloudfoundry.org:443 +cli.cloudfoundry.org:443 +github.com:443 +objects.githubusercontent.com:443 +HOSTS +python3 - "$KIT_DIR/spec.yaml" >"$ACTUAL_HOSTS" <<'PY' +import sys +from pathlib import Path + +in_allow = False +for line in Path(sys.argv[1]).read_text().splitlines(): + if line == " allow:": + in_allow = True + continue + if in_allow and line.startswith(" - "): + print(line.split("- ", 1)[1].strip().strip('"')) + continue + if in_allow and line and not line.startswith(" "): + break +PY +while IFS= read -r host; do + if grep -qx -- "$host" "$ACTUAL_HOSTS"; then + ok "spec.yaml allows $host" + else + bad "spec.yaml missing $host" + fi +done <"$EXPECTED_HOSTS" +while IFS= read -r host; do + if grep -qx -- "$host" "$EXPECTED_HOSTS"; then + : + else + bad "spec.yaml has unexpected egress host $host" + fi +done <"$ACTUAL_HOSTS" +rm -f "$EXPECTED_HOSTS" "$ACTUAL_HOSTS" + +info "3. Validate the neutral kit spec" +VALIDATOR="$REPO_ROOT/integrations/isolation/acq-kits/validate-kits.py" +if ! command -v python3 >/dev/null 2>&1; then + skip "validate-kits.py not run (python3 not on PATH)" +elif [ ! -f "$VALIDATOR" ]; then + skip "validate-kits.py not found at $VALIDATOR" +elif ! python3 -c 'import jsonschema, yaml' >/dev/null 2>&1; then + skip "validate-kits.py needs jsonschema + pyyaml" +else + if val_out="$(python3 "$VALIDATOR" --root "$REPO_ROOT" 2>&1)"; then + ok "acq-kits validate-kits.py passes" + else + bad "validate-kits.py failed:" + while IFS= read -r line; do + printf ' | %s\n' "$line" >&2 + done </dev/null 2>&1; then + bad "acq not on PATH" + exit 1 +fi +ok "acq on PATH" + +if acq secret has -g cloud-gov >/dev/null 2>&1; then + ok "global cloud-gov secret is set" +else + bad "RUN_ACQ=1 requires a global cloud-gov secret for authenticated verification" + printf ' Set it from a host shell with: cf oauth-token | acq secret set -g cloud-gov --host api.fr.cloud.gov --env CF_OAUTH_TOKEN\n' >&2 + exit 1 +fi + +info "5. Create sandbox with the kit" +printf ' creating %s (workspace %s)...\n' "$SBX_NAME" "$WORK" +if acq create --name "$SBX_NAME" --kit "$KIT_DIR" opencode "$WORK" >"$CREATE_LOG" 2>&1; then + ok "sandbox created with cloud-gov kit" +else + bad "acq create failed" + sed 's/^/ | /' "$CREATE_LOG" >&2 + exit 1 +fi + +info "6. CF CLI installed" +cf_version="$(in_acq 'cf version')" +printf ' %s\n' "${cf_version:-}" +case "$cf_version" in + *"cf version"*) ok "cf CLI reports a version" ;; + *) bad "cf CLI did not report a version" ;; +esac + +info "7. cloud.gov API and docs reachable" +for url in \ + https://api.fr.cloud.gov/v3/info \ + https://uaa.fr.cloud.gov/ \ + https://login.fr.cloud.gov/ \ + https://doppler.fr.cloud.gov/ \ + https://log-cache.fr.cloud.gov/ \ + https://cloud.gov/ \ + https://www.cloud.gov/ \ + https://docs.cloud.gov/ \ + https://docs.cloudfoundry.org/ \ + https://cli.cloudfoundry.org/; do + status="$(in_acq "curl -sS -o /dev/null -w '%{http_code}' '$url'")" + printf ' %s -> %s\n' "$url" "${status:-}" + case "$status" in + 2*|3*|401|403) ok "$url reachable" ;; + *) bad "$url not reachable (status ${status:-none})" ;; + esac +done + +info "8. Authenticated CF API wiring" +auth_status="$(in_acq "curl -sS -o /dev/null -w '%{http_code}' -H \"Authorization: \$CF_OAUTH_TOKEN\" https://api.fr.cloud.gov/v3/organizations")" +printf ' authenticated /v3/organizations -> %s\n' "${auth_status:-}" +case "$auth_status" in + 2*|3*) ok "authenticated CF API probe succeeded" ;; + 401|403) + bad "authenticated CF API probe rejected credentials (status $auth_status)" + printf ' Refresh the short-lived token, then retry: cf oauth-token | acq secret set -g cloud-gov --host api.fr.cloud.gov --env CF_OAUTH_TOKEN\n' >&2 + ;; + *) bad "authenticated CF API probe failed (status ${auth_status:-none})" ;; +esac + +info "Summary" +printf ' %d passed, %d failed\n' "$pass" "$fail" +[ "$fail" -eq 0 ] && printf ' \033[32mAll checks passed.\033[0m\n' || printf ' \033[31mSome checks failed - see notes above.\033[0m\n' + +[ "$fail" -eq 0 ] diff --git a/integrations/isolation/acq-kits/cloud-gov/spec.yaml b/integrations/isolation/acq-kits/cloud-gov/spec.yaml new file mode 100644 index 00000000..6d6ad9c6 --- /dev/null +++ b/integrations/isolation/acq-kits/cloud-gov/spec.yaml @@ -0,0 +1,90 @@ +# spec.yaml - cloud-gov (a neutral acq mixin kit, hybrid/v1) +# +# Configures a sandbox for cloud.gov work: +# - install/verify the Cloud Foundry CLI (`cf`) at create time; +# - allow-list exact cloud.gov control-plane and documentation hosts; +# - document the acq secret binding needed for CF OAuth token injection without +# placing the real token in files readable by the agent. +# +# AUTH MODEL: the kit does NOT store or expose a Cloud Foundry token. Users set a +# custom acq secret from `cf oauth-token` and bind it to api.fr.cloud.gov. acq's +# active backend injects a placeholder in CF_OAUTH_TOKEN and swaps it on the +# egress path for requests to the CF API host; the raw token stays in the +# host-side secret store / backend proxy. +schemaVersion: "hybrid/v1" +kind: mixin +name: cloud-gov +displayName: cloud.gov Cloud Foundry CLI +description: > + Installs the Cloud Foundry CLI and allow-lists exact cloud.gov control-plane + and documentation hosts so agents can inspect cloud.gov metadata and call the + Cloud Foundry API through proxied egress. Cloud Foundry authorization is + supplied through acq's custom secret mechanism, using a token captured from `cf + oauth-token`, so credentials are not stored in the kit or exposed as real token + material in the sandbox environment. + +caps: + network: + allow: + # Exact cloud.gov control-plane and documentation hosts. Avoid wildcard + # semantics so sbx and msb translate the same least-privilege egress shape. + - api.fr.cloud.gov:443 + - uaa.fr.cloud.gov:443 + - login.fr.cloud.gov:443 + - doppler.fr.cloud.gov:443 + - log-cache.fr.cloud.gov:443 + - cloud.gov:443 + - www.cloud.gov:443 + - docs.cloud.gov:443 + # Upstream Cloud Foundry documentation. + - docs.cloudfoundry.org:443 + - cli.cloudfoundry.org:443 + # Pinned CF CLI release tarballs used by cloud-gov-install-cf-cli.sh. + - github.com:443 + - objects.githubusercontent.com:443 + +files: + - path: /home/agent/cloud-gov-install-cf-cli.sh + mode: "0755" + source: files/home/cloud-gov-install-cf-cli.sh + description: Install or verify the Cloud Foundry CLI at sandbox create time + - path: /home/agent/cloud-gov-configure-cf-auth.sh + mode: "0755" + source: files/home/cloud-gov-configure-cf-auth.sh + description: Target cloud.gov and validate any injected token placeholder + +commands: + - phase: install + user: "0" + description: Install the Cloud Foundry CLI if it is missing + command: + - sh + - /home/agent/cloud-gov-install-cf-cli.sh + - phase: startup + user: "1000" + description: Target cloud.gov and validate the injected placeholder + command: + - sh + - /home/agent/cloud-gov-configure-cf-auth.sh + +agentContext: | + ## cloud.gov + + This sandbox has the Cloud Foundry CLI (`cf`) available for cloud.gov work and + egress allow-listed for exact cloud.gov API, login/UAA, log streaming, and + cloud.gov / Cloud Foundry documentation hosts. Public app routes and custom + domains require project-specific exact host allow-list entries. + + Authorization is handled through acq's secret injection path. The sandbox may + contain a backend placeholder in `CF_OAUTH_TOKEN`, but not the real token. The + user should set the secret from a host shell where they are already logged in to + cloud.gov: + + ``` + cf oauth-token | acq secret set -g cloud-gov --host api.fr.cloud.gov --env CF_OAUTH_TOKEN + ``` + + Do not ask the user to paste the token into chat or write it to the workspace. + Use `cf` for unauthenticated targeting and direct HTTPS CF API calls for + placeholder-backed authenticated checks; real token material remains outside + agent-readable files. diff --git a/integrations/isolation/acq-kits/kits.yaml b/integrations/isolation/acq-kits/kits.yaml index bab6eb7e..86964055 100644 --- a/integrations/isolation/acq-kits/kits.yaml +++ b/integrations/isolation/acq-kits/kits.yaml @@ -27,6 +27,17 @@ kits: the PLAYBOOK_SHA integrity gate. No backend shortcut. The GitHub token (while the playbook repo is private) is injected per-backend by acq. + cloud-gov: + backends: [sbx, msb] + parity: | + Both backends consume the neutral files, create-time install, startup auth + placeholder validation, and exact cloud.gov/control-plane/docs allow-list. + The shared kit avoids wildcard egress so sbx and msb receive concrete host + allow rules. Cloud Foundry authorization is supplied through acq's custom + secret binding for api.fr.cloud.gov, seeded from a host-side `cf + oauth-token`; the token is not stored in the kit or exposed as a readable + credential file. + zscaler-ca-certificate: backends: [sbx, msb] parity: |