Skip to content

feat(deploy): add Cloud Run sandbox support and streamline documentation - #80

Merged
kweinmeister merged 4 commits into
mainfrom
feat/cloud-run-sandboxing
Oct 10, 2026
Merged

kweinmeister merged 4 commits into
mainfrom
feat/cloud-run-sandboxing

Conversation

@kweinmeister

Copy link
Copy Markdown
Collaborator

Summary

Adds support for running Reach evaluations inside Google Cloud Run sandboxes, provides container build manifests, and streamlines the documentation.

Changes

  • Cloud Run Sandboxing: Added a root multi-stage Dockerfile (python:3.12-slim) and deploy/cloudrun/job.yaml for sandboxed batch runs. Secret credentials mount as a read-only volume (/secrets) to keep keys out of CLI arguments and sandbox execution logs.
  • Container Hygiene: Added .dockerignore and .gcloudignore to exclude local virtual environments and caches while packaging resident skill directories.
  • Documentation: Consolidated container engine instructions in docs/guides/sandboxing.md, replaced verbose CI bypass sections with a reference table, and cleaned up navigation titles in mkdocs.yml.
  • CLI & Tests: Hardened SDK import checks in reach doctor against missing parent modules, and anchored local configuration ignores in .gitignore.

Verification

  • Verified end-to-end execution of reach eval --auto inside a Cloud Run sandbox job.
  • All unit and documentation tests pass (uv run pytest, uv run pytest tests/test_docs.py).
  • Linters pass cleanly (uv run ruff check src tests, uv run ruff format --check src tests).

- Add root multi-stage Dockerfile and Cloud Run job spec
- Add .dockerignore and .gcloudignore for clean build contexts
- Secure Cloud Run sandbox credential handling via secret volume mounts
- Consolidate container runner docs and mkdocs navigation
- Improve doctor SDK import resilience
@github-actions

Copy link
Copy Markdown

🤖 Antigravity AI Code Review

Model: Gemini 3.8 Flash | Commit: 694bb7073f5a21d61c9695bb1dd80a09b454f957


Pull Request Code Inspection: skill-reach

Reviewer Assessment: This PR introduces Cloud Run batch sandbox execution configuration, image build packaging, documentation restructuring, and hardening in reach doctor. While the general approach is sound, there are several critical security and packaging issues (specifically around secret exposure, container packaging efficiency, and potential test drift) that must be addressed prior to merging.


🛑 [BLOCKER]

1. Security & Process Leakage: Plaintext API Key Exposure in Container Subshell

In deploy/cloudrun/job.yaml (and repeated in docs/guides/sandboxing.md), the job invokes:

-- /bin/bash -c 'export GEMINI_API_KEY="$(cat /secrets/GEMINI_API_KEY)" && reach eval --auto --yes'

While reading from a mounted file keeps the key out of the top-level command-line arguments in job.yaml, executing $(cat /secrets/GEMINI_API_KEY) in bash performs command substitution. On Linux environments (and depending on process accounting or tracing), command substitutions and environment assignments in subshells can expose secrets via /proc/<pid>/cmdline or /proc/<pid>/environ of child processes, or risk unquoted expansion quirks (e.g., if keys contain special characters).

Furthermore, Python SDKs and CLI tools can read secrets directly from files or file paths. If reach supports key reading via environment or standard configuration, pass the file path via an environment variable or use safe file streaming. Even better, avoid spawning nested bash -c subshells inside sandbox do by reading the secret inside Python or pointing the runtime directly to the mounted file path.

If GEMINI_API_KEY must be populated into the environment for child runtimes, avoid the command-substitution subshell pattern and load it cleanly:

# In deploy/cloudrun/job.yaml
              command:
                - /usr/local/gcp/bin/sandbox
                - do
                - --allow-egress
                - --write
                - --env
                - PATH=/opt/venv/bin:/usr/local/gcp/bin:/usr/bin:/bin
                - --env
                - REACH_YES=1
                - --mount
                - type=bind,source=/workspace,destination=/workspace
                - --mount
                - type=bind,source=/secrets,destination=/secrets,readonly
                - --workdir
                - /workspace
                - --
                - /bin/sh
                - -c
                - 'set -a; GEMINI_API_KEY=$(< /secrets/GEMINI_API_KEY); reach eval --auto --yes'

(Notice the use of $(< /secrets/GEMINI_API_KEY) which is a shell builtin in Bash, avoiding spawning a separate cat process).


2. Root Execution & Image Context Pollution in Dockerfile

In Dockerfile:

# Copy skill catalog and configuration
COPY . /workspace

CMD ["reach", "eval", "--yes"]
  1. Running as root: The container executes as root by default (UID 0). When agents probe skills inside /workspace, running as root grants excessive permissions within the container filesystem. Create a non-root user (e.g. reach / UID 10001).
  2. Copying whole context COPY . /workspace: This copies all local files into the runtime image if .dockerignore misses anything, and invalidates Docker layer caching whenever any local file touches the repo. Since dependencies were already installed via uv pip install ".[antigravity-sdk]" in builder stage, copying source code into /workspace can lead to stale code or shadowed packages.

Recommended fix:

# ── Stage 2: Minimal Runtime ────────────────────────────────────────────────
FROM python:3.12-slim AS runtime

RUN apt-get update && apt-get install -y --no-install-recommends \
    git \
    ca-certificates \
    curl \
    && rm -rf /var/lib/apt/lists/*

# Create unprivileged runner user
RUN groupadd -g 10001 reach && \
    useradd -u 10001 -g reach -m -d /home/reach reach

COPY --from=builder /opt/venv /opt/venv

ENV PATH="/opt/venv/bin:/usr/local/gcp/bin:$PATH" \
    PYTHONUNBUFFERED=1

WORKDIR /workspace
RUN chown -R reach:reach /workspace

USER reach

CMD ["reach", "eval", "--yes"]

⚠️ [WARNING]

1. .gitignore Anchor May Break Subdirectory Configs

In .gitignore:

-reach.toml
-reach.local.toml
+/reach.toml
+/reach.local.toml

Anchoring with /reach.toml ignores reach.toml strictly at the repository root. If nested skill testbeds, benchmark fixtures, or workspace subpackages contain localized reach.local.toml or reach.toml overrides, they will now be tracked by Git and may accidentally be committed.

Unless this was intended specifically for fixtures:

# Ignore reach configs across all workspaces and subdirectories
reach.toml
reach.local.toml

2. Broad Exception Handling in _check_sdk

In src/reach/cli/doctor.py:

    try:
        spec = importlib.util.find_spec(module_name)
    except (ModuleNotFoundError, ValueError):
        spec = None

importlib.util.find_spec can also raise AttributeError (when parent modules are not packages) or ImportError if parent initialization fails. Catching (ImportError, AttributeError, ValueError) is more defensive across arbitrary Python namespace packages.

def _check_sdk(name: str, module_name: str, required_by: str) -> CheckResult:
    """Check whether a Python SDK dependency is importable."""
    try:
        spec = importlib.util.find_spec(module_name)
    except (ImportError, AttributeError, ValueError):
        spec = None

    if spec is None:
        return CheckResult(
            category="Agent Runtime Drivers",
            name=name,
            status="warn",
            detail=f"{name} not installed (needed for {required_by})",
        )
    return CheckResult(
        category="Agent Runtime Drivers",
        name=name,
        status="ok",
        detail="installed and importable",
    )

3. Missing tests/test_config.py Fixture Teardown / Dependency

In tests/test_config.py:

def test_bundled_config_exists_and_provides_baseline_models() -> None:
    """Verify that bundled reach.toml exists and provides required default profiles."""
    from reach.config import BUNDLED_CONFIG_PATH, load_config
    from reach.runtime.profiles import model_profile
...

This test was added without being mentioned in the PR description or changes list. While verifying the bundled configuration is great, please ensure:

  1. model_profile("claude-sonnet-5") does not make remote network requests or expect external state during CI test execution.
  2. In-line imports inside tests should either be module-level imports (standard in pytest) or clearly justified if avoiding circular dependencies.

💡 [SUGGESTION]

1. Hardcoded CPU and Memory Limit Strings in job.yaml

In deploy/cloudrun/job.yaml:

              resources:
                limits:
                  cpu: "2"
                  memory: 2Gi

For large evaluation sweeps probing models with local tokenizers or multiple subagents, 2Gi may lead to container OOM kills. Consider documenting recommended minimum resource quotas in deploy/cloudrun/job.yaml comments or docs/guides/sandboxing.md.

2. Imperative Mood in Docstrings

In tests/cli/test_doctor.py:

  • """Verify SDK module checks handle found, missing, and uninstalled namespaces."""
    Per Google Python style guide for docstrings:
  • Use imperative mood: """Verify that SDK module checks handle found, missing, and uninstalled namespaces.""" or """Check that SDK module inspection handles missing parent packages gracefully."""

@kweinmeister

Copy link
Copy Markdown
Collaborator Author

@agy /review

@github-actions

Copy link
Copy Markdown

🤖 Antigravity AI Code Review

Model: Gemini 3.8 Flash | Commit: 4180e94ed2a66f3fc80350277076b06e7365b881


Senior Code Review: Pull Request #4180e94

Repository: skill-reach
Reviewer: Automated Senior Google Code Reviewer
Status: 🛑 Changes requested (Blockers present)


Summary of Changes Under Inspection

The PR introduces Cloud Run Sandbox execution manifests (Dockerfile, deploy/cloudrun/job.yaml), container build exclude manifests (.dockerignore, .gcloudignore), runtime support for *_FILE secret resolution (resolve_env_secret), documentation consolidation in docs/guides/sandboxing.md / mkdocs.yml, and toolchain lockfile bumps (biome, prettier).

While the credential-file resolution mechanism and documentation cleanup are sound improvements, there are critical permission and container filesystem flaws that prevent safe execution in Cloud Run sandboxes.


🛑 [BLOCKER] Issues

1. Insecure World-Writable Directory (chmod 1777 /workspace) in Production Container

  • File: Dockerfile
  • Problem:
    Setting mode 1777 (sticky bit world-writable) on /workspace in the base image leaves the workspace exposed to arbitrary writes or race conditions across different local or shared host container runs. Furthermore, files copied into /workspace/.agents/ via COPY --chown=reach:reach inherit the sticky directory semantics, allowing processes running as unprivileged users or guest sandbox UIDs to delete or replace skill definitions if not tightly scoped.
  • Fix:
    Ensure /workspace is owned cleanly by reach:reach (UID 10001) with standard directory permissions (0755 or 0775). Cloud Run's sandbox jail runtime operates in its own isolated user namespace where root/sandbox mappings handle bind mounts without requiring world-writable permissions on the image filesystem.
# Proposed replacement for Dockerfile (Lines 42-46):
RUN groupadd -g 10001 reach && \
    useradd -u 10001 -g reach -m -d /home/reach reach && \
    mkdir -p /workspace && \
    chown -R reach:reach /workspace && \
    chmod 0755 /workspace

2. Self-Referential Circular Bind Mount in Sandbox Invocation

  • File: deploy/cloudrun/job.yaml & docs/guides/sandboxing.md
  • Problem:
    In deploy/cloudrun/job.yaml, the container specifies:
    - --mount
    - type=bind,source=/workspace,destination=/workspace
    However, in Cloud Run's execution environment without an outer volume mount, /workspace is a directory in the container's overlay rootfs. Cloud Run sandbox isolation forbids binding an unmounted host overlay path directly over itself unless an underlying ephemeral volume (such as an emptyDir tmpfs or Cloud Storage volume) is mounted at that host path.
    Furthermore, the job specifies --write without designating an isolated target scratch space, which causes sandbox initialization failures or unpersisted writes.
  • Fix:
    Add an emptyDir scratch volume to the Job specification and mount it to /workspace (or /tmp), or use --write for ephemeral sandbox scratch space and mount /workspace read-only. For batch jobs outputting artifacts to .reach/, declare an emptyDir volume:
# In deploy/cloudrun/job.yaml:
            volumeMounts:
              - name: workspace-volume
                mountPath: /workspace
              - name: gemini-secret
                mountPath: /secrets
                readOnly: true
...
          volumes:
            - name: workspace-volume
              emptyDir:
                medium: Memory
                sizeLimit: 1Gi
            - name: gemini-secret
              secret:
                secretName: GEMINI_API_KEY
                items:
                  - key: latest
                    path: GEMINI_API_KEY

3. Unsafe / Unanchored Symlink & Path Resolution in resolve_env_secret

  • File: src/reach/runtime/_env.py
  • Problem:
    resolve_env_secret performs:
    Path(file_path).expanduser().read_text(encoding="utf-8").strip()
    While wrapping in try ... except OSError catches missing files and permission errors, it does not check file type (e.g. attempting to read FIFO pipes, character devices like /dev/urandom or /dev/zero, or unbounded directory streams will hang or exhaust memory during startup).
    Additionally, there is no size cap on the secret file read; reading an unexpectedly large file could cause unbounded memory consumption.
  • Fix:
    Verify the path is a regular file (is_file()) and limit reading to a reasonable max size (e.g. 16 KB for API keys and tokens):
def resolve_env_secret(env: Mapping[str, str], *var_names: str, max_bytes: int = 16384) -> str | None:
    """Resolve a secret value from direct environment variables or ``*_FILE`` paths."""
    for name in var_names:
        if val := env.get(name):
            return val
        if file_path := env.get(f"{name}_FILE"):
            try:
                p = Path(file_path).expanduser().resolve(strict=True)
                if not p.is_file():
                    continue
                with p.open("r", encoding="utf-8") as f:
                    content = f.read(max_bytes).strip()
                    if content:
                        return content
            except (OSError, UnicodeDecodeError):
                continue
    return None

⚠️ [WARNING] Issues

1. Inconsistent Secret Scrubbing in BaseAgentDriver.build_env

  • File: src/reach/runtime/__init__.py
  • Problem:
    When self.api_key_env_var is resolved from resolve_env_secret(env, self.api_key_env_var), env.pop(f"{self.api_key_env_var}_FILE", None) is popped, but if an explicit api_key was provided via self.options.api_key (line 949), f"{self.api_key_env_var}_FILE" is not popped from env. If an agent runtime inspects environment variables, it may see mismatched credentials or forward stale *_FILE paths down into sandboxed child processes.
  • Fix:
    Ensure ${VAR}_FILE is always purged once the primary key is populated:
        if (api_key := getattr(self.options, "api_key", None)) is not None and self.api_key_env_var:
            env[self.api_key_env_var] = str(api_key)
            env.pop(f"{self.api_key_env_var}_FILE", None)
        elif (
            self.api_key_env_var
            and self.api_key_env_var not in env
            and (resolved := resolve_env_secret(env, self.api_key_env_var))
        ):
            env[self.api_key_env_var] = resolved
            env.pop(f"{self.api_key_env_var}_FILE", None)

2. .gcloudignore Commented-out Include Directive

  • File: .gcloudignore
  • Problem:
    # Inherit rules from .gitignore
    #!include:.gitignore
    
    The #!include:.gitignore line is commented out. In standard .gcloudignore syntax, #!include:.gitignore is the active directive (the #! prefix indicates a special preprocessor command, not a comment). Commenting it out duplicates ignore management between .gitignore and .gcloudignore and risks drift where files ignored by Git are inadvertently packed by gcloud builds submit.
  • Fix:
    Uncomment #!include:.gitignore:
# Inherit rules from .gitignore
#!include:.gitignore

💡 [SUGGESTION] Improvements

1. Imperative Mood Docstrings

  • File: src/reach/runtime/_env.py & src/reach/cli/doctor.py
  • Problem:
    Google Python Style Guide and PEP 257 require public and module-level function docstrings to be written in the imperative mood (e.g. "Resolve secret value..." rather than "Resolve a secret value...", and "Check if environment variable..." rather than "Check if an environment variable...").
  • Fix:
    def resolve_env_secret(env: Mapping[str, str], *var_names: str) -> str | None:
        """Resolve secret value from direct environment variables or ``*_FILE`` paths."""

2. Missing Prettier Target Extension Sync

  • File: .github/dependabot.yml
  • Problem:
    The PR adds npm ecosystem updates to Dependabot for @biomejs/biome and prettier. Ensure CI runs npm run check or equivalent formatting check across markdown and json files alongside ruff in GitHub Actions.

Summary Checklist Before Merge

  • Remove chmod 1777 from Dockerfile and keep UID 10001 ownership.
  • Fix bind mount and volume configuration in deploy/cloudrun/job.yaml.
  • Add is_file() check and file size read bounds to resolve_env_secret().
  • Always strip *_FILE environment variables when setting api_key in BaseAgentDriver.build_env.
  • Uncomment #!include:.gitignore in .gcloudignore.

@kweinmeister

Copy link
Copy Markdown
Collaborator Author

@agy /review

@github-actions

Copy link
Copy Markdown

🤖 Antigravity AI Code Review

Model: Gemini 3.8 Flash | Commit: b23a6145e2523be5fa6e5345f0e047f5873c3d11


Code Review: Pull Request Inspection

Repository: skill-reach
Commit: b23a6145e2523be5fa6e5345f0e047f5873c3d11
Reviewer: Senior Google Software Engineer (Automated Review)


Executive Summary

This PR introduces valuable infrastructure for running Reach evaluations inside Cloud Run sandboxes and adds support for file-mounted secrets (*_FILE). However, there are two blocking security and architectural issues:

  1. Invalid Kubernetes / Cloud Run Knative Schema: sandboxLauncher: true is placed inside the container specification in deploy/cloudrun/job.yaml and documentation snippets, which causes API validation failure on deployment.
  2. Subprocess Isolation Breach in Environment Sanitizer: sanitize_subprocess_env proactively resolves all provider secret files and mutates the environment in place, unintentionally leaking ambient API keys to child processes.
  3. Attribute Access Regression in AgentRuntime.build_env: Direct access to self.api_key without fallback breaks runtime drivers where api_key is stored under options.

🛑 [BLOCKER] Must Fix Before Merge


1. Invalid Cloud Run Container Specification Schema in deploy/cloudrun/job.yaml

  • Location: deploy/cloudrun/job.yaml#L17 (and docs/guides/sandboxing.md#L221, docs/guides/sandboxing.md#L309)
  • Issue: In the Cloud Run Knative API (run.googleapis.com/v1), Container conforms to corev1.Container. The field sandboxLauncher does not exist on the container specification. Attempting to deploy with gcloud run jobs replace deploy/cloudrun/job.yaml will fail schema validation (unknown field "sandboxLauncher" in io.k8s.api.core.v1.Container).
  • Fix: In declarative YAML, Cloud Run second-generation sandbox launcher execution is activated via the template metadata annotation run.googleapis.com/sandbox-launcher: "true".
# deploy/cloudrun/job.yaml
apiVersion: run.googleapis.com/v1
kind: Job
metadata:
  name: reach-eval-job
  annotations:
    run.googleapis.com/launch-stage: BETA
spec:
  template:
    metadata:
      annotations:
        run.googleapis.com/sandbox-launcher: "true"
    spec:
      template:
        spec:
          maxRetries: 0
          timeoutSeconds: 1800
          containers:
            - name: reach-eval
              image: LOCATION-docker.pkg.dev/PROJECT_ID/REPO_NAME/reach-runner:latest
              volumeMounts:
                - name: gemini-secret
                  mountPath: /secrets
                  readOnly: true
              command:
                - /usr/local/gcp/bin/sandbox
              args:
                - do
                - --allow-egress
                - --write
                - --env
                - PATH=/opt/venv/bin:/usr/local/gcp/bin:/usr/local/bin:/usr/bin:/bin
                - --env
                - REACH_YES=1
                - --env
                - GEMINI_API_KEY_FILE=/secrets/GEMINI_API_KEY
                - --mount
                - type=bind,source=/secrets,destination=/secrets,readonly
                - --workdir
                - /workspace
                - --
                - reach
                - eval
                - --agent
                - antigravity-sdk
                - --auto
                - --yes
              resources:
                limits:
                  cpu: "2"
                  memory: 2Gi
              env:
                - name: REACH_YES
                  value: "1"
          volumes:
            - name: gemini-secret
              secret:
                secretName: GEMINI_API_KEY
                items:
                  - key: latest
                    path: GEMINI_API_KEY

2. Security: Inappropriate Secret Materialization in sanitize_subprocess_env

  • Location: src/reach/runtime/_env.py#L117-L121
  • Issue: The primary contract of sanitize_subprocess_env is to strip sensitive ambient tokens and credentials before launching untrusted child processes. The newly added loop:
    for key in _ALL_PROVIDER_KEY_ENV_VARS:
        if not env.get(key) and (resolved := resolve_env_secret(env, key)):
            env[key] = resolved
        env.pop(f"{key}_FILE", None)
    violates the principle of least privilege:
    1. If blocked_env_vars is customized by the caller (or does not include every provider key), this reads all provider secret files from disk and injects them into the child environment.
    2. Mutating env in place causes side-effects for callers sharing the dictionary.
    3. Secret resolution should occur specifically within the active agent driver's build_env or provider resolution logic, while sanitize_subprocess_env should strictly strip credentials and pointer variables.
  • Fix: Remove the proactive resolution of _ALL_PROVIDER_KEY_ENV_VARS from sanitize_subprocess_env. Instead, ensure all *_FILE pointer variables for blocked keys are stripped:
# src/reach/runtime/_env.py

def sanitize_subprocess_env(
    env: Mapping[str, str],
    keep: Iterable[str] = (),
    blocked_env_vars: Iterable[str] | None = None,
) -> dict[str, str]:
    """Strip sensitive ambient credentials, tokens, and secret file pointers from environment."""
    keep_set = set(keep)
    vertex_env_active = env.get("CLAUDE_CODE_USE_VERTEX") == "1" or has_agy_vertex_env(env)
    if blocked_env_vars is None and vertex_env_active:
        keep_set.add("GOOGLE_APPLICATION_CREDENTIALS")

    effective_blocked = (
        set(blocked_env_vars) if blocked_env_vars is not None else _DEFAULT_BLOCKED_SET
    )

    # Block both the credential variables and their *_FILE file pointers
    blocked_patterns = effective_blocked | {f"{var}_FILE" for var in effective_blocked}

    return {
        k: v
        for k, v in env.items()
        if (k not in blocked_patterns or k in keep_set)
    }

3. Potential AttributeError on self.api_key in AgentRuntime.build_env

  • Location: src/reach/runtime/__init__.py#L836
  • Issue: The refactored build_env moved from CliAgentRuntime to base AgentRuntime accesses self.api_key:
    if (api_key := self.api_key) is not None:
    AgentRuntime does not declare api_key: str | None = None nor does it define an @property def api_key(self). Previously, CliAgentRuntime safely inspected getattr(self.options, "api_key", None). Accessing self.api_key directly will raise an AttributeError for runtimes where api_key is encapsulated inside self.options or not defined.
  • Fix: Add a dedicated property on AgentRuntime or safely resolve through getattr(self.options, "api_key", None) with a fallback to getattr(self, "api_key", None):
# src/reach/runtime/__init__.py

    @property
    def api_key(self) -> str | None:
        """Return the API key configured on this runtime or its options."""
        return getattr(self.options, "api_key", None)

    def build_env(self, workdir: Path | None = None) -> dict[str, str]:
        """Assemble process environment with API keys and workspace overrides."""
        del workdir
        env = dict(os.environ)
        if (home_dir := getattr(self.options, "home_dir", None)) is not None:
            env["HOME"] = str(home_dir)
        if api_key_var := self.api_key_env_var:
            if (api_key := self.api_key) is not None:
                env[api_key_var] = str(api_key)
            elif not env.get(api_key_var) and (resolved := resolve_env_secret(env, api_key_var)):
                env[api_key_var] = resolved
            env.pop(f"{api_key_var}_FILE", None)
        return sanitize_subprocess_env(env, blocked_env_vars=self.blocked_env_vars)

⚠️ [WARNING] Needs Attention


4. Path Canonicalization & Safe Boundaries in resolve_env_secret

  • Location: src/reach/runtime/_env.py#L77-L88
  • Issue: Path(file_path).expanduser() accepts relative paths (e.g. GEMINI_API_KEY_FILE=../../key.txt). When evaluations navigate or change working directories during agent runs, relative paths resolve inconsistently against Path.cwd(). In addition, reading raw bytes and decoding can produce UnicodeDecodeError if a file is truncated mid-codepoint at max_bytes.
  • Fix: Require absolute paths (or explicitly resolve them) and handle decoding errors gracefully:
# src/reach/runtime/_env.py

def resolve_env_secret(
    env: Mapping[str, str],
    *var_names: str,
    max_bytes: int = 16384,
) -> str | None:
    """Resolve a secret value from direct environment variables or ``*_FILE`` paths."""
    for name in var_names:
        if val := env.get(name):
            return val
    for name in var_names:
        if file_path := env.get(f"{name}_FILE"):
            try:
                path = Path(file_path).expanduser().resolve()
                if not path.is_file():
                    continue
                with path.open("r", encoding="utf-8", errors="replace") as fh:
                    content = fh.read(max_bytes).strip()
                if content:
                    return content
            except OSError:
                continue
    return None

5. Local Docker Execution Runs as Root by Default

  • Location: Dockerfile#L42-L57
  • Issue: The Dockerfile creates user reach (UID 10001) but omits a USER reach directive to allow Cloud Run's outer launcher to configure network namespaces. However, users following the updated guide in docs/guides/sandboxing.md for standard Docker runs:
    docker run --rm -it ... python:3.12-slim bash -c "pip install skill-reach && reach eval"
    will execute as root on the host if using the pre-built image without passing --user reach.
  • Fix: In docs/guides/sandboxing.md, make sure the local standalone Docker snippet explicitly documents --user reach:reach when running the repository image:
    docker run --rm -it --user reach:reach -v "$PWD:/workspace" ...

6. Ephemeral Artifact Loss in Batch Cloud Run Job

  • Location: deploy/cloudrun/job.yaml
  • Issue: The Cloud Run job runs with --write (tmpfs overlay) and terminates. All .reach/ evaluation outputs, sweep metrics, and trajectories written to /workspace/.reach/ are lost when the container exits unless persisted.
  • Fix: Add a note or volume mount pattern in job.yaml indicating how to mount a Cloud Storage bucket via Cloud Storage FUSE or export artifacts via post-run webhook/step.

💡 [SUGGESTION] Polish & Style Nits


7. Update Documentation YAML Examples to Match Annotation Syntax

  • Location: docs/guides/sandboxing.md#L221 and docs/guides/sandboxing.md#L309
  • Suggestion: Replace the occurrences of sandboxLauncher: true inside container blocks with the corresponding template annotations so that readers copying the examples into their own manifests do not hit validation errors.

Suggested Merge Checklist

  • Move run.googleapis.com/sandbox-launcher: "true" to spec.template.metadata.annotations in deploy/cloudrun/job.yaml.
  • Remove proactive _ALL_PROVIDER_KEY_ENV_VARS loop from sanitize_subprocess_env to avoid secret leakage.
  • Add @property def api_key on AgentRuntime to prevent AttributeError.
  • Update resolve_env_secret to use .resolve() and safe text decoding.
  • Align docs/guides/sandboxing.md manifests with the updated Cloud Run annotations.

@kweinmeister
kweinmeister merged commit 1ec2a8c into main Oct 10, 2026
9 checks passed
@kweinmeister
kweinmeister deleted the feat/cloud-run-sandboxing branch October 10, 2026 13:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant