7 Commits
Author SHA1 Message Date
mathiasandClaude Opus 5 da76a6b3d2 feat(mcp): unpark the gitea server — it needs no AgentSecrets token
CI / Check / Test (push) Failing after 1s
CI / Tag release (push) Skipped
gitea-mcp was parked alongside cad on the stated reason that nothing puts its
token into the session environment: GITEA_MCP_STATIC_TOKEN lives in the
AgentSecrets vault, which the session identity deliberately cannot read
(infra#224), so enabling it was blocked behind #32/#33.

That reason never applied to gitea. gitea-mcp authenticates by PASS-THROUGH:
the bearer is a Gitea PAT, which the server validates against Gitea and then
acts as. No dedicated MCP token exists to be missing. DMABE_GITEA_API_TOKEN is
op://HomeLab and already in the session environment.

Verified on koala 2026-09-07 against tailscale serve 8446 -> NodePort 30340:
initialize returns 200 with serverInfo gitea-mcp, tools/list returns 41 tools.
Two DIFFERENT PATs both authenticate while GITEA_MCP_TOKEN 401s -- that is what
proves pass-through validation rather than a static-token comparison, and it
also shows GITEA_MCP_TOKEN is stale (it 401s against git-mcp.d-ma.be too).

cad stays parked; its blocker is real and unchanged.

Also adds bin/check-mcp-env and tests/mcp_config_test.sh, wired into task check.
The fault it gates is silent: a header interpolating an undefined variable does
not error, it sends an empty Authorization, and Claude Code reports "requires
authentication" -- indistinguishable from a revoked credential. That cost a full
investigation on koala today before the credential turned out to be fine. The
check exits 2 rather than 0 when it cannot run (no jq, no env-file, as in CI),
so an unverified config is never reported as a clean one. Servers under _parked
are exempt: referencing an unprovided variable is what parked means.

Bump-Type: minor

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 07:24:23 +02:00
mathias c377c23e21 fix(mcp): correct the parked reason — the tokens work, the delivery does not
CI / Check / Test (push) Successful in 2s
CI / Tag release (push) Successful in 1s
The previous note blamed infra#224 for cad and gitea-mcp being unreachable.
Both claims were wrong. infra#224 is closed, and closed deliberately: rather
than widening koala-homelab's grants, a second scoped token was provisioned at
~/.op-agent-write.env for AgentSecrets. Sourcing it makes `op vault list`
return AgentSecrets, and an MCP initialize against both endpoints returns 200 —
serverInfo gitea-mcp 0.1.0 and cad aed2aaf. Nothing about the services or the
proxying is blocked.

What is actually blocked is delivery into the session environment. By design no
single identity on koala reads both vaults, and the session env comes from one
`op run --env-file ~/.op-env` under the HomeLab-only token. A mixed-vault file
does not degrade gracefully: tested on a two-line copy, `op run` aborts on the
unresolvable reference and BRAIN_MCP_TOKEN goes unresolved with it — so
appending to ~/.op-env would take out the MCP servers that currently work.

Refs #32, #33
Bump-Type: patch
2026-08-29 14:22:17 +02:00
mathias 7ba01abdbe chore(mcp): park cad and gitea-mcp entries, blocked on vault visibility
CI / Tag release (push) Successful in 1s
CI / Check / Test (push) Successful in 1s
Both services are now proxied from koala and reachable: tailscale serve 8445 ->
127.0.0.1:31877 (cad) and 8446 -> 127.0.0.1:30340 (gitea-mcp). Both answer 401
unauthenticated, so the path is right and auth is enforced.

They stay out of mcpServers because neither token resolves on this host. The
static tokens live in the 1Password AgentSecrets vault; koala's op identity sees
HomeLab only:

  op vault list                    -> HomeLab, and nothing else
  op://HomeLab/CAD_STATIC_TOKEN    -> could not find item
  op://HomeLab/gitea-mcp-shared-token -> could not find item

That is infra#224, not the OAuth-discovery bug #32 and #33 both flagged as the
likely blocker. A live entry that cannot authenticate would fail on every
session start, so the wiring is recorded rather than enabled.

The item names come from the ExternalSecrets, read by reference only — no
secret value was retrieved, printed, or transformed at any point.

Refs #32, #33
Bump-Type: patch
2026-08-29 14:18:38 +02:00
mathiasandClaude Opus 5 7db7e0779b feat(mcp): serve brain and infra over Tailscale Serve with real TLS
release / tag (push) Successful in 1s
Replaces the raw NodePort URLs with Tailscale Serve endpoints on koala:

  brain  https://koala.tail30472e.ts.net:8443/mcp  -> 127.0.0.1:30330
  infra  https://koala.tail30472e.ts.net:8444/mcp  -> 127.0.0.1:30342

Why this beats both alternatives that were tried:

The public d-ma.be path exits to Cloudflare and NPM to reach a k3s service on
the machine the session is running on, and Cloudflare rate-limits and
size-caps tool results — the original complaint. The raw NodePort path fixed
that but only worked from koala: every namespace carries default-deny-ingress
and an allow policy naming the traefik namespace, so kube-proxy's
KUBE-EXTERNAL-SERVICES chain RSTs external callers. Admitting tailnet clients
there would need externalTrafficPolicy: Local on each service plus an ipBlock
per policy, because the default Cluster policy SNATs the source and an ipBlock
can never match.

Serve sidesteps all of it. tailscaled terminates TLS with a genuine cert and
proxies over loopback, so the request reaches the NodePort from koala itself —
the case the NetworkPolicy already permits. No policy change, no
externalTrafficPolicy change, no firewall rule, no cert-manager, no DNS record.

Port-per-service rather than path-per-service: the mapping is 1:1 with no
prefix rewriting, so what the client requests and what the server sees cannot
desync.

Verified: 200 authenticated in 7.6ms from koala, and `claude mcp list` reports
Connected against the Serve URL. An earlier apparent OAuth resource-metadata
mismatch turned out to be an artifact of testing without an Authorization
header — with a valid bearer the client never enters OAuth discovery.

Client requirement: `tailscale set --accept-dns=true`. Confirmed on iguana that
100.100.100.100 resolves the MagicDNS name correctly while the system resolver
does not, so the name fails to resolve until Tailscale is accepted as a DNS
provider. Tracked in #24 as the Mac prerequisite. Note Serve matches on SNI, so
substituting the raw tailnet IP is not a workaround.

Bump-Type: minor

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
2026-08-24 21:11:49 +02:00
mathiasandClaude Opus 5 6151b973bc perf(mcp): reach brain and infra over tailnet NodePorts, not the public edge
release / tag (push) Successful in 1s
Every MCP call from Claude Code was going out through claude.ai's connector
infrastructure, across the public internet, through Cloudflare and NPM on
piguard, back into koala's k3s ingress — to reach a service on the same
machine the session is running on. For brain that means a two-cloud round trip
to read /var/lib/supervisor/brain, a file on koala's local disk.

Cloudflare rate-limits and size-caps tool results on that path, which is the
actual user-facing symptom. Measured from koala:

  brain  koala:30330    200 in  6.8ms
  infra  koala:30342    200 in  3.6ms
  brain  public edge    200 in 27.7ms

infra starts working again as a side effect: its only fault was an SNI/cert
gap at the public edge (TLS "unrecognized name"), which the NodePort path does
not traverse. No NPM change needed for local use.

Note this is not a relocation of existing config. The mcpServers block that
lived in ~/.claude/settings.json was inert — Claude Code reads server
definitions from ~/.claude.json, project .mcp.json, plugins and connectors,
not from settings.json. Verified: a uniquely-named server placed there is
invisible to both `claude mcp list` and `claude mcp get`. So before the plugin
existed, koala had no direct path at all and every call went via Cloudflare by
construction. That also explains how a stale GITEA_MCP_TOKEN went unnoticed
for five days: nothing consumed it.

Safe to land now because koala is the only host with the plugin installed
(#24). The koala firewall must allow 30330/30342 on the Tailscale interface —
NOT on the LAN broadly, so tailnet membership stays the perimeter — before the
Macs get the plugin. Tracked as a prerequisite in #24. Measured today: those
ports are refused from iguana over both LAN and tailnet while :22 answers, so
the rule does not exist yet.

Bump-Type: minor

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
2026-08-24 18:00:17 +02:00
mathiasandClaude Opus 5 5c8341c797 fix(mcp): drop the gitea server — it was a redundant, silently stale copy
release / tag (push) Successful in 2s
Every gitea__* call from Claude Code on koala already resolves through the
claude.ai connector, not through this env-var-authenticated HTTP server. The
entry was a second path to the same service that nothing actually used.

That redundancy is exactly why it rotted unnoticed. GITEA_MCP_TOKEN on koala
resolves ~/.op-env -> op://HomeLab/GITEA_MCP_TOKEN, an item orphaned by the
2026-07-30 consolidation that moved gitea-mcp, agentsquad-serve and
crush-runner onto a single shared item in the AgentSecrets vault. The host
env-file was never migrated and cannot be: koala's 1Password service account
has no access to AgentSecrets, while onepassword-connect in-cluster maps both
vaults. So the host copy diverged and stayed diverged for five days.

The cluster was never affected — zero 401s from the pod CIDR in 30 days, every
rejection came from the gateway. Deleting the divergent copy beats maintaining
a second source of truth for a token this host does not need.

brain stays: it authenticates, and it is genuinely used from the plugin.
infra stays but is currently unreachable for an unrelated reason —
infra-mcp.d-ma.be fails TLS with "unrecognized name", an SNI/cert gap at the
NPM edge, tracked separately.

Bump-Type: patch

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
2026-08-24 17:31:11 +02:00
mathiasandClaude Opus 5 747c6c7f4c feat(plugin): ship the secret-guard and pre-task-ritual hooks + MCP servers
The plugin now carries the two hooks Mathias owns and the brain/gitea/infra
MCP server definitions, not just skills.

Why this matters more than tidiness: both hooks existed ONLY in koala's
~/.claude/, which is host-local and unsynced. flamingo and iguana have been
running with no secret-guard at all — that is the control which blocks the
decrypt-and-print shape behind the 2026-08-05 ntfy-token leak, the op-item-get
shape behind 2026-07-30, and the base64-bypass shape behind 2026-06-11. Its
absence on two of three machines is a security gap, not a config gap. Shipping
it in the plugin means installing the plugin installs the guard.

.mcp.json references tokens as ${BRAIN_MCP_TOKEN} / ${GITEA_MCP_TOKEN} only —
env indirection, never literal values, so the manifest is safe to commit.

tests/secret_guard_test.sh pins 14 cases. It asserts on
hookSpecificOutput.permissionDecision, NOT exit status: the guard always exits
0 by contract, so an exit-status assertion would pass vacuously while the guard
silently allowed everything. Each deny case names the incident that produced it.

gsd-* hooks are deliberately NOT migrated — GSD owns and updates those in place.

Bump-Type: minor

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
2026-08-23 14:31:04 +02:00