Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
15 changes: 12 additions & 3 deletions integrations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ here versus the playbook or quickstart repos.
[sbx](https://docs.docker.com/ai/sandboxes/) mixin kits that configure an
agentic-coding sandbox declaratively (provider config, egress, CA trust,
rules/skills delivery).
- **Orchestrator integrations** (`orchestrators/<orchestrator>/`) — an
orchestrator that *drives* a sandbox tool from the outside (owns the agent +
session lifecycle and calls `acq`/`sbx`/`msb`), e.g. Agor running its executor
inside an `acq` sandbox. Contrast with isolation kits, which `acq` *applies
inside* the sandbox. Direction of control decides the area (see
[orchestrators/README](orchestrators/README.md)).
- **CI / automation recipes** (future: `ci/`, `automation/`) — reusable snippets
for wiring agentic tooling into pipelines.

Expand All @@ -38,9 +44,11 @@ integrations/
│ └── <editor>/
│ ├── README.md # setup guide
│ └── <config files> # portable config to copy into your project
└── isolation/
└── sbx-kits/
└── <kit>/ # an sbx mixin kit (spec.yaml + files/ + docs)
├── isolation/
│ └── acq-kits/
│ └── <kit>/ # an acq mixin kit (spec.yaml + files/ + docs)
└── orchestrators/
└── <orchestrator>/ # a tool that DRIVES acq/sbx/msb (wrapper + setup guide)
```

## Rules
Expand All @@ -62,3 +70,4 @@ integrations/
| [isolation/sbx-kits/playbook-kit](isolation/sbx-kits/playbook-kit/) | sbx | Mixin kit: clone the GSA playbook at sandbox startup and link its AGENTS.md + skills into each agent. |
| [isolation/sbx-kits/zscaler-ca-certificate](isolation/sbx-kits/zscaler-ca-certificate/) | sbx | Mixin kit: install the public Zscaler Root CA into the sandbox trust store for HTTPS-inspecting proxies. |
| [isolation/sbx-kits/git-ssh-sign](isolation/sbx-kits/git-ssh-sign/) | sbx | Mixin kit: sign git commits/tags with the host-forwarded SSH key (vendored from sbx-kits-contrib). |
| [orchestrators/agor](orchestrators/agor/) | Agor → `acq` | Run Agor's executor inside an `acq` sandbox via `executor_command_template`; portable wrapper + setup guide. **v1: sbx backend.** |
1 change: 1 addition & 0 deletions integrations/isolation/acq-kits/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ not agent behavior. (Behavioral patterns live in `skills/`, `prompts/`, etc.)
| [`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; sbx-only for now (see its parity note). |
| [`agor-daemon-egress/`](agor-daemon-egress/) | Allow-list egress to the Agor daemon control-plane so an Agor executor running in the sandbox (via [`orchestrators/agor`](../../orchestrators/agor/)) can connect back. Security-relevant (widens egress). |

Each kit is self-contained: a `spec.yaml` (`hybrid/v1`), any `files/` payload, a
`scripts/verify` host-side check, a `README.md` (with a **backend parity** note),
Expand Down
114 changes: 114 additions & 0 deletions integrations/isolation/acq-kits/agor-daemon-egress/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# agor-daemon-egress (acq mixin kit, `hybrid/v1`)

A neutral [`acq`](https://github.com/GSA-TTS/agentic-coding-quickstart) **mixin
kit** that allow-lists outbound egress to the **Agor daemon** control-plane from
inside the sandbox, so an Agor executor running in the sandbox can connect back
to the daemon over WebSocket/Feathers using its scoped JWT.

> **Consumed by the [`orchestrators/agor`](../../../orchestrators/agor/)
> integration.** That wrapper *drives* `acq`; this kit is *applied inside* the
> sandbox. Per the
> [orchestrators area boundary](../../../orchestrators/docs/decisions/0001-orchestrators-area-and-agor-acq.md)
> (drives-vs-applied), a kit lives here under `acq-kits/` even when its only
> consumer is an orchestrator — the orchestrator merely references it.
>
> **Neutral (backend-agnostic) kit** (`schemaVersion: "hybrid/v1"`), consumed by
> `acq`, which selects a backend. See
> [`../../docs/decisions/0001-neutral-hybrid-v1-acq-kits.md`](../../docs/decisions/0001-neutral-hybrid-v1-acq-kits.md).

## Why this kit exists

`acq` has **no per-invocation network flag** — `--net-rule`, `--allow`, and
`acq policy` do not exist at the acq level. The **only** acq-native way to open
outbound egress is a kit's `caps.network.allow`. sbx is **default-deny** for
arbitrary hosts, so without an allow entry the in-sandbox `agor-executor` cannot
reach the daemon and the session never streams results. This kit is that entry,
scoped to exactly one host:port — the Agor daemon.

See [`GSA-TTS/agentic-coding-patterns#259`](https://github.com/GSA-TTS/agentic-coding-patterns/issues/259)
(the decision) and the map [#247](https://github.com/GSA-TTS/agentic-coding-patterns/issues/247).

## What it does

- **Network egress** — allow-lists a single host:port,
`host.docker.internal:3030` by default (the sbx host alias + the Agor default
daemon port). Nothing else: no files, no commands, no secret.

## Security posture

This kit **widens network egress**, so it is reviewed as a **security-relevant**
kit (`categories: [security]` in intent; `human_review_required`; PR labelled
`needs-human-review`). Its capability is deliberately minimal:

| Field | Value | Why |
|---|---|---|
| Egress | one host:port (the daemon) | least-privilege: only the control-plane the executor must reach |
| Filesystem | none | it drops no files |
| Commands | none | it runs nothing in the guest |
| Secrets | none | the daemon URL is not sensitive |

The `hybrid/v1` kit schema is `additionalProperties: false` and models **no**
security-governance frontmatter fields (those live on *skill* patterns, not kit
specs). The governance posture is therefore recorded here and in the kit's ADR
and enforced by **human review**, not by schema fields — consistent with
[`docs/security-skill-governance.md`](../../../../docs/security-skill-governance.md).

## Backend parity

| Backend | Support | Notes |
|---|---|---|
| **sbx** | Supported (validated) | `caps.network.allow` is synthesized into the sbx-v2 kit; the full `host.docker.internal:3030` is preserved (quoted). |
| **msb** | Works, port-stripped | acq emits `--net-rule allow@host.docker.internal` and **drops the `:port`** (msb keys on domain only). Egress is host-wide for that host on msb — acceptable. A live msb run is tracked at [#257](https://github.com/GSA-TTS/agentic-coding-patterns/issues/257). |
| **ppp** (later) | Deferred | Same `caps.network.allow` path as sbx. |

No backend shortcut — every backend uses `caps.network.allow`.

## Usage

Reference it from the `orchestrators/agor` wrapper via `AGOR_EGRESS_KIT`, either
as a **local directory** (bypasses the source allowlist) or a **git ref**
(`GSA-TTS/` is on acq's default kit-source allowlist):

```bash
# local dir
AGOR_EGRESS_KIT=integrations/isolation/acq-kits/agor-daemon-egress

# or git ref (full 40-char SHA required)
AGOR_EGRESS_KIT="git+https://github.com/GSA-TTS/agentic-coding-patterns.git#ref=<sha>&dir=integrations/isolation/acq-kits/agor-daemon-egress"
```

The wrapper passes it to `acq create … --kit "$AGOR_EGRESS_KIT"`.

## Adjusting the allow entry

The default assumes the **sbx host alias** `host.docker.internal` and the **Agor
default daemon port** `3030`. If your daemon uses a different port, or your
deploy exposes it under a different host alias, **edit the single
`caps.network.allow` entry** in [`spec.yaml`](spec.yaml). A `hybrid/v1` kit
cannot template a dynamic value; the wrapper can read the real `daemonUrl` from
the executor payload, but the allow-list itself is static. (On msb the port is
dropped either way.)

## Verifying

```bash
# Offline, backend-agnostic gate (schema + registry + README):
python ../validate-kits.py

# Live sbx check (needs sbx installed + logged in): creates a throwaway sandbox
# with this kit and confirms the daemon host:port is in the sandbox egress
# allow-list. Whether the sandbox can actually ROUTE to host.docker.internal is
# a Docker-Sandboxes runtime property; the live end-to-end connection is
# validated by the orchestrators/agor integration (#257).
./scripts/verify
```

## Layout

```
agor-daemon-egress/
├── spec.yaml # the kit (hybrid/v1: caps.network.allow only)
├── README.md # this file (with the backend-parity + security note)
├── scripts/verify # host-side check
└── docs/decisions/ # design records
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# TROUBLESHOOTING — agor-daemon-egress

## The executor can't reach the daemon / session never streams results

Symptoms: the Agor session starts but produces no output; executor logs show a
WebSocket/connection error to the daemon.

1. **Confirm the kit was applied.** The `orchestrators/agor` wrapper must pass
`AGOR_EGRESS_KIT` and include `--kit "$AGOR_EGRESS_KIT"` on `acq create`. Run
the wrapper with `AGOR_SANDBOX_DRY_RUN=1` and check the printed `acq create`
line includes `--kit`.
2. **Confirm the host alias + port match your daemon.** The default allow entry
is `host.docker.internal:3030`. If your daemon runs on a different port, edit
`spec.yaml`'s `caps.network.allow`. On sbx the alias is
`host.docker.internal`; other backends/deploys may differ.
3. **sbx is default-deny.** If you removed or mistyped the allow entry, egress to
the daemon is blocked. Re-check `spec.yaml`.
4. **Routing vs. allow-listing.** This kit only *allow-lists* the destination.
Whether the sandbox runtime can actually **route** to the
`host.docker.internal` host-gateway alias is a Docker-Sandboxes property, not
something the kit controls. Verify from inside the sandbox:
`acq exec <sandbox> -- sh -c 'getent hosts host.docker.internal'`.

## On msb, egress seems broader than the port I set

Expected. `acq`'s msb adapter emits `--net-rule allow@host.docker.internal` and
**strips the `:port`** — msb keys on the domain only, so egress is host-wide for
that host. sbx (the validated backend) keeps the port; the msb port-stripping is
benign for this single-host egress. A live msb run is tracked at
[#257](https://github.com/GSA-TTS/agentic-coding-patterns/issues/257).

## `validate-kits.py` fails for this kit

- **Missing registry entry** — add `agor-daemon-egress` to
[`../kits.yaml`](../kits.yaml).
- **Missing README** — this file's sibling `README.md` must exist (parity note).
- **Schema error** — the kit uses only `caps.network.allow` + `backend_shortcuts`;
it drops no files and runs no commands, so there is nothing else to validate.
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Decision: `agor-daemon-egress` as a security-categorized isolation kit

**Status:** accepted
**Date:** 2026-07-26

## Context

The [`orchestrators/agor`](../../../../../orchestrators/agor/) wrapper runs an Agor
executor inside an `acq` sandbox. The executor must connect back to the Agor
daemon over WebSocket. Research
([#248](https://github.com/GSA-TTS/agentic-coding-patterns/issues/248),
[#259](https://github.com/GSA-TTS/agentic-coding-patterns/issues/259)) established
that `acq` has **no per-invocation network flag** — the only acq-native way to
open outbound egress is a kit's `caps.network.allow`, and sbx is default-deny for
arbitrary hosts. So daemon reachability **must** be expressed as a kit.

Two questions had to be settled: (1) *where* the kit lives, and (2) whether it is
governed as a **security** kit.

## Decision

### 1. It is an isolation kit, not an orchestrator artifact

Per the orchestrators-area boundary
([ADR 0001](../../../../../orchestrators/docs/decisions/0001-orchestrators-area-and-agor-acq.md),
drives-vs-applied), a kit is something `acq` **applies inside** the sandbox — so
it lives under `integrations/isolation/acq-kits/`, even though its **only**
consumer is the `orchestrators/agor` integration. The orchestrator **references**
it (via `AGOR_EGRESS_KIT`); it does not own it. This keeps the two areas composing
cleanly and keeps all `caps.network.allow` kits in one place.

### 2. It is governed as a security-relevant kit

The kit **widens network egress**, which is a security-relevant capability. It is
therefore treated as a **security** kit: `human_review_required`, PR labelled
`needs-human-review`, one focused change per PR, and a release-visible
conventional-commit type. Its capability is nonetheless minimal by construction —
one host:port (the daemon), no files, no commands, no secret.

**Constraint / how the governance is recorded.** The `hybrid/v1` kit schema is
`additionalProperties: false` and models **no** security-governance frontmatter
fields — those (`categories`, `risk_tier`, `human_review_required`,
`network_policy`, …) are defined for *skill* patterns
(`schemas/skill.schema.json`), not kit specs. So we do **not** (and cannot) add
those fields to `spec.yaml`. Instead the security posture is recorded in prose in
the kit `README.md` (a "Security posture" table) and here, and enforced by
**human review**, consistent with
[`docs/security-skill-governance.md`](../../../../../../docs/security-skill-governance.md).
Approved by the human owner on 2026-07-26.

## Alternatives considered

- **Put the kit under `orchestrators/agor/`** — rejected: it is applied inside the
sandbox, so by the area boundary it belongs in `acq-kits/`.
- **Add security-governance frontmatter to `spec.yaml`** — rejected: the kit
schema forbids unknown fields, and those fields are a skill-pattern concept.
Recorded in prose + review instead.
- **Add a per-invocation `acq --allow` flag upstream** — out of scope here; a
reasonable upstream request, but the kit is the mechanism that exists today.

## Consequences

- Daemon egress is expressed declaratively and reviewably, scoped to one
host:port.
- The kit's static allow entry must be edited for a non-default daemon port/alias
(a `hybrid/v1` kit cannot template it); documented in the README.
- On msb the port is stripped (host-wide for that host); acceptable — sbx is the
validated backend, and a live msb run is tracked at
([#257](https://github.com/GSA-TTS/agentic-coding-patterns/issues/257)).
105 changes: 105 additions & 0 deletions integrations/isolation/acq-kits/agor-daemon-egress/scripts/verify
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
#
# verify — host-side validation for the agor-daemon-egress acq mixin kit.
#
# Backend-agnostic gate is integrations/isolation/acq-kits/validate-kits.py.
# This script does the sbx-path live check: it creates a throwaway sandbox with
# the kit applied and confirms the daemon host:port is present in the sandbox's
# egress allow-list. It does NOT prove routing to host.docker.internal (a
# Docker-Sandboxes runtime property) nor a real daemon connection — that live
# end-to-end path is validated by the orchestrators/agor integration
# (GSA-TTS/agentic-coding-patterns#257).
#
# Run on a host where `sbx` is installed and logged in. Creates a temporary
# sandbox "agor-egress-verify-*" and removes it at the end (and on interrupt).
# Nothing here is secret — the daemon URL is not sensitive.
#
# Usage:
# path/to/agor-daemon-egress/scripts/verify
# KEEP=1 path/to/.../scripts/verify # keep the sandbox for inspection

set -uo pipefail

KIT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
SPEC_FILE="$KIT_DIR/spec.yaml"
SBX_NAME="agor-egress-verify-$$"
WORK="$(mktemp -d "${TMPDIR:-/tmp}/agor-egress-verify.XXXXXX")"
CREATE_LOG="$WORK/sbx-create.log"
KEEP="${KEEP:-0}"
# The host:port the kit allow-lists (keep in sync with spec.yaml).
DAEMON_HOST="host.docker.internal"

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)); }
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
sbx rm -f "$SBX_NAME" >/dev/null 2>&1 || true
rm -rf "$WORK" >/dev/null 2>&1 || true
}
trap cleanup EXIT INT TERM

in_sbx() { sbx exec "$SBX_NAME" -- sh -c "$1" </dev/null 2>/dev/null || true; }

info "0. Preconditions"
if ! command -v sbx >/dev/null 2>&1; then
echo " sbx not on PATH. Install sbx and retry." >&2
exit 1
fi
if [ -f "$SPEC_FILE" ]; then
ok "kit spec.yaml present"
else
bad "spec.yaml missing"
exit 1
fi
if grep -q "$DAEMON_HOST" "$SPEC_FILE"; then
ok "spec allow-lists $DAEMON_HOST"
else
bad "spec.yaml does not mention $DAEMON_HOST (edit DAEMON_HOST or the spec)"
fi

info "1. sbx kit validate"
if sbx kit validate "$KIT_DIR" >/dev/null 2>&1; then
ok "kit validates"
else
bad "kit failed validation"
fi

info "2. Create a sandbox with the kit"
echo " creating $SBX_NAME (workspace $WORK)..."
agent="shell"
if ! sbx create --name "$SBX_NAME" --kit "$KIT_DIR" "$agent" "$WORK" >"$CREATE_LOG" 2>&1; then
agent="opencode"
if sbx create --name "$SBX_NAME" --kit "$KIT_DIR" "$agent" "$WORK" >"$CREATE_LOG" 2>&1; then
ok "sandbox created with --kit (agent: $agent)"
else
bad "sbx create failed (exit $?)"
sed 's/^/ | /' "$CREATE_LOG" >&2
exit 1
fi
else
ok "sandbox created with --kit (agent: $agent)"
fi
sleep 2

info "3. Daemon host resolves inside the sandbox (informational)"
# Allow-listing != routing. This probes whether the host-gateway alias resolves;
# a real daemon connection is validated by the orchestrators/agor integration.
if [ "$(in_sbx "getent hosts $DAEMON_HOST >/dev/null 2>&1 && echo yes")" = "yes" ]; then
ok "$DAEMON_HOST resolves in the sandbox"
else
echo " NOTE: $DAEMON_HOST did not resolve here — routing is a runtime property;" >&2
echo " the real daemon connection is validated by orchestrators/agor (#257)." >&2
fi

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 ]
Loading