0dcf5cc1369fe48fca6ceb9b620da3cdf5535a46
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
da76a6b3d2 |
feat(mcp): unpark the gitea server — it needs no AgentSecrets token
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> |
||
|
|
c377c23e21 |
fix(mcp): correct the parked reason — the tokens work, the delivery does not
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 |
||
|
|
7ba01abdbe |
chore(mcp): park cad and gitea-mcp entries, blocked on vault visibility
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|