Repository navigation
deploy-doctor and health-digest, on Claude Code headless, as piabrota-ops - #1
Merged
Merged
Conversation
…-ops Two bash agents for piabrota/nix-os, run as rax tasks on the VPS. deploy-doctor opens one issue per failed deploy run with Claude Code's diagnosis of its logs and the repository at that commit; health-digest comments daily on a health-digest issue from the box's snapshot, the last deploys and the previous digest. Both post as the GitHub App piabrota-ops, with tokens minted per run for nix-os alone and only the permissions each needs, as marola-devkit's bot-token does. Claude Code runs with read-only tools, no MCP, on the subscription's CLAUDE_CODE_OAUTH_TOKEN. The image pins Claude Code's standalone binary by checksum; test/run.sh runs both agents against a fake GitHub and a fake claude.
…sign reports Layer 1: Claude Code runs --restricted, with --tools limited to read-only ones, no MCP, a pinned model ID and a time limit. Layer 2: --json-schema per agent, the house rules (prompts/_rules.md) appended to every run, and the Markdown rendered from the JSON. Layer 3: the doctor checks that quoted evidence is in the file it names and named files exist, retries once with what failed, and otherwise posts only the facts as unclear; rules/transient.txt fixes the verdict of known transient failures before Claude runs; credentials are scrubbed. deploy-doctor also comments on the merged pull request a deploy came from (pull_requests:write; without it the issue still comes). Every report ends with a signature: agent, model, Claude Code version, turns and time, and where it ran. code-smell.yml adds ruff with every rule and radon (rank C fails) for the Python, and shfmt for the bash; the fake GitHub is now a route table.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The first two agents, run on the VPS as rax tasks (the pod runner from piabrota/rax#10, the runtime from piabrota/nix-os#36), on the Claude subscription (
CLAUDE_CODE_OAUTH_TOKENfromclaude setup-token), posting as the GitHub App piabrota-ops.deploy-doctordeployrun of nix-os in the last day with no issue yet: the failed jobs' logs (last 400 lines), the run, the repository at that commitdeploy-failure: verdict (transient, real, unclear), what failed, why with the log lines, what to do. With no new failure it exits without calling Claudehealth-digestax-tasks/health-snapshot, orHEALTH_SNAPSHOT_FILE), the last 10 deploys, the previous digesthealth-digestissue (opened the first time): status, services, memory and disk with the change, pods, deploys, errors, what to watchbin/agent: the entrypoint; a task's command isagent deploy-doctororagent health-digest.lib/github-app.sh: the App's JWT (RS256 withopenssl) and installation tokens for one named repository with named permissions, refusing an empty set, as marola-devkit'sbot-token(MIP-0086). deploy-doctor gets actions read, contents read, issues write; health-digest actions read, issues write.lib/common.sh: the GitHub API, labels, andclaude -pwith--allowedToolslimited to read-only tools,--strict-mcp-config,--no-session-persistence.prompts/: each agent's prompt, with the shape of its answer.Dockerfile: Debian slim,curl jq openssl git, Claude Code's standalone binary 2.1.316 pinned by its SHA-256 for amd64 and arm64, user 10001, auto-updater and telemetry off. 525 MB..github/workflows/ci.yml:shellcheckandtest/run.sh.image.yml: publishesghcr.io/piabrota/agentsas rax's does.Tests
test/run.sh, 23 checks: both agents against a fake GitHub API (test/fake_github.py, standard library) and a fakeclaude. The JWT verifies against the App's public key and is issued by its ID; the token is fornix-osalone with the agent's permissions; later calls carry the installation token; Claude Code gets the run, only the failed job's log and the repository (doctor) or the snapshot, deploys and previous digest (digest), read-only, on sonnet or haiku, without MCP; one issue per failed run, never twice, without calling Claude the second time; the digest issue is opened once and reused, and the next day sees yesterday's digest; the guards (permissions required, a missing token named with where it comes from).claude --versionis 2.1.316,agent health-digestwithout its Secret says what is missing.Next, in nix-os
Pin this image's digest; the tasks and their CronJobs (every 15 minutes for the doctor, daily for the digest); the box's health snapshot as the ConfigMap, and the service account that reads it.
Second commit: a strict harness, PR comments, signatures, code-smell CI
--restricted(no Bash or other code-running tool, no WebFetch, no settings files, file tools confined to the agent's directory),--toolsread-only only,--strict-mcp-config, pinned model IDs (claude-sonnet-5-5,claude-haiku-5-5),timeout.--json-schemaper agent (prompts/*.schema.json: enums, required fields, lengths); the house rulesprompts/_rules.mdappended to every run; the Markdown rendered by the harness from the JSON.unclearissue with only the harness's facts.rules/transient.txt(SSH timeouts, cache errors, a lost runner) fixes the verdict attransientbefore Claude runs. Credentials are scrubbed before posting. A digest that misses its schema twice fails the run instead of posting.<sub>agent · model via Claude Code version · turns, seconds · as user (uid) in pod · OS · kernel · vCPUs (CPU) · RAM, memory limit · image</sub>..github/workflows/code-smell.yml: ruff 0.14.2 with every rule (ruff.toml), radon 6.0.1 failing on rank C or worse, shfmt 3.10.0 (checksum pinned) on the bash.test/fake_github.pybecame a route table: worst complexity B (7), was E (31).pull_requests, the digest failing without posting.