From 01e0367b0089f49b9025607f89c389ac6ba1f03f Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Thu, 5 Nov 2026 00:17:52 +0100 Subject: [PATCH 1/3] feat(agentic-engineering): bundle Coroot's MCP endpoint Coroot v1.28 and later serve an MCP endpoint at /mcp: application topology, alerts, SLO incidents, traces, PromQL metrics and logs. It needs a bearer token on every request and checks each tool call against the caller's role, so a service account in the Viewer role lets agents investigate without resolving alerts. Add a coroot server whose address comes from COROOT_URL in Claude Code's environment, and coroot-mcp-headers.sh, which sends the service account's API key from coroot-api-key in the plugin data directory with no fallback. Neither the address nor the key enters any repository. Without them Claude Code reports one missing-variable error for coroot and the other servers keep working. Co-Authored-By: Claude Opus 5.5 --- .claude-plugin/marketplace.json | 2 +- .github/plugin/marketplace.json | 2 +- docs/plugins.md | 2 +- docs/resources.md | 15 ++-- .../.claude-plugin/plugin.json | 2 +- .../.codex-plugin/plugin.json | 2 +- plugins/agentic-engineering/.mcp.json | 5 ++ plugins/agentic-engineering/CHANGELOG.md | 16 +++++ plugins/agentic-engineering/CONNECTORS.md | 2 +- plugins/agentic-engineering/README.md | 24 ++++++- plugins/agentic-engineering/plugin.json | 2 +- .../scripts/coroot-mcp-headers.sh | 43 ++++++++++++ .../scripts/coroot-mcp-headers.test.sh | 69 +++++++++++++++++++ 13 files changed, 171 insertions(+), 15 deletions(-) create mode 100755 plugins/agentic-engineering/scripts/coroot-mcp-headers.sh create mode 100755 plugins/agentic-engineering/scripts/coroot-mcp-headers.test.sh diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index ea519f2..d03fc7c 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -20,7 +20,7 @@ { "name": "agentic-engineering", "description": "The autonomous engineering system for repository portfolios and everything its agents need — engineer, read-only surveyor, meta-engineer and Flux troubleshooter agents; portfolio, product, spend and improvement workflows; GitHub, CodeRabbit review, security, library documentation, Go, Kubernetes, Flux, Talos, SvelteKit frontend, Stripe payments, engineering-practice, design, product-management, marketing, finance and productivity skills; and the MCP servers they use; configured by the consumer AGENTS.md", - "version": "7.1.1", + "version": "7.2.0", "source": "./plugins/agentic-engineering" }, { diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index ea519f2..d03fc7c 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -20,7 +20,7 @@ { "name": "agentic-engineering", "description": "The autonomous engineering system for repository portfolios and everything its agents need — engineer, read-only surveyor, meta-engineer and Flux troubleshooter agents; portfolio, product, spend and improvement workflows; GitHub, CodeRabbit review, security, library documentation, Go, Kubernetes, Flux, Talos, SvelteKit frontend, Stripe payments, engineering-practice, design, product-management, marketing, finance and productivity skills; and the MCP servers they use; configured by the consumer AGENTS.md", - "version": "7.1.1", + "version": "7.2.0", "source": "./plugins/agentic-engineering" }, { diff --git a/docs/plugins.md b/docs/plugins.md index 18fd802..eb3298f 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -6,7 +6,7 @@ A **skill** teaches an assistant a task. An **agent** gives it a specialized rol | Plugin | Resources | Description | |--------|-----------|-------------| -| [`agentic-engineering`](../plugins/agentic-engineering/) | `accessibility-review`, `agent-improvement`, `agent-instructions`, `architecture`, `audit-support`, `autofix`, `brand-review`, `campaign-plan`, `check-skills`, `close-management`, `code-review`, `codeql`, `competitive-brief`, `connector-setup`, `content-creation`, `context7-mcp`, `conventional-release`, `debug`, `dependabot`, `dependency-scanning`, `deploy-checklist`, `design-critique`, `design-handoff`, `design-system`, `documentation`, `draft-content`, `email-sequence`, `financial-statements`, `find-skills`, `frontend-design`, `gh-cli`, `gh-stack`, `git-commit`, `github-actions-docs`, `github-actions-efficiency`, `github-actions-hardening`, `github-issues`, `gitops-cluster-debug`, `gitops-knowledge`, `gitops-tenant-onboarding`, `golang-pro`, `grilling`, `incident-response`, `journal-entry`, `journal-entry-prep`, `memory-management`, `metrics-review`, `performance-report`, `portfolio-maintenance`, `product-brainstorming`, `product-documentation`, `product-engineering`, `prototype`, `reconciliation`, `refactor`, `research-synthesis`, `roadmap-update`, `secret-scanning`, `self-improvement`, `seo-audit`, `siderolabs`, `sox-testing`, `sprint-planning`, `stakeholder-update`, `standup`, `start`, `stripe-best-practices`, `stripe-docs`, `svelte-core-bestpractices`, `synthesize-research`, `system-design`, `task-management`, `tech-debt`, `test-driven-development`, `testing-strategy`, `update`, `upgrade-stripe`, `user-research`, `ux-copy`, `variance-analysis`, `ways-of-working`, `web-design-guidelines`, `workspace-conventions`, `write-contributing`, `write-docs-introduction`, `write-readme`, `write-spec` (skills) · `agent-improver`, `agentic-engineer`, `flux-troubleshooter`, `portfolio-surveyor` (agents) · `aws-knowledge`, `cloudflare-docs`, `context7`, `flux-operator-mcp`, `github`, `github-ghas-tools`, `kubescape`, `siderolabs-docs`, `stripe`, `svelte`, `terraform-registry` (MCP servers) | The autonomous engineering system for a whole repository portfolio, and everything its agents need — engineer, read-only surveyor, meta-engineer and Flux troubleshooter agents; operating, product, spend and improvement workflows; GitHub, CodeRabbit review, security, library documentation, Go, Kubernetes, Flux, Talos, SvelteKit frontend, Stripe payments, engineering-practice, design, product-management, marketing, finance and productivity skills; and the MCP servers they use. Configured by the consumer's `AGENTS.md` | +| [`agentic-engineering`](../plugins/agentic-engineering/) | `accessibility-review`, `agent-improvement`, `agent-instructions`, `architecture`, `audit-support`, `autofix`, `brand-review`, `campaign-plan`, `check-skills`, `close-management`, `code-review`, `codeql`, `competitive-brief`, `connector-setup`, `content-creation`, `context7-mcp`, `conventional-release`, `debug`, `dependabot`, `dependency-scanning`, `deploy-checklist`, `design-critique`, `design-handoff`, `design-system`, `documentation`, `draft-content`, `email-sequence`, `financial-statements`, `find-skills`, `frontend-design`, `gh-cli`, `gh-stack`, `git-commit`, `github-actions-docs`, `github-actions-efficiency`, `github-actions-hardening`, `github-issues`, `gitops-cluster-debug`, `gitops-knowledge`, `gitops-tenant-onboarding`, `golang-pro`, `grilling`, `incident-response`, `journal-entry`, `journal-entry-prep`, `memory-management`, `metrics-review`, `performance-report`, `portfolio-maintenance`, `product-brainstorming`, `product-documentation`, `product-engineering`, `prototype`, `reconciliation`, `refactor`, `research-synthesis`, `roadmap-update`, `secret-scanning`, `self-improvement`, `seo-audit`, `siderolabs`, `sox-testing`, `sprint-planning`, `stakeholder-update`, `standup`, `start`, `stripe-best-practices`, `stripe-docs`, `svelte-core-bestpractices`, `synthesize-research`, `system-design`, `task-management`, `tech-debt`, `test-driven-development`, `testing-strategy`, `update`, `upgrade-stripe`, `user-research`, `ux-copy`, `variance-analysis`, `ways-of-working`, `web-design-guidelines`, `workspace-conventions`, `write-contributing`, `write-docs-introduction`, `write-readme`, `write-spec` (skills) · `agent-improver`, `agentic-engineer`, `flux-troubleshooter`, `portfolio-surveyor` (agents) · `aws-knowledge`, `cloudflare-docs`, `context7`, `coroot`, `flux-operator-mcp`, `github`, `github-ghas-tools`, `kubescape`, `siderolabs-docs`, `stripe`, `svelte`, `terraform-registry` (MCP servers) | The autonomous engineering system for a whole repository portfolio, and everything its agents need — engineer, read-only surveyor, meta-engineer and Flux troubleshooter agents; operating, product, spend and improvement workflows; GitHub, CodeRabbit review, security, library documentation, Go, Kubernetes, Flux, Talos, SvelteKit frontend, Stripe payments, engineering-practice, design, product-management, marketing, finance and productivity skills; and the MCP servers they use. Configured by the consumer's `AGENTS.md` | | [`vibe-coding`](../plugins/vibe-coding/) | `needs-stack-mapping`, `allowed-stack-guardrail`, `jargon-free-voice` (skills) · `vibe-coding-companion` (agent) | Build a product by conversation alone — plain-language companion agent + guardrailed needs-to-stack skills for people with no technical background | | [`ksail`](../plugins/ksail/) | `ksail` (skill) · `ksail` (MCP server) | Create and operate Kubernetes clusters and GitOps workloads with the KSail CLI — needs `ksail` on `PATH`; PolyForm Shield licensed | diff --git a/docs/resources.md b/docs/resources.md index 8479ab3..3849964 100644 --- a/docs/resources.md +++ b/docs/resources.md @@ -33,7 +33,7 @@ armed, or with a schedule pointing at an entrypoint that no longer resolves. The ## MCP servers A plugin may bundle [MCP](https://modelcontextprotocol.io) servers as well as skills. The -[`agentic-engineering`](../plugins/agentic-engineering/) plugin bundles eleven, authored once in its +[`agentic-engineering`](../plugins/agentic-engineering/) plugin bundles twelve, authored once in its [`.mcp.json`](../plugins/agentic-engineering/.mcp.json) (`mcpServers` map), and the [`ksail`](../plugins/ksail/) plugin bundles the KSail server, which needs `ksail` on `PATH`: @@ -42,6 +42,7 @@ A plugin may bundle [MCP](https://modelcontextprotocol.io) servers as well as sk | `aws-knowledge` | AWS's hosted knowledge server: AWS documentation and regional availability, read-only | Nothing; it works without an AWS account | | `cloudflare-docs` | Cloudflare's hosted documentation search, read-only | Nothing; it is public | | `context7` | Upstash's Context7: current documentation and code examples for libraries and frameworks, read-only | Nothing; it works without an account | +| `coroot` | Your own Coroot instance's `/mcp` endpoint: topology, alerts, incidents, traces, metrics and logs, read-only with a Viewer key | `COROOT_URL` and a service-account API key; see the plugin README | | `flux-operator-mcp` | A live cluster's Flux resources, read-only (`serve --read-only`), for the `gitops-cluster-debug` skill and the `flux-troubleshooter` agent | The `flux-operator-mcp` binary, a POSIX `sh`, and cluster access through `KUBECONFIG` (default `~/.kube/config`) | | `github` | GitHub's everyday tools: repositories, issues, pull requests, Actions, labels and projects, read, and issue, label, comment, branch, new pull request and project writes; merging, pull request edits, reviews, repository creation and deletion, file writes and workflow runs are excluded | A GitHub sign-in; see below | | `github-ghas-tools` | GitHub's security tools, read-only (`X-MCP-Readonly`): alerts, advisories and dependency vulnerability lookups | A GitHub sign-in; see below | @@ -51,12 +52,12 @@ A plugin may bundle [MCP](https://modelcontextprotocol.io) servers as well as sk | `svelte` | The Svelte team's hosted server at `https://mcp.svelte.dev/mcp`: Svelte 5 and SvelteKit documentation, the `svelte-autofixer` and playground links, all read-only | Nothing; it is public | | `terraform-registry` | HashiCorp's Terraform server, `registry` toolset only: public provider and module documentation, read-only | The `terraform-mcp-server` binary | -`flux-operator-mcp` talks only to your clusters. `kubescape` also downloads Kubescape's released -policy bundle from GitHub each time it starts, and `terraform-registry` runs locally but queries the -public Terraform Registry. Every other server is hosted by a third party, so every call to one, -including a documentation search, reaches that service. If your agents must not contact a third -party, disable -its server in your tool's MCP settings; denying single tools does not stop the connection. Among +`flux-operator-mcp` talks only to your clusters, and `coroot` only to the Coroot instance you point +it at. `kubescape` also downloads Kubescape's released policy bundle from GitHub each time it +starts, and `terraform-registry` runs locally but queries the public Terraform Registry. Every +other server is hosted by a third party, so every call to one, including a documentation search, +reaches that service. If your agents must not contact a third party, disable its server in your +tool's MCP settings; denying single tools does not stop the connection. Among the servers you keep, `github` and `stripe` can change things: `github` writes to GitHub as the signed-in account, and `stripe` writes to your Stripe account and can move money. The `siderolabs-docs` `submit_feedback` and `stripe` `send_stripe_feedback` tools send reports to Sidero diff --git a/plugins/agentic-engineering/.claude-plugin/plugin.json b/plugins/agentic-engineering/.claude-plugin/plugin.json index 2ea50f1..8b9e663 100644 --- a/plugins/agentic-engineering/.claude-plugin/plugin.json +++ b/plugins/agentic-engineering/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "agentic-engineering", "description": "The autonomous engineering system for repository portfolios and everything its agents need — engineer, read-only surveyor, meta-engineer and Flux troubleshooter agents; portfolio, product, spend and improvement workflows; GitHub, CodeRabbit review, security, library documentation, Go, Kubernetes, Flux, Talos, SvelteKit frontend, Stripe payments, engineering-practice, design, product-management, marketing, finance and productivity skills; and the MCP servers they use; configured by the consumer AGENTS.md", - "version": "7.1.1", + "version": "7.2.0", "author": { "name": "devantler-tech", "url": "https://github.com/devantler-tech" diff --git a/plugins/agentic-engineering/.codex-plugin/plugin.json b/plugins/agentic-engineering/.codex-plugin/plugin.json index 53bbbb0..01d20d8 100644 --- a/plugins/agentic-engineering/.codex-plugin/plugin.json +++ b/plugins/agentic-engineering/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "agentic-engineering", "description": "The autonomous engineering system for repository portfolios and everything its agents need — engineer, read-only surveyor, meta-engineer and Flux troubleshooter agents; portfolio, product, spend and improvement workflows; GitHub, CodeRabbit review, security, library documentation, Go, Kubernetes, Flux, Talos, SvelteKit frontend, Stripe payments, engineering-practice, design, product-management, marketing, finance and productivity skills; and the MCP servers they use; configured by the consumer AGENTS.md", - "version": "7.1.1", + "version": "7.2.0", "author": { "name": "devantler-tech", "url": "https://github.com/devantler-tech" diff --git a/plugins/agentic-engineering/.mcp.json b/plugins/agentic-engineering/.mcp.json index 73498ae..d84834d 100644 --- a/plugins/agentic-engineering/.mcp.json +++ b/plugins/agentic-engineering/.mcp.json @@ -12,6 +12,11 @@ "type": "http", "url": "https://mcp.context7.com/mcp" }, + "coroot": { + "type": "http", + "url": "${COROOT_URL}/mcp", + "headersHelper": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/coroot-mcp-headers.sh\" \"${CLAUDE_PLUGIN_DATA}\"" + }, "flux-operator-mcp": { "command": "sh", "args": [ diff --git a/plugins/agentic-engineering/CHANGELOG.md b/plugins/agentic-engineering/CHANGELOG.md index 090b708..95bf9ff 100644 --- a/plugins/agentic-engineering/CHANGELOG.md +++ b/plugins/agentic-engineering/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog — `agentic-engineering` +## 7.2.0 — 2026-11-05 + +**Added** — `coroot`, the MCP endpoint at `/mcp` that Coroot v1.28 and later serve themselves: +application topology, alerts, SLO incidents, traces, PromQL metrics and logs. Coroot checks every +tool call against the caller's role, so with a service account in the Viewer role the agents can +investigate but cannot resolve alerts. The server reads its address from `COROOT_URL` in Claude +Code's environment, and the new `scripts/coroot-mcp-headers.sh` sends the service account's API +key from `coroot-api-key` in the plugin data directory, so neither goes into any repository. + +### Upgrading to 7.2.0 + +Nothing is required. Until `COROOT_URL` and the key file are set, `coroot` shows a +missing-variable warning or a failed connection and the other servers keep working. To use it, set +them up as the README's *Bundled MCP servers* section describes. Copilot CLI, VS Code and Codex +need their own MCP settings for Coroot. + ## 7.1.1 — 2026-11-04 **Fixed** — the README now says why `github` excludes `delete_repository` although a plain probe diff --git a/plugins/agentic-engineering/CONNECTORS.md b/plugins/agentic-engineering/CONNECTORS.md index 33ba25f..ddacf49 100644 --- a/plugins/agentic-engineering/CONNECTORS.md +++ b/plugins/agentic-engineering/CONNECTORS.md @@ -11,7 +11,7 @@ says what each placeholder maps to here. | `~~CI/CD` | GitHub Actions, read through the `github` server's `actions` toolset or `gh` | Another CI system's own connector | | `~~project tracker` | GitHub Issues and Projects, through the `github` server or `gh` | Another tracker's own connector, if the user has one | | `~~knowledge base` | The repository's own documentation and `AGENTS.md`; `context7` for library documentation | Another knowledge base's own connector | -| `~~monitoring` | Live cluster state through `flux-operator-mcp` and `kubescape` | Metrics, logs and traces need the user's own connector | +| `~~monitoring` | Live cluster state through `flux-operator-mcp` and `kubescape`; metrics, logs, traces and alerts through `coroot` once it is set up | Another monitoring tool's own connector | | `~~design tool`, `~~design` | Nothing bundled | A design tool's own connector | | `~~chat` | Nothing bundled | A chat tool's own connector | | `~~incident management` | Nothing bundled | An incident tool's own connector | diff --git a/plugins/agentic-engineering/README.md b/plugins/agentic-engineering/README.md index ff6a46d..2e5c260 100644 --- a/plugins/agentic-engineering/README.md +++ b/plugins/agentic-engineering/README.md @@ -180,6 +180,7 @@ recorded in [`resources/THIRD-PARTY-NOTICES.md`](resources/THIRD-PARTY-NOTICES.m | `aws-knowledge` | AWS's hosted knowledge server: AWS documentation, regional availability and AWS's own guidance, read-only | Nothing; it works without an AWS account | | `cloudflare-docs` | Cloudflare's hosted documentation search: DNS, certificates, R2 and the rest of Cloudflare, read-only | Nothing; it is public | | `context7` | Upstash's Context7: current documentation and code examples for libraries and frameworks, read-only | Nothing; it works without an account | +| `coroot` | Your own Coroot instance's MCP endpoint: application topology, alerts, incidents, traces, metrics and logs, read-only with a Viewer key | `COROOT_URL` in your environment and a service-account API key, below | | `flux-operator-mcp` | A live cluster's Flux resources, read-only, for `gitops-cluster-debug` and `flux-troubleshooter` | The [`flux-operator-mcp`](https://fluxcd.control-plane.io/operator/mcp/) binary, a POSIX `sh`, and cluster access through `KUBECONFIG` or `~/.kube/config` | | `github` | GitHub's everyday tools: repositories, issues, pull requests, Actions, labels and projects, read, and issue, label, comment, branch, new pull request and project writes; merging, pull request edits, reviews, repository creation and deletion, file writes and workflow runs are excluded | A GitHub sign-in, below | | `github-ghas-tools` | GitHub's read-only security tools | A GitHub sign-in, below | @@ -192,11 +193,32 @@ recorded in [`resources/THIRD-PARTY-NOTICES.md`](resources/THIRD-PARTY-NOTICES.m `flux-operator-mcp` talks only to your clusters. `kubescape` also downloads Kubescape's released policy bundle from GitHub each time it starts, falling back to a local cache, and `terraform-registry` runs locally but looks providers and modules up in the public Terraform -Registry. Every other server is +Registry, and `coroot` reaches only the Coroot instance you point it at. Every other server is hosted by a third party, so every call to one, including a documentation search, reaches that service. If your agents must not contact a third party, disable its server in your tool's MCP settings; denying single tools does not stop the connection. +`coroot` connects to the MCP endpoint at `/mcp` that Coroot (v1.28 or later) serves itself. That +endpoint needs a bearer token on every request and checks each tool call against the caller's +role, so give the agents a Coroot **service account with the Viewer role**: it can investigate +but cannot resolve alerts, the one tool that changes anything. To use it in Claude Code: + +1. Create the service account and its API key, in Coroot's UI or, with the Kubernetes operator, + through the `Coroot` resource's `spec.serviceAccounts`. +2. Make sure an agent can reach `https:///mcp` without your browser sign-in; the + endpoint's own key check is what protects it. +3. Set `COROOT_URL` to your Coroot's address, without `/mcp`, in the environment Claude Code runs + in, for example under `env` in your user `settings.json`. The address stays out of any + repository. +4. Write the API key to `coroot-api-key` in this plugin's data directory, the same directory as the + optional `github-token` below, readable only by you. The plugin's + [`scripts/coroot-mcp-headers.sh`](scripts/coroot-mcp-headers.sh) sends it as the bearer token and + never falls back to anything else. + +Until both are set, Claude Code shows a missing-variable warning or a failed connection for +`coroot` and the rest keep working. Copilot CLI, VS Code and Codex neither expand `COROOT_URL` in a +plugin's server nor run its `headersHelper`; configure Coroot for them with their own MCP settings. + `kubescape` runs `kubescape mcpserver` from the Kubescape CLI, through the same `sh` wrapper as the Flux server, so it reads every cluster in your kubeconfig the same way. Its tools read the cluster and the results the in-cluster Kubescape operator stores. Its only write-shaped calls are diff --git a/plugins/agentic-engineering/plugin.json b/plugins/agentic-engineering/plugin.json index 2ea50f1..8b9e663 100644 --- a/plugins/agentic-engineering/plugin.json +++ b/plugins/agentic-engineering/plugin.json @@ -1,7 +1,7 @@ { "name": "agentic-engineering", "description": "The autonomous engineering system for repository portfolios and everything its agents need — engineer, read-only surveyor, meta-engineer and Flux troubleshooter agents; portfolio, product, spend and improvement workflows; GitHub, CodeRabbit review, security, library documentation, Go, Kubernetes, Flux, Talos, SvelteKit frontend, Stripe payments, engineering-practice, design, product-management, marketing, finance and productivity skills; and the MCP servers they use; configured by the consumer AGENTS.md", - "version": "7.1.1", + "version": "7.2.0", "author": { "name": "devantler-tech", "url": "https://github.com/devantler-tech" diff --git a/plugins/agentic-engineering/scripts/coroot-mcp-headers.sh b/plugins/agentic-engineering/scripts/coroot-mcp-headers.sh new file mode 100755 index 0000000..77b9a86 --- /dev/null +++ b/plugins/agentic-engineering/scripts/coroot-mcp-headers.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Print the Authorization header Claude Code sends to the bundled Coroot MCP server. +# +# Claude Code runs this as the `headersHelper` of `coroot` in ../.mcp.json on every connection +# and merges its output into the request headers. Coroot's /mcp endpoint needs a bearer token on +# every request: for an agent that cannot complete Coroot's browser sign-in, that is the API key +# of a Coroot service account, ideally one with the Viewer role, which can investigate but cannot +# resolve alerts. +# +# The key is the contents of `coroot-api-key` in the plugin data directory, passed as the first +# argument. Claude Code runs a plugin's helper without any environment variable whose name looks +# like a credential, and this helper has no fallback: with no usable key file it prints nothing +# and exits 1, so Claude Code reports a failed helper instead of sending an empty credential. It +# prints one JSON object and nothing else, and never writes the key to stderr. +# +# Usage: coroot-mcp-headers.sh +# Exit codes: 0 header printed, 1 no usable key, 2 usage error. +set -euo pipefail + +if [ $# -ne 1 ]; then + echo 'usage: coroot-mcp-headers.sh ' >&2 + exit 2 +fi +if [ -z "$1" ]; then + echo 'coroot-mcp-headers: no plugin data directory was given' >&2 + exit 1 +fi +if ! { [ -d "$1" ] && [ -x "$1" ]; }; then + echo 'coroot-mcp-headers: cannot search the plugin data directory' >&2 + exit 1 +fi +key_file="$1/coroot-api-key" +if ! [ -f "$key_file" ] || ! [ -r "$key_file" ] || ! key=$(< "$key_file"); then + echo 'coroot-mcp-headers: no readable coroot-api-key in the plugin data directory' >&2 + exit 1 +fi +# One line of bearer-token characters (RFC 6750 b64token), so it can neither break the JSON nor +# smuggle a second header. +if ! [[ $key =~ ^[A-Za-z0-9._~+/-]+=*$ ]]; then + echo 'coroot-mcp-headers: coroot-api-key is not a single API key' >&2 + exit 1 +fi +printf '{"Authorization":"Bearer %s"}\n' "$key" diff --git a/plugins/agentic-engineering/scripts/coroot-mcp-headers.test.sh b/plugins/agentic-engineering/scripts/coroot-mcp-headers.test.sh new file mode 100755 index 0000000..cee090c --- /dev/null +++ b/plugins/agentic-engineering/scripts/coroot-mcp-headers.test.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Hermetic self-test for coroot-mcp-headers.sh: the helper prints exactly one Authorization header +# from the API key in the plugin data directory, and refuses every other state without printing a +# header or the key. It has no fallback: a Coroot service-account key comes only from that file. +set -euo pipefail +here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +helper=$here/coroot-mcp-headers.sh +work=$(mktemp -d) +trap 'chmod -R u+rwx "$work" 2>/dev/null; rm -rf "$work"' EXIT +fail() { echo "FAIL: $*" >&2; exit 1; } + +run() { # expected-status data-dir [extra-arg] + local want=$1 status=0; shift + bash "$helper" "$@" > "$work/out" 2> "$work/err" || status=$? + [ "$status" -eq "$want" ] || fail "exit $status, want $want for: $* ($(cat "$work/err"))" + if grep -q 'k3y' "$work/err"; then fail "printed the key on stderr for: $*"; fi +} +expect_header() { # data-dir key + run 0 "$1" + jq -se --arg k "$2" 'length == 1 and .[0] == {Authorization: ("Bearer " + $k)}' "$work/out" > /dev/null || + fail "unexpected output: $(cat "$work/out")" +} +refused() { # description data-dir + run 1 "$2" + [ ! -s "$work/out" ] || fail "printed output for $1" +} + +mkdir -p "$work/data" +printf 'Abc_k3y-0123456789.XYZ~\n' > "$work/data/coroot-api-key" +chmod 600 "$work/data/coroot-api-key" +expect_header "$work/data" 'Abc_k3y-0123456789.XYZ~' + +# A key in the environment is never used, whatever it is called. +COROOT_API_KEY=env_k3y expect_header "$work/data" 'Abc_k3y-0123456789.XYZ~' + +refused 'no data directory argument' '' +mkdir -p "$work/empty" +refused 'a missing key file' "$work/empty" +printf '' > "$work/empty/coroot-api-key" +refused 'an empty key file' "$work/empty" +printf 'two words k3y\n' > "$work/empty/coroot-api-key" +refused 'a key with spaces' "$work/empty" +printf 'k3y"}{\n' > "$work/empty/coroot-api-key" +refused 'a key that would break the JSON' "$work/empty" +printf 'k3y\nsecond-line\n' > "$work/empty/coroot-api-key" +refused 'a key file with a second line' "$work/empty" +rm "$work/empty/coroot-api-key" +ln -s "$work/nowhere" "$work/empty/coroot-api-key" +refused 'a dangling key link' "$work/empty" +rm "$work/empty/coroot-api-key" +mkdir "$work/empty/coroot-api-key" +refused 'a key path that is a directory' "$work/empty" +printf 'not a directory\n' > "$work/file" +refused 'a data path that is a file' "$work/file" +if [ "$(id -u)" -ne 0 ]; then + mkdir -p "$work/locked" + printf 'k3y\n' > "$work/locked/coroot-api-key" + chmod 000 "$work/locked/coroot-api-key" + refused 'an unreadable key file' "$work/locked" + chmod 600 "$work/locked/coroot-api-key" + chmod 600 "$work/locked" + refused 'an unsearchable data directory' "$work/locked" + chmod 700 "$work/locked" +fi + +run 2 "$work/data" extra +[ ! -s "$work/out" ] || fail 'printed output for a usage error' + +printf 'coroot-mcp-headers: PASS\n' From 34e39fa91ce8dc64b15ff97d45488adf6751a488 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Sat, 7 Nov 2026 12:04:18 +0100 Subject: [PATCH 2/3] fix(agentic-engineering): send the Coroot key only to the pinned address The header helper now sends the API key only when Claude Code reports a server URL equal to the address pinned in coroot-url in the plugin data directory followed by /mcp, over https:// or loopback http://, so a changed COROOT_URL cannot redirect the key. Key and address files must hold exactly one line, which also refuses NUL bytes and extra newlines. The docs give v1.26.8 as the first Coroot with service-account keys, say that an Enterprise Viewer's investigate_anomaly can save an RCA onto an incident, ask to expose only the exact /mcp path, hedge the other tools' behaviour, and note that Kubescape's apply_remediation is a dry run. Co-Authored-By: Claude Opus 5.5 --- docs/resources.md | 6 +- plugins/agentic-engineering/CHANGELOG.md | 20 +++-- plugins/agentic-engineering/README.md | 48 ++++++----- .../scripts/coroot-mcp-headers.sh | 55 +++++++++--- .../scripts/coroot-mcp-headers.test.sh | 85 +++++++++++++------ 5 files changed, 147 insertions(+), 67 deletions(-) diff --git a/docs/resources.md b/docs/resources.md index 3849964..5cb25dd 100644 --- a/docs/resources.md +++ b/docs/resources.md @@ -42,7 +42,7 @@ A plugin may bundle [MCP](https://modelcontextprotocol.io) servers as well as sk | `aws-knowledge` | AWS's hosted knowledge server: AWS documentation and regional availability, read-only | Nothing; it works without an AWS account | | `cloudflare-docs` | Cloudflare's hosted documentation search, read-only | Nothing; it is public | | `context7` | Upstash's Context7: current documentation and code examples for libraries and frameworks, read-only | Nothing; it works without an account | -| `coroot` | Your own Coroot instance's `/mcp` endpoint: topology, alerts, incidents, traces, metrics and logs, read-only with a Viewer key | `COROOT_URL` and a service-account API key; see the plugin README | +| `coroot` | Your own Coroot instance's `/mcp` endpoint: topology, alerts, incidents, traces, metrics and logs; a Viewer key cannot resolve alerts, and in the Enterprise Edition can save an investigation onto an incident | `COROOT_URL`, a pinned address and a service-account API key; see the plugin README | | `flux-operator-mcp` | A live cluster's Flux resources, read-only (`serve --read-only`), for the `gitops-cluster-debug` skill and the `flux-troubleshooter` agent | The `flux-operator-mcp` binary, a POSIX `sh`, and cluster access through `KUBECONFIG` (default `~/.kube/config`) | | `github` | GitHub's everyday tools: repositories, issues, pull requests, Actions, labels and projects, read, and issue, label, comment, branch, new pull request and project writes; merging, pull request edits, reviews, repository creation and deletion, file writes and workflow runs are excluded | A GitHub sign-in; see below | | `github-ghas-tools` | GitHub's security tools, read-only (`X-MCP-Readonly`): alerts, advisories and dependency vulnerability lookups | A GitHub sign-in; see below | @@ -59,7 +59,9 @@ other server is hosted by a third party, so every call to one, including a docum reaches that service. If your agents must not contact a third party, disable its server in your tool's MCP settings; denying single tools does not stop the connection. Among the servers you keep, `github` and `stripe` can change things: `github` writes to GitHub as the -signed-in account, and `stripe` writes to your Stripe account and can move money. The +signed-in account, and `stripe` writes to your Stripe account and can move money. With a Viewer +key, `coroot`'s Enterprise Edition `investigate_anomaly` can save its analysis onto an incident. +The `siderolabs-docs` `submit_feedback` and `stripe` `send_stripe_feedback` tools send reports to Sidero Labs and Stripe. The Svelte autofixer and playground tools send the code they are given to Svelte's service, and the playground link carries that code. The diff --git a/plugins/agentic-engineering/CHANGELOG.md b/plugins/agentic-engineering/CHANGELOG.md index 95bf9ff..62c1db5 100644 --- a/plugins/agentic-engineering/CHANGELOG.md +++ b/plugins/agentic-engineering/CHANGELOG.md @@ -2,19 +2,23 @@ ## 7.2.0 — 2026-11-05 -**Added** — `coroot`, the MCP endpoint at `/mcp` that Coroot v1.28 and later serve themselves: -application topology, alerts, SLO incidents, traces, PromQL metrics and logs. Coroot checks every -tool call against the caller's role, so with a service account in the Viewer role the agents can -investigate but cannot resolve alerts. The server reads its address from `COROOT_URL` in Claude -Code's environment, and the new `scripts/coroot-mcp-headers.sh` sends the service account's API -key from `coroot-api-key` in the plugin data directory, so neither goes into any repository. +**Added** — `coroot`, the MCP endpoint at `/mcp` that Coroot serves itself, with +service-account API keys from v1.26.8: application topology, alerts, SLO incidents, traces, +PromQL metrics and logs. Coroot checks every tool call against the caller's role, so with a +service account in the Viewer role the agents can investigate but cannot resolve alerts. In the +Enterprise Edition a Viewer's `investigate_anomaly` can still save its analysis onto an incident +when given an `incident_key`. The server reads its address from `COROOT_URL` in Claude Code's +environment. The new `scripts/coroot-mcp-headers.sh` sends the API key from `coroot-api-key` in +the plugin data directory only when Claude Code connects to the address pinned in `coroot-url` +there, followed by `/mcp`, over `https://` or loopback `http://`; so neither the address nor the +key goes into any repository, and a changed `COROOT_URL` cannot redirect the key. ### Upgrading to 7.2.0 -Nothing is required. Until `COROOT_URL` and the key file are set, `coroot` shows a +Nothing is required. Until `COROOT_URL` and the two files are set, `coroot` shows a missing-variable warning or a failed connection and the other servers keep working. To use it, set them up as the README's *Bundled MCP servers* section describes. Copilot CLI, VS Code and Codex -need their own MCP settings for Coroot. +are expected to need their own MCP settings for Coroot. ## 7.1.1 — 2026-11-04 diff --git a/plugins/agentic-engineering/README.md b/plugins/agentic-engineering/README.md index 2e5c260..7ae34ff 100644 --- a/plugins/agentic-engineering/README.md +++ b/plugins/agentic-engineering/README.md @@ -180,7 +180,7 @@ recorded in [`resources/THIRD-PARTY-NOTICES.md`](resources/THIRD-PARTY-NOTICES.m | `aws-knowledge` | AWS's hosted knowledge server: AWS documentation, regional availability and AWS's own guidance, read-only | Nothing; it works without an AWS account | | `cloudflare-docs` | Cloudflare's hosted documentation search: DNS, certificates, R2 and the rest of Cloudflare, read-only | Nothing; it is public | | `context7` | Upstash's Context7: current documentation and code examples for libraries and frameworks, read-only | Nothing; it works without an account | -| `coroot` | Your own Coroot instance's MCP endpoint: application topology, alerts, incidents, traces, metrics and logs, read-only with a Viewer key | `COROOT_URL` in your environment and a service-account API key, below | +| `coroot` | Your own Coroot instance's MCP endpoint: application topology, alerts, incidents, traces, metrics and logs; a Viewer key cannot resolve alerts, and in the Enterprise Edition its one write saves an investigation onto an incident | `COROOT_URL` in your environment, plus the pinned address and a service-account API key in the plugin data directory, below | | `flux-operator-mcp` | A live cluster's Flux resources, read-only, for `gitops-cluster-debug` and `flux-troubleshooter` | The [`flux-operator-mcp`](https://fluxcd.control-plane.io/operator/mcp/) binary, a POSIX `sh`, and cluster access through `KUBECONFIG` or `~/.kube/config` | | `github` | GitHub's everyday tools: repositories, issues, pull requests, Actions, labels and projects, read, and issue, label, comment, branch, new pull request and project writes; merging, pull request edits, reviews, repository creation and deletion, file writes and workflow runs are excluded | A GitHub sign-in, below | | `github-ghas-tools` | GitHub's read-only security tools | A GitHub sign-in, below | @@ -198,31 +198,41 @@ hosted by a third party, so every call to one, including a documentation search, service. If your agents must not contact a third party, disable its server in your tool's MCP settings; denying single tools does not stop the connection. -`coroot` connects to the MCP endpoint at `/mcp` that Coroot (v1.28 or later) serves itself. That -endpoint needs a bearer token on every request and checks each tool call against the caller's -role, so give the agents a Coroot **service account with the Viewer role**: it can investigate -but cannot resolve alerts, the one tool that changes anything. To use it in Claude Code: +`coroot` connects to the MCP endpoint at `/mcp` that Coroot serves itself; it takes +service-account API keys from v1.26.8. That endpoint needs a bearer token on every request and +checks each tool call against the caller's role, so give the agents a Coroot **service account +with the Viewer role**. A Viewer can investigate but cannot resolve alerts, which in the Community +Edition is the only tool that changes anything. In the Enterprise Edition, `investigate_anomaly` +called with an `incident_key` also saves its root-cause analysis onto that incident, and a Viewer +may do that; deny that tool in your tool's settings if your agents must not write anything. To +use it in Claude Code: 1. Create the service account and its API key, in Coroot's UI or, with the Kubernetes operator, through the `Coroot` resource's `spec.serviceAccounts`. -2. Make sure an agent can reach `https:///mcp` without your browser sign-in; the - endpoint's own key check is what protects it. -3. Set `COROOT_URL` to your Coroot's address, without `/mcp`, in the environment Claude Code runs - in, for example under `env` in your user `settings.json`. The address stays out of any - repository. -4. Write the API key to `coroot-api-key` in this plugin's data directory, the same directory as the - optional `github-token` below, readable only by you. The plugin's - [`scripts/coroot-mcp-headers.sh`](scripts/coroot-mcp-headers.sh) sends it as the bearer token and - never falls back to anything else. - -Until both are set, Claude Code shows a missing-variable warning or a failed connection for -`coroot` and the rest keep working. Copilot CLI, VS Code and Codex neither expand `COROOT_URL` in a -plugin's server nor run its `headersHelper`; configure Coroot for them with their own MCP settings. +2. Make sure an agent can reach exactly `https:///mcp` without your browser sign-in; + the endpoint's own key check is what protects it. If your Coroot gives anonymous visitors a + role, expose only that exact path, never the whole host. +3. Set `COROOT_URL` to your Coroot's address, without `/mcp` and without a trailing slash, in the + environment Claude Code runs in, for example under `env` in your user `settings.json`. The + address stays out of any repository. +4. In this plugin's data directory, the same directory as the optional `github-token` below, + write the same address to `coroot-url` and the API key to `coroot-api-key`, readable only by + you. The plugin's [`scripts/coroot-mcp-headers.sh`](scripts/coroot-mcp-headers.sh) sends the + key as the bearer token only when Claude Code is connecting to that pinned address followed by + `/mcp`, so a changed `COROOT_URL` cannot send it elsewhere. It accepts only an `https://` + address, or plain `http://` to `localhost`, `127.0.0.1` or `[::1]`, for example through a port + forward, and never falls back to anything else. + +Until all three are set, Claude Code shows a missing-variable warning or a failed connection for +`coroot` and the rest keep working. Copilot CLI, VS Code and Codex are not expected to run the +Claude-only `headersHelper`, which is not yet verified on each tool; configure Coroot for them with +their own MCP settings. `kubescape` runs `kubescape mcpserver` from the Kubescape CLI, through the same `sh` wrapper as the Flux server, so it reads every cluster in your kubeconfig the same way. Its tools read the cluster and the results the in-cluster Kubescape operator stores. Its only write-shaped calls are -server-side dry-run patches, which change nothing; its remediation tool never applies a fix, and +server-side dry-run patches, which change nothing; despite its name, `apply_remediation` never +applies a fix (Kubescape v4.0.17 sends every patch it makes as a server-side dry run), and its admission-policy tool only drafts YAML. Its stored results exist only on a cluster that runs the Kubescape operator. `terraform-registry` needs no token in the `registry` toolset; the plugin leaves HashiCorp's HCP Terraform and Terraform Enterprise diff --git a/plugins/agentic-engineering/scripts/coroot-mcp-headers.sh b/plugins/agentic-engineering/scripts/coroot-mcp-headers.sh index 77b9a86..ec62fbd 100755 --- a/plugins/agentic-engineering/scripts/coroot-mcp-headers.sh +++ b/plugins/agentic-engineering/scripts/coroot-mcp-headers.sh @@ -4,17 +4,23 @@ # Claude Code runs this as the `headersHelper` of `coroot` in ../.mcp.json on every connection # and merges its output into the request headers. Coroot's /mcp endpoint needs a bearer token on # every request: for an agent that cannot complete Coroot's browser sign-in, that is the API key -# of a Coroot service account, ideally one with the Viewer role, which can investigate but cannot -# resolve alerts. +# of a Coroot service account, ideally one with the Viewer role. # -# The key is the contents of `coroot-api-key` in the plugin data directory, passed as the first -# argument. Claude Code runs a plugin's helper without any environment variable whose name looks -# like a credential, and this helper has no fallback: with no usable key file it prints nothing -# and exits 1, so Claude Code reports a failed helper instead of sending an empty credential. It -# prints one JSON object and nothing else, and never writes the key to stderr. +# Two files in the plugin data directory, passed as the first argument, hold the setup: +# coroot-url the Coroot address the key may go to, without /mcp: https://, or plain +# http:// only to localhost, 127.0.0.1 or [::1] +# coroot-api-key the API key +# The server's URL comes from COROOT_URL, which anything that sets Claude Code's environment can +# change. So the helper sends the key only when Claude Code reports, in CLAUDE_CODE_MCP_SERVER_URL, +# that it is connecting to exactly the pinned address followed by /mcp. +# +# Claude Code runs a plugin's helper without any environment variable whose name looks like a +# credential, and this helper has no fallback: when anything is missing or does not match, it +# prints nothing and exits 1, so Claude Code reports a failed helper instead of sending an empty +# credential. It prints one JSON object and nothing else, and never writes the key to stderr. # # Usage: coroot-mcp-headers.sh -# Exit codes: 0 header printed, 1 no usable key, 2 usage error. +# Exit codes: 0 header printed, 1 refused, 2 usage error. set -euo pipefail if [ $# -ne 1 ]; then @@ -29,15 +35,38 @@ if ! { [ -d "$1" ] && [ -x "$1" ]; }; then echo 'coroot-mcp-headers: cannot search the plugin data directory' >&2 exit 1 fi -key_file="$1/coroot-api-key" -if ! [ -f "$key_file" ] || ! [ -r "$key_file" ] || ! key=$(< "$key_file"); then - echo 'coroot-mcp-headers: no readable coroot-api-key in the plugin data directory' >&2 + +# read_exact : print the file's one line when the whole file is that line, +# optionally followed by one newline, and the line matches the pattern. The byte count catches +# what command substitution drops silently: a NUL byte or extra trailing newlines. +read_exact() { + local file="$1" pattern="$2" value size + [ -f "$file" ] && [ -r "$file" ] && value=$(< "$file") || return 1 + [[ $value =~ $pattern ]] || return 1 + size=$(wc -c < "$file") || return 1 + size=$((size)) + if [ "$size" -ne "${#value}" ]; then + [ "$size" -eq $((${#value} + 1)) ] || return 1 + [ "$(tail -c 1 "$file" | od -An -tx1 | tr -d ' ')" = 0a ] || return 1 + fi + printf '%s' "$value" +} + +# An origin and an optional path, with no trailing slash, query, fragment or user info. +url_pattern='^(https://[A-Za-z0-9.-]+|http://(localhost|127\.0\.0\.1|\[::1\]))(:[0-9]{1,5})?(/[A-Za-z0-9._~%-]+)*$' +if ! pinned=$(read_exact "$1/coroot-url" "$url_pattern"); then + echo 'coroot-mcp-headers: no single https:// (or loopback http://) address in coroot-url' >&2 + exit 1 +fi +if [ "${CLAUDE_CODE_MCP_SERVER_URL:-}" != "$pinned/mcp" ]; then + echo 'coroot-mcp-headers: the server URL is not the address pinned in coroot-url followed by /mcp' >&2 exit 1 fi + # One line of bearer-token characters (RFC 6750 b64token), so it can neither break the JSON nor # smuggle a second header. -if ! [[ $key =~ ^[A-Za-z0-9._~+/-]+=*$ ]]; then - echo 'coroot-mcp-headers: coroot-api-key is not a single API key' >&2 +if ! key=$(read_exact "$1/coroot-api-key" '^[A-Za-z0-9._~+/-]+=*$'); then + echo 'coroot-mcp-headers: coroot-api-key is not a single readable API key' >&2 exit 1 fi printf '{"Authorization":"Bearer %s"}\n' "$key" diff --git a/plugins/agentic-engineering/scripts/coroot-mcp-headers.test.sh b/plugins/agentic-engineering/scripts/coroot-mcp-headers.test.sh index cee090c..3651f26 100755 --- a/plugins/agentic-engineering/scripts/coroot-mcp-headers.test.sh +++ b/plugins/agentic-engineering/scripts/coroot-mcp-headers.test.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash # Hermetic self-test for coroot-mcp-headers.sh: the helper prints exactly one Authorization header -# from the API key in the plugin data directory, and refuses every other state without printing a -# header or the key. It has no fallback: a Coroot service-account key comes only from that file. +# from the API key in the plugin data directory, only when Claude Code connects to the address +# pinned in coroot-url followed by /mcp, and refuses every other state without printing a header +# or the key. It has no fallback: a Coroot service-account key comes only from that file. set -euo pipefail here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) helper=$here/coroot-mcp-headers.sh @@ -9,6 +10,9 @@ work=$(mktemp -d) trap 'chmod -R u+rwx "$work" 2>/dev/null; rm -rf "$work"' EXIT fail() { echo "FAIL: $*" >&2; exit 1; } +pinned=https://coroot.example.test +export CLAUDE_CODE_MCP_SERVER_URL=$pinned/mcp + run() { # expected-status data-dir [extra-arg] local want=$1 status=0; shift bash "$helper" "$@" > "$work/out" 2> "$work/err" || status=$? @@ -24,37 +28,68 @@ refused() { # description data-dir run 1 "$2" [ ! -s "$work/out" ] || fail "printed output for $1" } +setup() { # dir url key: a data directory holding the two files, written verbatim + rm -rf "$1"; mkdir -p "$1" + printf '%b' "$2" > "$1/coroot-url" + printf '%b' "$3" > "$1/coroot-api-key" + chmod 600 "$1/coroot-api-key" +} -mkdir -p "$work/data" -printf 'Abc_k3y-0123456789.XYZ~\n' > "$work/data/coroot-api-key" -chmod 600 "$work/data/coroot-api-key" -expect_header "$work/data" 'Abc_k3y-0123456789.XYZ~' +key='Abc_k3y-0123456789.XYZ~' +setup "$work/data" "$pinned\n" "$key\n" +expect_header "$work/data" "$key" # A key in the environment is never used, whatever it is called. -COROOT_API_KEY=env_k3y expect_header "$work/data" 'Abc_k3y-0123456789.XYZ~' +COROOT_API_KEY=env_k3y expect_header "$work/data" "$key" + +# Both files may end without a newline, and the key file may be a link to a readable key. +setup "$work/bare" "$pinned" "$key" +expect_header "$work/bare" "$key" +printf '%s\n' "$key" > "$work/real-key" +rm "$work/bare/coroot-api-key"; ln -s "$work/real-key" "$work/bare/coroot-api-key" +expect_header "$work/bare" "$key" + +# Plain http only to the loopback interface, and an address may carry a path. +for url in http://localhost:8080 http://127.0.0.1:8080 'http://[::1]:8080' https://example.test/coroot; do + setup "$work/url" "$url\n" "$key\n" + CLAUDE_CODE_MCP_SERVER_URL=$url/mcp expect_header "$work/url" "$key" +done + +# The key goes nowhere but the pinned address followed by /mcp. +CLAUDE_CODE_MCP_SERVER_URL='' refused 'an empty server URL' "$work/data" +(unset CLAUDE_CODE_MCP_SERVER_URL; refused 'an unset server URL' "$work/data") +for url in https://attacker.example.test/mcp http://coroot.example.test/mcp "$pinned//mcp" \ + "$pinned/mcp/" "$pinned/mcp?x=1" "$pinned" https://coroot.example.test.attacker.test/mcp; do + CLAUDE_CODE_MCP_SERVER_URL=$url refused "the server URL $url" "$work/data" +done + +# A pinned address that is not a single https:// (or loopback http://) origin and path. +for url in '' 'http://coroot.example.test' 'http://localhost.attacker.test' "$pinned/" \ + 'https://user@coroot.example.test' "$pinned?x=1" "$pinned#f" 'ftp://coroot.example.test' \ + "$pinned\n$pinned" "$pinned\n\n" "$pinned\r\n" "https://coro\0ot.example.test" ' '"$pinned"; do + setup "$work/bad-url" "$url" "$key\n" + CLAUDE_CODE_MCP_SERVER_URL=$(printf '%b' "$url" | tr -d '\0')/mcp refused "the pinned address $url" "$work/bad-url" +done +setup "$work/no-url" "$pinned\n" "$key\n"; rm "$work/no-url/coroot-url" +refused 'a missing coroot-url' "$work/no-url" refused 'no data directory argument' '' -mkdir -p "$work/empty" -refused 'a missing key file' "$work/empty" -printf '' > "$work/empty/coroot-api-key" -refused 'an empty key file' "$work/empty" -printf 'two words k3y\n' > "$work/empty/coroot-api-key" -refused 'a key with spaces' "$work/empty" -printf 'k3y"}{\n' > "$work/empty/coroot-api-key" -refused 'a key that would break the JSON' "$work/empty" -printf 'k3y\nsecond-line\n' > "$work/empty/coroot-api-key" -refused 'a key file with a second line' "$work/empty" -rm "$work/empty/coroot-api-key" -ln -s "$work/nowhere" "$work/empty/coroot-api-key" -refused 'a dangling key link' "$work/empty" -rm "$work/empty/coroot-api-key" -mkdir "$work/empty/coroot-api-key" -refused 'a key path that is a directory' "$work/empty" +for bad in '' 'two words k3y\n' 'k3y"}{\n' 'k3y\nsecond-line\n' 'k3y\n\n' 'k3y\r\n' 'k3\0y\n' ' k3y\n'; do + setup "$work/bad-key" "$pinned\n" "$bad" + refused "the key file $bad" "$work/bad-key" +done +setup "$work/link" "$pinned\n" "$key\n" +rm "$work/link/coroot-api-key" +refused 'a missing key file' "$work/link" +ln -s "$work/nowhere" "$work/link/coroot-api-key" +refused 'a dangling key link' "$work/link" +rm "$work/link/coroot-api-key" +mkdir "$work/link/coroot-api-key" +refused 'a key path that is a directory' "$work/link" printf 'not a directory\n' > "$work/file" refused 'a data path that is a file' "$work/file" if [ "$(id -u)" -ne 0 ]; then - mkdir -p "$work/locked" - printf 'k3y\n' > "$work/locked/coroot-api-key" + setup "$work/locked" "$pinned\n" "$key\n" chmod 000 "$work/locked/coroot-api-key" refused 'an unreadable key file' "$work/locked" chmod 600 "$work/locked/coroot-api-key" From edfd9f2657cc72aac3295b99d5c9402ca802612a Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Sat, 7 Nov 2026 12:11:54 +0100 Subject: [PATCH 3/3] fix(agentic-engineering): state the Coroot pin's limits and test it under bash 3.2 The README, CHANGELOG and helper now say the pinned address stops a changed COROOT_URL but not someone who controls Claude Code's whole environment, and which https:// hosts the pin accepts. The self-test runs the helper with the bash that runs the test, so /bin/bash 3.2 is exercised too. Co-Authored-By: Claude Opus 5.5 --- docs/resources.md | 5 ++--- plugins/agentic-engineering/CHANGELOG.md | 7 ++++--- plugins/agentic-engineering/README.md | 10 ++++++---- .../agentic-engineering/scripts/coroot-mcp-headers.sh | 6 ++++-- .../scripts/coroot-mcp-headers.test.sh | 2 +- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/resources.md b/docs/resources.md index 5cb25dd..a822510 100644 --- a/docs/resources.md +++ b/docs/resources.md @@ -61,9 +61,8 @@ tool's MCP settings; denying single tools does not stop the connection. Among the servers you keep, `github` and `stripe` can change things: `github` writes to GitHub as the signed-in account, and `stripe` writes to your Stripe account and can move money. With a Viewer key, `coroot`'s Enterprise Edition `investigate_anomaly` can save its analysis onto an incident. -The -`siderolabs-docs` `submit_feedback` and `stripe` `send_stripe_feedback` tools send reports to Sidero -Labs and Stripe. The Svelte autofixer and playground tools send the code they are given to Svelte's +The `siderolabs-docs` `submit_feedback` and `stripe` `send_stripe_feedback` tools send reports to +Sidero Labs and Stripe. The Svelte autofixer and playground tools send the code they are given to Svelte's service, and the playground link carries that code. The [plugin README](../plugins/agentic-engineering/README.md#bundled-mcp-servers) covers each server's sign-in and what to deny. diff --git a/plugins/agentic-engineering/CHANGELOG.md b/plugins/agentic-engineering/CHANGELOG.md index 62c1db5..a270d35 100644 --- a/plugins/agentic-engineering/CHANGELOG.md +++ b/plugins/agentic-engineering/CHANGELOG.md @@ -2,8 +2,8 @@ ## 7.2.0 — 2026-11-05 -**Added** — `coroot`, the MCP endpoint at `/mcp` that Coroot serves itself, with -service-account API keys from v1.26.8: application topology, alerts, SLO incidents, traces, +**Added** — `coroot`, the MCP endpoint at `/mcp` that Coroot serves itself, which accepts +service-account API keys from Coroot v1.26.8: application topology, alerts, SLO incidents, traces, PromQL metrics and logs. Coroot checks every tool call against the caller's role, so with a service account in the Viewer role the agents can investigate but cannot resolve alerts. In the Enterprise Edition a Viewer's `investigate_anomaly` can still save its analysis onto an incident @@ -11,7 +11,8 @@ when given an `incident_key`. The server reads its address from `COROOT_URL` in environment. The new `scripts/coroot-mcp-headers.sh` sends the API key from `coroot-api-key` in the plugin data directory only when Claude Code connects to the address pinned in `coroot-url` there, followed by `/mcp`, over `https://` or loopback `http://`; so neither the address nor the -key goes into any repository, and a changed `COROOT_URL` cannot redirect the key. +key goes into any repository, and a changed `COROOT_URL` cannot redirect the key (someone who +controls Claude Code's whole environment still could, through its proxy settings). ### Upgrading to 7.2.0 diff --git a/plugins/agentic-engineering/README.md b/plugins/agentic-engineering/README.md index 7ae34ff..5a6d3ff 100644 --- a/plugins/agentic-engineering/README.md +++ b/plugins/agentic-engineering/README.md @@ -198,8 +198,8 @@ hosted by a third party, so every call to one, including a documentation search, service. If your agents must not contact a third party, disable its server in your tool's MCP settings; denying single tools does not stop the connection. -`coroot` connects to the MCP endpoint at `/mcp` that Coroot serves itself; it takes -service-account API keys from v1.26.8. That endpoint needs a bearer token on every request and +`coroot` connects to the MCP endpoint at `/mcp` that Coroot serves itself, which accepts +service-account API keys from Coroot v1.26.8. That endpoint needs a bearer token on every request and checks each tool call against the caller's role, so give the agents a Coroot **service account with the Viewer role**. A Viewer can investigate but cannot resolve alerts, which in the Community Edition is the only tool that changes anything. In the Enterprise Edition, `investigate_anomaly` @@ -209,7 +209,7 @@ use it in Claude Code: 1. Create the service account and its API key, in Coroot's UI or, with the Kubernetes operator, through the `Coroot` resource's `spec.serviceAccounts`. -2. Make sure an agent can reach exactly `https:///mcp` without your browser sign-in; +2. Make sure an agent can reach exactly `/mcp` without your browser sign-in; the endpoint's own key check is what protects it. If your Coroot gives anonymous visitors a role, expose only that exact path, never the whole host. 3. Set `COROOT_URL` to your Coroot's address, without `/mcp` and without a trailing slash, in the @@ -219,7 +219,9 @@ use it in Claude Code: write the same address to `coroot-url` and the API key to `coroot-api-key`, readable only by you. The plugin's [`scripts/coroot-mcp-headers.sh`](scripts/coroot-mcp-headers.sh) sends the key as the bearer token only when Claude Code is connecting to that pinned address followed by - `/mcp`, so a changed `COROOT_URL` cannot send it elsewhere. It accepts only an `https://` + `/mcp`, so a changed `COROOT_URL` cannot send it elsewhere; that does not protect against + someone who controls Claude Code's whole environment, such as its proxy and trusted + certificates. It accepts only an `https://` address whose host is an ASCII DNS name or IPv4 address, or plain `http://` to `localhost`, `127.0.0.1` or `[::1]`, for example through a port forward, and never falls back to anything else. diff --git a/plugins/agentic-engineering/scripts/coroot-mcp-headers.sh b/plugins/agentic-engineering/scripts/coroot-mcp-headers.sh index ec62fbd..7c96290 100755 --- a/plugins/agentic-engineering/scripts/coroot-mcp-headers.sh +++ b/plugins/agentic-engineering/scripts/coroot-mcp-headers.sh @@ -12,7 +12,9 @@ # coroot-api-key the API key # The server's URL comes from COROOT_URL, which anything that sets Claude Code's environment can # change. So the helper sends the key only when Claude Code reports, in CLAUDE_CODE_MCP_SERVER_URL, -# that it is connecting to exactly the pinned address followed by /mcp. +# that it is connecting to exactly the pinned address followed by /mcp. That stops a changed +# COROOT_URL; it cannot stop someone who controls the whole environment, for example a proxy +# variable plus a trusted CA. # # Claude Code runs a plugin's helper without any environment variable whose name looks like a # credential, and this helper has no fallback: when anything is missing or does not match, it @@ -47,7 +49,7 @@ read_exact() { size=$((size)) if [ "$size" -ne "${#value}" ]; then [ "$size" -eq $((${#value} + 1)) ] || return 1 - [ "$(tail -c 1 "$file" | od -An -tx1 | tr -d ' ')" = 0a ] || return 1 + [ "$(tail -c 1 -- "$file" | od -An -tx1 | tr -d ' ')" = 0a ] || return 1 fi printf '%s' "$value" } diff --git a/plugins/agentic-engineering/scripts/coroot-mcp-headers.test.sh b/plugins/agentic-engineering/scripts/coroot-mcp-headers.test.sh index 3651f26..6e98561 100755 --- a/plugins/agentic-engineering/scripts/coroot-mcp-headers.test.sh +++ b/plugins/agentic-engineering/scripts/coroot-mcp-headers.test.sh @@ -15,7 +15,7 @@ export CLAUDE_CODE_MCP_SERVER_URL=$pinned/mcp run() { # expected-status data-dir [extra-arg] local want=$1 status=0; shift - bash "$helper" "$@" > "$work/out" 2> "$work/err" || status=$? + "${BASH:-bash}" "$helper" "$@" > "$work/out" 2> "$work/err" || status=$? [ "$status" -eq "$want" ] || fail "exit $status, want $want for: $* ($(cat "$work/err"))" if grep -q 'k3y' "$work/err"; then fail "printed the key on stderr for: $*"; fi }