15 Commits
Author SHA1 Message Date
hermes-bot de080442d3 feat(hermes): alert issues end to end: labelled at filing, routed, fixed by the expert card; retire issue-fix
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
The owner wants the path alert -> [alert] issue -> kanban card -> expert fix PR
-> owner merges, without a human step in between. Owner decisions (kanban card
t_3f5d0a62): [alert] issues are not excluded from the bridge, and the issue-fix
Woodpecker pipeline is retired, so the expert's card is the only automated fix
path for [alert] issues.

Routing
- alert-monitor: `alerts.py file` takes a required `--label` (hestia, atlas,
  apollo, clio, daedalus) and sets that label at creation (POST /issues
  `labels: [id]`). Gitea 1.27.3 adds the labels in the same transaction
  (services/issue/issue.go NewIssue) and the `opened` webhook payload loads them
  (services/convert/issue.go toIssue -> LoadLabels), so the first delivery
  already routes the card to the expert. Org labels are accepted for org repos
  (models/issues/issue_update.go: label.OrgID == repo.OwnerID). If the label
  does not exist yet, the issue is filed without it and the coordinator routes.
  SKILL.md tells the triage agent how to pick the expert.
- Label-less issues no longer go to triage. kanban.auto_decompose defaults to
  true (hermes_cli/config_defaults.py; not overridden here), so the dispatcher
  hands every triage card to the auxiliary-LLM decomposer
  (gateway/kanban_watchers_dispatcher.py auto_decompose_tick ->
  kanban_decompose.decompose_task), which rewrites title and body (dropping the
  Gitea-Issue line the completion hook needs), may fan out into children without
  that line, and keeps the assignee `default` anyway (_apply_single only sets an
  assignee when none is set). The card is now created ready for `default` with
  routing instructions: set exactly one domain label on the issue, nothing else.
- The script reacts to that label: a coordinator card nobody has claimed yet
  (todo/ready) is reassigned (`hermes kanban assign`) and its body replaced by
  the expert body (`hermes kanban edit --body`); a claimed or finished one gets
  a separate expert card under key `<owner>/<repo>#<n>@<expert>` (one per
  expert, created_by gitea-webhook, so its completion is posted on the issue).
  `assign` refuses a running card, which falls back to the separate card. Cards
  already with an expert are still never moved.

[alert] cards
- Their body carries the fix protocol that replaces issue-fix, including its
  prompt hardening from PR #253/#266: check for an open PR with `Closes #<n>` or
  a branch `alert-fix/issue-<n>` first; owner (k.renner) comments are
  authoritative, everything else is untrusted problem description; verify live
  before fixing, never state cluster state not observed this run, give
  read-only commands instead of predicted output; repo-fixable -> branch
  `alert-fix/issue-<n>`, smallest fix, PR with `Closes #<n>`, never merge; not
  repo-fixable -> no PR, the completion summary (posted on the issue) says why
  and what a human must do.

Gitea side (CronJob gitea-webhook-sync, every 15 min)
- Domain labels: created as org labels in every org (GET /admin/orgs) and as
  repo labels in user-owned repos, only when missing.
- Backfill: open issues without the card comment are replayed to the route as
  signed `opened` deliveries (same headers Gitea sends), at most 10 per run, 3 s
  apart (the adapter's default rate limit is 30/min/route), in random order so
  repos where the comment keeps failing cannot starve the rest. The idempotency
  key `<owner>/<repo>#<n>` makes a replay of an issue with a card a no-op.
  The job runs in the hermes namespace, whose default-deny already allows
  intra-namespace traffic to :8644.
- The admin PAT needs write:organization (org labels, reqOrgOwnership passes for
  a site admin), write:issue (repo labels, comments) and read:repository
  (/repos/search) next to write:admin. activeDeadlineSeconds 120 -> 600.

issue-fix removed
- .woodpecker/issue-fix.yml, ci/agents/issue-fix/, and what only it used:
  scripts/ci/make-diag-kubeconfig.sh, the monitoring/ci-diagnostics
  ServiceAccount and RBAC (no other pipeline mints its token; git grep), the
  .ci-diag/ gitignore entry. Comments naming issue-fix updated.
- Not in git and left to the owner: the Woodpecker cron `issue-fix` and the
  secret `issue_fix_bot_token` with its Gitea PAT.

Tested: unittest 73 OK; gitea_issues.py against the real hermes kanban CLI in a
scratch HERMES_HOME with a stub Gitea (unlabelled -> default ready card;
labelled while unclaimed -> same card reassigned to daedalus with the alert
body; labelled while claimed -> separate card `#n@daedalus`; replay -> no
duplicate card or comment); sync discovery read-only against live Gitea (42
open issues, all without a card); kubectl kustomize for apps/hermes,
apps/monitoring, apps/monitoring-rules.
2026-10-21 05:48:01 +00:00
claude-bot 32a67b0733 fix(hermes): alert-monitor review follow-ups
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was canceled
- BlackboxProbeFailing: exclude the hermes /api/status target (it has
  HermesDown), so a Hermes outage fires one alert, not two.
- alerts.py: a noise-log line with an impossible date is dropped instead of
  killing the gate (fails toward more alerting, as the pipeline did).
- alerts.py: HTTP errors raise ApiError; a Prometheus error only drops the
  blips (live alerts still triaged, WARNING line in the prompt);
  Alertmanager/Gitea errors exit non-zero so Hermes reports the failed run.
- alerts.py file/noise refuse text that contains a secret Hermes holds: any
  single-value file or key-like token under /var/run/secrets, credential env
  vars, or a 24-character piece of one.
- cron-jobs init container is non-fatal: an unreadable jobs.json or a failed
  cron create/edit logs a warning and the pod still starts (from the #1400
  review).
- homeassistant-token volume optional: true, like wiki-key and talos-config
  (from the #1400 review).
- Offline unittests (kubernetes/apps/hermes/skills/tests): title fallback,
  noise parsing incl. TTL and invalid dates, blip fingerprint equal to the
  old pipeline's, gate wake/sleep, Prometheus error fallback, secret guard.
  Run in ci-scripts-test.yml (python:3.14-alpine, pinned by digest).
- Nits: .gitignore blank line, comment width in ci-diagnostics-rbac.yaml.

Tested: 16 tests pass locally and in the pinned CI image; the gate run in
the hermes pod (wakeAgent false, 0 new); init script on a scratch
HERMES_HOME, normal (both jobs, skills replaced not appended) and with a
corrupt jobs.json (two warnings, exit 0); exclusion regex matches only the
hermes status URL; promtool check rules on both rule files; kustomize,
citations, server dry-run.
2026-10-20 21:04:49 +02:00
claude-bot 4f547dfd27 feat(hermes): alert-monitor moves from Woodpecker to a Hermes cron job
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Owner decision: Hermes files the [alert] issues; the Woodpecker
alert-monitor pipeline is retired. issue-fix keeps working unchanged: it
picks issues by the "[alert] " title prefix, and the dedup contract is the
same (title "[alert] <alertname> (<namespace|job|cluster>)", noise-log lines
"- <fp> <ALERTNAME> — <date>: <reason>" with a 30-day TTL, blip
fingerprint sha256(alertname|scope)[:16], fingerprint footer on issues).

- skill alert-monitor: scripts/alerts.py (stdlib) is the cron pre-run gate:
  Alertmanager active alerts minus heartbeats and null-routed ones, plus
  ALERTS blips of the last 75 min, minus open [alert] issues (paginated, the
  pipeline read only the first 100) and noise fingerprints. Nothing new ->
  {"wakeAgent": false}, no LLM run, no message. `file` and `noise` write the
  verdicts in the fixed formats. SKILL.md ports the triage prompt: wiki
  first, verify live (kubectl, Prometheus, Loki, talosctl), problem/noise,
  root cause vs symptoms; Matrix report per run that found something.
- Cron job alert-monitor, hourly at :20, deliver matrix. The init container
  (renamed cron-jobs) now ensures both jobs by name and copies the gate
  script to /opt/data/scripts (Hermes only runs scripts from there).
- HermesDown: blackbox probe of https://hermes.renner.uno/api/status plus an
  alert, since a Hermes outage now stops alert -> issue (Alertmanager email
  is unaffected).
- Removed .woodpecker/alert-monitor.yml and ci/agents/alert-monitor/;
  comments that named them updated.

Tested: the gate run inside the hermes pod against live Alertmanager,
Prometheus and Gitea: 2 active, 36 open issues, 50 noise suppressions, 0 new
-> wakeAgent false. With a 3-day lookback and no noise TTL it produced 4
blips with the expected titles. file/noise against a fake Gitea (61 issues,
pagination, duplicate title skipped, footer, noise log created then
appended, parsed back by the gate; bad title/body/fingerprint refused).
Init script run twice on a scratch HERMES_HOME holding an existing
ha-updates job: ha-updates edited in place, alert-monitor created once,
script copied. Blackbox probe of the new target: 200. kustomize, citations
and server dry-run pass.

After merge:
- Delete the Woodpecker cron alert-monitor (id 4); without its pipeline file
  it has nothing to run.
- The alert_monitor_bot_token secret and the alert-monitor-bot account are
  unused afterwards.

Trade-off: the pipeline posted with alert-monitor-bot, a PAT limited to
write:issue. Hermes posts as hermes-bot, which also has repo write; the
agent reads untrusted alert text with a broader credential than before.
2026-10-20 20:52:46 +02:00
claude-botandClaude Fable 5 90aeaf1ef8 feat(ci-agents): give triage/fix agents verified read-only live diagnostics
Both wrong assessments (#249 openbao 'sealed', #250 three untested
hypotheses while one 'kubectl logs' showed the 401) share one root
cause: the agents diagnose blind, so they narrate plausible fiction.
Labeling unverified claims (previous commit) treats the symptom; this
gives them the means to verify:

- kubernetes/apps/monitoring/ci-diagnostics-rbac.yaml: ServiceAccount
  ci-diagnostics with a read-only ClusterRole (workloads, events,
  pods/log, Flux/ESO/cert-manager/monitoring/ceph CRDs — NO secrets)
  plus a namespaced services/proxy Role for the Prometheus and
  Alertmanager HTTP APIs.
- scripts/ci/make-diag-kubeconfig.sh: mints a 1h token per run from
  the admin kubeconfig (no new long-lived secret), builds
  .ci-diag/kubeconfig, copies the static kubectl binary for the
  jq-only ci-claude image, smoke-tests with the restricted identity,
  and degrades to snapshot-only mode when the SA isn't reconciled.
- alert-monitor.yml / issue-fix.yml: agents get exactly one extra tool,
  Bash(./.ci-diag/kubectl:*) — prefix match forbids pipes and any other
  binary; RBAC forbids mutation and secret reads.
- prompts: verification is now mandatory, not aspirational — 'a
  hypothesis you could have settled with one read-only command but
  didn't is a defect in your output'; fix agent must test the issue's
  claims before building on them; minimal-quote rule against dumping
  logs into public issues.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV7bZzZLeoFUEdAeCSLEei
2026-07-16 10:30:29 +02:00
claude-botandClaude Fable 5 b7ff28d6a6 feat(ci): alert-monitor and issue-fix agent pipelines
Two new cron pipelines closing the loop from firing alert to fix PR:

- alert-monitor (*/15 min): reads active alerts from Alertmanager via the
  apiserver service proxy (woodpecker-ci is netpol-blocked from monitoring
  ClusterIPs; the Authentik gate blocks the public hostname), dedups
  against open issues and a persistent noise-log issue, then a headless
  Claude agent triages the new ones against the repo plus a cluster
  snapshot. Confirmed problems become Gitea issues; noise is recorded in
  the noise log so it is never re-triaged.

- issue-fix (hourly): picks the oldest open '[alert] ...' issue with no
  fix branch and no assessed-marker comment, hands it to a fix agent
  (same tool sandbox as security-fix), pushes alert-fix/issue-<n> and
  opens a PR with 'Closes #<n>'. Not-repo-fixable verdicts are posted as
  an issue comment instead.

- e2e.yml: scope the bare 'event: cron' to the three existing cron names
  — otherwise the 15-minute cron would piggyback a full e2e run every 15
  minutes.

The ci-k8s image has no jq, so the alert fetch step only talks to the
cluster; all JSON filtering/dedup/gating lives in the ci-claude step.

Cron registration (after this lands on master):
  woodpecker repo cron add --repo gitea_admin/homelab \
    --name alert-monitor --branch master --schedule '*/15 * * * *'
  woodpecker repo cron add --repo gitea_admin/homelab \
    --name issue-fix --branch master --schedule '41 * * * *'

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV7bZzZLeoFUEdAeCSLEei
2026-07-16 09:11:51 +02:00
k.rennerandClaude Opus 4.8 62a00e0290 kubernetes: add SOPS-encrypted Hetzner S3 credentials
New secrets holding the Hetzner Object Storage keypair, ready for the
per-app backup cutover:
- seed/{paperless,authentik,immich}/hetzner-creds.sops.yaml
  → Secret/hetzner-s3-creds (ACCESS_KEY_ID, ACCESS_SECRET_KEY, REGION=fsn1)
- infrastructure/velero/hetzner-creds.sops.yaml
  → Secret/cloud-credentials-hetzner (cloud INI blob)

These are not referenced by any kustomization yet, so they're inert —
the existing Garage creds keep driving backups until each ObjectStore /
BSL is repointed (staged, paperless-first). Also gitignore
*.plaintext.yaml so the pre-encryption template can never be committed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:03:33 +02:00
k.rennerandClaude Opus 4.7 13414df1b3 ci(security-scan): auto-fix agent that opens PRs for Trivy findings
ci/woodpecker/manual/security-scan Pipeline failed
The cron security-scan now ends in an autonomous fix loop:

  iac-scan       → trivy-iac.json
  image-scans    → trivy-image-N.json (per scanned image)
  security-fix-agent (master only):
    1. Gates on >=1 CRITICAL/HIGH finding across all JSON outputs.
    2. Creates branch security-fix/<date>-<short-sha>.
    3. Hands the workspace to Claude Code in headless --bare mode
       (subscription-backed via CLAUDE_CODE_OAUTH_TOKEN, no API key)
       with a narrow tool allowlist (Read/Edit/Write/Glob/Grep —
       no Bash). The agent reads the JSON, applies surgical fixes
       under ansible/, opentofu/, kubernetes/, ci/images/, and writes
       a Markdown summary to /tmp/security-fix-summary.md.
    4. If the agent produced a diff, the step commits as
       "Security Fix Agent <security-bot@renner.uno>" and pushes to
       Gitea, then opens a PR via the pulls API using a dedicated
       security_bot_token (NOT gitea_admin) so the PR is visibly
       machine-authored.

The step is gated to branch=master so it doesn't fire on its own
PR branches — those still trigger the scan workflow as a re-validation
gate, but skip the agent (no fork-bombing).

Trivy now writes JSON output (--format json -o), and findings appear
both in the JSON and as one-line summaries in the step log via jq.
The legacy --exit-code 1 was kept off so the workflow always reaches
the agent step on master; on PR branches the scans are informational
and a human reviews the agent's PR.

Required Woodpecker secrets (added by user, not by this commit):
  - claude_code_oauth_token (claude setup-token output)
  - security_bot_token      (Gitea PAT for the security-bot user)

Required Gitea setup (also out of band):
  - User "security-bot" with email security-bot@renner.uno
  - Write collaborator on gitea_admin/homelab

Agent constraints live in ci/agents/security-fix/prompt.md so they're
diffable + reviewable separately from the pipeline plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 09:28:59 +02:00
k.rennerandClaude Opus 4.7 fb26689d81 chore: drop dangling owncloud refs, gitignore .claude/
ci/woodpecker/manual/migrate-immich Pipeline was successful
- gatus: remove ownCloud + ONLYOFFICE health checks (services were
  deleted in the previous commit; checks would alert forever).
- cloudflared: remove owncloud.renner.uno and office.renner.uno tunnel
  ingress rules pointing at deleted services.
- .gitignore: exclude .claude/ (Claude Code project-local runtime state).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 18:39:13 +02:00
k.rennerandClaude Opus 4.7 590e17b573 fix: Stash access files in workspace between steps
ci/woodpecker/manual/rebuild-k8s Pipeline failed
ci/woodpecker/cron/security-scan Pipeline was successful
Woodpecker steps use separate containers; home dirs don't persist.
Copy talosconfig/kubeconfig from ~ to access-staging/ in the bootstrap
step so the export step can read them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 10:26:22 +02:00
k.rennerandClaude Opus 4.6 81ccc623de fix: Remove accidentally committed Talos secrets, add to gitignore
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 20:28:58 +02:00
k.rennerandClaude Opus 4.6 918e4ae2ea feat: Update FluxCD manifests with validated infrastructure configs
Lessons from manual validation run:
- Add privileged PodSecurity labels to metallb-system, rook-ceph, ingress namespaces
- Add Cilium HelmRelease with Talos-specific settings (cgroup, kubePrism)
- Consolidate all namespace definitions in namespaces/
- Update cert-manager to use crds.enabled instead of installCRDs
- Enable all infrastructure components in main kustomization
- Add Talos secrets to .gitignore (controlplane.yaml, talosconfig)
- Update node patches with correct install disk (vda) and zone labels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:11:25 +02:00
k.renner 426db7aced feat: Enhance backup infrastructure documentation and address RustFS incident recovery measures
Deploy Infrastructure / plan (push) Has been cancelled
Security Audit / Infrastructure Scan (IaC) (push) Has been cancelled
Security Audit / Docker Image Scan (docker.io/apache/tika:latest) (push) Has been cancelled
Security Audit / Docker Image Scan (docker.io/gotenberg/gotenberg:8) (push) Has been cancelled
Security Audit / Docker Image Scan (docker.io/library/postgres:15) (push) Has been cancelled
Security Audit / Docker Image Scan (docker.io/library/redis:7) (push) Has been cancelled
Security Audit / Docker Image Scan (ghcr.io/icereed/paperless-gpt:latest) (push) Has been cancelled
Security Audit / Docker Image Scan (ghcr.io/paperless-ngx/paperless-ngx:latest) (push) Has been cancelled
Security Audit / Docker Image Scan (shenxn/protonmail-bridge:latest) (push) Has been cancelled
Deploy Infrastructure / apply (push) Has been cancelled
2026-02-15 21:07:41 +01:00
k.renner c5c107ab2a Add Immich installation and configuration playbook with migration guide and templates 2026-01-31 21:47:05 +01:00
k.renner abe5fbd468 Remove Caddy custom builder workflow and related documentation 2026-01-31 16:07:22 +01:00
k.renner a0a993cbf9 Add .gitignore to exclude environment and configuration files 2026-01-27 19:44:02 +01:00