diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index bf40913..b2a20d6 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 bf40913..b2a20d6 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..a822510 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; 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 | @@ -51,16 +52,17 @@ 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 -Labs and Stripe. The Svelte autofixer and playground tools send the code they are given to Svelte's +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 [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/.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..a270d35 100644 --- a/plugins/agentic-engineering/CHANGELOG.md +++ b/plugins/agentic-engineering/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog — `agentic-engineering` +## 7.2.0 — 2026-11-05 + +**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 +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 (someone who +controls Claude Code's whole environment still could, through its proxy settings). + +### Upgrading to 7.2.0 + +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 +are expected to 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..5a6d3ff 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; 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 | @@ -192,15 +193,48 @@ 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 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` +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 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 + 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; 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. + +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/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..7c96290 --- /dev/null +++ b/plugins/agentic-engineering/scripts/coroot-mcp-headers.sh @@ -0,0 +1,74 @@ +#!/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. +# +# 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. 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 +# 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 refused, 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 + +# 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=$(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 new file mode 100755 index 0000000..6e98561 --- /dev/null +++ b/plugins/agentic-engineering/scripts/coroot-mcp-headers.test.sh @@ -0,0 +1,104 @@ +#!/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, 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 +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:-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" +} +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" +} + +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" "$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' '' +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 + 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" + 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'