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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- run: shellcheck bin/agent agents/*.sh lib/*.sh test/run.sh test/fake-claude
- run: shellcheck bin/agent agents/*.sh lib/*.sh test/run.sh test/fake-claude test/fake-ao test/ao-smoke.sh
# The agents against a fake GitHub and a fake claude: no network, no secrets.
- run: test/run.sh
17 changes: 16 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@
# the agent with its command, `agent deploy-doctor` or `agent health-digest`.
#
# docker build -t agents .
# Agent Orchestrator's daemon and CLI for issue-worker, from a pinned commit (v0.14.8). Only `ao`,
# in pure Go: the desktop app, Node and the release's Electron bundle stay out of the image.
FROM golang:1.27.1-bookworm@sha256:8d48e12ec56735e9358640898b9d9b9fcca110612ed8a5567438c0a1baa24e66 AS ao
ARG AO_COMMIT=403a8c8385ed43bbf9e9289e3f3bbaa6517dc437
RUN set -eu; \
git init -q /src; cd /src; \
git fetch -q --depth 1 https://github.com/OrchestratorInc/agent-orchestrator "$AO_COMMIT"; \
git checkout -q FETCH_HEAD; \
cd backend; CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /ao ./cmd/ao

FROM debian:bookworm-slim

# Claude Code's standalone binary (no Node), pinned with its checksum from the release's
Expand All @@ -12,7 +22,7 @@ ARG CLAUDE_CODE_SHA256_ARM64=55cdb8e162d190432696ea54010e0fca2e18d06fa7a7127fd56
ARG TARGETARCH

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

RUN set -eu; \
Expand All @@ -26,6 +36,11 @@ RUN set -eu; \
echo "$sum /usr/local/bin/claude" | sha256sum -c -; \
chmod 755 /usr/local/bin/claude

COPY --from=ao /ao /usr/local/bin/ao
# issue-worker's limits, as Claude Code's managed settings: read-only to the user the agents run as,
# and above every other setting. They bind every agent; no other agent uses a denied tool.
COPY rules/worker-settings.json /etc/claude-code/managed-settings.json

# A user of its own: the pods run with runAsNonRoot.
RUN useradd --uid 10001 --create-home --home-dir /home/agent agent
COPY bin /opt/agents/bin
Expand Down
38 changes: 36 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ tasks, one pod per run (rax's pod runner), on a Claude subscription rather than
| `mirror-keeper` | in `tick` | open pull requests that change a doc but not its mirror (pt-BR and English) | a pull request into that pull request's branch that brings the mirrors up to date |
| `flake-annotator` | in `tick` | open pull requests labelled `flake-update` (nix-os' weekly workflow opens them with the closure difference) | one comment per head: the update's risk and what to watch after it deploys |
| `feedback` | in `tick` | the agents' issues labelled `report-wrong`, and their comments with a 👎 from a trusted person | lessons in memory, which each agent reads before it writes again |
| `issue-worker` | on its own schedule, one issue per run | the oldest open issue labelled `agent-ready` by someone who can write to the repository, and the repository at `main` | **a pull request** that closes it, on `agents/issue-<n>`; or, when it cannot, a comment on the issue saying why, and the label `agent-blocked` |
| `gramatico-loop` | its own task, on [piabrota/gramatico](https://github.com/piabrota/gramatico), as the App **pirsig-2nd-brain** | one approved issue, or the failed checks of its own open pull request, and the repository | one pull request at a time, its repairs, and a comment that stops and asks a person when the attempts run out |
| `health-digest` | once a day | the box's health snapshot (the ConfigMap `ax-tasks/health-snapshot`, which the box writes), the last deploys, the previous digest and the last week's from memory | a comment on the open issue labelled `health-digest` (opened the first time) |

Expand Down Expand Up @@ -42,6 +43,38 @@ shaped like a credential; the mirror keeper may change only the mirrors. Pull re
(pt-BR, then English) and carry no signature; the signature goes in a comment. They need Contents
read and write on the App; without it, these two do nothing and the rest work as before.

## The issue worker

`issue-worker` runs a session of [Agent Orchestrator](https://github.com/OrchestratorInc/agent-orchestrator)
(`ao`, built from a pinned commit into the image) in its pod. The daemon lives only as long as the
run: it makes the worktree, starts Claude Code in `tmux` and watches the session. A pod has one
session, so nothing here routes CI failures or review comments back to a worker: a failing pull
request stays a person's, or a new issue's.

A worker edits files and nothing else, because the pod's environment holds the App's key, and with
it a merge, which on nix-os is a deploy:

1. **No shell, no network, no git.** `rules/worker-settings.json`, installed as Claude Code's
managed settings, denies `Bash`, the web tools, sub-agents, reading `/proc` and writing `.claude/` or
`.git/`; it runs above every other setting and the agents' user cannot change it. Without it, the
worker does not start.
2. **No credential but Claude's.** `ao` starts from an empty environment with Claude's token only.
3. **The harness does the rest.** It reads `.agent-result.json`, checks the diff (at most 12 files
and 400 lines, as `MAX_WORK_FILES` and `MAX_WORK_LINES`, nothing under `.github/`, `.claude/` or `keys/`, not
`flake.lock`, a key file or `system.stateVersion`, nothing shaped like a credential), and only then mints a
write token, commits with the repository's hooks off, pushes and opens the pull request. Every
git call names the git directory in the clone, out of the worker's reach: the worktree's `.git`
file is never read, as in the gramatico loop.

Labels are the controls. `agent-ready` is the go-ahead, and counts only from someone who can write
to the repository; the worker reads the issue's title and body, never its comments. The run moves
it to `agent-working` before it starts, so a run that dies is not paid for twice, and to
`agent-blocked` when a person has to step in. Optional keys: `ISSUE_WORKER_MODEL`
(`claude-sonnet-5-5`), `ISSUE_WORKER_LABEL`, `WORK_TIMEOUT` (`2400` seconds), `IDLE_GRACE` (`180`).

`test/ao-smoke.sh` runs the real `ao` from the image in a pod's limits, with a stand-in for Claude
Code; run it by hand when `AO_COMMIT` changes.

## The gramatico loop

`agent gramatico-loop` runs one step of a loop on piabrota/gramatico. Run it every 30 minutes as its
Expand Down Expand Up @@ -124,7 +157,8 @@ reviewer model. Here nothing can be built in the pod, so a wrong change only mee

## The image

`ghcr.io/piabrota/agents`, from `Dockerfile`: Debian slim, `curl`, `jq`, `openssl`, `git`, `psql` (for memory), and
`ghcr.io/piabrota/agents`, from `Dockerfile`: Debian slim, `curl`, `jq`, `openssl`, `git`, `tmux`, `psql` (for memory),
`ao` (built from a pinned commit, for `issue-worker`), and
Claude Code's standalone binary (2.1.316, its checksum pinned), as user 10001. About 525 MB, most
of it Claude Code. `.github/workflows/image.yml` publishes it on every push to `main` that changes
it; the run summary prints the digest that nix-os pins. A task's command picks the agent:
Expand All @@ -151,7 +185,7 @@ Optional for the gramatico loop: `PIRSIG_APP_ID`, `PIRSIG_APP_PRIVATE_KEY_B64` (
Optional: `AGENTS_DB_URL` (memory; nix-os writes it as its own Secret), `REPO` (default
`piabrota/nix-os`), `TRUSTED_USERS` (`h0ffmann`; beyond the repository's owners, members and
collaborators), the models (`DEPLOY_DOCTOR_MODEL`, `PR_REVIEWER_MODEL`, `MENTIONS_MODEL`,
`MIRROR_KEEPER_MODEL`, `FLAKE_ANNOTATOR_MODEL`: `claude-sonnet-5-5`; `HEALTH_DIGEST_MODEL`:
`MIRROR_KEEPER_MODEL`, `FLAKE_ANNOTATOR_MODEL`, `ISSUE_WORKER_MODEL`: `claude-sonnet-5-5`; `HEALTH_DIGEST_MODEL`:
`claude-haiku-5-5`), `FIX_PRS` (`1`), `TICK_AGENTS`, `LOOKBACK_HOURS` (`24`), `LOG_LINES` (`400`),
`HEALTH_SNAPSHOT_FILE` (a file instead of the ConfigMap).

Expand Down
Loading
Loading