Files
unkin-agent 155392a809
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Add agentvault seed-oauth for oauth2-proxy credentials
Seeding an oauth2-proxy secret by hand means an agent shell-plumbing a
client secret and a cookie secret, which the classifier blocks. seed-oauth
does it in one self-contained invocation: it reads the KV path, fills in
only the keys that are missing, preserves everything else and prints key
names and the new version, never a value.

- Add SeedOAuth in internal/agent: read-modify-write of the client_id,
  client_secret and cookie_secret keys with per-key created/kept/rotated
  actions and a no-op when nothing changed.
- Generate secrets from 32 crypto/rand bytes; cookie_secret is base64url so
  it decodes to exactly the 32 bytes oauth2-proxy requires.
- Add ReadKVOptional (missing secret = empty) and WriteKVAny (non-string
  fields survive a round trip) to the KV-v2 client.
- Wire the seed-oauth subcommand and document it in README and AGENTS.md.
- Cover fresh create, patch-preserves-client_secret, other-key
  preservation, --rotate, idempotence, denial errors and secret leakage.
2026-08-30 15:31:25 +10:00

8.4 KiB

agent-tools

Small Gitea-automation CLIs, shipped together in one RPM (agent-tools). They act as an agent user (unkin-agent by default) by minting a scoped Gitea token from Vault, so automated PRs, comments and pushes are attributed to the agent — not to whoever happens to run the command. Set AGENT_LOGIN to act as a different agent identity.

  • agentpr — create pull requests and post PR comments as the agent user.
  • watchpr — poll one or more PRs and exit when one changes in a way worth acting on.
  • agentws — manage per-branch git worktrees for unkin-agent, cloning into Ben's source checkout and isolating agent work under the XDG cache.
  • agentvault — run deterministic Vault flows in one invocation, so agents never plumb secret material through a shell.

How it gets a token

On first use each tool performs a Vault AppRole login (role_id only, no secret_id), then reads gitea/creds/$AGENT_LOGIN — or GITEA_CREDS_PATH when set — to obtain a short-lived Gitea token, cached in-process for the run. With neither variable set that is gitea/creds/unkin-agent, as before.

Everything is configured by environment variables, all with 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 agent identity: selects gitea/creds/<login>, and the login whose comments watchpr ignores
GITEA_CREDS_PATH gitea/creds/$AGENT_LOGIN Vault path minting the Gitea token (wins over AGENT_LOGIN)
AGENTWS_SRC_ROOT ~/src/prodenv source-of-truth checkout root (agentws)
AGENTWS_ROOT ~/.cache/agentws worktree root (agentws)
AGENTWS_OWNER unkin Gitea org that owns the repos (agentws)
AUTHENTIK_URL https://identity.k8s.syd1.au.unkin.net Authentik base URL (agentvault)

agentpr

# Verify identity (prints the agent login, unkin-agent by default)
agentpr whoami

# Open a PR
agentpr pr create --repo unkin/argocd-apps \
  --base main --head benvin/my-change \
  --title "Add woodpecker SA" --body "Adds the ServiceAccount ..."
# prints: #<number> <html_url>

# Comment on a PR
agentpr pr comment --repo unkin/argocd-apps --pr 42 --body "Rebased, CI green."

agentpr --version
agentpr --help

Non-zero exit on any API error.

watchpr

Poll PRs and exit (reporting what changed) when a tracked PR merges/closes, gets a new comment from someone other than the agent, its CI fails (failure/error), or it loses mergeability (a conflict appears). Benign transitions — CI pendingsuccess, the agent's own comments — are ignored.

# Watch until something meaningful happens (default interval 60s)
watchpr unkin/argocd-apps#42

# Multiple PRs, custom interval; refs accept #N or :N
watchpr --interval 30s unkin/argocd-apps#42 unkin/terraform-vault:98

# One-shot: print current state and exit 0 (great for scripts)
watchpr --once unkin/argocd-apps#42
watchpr --once --json unkin/argocd-apps#42

On a meaningful change watchpr prints the reason and the PR's current state, then exits 0. Use --json for machine-readable output.

agentws

agentws gives an agent an isolated git worktree per branch without disturbing Ben's shared checkouts. Repos are cloned into the source root (~/src/prodenv/<repo>) so branches created here are visible in the main checkout too; the worktrees themselves live under the worktree root (~/.cache/agentws/<repo>__<branch>).

# Clone unkin/argocd-apps into ~/src/prodenv if missing, then add a worktree for
# a new branch off the remote default branch. Prints the worktree path.
agentws new argocd-apps --branch benvin/my-change

# Branch off a specific base instead of the remote default
agentws new argocd-apps --branch benvin/hotfix --from release-1.2

# List managed worktrees (repo, branch, path)
agentws list

# Remove a worktree (by path or branch); refreshes the source repo afterwards
agentws rm benvin/my-change
agentws rm ~/.cache/agentws/argocd-apps__benvin-my-change --delete-branch

# Remove every managed worktree and prune each source repo
agentws clean

# Print a fresh Gitea token for the agent login
agentws token

Auth / credential-helper design

Gitea tokens minted from Vault are short-lived (~1h), so agentws never persists one in a remote URL or in git config. Instead it wires itself as an ephemeral git credential helper:

  • agentws token prints a fresh token to stdout (handy for scripts).
  • agentws credential get speaks the git credential protocol on stdin and, for the configured Gitea host only, emits username=$AGENT_LOGIN + password=<fresh token>.

agentws new sets this up per worktree without touching the shared checkout: it enables extensions.worktreeConfig on the repo once, then writes user.name / user.email and credential.helper = !<agentws> credential to the per-worktree config. Clone/fetch use the same helper via a transient -c credential.helper=...; the shared origin URL is left clean. On worktree removal agentws fetches in ~/src/prodenv/<repo> so its default branch stays current.

agentvault

Deterministic Vault flows, each a single self-contained invocation: the tool reads and writes the secrets itself, and prints only identifiers.

seed-outpost

Copy an Authentik outpost's token into Vault KV-v2. agentvault reads the Authentik API token from kv/service/authentik/agent-api-token, resolves the named outpost's token_identifier, fetches its key via /api/v3/core/tokens/<identifier>/view_key/ and writes it to the destination KV path. The token value is never printed or logged.

agentvault seed-outpost \
  --outpost k8s-outpost \
  --dest-path kubernetes/namespace/authentik/default/outpost-token
outpost:          k8s-outpost
token_identifier: ak-outpost-k8s-outpost
dest:             kv/kubernetes/namespace/authentik/default/outpost-token
version:          3

Re-running is safe: it writes a new KV version. Flags: --outpost and --dest-path are required; --dest-key (default token), --kv-mount (default kv), --token-path (default service/authentik/agent-api-token) and --authentik-url override the rest.

Errors name the failing stage: AppRole login, KV read denied (policy not applied), outpost not found (terraform not applied), view_key failure, or KV write denied.

seed-oauth

Make a Vault KV-v2 path hold a complete oauth2-proxy credential set. It is a read-modify-write: client_id is set from the flag, client_secret and cookie_secret are generated (32 bytes from crypto/rand) only when missing, every other key on the path is written back untouched, and nothing is written at all when the secret is already correct. cookie_secret is base64url so it decodes to exactly the 32 bytes oauth2-proxy requires.

agentvault seed-oauth \
  --path kubernetes/namespace/repospawner/default/oauth-credentials \
  --client-id 4f1c…
path:          kv/kubernetes/namespace/repospawner/default/oauth-credentials
keys:          client_id, client_secret, cookie_secret
  client_id:     created
  client_secret: kept
  cookie_secret: created
version:       4

That is the common case: the provider's client_secret already lives on the path, so only the missing keys are added. A run with nothing to do prints version: unchanged and issues no write.

Flags: --path and --client-id are required; --kv-mount (default kv) and --rotate override the rest. --rotate regenerates both secrets — only use it when the IdP provider's secret is being rotated alongside, since a rotated client_secret no longer matches the provider.

Errors name the failing stage: AppRole login, KV read denied, or KV write denied. Only key names, actions and the KV version are printed.

Build & package

make build                 # -> dist/agentpr, dist/watchpr, dist/agentws, dist/agentvault
make test                  # go test -race ./...
make rpm                   # build + package dist/agent-tools-<version>-1.x86_64.rpm

Release is tag-driven (v*) via Woodpecker: builds the RPM, PUTs it to the artifactapi rpm-internal yum repo, and cuts a Gitea release with cross-compiled binaries attached.

Version bump

make patch   # or: make minor / make major  — tags vX.Y.Z and pushes the tag