Skip to content

Implement sealed probes for bounded private-repository computation #6661

Description

@lpcox

Summary

Implement sealed probes: a lightweight way for the primary AWF agent to run agent-authored Python against one configured private repository without learning the repository contents or observing the computation.

The agent invokes a fixed local sealed-probe CLI through a generated skill. The CLI forwards a narrow request to a trusted offline probe proxy. The proxy gives each invocation a fresh writable copy of exactly one pre-staged repository, launches the script in a hardened no-network sandbox, validates a closed JSON result schema, and returns one of exactly four symbols: three caller-defined outcomes or fixed ERROR.

This replaces the heavier isolated-agent-group approach for v1. No secondary LLM, MCP session, mailbox, or long-lived child agent is needed.

Security and information-flow goal

A sealed probe is:

  1. Lightweight: Python code, not another agent or LLM.
  2. Deterministic and auditable: the exact script accessing private data is known.
  3. Sandboxed: no network, credentials, host access, Docker control, primary workspace, or other repositories.
  4. Sealed: the caller cannot observe repository contents, stdout/stderr, files, diffs, errors, exit status, or diagnostics. It observes only one of four predeclared symbols.

The three non-error symbols are known before execution. Selecting among them plus ERROR conveys at most log2(4) = 2 content bits per invocation. Completion timing remains an acknowledged v1 side channel. A bounded per-run invocation count is required to limit cumulative disclosure.

Configuration

Add a top-level sealedProbes block to the AWF standard configuration. It is a subsystem like apiProxy, not a primary-sandbox setting under security.

sealedProbes:
  enabled: true
  privateRepos:
    - "lpcox/foo"
    - "lpcox/bar"
  runtime: gvisor          # docker | gvisor | sbx
  timeout: 30              # seconds per invocation
  memoryLimit: "512m"
  interpreter: python3     # v1: fixed standard-library-only environment
  maxInvocations: 32       # conservative bounded default; exact default may be adjusted

The block must behave identically from a file and AWF stdin config (--config -). No direct CLI configuration flags are required in v1.

Semantics

  • Absent or enabled: false: do not stage repositories, start the proxy, mount its socket, expose the skill, or change existing behavior.
  • privateRepos is the complete allowlist and each value is exactly normalized owner/repo.
  • Credentials come from the environment/existing resolution system, never config.
  • Credentials exist only in a separate trusted staging phase. They must never enter the agent, probe proxy, probe sandbox, arguments, logs, mounts, or /proc-visible state.
  • runtime selects the probe runtime independently of the primary agent runtime. Fail closed if unavailable; never downgrade.
  • V1 uses fixed Python 3 stdlib only. No runtime package installation.
  • maxInvocations bounds cumulative leakage/resource use. Exhaustion returns canonical ERROR without launching a probe.

Validation/preflight

Fail before the primary agent starts when:

  • enabled but privateRepos is empty;
  • a repo is unsafe, duplicated, or contains URL syntax, credentials, wildcard, query, fragment, or traversal;
  • runtime is unsupported/unavailable;
  • timeout, memory, or invocation limit is invalid/unbounded;
  • interpreter is unsupported;
  • staging credentials cannot be resolved;
  • any required seed snapshot cannot be produced and verified.

JSON Schema validates shape; credential/runtime/snapshot availability is a fail-fast preflight.

Agent-facing interface

Expose one generated sealed_probe skill, implemented through a fixed CLI wrapper following the existing agent-local gh wrapper to trusted cli-proxy pattern.

sealed-probe \
  --repo owner/repo \
  --outcome YES \
  --outcome NO \
  --outcome UNKNOWN \
  < probe.py

The skill lists configured repositories, instructs the agent to choose exactly three bounded outcome names, and explains the script/output contract. It is guidance, not a security boundary.

Local CLI

Install /usr/local/bin/sealed-probe only when enabled.

  • Accept only --repo, exactly three --outcome values, and bounded script bytes on stdin.
  • Do not accept executable, image, command, cwd, environment, URL, ref, path, mount, volume, socket, runtime, timeout, or credentials.
  • Forward a bounded/versioned request over a dedicated AWF Unix socket. Only that capability enters the agent.
  • Print exactly one canonical JSON result, no diagnostics, and use one exit status for all outcomes/failures (exit 0 recommended).
  • If the proxy is unavailable or invalid, emit canonical {"result":"ERROR"} locally with identical formatting/status and no stderr.

Do not copy cli-proxy's generic /exec API. The probe API must not permit arbitrary execution.

Request and result contract

Conceptual request:

{
  "privateRepo": "owner/repo",
  "outcomes": ["YES", "NO", "UNKNOWN"],
  "script": "..."
}

Before creating a repo copy or launching a sandbox, validate:

  • framing/version and fixed request/script limits;
  • repository membership in the normalized allowlist;
  • exactly three outcome strings;
  • outcomes are unique, non-empty, bounded (maximum 64 UTF-8 bytes recommended), contain no controls, and are not ERROR;
  • no caller schema or launch controls are present;
  • invocation budget remains.

Invalid requests return canonical ERROR without launching.

Proxy-constructed schema

For outcomes A, B, and C, construct exactly:

{
  "type": "object",
  "properties": {
    "result": {
      "type": "string",
      "enum": ["A", "B", "C", "ERROR"]
    }
  },
  "required": ["result"],
  "additionalProperties": false
}

Do not accept arbitrary JSON Schema. V1 permits no free values, arrays, nested objects, optional/additional fields, $ref, combinators, regex, annotations, or caller-selected error symbol.

Script contract

The script may read/modify /probe/repo, then writes one JSON object:

import json
from pathlib import Path

repo = Path("/probe/repo")
# Deterministic computation; repo may be modified locally.
Path("/probe/out").write_text(json.dumps({"result": "YES"}))

The proxy:

  • reads /probe/out with a strict cap;
  • rejects malformed UTF-8/JSON, duplicate keys, trailing data, non-object roots, missing/extra fields, and values outside the enum;
  • validates the closed schema;
  • never returns script bytes directly;
  • canonically reserializes valid output itself.

Valid response example:

{"result":"YES"}

The failure response is always:

{"result":"ERROR"}

A script-written canonical ERROR is equivalent to proxy failure and adds no symbol.

Failure closure

Every failure returns identical canonical ERROR with the same transport shape and CLI status:

  • invalid request, repo, outcomes, or exhausted budget;
  • missing staging data or unavailable proxy/transport/runtime;
  • launch failure, timeout, crash, signal, OOM, non-zero exit, cancellation;
  • missing, malformed, oversized, duplicate-key, or nonconformant output;
  • cleanup failure or internal exception.

Do not expose status classes, error bodies/headers, exception categories, exit codes, sizes, commit metadata, or diagnostics. Protected audit logs may retain bounded details but are never agent/probe-visible.

Offline staging

Nothing in the probe proxy or probe sandbox may access the internet.

Before proxy/agent readiness, a separate trusted AWF setup phase:

  1. resolves read-only credentials;
  2. materializes one immutable seed per configured repo;
  3. records its commit in protected audit state;
  4. strips credential-bearing config/remotes/helpers, hooks, alternates, worktree links, and external references;
  5. makes and verifies each seed read-only;
  6. removes staging processes/containers and credentials before readiness.

Prefer pre-provisioned local clones. There is no proxy fallback clone/fetch.

Seeds use run-unique isolated storage. Do not share object stores, alternates, worktrees, writable parents, or credential metadata. Safest v1 submodule policy is reject/omit.

Trusted probe proxy

Implement a deterministic trusted host process or tightly controlled sidecar that:

  • has no DNS, Squid, awf-ext, mcpg, api-proxy, cli-proxy, host-network, or public-network path;
  • is reachable only through the narrow dedicated socket/API;
  • receives only repo selector, three outcomes, and script;
  • maps normalized repo IDs through an AWF-generated static map to opaque seeds;
  • never accepts caller paths, URLs, refs, mounts, images, commands, env, or runtime flags;
  • creates/cleans per-invocation writable repo copies;
  • launches caller code only inside a probe sandbox;
  • validates/canonicalizes results and returns only the closed object;
  • caps/discards stdout/stderr and private artifacts;
  • uses run-unique names/labels for orphan cleanup;
  • keeps protected diagnostics outside agent/probe mounts.

Use descriptor-relative/openat2-style beneath/no-follow access where needed. Reject symlink, hardlink, device, FIFO, socket, alternate, worktree, submodule, and parent escapes.

The trusted proxy may coordinate all seeds, but no probe receives their parent or multiple repos.

Per-invocation writable repository

For every valid request:

  1. Resolve exactly one immutable seed.
  2. Create a fresh private writable full copy or proven-isolated copy-on-write snapshot.
  3. Mount only that copy at /probe/repo:rw.
  4. Give it a distinct mount namespace, tmpfs/output, PIDs, identity, and limits.
  5. Destroy it after validation, timeout, cancellation, or teardown.

The script may create, edit, delete, stage, and commit inside its assigned copy. Mutations are ephemeral: no diff, commit, artifact, file, metadata, or safe-output intent is returned/persisted.

A full copy is the safe fallback. Copy-on-write requires proof that data, metadata, object stores, and writes cannot cross repos/invocations.

Probe sandbox

Construct a fresh root containing only:

  • minimal fixed Python runtime/rootfs;
  • submitted script at a fixed read-only path;
  • selected writable repo at /probe/repo;
  • private tmpfs/output containing /probe/out.

The sandbox:

  • has network: none or runtime-equivalent isolation;
  • cannot reach public internet, DNS, host gateway, bridge peers, proxies, or external loopback services;
  • uses a read-only root except explicit repo/tmpfs mounts;
  • never mounts proxy/Docker sockets, primary workspace/home, credentials, host /tmp, seed parent, another repo, or prior invocation;
  • runs non-root with all capabilities dropped and no-new-privileges;
  • applies a minimal Python seccomp profile;
  • enforces wall-clock, memory, CPU, PID, file-size, inode, and storage limits;
  • supports Docker, gVisor, and sbx without downgrade;
  • contains no package manager/install path.

chroot may be defense in depth, but is not the security boundary. Chroot into the constructed minimal rootfs, not directly into a Git checkout.

AWF integration points

Config/schema/types

  • src/config-file.ts: top-level type.
  • docs/awf-config.schema.json: canonical schema.
  • src/awf-config-schema.json: regenerate; do not hand-diverge.
  • src/config-mapper.ts, src/commands/build-config.ts, and WrapperConfig: map stdin/file config without CLI flags.
  • docs/awf-config-spec.md: processing, validation, credentials, CLI/skill, result, and security semantics.

Lifecycle/services

  • Add trusted staging and proxy/launcher components.
  • Reuse optional-service patterns in src/services/optional-services.ts and src/compose-generator.ts where appropriate.
  • Inject only the dedicated socket and fixed CLI into the agent.
  • Gate agent startup on successful staging and proxy readiness after staging credentials/resources are removed.
  • Extend cancellation, teardown, diagnostics, labels, orphan cleanup, and ARC/DinD path translation.
  • Keep probe execution separate from runAgentCommand; the primary agent controls the AWF job exit.

Agent image/skill

  • Add the fixed wrapper analogous to containers/agent/gh-cli-proxy-wrapper.sh, with the narrower API.
  • Generate/install the skill only when enabled.
  • Do not add MCP or generic execution endpoints.

Required tests

Config/unit

  • Accept valid file/stdin configs; reject all invalid/preflight cases.
  • Disabled/absent config produces no staging, proxy, socket, env, CLI activation, or skill.
  • Schema copies remain synchronized.
  • Repo normalization, invocation budget, framing, size, outcomes, paths, and result parsing are table-tested/fuzzed.

Lifecycle/service

  • Staging network/credentials are gone before proxy/agent readiness.
  • Proxy/probes have no network or credentials.
  • Only the socket enters the agent; no Docker control/staging paths do.
  • Runtime selection fails closed.
  • Cancellation/cleanup are idempotent and race-safe.

Security integration

Using local fixture repos where possible:

  • Probe reads/modifies its selected writable copy; mutations disappear and seed is unchanged.
  • Repo A cannot observe repo B, seed parents, prior/concurrent invocations, broker state, workspace, host, credentials, or sockets.
  • Cover traversal, symlink/hardlink/device/FIFO/socket, alternates/worktrees/submodules, and copy-on-write isolation.
  • Probe cannot reach internet, DNS, host gateway, bridge peers, proxies, or external loopback.
  • Disallowed repo is rejected when bypassing client checks.
  • Exactly three bounded unique outcomes are required and ERROR is reserved.
  • Proxy canonicalizes valid results.
  • Duplicate keys, trailing bytes, extra fields, malformed UTF-8/JSON, oversized output, invalid values, formatting, stdout/stderr, exceptions, exit codes, files, diffs, and metadata create no extra symbols.
  • All failures produce identical canonical ERROR, uniform exit status, and no stderr.
  • Parallel probes/teardown leave no private data or orphans.
  • Exercise Docker, gVisor, and sbx where available; never silently skip/downgrade a requested runtime.

Acceptance criteria

  • Top-level config works through files and stdin.
  • Enabling pre-stages repos, removes credential/network staging resources, starts an offline proxy, and exposes one CLI-backed skill.
  • CLI accepts one configured repo, exactly three outcomes, and script bytes only.
  • Proxy validates before copying a repo or launching.
  • Each invocation gets a writable copy of exactly one repo and cannot access others.
  • Sandbox has no network, credentials, Docker control, workspace, proxy socket, or unrelated mounts.
  • Script returns the closed JSON object; proxy strictly validates and canonically reserializes it.
  • Visible output is limited to three declared values plus fixed ERROR; all failures are identical ERROR with uniform status/no stderr.
  • Mutations are ephemeral and private resources are removed.
  • Invocation count is bounded; docs state two-bit content capacity and timing residual.
  • Docker, gVisor, and sbx fail closed without downgrade.
  • Disabled/absent behavior is unchanged.
  • Schema, spec, docs, unit, lifecycle, and security tests are complete.

Out of scope for v1

  • secondary LLMs, agent groups, mailboxes, or long-lived peers;
  • live gh, GitHub MCP, or probe/proxy internet;
  • safe outputs or persistence/return of repo mutations;
  • third-party packages or arbitrary images/interpreters;
  • arbitrary JSON Schema, free-form output, or more than four symbols;
  • fixed-interval release/full timing mitigation;
  • strict noninterference claims beyond the stated boundary/residuals.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions