You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Tracker for the "the repo maintains itself" goal. Captures the reasoning behind #172/#173/#174 so the next session does not re-derive it, and records one architectural decision that is not yet made.
The design principle
Durable state belongs in the repo, not in the compute. Issues are the queue, labels are the state machine, AGENTS.md is the learned context, PRs are the output. Get that right and the compute becomes fungible — a runner when it is cheap, a box when warm context matters. The "file an issue when you hit something, even mid-task" rule in AGENTS.md is already the queue-writing half of this, written down.
Corollary: an agent run has no memory. Anything a future run needs must be committed, labelled, or written into an issue. This is why triage claims with agent-triaged rather than tracking state anywhere else.
Why GitHub Actions rather than a persistent box
For repo maintenance the work is the repository's software, so it is squarely in scope for Actions, and ephemerality stops being a compromise: every run starts from a known-clean state, which is worth more for unattended work than warm context is. No inbound network needed, so no tunnel.
This does not generalise to hosting agent-box itself on Actions — that fails on no-inbound-network, no persistence (6h cap, 10 GB LRU cache), and the ToS scope limit. Do not revisit that; it is a different question with a settled answer.
Division of labour:
Actions
The box
Good for
Bounded, verifiable, event-shaped work whose artifact is a reviewable diff
Long-horizon, cross-repo work needing warm context and a human able to interrupt
Examples
Dep/pin bumps, AMI refresh, doc drift, triage, "a VM test flaked → open a PR"
Confused agent burns its budget and opens a bad PR
You can talk it down mid-task
Two costs to plan for, since they are what actually bite:
Auth model flips. An API key means metered billing rather than the box's subscription sign-in. CLAUDE_CODE_OAUTH_TOKEN avoids that; scope each run to one issue regardless.
Cold nix store. CI spends most of its ~10 min building VM tests from scratch. An agent that must iterate on nix build .#checks… spends its budget on the store, not on thinking. Cachix, or a self-hosted runner on a box (which would also give CI the native aarch64 coverage it currently lacks — see the vmSystems note in AGENTS.md). Only on a private repo or with fork PRs blocked: self-hosted + public pull_request is arbitrary code execution on the box.
Trust boundary, not task, is the right axis to split on
#172 splits three workflows this way, and the split is the load-bearing idea:
Own PRs (agent-pr.yml) — trusted input, so the agent gets --dangerously-skip-permissions, contents: write, and can push fixes.
Fork PRs (agent-review-fork.yml) — attacker-controlled input, so the opposite: pull_request_target for secrets, but the PR head is never checked out, pull-requests: write is the only permission, and the agent runs with an explicit --allowedTools "Read,Grep,Glob" allowlist and no skip-permissions. A diff instructing the model to exfiltrate the API key has no tool with which to do it. Review only; the checks are not run.
The queue (agent-triage.yml) — contents: read, so it structurally cannot push code.
Getting these backwards is how the pattern goes wrong, so each file documents its own reasoning in its header.
The one thing that keeps recurring: GITHUB_TOKEN does not trigger workflows
The same mechanism has now shown up three times wearing different clothes:
As a bug — fix(ci): refresh-amis must dispatch the publisher, not rely on its push #173: refresh-amis.yml pushed the AMI map with GITHUB_TOKEN, so publish-template.yml never fired and the refreshed map never reached the S3 template the 1-click Launch buttons serve. Went unnoticed for a month (5542d8f, 2026-07-06; no publish run for it in 60 runs).
As a safety net — it is the only reason an agent cannot currently open a PR, review its own PR, comment on itself, and cascade.
As a ceiling — genuine event-driven self-maintenance is a chain (agent opens PR → review fires → fix fires), and GITHUB_TOKEN cannot start one.
So the safety net and the ceiling are the same wall. You cannot remove one without the other.
Open decision: does this warrant a GitHub App?
Not an either/or. An App is an identity-and-permissions layer, not compute — workflows still run the agent. The question is whether one is needed in addition.
Recommendation: not yet, but adopt one when the first of these bites — and two are already visible.
Cross-repo writes.GITHUB_TOKEN is repo-scoped. AGENTS.md already requires filing in the repo that owns the fix (defangdevs/local-channels for local-webhook behaviour) and cross-linking. A triage agent cannot do that today, so this condition is arguably already met.
Chaining. Per the wall above. This is the strongest argument, and adopting an App is exactly the moment the github.actor != 'github-actions[bot]' guard in feat(ci): agent workflows for PR fixes, fork review, and queue triage #172 stops being redundant and becomes mandatory, alongside explicit loop breakers.
Attribution. Everything is github-actions[bot] today. Fine for a test runner, less fine when the bot is a participant in review threads.
When you do: App, not PAT. Installation tokens are short-lived, minted per run from a private key, and scopeable to chosen repos with fine-grained permissions. A PAT is long-lived, tied to a person, and carries their whole access.
Cheapest intermediate step (already applied to #173, worth generalising): one App used only by the elevated jobs — the template publisher, cross-repo issue filing — with the agent loop staying on GITHUB_TOKEN. Cross-repo and chaining exactly where wanted; the loop keeps its structural safety net.
Do not adopt an App for rate limits (irrelevant at this scale) or tidiness.
Consequence: this re-opens the local-channels question
An App can receive webhooks directly, which is what would make the box viable as the maintenance host again: App → local-webhook → warm persistent session, instead of a cold runner per event.
If the maintenance loop lives on runners, local-webhook is genuinely redundant for this purpose — on: pull_request / on: issues / on: schedule replaces the HMAC receiver, the Caddy ingress route, the subscription topics, the TTL semantics, and the standing-watch bugs (#170), and the warm context those subscriptions exist to preserve is worth nothing to a process that starts cold.
But local-webhook is a shipped, tested, default-on feature of the product (tests/webhook.nix, pinned at 0.10.0). Both can be true — the box keeps earning its keep for interactive, long-horizon, cross-repo work a 6h cold runner cannot do — but this should be scoped deliberately rather than left to rot. Decide it together with the App question, not separately.
Tracker for the "the repo maintains itself" goal. Captures the reasoning behind #172/#173/#174 so the next session does not re-derive it, and records one architectural decision that is not yet made.
The design principle
Durable state belongs in the repo, not in the compute. Issues are the queue, labels are the state machine,
AGENTS.mdis the learned context, PRs are the output. Get that right and the compute becomes fungible — a runner when it is cheap, a box when warm context matters. The "file an issue when you hit something, even mid-task" rule in AGENTS.md is already the queue-writing half of this, written down.Corollary: an agent run has no memory. Anything a future run needs must be committed, labelled, or written into an issue. This is why triage claims with
agent-triagedrather than tracking state anywhere else.Why GitHub Actions rather than a persistent box
For repo maintenance the work is the repository's software, so it is squarely in scope for Actions, and ephemerality stops being a compromise: every run starts from a known-clean state, which is worth more for unattended work than warm context is. No inbound network needed, so no tunnel.
This does not generalise to hosting agent-box itself on Actions — that fails on no-inbound-network, no persistence (6h cap, 10 GB LRU cache), and the ToS scope limit. Do not revisit that; it is a different question with a settled answer.
Division of labour:
Two costs to plan for, since they are what actually bite:
CLAUDE_CODE_OAUTH_TOKENavoids that; scope each run to one issue regardless.nix build .#checks…spends its budget on the store, not on thinking. Cachix, or a self-hosted runner on a box (which would also give CI the native aarch64 coverage it currently lacks — see thevmSystemsnote in AGENTS.md). Only on a private repo or with fork PRs blocked: self-hosted + publicpull_requestis arbitrary code execution on the box.Trust boundary, not task, is the right axis to split on
#172 splits three workflows this way, and the split is the load-bearing idea:
agent-pr.yml) — trusted input, so the agent gets--dangerously-skip-permissions,contents: write, and can push fixes.agent-review-fork.yml) — attacker-controlled input, so the opposite:pull_request_targetfor secrets, but the PR head is never checked out,pull-requests: writeis the only permission, and the agent runs with an explicit--allowedTools "Read,Grep,Glob"allowlist and no skip-permissions. A diff instructing the model to exfiltrate the API key has no tool with which to do it. Review only; the checks are not run.agent-triage.yml) —contents: read, so it structurally cannot push code.Getting these backwards is how the pattern goes wrong, so each file documents its own reasoning in its header.
The one thing that keeps recurring: GITHUB_TOKEN does not trigger workflows
The same mechanism has now shown up three times wearing different clothes:
refresh-amis.ymlpushed the AMI map withGITHUB_TOKEN, sopublish-template.ymlnever fired and the refreshed map never reached the S3 template the 1-click Launch buttons serve. Went unnoticed for a month (5542d8f, 2026-07-06; no publish run for it in 60 runs).GITHUB_TOKENcannot start one.So the safety net and the ceiling are the same wall. You cannot remove one without the other.
Open decision: does this warrant a GitHub App?
Not an either/or. An App is an identity-and-permissions layer, not compute — workflows still run the agent. The question is whether one is needed in addition.
Recommendation: not yet, but adopt one when the first of these bites — and two are already visible.
GITHUB_TOKENis repo-scoped. AGENTS.md already requires filing in the repo that owns the fix (defangdevs/local-channelsfor local-webhook behaviour) and cross-linking. A triage agent cannot do that today, so this condition is arguably already met.github.actor != 'github-actions[bot]'guard in feat(ci): agent workflows for PR fixes, fork review, and queue triage #172 stops being redundant and becomes mandatory, alongside explicit loop breakers.github-actions[bot]today. Fine for a test runner, less fine when the bot is a participant in review threads.When you do: App, not PAT. Installation tokens are short-lived, minted per run from a private key, and scopeable to chosen repos with fine-grained permissions. A PAT is long-lived, tied to a person, and carries their whole access.
Cheapest intermediate step (already applied to #173, worth generalising): one App used only by the elevated jobs — the template publisher, cross-repo issue filing — with the agent loop staying on
GITHUB_TOKEN. Cross-repo and chaining exactly where wanted; the loop keeps its structural safety net.Do not adopt an App for rate limits (irrelevant at this scale) or tidiness.
Consequence: this re-opens the local-channels question
An App can receive webhooks directly, which is what would make the box viable as the maintenance host again: App → local-webhook → warm persistent session, instead of a cold runner per event.
If the maintenance loop lives on runners, local-webhook is genuinely redundant for this purpose —
on: pull_request/on: issues/on: schedulereplaces the HMAC receiver, the Caddy ingress route, the subscription topics, the TTL semantics, and the standing-watch bugs (#170), and the warm context those subscriptions exist to preserve is worth nothing to a process that starts cold.But local-webhook is a shipped, tested, default-on feature of the product (
tests/webhook.nix, pinned at 0.10.0). Both can be true — the box keeps earning its keep for interactive, long-horizon, cross-repo work a 6h cold runner cannot do — but this should be scoped deliberately rather than left to rot. Decide it together with the App question, not separately.State
GITHUB_TOKEN.claude-codepin confirmed, and four labels created (agent-fix,agent-triaged,stale,needs-decision).Out of scope
Hosting agent-box itself on Actions (settled: no). Non-GitHub forges. Replacing the box.