Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 107 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,110 @@ jobs:
--type spdxjson \
ghcr.io/${{ github.repository }}/bounded-query-broker@${{ steps.build_bounded_query_broker.outputs.digest }}

# Build the minimal bounded-agent enclave and its trusted broker from separate
# Dockerfile targets. The build context is ./containers (not
# ./containers/bounded-agent) because the broker reuses the shared
# bounded-execution foundation and sandbox seccomp profile that live under
# containers/bounded-query.
build-bounded-agent:
name: Build Bounded Agent Image
runs-on: ubuntu-latest
needs: bump-version
outputs:
enclave_digest: ${{ steps.build_bounded_agent.outputs.digest }}
broker_digest: ${{ steps.build_bounded_agent_broker.outputs.digest }}
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4
with:
ref: ${{ needs.bump-version.outputs.version }}

- name: Log in to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3

- name: Set up QEMU
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
with:
platforms: arm64

- name: Install cosign
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0

- name: Build and push Bounded Agent image
id: build_bounded_agent
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
with:
context: ./containers
file: ./containers/bounded-agent/Dockerfile
target: enclave
push: true
platforms: linux/amd64,linux/arm64
tags: |
ghcr.io/${{ github.repository }}/bounded-agent:${{ needs.bump-version.outputs.version_number }}
ghcr.io/${{ github.repository }}/bounded-agent:latest
cache-from: type=gha,scope=bounded-agent
cache-to: type=gha,mode=max,scope=bounded-agent

- name: Sign Bounded Agent image with cosign
run: |
cosign sign --yes \
ghcr.io/${{ github.repository }}/bounded-agent@${{ steps.build_bounded_agent.outputs.digest }}

- name: Generate SBOM for Bounded Agent image
uses: anchore/sbom-action@28d71544de8eaf1b958d335707167c5f783590ad # v0.22.2
with:
image: ghcr.io/${{ github.repository }}/bounded-agent@${{ steps.build_bounded_agent.outputs.digest }}
format: spdx-json
output-file: bounded-agent-sbom.spdx.json

- name: Attest SBOM for Bounded Agent image
run: |
cosign attest --yes \
--predicate bounded-agent-sbom.spdx.json \
--type spdxjson \
ghcr.io/${{ github.repository }}/bounded-agent@${{ steps.build_bounded_agent.outputs.digest }}

- name: Build and push Bounded Agent Broker image
id: build_bounded_agent_broker
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
with:
context: ./containers
file: ./containers/bounded-agent/Dockerfile
target: broker
push: true
platforms: linux/amd64,linux/arm64
tags: |
ghcr.io/${{ github.repository }}/bounded-agent-broker:${{ needs.bump-version.outputs.version_number }}
ghcr.io/${{ github.repository }}/bounded-agent-broker:latest
cache-from: type=gha,scope=bounded-agent-broker
cache-to: type=gha,mode=max,scope=bounded-agent-broker

- name: Sign Bounded Agent Broker image with cosign
run: |
cosign sign --yes \
ghcr.io/${{ github.repository }}/bounded-agent-broker@${{ steps.build_bounded_agent_broker.outputs.digest }}

- name: Generate SBOM for Bounded Agent Broker image
uses: anchore/sbom-action@28d71544de8eaf1b958d335707167c5f783590ad # v0.22.2
with:
image: ghcr.io/${{ github.repository }}/bounded-agent-broker@${{ steps.build_bounded_agent_broker.outputs.digest }}
format: spdx-json
output-file: bounded-agent-broker-sbom.spdx.json

- name: Attest SBOM for Bounded Agent Broker image
run: |
cosign attest --yes \
--predicate bounded-agent-broker-sbom.spdx.json \
--type spdxjson \
ghcr.io/${{ github.repository }}/bounded-agent-broker@${{ steps.build_bounded_agent_broker.outputs.digest }}

# Build agent-act image with catthehacker/ubuntu:act-24.04 base for GitHub Actions parity
# amd64-only: catthehacker/ubuntu:act-24.04 does not publish arm64 manifests
build-agent-act:
Expand Down Expand Up @@ -684,7 +788,7 @@ jobs:
release:
name: Create Release
runs-on: ubuntu-latest
needs: [bump-version, build-squid, build-agent, build-api-proxy, build-cli-proxy, build-agent-act, build-build-tools, build-bounded-query, build-gh-aw-node]
needs: [bump-version, build-squid, build-agent, build-api-proxy, build-cli-proxy, build-agent-act, build-build-tools, build-bounded-query, build-bounded-agent, build-gh-aw-node]
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4
Expand Down Expand Up @@ -786,6 +890,8 @@ jobs:
"ghcr.io/${{ github.repository }}/cli-proxy@${{ needs['build-cli-proxy'].outputs.digest }}" \
"ghcr.io/${{ github.repository }}/bounded-query@${{ needs['build-bounded-query'].outputs.query_digest }}" \
"ghcr.io/${{ github.repository }}/bounded-query-broker@${{ needs['build-bounded-query'].outputs.broker_digest }}" \
"ghcr.io/${{ github.repository }}/bounded-agent@${{ needs['build-bounded-agent'].outputs.enclave_digest }}" \
"ghcr.io/${{ github.repository }}/bounded-agent-broker@${{ needs['build-bounded-agent'].outputs.broker_digest }}" \
"ghcr.io/${{ github.repository }}/gh-aw-node@${{ needs['build-gh-aw-node'].outputs.digest }}" \
> release/containers.txt
echo "Generated containers.txt:"
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ release/
# AWF skip-cache files (generated by gh-aw tooling)
paper-data/.skip-cache.json
paper-data/.gh-aw-skip-cache.json

# Python bytecode from local enclave-bootstrap syntax checks
__pycache__/
16 changes: 16 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ The system is orchestrated by `src/cli.ts` and managed by `src/docker-manager.ts
- Trusted host staging (`src/bounded-query/staging.ts`) materializes an immutable seed per configured repo *before* the agent starts, using `GH_TOKEN`/`GITHUB_TOKEN` only in a child-process env — never in argv, a URL, a log, or the compose file
- See [docs/awf-config-spec.md](docs/awf-config-spec.md) §14 for the full model, including per-repository information-budget accounting and residual channels

**5. Bounded-Agent Enclave (optional)** — `containers/bounded-agent/`, API-proxy-only network
- Enabled via `boundedAgents.enabled` in the AWF config file (config-only; there is no CLI flag family)
- Agentic sibling of bounded queries: a trusted broker runs a **fixed, AWF-authored model loop** in a single-use enclave instead of an agent-authored script
- Requires `--enable-api-proxy` plus a configured `profile`/`model` route (`openai` or `anthropic`); preflight fails closed otherwise
- Broker (`awf-bounded-agent-broker`) is `network_mode: none` and never joins the enclave network; it receives the Docker socket only to launch enclaves
- Enclaves join **only** the dedicated `internal` `awf-bounded-agent` network (172.31.0.0/24). A dedicated API-proxy instance with private telemetry joins it at a fixed address/alias and is the enclave's only upstream egress. No primary agent, Squid, general proxy, broker, safe outputs, MCP gateway, or CLI proxy is on that network.
- Enclaves run `--read-only` with the immutable seed bind-mounted `ro`, bounded tmpfs for work/result/`/tmp`, fixed non-root UID/GID, `--cap-drop ALL`, `no-new-privileges`, seccomp, and memory/CPU/PID/file-size/timeout bounds; every container is labelled `awf.bounded-agent.run=<runId>` for deterministic orphan cleanup
- Separate private root (`/var/tmp/awf-bounded-agent-private-*`) and a **separate ledger** from bounded queries
- Agent surface: `bounded-agent` command at `/tmp/awf-lib/bounded-agent` plus a generated read-only `SKILL.md`; accepts only `--repo`, `--schema`, and task text on stdin
- Image build context is `containers/` (not `containers/bounded-agent/`) because the broker reuses the shared PR1 `bounded-execution` foundation and sandbox seccomp profile under `containers/bounded-query/`
- `runtime: "sbx"` is schema-accepted but fails closed with a not-yet-implemented capability error; `gvisor` requires an exactly registered `runsc` and never downgrades
- **Provider disclosure caveat:** repository-derived content reaches the configured model provider through the API proxy. The ledger bounds what the *calling agent* learns, not what the *provider* sees.
- See [docs/bounded-agents.md](docs/bounded-agents.md) and [docs/awf-config-spec.md](docs/awf-config-spec.md) §15

### Documentation Files

- **[README.md](README.md)** - Main project documentation and usage guide
Expand All @@ -47,6 +61,8 @@ The system is orchestrated by `src/cli.ts` and managed by `src/docker-manager.ts
- **[docs/logging_quickref.md](docs/logging_quickref.md)** - Quick reference for log queries and monitoring
- **[docs/releasing.md](docs/releasing.md)** - Release process and versioning instructions
- **[docs/INTEGRATION-TESTS.md](docs/INTEGRATION-TESTS.md)** - Integration test coverage guide with gap analysis
- **[docs/bounded-queries.md](docs/bounded-queries.md)** - Bounded-query (no-network script sandbox) guide
- **[docs/bounded-agents.md](docs/bounded-agents.md)** - Bounded-agent (API-proxy-only enclave) guide and threat model

## Development Workflow

Expand Down
6 changes: 4 additions & 2 deletions containers/agent/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -273,15 +273,17 @@ RUN if ! getent group awfuser >/dev/null 2>&1; then \

# Copy iptables setup script, PID logger, API proxy health check, Claude key helper,
# gh CLI proxy wrapper (used when --enable-cli-proxy is active), and the
# bounded-query wrapper (installed as `bounded-query` when bounded queries are enabled)
# bounded-query wrapper (installed as `bounded-query` when bounded queries are enabled),
# and the bounded-agent wrapper (installed as `bounded-agent` when bounded agents are enabled)
COPY setup-iptables.sh /usr/local/bin/setup-iptables.sh
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
COPY pid-logger.sh /usr/local/bin/pid-logger.sh
COPY api-proxy-health-check.sh /usr/local/bin/api-proxy-health-check.sh
COPY get-claude-key.sh /usr/local/bin/get-claude-key.sh
COPY gh-cli-proxy-wrapper.sh /usr/local/bin/gh-cli-proxy-wrapper.sh
COPY bounded-query-wrapper.sh /usr/local/bin/bounded-query-wrapper.sh
RUN chmod +x /usr/local/bin/setup-iptables.sh /usr/local/bin/entrypoint.sh /usr/local/bin/pid-logger.sh /usr/local/bin/api-proxy-health-check.sh /usr/local/bin/get-claude-key.sh /usr/local/bin/gh-cli-proxy-wrapper.sh /usr/local/bin/bounded-query-wrapper.sh
COPY bounded-agent-wrapper.sh /usr/local/bin/bounded-agent-wrapper.sh
RUN chmod +x /usr/local/bin/setup-iptables.sh /usr/local/bin/entrypoint.sh /usr/local/bin/pid-logger.sh /usr/local/bin/api-proxy-health-check.sh /usr/local/bin/get-claude-key.sh /usr/local/bin/gh-cli-proxy-wrapper.sh /usr/local/bin/bounded-query-wrapper.sh /usr/local/bin/bounded-agent-wrapper.sh

# Copy pre-built one-shot-token library from rust-builder stage
# This prevents tokens from being read multiple times (e.g., by malicious code)
Expand Down
121 changes: 121 additions & 0 deletions containers/agent/bounded-agent-wrapper.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/bin/sh
# /usr/local/bin/bounded-agent
#
# Agent-facing bounded-agent CLI (protocol v1).
#
# Forwards a *narrow* request to the trusted bounded-agent broker over the
# Compose Unix socket. Like bounded-query-wrapper.sh, the API is deliberately
# far narrower than a general tool: this wrapper cannot express a command, an
# image, an executable, a path, a URL, a ref, a mount, an environment variable,
# an endpoint, a network, a proxy, a credential, a runtime, a timeout, a
# resource limit, a model, a provider, a system prompt, or a tool definition.
# It accepts exactly:
#
# --repo owner/repo (exactly once)
# --schema '<json>' (exactly once; a finite response schema, see
# src/bounded-execution/finite-disclosure.ts)
# the bounded task text on stdin
#
# Output contract: exactly one line of canonical JSON on stdout, nothing on
# stderr, and exit status 0 — for every outcome and for every failure.
# Transport, framing, and validation failures all produce the same local
# {"status":"error"} so the agent cannot distinguish them by exit status. The
# remaining information budget is never disclosed.
#
# Dependencies: curl, base64 (both already required/available in the agent
# image).

CANONICAL_ERROR='{"status":"error"}'
SOCKET="${AWF_BOUNDED_AGENT_SOCKET:-}"
PROTOCOL_VERSION=1
# Keep in sync with MAX_SCHEMA_BYTES in src/bounded-execution/finite-disclosure.ts
# and containers/bounded-query/bounded-execution/finite-disclosure.js.
MAX_SCHEMA_BYTES=4096

emit_error() {
printf '%s\n' "$CANONICAL_ERROR"
exit 0
}

REPO=""
SCHEMA=""
HAVE_REPO=0
HAVE_SCHEMA=0

while [ $# -gt 0 ]; do
case "$1" in
--repo)
[ $# -ge 2 ] || emit_error
[ "$HAVE_REPO" -eq 0 ] || emit_error
REPO="$2"
HAVE_REPO=1
shift 2
;;
--schema)
[ $# -ge 2 ] || emit_error
[ "$HAVE_SCHEMA" -eq 0 ] || emit_error
SCHEMA="$2"
HAVE_SCHEMA=1
shift 2
;;
*)
# Any other flag, any `--flag=value` form, and any positional argument
# is an unsupported control.
emit_error
;;
esac
done

[ "$HAVE_REPO" -eq 1 ] || emit_error
[ "$HAVE_SCHEMA" -eq 1 ] || emit_error

printf '%s' "$REPO" | LC_ALL=C grep -Eq '^[A-Za-z0-9][A-Za-z0-9-]{0,38}/[A-Za-z0-9._-]{1,100}$' || emit_error
case "$REPO" in
*..* ) emit_error ;;
esac

[ -n "$SCHEMA" ] || emit_error
[ "$(printf '%s' "$SCHEMA" | wc -c)" -le "$MAX_SCHEMA_BYTES" ] || emit_error

# base64url, no padding: standard base64 with `+/` -> `-_`, `=` stripped, and
# newlines removed (wrapping width varies across base64 implementations).
SCHEMA_B64=$(printf '%s' "$SCHEMA" | base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=') || emit_error

# The task must arrive on stdin; an interactive terminal means no task.
[ ! -t 0 ] || emit_error

[ -n "$SOCKET" ] || emit_error
[ -S "$SOCKET" ] || emit_error

RESPONSE=$(
curl --silent --show-error \
--noproxy '*' \
--unix-socket "$SOCKET" \
--max-time 660 \
-X POST \
-H "Expect:" \
-H "Content-Type: application/octet-stream" \
-H "X-AWF-Agent-Version: ${PROTOCOL_VERSION}" \
-H "X-AWF-Repo: ${REPO}" \
-H "X-AWF-Schema-B64: ${SCHEMA_B64}" \
--data-binary @- \
"http://localhost/query" 2>/dev/null
) || emit_error

# Pass the broker's canonical response through unmodified, but only if it has
# one of the two shapes the protocol ever produces. Anything else (a dead or
# misbehaving broker, a transport-level fragment) is treated as a failure
# rather than forwarded verbatim.
case "$RESPONSE" in
'{"status":"error"}')
printf '%s\n' "$RESPONSE"
exit 0
;;
'{"status":"ok","result":'*'}')
printf '%s\n' "$RESPONSE"
exit 0
;;
*)
emit_error
;;
esac
6 changes: 3 additions & 3 deletions containers/agent/bounded-query-wrapper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
#
# --repo owner/repo (exactly once)
# --schema '<json>' (exactly once; a finite response schema, see
# src/bounded-query/protocol.ts)
# src/bounded-execution/finite-disclosure.ts)
# the query script on stdin
#
# Output contract: exactly one line of canonical JSON on stdout, nothing on
Expand All @@ -34,8 +34,8 @@ SOCKET="${AWF_BOUNDED_QUERY_SOCKET:-}"
ENDPOINT="${AWF_BOUNDED_QUERY_ENDPOINT:-}"
CAPABILITY="${AWF_BOUNDED_QUERY_CAPABILITY:-}"
PROTOCOL_VERSION=2
# Keep in sync with MAX_SCHEMA_BYTES in src/bounded-query/protocol.ts and
# containers/bounded-query/broker/protocol.js.
# Keep in sync with MAX_SCHEMA_BYTES in src/bounded-execution/finite-disclosure.ts
# and containers/bounded-query/bounded-execution/finite-disclosure.js.
MAX_SCHEMA_BYTES=4096

emit_error() {
Expand Down
55 changes: 55 additions & 0 deletions containers/agent/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,35 @@ copy_agent_helper_scripts() {
echo "[entrypoint][WARN] Could not install bounded-query SKILL.md"
fi
fi

# Activate the bounded-agent CLI when the bounded-agent broker socket is
# present. Same mechanism and PATH entry as the bounded-query CLI.
if [ -n "$AWF_BOUNDED_AGENT_SOCKET" ] && [ -f /usr/local/bin/bounded-agent-wrapper.sh ]; then
if mkdir -p /host/tmp/awf-lib 2>/dev/null; then
if cp /usr/local/bin/bounded-agent-wrapper.sh /host/tmp/awf-lib/bounded-agent 2>/dev/null && \
chmod +x /host/tmp/awf-lib/bounded-agent 2>/dev/null; then
echo "[entrypoint] bounded-agent CLI installed at /tmp/awf-lib/bounded-agent (inside chroot)"
case ":${AWF_HOST_PATH:-$PATH}:" in
*":/tmp/awf-lib:"*) ;;
*) export AWF_HOST_PATH="/tmp/awf-lib:${AWF_HOST_PATH:-$PATH}" ;;
esac
else
echo "[entrypoint][WARN] Could not install bounded-agent CLI"
fi
fi
fi

# Install the bounded-agent SKILL.md alongside the bounded-query one.
if [ -n "$AWF_BOUNDED_AGENT_SKILL" ] && [ -f "$AWF_BOUNDED_AGENT_SKILL" ] && \
[ -n "$SYNTH_HOME" ]; then
AGENT_SKILL_DEST_DIR="/host${SYNTH_HOME}/.github/skills/bounded-agent"
if mkdir -p "$AGENT_SKILL_DEST_DIR" 2>/dev/null && \
cp "$AWF_BOUNDED_AGENT_SKILL" "$AGENT_SKILL_DEST_DIR/SKILL.md" 2>/dev/null; then
echo "[entrypoint] bounded-agent SKILL.md installed at ${SYNTH_HOME}/.github/skills/bounded-agent/SKILL.md (inside chroot)"
else
echo "[entrypoint][WARN] Could not install bounded-agent SKILL.md"
fi
fi
}

copy_dind_runner_binary() {
Expand Down Expand Up @@ -1408,6 +1437,32 @@ run_non_chroot_command() {
fi
fi

# Activate the bounded-agent CLI in non-chroot mode.
if [ -n "$AWF_BOUNDED_AGENT_SOCKET" ] && [ -f /usr/local/bin/bounded-agent-wrapper.sh ]; then
mkdir -p /tmp/awf-lib
if cp /usr/local/bin/bounded-agent-wrapper.sh /tmp/awf-lib/bounded-agent 2>/dev/null && \
chmod +x /tmp/awf-lib/bounded-agent 2>/dev/null; then
case ":${PATH}:" in
*":/tmp/awf-lib:"*) ;;
*) export PATH="/tmp/awf-lib:${PATH}" ;;
esac
echo "[entrypoint] bounded-agent CLI installed at /tmp/awf-lib/bounded-agent"
else
echo "[entrypoint][WARN] Could not install bounded-agent CLI"
fi
fi

# Install the bounded-agent SKILL.md (non-chroot mode).
if [ -n "$AWF_BOUNDED_AGENT_SKILL" ] && [ -f "$AWF_BOUNDED_AGENT_SKILL" ]; then
AGENT_SKILL_DEST_DIR="${HOME}/.github/skills/bounded-agent"
if mkdir -p "$AGENT_SKILL_DEST_DIR" 2>/dev/null && \
cp "$AWF_BOUNDED_AGENT_SKILL" "$AGENT_SKILL_DEST_DIR/SKILL.md" 2>/dev/null; then
echo "[entrypoint] bounded-agent SKILL.md installed at ${AGENT_SKILL_DEST_DIR}/SKILL.md"
else
echo "[entrypoint][WARN] Could not install bounded-agent SKILL.md"
fi
fi

# This prevents malicious code from modifying iptables rules or using chroot
# Security note: capsh --drop removes capabilities from the bounding set,
# preventing any process (even if it escalates to root) from acquiring them
Expand Down
Loading
Loading