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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 57 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,14 @@ PROCEED or SKIP decision with reasoning.
**Usage:**

```yaml
# Check the target repo out into a subdirectory; the sandbox rejects
# the workspace root itself. See the "autosolve sandbox" section below.
- uses: actions/checkout@v6
with:
path: repo
- uses: cockroachdb/actions/autosolve/assess@v0
with:
working_directory: ./repo
system_prompt: "Assess whether this issue can be resolved automatically."
context_vars: "ISSUE_TITLE,ISSUE_BODY"
env:
Expand All @@ -138,7 +144,8 @@ PROCEED or SKIP decision with reasoning.
| `model` | `claude-opus-4-6` | Claude model ID |
| `blocked_paths` | `""` | Comma-separated path prefixes that cannot be modified (case-sensitive). `.github/` is always blocked. |
| `log_level` | `error` | Controls Claude output in the step log: `error` (status only), `info` (result summary, permission denial warnings), `debug` (stream everything). |
| `working_directory` | `.` | Directory to run in (relative to workspace root) |
| `working_directory` | **required** | Directory the action runs in (relative to `GITHUB_WORKSPACE`). Must be a strict subdirectory — the workspace root is rejected. See [autosolve sandbox](#autosolve-sandbox). |
| `read_paths` | `""` | Comma-separated extra host paths (files or directories) to bind read-only into the sandbox. Each entry may be absolute or relative to `GITHUB_WORKSPACE`, and must exist when the action runs. See [autosolve sandbox](#autosolve-sandbox). |

Comment thread
fantapop marked this conversation as resolved.
**Outputs:**

Expand All @@ -164,8 +171,14 @@ enforcement, sensitive file detection, and token usage tracking.
**Usage:**

```yaml
# Check the target repo out into a subdirectory; the sandbox rejects
# the workspace root itself. See the "autosolve sandbox" section below.
- uses: actions/checkout@v6
with:
path: repo
- uses: cockroachdb/actions/autosolve/implement@v0
with:
working_directory: ./repo
system_prompt: "Fix the issue described in the environment variables."
context_vars: "ISSUE_TITLE,ISSUE_BODY"
fork_owner: my-bot
Expand Down Expand Up @@ -204,7 +217,8 @@ enforcement, sensitive file detection, and token usage tracking.
| `commit_signature` | `Co-Authored-By: Claude <noreply@anthropic.com>` | Signature line appended to commit messages |
| `pr_footer` | [see below](#pr_footer-default) | Footer appended to the PR body |
| `log_level` | `error` | Controls Claude output in the step log: `error` (status only), `info` (result summary, permission denial warnings), `debug` (stream everything). |
| `working_directory` | `.` | Directory to run in (relative to workspace root) |
| `working_directory` | **required** | Directory the action runs in (relative to `GITHUB_WORKSPACE`). Must be a strict subdirectory — the workspace root is rejected. See [autosolve sandbox](#autosolve-sandbox). |
| `read_paths` | `""` | Comma-separated extra host paths (files or directories) to bind read-only into the sandbox. Each entry may be absolute or relative to `GITHUB_WORKSPACE`, and must exist when the action runs. See [autosolve sandbox](#autosolve-sandbox). |

<a id="allowed_tools-default"></a>
> Default `allowed_tools`:
Expand Down Expand Up @@ -262,6 +276,47 @@ For organizations using SAML/SSO, if a classic token is used, it must be
authorized for the organization that owns the target repository. See
[GitHub docs on SSO authorization](https://docs.github.com/en/enterprise-cloud@latest/authentication/authenticating-with-saml-single-sign-on/authorizing-a-personal-access-token-for-use-with-saml-single-sign-on).

<a id="autosolve-sandbox"></a>
### autosolve sandbox

Both `autosolve/assess` and `autosolve/implement` run Claude inside a
[bubblewrap](https://github.com/containers/bubblewrap) filesystem sandbox
that denies access to anything not explicitly bound. This blocks
inadvertent reads of credentials or other repositories that earlier
steps may have dropped into `GITHUB_WORKSPACE` or `RUNNER_TEMP`.

**What's bound:**

| Path | Mode | Notes |
| ------------------------------------------ | ---- | ------------------------------------------------------------------------------ |
| `working_directory` | RW | Claude's cwd; the only writable host path you control. |
| `read_paths` | RO | Opt-in extras for shared schemas, generated protos, or other references. |
| `GOOGLE_APPLICATION_CREDENTIALS` (file) | RO | Auto-bound when set so Claude can refresh Vertex tokens. |
| `/usr`, `/etc`, `/lib`, `/lib64`, `/opt` | RO | OS and tooling. Contains no per-user data. |
| `/run/systemd/resolve` | RO | DNS resolver state (only when present). `/run` itself is **not** bound, so `/run/docker.sock` stays out of reach. |
| Private `/tmp` | RW | tmpfs; discarded after the run. |

Everything else is denied — including the workspace root, `RUNNER_TEMP`,
`/home/runner`, ssh keys, and credential files dropped by other steps.

**Caveats:**

- **Ubuntu 24.04+ only.** bubblewrap is Linux-only, and the setup step
requires (and disables) the
`kernel.apparmor_restrict_unprivileged_userns` sysctl so unprivileged
user namespaces work. The setup fails fast on runners that do not
expose this sysctl. Disabling it on an ephemeral hosted runner is
fine; persistent self-hosted runners should evaluate the tradeoff
before adopting the action.
- **`working_directory` must be a strict subdirectory** of
`GITHUB_WORKSPACE`. Check the target repo into a path like `./repo`
and pass `working_directory: ./repo`. The workspace root itself is
rejected because other actions (e.g. `google-github-actions/auth`)
drop credential files there.
- **The host network namespace is shared** (`--share-net`). The sandbox
enforces filesystem isolation, not network isolation; Claude can
reach the same network the runner can.

### get-workflow-ref

Resolves the git ref that a caller used to invoke a reusable workflow by parsing
Expand Down
62 changes: 59 additions & 3 deletions autosolve/assess/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,27 @@ inputs:
required: false
default: "error"
working_directory:
description: Directory to run in (relative to workspace root). Defaults to workspace root.
description: >
Directory the action runs in, relative to GITHUB_WORKSPACE. Required.
Must be a strict subdirectory of GITHUB_WORKSPACE — the workspace
root itself is rejected because other actions (e.g.
google-github-actions/auth) drop credential files there. Check the
target repo out into a subdirectory like ./repo and set
working_directory to that path. This directory is the only
caller-controlled writable location bound into the bubblewrap
sandbox; Claude additionally gets a private writable /tmp
(tmpfs, discarded after the run).
required: true
read_paths:
description: >
Comma-separated extra host paths (files or directories) to bind
read-only into the sandbox. Use for shared schemas, generated
protos, or other references the task needs to consult but not
modify. Paths may be absolute or relative to GITHUB_WORKSPACE.
The file referenced by GOOGLE_APPLICATION_CREDENTIALS, if set,
is auto-bound and does not need to be listed here.
required: false
default: "."
default: ""

outputs:
assessment:
Expand All @@ -73,16 +91,48 @@ outputs:
runs:
using: "composite"
steps:
- name: Set up sandbox
shell: bash
run: |
source "${{ github.action_path }}/../../actions_helpers.sh"
if [[ "$RUNNER_OS" != "Linux" ]]; then
log_error "autosolve requires Linux runners (the sandbox uses bubblewrap, which is Linux-only)"
exit 1
fi
sudo apt-get update
sudo apt-get install --yes bubblewrap
# Ubuntu 24.04+ restricts unprivileged user namespaces by default,
# which blocks bwrap's uid mapping. Disabling this on an ephemeral
# hosted runner is acceptable; persistent self-hosted runners
# should evaluate the tradeoff before adopting this action.
# We require the sysctl to be present (rather than skipping it on
# older Ubuntu) so the supported runner surface is unambiguous —
# silent skips would let the action limp along on untested kernels
# and surface confusing bwrap failures later.
if [ ! -e /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]; then
log_error "autosolve requires Ubuntu 24.04+ runners: kernel.apparmor_restrict_unprivileged_userns sysctl is missing"
exit 1
fi
sudo sysctl --write kernel.apparmor_restrict_unprivileged_userns=0
Comment thread
fantapop marked this conversation as resolved.
mkdir -p "$RUNNER_TEMP/autosolve-scratch/home"

- name: Set up Claude CLI
shell: bash
# The sandbox ro-binds /usr unconditionally, so installing into
# /usr/local/bin keeps the claude binary reachable inside bwrap
# without per-binary bind plumbing in the Go code. The native
# installer drops it under ~/.local/bin and offers no override
# flag or env var, so we move it ourselves.
run: |
if command -v roachdev >/dev/null; then
printf '#!/bin/sh\nexec roachdev claude -- "$@"\n' > /usr/local/bin/claude
chmod +x /usr/local/bin/claude
echo "Claude CLI: using roachdev wrapper ($(roachdev version))"
else
curl --fail --silent --show-error --location https://claude.ai/install.sh | bash -s -- "$CLAUDE_CLI_VERSION"
echo "Claude CLI installed: $(claude --version)"
install --mode=0755 ~/.local/bin/claude /usr/local/bin/claude
rm ~/.local/bin/claude
echo "Claude CLI installed to /usr/local/bin/claude: $(claude --version)"
fi
env:
CLAUDE_CLI_VERSION: ${{ inputs.claude_cli_version }}
Expand Down Expand Up @@ -115,6 +165,11 @@ runs:
- name: Run assessment
id: assess
shell: bash
# working-directory is load-bearing — the autosolve binary reads
# the cwd via os.Getwd() in internal/config/loadSandboxConfig and
# validates it against GITHUB_WORKSPACE. Removing this line will
# cause validation to fail confusingly (cwd defaults to the
# workspace root, which the binary explicitly rejects).
working-directory: ${{ inputs.working_directory }}
run: $RUNNER_TEMP/autosolve assess
env:
Expand All @@ -125,3 +180,4 @@ runs:
INPUT_MODEL: ${{ inputs.model }}
INPUT_BLOCKED_PATHS: ${{ inputs.blocked_paths }}
INPUT_LOG_LEVEL: ${{ inputs.log_level }}
INPUT_READ_PATHS: ${{ inputs.read_paths }}
2 changes: 1 addition & 1 deletion autosolve/go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/cockroachdb/actions/autosolve

go 1.26
go 1.25
62 changes: 59 additions & 3 deletions autosolve/implement/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,27 @@ inputs:
required: false
default: "error"
working_directory:
description: Directory to run in (relative to workspace root). Defaults to workspace root.
description: >
Directory the action runs in, relative to GITHUB_WORKSPACE. Required.
Must be a strict subdirectory of GITHUB_WORKSPACE — the workspace
root itself is rejected because other actions (e.g.
google-github-actions/auth) drop credential files there. Check the
target repo out into a subdirectory like ./repo and set
working_directory to that path. This directory is the only
caller-controlled writable location bound into the bubblewrap
sandbox; Claude additionally gets a private writable /tmp
(tmpfs, discarded after the run).
required: true
read_paths:
description: >
Comma-separated extra host paths (files or directories) to bind
read-only into the sandbox. Use for shared schemas, generated
protos, or other references the task needs to consult but not
modify. Paths may be absolute or relative to GITHUB_WORKSPACE.
The file referenced by GOOGLE_APPLICATION_CREDENTIALS, if set,
is auto-bound and does not need to be listed here.
required: false
default: "."
default: ""

outputs:
status:
Expand All @@ -142,16 +160,48 @@ outputs:
runs:
using: "composite"
steps:
- name: Set up sandbox
shell: bash
run: |
source "${{ github.action_path }}/../../actions_helpers.sh"
if [[ "$RUNNER_OS" != "Linux" ]]; then
log_error "autosolve requires Linux runners (the sandbox uses bubblewrap, which is Linux-only)"
exit 1
fi
sudo apt-get update
sudo apt-get install --yes bubblewrap
# Ubuntu 24.04+ restricts unprivileged user namespaces by default,
# which blocks bwrap's uid mapping. Disabling this on an ephemeral
# hosted runner is acceptable; persistent self-hosted runners
# should evaluate the tradeoff before adopting this action.
# We require the sysctl to be present (rather than skipping it on
# older Ubuntu) so the supported runner surface is unambiguous —
# silent skips would let the action limp along on untested kernels
# and surface confusing bwrap failures later.
if [ ! -e /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]; then
log_error "autosolve requires Ubuntu 24.04+ runners: kernel.apparmor_restrict_unprivileged_userns sysctl is missing"
exit 1
fi
sudo sysctl --write kernel.apparmor_restrict_unprivileged_userns=0
Comment thread
fantapop marked this conversation as resolved.
mkdir -p "$RUNNER_TEMP/autosolve-scratch/home"

- name: Set up Claude CLI
shell: bash
# The sandbox ro-binds /usr unconditionally, so installing into
# /usr/local/bin keeps the claude binary reachable inside bwrap
# without per-binary bind plumbing in the Go code. The native
# installer drops it under ~/.local/bin and offers no override
# flag or env var, so we move it ourselves.
run: |
if command -v roachdev >/dev/null; then
printf '#!/bin/sh\nexec roachdev claude -- "$@"\n' > /usr/local/bin/claude
chmod +x /usr/local/bin/claude
echo "Claude CLI: using roachdev wrapper ($(roachdev version))"
else
curl --fail --silent --show-error --location https://claude.ai/install.sh | bash -s -- "$CLAUDE_CLI_VERSION"
echo "Claude CLI installed: $(claude --version)"
install --mode=0755 ~/.local/bin/claude /usr/local/bin/claude
rm ~/.local/bin/claude
echo "Claude CLI installed to /usr/local/bin/claude: $(claude --version)"
fi
env:
CLAUDE_CLI_VERSION: ${{ inputs.claude_cli_version }}
Expand Down Expand Up @@ -184,6 +234,11 @@ runs:
- name: Run implementation
id: implement
shell: bash
# working-directory is load-bearing — the autosolve binary reads
# the cwd via os.Getwd() in internal/config/loadSandboxConfig and
# validates it against GITHUB_WORKSPACE. Removing this line will
# cause validation to fail confusingly (cwd defaults to the
# workspace root, which the binary explicitly rejects).
working-directory: ${{ inputs.working_directory }}
run: $RUNNER_TEMP/autosolve implement
env:
Expand All @@ -210,3 +265,4 @@ runs:
INPUT_COMMIT_SIGNATURE: ${{ inputs.commit_signature }}
INPUT_PR_FOOTER: ${{ inputs.pr_footer }}
INPUT_LOG_LEVEL: ${{ inputs.log_level }}
INPUT_READ_PATHS: ${{ inputs.read_paths }}
5 changes: 5 additions & 0 deletions autosolve/internal/assess/assess.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ func Run(ctx context.Context, cfg *config.Config, runner claude.Runner, tmpDir s
OutputFile: outputFile,
ContextVars: cfg.ContextVars,
LogLevel: cfg.LogLevel,
Sandbox: claude.SandboxOptions{
WorkingDir: cfg.WorkingDir,
ScratchDir: cfg.ScratchDir,
ReadPaths: cfg.ReadPaths,
},
})
if cfg.LogLevel != "error" {
action.EndLogGroup()
Expand Down
Loading
Loading