chore: gardener housekeeping 2026-04-12
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful

This commit is contained in:
Claude 2026-04-12 02:43:22 +00:00
parent 34d4136f2e
commit 0bc027a25a
10 changed files with 12 additions and 27 deletions

View file

@ -1,4 +1,4 @@
<!-- last-reviewed: 3e65878093bbbcea6dfe4db341f82dc89d4e0ac0 -->
<!-- last-reviewed: 34d4136f2e20633cd38768265db08bce9301e392 -->
# Disinto — Agent Instructions
## What this repo is
@ -35,12 +35,12 @@ disinto/ (code repo)
│ SCHEMA.md — vault item schema documentation
│ validate.sh — vault item validator
│ examples/ — example vault action TOMLs (promote, publish, release, webhook-call)
├── lib/ env.sh, agent-sdk.sh, ci-helpers.sh, ci-debug.sh, load-project.sh, parse-deps.sh, guard.sh, mirrors.sh, pr-lifecycle.sh, issue-lifecycle.sh, worktree.sh, formula-session.sh, stack-lock.sh, forge-setup.sh, forge-push.sh, ops-setup.sh, ci-setup.sh, generators.sh, hire-agent.sh, release.sh, build-graph.py,
│ branch-protection.sh, secret-scan.sh, tea-helpers.sh, vault.sh, ci-log-reader.py, git-creds.sh
├── lib/ env.sh, agent-sdk.sh, ci-helpers.sh, ci-debug.sh, load-project.sh, parse-deps.sh, guard.sh, mirrors.sh, pr-lifecycle.sh, issue-lifecycle.sh, worktree.sh, formula-session.sh, stack-lock.sh, forge-setup.sh, forge-push.sh, ops-setup.sh, ci-setup.sh, generators.sh, hire-agent.sh, release.sh, build-graph.py, branch-protection.sh, secret-scan.sh, tea-helpers.sh, vault.sh, ci-log-reader.py, git-creds.sh
│ hooks/ — Claude Code session hooks (on-compact-reinject, on-idle-stop, on-phase-change, on-pretooluse-guard, on-session-end, on-stop-failure)
├── projects/ *.toml.example — templates; *.toml — local per-box config (gitignored)
├── formulas/ Issue templates (TOML specs for multi-step agent tasks)
├── docker/ Dockerfiles and entrypoints: reproduce, triage, edge dispatcher, chat (server.py, Dockerfile, ui/)
├── docker/ Dockerfiles and entrypoints: reproduce, triage, edge dispatcher, chat (server.py, entrypoint-chat.sh, Dockerfile, ui/)
├── tools/ Operational tools: edge-control/ (register.sh, install.sh, verify-chat-sandbox.sh)
├── docs/ Protocol docs (PHASE-PROTOCOL.md, EVIDENCE-ARCHITECTURE.md)
├── site/ disinto.ai website content
├── tests/ Test files (mock-forgejo.py, smoke-init.sh)

View file

@ -1,4 +1,4 @@
<!-- last-reviewed: 3e65878093bbbcea6dfe4db341f82dc89d4e0ac0 -->
<!-- last-reviewed: 34d4136f2e20633cd38768265db08bce9301e392 -->
# Architect — Agent Instructions
## What this agent is

View file

@ -1,4 +1,4 @@
<!-- last-reviewed: 3e65878093bbbcea6dfe4db341f82dc89d4e0ac0 -->
<!-- last-reviewed: 34d4136f2e20633cd38768265db08bce9301e392 -->
# Dev Agent
**Role**: Implement issues autonomously — write code, push branches, address

View file

@ -1,4 +1,4 @@
<!-- last-reviewed: c51cc9dba649ed543b910b561231a5c8bd2130bc -->
<!-- last-reviewed: 34d4136f2e20633cd38768265db08bce9301e392 -->
# Gardener Agent
**Role**: Backlog grooming — detect duplicate issues, missing acceptance

View file

@ -1,19 +1,4 @@
[
{
"action": "edit_body",
"issue": 707,
"body": "## Goal\n\nGive `disinto-chat` its own Claude identity mount so its OAuth refresh races cannot corrupt the factory agents' shared `~/.claude` credentials. Default to a separate `~/.claude-chat/` on the host; support `ANTHROPIC_API_KEY` as a fallback that skips OAuth entirely.\n\n## Why\n\n- #623 root-caused this: Claude Code's internal refresh lock in `~/.claude.lock` operates outside bind-mounted directories, so two containers sharing `~/.claude` can race during token refresh and invalidate each other. The factory has already had OAuth expiry incidents traced to multiple agents sharing credentials.\n- Scoping chat to its own identity dir means chat can be logged in as a different Anthropic account, or pinned to an API key, without touching agent credentials.\n\n## Scope\n\n### Files to touch\n\n- `lib/generators.sh` chat service block (from #705):\n - Replace the throwaway named volume with `${CHAT_CLAUDE_DIR:-${HOME}/.claude-chat}:/home/chat/.claude-chat`.\n - Env: `CLAUDE_CONFIG_DIR=/home/chat/.claude-chat/config`, `CLAUDE_CREDENTIALS_DIR=/home/chat/.claude-chat/config/credentials`.\n - Conditional: if `ANTHROPIC_API_KEY` is set in `.env`, pass it through and **do not** mount `~/.claude-chat` at all (no credentials on disk in that mode).\n- `bin/disinto disinto_init()` — after #620's admin password prompt, add an optional prompt: `Use separate Anthropic identity for chat? (y/N)`. On yes, create `~/.claude-chat/` and invoke `claude login` in a subshell with `CLAUDE_CONFIG_DIR=~/.claude-chat/config`.\n- `lib/claude-config.sh` — factor out the existing `~/.claude` setup logic so a non-default `CLAUDE_CONFIG_DIR` is a first-class parameter. If it is already parameterised, just document it; if not, extract a helper `setup_claude_dir <dir>` and have the existing path call it with the default dir.\n- `docker/chat/Dockerfile` — declare `VOLUME /home/chat/.claude-chat`, set owner to the non-root chat user introduced in #706.\n\n### Out of scope\n\n- Cross-session lock coherence for multiple concurrent chat containers (single-chat-container assumption is fine for MVP).\n- Anthropic team / workspace support — single identity is enough.\n\n## Acceptance\n\n- [ ] Fresh `disinto init` with \"use separate chat identity\" answered yes creates `~/.claude-chat/` and logs in successfully.\n- [ ] With `ANTHROPIC_API_KEY=sk-ant-...` set in `.env`, chat starts without any `~/.claude-chat` mount (verified via `docker inspect disinto-chat`) and successfully completes a test prompt.\n- [ ] Running the factory agents AND chat simultaneously for 24h does not produce any OAuth refresh failures on either side (manual soak test — document result in PR).\n- [ ] `CLAUDE_CONFIG_DIR` and `CLAUDE_CREDENTIALS_DIR` inside the chat container resolve to `/home/chat/.claude-chat/config*`, not the shared factory path.\n\n## Depends on\n\n- #705 (chat scaffold).\n- #620 (admin password prompt — same init flow this adds a step to).\n\n## Notes\n\n- The factory's existing shared mount is `/var/lib/disinto/claude-shared` (see `lib/generators.sh:113,327,381,426`). Chat must NOT use this path.\n- `flock(\"${HOME}/.claude/session.lock\")` logic mentioned in #623 is load-bearing, not redundant — do not \"simplify\" it.\n- Prefer the API-key path for anyone running the factory on shared hardware; call this out in README updates.\n\n## Boundaries for dev-agent\n\n- Do not try to make chat share `~/.claude` with the agents \"just for convenience\". The whole point of this chunk is the opposite.\n- Do not add a third claude config dir. One for agents, one for chat, done.\n- Do not refactor `lib/claude-config.sh` beyond extracting a parameterised helper if needed.\n- Parent vision: #623.\n\n## Affected files\n- `lib/generators.sh` — chat service bind mount and env vars for CLAUDE_CONFIG_DIR\n- `bin/disinto` — disinto_init() prompt for separate chat identity\n- `lib/claude-config.sh` — factor out claude dir setup (new helper setup_claude_dir)\n- `docker/chat/Dockerfile` — declare VOLUME /home/chat/.claude-chat"
},
{
"action": "edit_body",
"issue": 708,
"body": "## Goal\n\nGate `/chat/*` behind Forgejo OAuth. Register a second OAuth2 app on the internal Forgejo (alongside the existing Woodpecker one), implement the server-side authorization-code flow in the chat backend, and enforce that the logged-in user is `disinto-admin` (or a member of a configured allowlist).\n\n## Why\n\n- #623: the single `disinto-admin` password (bootstrap secret from #620) is the only auth credential; chat must reuse it via Forgejo OAuth, not invent a second password.\n- Keeps attack surface flat: exactly one identity provider for forge, CI, and chat.\n\n## Scope\n\n### Files to touch\n\n- `lib/ci-setup.sh` — generalise the Woodpecker OAuth-app creation helper `_create_woodpecker_oauth_impl()` (at `lib/ci-setup.sh:96`) into `_create_forgejo_oauth_app <name> <redirect_uri>` that both Woodpecker and chat can call. The existing Woodpecker callsite becomes a thin wrapper; no behaviour change for Woodpecker.\n- `bin/disinto disinto_init()` — after Woodpecker OAuth creation (around `bin/disinto:847`), add a call to create the chat OAuth app with redirect URI `https://${EDGE_TUNNEL_FQDN}/chat/oauth/callback`. Write `CHAT_OAUTH_CLIENT_ID` / `CHAT_OAUTH_CLIENT_SECRET` to `.env`.\n- `docker/chat/server.{py,go}` — new routes:\n - `GET /chat/login` → 302 to Forgejo `/login/oauth/authorize?client_id=...&state=...`.\n - `GET /chat/oauth/callback` → exchange code for token, fetch `/api/v1/user`, assert `login == \"disinto-admin\"` (or `DISINTO_CHAT_ALLOWED_USERS` CSV if set), set an `HttpOnly` session cookie, 302 to `/chat/`.\n - Any other route with no valid session → 302 to `/chat/login`.\n - Session store: in-memory map keyed by random token; TTL 24h; no persistence across container restarts (by design — forces re-auth after deploy).\n- `lib/generators.sh` chat service env: pass `FORGE_URL`, `CHAT_OAUTH_CLIENT_ID`, `CHAT_OAUTH_CLIENT_SECRET`, `EDGE_TUNNEL_FQDN`, `DISINTO_CHAT_ALLOWED_USERS`.\n\n### Out of scope\n\n- Defense-in-depth `Remote-User` header check — #709.\n- Team membership lookup via Forgejo API — start with a plain user allowlist, add teams later.\n- CSRF protection on `POST /chat` beyond the session cookie — add only if soak testing reveals a need.\n\n## Acceptance\n\n- [ ] `disinto init` on a fresh project creates TWO OAuth apps on Forgejo (woodpecker + chat), both visible in Forgejo admin UI.\n- [ ] `curl -c cookies -L http://edge/chat/` follows through the OAuth flow when seeded with `disinto-admin` credentials and lands on the chat UI.\n- [ ] Same flow as a non-admin Forgejo user lands on a \"not authorised\" page with a 403.\n- [ ] Expired / missing session cookie redirects to `/chat/login`.\n- [ ] `DISINTO_CHAT_ALLOWED_USERS=alice,bob` permits those users in addition to `disinto-admin`.\n\n## Depends on\n\n- #705 (chat scaffold).\n- #620 (admin password prompt — the password this auth leans on).\n\n## Notes\n\n- The existing Woodpecker OAuth exchange at `lib/ci-setup.sh:273-294` is the reference for the server-side code→token exchange. Read it before implementing; do not guess the Forgejo OAuth flow from docs.\n- Forgejo OAuth2 endpoints are `/login/oauth/authorize` and `/login/oauth/access_token`. Do not hit `/api/v1/...` for OAuth — that is the REST API, not the OAuth endpoints.\n- Session cookie must be `Secure; HttpOnly; SameSite=Lax` in prod; permit non-secure in local dev when `EDGE_TUNNEL_FQDN` is unset.\n\n## Boundaries for dev-agent\n\n- Do not add a new OAuth library dependency if the standard library of the chosen backend language has HTTP client + JSON — the flow is two HTTP calls.\n- Do not reuse the Woodpecker OAuth app. Create a second one with a distinct name and redirect URI. They are different principals.\n- Do not persist sessions to disk. In-memory is correct for MVP; persistence is a separate conversation.\n- Do not implement team membership this chunk. Static allowlist first; teams later.\n- Parent vision: #623.\n\n## Affected files\n- `lib/ci-setup.sh` — generalise _create_woodpecker_oauth_impl() into _create_forgejo_oauth_app helper\n- `bin/disinto` — disinto_init() to create chat OAuth app and write CHAT_OAUTH_CLIENT_ID/SECRET to .env\n- `docker/chat/server.{py,go}` — new OAuth routes: /chat/login, /chat/oauth/callback, session middleware\n- `lib/generators.sh` — chat service env vars: FORGE_URL, CHAT_OAUTH_CLIENT_ID, CHAT_OAUTH_CLIENT_SECRET, EDGE_TUNNEL_FQDN, DISINTO_CHAT_ALLOWED_USERS"
},
{
"action": "edit_body",
"issue": 709,
"body": "## Goal\n\nAdd a second, independent auth check on top of #708: Caddy injects an `X-Forwarded-User` header from the validated Forgejo session, and the chat backend refuses any request whose session cookie disagrees with the header. This is the belt to #708's braces.\n\n## Why\n\n- #623 explicitly calls this out as defense-in-depth. If the chat backend session logic has a bug (forged cookie, state confusion), a correctly-configured Caddy `forward_auth` layer catches it — and vice versa.\n- Cheap to add on top of #704 and #708; expensive to bolt on after an incident.\n\n## Scope\n\n### Files to touch\n\n- `docker/Caddyfile` — the `/chat/*` block:\n - Add `forward_auth chat:8080 { uri /chat/auth/verify; copy_headers X-Forwarded-User }`.\n - Requests without a valid session are forwarded to `/chat/login` by chat itself; `forward_auth` just stamps the header when there is one.\n- `docker/chat/server.{py,go}`:\n - New route `GET /chat/auth/verify` — reads the session cookie, returns 200 + `X-Forwarded-User: <login>` if valid, 401 otherwise.\n - On `POST /chat` and other authenticated routes: read `X-Forwarded-User`, read the session cookie, assert both resolve to the same user. On mismatch: log a warning with the request ID and return 403.\n\n### Out of scope\n\n- Rewriting the session store. The verify endpoint reads the same in-memory map #708 introduced.\n\n## Acceptance\n\n- [ ] `curl http://edge/chat/` with a valid session cookie still works; chat backend logs show `X-Forwarded-User` matching the cookie user.\n- [ ] Editing the session cookie client-side to impersonate another user while keeping the forged cookie valid triggers a 403 with a clear log line (simulate by swapping cookies mid-session).\n- [ ] Removing the `forward_auth` block from Caddyfile and restarting causes the chat backend to fail-closed (all authenticated routes 403) — documented as the intended failure mode.\n- [ ] The verify endpoint does not accept arbitrary external requests from outside Caddy: the chat backend rejects calls to `/chat/auth/verify` that lack a shared-secret header (or whose origin IP is not the edge container).\n\n## Depends on\n\n- #704 (Caddy subpath routing).\n- #708 (chat OAuth gate — provides the session store this chunk reads).\n\n## Notes\n\n- Caddy `forward_auth` reference: https://caddyserver.com/docs/caddyfile/directives/forward_auth — stick to the documented directives, do not hand-roll header passing.\n- If network-level origin validation on `/chat/auth/verify` is fiddly, a shared-secret header between Caddy and chat is acceptable — but prefer network-level if possible.\n\n## Boundaries for dev-agent\n\n- Do not replace the #708 session store with something new. Read it, do not rewrite it.\n- Do not push the entire auth decision into Caddy. The chat backend is still the source of truth; Caddy adds a redundant check.\n- Parent vision: #623.\n\n## Affected files\n- `docker/Caddyfile` — add forward_auth block to /chat/* with X-Forwarded-User header copy\n- `docker/chat/server.{py,go}` — new GET /chat/auth/verify route; X-Forwarded-User vs session cookie check on authenticated routes"
},
{
"action": "edit_body",
"issue": 710,

View file

@ -1,4 +1,4 @@
<!-- last-reviewed: 3e65878093bbbcea6dfe4db341f82dc89d4e0ac0 -->
<!-- last-reviewed: 34d4136f2e20633cd38768265db08bce9301e392 -->
# Shared Helpers (`lib/`)
All agents source `lib/env.sh` as their first action. Additional helpers are

View file

@ -1,4 +1,4 @@
<!-- last-reviewed: 3e65878093bbbcea6dfe4db341f82dc89d4e0ac0 -->
<!-- last-reviewed: 34d4136f2e20633cd38768265db08bce9301e392 -->
# Planner Agent
**Role**: Strategic planning using a Prerequisite Tree (Theory of Constraints),

View file

@ -1,4 +1,4 @@
<!-- last-reviewed: 3e65878093bbbcea6dfe4db341f82dc89d4e0ac0 -->
<!-- last-reviewed: 34d4136f2e20633cd38768265db08bce9301e392 -->
# Predictor Agent
**Role**: Abstract adversary (the "goblin"). Runs a 2-step formula

View file

@ -1,4 +1,4 @@
<!-- last-reviewed: 3e65878093bbbcea6dfe4db341f82dc89d4e0ac0 -->
<!-- last-reviewed: 34d4136f2e20633cd38768265db08bce9301e392 -->
# Review Agent
**Role**: AI-powered PR review — post structured findings and formal

View file

@ -1,4 +1,4 @@
<!-- last-reviewed: 3e65878093bbbcea6dfe4db341f82dc89d4e0ac0 -->
<!-- last-reviewed: 34d4136f2e20633cd38768265db08bce9301e392 -->
# Supervisor Agent
**Role**: Health monitoring and auto-remediation, executed as a formula-driven