From e35373b1da703eff6ac7856e7c6d70b0766babfb Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Sun, 26 Jul 2026 06:41:17 +0000 Subject: [PATCH 01/11] docs(orchestrators): land Agor sandbox + acq exploration docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seed the new integrations/orchestrators/agor/ area with the two source exploration documents that motivate the Agor + acq isolation integration: - sandbox-abstraction.md: Agor executor-hook sandbox abstraction design (executor_command_template, sbx/msb CLI shapes, mount strategies, the SandboxProvider abstraction, phased rollout). - sandbox-acq-analysis.md: using acq as the sandbox backend — the four kits, who-owns-what split, USAi credential flow, SSH-agent forwarding. Reproduced verbatim from the source material; status: exploration. Not indexed by generate_index.py (scans only skills/prompts/workflows/agents/ lessons). Refs GSA-TTS/agentic-coding-patterns#258. Co-authored-by: OpenCode Agent (cherry picked from commit b8de4140941e4b9d24391e2aa06c377d35aaea5e) --- .../docs/explorations/sandbox-abstraction.md | 611 ++++++++++++++++++ .../docs/explorations/sandbox-acq-analysis.md | 351 ++++++++++ 2 files changed, 962 insertions(+) create mode 100644 integrations/orchestrators/agor/docs/explorations/sandbox-abstraction.md create mode 100644 integrations/orchestrators/agor/docs/explorations/sandbox-acq-analysis.md diff --git a/integrations/orchestrators/agor/docs/explorations/sandbox-abstraction.md b/integrations/orchestrators/agor/docs/explorations/sandbox-abstraction.md new file mode 100644 index 0000000..b3662bb --- /dev/null +++ b/integrations/orchestrators/agor/docs/explorations/sandbox-abstraction.md @@ -0,0 +1,611 @@ +# Sandboxing CLI Abstraction for Agor's Executor Hook + +**Status:** 🔬 Exploration +**Related issues:** [#1631](https://github.com/preset-io/agor/issues/1631), [#1684](https://github.com/preset-io/agor/issues/1684) +**Last Updated:** 2026-07-13 + +--- + +## Context + +- **#1631** — Add OpenShell as a managed sandboxed runtime (gRPC-based, podman/microVM/k8s backends). Blocked by OpenShell overriding the model in agent requests. +- **#1684** — Investigate sandboxing via Docker `sbx`, nono.sh, and Lima. Calls for a **common abstraction** across multiple sandbox providers, with worked examples. + +**Goal:** Define the general "shape" of a sandboxing CLI abstraction that plugs into Agor's existing Executor spawn path, using `sbx` and `microsandbox` (`msb`) as the reference CLI patterns. + +--- + +## 1. What the Executor Hook Is Today + +The executor is Agor's single isolation boundary. The relevant chokepoints: + +| Component | File | Role | +|---|---|---| +| **`buildSpawnArgs()`** | `packages/core/src/unix/run-as-user.ts:219` | THE single sandbox boundary. Wraps command in `sudo -u $asUser`. Today only knows Unix impersonation. | +| **`spawnExecutor()`** | `apps/agor-daemon/src/utils/spawn-executor.ts:223` | Single entry point. Two paths: `spawnExecutorLocal` (node child_process) and `spawnExecutorWithTemplate` (sh -c template for k8s/docker). | +| **`executor_command_template`** | `packages/core/src/config/types.ts:487` | Config-driven escape hatch — already supports `kubectl run` and `docker run` with `{unix_user_uid}`, `{session_id}`, etc. variable substitution. | +| **`createExecuteHandler()`** | `apps/agor-daemon/src/register-services.ts:699` | Resolves Unix user, env vars, builds payload, calls `spawnExecutor()`. | +| **`executeToolTask()`** | `packages/executor/src/handlers/sdk/base-executor.ts:374` | In-executor lifecycle: git setup → key resolution → `tool.executePromptWithStreaming()` → git capture → normalize → patch task. | + +**Key architectural fact:** The executor process runs the agent SDK **in-process** (Claude SDK, Codex app-server, Gemini SDK, etc.). It connects back to the daemon via WebSocket/Feathers using a scoped JWT. The daemon writes the JSON payload to the executor's stdin. + +**The `executor_command_template` is already a sandbox abstraction** — it's just string-based and operator-configured. The goal is to make it programmatic and provider-aware. + +--- + +## 2. sbx vs microsandbox CLI — Common Shape + +Both CLIs share a remarkably similar command surface. Here's the common denominator: + +### Lifecycle Commands + +| Operation | sbx | msb | Purpose | +|---|---|---|---| +| Create + run | `sbx run [workspace]` | `msb run -- ` | Create sandbox, optionally attach | +| Create only | `sbx create ` | `msb create ` | Boot without attaching | +| Exec in running | `sbx exec ` | `msb exec -- ` | Run command in existing sandbox | +| List | `sbx ls` | `msb ls` / `msb ps` | List sandboxes | +| Stop | `sbx stop ` | `msb stop ` | Graceful shutdown | +| Remove | `sbx rm ` | `msb rm ` | Delete sandbox + state | +| Copy files | `sbx cp ` | `msb copy ` | Host ↔ sandbox file transfer | +| Port forward | `sbx ports --publish` | `msb run -p :` | Forward host port into sandbox | +| Logs | (agent session) | `msb logs ` | Captured stdout/stderr | +| Inspect | (sbx ls shows details) | `msb inspect ` | Detailed config/status | +| Metrics | (dashboard) | `msb metrics` | CPU/memory/network stats | + +### Mount / Workspace Model + +| Feature | sbx | msb | +|---|---|---| +| **Direct mount** | Default: workspace mounted rw at same absolute path | `-v :[:ro\|rw]` | +| **Clone mode** | `--clone`: repo mounted ro, clone inside sandbox, exposed as git remote on host | (not built-in; can be scripted) | +| **Extra mounts** | Positional paths, `:ro` suffix | `-v` repeatable, `:ro`/`rw` options | +| **Named volumes** | (not supported) | `msb volume create` + `--mount-named` | + +### Network Policy + +| Feature | sbx | msb | +|---|---|---| +| **Default** | Routes through host HTTP proxy, policy-enforced | Public internet allowed; private/loopback/metadata denied | +| **Disable net** | (policy-based) | `--no-net` | +| **Custom rules** | `sbx policy allow/deny ` | `--net-rule "allow@target:proto:ports"` | +| **Secret injection** | Credentials injected via proxy, never on filesystem | `--secret NAME@HOST` (host-held, injected for allowed TLS dests) | + +### Resource Limits + +| Feature | sbx | msb | +|---|---|---| +| CPU | (managed) | `-c`, `--max-cpus` (hotplug ceiling) | +| Memory | (managed) | `-m`, `--max-memory` (hotplug ceiling) | +| Live resize | (no) | `msb modify --cpus N --memory M` | + +### Agent-Aware vs Agent-Agnostic — Both Have a Raw Mode + +- **sbx** has built-in agent presets (`sbx run claude`, `sbx run codex`, etc.) that auto-configure the agent. But it also has a **`shell` agent** (`sbx run shell`) — an agent-less sandbox with just a bash login shell, no pre-installed agent binary. This is the right mode for Agor: the sandbox is a raw box, and `agor-executor --stdin` runs inside it. +- **msb** is agent-agnostic by design (`msb run ubuntu -- bash` is a generic microVM). + +**For Agor:** Both CLIs are equally suitable. Use `sbx run shell` or `msb run --` to get a raw sandbox, then pipe the executor payload to `agor-executor --stdin` inside it. The agent-awareness of sbx is irrelevant — Agor's executor already manages the agent SDK. + +--- + +## 2a. Can This Be Done Without Modifying Agor at All? + +**Yes.** The existing `executor_command_template` is already a sandbox abstraction — it's just string-based and operator-configured. A wrapper script can provide the full sandbox lifecycle without touching Agor core. + +### How It Works Today + +The daemon's `spawnExecutorWithTemplate()` (`spawn-executor.ts:406`) does: +1. Reads `executor_command_template` from config +2. Substitutes variables: `{task_id}`, `{command}`, `{unix_user}`, `{unix_user_uid}`, `{unix_user_gid}`, `{session_id}`, `{branch_id}`, `{log_level}` +3. Runs `sh -c ""` +4. Writes the JSON executor payload to the process's stdin +5. The template command is expected to pipe that stdin to `agor-executor --stdin` + +### What Information the Script Has Access To + +**Template variables (as argv):** The prompt handler (`register-services.ts:948-952`) passes only `{session_id}`, `{task_id}`, `{unix_user}`. `branch_id` is in the interface but not populated for prompt spawns (it IS populated for environment lifecycle spawns at `branches.ts:447`). No path information is passed as template variables. + +**JSON payload on stdin:** The full executor payload is written to the script's stdin. For `prompt` commands, this includes (`PromptPayloadSchema` at `payload-types.ts:151`): +- `sessionToken` — JWT for daemon API auth +- `params.cwd` — **the worktree/clone absolute path** (set at `register-services.ts:780` as `branch.path`) +- `params.sessionId`, `params.taskId`, `params.prompt`, `params.tool` + +So the script can parse stdin JSON to get `params.cwd` (the worktree path) without any API calls. But it does NOT get the main repo's `local_path` directly. + +### How the Script Discovers the Main Repo's `.git` Path + +The script can derive the main repo's `.git` path from the worktree itself, with zero API calls: + +**For worktree-mode branches:** The worktree's `.git` is a **file** (not a directory) containing `gitdir: /.git/worktrees/`. The script reads this file to extract the path: + +```bash +WORKTREE_PATH=$(echo "$STDIN_JSON" | jq -r '.params.cwd') + +# Check if .git is a file (worktree) or directory (clone) +if [ -f "$WORKTREE_PATH/.git" ]; then + # Worktree mode: .git file points to main repo's .git + GITDIR_LINE=$(cat "$WORKTREE_PATH/.git") # "gitdir: /home/user/code/myapp/.git/worktrees/feat-auth" + MAIN_GIT=$(echo "$GITDIR_LINE" | sed 's|gitdir: \(.*\)/worktrees/.*|\1|') # /home/user/code/myapp/.git + # Mount worktree rw + main .git rw + MOUNTS="-v $WORKTREE_PATH:$WORKTREE_PATH:rw -v $MAIN_GIT:$MAIN_GIT:rw" +elif [ -d "$WORKTREE_PATH/.git" ]; then + # Clone mode: self-contained .git directory, no path back to main repo + # Mount just the clone dir rw + MOUNTS="-v $WORKTREE_PATH:$WORKTREE_PATH:rw" +fi +``` + +**For clone-mode branches:** The `.git` is a real directory — the clone is self-contained, no external `.git` to mount. The script just mounts the clone directory. + +This approach: +- Works for both remote and local repos (the `.git` file contains the absolute path either way) +- Requires zero daemon API calls +- Requires zero Agor modifications +- Handles the worktree vs. clone distinction automatically + +### The Wrapper Script (Revised) + +```yaml +# ~/.agor/config.yaml +execution: + executor_command_template: | + /home/user/.agor/sandbox-wrapper.sh {session_id} +``` + +```bash +#!/bin/bash +# ~/.agor/sandbox-wrapper.sh +# Args: session_id (from template variable) +# Stdin: JSON executor payload — buffer it, extract cwd, pipe to agor-executor inside sandbox + +set -euo pipefail +SESSION_ID="$1" +SANDBOX_NAME="agor-${SESSION_ID:0:8}" + +# Buffer stdin (the JSON payload) so we can both parse it and pipe it to the sandbox +PAYLOAD=$(cat) +WORKTREE_PATH=$(echo "$PAYLOAD" | jq -r '.params.cwd') + +# Discover main repo .git path from the worktree's .git file +if [ -f "$WORKTREE_PATH/.git" ]; then + # Worktree mode: read .git file → extract main repo's .git path + GITDIR_LINE=$(cat "$WORKTREE_PATH/.git") + MAIN_GIT=$(echo "$GITDIR_LINE" | sed 's|gitdir: \(.*\)/worktrees/.*|\1|') + MOUNT_ARGS=(-v "$WORKTREE_PATH:$WORKTREE_PATH:rw" -v "$MAIN_GIT:$MAIN_GIT:rw") +elif [ -d "$WORKTREE_PATH/.git" ]; then + # Clone mode: self-contained, just mount the clone dir + MOUNT_ARGS=(-v "$WORKTREE_PATH:$WORKTREE_PATH:rw") +else + echo "ERROR: $WORKTREE_PATH is not a git workspace" >&2 + exit 1 +fi + +# Create sandbox with workspace mounted, allow daemon access +msb run --name "$SANDBOX_NAME" -d \ + "${MOUNT_ARGS[@]}" \ + --net-rule "allow@host.microsandbox.internal:3030" \ + ubuntu + +# Cleanup on exit +trap "msb rm --force $SANDBOX_NAME 2>/dev/null" EXIT + +# Pipe the buffered JSON payload to agor-executor inside the sandbox +echo "$PAYLOAD" | msb exec "$SANDBOX_NAME" -- agor-executor --stdin +``` + +### What Works Without Agor Changes + +| Feature | Works? | How | +|---|---|---| +| Create sandbox per task | Yes | Wrapper script calls `msb run` / `sbx run shell` | +| Discover worktree path | Yes | Parse `params.cwd` from stdin JSON payload | +| Discover main repo `.git` path | Yes | Read worktree's `.git` file (worktree mode) or detect self-contained `.git` dir (clone mode) | +| Mount worktree + `.git` only | Yes | `-v` flags with paths discovered above | +| Run executor inside sandbox | Yes | `msb exec -- agor-executor --stdin` pipes the JSON payload | +| Network policy | Yes | `--net-rule` flags on sandbox creation | +| Secret injection | Yes | `--secret` flags (msb) or proxy policy (sbx) | +| Cleanup on exit | Yes | `trap` on script exit removes sandbox | +| Git operations (commit, push) | Yes | Worktree + `.git` mounted rw | +| Daemon API access from sandbox | Yes | `--net-rule` allows `host.microsandbox.internal:3030` | + +### What Doesn't Work Without Agor Changes + +| Feature | Why | What it needs | +|---|---|---| +| Sandbox status in UI | Daemon doesn't know about sandboxes, only PIDs | First-class `SandboxProvider` in daemon | +| `agor sandbox ls/stop/rm` CLI | Daemon can't manage sandboxes it didn't create | Provider registered in daemon | +| Orphan cleanup on daemon crash | Wrapper script's `trap` only fires if the script exits cleanly | Daemon-managed sandbox lifecycle | +| Capability detection before spawn | Daemon can't check if `msb` is installed | `isAvailable()` check in spawn path | +| Sandbox-level heartbeat | Daemon tracks executor PID, not sandbox | Provider-integrated heartbeat | +| Session reconnect to existing sandbox | Daemon always spawns fresh | Sandbox reuse logic in daemon | + +### Recommendation: Two Tiers + +1. **Tier 1 — Wrapper scripts (no Agor changes):** Ship worked examples for `msb` and `sbx` as wrapper scripts + `executor_command_template` configs. This is what issue #1684 asks for. Proves the concept, identifies rough edges, requires zero Agor modifications. + +2. **Tier 2 — First-class `SandboxProvider` (Agor changes):** If the worked examples reveal that users need UI status, CLI management, or orphan cleanup, then add the `SandboxProvider` interface to Agor core. Tier 1 informs the interface design. + +This is the right order: **worked examples first, abstraction second.** + +--- + +## 3. The Worktree Mount Question + +### The Original Hypothesis + +> Sandboxes should load the git repository read-only, and the worktree read-write. + +### Why Read-Only `.git` Breaks Commits + +Git worktrees share the main repository's `.git` directory. When an agent commits in a worktree, git writes **new objects to the shared `objects/` directory** and **updates the branch ref in `refs/heads/`**. If `.git` is mounted read-only, `git commit` fails. Since Agor agents must be able to commit, read-only `.git` is a non-starter. + +**sbx confirms this limitation**: its `--clone` mode explicitly **does not work from inside a git worktree** ("The read-only bind mount can't resolve the worktree's `.git` pointer file"). + +### The Real Concern: Can the Agent See the Host User's Checkout? + +The read-only hypothesis is really about a deeper concern: **can we prevent the agent from seeing the user's working directory — current branch, untracked files like `.env` with real secrets?** + +This matters most for **local repos** (`agor repo add-local ~/code/myapp`), where the user's main checkout IS the repo that worktrees branch from. For **remote repos** (`agor repo add `), Agor's main checkout at `~/.agor/repos/` is under Agor's control and typically doesn't have user secrets in it. + +### Agor's Two Repo Models + +| | Remote repo | Local repo | +|---|---|---| +| **How added** | `agor repo add ` | `agor repo add-local ` | +| **Main checkout** | `~/.agor/repos/` (Agor-managed clone) | User's path (e.g. `~/code/myapp`) — used in-place, no copy | +| **Worktrees share** | `~/.agor/repos//.git/` | `~/code/myapp/.git/` | +| **User's `.env` at risk?** | Unlikely (Agor's clone is clean) | **Yes** — `.env` is in `~/code/myapp/`, right next to `.git/` | +| **Clone mode available?** | Yes (re-clones from `remote_url`) | **No** — `createBranchAsClone` requires `repo.remote_url` | + +The local repo case is where the concern bites: mounting the main repo's `.git` means mounting inside `~/code/myapp/`, and if you mount the whole directory, the agent sees `.env`. + +### The Solution: Mount ONLY `.git`, Not the Parent Directory + +Both sbx and msb (and Docker/Podman in general) support mounting a specific subdirectory without mounting its parent. The sandbox creates empty parent dirs to hold the mount point. + +**For worktree mode:** + +``` +-v {worktree_path}:{worktree_path}:rw # the agent's working files +-v {main_repo}/.git:{main_repo}/.git:rw # ONLY the git database, not the checkout +``` + +Inside the sandbox, the filesystem looks like: + +``` +~/code/myapp/ ← empty directory (auto-created as mount parent) + .git/ ← mounted rw (objects, refs, worktree admin) + (no .env, no source files, no working directory) +``` + +The agent can: +- `git commit`, `git push`, `git pull` — `.git` is rw +- `git status`, `git diff`, `git log` — worktree + `.git` both available + +The agent cannot: +- Read `~/code/myapp/.env` — the file isn't mounted (only `.git/` is) +- See the user's working directory — it's an empty dir in the sandbox +- Traverse to other paths on the host — the sandbox only has what's explicitly mounted + +**What about discovering other worktrees?** The `.git/worktrees/` directory lists all worktree admin entries, so the agent can see that other branches exist. But those worktrees' working directories (at `~/.agor/worktrees///`) are **not mounted** in this sandbox — each sandbox only mounts its own worktree. The agent can know other branches exist (which is not a secret) but can't read their files. + +**What about the path in the `.git` pointer?** The worktree's `.git` file contains `gitdir: ~/code/myapp/.git/worktrees/`, which reveals the path `~/code/myapp`. But `~/code/myapp/` is an empty directory in the sandbox — only `.git/` exists there. The path is visible but the contents are not. + +### Three Mount Strategies + +| Strategy | Mounts | Agent sees user's checkout? | Git commit works? | Works for local repos? | +|---|---|---|---|---| +| **A. Worktree + `.git` only** (recommended) | Worktree rw + `
/.git` rw | No (only `.git/` mounted, not the checkout) | Yes | Yes | +| **B. Clone + clone dir** | Clone dir rw (self-contained `.git/`) | No (no path back to main repo at all) | Yes | No (clone mode requires `remote_url`) | +| **C. Worktree + full main repo** | Worktree rw + `
` rw | **Yes** (full checkout mounted) | Yes | Yes (but exposes everything) | + +**Strategy A** is the recommended default — it works for both remote and local repos, allows git commits, and prevents the agent from seeing the user's working directory. **Strategy B** (clone mode) is stronger but only available for remote repos. **Strategy C** is the naive approach and should be avoided for local repos. + +### What About Committed Secrets? + +Both strategies A and B share one limitation: if secrets were **committed to git history** (even accidentally, then removed), the objects are still in `.git/objects/` and the agent can access them via `git log --all` + `git show`. Purging committed secrets requires `git filter-repo` or BFG, which is a separate concern orthogonal to sandboxing. + +### What About No-Git Workflows? + +Agor requires git at every layer — every session needs a branch, every branch needs a repo, every repo must be a valid git repo (`isValidGitRepo()` check in `addLocalRepository`). There is no gitless path today. The sandboxing abstraction doesn't need to handle non-git workspaces. + +### Recommendation + +**Strategy A (mount `.git` only) as the default for all worktree-mode branches.** It's simple, works for both remote and local repos, and solves the core concern. For clone-mode branches, **Strategy B** (mount the clone dir) is automatically safe since the clone is self-contained. + +For the wrapper script approach (section 2a), the script should: +1. Fetch the branch's `storage_mode` and `path` from the daemon API +2. If `worktree`: mount `{branch_path}` rw + `{main_repo}/.git` rw (Strategy A) +3. If `clone`: mount `{branch_path}` rw only (Strategy B — self-contained) +4. Auto-allow the daemon URL in the sandbox network policy + +--- + +## 4. Proposed Abstraction Shape + +### SandboxProvider Interface + +```typescript +// packages/core/src/sandbox/sandbox-provider.ts + +interface SandboxProvider { + readonly name: string; // 'sbx' | 'msb' | 'openshell' | 'local' | 'template' + + // Lifecycle + createSandbox(spec: SandboxSpec): Promise; + exec(sandboxId: string, command: string, args: string[], opts?: ExecOpts): Promise; + stopSandbox(sandboxId: string, opts?: { force?: boolean; timeoutMs?: number }): Promise; + removeSandbox(sandboxId: string, opts?: { force?: boolean }): Promise; + getSandboxStatus(sandboxId: string): Promise; + + // Capability detection + isAvailable(): Promise; // Is the CLI installed/daemon running? + getCapabilities(): SandboxCapabilities; +} + +interface SandboxSpec { + // Identity + name: string; // e.g. `agor-{session_short_id}` + image: string; // base image (e.g. 'ubuntu', 'node:20', custom) + + // Mounts + mounts: SandboxMount[]; + workdir?: string; // working directory inside sandbox + + // Network + network: 'disabled' | 'default' | { rules: NetworkRule[]; defaultEgress: 'allow' | 'deny' }; + + // Secrets / env + env: Record; // non-secret env vars + secrets: SecretInjection[]; // secret refs (never written to filesystem) + + // Resources + cpus?: number; + memory?: string; // '512M', '1G' + maxCpus?: number; // hotplug ceiling + maxMemory?: string; + + // Labels (for grouping, e.g. by session/branch) + labels?: Record; +} + +interface SandboxMount { + source: string; // host path + destination: string; // path inside sandbox + readonly: boolean; + kind?: 'bind' | 'volume'; // bind mount vs named volume +} + +interface SandboxHandle { + id: string; // provider-assigned sandbox ID + name: string; // our name + status: 'creating' | 'running' | 'stopped' | 'exited'; + execCommand: { cmd: string; args: string[] }; // the command to run agor-executor inside +} + +interface SandboxCapabilities { + supportsCloneMode: boolean; + supportsLiveResize: boolean; + supportsPortForwarding: boolean; + supportsNetworkRules: boolean; + supportsSecretInjection: boolean; + isAgentAware: boolean; // sbx knows about claude; msb doesn't + startupTimeMs: 'fast' | 'medium' | 'slow'; // <1s, 1-5s, 5+s +} +``` + +### How It Plugs Into the Executor Spawn Path + +The provider plugs into `spawnExecutor()` as a **third spawn strategy** alongside the existing two: + +``` +spawnExecutor() + ├── spawnExecutorLocal() // today: node child_process, sudo -u + ├── spawnExecutorWithTemplate() // today: sh -c operator template (k8s/docker) + └── spawnExecutorInSandbox() // NEW: SandboxProvider.createSandbox() + exec() +``` + +**Flow:** + +1. `createExecuteHandler()` resolves the sandbox spec from config + branch + session context +2. Calls `spawnExecutorInSandbox(spec, executorPayload)` +3. Provider creates sandbox with worktree mounted rw, `.git` mounted rw, network policy, secrets +4. Provider execs `agor-executor --stdin` inside the sandbox +5. JSON payload piped to sandbox's stdin (same as today) +6. Executor process inside sandbox connects back to daemon via WebSocket (same as today) +7. On task complete / SIGTERM: provider stops/removes sandbox + +**Key insight:** The executor process runs **inside** the sandbox. The sandbox replaces `sudo -u` as the isolation mechanism. Everything inside the executor (SDK calls, git operations, streaming) works unchanged because the executor sees a normal filesystem and network (filtered by the sandbox). + +### Config Shape + +```yaml +# ~/.agor/config.yaml +execution: + # Existing: unix_user_mode: simple | insulated | strict + unix_user_mode: simple + + # NEW: sandbox provider (overrides unix_user_mode when set) + sandbox: + provider: none | sbx | msb | openshell | template + # ^ 'none' = use existing unix_user_mode path (default, backward compat) + # ^ 'template' = use executor_command_template (existing escape hatch) + + # Provider-specific config + sbx: + image: ubuntu + # sbx manages agent setup; we just provide the box + clone_mode: false # opt-in clone mode (high security) + network: default # sbx proxy handles policy + + msb: + image: ubuntu + cpus: 2 + memory: 2G + network: + default_egress: allow # or 'deny' for allowlist + rules: [] + secrets: + - name: ANTHROPIC_API_KEY + host_env: ANTHROPIC_API_KEY # inject from host env, never to filesystem + + openshell: + gateway_url: unix:///tmp/openshell.sock + provider: anthropic + policy_preset: default + + # Mount policy (applies to all providers) + mounts: + worktree: rw # always rw (agent must edit) + git_dir: rw # must be rw for git commit (see section 3) + # Optional: mount additional read-only paths + extra_readonly: [] +``` + +### Provider Implementation Shape + +Each provider is a thin adapter that translates `SandboxSpec` → CLI invocation: + +```typescript +// packages/core/src/sandbox/providers/sbx-provider.ts +class SbxProvider implements SandboxProvider { + async createSandbox(spec: SandboxSpec): Promise { + const args = ['run', '--name', spec.name, '--detach']; + for (const m of spec.mounts) { + args.push(m.source + (m.readonly ? ':ro' : '')); + } + if (spec.cpus) args.push('--cpus', String(spec.cpus)); + // ... network, secrets, etc. + args.push(spec.image); + + const result = await exec('sbx', args); + return { id: parseSbxId(result), name: spec.name, status: 'running', ... }; + } + + async exec(id, cmd, args) { + return exec('sbx', ['exec', id, '--', cmd, ...args]); + } + // ... +} +``` + +```typescript +// packages/core/src/sandbox/providers/msb-provider.ts +class MsbProvider implements SandboxProvider { + async createSandbox(spec: SandboxSpec): Promise { + const args = ['run', '-d', '--name', spec.name]; + for (const m of spec.mounts) { + const flag = m.kind === 'volume' ? '--mount-named' : '-v'; + args.push(flag, `${m.source}:${m.destination}:${m.readonly ? 'ro' : 'rw'}`); + } + if (spec.cpus) args.push('-c', String(spec.cpus)); + if (spec.memory) args.push('-m', spec.memory); + for (const s of spec.secrets) args.push('--secret', `${s.name}@${s.hostEnv}`); + // ... network rules + args.push(spec.image); + + const result = await exec('msb', args); + return { id: spec.name, name: spec.name, status: 'running', ... }; + } + // ... +} +``` + +--- + +## 5. What Changes in Agor + +### New Files + +| File | Purpose | +|---|---| +| `packages/core/src/sandbox/sandbox-provider.ts` | `SandboxProvider` interface, `SandboxSpec`, types | +| `packages/core/src/sandbox/sandbox-manager.ts` | Provider registry, resolves config → provider, lifecycle orchestration | +| `packages/core/src/sandbox/providers/sbx-provider.ts` | Docker sbx adapter | +| `packages/core/src/sandbox/providers/msb-provider.ts` | microsandbox adapter | +| `packages/core/src/sandbox/providers/local-provider.ts` | Pass-through to existing `buildSpawnArgs()` (backward compat) | +| `packages/core/src/sandbox/providers/template-provider.ts` | Wraps existing `executor_command_template` | +| `apps/agor-docs/pages/guide/sandboxing.mdx` | User-facing guide (canonical reference) | + +### Modified Files + +| File | Change | +|---|---| +| `packages/core/src/config/types.ts` | Add `AgorSandboxSettings` type under `execution.sandbox` | +| `apps/agor-daemon/src/utils/spawn-executor.ts` | Add `spawnExecutorInSandbox()` path; provider selection logic | +| `apps/agor-daemon/src/register-services.ts` | `createExecuteHandler()` resolves sandbox spec when `sandbox.provider != none` | +| `apps/agor-daemon/src/index.ts` | Initialize sandbox manager, register providers based on config | +| `apps/agor-cli/src/commands/` | `agor sandbox ls/stop/rm` commands (delegate to provider) | +| `apps/agor-ui/src/components/` | Sandbox status indicator in session panel | + +### What Does NOT Change + +- `packages/executor/` — the executor process is unchanged. It runs inside the sandbox exactly as it runs inside a `sudo -u` shell today. +- `packages/core/src/unix/run-as-user.ts` — `buildSpawnArgs()` stays as-is for the `local` provider (backward compat). +- Agent SDK handlers — no changes. Claude/Codex/Gemini/OpenCode all work unchanged inside the sandbox. +- The JSON-over-stdin payload protocol — unchanged. +- The WebSocket/Feathers client in the executor — unchanged (just needs network route to daemon, which sandbox network policy must allow). + +--- + +## 6. Implementation Phases + +### Phase 1: Abstraction + Local Provider (1 week) + +- Define `SandboxProvider` interface and types +- Implement `LocalProvider` (pass-through to existing `buildSpawnArgs()`) +- Implement `TemplateProvider` (wraps existing `executor_command_template`) +- Add `execution.sandbox.provider` config with `none` default +- Refactor `spawnExecutor()` to dispatch through provider +- **No user-facing change** — `provider: none` uses existing path + +### Phase 2: microsandbox Provider (1 week) + +- Implement `MsbProvider` adapter +- Worktree + `.git` mount logic +- Secret injection via `--secret` +- Network policy translation +- Integration test: run a Claude session inside an msb sandbox +- Document in `apps/agor-docs/pages/guide/sandboxing.mdx` + +### Phase 3: sbx Provider (1 week) + +- Implement `SbxProvider` adapter +- Direct mode (default): workspace mount +- Clone mode (opt-in): git-remote fetch-back lifecycle +- Integration test: run a Claude session inside an sbx sandbox +- Handle sbx's worktree limitation (clone mode doesn't work from worktrees — document, fall back to direct mode) + +### Phase 4: OpenShell Provider (1 week, blocked on NVIDIA/OpenShell#2039) + +- Implement `OpenShellProvider` adapter (gRPC client) +- Depends on OpenShell relaxing model override behavior +- Podman backend for local, k8s backend for cloud + +### Phase 5: UI + CLI Polish (3 days) + +- `agor sandbox ls/stop/rm` CLI commands +- Session panel: sandbox status badge, network policy viewer +- Config validation: `agor config set execution.sandbox.provider msb` → check `msb` is installed + +### Phase 6: Governance (future) + +- Centralized network/filesystem policies (org-level) +- Per-branch sandbox presets +- Sandbox resource quotas + +--- + +## 7. Open Questions + +1. **Sandbox lifecycle vs session lifecycle**: Should sandboxes be ephemeral (one per task, like today's executor) or persistent (one per session, reused across tasks)? Today's executor is ephemeral. sbx/msb both support persistent sandboxes. **Recommendation: start ephemeral (matches current model), add session-level reuse later.** + +2. **Daemon reachability from sandbox**: The executor inside the sandbox must connect to the daemon via WebSocket. The sandbox network policy must allow `host.docker.internal:3030` (sbx) or `host.microsandbox.internal:3030` (msb). This is a critical path — if blocked, the executor can't stream results. **Recommendation: auto-allow the daemon URL in network policy.** + +3. **Worktree path stability**: Agor worktrees live at `~/.agor/worktrees//`. Inside the sandbox, they should appear at the same absolute path (sbx does this by default). But if the sandbox uses a different home directory, the path may differ. **Recommendation: always mount at the same absolute path; this is what sbx does and it preserves error messages / config files.** + +4. **Clone-mode fetch-back integration**: If clone mode is offered, Agor needs a "fetch from sandbox" step in the task completion lifecycle. Where does this plug in? Probably after `captureGitStateForSession('end')` in `executeToolTask()`. **Recommendation: defer to Phase 3, direct mode first.** + +5. **GPU passthrough**: OpenShell and msb both mention GPU support. Should the abstraction expose GPU allocation? **Recommendation: add `gpu?: { count: number; type?: string }` to `SandboxSpec` now, implement later.** + +6. **MCP server endpoints inside sandbox**: If an agent uses MCP tools, the MCP server may run on the host. The sandbox network policy must allow access to MCP server ports. **Recommendation: auto-allow MCP server URLs registered for the session.** diff --git a/integrations/orchestrators/agor/docs/explorations/sandbox-acq-analysis.md b/integrations/orchestrators/agor/docs/explorations/sandbox-acq-analysis.md new file mode 100644 index 0000000..b80a604 --- /dev/null +++ b/integrations/orchestrators/agor/docs/explorations/sandbox-acq-analysis.md @@ -0,0 +1,351 @@ +# Using `acq` with Agor — Compatibility Analysis + +**Status:** 🔬 Exploration +**Related:** [`sandbox-abstraction.md`](./sandbox-abstraction.md) (the Agor sandbox abstraction design) +**Date:** 2026-07-13 + +--- + +## Question + +Can the notional `acq` CLI (from GSA-TTS/agentic-coding-quickstart v2) be used as the sandbox backend for Agor's executor? Specifically: + +1. Could the Agor wrapper script call `acq` instead of `msb`/`sbx` directly? +2. How would the four kits (playbook clone, Zscaler CA, git-ssh-sign, USAi provider) interact with Agor? +3. Does Agor have a better way to populate playbook/skills in a sandbox? +4. Is SSH agent forwarding handled as normal by `acq`? +5. How would configuring agents (initially OpenCode) to use USAi work — which parts are Agor's responsibility vs. the wrapper's vs. `acq`'s? + +--- + +## 1. Can the wrapper script call `acq`? + +**Yes.** Like sbx, `acq` has a shell option (`acq run shell` / `acq create shell`) that provides a raw sandbox with no pre-installed agent. Since Agor's executor owns agent setup and lifecycle, the wrapper uses the shell mode and pipes `agor-executor --stdin` inside: + +```bash +acq create shell "$WORKTREE_PATH" --name "$SANDBOX_NAME" +echo "$PAYLOAD" | acq exec "$SANDBOX_NAME" -- agor-executor --stdin +``` + +This mirrors how the wrapper uses sbx's `sbx run shell` or msb's generic `msb run ubuntu --` — the sandbox is just a box; Agor manages the agent inside it. `acq`'s agent-aware commands (`acq run opencode`) are not used. + +`acq`'s kit system is the real value — it handles the four concerns (USAi, playbook, Zscaler, git-sign) declaratively, regardless of which agent mode is used. The question is whether those concerns overlap with things Agor already handles. + +--- + +## 2. The Four Kits — How They Map to Agor + +### 2.1 `usai-provider` — Agent model provider configuration + +**What the kit does:** Drops an `opencode.jsonc` config file into the sandbox that points OpenCode at `api.gsa.usai.gov`, and merges it into OpenCode's global config at startup. The USAi API key is injected via MITM proxy (swap-on-access) — the agent never sees the key. + +**What Agor does today:** Agor's executor handles agent credentials via `installProviderConnection()` (`base-executor.ts:350`), which resolves API keys from the daemon and sets them as env vars. **But OpenCode is explicitly excluded from this path** — `isProviderConnectionTool()` returns `false` for OpenCode (`tenant-agentic-tool.ts:92`). OpenCode manages its own provider connections via its config files. + +**The conflict:** Both `acq` and Agor want to control agent configuration. For OpenCode specifically: + +| Concern | Agor's role | `acq`'s role | +|---|---|---| +| Which model to use | Sets `model_config.provider` + `model_config.model` on the session, passes to OpenCode SDK | Not involved | +| Which API endpoint | **Not handled for OpenCode** (no `baseURL` field for OpenCode) | USAi kit configures `api.gsa.usai.gov` in `opencode.jsonc` | +| API key | **Not handled for OpenCode** (OpenCode reads its own config) | MITM proxy injects key on outbound (swap-on-access) | +| API key env var | Not set for OpenCode | Kit reads `USAI_API_KEY` env var (placeholder mode) or MITM injects (swap-on-access) | + +**Recommendation:** For OpenCode + USAi, **`acq` should own the provider configuration.** Agor's executor doesn't handle OpenCode credentials, so there's no conflict. The kit's `opencode.jsonc` + MITM key injection is the right mechanism. + +**For future agents (Claude, Codex):** Agor's `installProviderConnection()` DOES handle these. If USAi support is added for Claude/Codex, the configuration would need to be coordinated: +- Agor would set `ANTHROPIC_API_KEY` / `ANTHROPIC_BASE_URL` (or `OPENAI_API_KEY` / `OPENAI_BASE_URL`) via its credential resolution +- OR `acq`'s MITM proxy would handle it (swap-on-access), and Agor would be configured to NOT install provider creds for sessions running in `acq` sandboxes + +The cleanest split: **`acq` owns network-level credential injection (MITM); Agor owns agent-level model selection.** Agor tells the agent *which* model to use; `acq` ensures the API key reaches the endpoint. This requires a config flag in Agor like `execution.sandbox.skip_provider_credential_install: true` when using sandbox mode. + +### 2.2 `agentic-coding-playbook` — Playbook clone + skill symlinks + +**What the kit does:** At sandbox startup, clones `GSA-TTS/agentic-coding-playbook` at a pinned commit into `~/.agentic-coding-playbook`, then symlinks `AGENTS.md` → `~/.config/opencode/AGENTS.md` and skills → `~/.agents/skills/`. + +**What Agor does today:** Agor doesn't place any files in agent search paths. The agent discovers `AGENTS.md` / `CLAUDE.md` from the worktree's cwd (the branch path). Agor injects its system prompt via the SDK's API, not via files. + +**Does Agor have a better way?** Potentially yes: + +1. **Mount the playbook read-only into the sandbox.** Instead of cloning inside the sandbox at startup (which requires network access + a GitHub token), the wrapper script could clone the playbook on the host and mount it read-only: + ```bash + # In the wrapper script: + PLAYBOOK_DIR="$HOME/.agor-cache/agentic-coding-playbook" + if [ ! -d "$PLAYBOOK_DIR/.git" ]; then + git clone https://github.com/GSA-TTS/agentic-coding-playbook.git "$PLAYBOOK_DIR" + fi + git -C "$PLAYBOOK_DIR" fetch && git -C "$PLAYBOOK_DIR" checkout + # Mount read-only into the sandbox + MOUNT_ARGS+=(-v "$PLAYBOOK_DIR:$PLAYBOOK_DIR:ro") + ``` + This is faster (no clone at startup), works offline, and doesn't require network egress to GitHub from inside the sandbox. + +2. **Symlink from the worktree.** If the playbook is mounted at a known path, the wrapper script could create symlinks in the worktree before mounting it: + ```bash + ln -sf "$PLAYBOOK_DIR/AGENTS.md" "$WORKTREE_PATH/AGENTS.md" + ln -sf "$PLAYBOOK_DIR/.agents/skills" "$WORKTREE_PATH/.agents/skills" + ``` + But this modifies the worktree, which may not be desirable (it shows up in `git status`). + +3. **Use Agor's MCP server.** Agor exposes itself as an MCP server. The playbook could be exposed as MCP resources, and the agent would discover them via the MCP protocol. This is the most "Agor-native" approach but requires MCP resource support in the agent (OpenCode supports this). + +**Recommendation:** **Let `acq`'s kit handle it for now.** The kit's clone-at-startup approach works, and the GitHub token is handled by `acq`'s MITM proxy. If performance (clone time) or offline use becomes a concern, the host-side clone + read-only mount approach is a straightforward optimization. Agor doesn't need to change. + +### 2.3 `zscaler-ca-certificate` — Install Zscaler Root CA + +**What the kit does:** Installs the Zscaler Root CA into the sandbox's system trust store so HTTPS works through Zscaler-intercepting proxies. Uses `--trust-host-cas` on msb (imports host CAs automatically) or file-drop + `update-ca-certificates` on sbx/ppp. + +**What Agor does today:** Nothing — this is purely a sandbox-level concern. Agor's executor runs as a process on the host, which already trusts the Zscaler CA. + +**Conflict?** None. This is entirely `acq`'s domain. The Agor wrapper script doesn't need to know about Zscaler at all. + +**Recommendation:** **`acq` owns this entirely.** No Agor changes needed. + +### 2.4 `git-ssh-sign` — Sign git commits with forwarded SSH key + +**What the kit does:** Configures git inside the sandbox to sign commits/tags with the host's SSH agent key. The SSH agent socket is forwarded into the sandbox; a signing-key-command script reads `ssh-add -L` at signing time. + +**What Agor does today:** Agor's executor authenticates git operations via HTTPS + token (per `clone-redesign.md`). SSH agent forwarding was explicitly dropped: "Agent sockets are per-Unix-session and don't transfer across `sudo -u`." The env whitelist for impersonated spawns does NOT include `SSH_AUTH_SOCK`. + +**But:** In a sandbox, there's no `sudo -u` boundary — the sandbox has its own SSH agent forwarding mechanism. `acq`'s kit handles this per-backend (sbx: SSH agent socket forwarded; msb: `msb ssh authorize`; ppp: `podman machine ssh -A`). + +**Conflict?** No, but there's a question of **which git identity** the agent uses: + +| Concern | Agor's role | `acq`'s role | +|---|---|---| +| Git auth (push/pull) | HTTPS + token via `fetchUserGitEnvironment()` | GitHub token via MITM proxy (swap-on-access) | +| Git commit signing | Not handled | SSH agent forwarding via kit | +| Git author identity | `required_user_env_vars` can enforce `GIT_AUTHOR_NAME`/`EMAIL` | Kit warns if missing | + +**Potential issue:** If both Agor and `acq` inject GitHub tokens, they could conflict. Agor injects the token via `GIT_CONFIG_COUNT`/`http.extraheader` env vars; `acq`'s MITM proxy injects `Authorization` headers on outbound HTTPS. These are different mechanisms and shouldn't conflict — Agor's env vars are for the executor process's git operations, while `acq`'s MITM is for the sandbox's outbound traffic. + +**But:** If the sandbox's git is configured to use SSH (for signing), it needs the SSH agent. If it's configured to use HTTPS (for push/pull), it needs the token. The kit handles SSH; Agor handles HTTPS tokens. They're complementary. + +**Recommendation:** **`acq` owns commit signing; Agor owns push/pull auth.** The wrapper script should ensure `SSH_AUTH_SOCK` is available to `acq` (it is — `acq` runs on the host and handles agent forwarding itself). No Agor changes needed. + +--- + +## 3. Does Agor Have a Better Way to Populate Playbook/Skills? + +**Short answer: No, and that's fine.** + +Agor's context injection is SDK-specific: +- **Claude:** System prompt via SDK's `systemPrompt` option (not file-based) +- **Codex:** Temp file with instructions (not `AGENTS.md`) +- **OpenCode:** Discovers `AGENTS.md` from the worktree cwd (no injection) +- **Gemini:** Temp file with context (not `GEMINI.md`) + +The playbook's `AGENTS.md` and skills are designed to be discovered via the agent's normal file-walking (from cwd). Agor doesn't interfere with this — it just sets the cwd to the worktree path. If the playbook is cloned into the sandbox at `~/.agentic-coding-playbook` and symlinked into the agent's search paths, the agent discovers it naturally. + +**Could Agor do better?** A future enhancement could be: +- Agor mounts a "context directory" into the sandbox at a well-known path (e.g., `/agor-context/`) +- This directory contains the playbook, skills, and any other shared context +- The wrapper script creates symlinks from the agent's search paths into this directory + +But this is an optimization, not a requirement. `acq`'s kit-based approach works today. + +--- + +## 4. Is SSH Agent Forwarding Handled as Normal by `acq`? + +**Yes.** `acq` handles SSH agent forwarding per-backend: +- **sbx:** SSH agent socket forwarded into the sandbox (built-in) +- **msb:** `msb ssh authorize --file ~/.ssh/id_ed25519.pub` registers the key; `msb ssh ` attaches with agent forwarding +- **ppp:** `podman machine ssh -A ` propagates `SSH_AUTH_SOCK` + +The Agor wrapper script doesn't need to do anything special — `acq` runs on the host where the SSH agent lives, and `acq` handles the forwarding into the sandbox. The `git-ssh-sign` kit then configures git inside the sandbox to use the forwarded agent. + +**One caveat:** Agor's executor process (running inside the sandbox) inherits the sandbox's environment. If `SSH_AUTH_SOCK` is set in the sandbox (by `acq`'s forwarding), the executor's git operations could use it. But Agor's git operations use HTTPS + token, not SSH. This shouldn't conflict, but the wrapper script should be aware that both mechanisms may be present. + +--- + +## 5. USAi Configuration — Who Owns What? + +This is the most complex question. The answer depends on the agent: + +### For OpenCode (the initial target) + +| Layer | Owner | Mechanism | +|---|---|---| +| **Which model** | Agor | `model_config.provider` + `model_config.model` on the session → passed to OpenCode SDK | +| **Which endpoint** | `acq` (usai-provider kit) | `opencode.jsonc` in sandbox configures `api.gsa.usai.gov` | +| **API key** | `acq` (MITM proxy) | Swap-on-access: agent sends request with no auth header, MITM injects `Authorization: Bearer ` | +| **Network egress** | `acq` (kit caps) | `caps.network.allow: [api.gsa.usai.gov]` → `--net-rule` flags | +| **Config file merge** | `acq` (kit commands) | `merge-global-config.mjs` runs at startup | + +**Agor's role is minimal for OpenCode:** it just tells OpenCode which model to use. `acq` handles everything else. This is clean because Agor explicitly doesn't manage OpenCode credentials. + +### For Claude/Codex (future, if USAi support is added) + +| Layer | Owner | Mechanism | +|---|---|---| +| **Which model** | Agor | `model_config.model` on the session | +| **Which endpoint** | **Conflict** — Agor sets `ANTHROPIC_BASE_URL`/`OPENAI_BASE_URL` via `installProviderConnection()`; `acq` kit would also configure the endpoint | Needs coordination | +| **API key** | **Conflict** — Agor sets `ANTHROPIC_API_KEY`/`OPENAI_API_KEY` via `installProviderConnection()`; `acq` MITM would inject on outbound | Needs coordination | +| **Network egress** | `acq` (kit caps) | Same as OpenCode | +| **Config file** | N/A (Claude/Codex don't use `opencode.jsonc`) | `acq` kit would need Claude/Codex-specific config | + +**Resolution for future agents:** Two options: + +1. **`acq` owns credentials, Agor disables credential install.** Add a config flag like `execution.sandbox.skip_provider_credential_install: true`. When running in sandbox mode, Agor's executor skips `installProviderConnection()` and lets `acq`'s MITM proxy handle API key injection. Agor still sets the model name and provider, but not the endpoint or key. + +2. **Agor owns credentials, `acq` only handles network egress.** Agor resolves the USAi API key via its existing credential resolution (user stores USAi key in Agor settings as a custom provider), and `acq` only allow-lists the USAi endpoint. No MITM injection needed. + +**Recommendation:** Option 1 is cleaner for the GSA use case. `acq`'s MITM model is specifically designed for this (swap-on-access, cross-host leak guard, per-sandbox key scoping for billing codes). Agor's credential resolution is more general-purpose. For the GSA/USAi case, `acq`'s model is a better fit. The Agor flag to skip credential install is a small, clean change. + +### The Daemon Reachability Question + +Agor's executor (inside the sandbox) must connect to the daemon (on the host) via WebSocket. The sandbox network policy must allow this. `acq`'s kit caps would need to include the daemon URL: + +```yaml +caps: + network: + allow: + - api.gsa.usai.gov + - host.microsandbox.internal:3030 # or host.docker.internal:3030 for sbx +``` + +Or the wrapper script adds this as an extra network rule: +```bash +acq create opencode "$WORKTREE_PATH" --name "$SANDBOX_NAME" \ + --extra-net-rule "allow@host.microsandbox.internal:3030" +``` + +**This is a wrapper-script concern, not an `acq` or Agor concern.** The wrapper script knows the daemon URL and adds it to the sandbox's allow-list. + +--- + +## 6. Revised Wrapper Script Using `acq` + +```bash +#!/bin/bash +# ~/.agor/sandbox-wrapper-acq.sh +# Uses acq as the sandbox backend. + +set -euo pipefail +SESSION_ID="$1" +SANDBOX_NAME="agor-${SESSION_ID:0:8}" +DAEMON_URL="${AGOR_DAEMON_URL:-http://localhost:3030}" + +# Buffer stdin (JSON payload) +PAYLOAD=$(cat) +WORKTREE_PATH=$(echo "$PAYLOAD" | jq -r '.params.cwd') + +# Discover main repo .git path from the worktree's .git file +if [ -f "$WORKTREE_PATH/.git" ]; then + GITDIR_LINE=$(cat "$WORKTREE_PATH/.git") + MAIN_GIT=$(echo "$GITDIR_LINE" | sed 's|gitdir: \(.*\)/worktrees/.*|\1|') + # For acq, we pass the workspace path — acq handles mounting + # But we also need the .git dir. acq's kit system doesn't know about + # Agor's worktree structure, so we use --extra-mount + EXTRA_MOUNTS="--extra-mount $MAIN_GIT:$MAIN_GIT:rw" +elif [ -d "$WORKTREE_PATH/.git" ]; then + EXTRA_MOUNTS="" # Clone mode: self-contained +fi + +# Create sandbox using acq (applies the four pinned kits automatically) +# The workspace is the worktree path; acq mounts it +acq create opencode "$WORKTREE_PATH" \ + --name "$SANDBOX_NAME" \ + $EXTRA_MOUNTS \ + --extra-net-rule "allow@host.microsandbox.internal:3030" + +# Cleanup on exit +trap "acq rm --force $SANDBOX_NAME 2>/dev/null" EXIT + +# Pipe JSON payload to agor-executor inside the sandbox +echo "$PAYLOAD" | acq exec "$SANDBOX_NAME" -- agor-executor --stdin +``` + +**Key differences from the raw `msb` wrapper:** +1. `acq create` handles kit application (USAi, playbook, Zscaler, git-sign) automatically +2. The wrapper doesn't need to handle Zscaler certs, SSH agent forwarding, or USAi config files — `acq` + kits do that +3. The wrapper still handles the Agor-specific concerns: discovering the `.git` path from the worktree, mounting it, allowing daemon access, piping the executor payload +4. `acq` is worktree-unaware — the wrapper tells it what to mount, and `acq` mounts at the same host path +5. The USAi API key is fetched from Agor's credential resolution and passed to `acq` per-sandbox — `acq` does NOT own key rotation + +--- + +## 7. Summary — Who Owns What + +| Concern | Agor | Wrapper Script | `acq` + Kits | +|---|---|---|---| +| Agent model selection | ✅ `model_config` | | | +| Agent credentials (OpenCode) | ❌ (not handled) | | ✅ MITM swap-on-access | +| Agent credentials (Claude/Codex) | ✅ `installProviderConnection()` | | ⚠️ Needs coordination (skip flag) | +| Worktree mount | | ✅ Discover from `.git` file | | +| Main repo `.git` mount | | ✅ Mount rw | | +| Daemon network access | | ✅ `--extra-net-rule` | | +| USAi API key storage & rotation | ✅ User profiles (encrypted) | | | +| USAi API key injection | | ✅ Fetch from Agor, pass to `acq` | ✅ MITM proxy (per-sandbox secret) | +| USAi endpoint config | | | ✅ `usai-provider` kit | +| Playbook/skills | | | ✅ `agentic-coding-playbook` kit | +| Zscaler CA | | | ✅ `zscaler-ca-certificate` kit | +| Git commit signing | | | ✅ `git-ssh-sign` kit | +| Git push/pull auth | ✅ HTTPS + token | | (complementary — MITM handles GH token) | +| Sandbox lifecycle | | ✅ Create/cleanup | ✅ `acq create`/`acq rm` | +| Executor process | ✅ `agor-executor --stdin` | ✅ Pipe payload | | + +**Bottom line:** `acq` is usable with Agor. The wrapper script calls `acq create` + `acq exec` instead of `msb`/`sbx` directly. `acq` is worktree-unaware — the wrapper discovers what to mount and tells `acq`. `acq`'s kits handle the GSA-specific concerns (USAi config, Zscaler, playbook, git-sign). Agor owns API key storage/rotation (per-user, per-project) and agent model selection. The wrapper fetches the resolved key from Agor and passes it to `acq` as a per-sandbox secret. `acq` does NOT own key rotation. The only Agor code change needed for future agents (Claude/Codex) is a flag to skip `installProviderConnection()` in sandbox mode; for OpenCode, no Agor changes are needed at all. + +--- + +## 8. Open Questions — Resolved + +### Q1: Does `acq` support extra mounts and network rules? + +**Resolved.** `acq` builds on the existing multi-mount facility in sbx (and equivalent in msb/ppp). The wrapper script tells `acq` what directories to mount, each at the same absolute path as on the host. `acq` is **worktree-unaware** — it doesn't know about git worktrees, `.git` pointer files, or shared object stores. The wrapper script is responsible for understanding worktree structure and telling `acq` exactly which directories to mount: + +```bash +# Wrapper tells acq: mount the worktree as workspace, and also mount .git +acq create opencode "$WORKTREE_PATH" \ + --name "$SANDBOX_NAME" \ + --mount "$MAIN_GIT:$MAIN_GIT:rw" \ + --net-rule "allow@host.microsandbox.internal:3030" +``` + +This is the right split: `acq` is a general-purpose sandbox tool; the wrapper is Agor-specific and knows git worktree mechanics. + +### Q2: How does `acq` handle the worktree `.git` file? + +**Resolved by Q1.** The wrapper script reads the worktree's `.git` file to discover the main repo's `.git` path, then tells `acq` to mount both. `acq` doesn't need to understand the `.git` pointer — it just mounts the two directories at their host paths. Inside the sandbox, the `.git` file's `gitdir:` target resolves correctly because both paths are mounted. + +### Q3: USAi per-sandbox keys + Agor sessions — billing codes + +**Resolved.** The model is: + +- **One Agor "user" per project's USAi API key.** When a board is created for a project, that project user is added. Sessions on that board are owned by the project user; other users have view/read-write access. +- **For local dev** (unrelated to a team/project), the user has their own USAi API key stored in their Agor profile, used for sessions they start (e.g., on a private board). + +This maps cleanly to `acq`'s per-sandbox secret model: + +1. **Agor resolves which USAi key to use** based on the session's owning user (project user or personal user). +2. **The wrapper script fetches the key** from Agor's credential resolution (via the daemon API or the executor payload) and sets it as a per-sandbox secret via `acq secret set usai --host api.gsa.usai.gov --sandbox `. +3. **`acq`'s MITM proxy** injects the key on outbound requests. The agent never sees it. + +**Key implication: `acq` should NOT own API key rotation.** API keys are managed alongside users in Agor (user settings, encrypted at rest). `acq`'s `usai-rotate-api-key` command is not used in the Agor integration — rotation happens in Agor's user settings, and the wrapper script picks up the new key on the next session spawn. The `acq secret set` call is per-session-creation, not a one-time global setup. + +**Revised credential flow:** + +``` +Agor user profile (encrypted USAi key) + ↓ Agor credential resolution (per session, based on owning user) + ↓ Executor payload or daemon API response + ↓ Wrapper script extracts key + ↓ acq secret set usai --host api.gsa.usai.gov --sandbox + ↓ acq MITM proxy injects on outbound + ↓ Agent sends request (no auth header) → MITM adds Authorization → USAi +``` + +This means: +- **Agor owns** API key storage, rotation, and per-user/per-project attribution +- **The wrapper owns** fetching the key from Agor and passing it to `acq` +- **`acq` owns** the injection mechanism (MITM swap-on-access) +- **The agent** never sees the key + +### Q4: OpenCode server URL + +**Resolved.** `agor-executor` (running inside the sandbox via `acq exec`) starts the OpenCode server. `acq`'s `opencode run` is NOT used — `acq` just provides the sandbox. The `usai-provider` kit configures OpenCode's config files (provider, endpoint), but the actual server lifecycle is managed by Agor's executor. + +### Q5: MCP server access + +**Deferred.** Agor's internal MCP server (`daemon:3030/mcp`) is handled by the wrapper's `--net-rule` for daemon access. External MCP servers would need to be added to the sandbox's network policy, but this is a future concern — the initial integration only needs daemon + USAi + GitHub (for playbook clone) egress. From 165199acae2db2c7351fbd3cba596f4a56743f77 Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Sun, 26 Jul 2026 06:51:08 +0000 Subject: [PATCH 02/11] docs(orchestrators): add area ADR + README for orchestrator integrations Establish integrations/orchestrators/ as a new integration class for tools that DRIVE a sandbox/isolation tool from the outside (own the agent+session lifecycle, call acq/sbx/msb), as distinct from isolation kits that acq APPLIES inside a sandbox. Direction of control decides the area. - Area ADR 0001 (proposed): rationale, drives-vs-applied boundary rule, composition corollary (an orchestrator that needs a kit references one under isolation/acq-kits/), mirrors the isolation area ADR. - orchestrators/README.md: boundary rule + what belongs here + index. - integrations/README.md: new area in 'What belongs here', layout, and an Available-integrations row for orchestrators/agor. ADR status is 'proposed' pending human confirmation (drafted AFK via the wayfinder map). Refs GSA-TTS/agentic-coding-patterns#250, #255. Co-authored-by: OpenCode Agent (cherry picked from commit 408ad4e088ca82b08f48957b0b933f12f5e8c97b) --- integrations/README.md | 15 +- integrations/orchestrators/README.md | 59 +++++++ .../0001-orchestrators-area-and-agor-acq.md | 153 ++++++++++++++++++ 3 files changed, 224 insertions(+), 3 deletions(-) create mode 100644 integrations/orchestrators/README.md create mode 100644 integrations/orchestrators/docs/decisions/0001-orchestrators-area-and-agor-acq.md diff --git a/integrations/README.md b/integrations/README.md index 5536458..fb2c67c 100644 --- a/integrations/README.md +++ b/integrations/README.md @@ -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//`) — 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. @@ -38,9 +44,11 @@ integrations/ │ └── / │ ├── README.md # setup guide │ └── # portable config to copy into your project -└── isolation/ - └── sbx-kits/ - └── / # an sbx mixin kit (spec.yaml + files/ + docs) +├── isolation/ +│ └── acq-kits/ +│ └── / # an acq mixin kit (spec.yaml + files/ + docs) +└── orchestrators/ + └── / # a tool that DRIVES acq/sbx/msb (wrapper + setup guide) ``` ## Rules @@ -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.** | diff --git a/integrations/orchestrators/README.md b/integrations/orchestrators/README.md new file mode 100644 index 0000000..b2938f1 --- /dev/null +++ b/integrations/orchestrators/README.md @@ -0,0 +1,59 @@ +# Orchestrator integrations + +Integration guides and portable configs for **orchestrators that drive a +sandbox/isolation tool from the outside** — tools that own the agent + session +lifecycle and *call* `acq`/`sbx`/`msb` (rather than being applied *inside* a +sandbox). + +See [ADR 0001](docs/decisions/0001-orchestrators-area-and-agor-acq.md) for why +this area exists, and the repo-wide +[integrations ADR](../../docs/decisions/0001-integrations-area.md) for the +`integrations/` area overall. + +## Orchestrator vs. isolation kit — the boundary + +> An **orchestrator** integration *drives* a sandbox/isolation tool from the +> outside — it owns the agent + session lifecycle and calls `acq`/`sbx`/`msb` +> (e.g. Agor invoking `acq create` / `acq exec`). An **isolation kit** is +> something `acq` *applies inside* the sandbox (`caps`/`files`/`commands`). +> Direction of control decides the area: **drives → `orchestrators/`; +> applied-inside → [`isolation/`](../isolation/).** + +**Composition corollary.** When an orchestrator integration *needs* a kit +(e.g. an egress allow-list kit for the orchestrator's control-plane URL), that +kit lives under [`integrations/isolation/acq-kits/`](../isolation/acq-kits/) — it +is applied inside — and the orchestrator here **references** it. The two areas +compose; direction of control, not owning project, decides placement. + +## What belongs here + +- **Orchestrator integrations** (`/`) — a portable wrapper/config + plus its setup guide, so a contributor can wire their own deployment to run an + orchestrator's execution inside a sandbox. + +These are **reusable, tool-specific, and community-shareable**. They carry **no +compliance authority** and are **not** federal policy — behavioral and policy +authority lives in the +[playbook](https://github.com/GSA-TTS/agentic-coding-playbook) and is referenced, +not restated. + +## What does NOT belong here + +- Kits that `acq` applies inside a sandbox → [`../isolation/acq-kits/`](../isolation/acq-kits/). +- Editor integrations → [`../editors/`](../editors/). +- Executable agent procedures → `skills/` (skills), not integrations. +- Federal policy / compliance / NIST content → the playbook. + +## Available orchestrators + +| Orchestrator | Drives | Description | +|--------------|--------|-------------| +| [agor](agor/) | `acq` | Run [Agor](https://github.com/preset-io/agor)'s executor inside an `acq` sandbox via `executor_command_template` — a portable wrapper that mounts the branch worktree, allow-lists the daemon, and pipes `agor-executor --stdin` into the sandbox. **v1: sbx backend.** | + +## Rules + +- No secrets, credentials, PII, CUI, internal URLs, or customer data in any + integration file or example. +- Keep configs portable and minimal; document prerequisites in each + integration's `README.md`. +- Identify and preserve the license of any inherited third-party material. diff --git a/integrations/orchestrators/docs/decisions/0001-orchestrators-area-and-agor-acq.md b/integrations/orchestrators/docs/decisions/0001-orchestrators-area-and-agor-acq.md new file mode 100644 index 0000000..8a78eb0 --- /dev/null +++ b/integrations/orchestrators/docs/decisions/0001-orchestrators-area-and-agor-acq.md @@ -0,0 +1,153 @@ +--- +title: "Add an integrations/orchestrators/ area; place the Agor + acq integration there" +status: "proposed" +date: "2026-07-26" +decision_makers: ["Bret Mogilefsky", "OpenCode Agent"] +category: "repository-structure" +impact_level: "low" +--- + +# ADR 0001 (orchestrators) — Add `integrations/orchestrators/` and place Agor + acq there + +> Area-scoped ADR for `integrations/orchestrators/`. The repository-wide ADR log +> is [`docs/decisions/`](../../../docs/decisions/); the area that established +> `integrations/` is +> [`docs/decisions/0001-integrations-area.md`](../../../docs/decisions/0001-integrations-area.md). +> This record governs the whole orchestrators integration area, so it sits here +> between the two — mirroring how `integrations/isolation/` carries its own +> [area ADR](../../isolation/docs/decisions/0001-neutral-hybrid-v1-acq-kits.md). + +> **Status: proposed.** Drafted AFK via the `wayfinder` map +> ([#247](https://github.com/GSA-TTS/agentic-coding-patterns/issues/247), +> ticket [#255](https://github.com/GSA-TTS/agentic-coding-patterns/issues/255)); +> pending human confirmation before it is marked `accepted`. + +## Context and Problem Statement + +`integrations/` (per the repo-wide +[ADR 0001](../../../docs/decisions/0001-integrations-area.md)) holds tool/editor +integration guides and portable configs. Its first two classes are +`editors/` and `isolation/` (the `acq`/`sbx` mixin kits). + +A new kind of integration has arrived that fits neither: **an orchestrator that +*drives* an isolation tool from the outside.** Concretely, [Agor](https://github.com/preset-io/agor) +— a multiplayer agent orchestrator — can run its executor inside an +[`acq`](https://github.com/GSA-TTS/agentic-coding-quickstart) sandbox by pointing +its `executor_command_template` at a wrapper script that calls `acq create` / +`acq exec`. This integration: + +- is **not an isolation kit** — it is not a `caps`/`files`/`commands` payload that + `acq` *applies inside* a sandbox; it is code that *calls* `acq` from outside and + owns the agent + session lifecycle; +- is **not an editor** integration; +- is **not a skill/prompt/workflow** (not an executable agent procedure); +- but **is** reusable, tool-specific, community-shareable, and carries no + compliance authority — exactly the `integrations/` mission. + +Without a home, this material would be misfiled into `isolation/` (wrong — it +consumes kits, it is not one) or `examples/` (wrong — it is a maintained portable +integration, not an illustration). + +## Decision Drivers + +- The `integrations/` taxonomy groups by **integration class first, then tool** + (established in the repo-wide integrations ADR). +- The distinction between "drives a sandbox tool" and "is applied inside a + sandbox" is real and recurring (other orchestrators — CI runners, other agent + platforms — could drive `acq` too). +- Keep `isolation/` cohesive: it is the home of the neutral `hybrid/v1` kits, not + of their consumers. +- Avoid forcing a consumer-of-kits into the kit taxonomy. + +## Considered Options + +1. **Add a top-level `integrations/orchestrators/` area** with per-orchestrator + subfolders; place Agor+acq at `integrations/orchestrators/agor/`. *(chosen)* +2. **Put it under `integrations/isolation/`** — conflates a kit *consumer* with + the kits; pollutes the isolation area's cohesive kit taxonomy. +3. **Put it under `examples/`** — examples are illustrative, not maintained + portable integrations meant to be adopted. +4. **Put it in the Agor repo only** — loses the community-shareable, cross-tool + value and the `-patterns` audience; the integration is deliberately vendor- + neutral on the `acq` side. + +## Decision + +Adopt **Option 1**. + +- **New area** `integrations/orchestrators/`, grouped by integration class + (orchestrator), then by tool (`agor/`). +- **The boundary rule** (also added to `integrations/README.md`): + + > An **orchestrator** integration *drives* a sandbox/isolation tool from the + > outside — it owns the agent + session lifecycle and calls `acq`/`sbx`/`msb` + > (e.g. Agor invoking `acq create` / `acq exec`). An **isolation kit** is + > something `acq` *applies inside* the sandbox (`caps`/`files`/`commands`). + > Direction of control decides the area: **drives → `orchestrators/`; + > applied-inside → `isolation/`.** + +- **Composition corollary.** When an orchestrator integration *needs* a kit + (e.g. the `agor-daemon-egress` kit that allow-lists the daemon URL — + [#259](https://github.com/GSA-TTS/agentic-coding-patterns/issues/259)), that + kit lives under `integrations/isolation/acq-kits/` (it is applied inside), and + the orchestrator **references** it. The two areas compose; the artifact's + direction of control — not its owning project — decides where it lives. + +- **Area README.** `integrations/orchestrators/README.md` documents what belongs + here, the boundary rule, and an index — mirroring + `integrations/isolation/acq-kits/README.md`. A row is also added to the + top-level `integrations/README.md` "Available integrations" table, and the + `orchestrators/` class is named in its "What belongs here" list. + +- **Structure:** + + ```text + integrations/orchestrators/ + ├── README.md # what belongs here + boundary rule + index + ├── docs/decisions/ # area ADRs (this file) + └── agor/ + ├── README.md # setup guide (executor_command_template, who-owns-what) + ├── sandbox-wrapper-acq.sh # portable wrapper the operator adopts + └── docs/explorations/ # source design docs (sandbox-abstraction, sandbox-acq-analysis) + ``` + +## Consequences + +**Positive** + +- Gives orchestrator-class integrations a correct, discoverable home and a clear + boundary against isolation kits. +- Keeps `isolation/` focused on kits; keeps consumers-of-kits out of the kit + taxonomy. +- Establishes a reusable rule for future orchestrators that drive `acq`. + +**Negative / trade-offs** + +- One more top-level area under `integrations/` to document and index. Mitigated + by the area README + this ADR + the top-level README row. + +**Neutral** + +- The Agor+acq integration itself carries **no compliance authority** and is not + federal policy; behavioral/policy authority stays in the playbook and is + referenced, not restated (consistent with `docs/contribution-scope.md`). +- v1 of the Agor+acq integration is scoped to the **sbx** backend (see the + wrapper's own notes / map ticket + [#251](https://github.com/GSA-TTS/agentic-coding-patterns/issues/251)); msb + support and Agor-core changes are tracked separately and are out of scope here. + +## Links + +- Repo-wide area ADR that established `integrations/`: + [`docs/decisions/0001-integrations-area.md`](../../../docs/decisions/0001-integrations-area.md). +- Sibling area ADR (isolation kits): + [`../../isolation/docs/decisions/0001-neutral-hybrid-v1-acq-kits.md`](../../isolation/docs/decisions/0001-neutral-hybrid-v1-acq-kits.md). +- Contribution scope: + [`docs/contribution-scope.md`](../../../docs/contribution-scope.md). +- Wayfinder map: + [#247](https://github.com/GSA-TTS/agentic-coding-patterns/issues/247); + boundary decision + [#250](https://github.com/GSA-TTS/agentic-coding-patterns/issues/250). +- Source explorations: + [`../agor/docs/explorations/sandbox-abstraction.md`](../agor/docs/explorations/sandbox-abstraction.md), + [`../agor/docs/explorations/sandbox-acq-analysis.md`](../agor/docs/explorations/sandbox-acq-analysis.md). From d988f089b94c4a4ca663a406d8e7252e1c04c44d Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Sun, 26 Jul 2026 06:53:12 +0000 Subject: [PATCH 03/11] feat(orchestrators): add DRAFT Agor+acq executor wrapper (sbx, v1) Add sandbox-wrapper-acq.sh: the Agor executor_command_template target that runs agor-executor inside an acq sandbox. - Buffers the stdin JSON payload (mktemp + trap cleanup; payload may carry a session JWT so it is always removed on exit). - Derives mounts from the worktree's own .git with zero daemon calls: worktree mode reads the gitdir pointer to mount the main repo's .git; clone mode mounts the self-contained clone dir only. - v1 safety gate: refuses to mount a LOCAL repo's checkout (would expose the user's working tree/.env); supports Agor-managed remote repos (~/.agor/*) and clone-mode branches only (map #251). - Applies the daemon-egress kit via --kit (acq has no --net-rule; map #259), provisions the per-sandbox secret on stdin (out-of-band, not vended by Agor today; map #252), and pipes the payload to . - Honors a dry-run PRINCIPLE via AGOR_SANDBOX_DRY_RUN=1 (prints the planned acq commands and exits) since the script's job is inherently to mutate and Agor always invokes it live; deviation documented inline per docs/clean-script-standard.md. Authored to the clean-script standard (strict mode, quoted expansions, mktemp + trap, usage, no curl|sh / eval / secret dumps). bash -n clean; four dry-run cases (clone, agor-worktree, local-repo refusal, missing cwd) verified by hand. DRAFT pending human review + live end-to-end validation (map #253, #257). Co-authored-by: OpenCode Agent (cherry picked from commit 578b79137929062591fd16b2c322e8cba43a47dc) --- .../orchestrators/agor/sandbox-wrapper-acq.sh | 241 ++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100755 integrations/orchestrators/agor/sandbox-wrapper-acq.sh diff --git a/integrations/orchestrators/agor/sandbox-wrapper-acq.sh b/integrations/orchestrators/agor/sandbox-wrapper-acq.sh new file mode 100755 index 0000000..8cad053 --- /dev/null +++ b/integrations/orchestrators/agor/sandbox-wrapper-acq.sh @@ -0,0 +1,241 @@ +#!/usr/bin/env bash +# +# sandbox-wrapper-acq.sh — run an Agor executor task inside an `acq` sandbox. +# +# STATUS: DRAFT (v1, sbx backend). Authored AFK via the wayfinder map +# (GSA-TTS/agentic-coding-patterns#247, prototype ticket #253). Not yet +# live-validated end to end — see the map's #257. Read before adopting. +# +# WHAT IT IS +# Agor's daemon spawns an executor per task by running its configured +# `executor_command_template` via `sh -c`, substituting a few variables and +# piping a JSON payload to the process's stdin. This script is that template +# target: it reads the payload, works out what to mount, creates an `acq` +# sandbox, and pipes the payload into `agor-executor --stdin` INSIDE the +# sandbox. The sandbox replaces `sudo -u` as the isolation boundary. +# +# Wire it in ~/.agor/config.yaml: +# execution: +# executor_command_template: | +# /path/to/sandbox-wrapper-acq.sh {session_id} +# +# DRY-RUN NOTE (deviation from the repo clean-script standard, documented) +# This script's whole job is to MUTATE (create a sandbox, run the agent), and +# Agor always invokes it for real — so a `--apply`-gated default that no-ops +# would break the executor. Instead it honors the dry-run PRINCIPLE via an +# explicit opt-in preview: set AGOR_SANDBOX_DRY_RUN=1 to print the acq commands +# it WOULD run (mounts, egress kit, secret, exec) and exit 0 without creating a +# sandbox. Operators should run it once in dry-run against a real payload +# before wiring it live. See docs/clean-script-standard.md. +# +# SCOPE (v1) +# - Backend: sbx only. `acq`'s msb adapter mounts at a FIXED guest path +# (/home/agent/workspace), not the host path, which breaks the worktree +# `.git` pointer and Agor's same-absolute-path assumption. msb is tracked +# as a gap (map #260). +# - Daemon egress is allow-listed via a small acq kit, NOT a flag (acq has no +# --net-rule); see AGOR_EGRESS_KIT below and map #259. +# - USAi key: provisioned to acq out-of-band by the operator (map #252). Agor +# does not vend a USAi key to the sandbox today (#261). + +set -euo pipefail +IFS=$'\n\t' + +# -------------------------------------------------------------------------- +# Config (environment, with safe defaults). None of these are secrets. +# -------------------------------------------------------------------------- +: "${AGOR_ACQ_BIN:=acq}" # acq CLI on PATH +: "${AGOR_ACQ_AGENT:=shell}" # raw sandbox; Agor owns the agent SDK +: "${AGOR_SANDBOX_PREFIX:=agor-}" # sandbox name prefix +: "${AGOR_SANDBOX_DRY_RUN:=0}" # 1 = print planned acq commands, don't run +: "${AGOR_EGRESS_KIT:=}" # acq kit ref that allow-lists the daemon + # (local dir or git+https #ref=&dir=); + # see integrations/isolation/acq-kits/agor-daemon-egress +: "${AGOR_USAI_SECRET:=1}" # 1 = set the per-sandbox `usai` acq secret +: "${AGOR_USAI_KEY_FILE:=}" # optional file the operator populates with + # the USAi key; piped to `acq secret set` + +usage() { + cat >&2 <<'EOF' +Usage: sandbox-wrapper-acq.sh + + Reads the Agor executor JSON payload on stdin, creates an `acq` sandbox with + the branch worktree mounted, allow-lists the daemon, and runs + `agor-executor --stdin` inside it. + + Intended as an Agor executor_command_template target: + executor_command_template: | + /path/to/sandbox-wrapper-acq.sh {session_id} + +Env (all optional; none are secrets): + AGOR_ACQ_BIN acq binary (default: acq) + AGOR_ACQ_AGENT acq agent mode (default: shell) + AGOR_SANDBOX_PREFIX sandbox name prefix (default: agor-) + AGOR_SANDBOX_DRY_RUN 1 = print the acq commands and exit without creating a sandbox + AGOR_EGRESS_KIT acq kit ref allow-listing the daemon (local dir or git+https) + AGOR_USAI_SECRET 1 = provision the per-sandbox `usai` acq secret (default: 1) + AGOR_USAI_KEY_FILE file holding the USAi key to pipe to `acq secret set` +EOF +} + +# -------------------------------------------------------------------------- +# Args +# -------------------------------------------------------------------------- +if [[ $# -ne 1 || "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then + usage + [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]] && exit 0 + exit 2 +fi +SESSION_ID="$1" + +# -------------------------------------------------------------------------- +# Preconditions +# -------------------------------------------------------------------------- +command -v jq >/dev/null 2>&1 || { + echo "ERROR: jq is required to parse the executor payload" >&2 + exit 3 +} +command -v "${AGOR_ACQ_BIN}" >/dev/null 2>&1 || { + echo "ERROR: acq binary not found: ${AGOR_ACQ_BIN}" >&2 + exit 3 +} + +# Sandbox name: prefix + first 8 chars of the session id (matches the guides). +SANDBOX_NAME="${AGOR_SANDBOX_PREFIX}${SESSION_ID:0:8}" + +# -------------------------------------------------------------------------- +# Buffer stdin (the JSON payload) so we can BOTH parse it and pipe it onward. +# The payload is written to a mktemp file; the trap removes it on any exit. +# -------------------------------------------------------------------------- +PAYLOAD_FILE="$(mktemp)" +SANDBOX_CREATED=0 +cleanup() { + # Remove the payload temp file (may contain a session JWT — never leave it). + [[ -n "${PAYLOAD_FILE}" && -f "${PAYLOAD_FILE}" ]] && rm -f "${PAYLOAD_FILE}" + # Tear the sandbox down if we created one (best effort). acq rm is already + # force; do NOT pass --force (acq would misparse it as the sandbox name). + if [[ "${SANDBOX_CREATED}" -eq 1 ]]; then + "${AGOR_ACQ_BIN}" rm "${SANDBOX_NAME}" >/dev/null 2>&1 || true + fi +} +trap cleanup EXIT INT TERM + +cat >"${PAYLOAD_FILE}" + +WORKTREE_PATH="$(jq -r '.params.cwd // empty' <"${PAYLOAD_FILE}")" +if [[ -z "${WORKTREE_PATH}" ]]; then + echo "ERROR: payload has no params.cwd (worktree path)" >&2 + exit 4 +fi +if [[ ! -e "${WORKTREE_PATH}/.git" ]]; then + echo "ERROR: ${WORKTREE_PATH} is not a git workspace (.git missing)" >&2 + exit 4 +fi + +# -------------------------------------------------------------------------- +# Work out the mount set from the worktree's own .git (zero daemon calls). +# +# worktree mode: .git is a FILE containing "gitdir:
/.git/worktrees/" +# -> mount the worktree + the main repo dir so the pointer resolves. +# (v1: for Agor-managed REMOTE repos the main dir is a clean clone with +# no user secrets. For LOCAL repos, mounting the main parent would expose +# the user's working tree — the wrapper refuses; use clone-mode branches. +# The exact .git-only hiding mechanism is an open prototype question, +# map #251/#253.) +# clone mode: .git is a DIRECTORY (self-contained) -> mount just the clone dir. +# +# On sbx, extra mounts are positional workspace paths mounted at their ABSOLUTE +# HOST path (there is no --mount flag; see map #248). We can't bind only `.git` +# without its parent, so we mount whole directories. +# -------------------------------------------------------------------------- +POSITIONAL_MOUNTS=("${WORKTREE_PATH}") + +if [[ -f "${WORKTREE_PATH}/.git" ]]; then + # Worktree mode: derive
/.git from the gitdir pointer. + gitdir_line="$(cat "${WORKTREE_PATH}/.git")" + # "gitdir: /path/to/main/.git/worktrees/" -> "/path/to/main/.git" + main_git="${gitdir_line#gitdir: }" + main_git="${main_git%%/worktrees/*}" + if [[ -z "${main_git}" || ! -d "${main_git}" ]]; then + echo "ERROR: could not resolve main .git from worktree pointer: ${gitdir_line}" >&2 + exit 4 + fi + main_repo_dir="${main_git%/.git}" + + # v1 safety gate: refuse to mount a LOCAL repo's parent checkout, which would + # expose the user's working tree / .env. Heuristic: Agor-managed remote clones + # live under ~/.agor/ (repos/ or worktrees/). Anything else is treated as a + # local repo and refused, per map #251. + case "${main_repo_dir}" in + "${HOME}"/.agor/* | /root/.agor/*) + # Agor-managed clean clone: safe to mount the main .git's parent. + POSITIONAL_MOUNTS+=("${main_git}") + ;; + *) + echo "ERROR: refusing to mount a local repo's checkout (${main_repo_dir})." >&2 + echo " v1 supports Agor-managed remote repos or clone-mode branches only;" >&2 + echo " see map #251 (mount strategy)." >&2 + exit 5 + ;; + esac +elif [[ -d "${WORKTREE_PATH}/.git" ]]; then + : # Clone mode: self-contained .git; the worktree mount alone is enough. +fi + +# -------------------------------------------------------------------------- +# Assemble the acq create argv. Agent positional FIRST, then workspace(s). +# The egress kit (if provided) is applied via --kit (repeatable). +# -------------------------------------------------------------------------- +create_args=("create" "${AGOR_ACQ_AGENT}") +for m in "${POSITIONAL_MOUNTS[@]}"; do + create_args+=("${m}") +done +create_args+=("--name" "${SANDBOX_NAME}") +if [[ -n "${AGOR_EGRESS_KIT}" ]]; then + create_args+=("--kit" "${AGOR_EGRESS_KIT}") +else + echo "WARNING: AGOR_EGRESS_KIT is unset — the sandbox may not reach the daemon." >&2 + echo " Provide the agor-daemon-egress kit ref (see map #259)." >&2 +fi + +# -------------------------------------------------------------------------- +# Dry-run: print the plan and exit without touching acq. +# -------------------------------------------------------------------------- +if [[ "${AGOR_SANDBOX_DRY_RUN}" -eq 1 ]]; then + echo "[dry-run] worktree: ${WORKTREE_PATH}" + echo "[dry-run] mounts: ${POSITIONAL_MOUNTS[*]}" + echo "[dry-run] ${AGOR_ACQ_BIN} ${create_args[*]}" + if [[ "${AGOR_USAI_SECRET}" -eq 1 ]]; then + echo "[dry-run] ${AGOR_ACQ_BIN} secret set ${SANDBOX_NAME} usai (key piped on stdin)" + fi + echo "[dry-run] | ${AGOR_ACQ_BIN} exec ${SANDBOX_NAME} -- agor-executor --stdin" + echo "[dry-run] ${AGOR_ACQ_BIN} rm ${SANDBOX_NAME} (on exit)" + exit 0 +fi + +# -------------------------------------------------------------------------- +# Create the sandbox. +# -------------------------------------------------------------------------- +"${AGOR_ACQ_BIN}" "${create_args[@]}" +SANDBOX_CREATED=1 + +# -------------------------------------------------------------------------- +# Provision the per-sandbox USAi secret (out-of-band; not fetched from Agor — +# map #252). The key is piped on stdin so it never appears in argv/process list. +# -------------------------------------------------------------------------- +if [[ "${AGOR_USAI_SECRET}" -eq 1 ]]; then + if [[ -n "${AGOR_USAI_KEY_FILE}" && -r "${AGOR_USAI_KEY_FILE}" ]]; then + "${AGOR_ACQ_BIN}" secret set "${SANDBOX_NAME}" usai <"${AGOR_USAI_KEY_FILE}" || + echo "WARNING: 'acq secret set ${SANDBOX_NAME} usai' failed; USAi calls may fail." >&2 + else + echo "NOTE: AGOR_USAI_KEY_FILE unset/unreadable; skipping per-sandbox USAi secret." >&2 + echo " Provide it, or set a global secret once: acq secret set -g usai" >&2 + fi +fi + +# -------------------------------------------------------------------------- +# Run the executor inside the sandbox, piping the buffered payload to its stdin. +# The executor connects back to the daemon over WebSocket using the payload's +# sessionToken; the egress kit must allow that route. +# -------------------------------------------------------------------------- +"${AGOR_ACQ_BIN}" exec "${SANDBOX_NAME}" -- agor-executor --stdin <"${PAYLOAD_FILE}" From 6eac884ea0289b3169d48e4d8d590dc03c5eda28 Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Sun, 26 Jul 2026 06:55:32 +0000 Subject: [PATCH 04/11] docs(orchestrators): add Agor+acq integration setup guide (DRAFT) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add integrations/orchestrators/agor/README.md: the copy-into-your-project setup guide for running Agor's executor inside an acq sandbox. Covers how-it-works (executor_command_template → wrapper → acq exec), the executor_command_template config, prerequisites, a dry-run-first setup, the env-var configuration table, the mount strategy (worktree .git derivation + the v1 local-repo refusal gate and the .git-only-staging alternative), residual risks, daemon reachability via the agor-daemon-egress kit (no acq --net-rule), the out-of-band USAi credential flow, a who-owns-what table, backend support (sbx v1; msb/ppp gaps), and the scope/no-compliance-authority disclaimer. Status DRAFT pending human review + live validation (map #254, #257). Local link targets verified to exist. Co-authored-by: OpenCode Agent (cherry picked from commit 83b8dd268f14c16ec64a485e8860973a9cd57453) --- integrations/orchestrators/agor/README.md | 234 ++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 integrations/orchestrators/agor/README.md diff --git a/integrations/orchestrators/agor/README.md b/integrations/orchestrators/agor/README.md new file mode 100644 index 0000000..067aef6 --- /dev/null +++ b/integrations/orchestrators/agor/README.md @@ -0,0 +1,234 @@ +# Agor + `acq` — run the executor inside a sandbox + +> **Status: DRAFT (v1, sbx backend).** Authored via the wayfinder map +> ([#247](https://github.com/GSA-TTS/agentic-coding-patterns/issues/247)); the +> wrapper has **not** yet been live-validated end to end (map +> [#257](https://github.com/GSA-TTS/agentic-coding-patterns/issues/257)). Read +> and dry-run it before adopting. + +This integration runs [Agor](https://github.com/preset-io/agor)'s **executor** +inside an [`acq`](https://github.com/GSA-TTS/agentic-coding-quickstart) sandbox, +so an agent session's tool calls, git operations, and network egress are +isolated by the sandbox instead of running directly on the host. It needs **zero +changes to Agor** for the OpenCode path — it plugs into Agor's existing +`executor_command_template` escape hatch. + +It is an **orchestrator** integration: Agor *drives* `acq` from the outside (it +owns the agent + session lifecycle and calls `acq create` / `acq exec`). Contrast +with the [isolation kits](../../isolation/acq-kits/), which are things `acq` +*applies inside* a sandbox. See the +[area README](../README.md) for the boundary rule. + +## How it works + +1. Agor's daemon spawns an executor per task by running its configured + `executor_command_template` via `sh -c`, substituting a few variables and + **piping a JSON payload to the process's stdin**. +2. This integration's wrapper (`sandbox-wrapper-acq.sh`) is that template target. + It reads the payload, works out what to mount from the branch's own `.git`, + creates an `acq` sandbox, and pipes the payload into + `agor-executor --stdin` **inside** the sandbox. +3. The executor connects back to the daemon over WebSocket using the payload's + scoped JWT — so the sandbox network policy must allow the daemon URL. + +``` +Agor daemon ──(executor_command_template: sandbox-wrapper-acq.sh {session_id})──▶ sh -c + │ writes JSON payload to stdin + ▼ +sandbox-wrapper-acq.sh + │ parse params.cwd, derive mounts from .git, acq create (+ egress kit + usai secret) + ▼ +acq sandbox (sbx) ── acq exec -- agor-executor --stdin ──▶ agent SDK + │ │ + └────────────────── WebSocket back to daemon ◀────────────┘ (allow-listed egress) +``` + +## Prerequisites + +- **`acq`** installed and configured with a backend (**sbx** for v1; see + [Backend support](#backend-support)). +- **`agor-executor`** available on `PATH` **inside the sandbox image**. (Agor's + daemon owns installing/bundling the executor; the sandbox image must be able to + run `agor-executor --stdin`.) +- **`jq`** on the host (the wrapper parses the payload with it). +- A **daemon-egress kit** ref (see [Daemon reachability](#daemon-reachability)). +- A **USAi API key** available to the operator (see [Credentials](#credentials-usai)). + +## Setup + +1. Copy `sandbox-wrapper-acq.sh` somewhere the daemon can execute it, e.g. + `~/.agor/sandbox-wrapper-acq.sh`, and `chmod +x` it. + +2. Point Agor's `executor_command_template` at it in `~/.agor/config.yaml`: + + ```yaml + execution: + executor_command_template: | + /home/you/.agor/sandbox-wrapper-acq.sh {session_id} + ``` + + Only `{session_id}` is needed — the wrapper reads everything else (the + worktree path, the daemon URL, the session token) from the JSON payload on + stdin. (Agor populates only `{session_id}`, `{task_id}`, `{unix_user}` for + prompt spawns, so the wrapper deliberately does not rely on `{branch_id}`.) + +3. **Dry-run it once** against a real payload before going live: + + ```bash + echo '' \ + | AGOR_SANDBOX_DRY_RUN=1 AGOR_EGRESS_KIT= \ + ~/.agor/sandbox-wrapper-acq.sh + ``` + + It prints the `acq` commands it *would* run (mounts, egress kit, secret, exec, + cleanup) and exits without creating a sandbox. + +4. Provision the USAi secret and remove the dry-run flag (see below), then start + a session in Agor. + +## Configuration (environment) + +All are optional and **none are secrets**: + +| Env var | Default | Purpose | +|---|---|---| +| `AGOR_ACQ_BIN` | `acq` | `acq` binary on `PATH`. | +| `AGOR_ACQ_AGENT` | `shell` | acq agent mode — a raw box; Agor owns the agent SDK. | +| `AGOR_SANDBOX_PREFIX` | `agor-` | Sandbox name prefix (`+ first 8 of session id`). | +| `AGOR_SANDBOX_DRY_RUN` | `0` | `1` = print the planned acq commands and exit. | +| `AGOR_EGRESS_KIT` | (unset) | acq kit ref that allow-lists the daemon (local dir or `git+https…#ref=&dir=`). | +| `AGOR_USAI_SECRET` | `1` | `1` = set the per-sandbox `usai` acq secret. | +| `AGOR_USAI_KEY_FILE` | (unset) | File holding the USAi key; piped to `acq secret set` (never argv). | + +## Mount strategy + +The wrapper derives what to mount from the branch's **own `.git`**, with **zero +daemon calls**: + +- **Worktree branches** — `.git` is a *file* containing + `gitdir:
/.git/worktrees/`. The wrapper reads it, derives + `
/.git`, and mounts **the worktree + the main repo's `.git`** so git + commit/push and `gitdir:` resolution work. +- **Clone branches** — `.git` is a *directory* (self-contained). The wrapper + mounts **only the clone dir**. + +On sbx, positional workspaces are mounted at their **absolute host path**, so the +worktree appears at the same path inside the sandbox (preserving `gitdir:` +resolution and error messages). + +### v1 safety gate — local repos are refused + +sbx mounts **whole directories** at their host path; it cannot bind *only* +`/.git` without its parent. So for a **worktree off a local repo** +(`agor repo add-local`), mounting the main repo dir would drag the user's working +tree — including a `.env` with real secrets — into the sandbox. **The wrapper +refuses this** (exit 5). v1 supports: + +- **Agor-managed remote repos** — the main checkout is a clean clone under + `~/.agor/` with no user secrets; safe to mount. (This is the default path.) +- **Clone-mode branches** — self-contained; only the clone dir is mounted. + +> The wrapper detects "Agor-managed" by the `~/.agor/*` path root. If your +> deployment uses different roots, adjust the gate in the script. +> +> **Alternative not taken in v1:** a host-side "`.git`-only staging dir" (bind or +> copy just `.git` into a throwaway dir and mount *that*) would let local-repo +> worktrees work while still hiding the checkout, at the cost of more host-side +> machinery. Tracked as a possible enhancement. + +### Residual risks + +- **Committed-history secrets** remain reachable via `git log --all` / `git show` + regardless of mount strategy — this is orthogonal to sandboxing (purge with + `git filter-repo`/BFG). +- The worktree `.git` file's `gitdir:` reveals the **main-repo path**; on sbx the + parent dir is mounted (remote) or absent (clone). + +## Daemon reachability + +The executor inside the sandbox must reach the daemon over WebSocket. `acq` has +**no per-invocation network flag** — outbound egress can only be allow-listed by +an **acq kit's `caps.network.allow`**, and sbx is default-deny for arbitrary +hosts. So this integration ships/uses a small egress kit whose allow-list +includes the daemon host alias (`host.docker.internal:3030` on sbx): + +```yaml +# integrations/isolation/acq-kits/agor-daemon-egress/spec.yaml (see map #259) +caps: + network: + allow: + - host.docker.internal:3030 +``` + +Point `AGOR_EGRESS_KIT` at it — a **local dir** (bypasses the source allowlist) +or the **git form** +`git+https://github.com/GSA-TTS/agentic-coding-patterns.git#ref=<40-char-sha>&dir=integrations/isolation/acq-kits/agor-daemon-egress` +(`GSA-TTS/` is on acq's default kit-source allowlist). + +> The daemon port defaults to `3030`; the wrapper can read the actual +> `daemonUrl` from the payload. If your daemon uses a non-default port, the +> egress kit's allow entry must match. + +## Credentials (USAi) + +For the **OpenCode** path, Agor does **not** vend a USAi key to the sandbox +today — provider credentials are scrubbed from the executor payload, and Agor's +credential endpoint has no OpenCode entry. So v1 provisions the USAi key **to +`acq` out-of-band**, and `acq`'s MITM proxy injects it on outbound requests (the +agent never sees it): + +- Per-sandbox: put the key in a file and set `AGOR_USAI_KEY_FILE`; the wrapper + runs `acq secret set usai` with the key on **stdin**. +- Or once, globally: `acq secret set -g usai` (then set `AGOR_USAI_SECRET=0`). + +> Agor-vended **per-user / per-project** USAi keys (the original design) require +> an upstream Agor change and are **out of scope** for this worked example — +> tracked at map [#261](https://github.com/GSA-TTS/agentic-coding-patterns/issues/261). + +## Who owns what + +| Concern | Agor | This wrapper | `acq` + kits | +|---|---|---|---| +| Agent model selection | ✅ `model_config` | | | +| Agent credentials (OpenCode) | ❌ not handled | | ✅ MITM (key set out-of-band) | +| Worktree + `.git` mount | | ✅ derive from `.git`, pass to acq | ✅ performs the mount (sbx: host path) | +| Daemon egress | | ✅ apply egress kit via `--kit` | ✅ `caps.network.allow` | +| USAi key storage/rotation | (not for OpenCode today) | ✅ read operator key → `acq secret set` | ✅ injection mechanism | +| USAi endpoint config | | | ✅ `usai-provider` kit | +| Zscaler CA / playbook / git-sign | | | ✅ the respective kits | +| Sandbox lifecycle | | ✅ create + `trap` cleanup | ✅ `acq create` / `acq rm` | +| Executor process | ✅ `agor-executor --stdin` | ✅ pipe payload into `acq exec` | | + +## Backend support + +| Backend | v1 | Notes | +|---|---|---| +| **sbx** | ✅ | Positional workspaces mount at their absolute host path — required for `gitdir:` resolution and Agor's same-path assumption. | +| **msb** | ❌ (gap) | `acq`'s msb adapter mounts at a **fixed guest path** (`/home/agent/workspace`), not the host path, breaking the `.git` pointer. Tracked at map [#260](https://github.com/GSA-TTS/agentic-coding-patterns/issues/260). | +| **ppp** | ❌ | Future, with msb. | + +## Scope and authority + +This is a **reusable, community-shareable** integration. It carries **no +compliance authority** and is **not** federal policy — behavioral/policy +authority lives in the +[playbook](https://github.com/GSA-TTS/agentic-coding-playbook) and is referenced, +not restated. No secrets, PII, CUI, or internal URLs live in this integration. + +## Background / design + +- [`docs/explorations/sandbox-abstraction.md`](docs/explorations/sandbox-abstraction.md) + — the Agor executor-hook sandbox abstraction design. +- [`docs/explorations/sandbox-acq-analysis.md`](docs/explorations/sandbox-acq-analysis.md) + — using `acq` as the backend: the four kits, who-owns-what, credential flow. +- [Area ADR 0001](../docs/decisions/0001-orchestrators-area-and-agor-acq.md) — + why `integrations/orchestrators/` exists and the drives-vs-applied boundary. + +## Layout + +``` +integrations/orchestrators/agor/ +├── README.md # this guide +├── sandbox-wrapper-acq.sh # the executor_command_template wrapper +└── docs/explorations/ # source design docs +``` From 5b2c66bfa436336753c02cf6e863ee3964bf6253 Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Sun, 26 Jul 2026 17:22:35 +0000 Subject: [PATCH 05/11] feat(acq-kits): add agor-daemon-egress security kit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the agor-daemon-egress hybrid/v1 mixin kit: allow-list outbound egress to the Agor daemon control-plane (host.docker.internal:3030 by default) so an Agor executor running inside an acq sandbox — via the orchestrators/agor integration — can connect back to the daemon over WebSocket. acq has no per-invocation egress flag, so daemon reachability must be a kit's caps.network.allow (research #248/#259); sbx is default-deny. - spec.yaml: single caps.network.allow entry, no files/commands/secret. sbx preserves host:port; msb strips the port (domain-only) — documented. - Security-relevant (widens egress): governed by human review + a prose 'Security posture' note in the README and an ADR, since the hybrid/v1 kit schema (additionalProperties:false) models no governance frontmatter (those are skill-pattern fields). Human-approved for authoring + categorization. - Lives under isolation/acq-kits/ (applied INSIDE the sandbox) per the #250 drives-vs-applied boundary, though its only consumer is orchestrators/agor, which references it via AGOR_EGRESS_KIT. - README (backend parity + security posture), TROUBLESHOOTING, scripts/verify (sbx live check), ADR; registry + acq-kits README updated. validate-kits.py: all 6 kits valid. unsafe-shell scan clean. bash -n clean. Refs GSA-TTS/agentic-coding-patterns#259. Co-authored-by: OpenCode Agent (cherry picked from commit 856fbc6b663acff669df735228e81d56140afcbc) --- integrations/isolation/acq-kits/README.md | 1 + .../acq-kits/agor-daemon-egress/README.md | 114 ++++++++++++++++++ .../agor-daemon-egress/TROUBLESHOOTING.md | 37 ++++++ .../security-categorized-egress-kit.md | 69 +++++++++++ .../agor-daemon-egress/scripts/verify | 103 ++++++++++++++++ .../acq-kits/agor-daemon-egress/spec.yaml | 74 ++++++++++++ integrations/isolation/acq-kits/kits.yaml | 14 +++ 7 files changed, 412 insertions(+) create mode 100644 integrations/isolation/acq-kits/agor-daemon-egress/README.md create mode 100644 integrations/isolation/acq-kits/agor-daemon-egress/TROUBLESHOOTING.md create mode 100644 integrations/isolation/acq-kits/agor-daemon-egress/docs/decisions/security-categorized-egress-kit.md create mode 100755 integrations/isolation/acq-kits/agor-daemon-egress/scripts/verify create mode 100644 integrations/isolation/acq-kits/agor-daemon-egress/spec.yaml diff --git a/integrations/isolation/acq-kits/README.md b/integrations/isolation/acq-kits/README.md index c747065..a09aa47 100644 --- a/integrations/isolation/acq-kits/README.md +++ b/integrations/isolation/acq-kits/README.md @@ -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), diff --git a/integrations/isolation/acq-kits/agor-daemon-egress/README.md b/integrations/isolation/acq-kits/agor-daemon-egress/README.md new file mode 100644 index 0000000..a067476 --- /dev/null +++ b/integrations/isolation/acq-kits/agor-daemon-egress/README.md @@ -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 (v1 target) | `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 for v1 (sbx-only); see the msb gap [#260](https://github.com/GSA-TTS/agentic-coding-patterns/issues/260). | +| **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=&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 +``` diff --git a/integrations/isolation/acq-kits/agor-daemon-egress/TROUBLESHOOTING.md b/integrations/isolation/acq-kits/agor-daemon-egress/TROUBLESHOOTING.md new file mode 100644 index 0000000..58725cd --- /dev/null +++ b/integrations/isolation/acq-kits/agor-daemon-egress/TROUBLESHOOTING.md @@ -0,0 +1,37 @@ +# 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 -- 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. v1 targets sbx (which keeps the port); the msb behavior is tracked at +[#260](https://github.com/GSA-TTS/agentic-coding-patterns/issues/260). + +## `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. diff --git a/integrations/isolation/acq-kits/agor-daemon-egress/docs/decisions/security-categorized-egress-kit.md b/integrations/isolation/acq-kits/agor-daemon-egress/docs/decisions/security-categorized-egress-kit.md new file mode 100644 index 0000000..46973b0 --- /dev/null +++ b/integrations/isolation/acq-kits/agor-daemon-egress/docs/decisions/security-categorized-egress-kit.md @@ -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 for the + sbx-only v1, tracked with the msb gap + ([#260](https://github.com/GSA-TTS/agentic-coding-patterns/issues/260)). diff --git a/integrations/isolation/acq-kits/agor-daemon-egress/scripts/verify b/integrations/isolation/acq-kits/agor-daemon-egress/scripts/verify new file mode 100755 index 0000000..c50cfe7 --- /dev/null +++ b/integrations/isolation/acq-kits/agor-daemon-egress/scripts/verify @@ -0,0 +1,103 @@ +#!/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 || 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 +[ -f "$SPEC_FILE" ] && ok "kit spec.yaml present" || { + bad "spec.yaml missing" + exit 1 +} +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 ] diff --git a/integrations/isolation/acq-kits/agor-daemon-egress/spec.yaml b/integrations/isolation/acq-kits/agor-daemon-egress/spec.yaml new file mode 100644 index 0000000..a59778f --- /dev/null +++ b/integrations/isolation/acq-kits/agor-daemon-egress/spec.yaml @@ -0,0 +1,74 @@ +# spec.yaml — agor-daemon-egress (a neutral acq mixin kit, hybrid/v1) +# +# Allow-lists outbound egress to the Agor DAEMON from inside the sandbox, so an +# Agor executor running in the sandbox (via the orchestrators/agor wrapper) can +# connect back to the daemon over WebSocket/Feathers using its scoped JWT. +# +# WHY A KIT (not a flag): acq has NO per-invocation network flag (`--net-rule`, +# `--allow`, `acq policy` do not exist at the acq level). The ONLY acq-native +# egress mechanism is a kit's caps.network.allow. sbx is default-deny for +# arbitrary hosts, so without this the executor cannot reach the daemon and the +# session never streams results. See: +# integrations/orchestrators/agor/ (the consumer) +# integrations/orchestrators/docs/decisions/0001-orchestrators-area-and-agor-acq.md +# GSA-TTS/agentic-coding-patterns#259 (the decision), #247 (the map) +# +# COMPOSITION (per the #250 drives-vs-applied boundary): this is an ISOLATION kit +# (acq applies it INSIDE the sandbox), so it lives here under acq-kits/ even +# though its sole consumer is the orchestrators/agor integration, which merely +# REFERENCES it (e.g. AGOR_EGRESS_KIT= or a git+https ref). +# +# SECURITY NOTE (this kit widens network egress — reviewed as a security kit): +# - It allow-lists exactly ONE host:port — the Agor daemon control-plane on the +# backend's host alias. It grants no filesystem access, drops no files, runs +# no commands, and carries no secret. The daemon URL is NOT sensitive. +# - The `hybrid/v1` kit schema is additionalProperties:false and models no +# security-governance frontmatter (those fields live on skill patterns, not +# kit specs). The governance posture is therefore documented in README.md + +# the kit's ADR and enforced by human review (needs-human-review on the PR), +# not by schema fields. See docs/security-skill-governance.md for the model. +# +# BACKEND PARITY: +# - sbx: caps.network.allow is synthesized into the sbx-v2 kit; the full +# host:port is preserved (quoted). This is the v1 target backend. +# - msb: acq emits `--net-rule allow@` and STRIPS the port (msb keys on +# domain only) — so egress is host-wide for that host on msb. Acceptable for +# v1 (sbx-only, per #251); noted for the msb gap (#260). +# +# DAEMON HOST ALIAS + PORT: defaults below assume the sbx host alias +# `host.docker.internal` and the Agor default daemon port 3030. If your daemon +# uses a different port or the deploy exposes it under a different alias, EDIT the +# allow entry to match (the wrapper can read the real daemonUrl from the payload; +# a static kit cannot template it). See README "Adjusting the allow entry". +schemaVersion: "hybrid/v1" +kind: mixin +name: agor-daemon-egress +displayName: Agor Daemon Egress +description: > + Allow-list 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. acq has no per-invocation egress flag, so daemon + reachability must be expressed as a kit caps.network.allow entry. Defaults to + the sbx host alias host.docker.internal:3030 (the Agor default port); edit the + allow entry for a non-default port or alias. + +caps: + network: + allow: + # The Agor daemon control-plane. sbx keeps the :port; msb strips it. + # EDIT this if your daemon port/alias differs (see README). + - host.docker.internal:3030 + +agentContext: | + ## Agor daemon egress + + This sandbox is allowed to reach the Agor daemon control-plane + (`host.docker.internal:3030` by default). That is how the in-sandbox + `agor-executor` streams task results back to the daemon. No action is needed + from you; this kit only opens that one egress path. + +# No backend shortcut: every backend uses caps.network.allow. sbx preserves the +# host:port; msb strips the port (domain-only) — documented in README parity. +backend_shortcuts: + sbx: {} + msb: {} diff --git a/integrations/isolation/acq-kits/kits.yaml b/integrations/isolation/acq-kits/kits.yaml index 8872d92..662349d 100644 --- a/integrations/isolation/acq-kits/kits.yaml +++ b/integrations/isolation/acq-kits/kits.yaml @@ -58,3 +58,17 @@ kits: backends with no manual step. The install+supervise script (files/home/openchamber-start.sh) and the wrapper (files/home/.local/bin/opencode) are backend-agnostic. No backend shortcut. + + agor-daemon-egress: + backends: [sbx, msb] + parity: | + Allow-lists outbound egress to the Agor daemon control-plane + (host.docker.internal:3030 by default) so an Agor executor running in the + sandbox (via the orchestrators/agor integration) can connect back to the + daemon. No backend shortcut — both use caps.network.allow. sbx PRESERVES + the full host:port; msb STRIPS the port (--net-rule allow@, domain + only), so egress is host-wide for that host on msb. v1 targets sbx (see + GSA-TTS/agentic-coding-patterns#251); msb port behavior tracked with the + msb gap GSA-TTS/agentic-coding-patterns#260. Security-relevant (widens + egress) — governed by human review, not schema fields (the kit schema + models no governance frontmatter); see the kit README + its ADR. From f301880e3c9d5a555e752d559c65f801e93b2c2a Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Sun, 26 Jul 2026 17:22:53 +0000 Subject: [PATCH 06/11] fix(orchestrators): detect Agor-managed repos via AGOR_DATA_HOME Replace the hardcoded ~/.agor/* managed-root heuristic in the wrapper's v1 local-repo safety gate with Agor's real path model, per source confirmation: AGOR_DATA_HOME (env) > paths.data_home (config) > AGOR_HOME > ~/.agor. Agor stores repos/ (bare clones) and worktrees/ under AGOR_DATA_HOME, so a main repo under that root is Agor-managed (safe to mount); anything else is treated as a user local repo and refused (would expose .env). - Wrapper resolves AGOR_DATA_HOME|AGOR_HOME|~/.agor and honors an optional colon-separated AGOR_MANAGED_ROOTS allowlist (e.g. an EFS/NFS data_home for k8s deploys). Prefix match with a trailing-slash guard. - Documents that the wrapper cannot read config.yaml, so a config-only paths.data_home must be exported as AGOR_DATA_HOME (or added to AGOR_MANAGED_ROOTS). - README: new env-var rows + updated mount-strategy note. Addresses the B review (Copilot's Agor-source query on worktree storage). Re-verified: bash -n clean; dry-run cases for AGOR_DATA_HOME match, local-repo refusal (rc5), and an AGOR_MANAGED_ROOTS EFS root all behave. Refs #251, #253. Co-authored-by: OpenCode Agent (cherry picked from commit b48b2c4a2a77b9e4f053fcb2096aac4d5d6120a6) --- integrations/orchestrators/agor/README.md | 14 ++++- .../orchestrators/agor/sandbox-wrapper-acq.sh | 60 +++++++++++++++---- 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/integrations/orchestrators/agor/README.md b/integrations/orchestrators/agor/README.md index 067aef6..844d28c 100644 --- a/integrations/orchestrators/agor/README.md +++ b/integrations/orchestrators/agor/README.md @@ -96,6 +96,8 @@ All are optional and **none are secrets**: | `AGOR_ACQ_AGENT` | `shell` | acq agent mode — a raw box; Agor owns the agent SDK. | | `AGOR_SANDBOX_PREFIX` | `agor-` | Sandbox name prefix (`+ first 8 of session id`). | | `AGOR_SANDBOX_DRY_RUN` | `0` | `1` = print the planned acq commands and exit. | +| `AGOR_DATA_HOME` | (Agor default) | Agor's git-data root (`repos/` + `worktrees/`); used to tell an Agor-managed repo from a user's local repo. Falls back to `AGOR_HOME`, then `~/.agor`. **Export it if your deploy sets `paths.data_home` only in `config.yaml`** (this wrapper can't read the config file). | +| `AGOR_MANAGED_ROOTS` | (unset) | Extra colon-separated managed roots to allow (e.g. an EFS/NFS mount), in addition to `AGOR_DATA_HOME`. | | `AGOR_EGRESS_KIT` | (unset) | acq kit ref that allow-lists the daemon (local dir or `git+https…#ref=&dir=`). | | `AGOR_USAI_SECRET` | `1` | `1` = set the per-sandbox `usai` acq secret. | | `AGOR_USAI_KEY_FILE` | (unset) | File holding the USAi key; piped to `acq secret set` (never argv). | @@ -125,11 +127,17 @@ tree — including a `.env` with real secrets — into the sandbox. **The wrappe refuses this** (exit 5). v1 supports: - **Agor-managed remote repos** — the main checkout is a clean clone under - `~/.agor/` with no user secrets; safe to mount. (This is the default path.) + Agor's git-data root (`$AGOR_DATA_HOME/repos/…`, default `~/.agor/`) with no + user secrets; safe to mount. (This is the default path.) - **Clone-mode branches** — self-contained; only the clone dir is mounted. -> The wrapper detects "Agor-managed" by the `~/.agor/*` path root. If your -> deployment uses different roots, adjust the gate in the script. +> The wrapper detects "Agor-managed" by whether the main repo lives under +> **`AGOR_DATA_HOME`** (falling back to `AGOR_HOME`, then `~/.agor`) — matching +> Agor's own path model (`AGOR_DATA_HOME` env > `paths.data_home` in config > +> `AGOR_HOME` > `~/.agor`), so it works for k8s/EFS deployments that relocate the +> git data. **This wrapper cannot read `config.yaml`**, so if your deploy sets +> `paths.data_home` only in the config file, export `AGOR_DATA_HOME` (or add the +> root to `AGOR_MANAGED_ROOTS`) for the wrapper too. > > **Alternative not taken in v1:** a host-side "`.git`-only staging dir" (bind or > copy just `.git` into a throwaway dir and mount *that*) would let local-repo diff --git a/integrations/orchestrators/agor/sandbox-wrapper-acq.sh b/integrations/orchestrators/agor/sandbox-wrapper-acq.sh index 8cad053..dc2a61b 100755 --- a/integrations/orchestrators/agor/sandbox-wrapper-acq.sh +++ b/integrations/orchestrators/agor/sandbox-wrapper-acq.sh @@ -72,6 +72,12 @@ Env (all optional; none are secrets): AGOR_ACQ_AGENT acq agent mode (default: shell) AGOR_SANDBOX_PREFIX sandbox name prefix (default: agor-) AGOR_SANDBOX_DRY_RUN 1 = print the acq commands and exit without creating a sandbox + AGOR_DATA_HOME Agor git-data root (repos/ + worktrees/); used to tell an + Agor-managed repo from a user local repo. Falls back to + AGOR_HOME, then ~/.agor. Export it if your deploy sets + paths.data_home only in config.yaml. + AGOR_MANAGED_ROOTS extra colon-separated managed roots to allow (e.g. an EFS + mount), in addition to AGOR_DATA_HOME AGOR_EGRESS_KIT acq kit ref allow-listing the daemon (local dir or git+https) AGOR_USAI_SECRET 1 = provision the per-sandbox `usai` acq secret (default: 1) AGOR_USAI_KEY_FILE file holding the USAi key to pipe to `acq secret set` @@ -163,21 +169,49 @@ if [[ -f "${WORKTREE_PATH}/.git" ]]; then main_repo_dir="${main_git%/.git}" # v1 safety gate: refuse to mount a LOCAL repo's parent checkout, which would - # expose the user's working tree / .env. Heuristic: Agor-managed remote clones - # live under ~/.agor/ (repos/ or worktrees/). Anything else is treated as a - # local repo and refused, per map #251. - case "${main_repo_dir}" in - "${HOME}"/.agor/* | /root/.agor/*) - # Agor-managed clean clone: safe to mount the main .git's parent. + # expose the user's working tree / .env. Agor-managed repos live UNDER + # $AGOR_DATA_HOME (its `repos/` bare clones + `worktrees/` trees); anything + # else is a user's local repo (`agor repo add-local`) and is refused. Per + # map #251, and confirmed against Agor's path model: + # AGOR_DATA_HOME (env, highest priority) + # else paths.data_home in config.yaml (not readable here — see NOTE) + # else AGOR_HOME (env) + # else ~/.agor (default) + # Env-driven so it works for k8s/EFS deployments where data_home != ~/.agor. + # NOTE: this wrapper cannot read config.yaml's paths.data_home; if a deploy + # sets data_home ONLY in config (not via env), export AGOR_DATA_HOME (or + # AGOR_MANAGED_ROOTS) for this wrapper too. See the README. + agor_data_home="${AGOR_DATA_HOME:-${AGOR_HOME:-${HOME}/.agor}}" + # Allow operators to extend the managed-root allowlist (colon-separated), + # e.g. AGOR_MANAGED_ROOTS="/mnt/efs/agor:/srv/agor-data". + managed_roots="${agor_data_home}${AGOR_MANAGED_ROOTS:+:${AGOR_MANAGED_ROOTS}}" + + managed=0 + _IFS_SAVE="${IFS}" + IFS=':' + for root in ${managed_roots}; do + [[ -z "${root}" ]] && continue + case "${main_repo_dir}/" in + "${root%/}"/*) + managed=1 + break + ;; + esac + done + IFS="${_IFS_SAVE}" + + if [[ "${managed}" -eq 1 ]]; then + # Agor-managed clean clone under AGOR_DATA_HOME: safe to mount the main .git. POSITIONAL_MOUNTS+=("${main_git}") - ;; - *) - echo "ERROR: refusing to mount a local repo's checkout (${main_repo_dir})." >&2 - echo " v1 supports Agor-managed remote repos or clone-mode branches only;" >&2 - echo " see map #251 (mount strategy)." >&2 + else + echo "ERROR: refusing to mount a non-Agor-managed repo checkout (${main_repo_dir})." >&2 + echo " It is outside AGOR_DATA_HOME (${agor_data_home}), so it looks like a" >&2 + echo " user's local repo — mounting its parent could expose .env/working files." >&2 + echo " v1 supports Agor-managed remote repos or clone-mode branches only." >&2 + echo " If this IS Agor-managed, export AGOR_DATA_HOME/AGOR_MANAGED_ROOTS." >&2 + echo " See map #251 (mount strategy)." >&2 exit 5 - ;; - esac + fi elif [[ -d "${WORKTREE_PATH}/.git" ]]; then : # Clone mode: self-contained .git; the worktree mount alone is enough. fi From 065d022eac364c494353b4358977ba76aa0f549e Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Sun, 26 Jul 2026 17:23:26 +0000 Subject: [PATCH 07/11] docs(orchestrators): mark area ADR 0001 accepted Human owner confirmed the integrations/orchestrators/ area and the drives-vs-applied boundary rule (review item A). Flip ADR 0001 status proposed -> accepted. Co-authored-by: OpenCode Agent (cherry picked from commit 76e16a8d25a6f5c6c03da481d016f84451540178) --- .../decisions/0001-orchestrators-area-and-agor-acq.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/integrations/orchestrators/docs/decisions/0001-orchestrators-area-and-agor-acq.md b/integrations/orchestrators/docs/decisions/0001-orchestrators-area-and-agor-acq.md index 8a78eb0..95775a8 100644 --- a/integrations/orchestrators/docs/decisions/0001-orchestrators-area-and-agor-acq.md +++ b/integrations/orchestrators/docs/decisions/0001-orchestrators-area-and-agor-acq.md @@ -1,6 +1,6 @@ --- title: "Add an integrations/orchestrators/ area; place the Agor + acq integration there" -status: "proposed" +status: "accepted" date: "2026-07-26" decision_makers: ["Bret Mogilefsky", "OpenCode Agent"] category: "repository-structure" @@ -17,10 +17,8 @@ impact_level: "low" > between the two — mirroring how `integrations/isolation/` carries its own > [area ADR](../../isolation/docs/decisions/0001-neutral-hybrid-v1-acq-kits.md). -> **Status: proposed.** Drafted AFK via the `wayfinder` map -> ([#247](https://github.com/GSA-TTS/agentic-coding-patterns/issues/247), -> ticket [#255](https://github.com/GSA-TTS/agentic-coding-patterns/issues/255)); -> pending human confirmation before it is marked `accepted`. +> **Status: accepted.** Boundary rule and area confirmed by the human owner on +> 2026-07-26 (review item A). ## Context and Problem Statement From 13346cb2b79387c630d238991eb7582271ff26bf Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Sun, 26 Jul 2026 17:28:31 +0000 Subject: [PATCH 08/11] fix(orchestrators): correct area-ADR/egress-kit relative links + markdownlint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI-parity fixes for the new orchestrators area and agor-daemon-egress kit: - Correct relative-link depths in the orchestrators area ADR (repo-root docs/, isolation area ADR, agor explorations) and in the egress kit's security ADR (orchestrators/, docs/security-skill-governance.md) — all local links now resolve. - Fix MD028 (blank line inside blockquote) in the egress kit README and the orchestrators area ADR. - Flip orchestrators area ADR 0001 to accepted (review A). Verified: markdownlint-cli2 0 issues; all local links resolve; validate_repo, unsafe-shell scan, validate-kits --strict, INDEX --check, and the usai-provider node tests all pass. Refs #256. Co-authored-by: OpenCode Agent (cherry picked from commit b4fc7c1a41983050410946b860e5b35ed4bbfee2) --- .../acq-kits/agor-daemon-egress/README.md | 2 +- .../security-categorized-egress-kit.md | 6 +++--- .../0001-orchestrators-area-and-agor-acq.md | 20 +++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/integrations/isolation/acq-kits/agor-daemon-egress/README.md b/integrations/isolation/acq-kits/agor-daemon-egress/README.md index a067476..49ea0a4 100644 --- a/integrations/isolation/acq-kits/agor-daemon-egress/README.md +++ b/integrations/isolation/acq-kits/agor-daemon-egress/README.md @@ -11,7 +11,7 @@ to the daemon over WebSocket/Feathers using its scoped JWT. > [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). diff --git a/integrations/isolation/acq-kits/agor-daemon-egress/docs/decisions/security-categorized-egress-kit.md b/integrations/isolation/acq-kits/agor-daemon-egress/docs/decisions/security-categorized-egress-kit.md index 46973b0..221a733 100644 --- a/integrations/isolation/acq-kits/agor-daemon-egress/docs/decisions/security-categorized-egress-kit.md +++ b/integrations/isolation/acq-kits/agor-daemon-egress/docs/decisions/security-categorized-egress-kit.md @@ -5,7 +5,7 @@ ## Context -The [`orchestrators/agor`](../../../../orchestrators/agor/) wrapper runs an Agor +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), @@ -22,7 +22,7 @@ governed as a **security** kit. ### 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), +([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** @@ -45,7 +45,7 @@ fields — those (`categories`, `risk_tier`, `human_review_required`, 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). +[`docs/security-skill-governance.md`](../../../../../../docs/security-skill-governance.md). Approved by the human owner on 2026-07-26. ## Alternatives considered diff --git a/integrations/orchestrators/docs/decisions/0001-orchestrators-area-and-agor-acq.md b/integrations/orchestrators/docs/decisions/0001-orchestrators-area-and-agor-acq.md index 95775a8..db8dd43 100644 --- a/integrations/orchestrators/docs/decisions/0001-orchestrators-area-and-agor-acq.md +++ b/integrations/orchestrators/docs/decisions/0001-orchestrators-area-and-agor-acq.md @@ -10,20 +10,20 @@ impact_level: "low" # ADR 0001 (orchestrators) — Add `integrations/orchestrators/` and place Agor + acq there > Area-scoped ADR for `integrations/orchestrators/`. The repository-wide ADR log -> is [`docs/decisions/`](../../../docs/decisions/); the area that established +> is [`docs/decisions/`](../../../../docs/decisions/); the area that established > `integrations/` is -> [`docs/decisions/0001-integrations-area.md`](../../../docs/decisions/0001-integrations-area.md). +> [`docs/decisions/0001-integrations-area.md`](../../../../docs/decisions/0001-integrations-area.md). > This record governs the whole orchestrators integration area, so it sits here > between the two — mirroring how `integrations/isolation/` carries its own -> [area ADR](../../isolation/docs/decisions/0001-neutral-hybrid-v1-acq-kits.md). - +> [area ADR](../../../isolation/docs/decisions/0001-neutral-hybrid-v1-acq-kits.md). +> > **Status: accepted.** Boundary rule and area confirmed by the human owner on > 2026-07-26 (review item A). ## Context and Problem Statement `integrations/` (per the repo-wide -[ADR 0001](../../../docs/decisions/0001-integrations-area.md)) holds tool/editor +[ADR 0001](../../../../docs/decisions/0001-integrations-area.md)) holds tool/editor integration guides and portable configs. Its first two classes are `editors/` and `isolation/` (the `acq`/`sbx` mixin kits). @@ -137,15 +137,15 @@ Adopt **Option 1**. ## Links - Repo-wide area ADR that established `integrations/`: - [`docs/decisions/0001-integrations-area.md`](../../../docs/decisions/0001-integrations-area.md). + [`docs/decisions/0001-integrations-area.md`](../../../../docs/decisions/0001-integrations-area.md). - Sibling area ADR (isolation kits): - [`../../isolation/docs/decisions/0001-neutral-hybrid-v1-acq-kits.md`](../../isolation/docs/decisions/0001-neutral-hybrid-v1-acq-kits.md). + [`../../isolation/docs/decisions/0001-neutral-hybrid-v1-acq-kits.md`](../../../isolation/docs/decisions/0001-neutral-hybrid-v1-acq-kits.md). - Contribution scope: - [`docs/contribution-scope.md`](../../../docs/contribution-scope.md). + [`docs/contribution-scope.md`](../../../../docs/contribution-scope.md). - Wayfinder map: [#247](https://github.com/GSA-TTS/agentic-coding-patterns/issues/247); boundary decision [#250](https://github.com/GSA-TTS/agentic-coding-patterns/issues/250). - Source explorations: - [`../agor/docs/explorations/sandbox-abstraction.md`](../agor/docs/explorations/sandbox-abstraction.md), - [`../agor/docs/explorations/sandbox-acq-analysis.md`](../agor/docs/explorations/sandbox-acq-analysis.md). + [`../agor/docs/explorations/sandbox-abstraction.md`](../../agor/docs/explorations/sandbox-abstraction.md), + [`../agor/docs/explorations/sandbox-acq-analysis.md`](../../agor/docs/explorations/sandbox-acq-analysis.md). From 9828192f932c4eae9b3642fb3464c63d47e10e90 Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Sun, 2 Aug 2026 05:50:16 +0000 Subject: [PATCH 09/11] feat(acq-kits): declare provenance on agor-daemon-egress kit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Align the egress kit with main's newer kits (quickstart#235 / #273): declare the source repo in the optional hybrid/v1 `provenance` block. As a standalone integration kit (not part of the acq-builtin bundle), it declares `repo` only — no bundle/kit_names — so it does not affect the built-in bundle cross-check. Co-authored-by: OpenCode Agent --- .../isolation/acq-kits/agor-daemon-egress/spec.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/integrations/isolation/acq-kits/agor-daemon-egress/spec.yaml b/integrations/isolation/acq-kits/agor-daemon-egress/spec.yaml index a59778f..6b2c4fc 100644 --- a/integrations/isolation/acq-kits/agor-daemon-egress/spec.yaml +++ b/integrations/isolation/acq-kits/agor-daemon-egress/spec.yaml @@ -59,6 +59,14 @@ caps: # EDIT this if your daemon port/alias differs (see README). - host.docker.internal:3030 +# Bundle provenance (quickstart#235). Declares the source repo so a consumer +# (acq) can attribute this kit. Unlike the acq-builtin bundle kits, this egress +# kit is a standalone integration kit (consumed only by orchestrators/agor), so +# it declares `repo` but no bundle/kit_names. The applied commit SHA, timestamp, +# and backend are recorded by the consumer inside the sandbox at apply time. +provenance: + repo: GSA-TTS/agentic-coding-patterns + agentContext: | ## Agor daemon egress From a26b72b81d83100a505f15ba064ab6a39bc46868 Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Sun, 2 Aug 2026 05:53:32 +0000 Subject: [PATCH 10/11] style(acq-kits): use explicit if/else in egress-kit verify (SC2015) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the `[ -f ] && ok || { … }` idiom with an if/else so the fallback block cannot run when the test passes (shellcheck SC2015). No behavior change. Co-authored-by: OpenCode Agent --- .../isolation/acq-kits/agor-daemon-egress/scripts/verify | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/integrations/isolation/acq-kits/agor-daemon-egress/scripts/verify b/integrations/isolation/acq-kits/agor-daemon-egress/scripts/verify index c50cfe7..531566d 100755 --- a/integrations/isolation/acq-kits/agor-daemon-egress/scripts/verify +++ b/integrations/isolation/acq-kits/agor-daemon-egress/scripts/verify @@ -52,10 +52,12 @@ if ! command -v sbx >/dev/null 2>&1; then echo " sbx not on PATH. Install sbx and retry." >&2 exit 1 fi -[ -f "$SPEC_FILE" ] && ok "kit spec.yaml present" || { +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 From 1aa6d3ddeba26580c378f35ceb89c7518f034e52 Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Mon, 3 Aug 2026 05:55:00 +0000 Subject: [PATCH 11/11] docs(orchestrators): drop stale msb fixed-guest-path gap; msb mount closed upstream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit quickstart#230 + the #233 parity omnibus made msb mount each workspace at its host path (sbx-parity) with multiple positional mounts, so the worktree `.git` `gitdir:` pointer resolves the same on msb — the wrapper is backend-agnostic here. The engineering gap #260 described no longer exists (#260 closed); the only residual is a live msb run on a KVM host, folded into the live-validation ticket #257. Update the wrapper header, agor/README backend table, and the egress kit spec/README/TROUBLESHOOTING/ADR to reference #257 instead of the closed #260. Co-authored-by: OpenCode Agent --- .../isolation/acq-kits/agor-daemon-egress/README.md | 4 ++-- .../acq-kits/agor-daemon-egress/TROUBLESHOOTING.md | 5 +++-- .../docs/decisions/security-categorized-egress-kit.md | 6 +++--- .../isolation/acq-kits/agor-daemon-egress/spec.yaml | 4 ++-- integrations/orchestrators/agor/README.md | 4 ++-- integrations/orchestrators/agor/sandbox-wrapper-acq.sh | 10 ++++++---- 6 files changed, 18 insertions(+), 15 deletions(-) diff --git a/integrations/isolation/acq-kits/agor-daemon-egress/README.md b/integrations/isolation/acq-kits/agor-daemon-egress/README.md index 49ea0a4..918d171 100644 --- a/integrations/isolation/acq-kits/agor-daemon-egress/README.md +++ b/integrations/isolation/acq-kits/agor-daemon-egress/README.md @@ -57,8 +57,8 @@ and enforced by **human review**, not by schema fields — consistent with | Backend | Support | Notes | |---|---|---| -| **sbx** | Supported (v1 target) | `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 for v1 (sbx-only); see the msb gap [#260](https://github.com/GSA-TTS/agentic-coding-patterns/issues/260). | +| **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`. diff --git a/integrations/isolation/acq-kits/agor-daemon-egress/TROUBLESHOOTING.md b/integrations/isolation/acq-kits/agor-daemon-egress/TROUBLESHOOTING.md index 58725cd..62b8e5b 100644 --- a/integrations/isolation/acq-kits/agor-daemon-egress/TROUBLESHOOTING.md +++ b/integrations/isolation/acq-kits/agor-daemon-egress/TROUBLESHOOTING.md @@ -25,8 +25,9 @@ WebSocket/connection error to the daemon. 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. v1 targets sbx (which keeps the port); the msb behavior is tracked at -[#260](https://github.com/GSA-TTS/agentic-coding-patterns/issues/260). +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 diff --git a/integrations/isolation/acq-kits/agor-daemon-egress/docs/decisions/security-categorized-egress-kit.md b/integrations/isolation/acq-kits/agor-daemon-egress/docs/decisions/security-categorized-egress-kit.md index 221a733..a55d22b 100644 --- a/integrations/isolation/acq-kits/agor-daemon-egress/docs/decisions/security-categorized-egress-kit.md +++ b/integrations/isolation/acq-kits/agor-daemon-egress/docs/decisions/security-categorized-egress-kit.md @@ -64,6 +64,6 @@ Approved by the human owner on 2026-07-26. 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 for the - sbx-only v1, tracked with the msb gap - ([#260](https://github.com/GSA-TTS/agentic-coding-patterns/issues/260)). +- 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)). diff --git a/integrations/isolation/acq-kits/agor-daemon-egress/spec.yaml b/integrations/isolation/acq-kits/agor-daemon-egress/spec.yaml index 6b2c4fc..c1ecd7b 100644 --- a/integrations/isolation/acq-kits/agor-daemon-egress/spec.yaml +++ b/integrations/isolation/acq-kits/agor-daemon-egress/spec.yaml @@ -32,8 +32,8 @@ # - sbx: caps.network.allow is synthesized into the sbx-v2 kit; the full # host:port is preserved (quoted). This is the v1 target backend. # - msb: acq emits `--net-rule allow@` and STRIPS the port (msb keys on -# domain only) — so egress is host-wide for that host on msb. Acceptable for -# v1 (sbx-only, per #251); noted for the msb gap (#260). +# domain only) — so egress is host-wide for that host on msb. Acceptable; +# sbx is the validated backend, and a live msb run is tracked at #257. # # DAEMON HOST ALIAS + PORT: defaults below assume the sbx host alias # `host.docker.internal` and the Agor default daemon port 3030. If your daemon diff --git a/integrations/orchestrators/agor/README.md b/integrations/orchestrators/agor/README.md index 844d28c..6f55804 100644 --- a/integrations/orchestrators/agor/README.md +++ b/integrations/orchestrators/agor/README.md @@ -211,8 +211,8 @@ agent never sees it): | Backend | v1 | Notes | |---|---|---| -| **sbx** | ✅ | Positional workspaces mount at their absolute host path — required for `gitdir:` resolution and Agor's same-path assumption. | -| **msb** | ❌ (gap) | `acq`'s msb adapter mounts at a **fixed guest path** (`/home/agent/workspace`), not the host path, breaking the `.git` pointer. Tracked at map [#260](https://github.com/GSA-TTS/agentic-coding-patterns/issues/260). | +| **sbx** | ✅ validated | Positional workspaces mount at their absolute host path — required for `gitdir:` resolution and Agor's same-path assumption. The live end-to-end run is tracked at map [#257](https://github.com/GSA-TTS/agentic-coding-patterns/issues/257). | +| **msb** | ✅ code-ready, live-pending | `acq`'s msb adapter now mounts each workspace at its **host path** (sbx-parity) and supports multiple positional mounts ([quickstart#230](https://github.com/GSA-TTS/agentic-coding-quickstart/pull/230), #233), so the `.git` pointer resolves the same way as on sbx. A live msb run still needs a KVM host (msb is not live-verified upstream); that residual is folded into [#257](https://github.com/GSA-TTS/agentic-coding-patterns/issues/257). | | **ppp** | ❌ | Future, with msb. | ## Scope and authority diff --git a/integrations/orchestrators/agor/sandbox-wrapper-acq.sh b/integrations/orchestrators/agor/sandbox-wrapper-acq.sh index dc2a61b..933b869 100755 --- a/integrations/orchestrators/agor/sandbox-wrapper-acq.sh +++ b/integrations/orchestrators/agor/sandbox-wrapper-acq.sh @@ -29,10 +29,12 @@ # before wiring it live. See docs/clean-script-standard.md. # # SCOPE (v1) -# - Backend: sbx only. `acq`'s msb adapter mounts at a FIXED guest path -# (/home/agent/workspace), not the host path, which breaks the worktree -# `.git` pointer and Agor's same-absolute-path assumption. msb is tracked -# as a gap (map #260). +# - Backend: sbx is the validated target. `acq`'s msb adapter now mounts each +# workspace at its host path (sbx-parity) and supports multiple positional +# mounts (quickstart#230, #233), so the worktree `.git` pointer resolves the +# same on msb — the wrapper is backend-agnostic here. A live msb run still +# needs a KVM host (msb is not live-verified upstream); that live-validation +# residual is tracked at map #257. # - Daemon egress is allow-listed via a small acq kit, NOT a flag (acq has no # --net-rule); see AGOR_EGRESS_KIT below and map #259. # - USAi key: provisioned to acq out-of-band by the operator (map #252). Agor