Files
agent-tools/AGENTS.md
T
unkin-agent 61bb464e32
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Add agentvault with a seed-outpost subcommand
Interactive agents are classifier-blocked from plumbing credentials through
a shell, so seeding an Authentik outpost token into Vault KV needs to happen
inside one binary invocation that never exposes the secret.

- Add cmd/agentvault, a fourth CLI sharing the agentpr Vault AppRole login
  (role_id only, VAULT_ADDR/AGENT_APPROLE_ROLE_ID defaults unchanged).
- Add `agentvault seed-outpost`: read the Authentik API token from
  kv/service/authentik/agent-api-token (field `token`, falling back to
  `api_token`), exact-match the outpost by name via the instances search,
  fetch its key from /api/v3/core/tokens/<identifier>/view_key/ and write it
  to --dest-path under --dest-key.
- Print only the outpost name, token identifier, dest path and new KV
  version; keep secret material out of results, errors and logs.
- Distinguish the failure stages (login, KV read denied, outpost missing,
  view_key, KV write denied) with ErrVaultDenied/ErrVaultNotFound/
  ErrOutpostNotFound sentinels and actionable messages.
- Add internal/agent vaultkv.go (AppRole-authenticated KV-v2 client) and
  authentik.go (outpost search + view_key) for reuse by future flows.
- Cover the happy path, idempotent re-run, field fallback and every failure
  mode with httptest servers, including a leak check on error strings.
- Wire agentvault into the Makefile, build-rpm.sh, nfpm contents, release
  cross-builds/assets, README and AGENTS.md.
2026-08-29 21:03:31 +10:00

7.3 KiB

AGENTS.md

Project Overview

This repo ships several Gitea-automation CLIs in one RPM (agent-tools). They act as the unkin-agent user by minting a scoped Gitea token from Vault, so actions are attributed to the agent rather than to whoever runs the tool.

  • agentpr — create pull requests and post PR comments as unkin-agent (fixes the "tea posts as Ben" attribution problem). Subcommands: pr create, pr comment, whoami.
  • watchpr — poll one or more PRs and exit when a tracked PR changes meaningfully: it merges/closes, gets a new non-agent comment, its CI fails, or it loses mergeability. Benign transitions (CI pending→success, the agent's own comments) are ignored.
  • agentws — manage per-branch git worktrees for unkin-agent. Clones repos into the source root (~/src/prodenv/<repo>), creates worktrees under the worktree root (~/.cache/agentws/<repo>__<branch>), and authenticates clone/fetch/push via an ephemeral credential helper. Subcommands: new, list, rm, clean, token, credential.

All tools are separate main packages under cmd/ and share the internal/agent package (Vault AppRole login, Gitea REST client, PR-ref parsing, watch-state comparison, git worktree helpers).

Structure

cmd/agentpr/main.go      # agentpr CLI (pr create / pr comment / whoami)
cmd/watchpr/main.go      # watchpr CLI (poll + meaningful-change exit)
cmd/agentws/main.go      # agentws CLI (new / list / rm / clean / token / credential)
cmd/agentvault/main.go   # agentvault CLI (seed-outpost)
internal/agent/          # shared plumbing:
  token.go               #   env config + in-process Gitea-token cache
  vault.go               #   AppRole login + read gitea/creds/unkin-agent
  gitea.go               #   Gitea REST client (PR create/get, comments, status, whoami)
  parse.go               #   owner/repo#N and owner/repo parsing
  watch.go               #   PRState snapshot + MeaningfulChange comparison
  git.go                 #   git worktree/clone/fetch helpers (os/exec, no go-git)
  vaultkv.go             #   AppRole-authenticated Vault client + KV-v2 read/write
  authentik.go           #   Authentik REST client (outpost search, token view_key)
  seedoutpost.go         #   seed-outpost flow (Authentik token -> Vault KV)
go.mod                   # module git.unkin.net/unkin/agent-tools
Makefile                 # build / test / lint / completions / rpm / version-bump
packaging/nfpm.yaml      # nfpm spec (envsubst-templated) for the RPM (all binaries)
scripts/build-rpm.sh     # generates completions + packages the RPM with nfpm
.woodpecker/             # CI: build, test, pre-commit (PR) + release (tag)
dist/                    # build output: binaries, completions, RPM (not committed)

Every binary is a separate main package, so make build builds each with its own -o (a single go build ./... can't emit multiple mains to one file).

Token acquisition (shared)

All tools call agent.GiteaToken(), which (once per process):

  1. AppRole login: POST $VAULT_ADDR/v1/auth/approle/login with role_id only (no secret_id) → client_token.
  2. GET $VAULT_ADDR/v1/gitea/creds/unkin-agent with X-Vault-Token.data.token.

Config via env (all have defaults):

Variable Default Purpose
VAULT_ADDR https://vault.service.consul:8200 Vault/OpenBao address
AGENT_APPROLE_ROLE_ID built-in default AppRole role_id (overridable)
GITEA_URL https://git.unkin.net Gitea base URL
AGENT_LOGIN unkin-agent login whose comments watchpr ignores; agentws git identity
AGENTWS_SRC_ROOT ~/src/prodenv agentws source-of-truth checkout root
AGENTWS_ROOT ~/.cache/agentws agentws worktree root
AGENTWS_OWNER unkin Gitea org that owns agentws-managed repos
AUTHENTIK_URL https://identity.k8s.syd1.au.unkin.net Authentik base URL (agentvault)

agentws git auth (ephemeral credential helper)

Gitea tokens are ~1h ephemeral, so agentws never bakes one into a remote URL or config. agentws token prints a fresh token; agentws credential get implements the git credential protocol (reads the key=value request on stdin, and for the configured Gitea host only emits username=unkin-agent + password=<fresh token>). agentws new wires this per worktree — it enables extensions.worktreeConfig on the repo once, then writes user.name, user.email and credential.helper = !<agentws> credential to the per-worktree config so the shared checkout's identity/config is untouched. Clone/fetch pass the same helper transiently via -c credential.helper=.... Worktrees are created FROM ~/src/prodenv/<repo> (git worktree add) so agent branches are visible in Ben's main checkout; rm/clean fetch there afterwards to keep the default branch current.

Build

make build          # -> dist/agentpr, dist/watchpr, dist/agentws, dist/agentvault (CGO disabled, static)

Requires Go 1.21+. Dependency: github.com/spf13/cobra (CLI).

Packaging (RPM)

make rpm            # build all binaries + package into dist/*.rpm via nfpm

scripts/build-rpm.sh generates bash/zsh/fish completions from the built binaries and bundles them alongside /usr/bin/agentpr, /usr/bin/watchpr, /usr/bin/agentws and /usr/bin/agentvault. On a v* tag the release pipeline builds the RPM and PUTs it to the artifactapi rpm-internal repo, then cuts a Gitea release.

Shell completions

Cobra provides a completion subcommand for each binary (agentpr completion bash, etc.). The RPM installs them to the standard system paths (/usr/share/bash-completion/completions/, /usr/share/zsh/site-functions/, /usr/share/fish/vendor_completions.d/).

Testing

make test           # go test -v -race ./...

internal/agent covers PR-ref parsing, the MeaningfulChange table (benign vs alerting transitions), request-body construction, and the Vault+Gitea client against httptest servers (fake AppRole login + gitea creds + PR create / comment / whoami / status). No live Vault/Gitea access is required for tests.

agentvault seed-outpost

agentvault seed-outpost --outpost <name> --dest-path <kv/path> does the whole flow in-process:

  1. AppRole login (shared approleLogin), then KV-v2 read of kv/service/authentik/agent-api-token (field token, falling back to api_token).
  2. GET /api/v3/outposts/instances/?search=<name> — Authentik's search is a substring match, so the exact name is re-checked client-side.
  3. GET /api/v3/core/tokens/<token_identifier>/view_key/ for the key.
  4. KV-v2 write to --dest-path under --dest-key (default token).

Only the outpost name, token identifier, dest path and new KV version are printed. Errors are wrapped per stage (login / read denied / outpost missing / view_key / write denied) via the ErrVaultDenied, ErrVaultNotFound and ErrOutpostNotFound sentinels.

Gotchas

  • watchpr exits 0 with no output changes on --once (just prints state).
  • The token cache is process-wide (sync.Once); tests call the unexported fetchGiteaToken to avoid it.
  • agentvault never puts a secret in an error string: Vault decode failures and Authentik view_key responses are reported without their bodies.
  • CI "combined status" comes from /commits/{sha}/status; an empty head SHA yields an empty state without an API call.