diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 855bb56..974068e 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -6,7 +6,7 @@ }, "metadata": { "description": "JFrog Platform plugins for Cursor", - "version": "0.5.9", + "version": "0.5.10", "pluginRoot": "plugins" }, "plugins": [ diff --git a/.github/workflows/validate-inject-instructions.yml b/.github/workflows/validate-inject-instructions.yml deleted file mode 100644 index 0e2a041..0000000 --- a/.github/workflows/validate-inject-instructions.yml +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) JFrog Ltd. 2026 -# Licensed under the Apache License, Version 2.0 -# https://www.apache.org/licenses/LICENSE-2.0 - -name: Validate hook injection - -on: - pull_request: - branches: [main] - paths: - - "plugins/jfrog/scripts/inject-instructions.mjs" - - "plugins/jfrog/templates/jfrog-mcp-management.md" - - "plugins/jfrog/hooks/hooks.json" - - "plugins/jfrog/.cursor-plugin/plugin.json" - - "scripts/validate-hook-injector.mjs" - workflow_dispatch: - -permissions: - contents: read - -jobs: - validate: - name: Validate hook injection - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: "20" - - - name: Run injector validation - run: node scripts/validate-hook-injector.mjs diff --git a/README.md b/README.md index ed92ce3..4e7fe8b 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ The JFrog plugin provides the following capabilities, grouped by component: | **Skill** | JFrog Platform | Interact with Artifactory repositories, builds, permissions, users, access tokens, projects, release bundles, and platform administration via the JFrog CLI and REST/GraphQL APIs. Also covers security audits, CVE lookups, and Advanced Security exposure queries. | | **Skill** | Package safety & download | Check whether npm, Maven, PyPI, Go, and other packages are safe, curated, or allowed, then download them through Artifactory remote caches or curation-aware package managers. | | **Hook + Skill** | Agent Package Resolution (Preview) | Automatically route packages installed by the AI agent through your organization's JFrog Artifactory, keeping agent-driven installs inside your Curation, Xray, and governance perimeter. | -| **Hook** | Agent Guard | Cursor manage MCPs through the JFrog Agent Guard. Through the Agent Guard you can discover, install, configure, update, and remove MCP servers from the JFrog AI Catalog approved for your project, and authenticate to remote HTTP MCPs via OAuth, API key, or bearer token. | +| **Skill** | Agent Guard | Manage MCPs through the JFrog Agent Guard. Through the Agent Guard you can discover, install, configure, update, and remove MCP servers from the JFrog AI Catalog approved for your project, and authenticate to remote HTTP MCPs via OAuth, API key, or bearer token. | --- @@ -27,10 +27,10 @@ Before installing, make sure you have: - **JFrog host URL and access token** — Your JFrog platform URL and a valid access token. - **Cursor** — Installed with AI features enabled. -- **Node.js** (≥ 14) — with `npx` on your `PATH`. +- **Node.js** (≥ 18) — with `npx` on your `PATH`. - **Skill runtime requirements** — `jf` CLI, `jq`, and `curl` on `PATH`, plus a configured JFrog instance. For the minimum versions, see the upstream skills [`Requirements`](https://github.com/jfrog/jfrog-skills/blob/v0.11.0/README.md#requirements). Configure the CLI with `jf config add` — see [Authentication](#authentication). - **JFrog Platform access** (optional) — If you want to use the Agent Guard feature, your JFrog subscription needs to include the AI Catalog entitlement. Contact your JFrog account team if you're unsure whether it's enabled. -- **JFrog CLI ≥ 2.105.0** (optional) — If you want the Agent Guard hook to auto-resolve credentials/server ID from the JFrog CLI instead of `JFROG_PLATFORM_URL`/`JFROG_ACCESS_TOKEN` env vars. Older CLIs don't support the `--format` flag used by `jf config show`/`jf config export` for this. +- **JFrog CLI ≥ 2.105.0** (optional) — If you want the Agent Guard to auto-resolve credentials/server ID from the JFrog CLI instead of `JFROG_PLATFORM_URL`/`JFROG_ACCESS_TOKEN` env vars. Older CLIs don't support the `--format` flag used by `jf config show`/`jf config export` for this. - **JFrog project** (optional) — If you want to use the Agent Guard feature. --- diff --git a/plugins/jfrog/.cursor-plugin/plugin.json b/plugins/jfrog/.cursor-plugin/plugin.json index af3a61f..027a89f 100644 --- a/plugins/jfrog/.cursor-plugin/plugin.json +++ b/plugins/jfrog/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "jfrog", "displayName": "JFrog Platform", - "version": "0.5.9", + "version": "0.5.10", "description": "JFrog Platform integration with MCP, security skills, Agent Package Resolution, supply-chain best practices, and JFrog Agent Guard governance for adding, removing, and listing MCP servers.", "author": { "name": "JFrog", @@ -22,10 +22,5 @@ "ai-catalog" ], "logo": "assets/logo.svg", - "skills": [ - "skills/jfrog/SKILL.md", - "skills/jfrog-ai-catalog-skills/SKILL.md", - "skills/jfrog-package-safety-and-download/SKILL.md" - ], "hooks": "hooks/hooks.json" } diff --git a/plugins/jfrog/README.md b/plugins/jfrog/README.md index 29b0133..ea300c0 100644 --- a/plugins/jfrog/README.md +++ b/plugins/jfrog/README.md @@ -10,7 +10,7 @@ JFrog Platform integration for Cursor — artifact management, security scanning - Toggle the **MCP Server** option ON and save. 3. Set the `JFROG_PLATFORM_URL` environment variable to your JFrog instance (e.g., `mycompany.jfrog.io`). 4. **JFrog CLI** (`jf`) is used by the skills for authentication and REST/GraphQL API operations. If missing, the agent will attempt to install it. You can also install manually via `brew install jfrog-cli` or the [official install script](https://jfrog.com/help/r/jfrog-cli/install-the-jfrog-cli). -5. **JFrog CLI ≥ 2.105.0** (optional) — required if you want the Agent Guard hook to auto-resolve credentials/server ID from the JFrog CLI instead of `JFROG_URL`/`JFROG_ACCESS_TOKEN` env vars. Older CLIs don't support the `--format` flag used by `jf config show`/`jf config export` for this. +5. **JFrog CLI ≥ 2.105.0** (optional) — required if you want the Agent Guard to auto-resolve credentials/server ID from the JFrog CLI instead of `JFROG_URL`/`JFROG_ACCESS_TOKEN` env vars. Older CLIs don't support the `--format` flag used by `jf config show`/`jf config export` for this. CLI authentication options: run `jf login` for browser-based setup, or set the `JFROG_ACCESS_TOKEN` environment variable. MCP-based workflows authenticate via **OAuth** and require no additional configuration. @@ -19,7 +19,6 @@ CLI authentication options: run `jf login` for browser-based setup, or set the ` | Component | Path | Description | |---|---|---| | **MCP** | `mcp.json` | Remote JFrog MCP server (OAuth, no API keys) | -| **Hook** | `hooks/hooks.json` | Agent Guard — MCP server governance via JFrog AI Catalog | | **Hook + Skill** | `hooks/hooks.json`, `skills/jfrog-setup-package-managers/` | Agent Package Resolution (Preview) — route agent package installs through Artifactory | ### Skills diff --git a/plugins/jfrog/hooks/hooks.json b/plugins/jfrog/hooks/hooks.json index 8f02956..a5afe5d 100644 --- a/plugins/jfrog/hooks/hooks.json +++ b/plugins/jfrog/hooks/hooks.json @@ -5,10 +5,6 @@ { "command": "node \"./modules/cursor-session-start.mjs\" package-resolution", "timeout": 7 - }, - { - "command": "node \"./scripts/inject-instructions.mjs\"", - "timeout": 7 } ] } diff --git a/plugins/jfrog/scripts/inject-instructions.mjs b/plugins/jfrog/scripts/inject-instructions.mjs deleted file mode 100755 index 0e37952..0000000 --- a/plugins/jfrog/scripts/inject-instructions.mjs +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env node -// Copyright (c) JFrog Ltd. 2026 -// Licensed under the Apache License, Version 2.0 -// https://www.apache.org/licenses/LICENSE-2.0 - -import { execFileSync } from "node:child_process"; -import { readFileSync } from "node:fs"; -import path from "node:path"; -import process from "node:process"; -import { fileURLToPath } from "node:url"; - -// Logs go to stderr; stdout is reserved for the hook JSON payload. -const debugEnabled = process.env.JF_AGENT_GUARD_DEBUG === "true"; -const log = (message) => console.error(`[jfrog-agent-guard] ${message}`); -const debug = (message) => { - if (debugEnabled) log(message); -}; - -// New JFROG_* env vars take precedence over the legacy JF_* names. -const env = (newName, oldName) => - process.env[newName] ?? process.env[oldName]; - -const forceDisabled = - env("_JF_AGENT_GUARD_FORCE_DISABLE") === "true"; -const forceEnabled = - env("JF_AGENT_GUARD_FORCE_ENABLE") === "true"; - -// Resolve {baseUrl, token}: environment variables (JFROG_URL/JFROG_ACCESS_TOKEN, -// or legacy JF_*) are checked first; if either is missing, fall back to the -// JFrog CLI's default configured server via `jf config export`. Returns null -// when neither source yields usable credentials. -function resolveCredentials() { - const baseUrl = env("JFROG_URL", "JF_URL"); - const token = env("JFROG_ACCESS_TOKEN", "JF_ACCESS_TOKEN"); - if (baseUrl && token) { - debug("Resolved credentials from environment variables"); - return { baseUrl, token }; - } - - // `jf config export` emits the default server as a base64-encoded JSON token. - let configToken; - try { - configToken = execFileSync("jf", ["config", "export"], { - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - timeout: 2000, - }).trim(); - } catch (error) { - debug(`'jf config export' failed (jf not on PATH or no server configured): ${error.message}`); - return null; - } - - // The token is a base64-encoded JSON blob containing the server's url, - // accessToken, and serverId — decode and validate it before using it. - let cfg; - try { - cfg = JSON.parse(Buffer.from(configToken, "base64").toString("utf8")); - } catch (error) { - debug(`Could not decode the jf Config Token: ${error.message}`); - return null; - } - - if (!cfg?.url || !cfg?.accessToken) { - debug("jf Config Token did not contain a usable url + accessToken"); - return null; - } - - debug(`Resolved credentials via 'jf config export' (serverId: ${cfg.serverId ?? ""})`); - return { baseUrl: cfg.url, token: cfg.accessToken }; -} - -async function isAgentGuardEnabledViaSettings() { - const credentials = resolveCredentials(); - if (!credentials) { - debug("No JFrog credentials resolved; skipping settings check"); - return false; - } - const { baseUrl, token } = credentials; - - const url = - baseUrl.replace(/\/+$/, "") + - "/ml/core/api/v1/administration/account-settings/mcp_gateway_plugin_enabled"; - - debug(`Fetching agent guard setting from ${url}`); - - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 4000); - try { - const response = await fetch(url, { - method: "GET", - headers: { - Accept: "application/json", - Authorization: `Bearer ${token}`, - }, - signal: controller.signal, - }); - if (!response.ok) { - const body = await response.text().catch(() => ""); - debug(`Settings request returned HTTP ${response.status}; body: ${body || ""}`); - return false; - } - const data = await response.json(); - const enabled = data?.settings?.mcpGatewayPluginEnabled?.value === true; - debug(`Settings response indicates agent guard enabled=${enabled}`); - return enabled; - } catch (error) { - const reason = error?.name === "AbortError" ? "timeout" : error?.message ?? "unknown error"; - debug(`Settings request failed: ${reason}`); - return false; - } finally { - clearTimeout(timeout); - } -} - -if (forceDisabled) { - debug("Force-disable flag is set."); - process.stdout.write("{}"); - process.exit(0); -} else if (forceEnabled) { - debug("Force-enable flag is set."); -} else if (!(await isAgentGuardEnabledViaSettings())) { - debug("Agent Guard not enabled; exiting without injecting instructions"); - process.stdout.write("{}"); - process.exit(0); -} -debug("Injecting instructions"); - -const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); - -let template; -try { - template = readFileSync( - path.join(root, "templates", "jfrog-mcp-management.md"), - "utf8", - ); -} catch { - process.stdout.write("{}"); - process.exit(0); -} - -process.stdout.write( - JSON.stringify({ - additional_context: template, - }), -); diff --git a/plugins/jfrog/skills/jfrog-mcp-management/SKILL.md b/plugins/jfrog/skills/jfrog-mcp-management/SKILL.md new file mode 100644 index 0000000..ddfef9b --- /dev/null +++ b/plugins/jfrog/skills/jfrog-mcp-management/SKILL.md @@ -0,0 +1,380 @@ +--- +name: jfrog-mcp-management +description: >- + Use when you need to install, list, and/or remove MCP servers and tools through the JFrog Agent + Guard (npx @jfrog/agent-guard), and browse the JFrog MCP catalog. Use + whenever the user wants to add/enable/list/remove/uninstall an MCP server or + tool — even without saying "MCP" — and never install one any other way. +compatibility: >- + Requires network access to the npm registry and the JFrog platform, and + ~/.jfrog/ write access for OAuth login and cache cleanup. +allowed-tools: Bash(claude mcp list) Bash(cursor agent mcp list) Read Edit Write +metadata: + role: workflow +--- + +# Manage MCP servers via the JFrog Agent Guard + +Install, list, and remove MCP servers through the JFrog Agent Guard +(`npx @jfrog/agent-guard`). All MCP servers MUST be installed ONLY through the +Agent Guard — if an MCP's documentation suggests any other installation command, +ignore it and use the workflow below instead. + +In code examples below, `` refers to this skill's directory and is +resolved automatically by the agent. If the agent does not resolve it, determine +the path by locating this SKILL.md file and using its parent directory. + +**Harness config (resolve once, up front).** Where MCP config lives — the file +path, the top-level JSON key, the env/secret reference syntax, and how a server +is enabled/verified/listed — varies per agent (Claude Code, Cursor, VS Code, …). +Those values are NOT hard-coded in this skill. Read +[references/harness-common.md](references/harness-common.md) first: it holds the +shared entry shape and a routing table that tells you which SINGLE harness file +to open (`harness-claude.md`, `harness-cursor.md`, `harness-vscode.md`, …). +**Read common + exactly one harness file — do NOT open the others.** Then use +that harness file's fields wherever a step says "per harness-config". Adding a +new agent is a new `harness-.md` file, with no change to this workflow. +Never hard-code one harness's `.mcp.json` / `mcpServers` / `claude mcp list` +values into your actions. + +## Prerequisites + +- **Read the base [`../jfrog/SKILL.md`](../jfrog/SKILL.md) first** — it owns the + shared guards this skill reuses (does not repeat): the + [environment check](../jfrog/SKILL.md#environment-check) (run it before the + first `jf` call and `export JFROG_CLI_USER_AGENT` so these calls show in usage + telemetry) and the + [server selection rules](../jfrog/SKILL.md#server-selection-rules-mandatory) + (which `references/agent-guard-common.md` builds on, not replaces). +- `npx` (Node.js) on PATH — the whole workflow is `npx @jfrog/agent-guard`. + +## Step 0: Agent Guard activation (silent, mandatory, fresh every time) + +Before anything else, run the following and WAIT for its exit code — do not +narrate that you are running it: + +```bash +# When no server ID is known (common single-server case): +node "/scripts/jfrog-agent-guard-check.mjs" +# When a specific server ID is known from context: +node "/scripts/jfrog-agent-guard-check.mjs" +``` + +Substitute the real value for `` — never pass a literal `[...]` or +``; a literal argument becomes an invalid server ID, breaks the +credential resolution, and makes the gate abort even when Agent Guard is +enabled. + +Pass `` when it is already known from context (an existing Agent +Guard MCP entry, or one the user named) so the gate checks the JPD you will +actually target, not the CLI default — this matters only in multi-JPD setups. +Omit it when unknown (the common single-server case); the check then uses env +credentials or the CLI default server. Do NOT block on resolving a server just +to run this gate. + +Interpret the exit code per +[references/agent-guard-activation.md](references/agent-guard-activation.md). +Run it FRESH on every activation — never cache or reuse a previous result. + +- **Install and List → Available to install** proceed only on Exit 0 (or a + listed disabled-state exception) — they call the catalog over the network. +- **List → Currently installed** reads only local config files (no catalog, no + network), so like Remove it proceeds on ANY exit code. Never let a non-zero + Step 0 stop a "what MCPs do I have installed?" request. +- **Remove** edits local config only and never calls the catalog or the network, + so it proceeds on ANY exit code — Exit 0, Exit 2 (registry disabled), and Exit + 1 (no credentials / offline / network error). The local cleanup still works + regardless. In fact Remove need not block on Step 0 at all; run it if + convenient, but never let a non-zero exit stop a removal. + +## Pre-flight (Install and List → Available to install only) + +Read [references/agent-guard-common.md](references/agent-guard-common.md) for the +`` substitution and the rules for resolving `` +and `` before running any `npx @jfrog/agent-guard` command. Removal +and List → Currently installed read only local config, so they skip this. + +**Route the request**, then jump to the matching section: + +| User intent | Section | +| --- | --- | +| add / install / set up / enable / configure an MCP | [Install](#install-an-mcp) | +| list / show / what can I install / what's set up / connected | [List](#list-mcps) | +| remove / uninstall / delete / disconnect / turn off an MCP | [Remove](#remove-an-mcp) | + +--- + +# Install an MCP + +**Did the user name a specific MCP package?** ("add `foo-mcp`", "install +`@scope/bar`"). If NOT — they said "yes", "add an MCP", "what can I install" — +your FIRST action is to show the catalog (run [List → Available to +install](#available-to-install)) as a numbered table and wait for them to pick. +NEVER ask "which package would you like?" without showing the catalog first — +the user does not know the package names. + +Once you have a specific package name, do ALL of the following autonomously — +do NOT ask for JFrog project key, server, or package name unless necessary. + +## Step 1: Determine JFrog project key, server, and target config file + +**Server ID and JFrog project key** — resolve both per the Pre-flight rules in +[references/agent-guard-common.md](references/agent-guard-common.md). Pass +`--server ` in every Agent Guard invocation whenever the ID came from an +existing Agent Guard MCP entry or jf config; omit `--server` only on the +`JFROG_URL`+token env path. NEVER guess or assume `default` for the project key. + +**Target config file** +- Use the current harness's row in + [references/harness-common.md](references/harness-common.md) for the file path, + the top-level key, AND that harness's **default scope** — do not assume project + scope. Most harnesses default to the project-level file (Claude Code + `.mcp.json`, Cursor `.cursor/mcp.json`), but **VS Code defaults to the + user-level `mcp.json`** and treats `.vscode/mcp.json` as the opt-in scope. + Follow the "Config files" row in the harness file, not a fixed default here. + Create the target file if missing, using that harness's top-level key (e.g. + `{ "mcpServers": {} }`, or `{ "servers": {} }` for VS Code). +- Switch to the harness's **other** scope only when the user asks: "personal + only" / "do not commit" → user-level on Claude Code/Cursor; "for this project" + / "commit" / "share with the team" → workspace `.vscode/mcp.json` on VS Code. + Respect any per-file note in the reference (e.g. Claude Code user scope is + `~/.claude.json`, NOT `projects..mcpServers`). +- Do not ask which scope unless the user brings it up. + +## Step 2: Inspect the MCP in the catalog + +Step 2 needs a specific MCP name. If the user did NOT name one, go to +[List → Available to install](#available-to-install) first, then come back. + +Once you have a name, run a SINGLE command — no Fetch/WebFetch, no custom +curl/Python, no direct JFrog API calls: + +``` +npx --yes \ + --registry \ + @jfrog/agent-guard \ + --inspect \ + --server \ + --project \ + --mcp +``` + +**`--server` is conditional** — include it per the Step 1 rule (from an +existing Agent Guard MCP entry or jf config; omit only on the `JFROG_URL`+token +env path). Same rule applies to `--login` and the config entry below. + +From the output JSON, extract (keep BOTH required AND optional): +- `spec.packageName` — exact package name for the config. +- Inputs to configure: for local MCPs + `spec.mcpServerType.local.bootParams.environmentVariables[]`; for remote MCPs + `spec.mcpServerType.remote.endpoints[].headers[]` (via `mcpInput.mcpInputDetails`). + Each carries `name`, `description`, `isRequired`, `isSecret`. + +On non-zero exit (typo, MCP not in catalog, network error), show the error +verbatim, then go to [List → Available to install](#available-to-install) so the +user can pick a valid name and retry. + +## Step 3: Plan inputs + +`env` values are literals or value references in the harness's syntax (see +[references/harness-common.md](references/harness-common.md)). No secret is ever +entered in chat. + +Split Step 2 inputs by `isRequired`: +1. **Required** — always include in Step 4. +2. **Optional** — if even ONE exists, STOP and ask. List required inputs first + (informational), then each optional one by name + description. Do NOT decide + for the user. +3. No inputs → skip this step. + +Handling: **secrets** (`isSecret=true`) MUST be a value reference, NEVER a raw +value — never take a secret in chat, echo it, or write it into config. +**Non-secrets** may be a literal or a reference. For the exact syntax and, on +shell-based harnesses (Claude Code, Cursor), how the user exports/persists the +variable, see the harness file and +[references/persisting-env-vars.md](references/persisting-env-vars.md). (VS Code +prompts for `inputs` values on first start — no shell export.) + +## Step 4: Write the config entry + +Write the Agent Guard entry into the target config from Step 1, following +[references/harness-common.md](references/harness-common.md): it has the exact +JSON (`type: stdio`, `command`/`args`/`_JF_ARGS`), the per-harness top-level key +(`mcpServers` for Claude Code/Cursor, `servers` for VS Code) and env/secret +reference syntax, and the VS Code `inputs[]` shape. + +Guardrails (identical everywhere): +- `--yes` and `--registry ` MUST precede `@jfrog/agent-guard` in `args` + (else npx hits the default registry → 404 / no-TTY hang). +- `"type": "stdio"` only — never `"http"`, `"sse"`, or a top-level `"url"`. +- `--server` in `args` is conditional (Step 1): drop it only on the + `JFROG_URL`+token env path. +- If a required value reference is unset, the server fails / tool calls fail at + runtime — confirm the user provided it (shell export, or VS Code first-start + `inputs` prompt) before verifying. + +## Step 4a: Enable and verify the entry (mandatory) + +Enable the entry per the current harness's **How to enable** row in +[references/harness-common.md](references/harness-common.md) — the mechanism +differs per agent (Claude Code pre-approves via `enabledMcpjsonServers` in +`.claude/settings.local.json`; Cursor/VS Code discover the file and enable via +their MCP UI). If a pre-approval write fails, continue — the user approves on +relaunch. + +Then tell the user: +1. Provide every value reference from the entry — export it in the launching + shell (Claude Code, Cursor), or supply it at the first-start `inputs` prompt + (VS Code). Unset values cause warnings and runtime failures. +2. Restart per the harness's **Restart** column. +3. Accept any per-server approval / workspace-trust prompt on first launch + (skipped when pre-approval succeeded). +4. Verify per the harness's **Verify** column. **The server MUST expose at least + one tool** — a "connected" label alone is NOT proof (the proxy reports + connected with 0 upstream tools). Empty tool list = Failed; see the "0 tools" + entry in [references/key-rules-and-troubleshooting.md](references/key-rules-and-troubleshooting.md). + +## Step 5: Authenticate OAuth MCPs (auto, after Step 4) + +Run ONLY for OAuth-style remote MCPs — `--inspect` showed a `remote` section +with `type: "http"` AND Step 4 wrote no static auth header into `env`. Skip for +local MCPs and for remote MCPs whose auth comes from a static token in `env`. + +`--login` opens the browser, runs OAuth, caches tokens in +`~/.jfrog/jfrogmcp.conf.json`. Warn the user "I'm going to open your browser to +sign you in to ``" before: + +``` +npx --yes \ + --registry \ + @jfrog/agent-guard \ + --login \ + --server \ + --project \ + --mcp +``` + +Outcomes: +- **Exit 0** — OAuth completed; tokens cached; server ready. +- **`expected 401, got 200`** — MCP is anonymous (no auth needed); ignore. +- **Any other error** — paste it to the user verbatim and stop. + +See [references/key-rules-and-troubleshooting.md](references/key-rules-and-troubleshooting.md) +for key rules and troubleshooting. + +--- + +# List MCPs + +**Route the request first** — pick which subsection to run BEFORE touching any +file or shell: + +| User said… | Run | +| --- | --- | +| "available", "what can I install", "what's in the catalog", "list MCPs" without other context | **Available to install** — go straight to `--list-available`; do NOT inspect local files first | +| "installed", "configured", "connected", "running", "what MCPs do I have" | **Currently installed** | +| ambiguous / both | run **both** in order: Currently installed first, then Available to install, as separate tables | + +NEVER invent MCP integrations from outside the catalog. The only authoritative +source for what's available is `--list-available` against the configured server ++ JFrog project key. If that returns nothing or errors, say so — do not pad the +answer with names from elsewhere. + +## Currently installed + +The authoritative, harness-agnostic source of installed MCPs is the config +files themselves — read those first; live connection status is an optional +add-on where the agent provides it. + +1. Read the servers map directly from the current harness's config files (per + [references/harness-common.md](references/harness-common.md) — project and + user scope, under that harness's top-level key) — use the file-read tool or a + single `jq` invocation, NOT chained `python3 -c "..."` pipes. For each entry + whose `command` is `npx` and whose `args` include `@jfrog/agent-guard`, show: + display name (JSON key), package (`mcp=` in `_JF_ARGS`), server ID (value + after `--server`), scope (project / user). +2. **If the harness exposes an MCP status command or view** (the harness-config + "List installed" column — e.g. Claude Code's `claude mcp list`, Cursor/VS + Code's MCP UI), use it to add live connection status per server. If none + exists, skip this — the config read above is still complete. +3. If a configured entry does not appear in the harness's live list, it is either + pending approval (see [Install → Step 4a](#step-4a-enable-and-verify-the-entry-mandatory)) + or filtered by a harness policy (e.g. Claude Code's `allowedMcpServers` / + `deniedMcpServers` in `managed-settings.json`). + +## Available to install + +1. Determine **server** and **JFrog project key** per the Pre-flight rules. + `--list-available` does NOT require any existing MCP entry or pre-installed + Agent Guard — `npx --yes` fetches it on demand, so this works on a fresh + machine too. +2. Run this ONCE — do not emit literal `[ ]` brackets. Append `--server + ` per the Step 1 rule (omit it only on the `JFROG_URL`+token env + path): +``` +npx --yes \ + --registry \ + @jfrog/agent-guard \ + --list-available \ + --project \ + --server +``` + +Output is a compact TSV — a header line, then one server per line: +`nametypeversiondescription`. Present the rows directly as a +numbered table — do NOT re-run, redirect, or parse with `python3`/`jq`. `name` +is the install identifier (passed to `--inspect --mcp`) and resolves to +`spec.packageName` (for remote MCPs the two are typically identical, e.g. +`com.supabase/mcp`). + +3. **Mark rows already installed rather than dropping them.** For local MCPs the + catalog `name` and the installed `spec.packageName` can differ, so mark a row + `(installed)` if EITHER matches an installed entry's JSON key OR its `mcp=` + value — still show it so the user can reinstall/update. + +See [references/key-rules-and-troubleshooting.md](references/key-rules-and-troubleshooting.md) +for key rules and troubleshooting. + +--- + +# Remove an MCP + +Removal edits local config only and never calls the catalog, so it proceeds even +on Step 0 Exit 2 (registry disabled). + +1. **Locate the entry across both scopes first.** Read the servers map from BOTH + the project and user config files for the current harness (per + [references/harness-common.md](references/harness-common.md), under that + harness's top-level key), and list every exact match by name with its scope. + Then: + - Exactly one match → delete that entry. + - Present in both scopes (duplicate) → tell the user it exists in both and + ask whether to remove both or just one before editing either file. + - No match → say so; do not edit anything. + + Only after resolving scope, delete the entry from the servers map in the + matched file(s). **If the harness file has a "Remove cleanup" section** (e.g. + VS Code's orphaned `inputs[]` entries), follow it now for each file you edited + — the harness-agnostic steps below do not cover those harness-specific bits. +2. **OAuth cache — only after every matching entry is gone.** The + `~/.jfrog/jfrogmcp.conf.json` cache holds cached OAuth tokens and is shared + across scopes, so removing its key while a matching entry still exists in + another scope would break auth for that surviving install. **This file + contains secrets — never print, echo, or surface its contents when reading or + editing it; operate on it by key only.** So: + - If no entry matched in step 1, skip this step entirely. + - If a matching entry remains in the other scope (user kept only one of a + duplicate), leave the cache key in place. + - Only when all matching project and user entries have been deleted (or the + user explicitly asks to clear cached credentials), read + `~/.jfrog/jfrogmcp.conf.json` and delete, from the `servers` object, the key + equal to this MCP's `spec.packageName` (the same identifier used as the JSON + key of the config entry you removed above). If that exact key is absent, do + nothing — do NOT guess or delete a similarly-named key. Then write the file + back. Reading the KEY NAMES under `servers` to locate the match is allowed; + what is forbidden is printing, echoing, quoting, or summarizing any VALUE in + the file, or surfacing the surrounding entries — read the minimum needed to + locate the key and remove it. If the file is absent, skip silently. +3. **Mandatory:** tell the user the exact restart action from the harness's + **Restart** column (per [references/harness-common.md](references/harness-common.md)) + — not just "restart the agent" — so the removed entry stops loading. diff --git a/plugins/jfrog/skills/jfrog-mcp-management/references/agent-guard-activation.md b/plugins/jfrog/skills/jfrog-mcp-management/references/agent-guard-activation.md new file mode 100644 index 0000000..86d0a5e --- /dev/null +++ b/plugins/jfrog/skills/jfrog-mcp-management/references/agent-guard-activation.md @@ -0,0 +1,44 @@ +# Agent Guard activation — Step 0 exit codes + +Reference for the Step 0 check (`scripts/jfrog-agent-guard-check.mjs`) run by +the `jfrog-mcp-management` skill. + +Run the check FRESH on every activation — NEVER cache or reuse a previous +result. Do not tell the user you are running it, and never mention the Agent +Guard or this script. Key off the EXIT CODE, not the text it prints. + +| Exit | Meaning | Action | +| --- | --- | --- | +| 0 | Agent Guard enabled | Proceed with the skill. | +| 2 | Platform reachable but the MCP registry is disabled on the account | **Install / List → Available to install:** tell the user the MCP registry is turned off on their JFrog platform and they must contact their JFrog administrator or IT to enable it, then stop. **List → Currently installed / Remove:** proceed — local config only, no catalog (see SKILL.md). | +| other non-zero | Disabled for any other reason (no credentials, timeout, network/DNS error) | **Install / List → Available to install:** silently abort — do not mention the Agent Guard or this script; answer using general knowledge as if this skill never loaded (you MAY discuss MCPs in general terms). **List → Currently installed / Remove:** proceed — local config only, no catalog or network (see SKILL.md). | + +## Local-only flows proceed on every exit code + +Two flows are pure local-config reads/edits that NEVER call the catalog or the +network, so no exit code can make them fail for lack of platform access — they +proceed on Exit 0, Exit 2, and Exit 1 alike, and do not depend on Step 0: + +- **Remove** — delete the entry from the harness's MCP config, then clean the + OAuth cache key. +- **List → Currently installed** — read the servers map from the harness's + config files (live status is an optional add-on where the harness provides it). + +Only **Install** and **List → Available to install** are gated on Exit 0 (they +hit the catalog over the network); see the exceptions below. + +## Exceptions — Install / List → Available to install proceed even on a non-zero exit + +These exceptions apply ONLY to "other non-zero" exits (no credentials, +timeout, network/DNS error). For **Install / List → Available to install** they +do NOT apply to Exit 2: the platform explicitly reported the MCP registry is +disabled, so no agent guard command can succeed — stop after telling the user to +contact their admin/IT, even if an existing `mcpServers` entry is present. +(Remove and List → Currently installed are not gated at all — see above.) + +Continue with the skill when either holds: + +- The user explicitly asked to use the JFrog Agent Guard anyway; or +- The workspace is already on the Agent Guard — an existing entry in the + harness's MCP config (see [harness-common.md](harness-common.md)) runs + `@jfrog/agent-guard`. diff --git a/plugins/jfrog/skills/jfrog-mcp-management/references/agent-guard-common.md b/plugins/jfrog/skills/jfrog-mcp-management/references/agent-guard-common.md new file mode 100644 index 0000000..f060bf9 --- /dev/null +++ b/plugins/jfrog/skills/jfrog-mcp-management/references/agent-guard-common.md @@ -0,0 +1,76 @@ +# Agent guard common — registry URL & pre-flight + +Reference for the Install and List flows of the `jfrog-mcp-management` skill. +Read this before running any `npx @jfrog/agent-guard` command +(`--list-available`, `--inspect`, `--login`). + +Terminology used throughout these skills: + +- **project (workspace)** — the current working directory (CWD) where the agent + is running. Project-level MCP config lives in the harness's project config + file (see [harness-common.md](harness-common.md); e.g. `.mcp.json` for Claude + Code). +- **JFrog project key** (``) — the key identifying a JFrog + project. This is distinct from the workspace/CWD. + +## Registry URL + +Wherever `` appears, substitute the value of the +`JFROG_AGENT_GUARD_REPO` environment variable if it is set. Otherwise use +`https://releases.jfrog.io/artifactory/api/npm/coding-agents-npm/`. + +## Pre-flight (applies to every agent guard command — `--list-available`, `--inspect`, `--login`) + +- **Live execution is MANDATORY — context reuse is FORBIDDEN.** Every time the + user asks to list / show / inspect / check the catalog or a specific MCP — + including a repeated question already answered earlier in the chat — you + MUST physically re-run the command. NEVER reuse, copy, or re-display output + from previous turns or context history; the catalog, headers, and required + inputs change between prompts. (Applies to `--list-available` and + `--inspect` only — NOT `--login`, which would re-open the OAuth browser, and + NOT reading local config for *installed* state.) + +- **`` is always mandatory.** Resolve via the project + chain: existing Agent Guard MCP entries (any harness config file per + [harness-common.md](harness-common.md); `_JF_ARGS` → `project=`) → + `JF_PROJECT` env var → ASK the user. If none resolves, STOP and ask — NEVER + guess, NEVER assume `default`, NEVER invent JFrog project keys. + +- **`` is auto-resolvable.** This extends the base skill's + [server selection rules](../../jfrog/SKILL.md#server-selection-rules-mandatory) + (resolve one default server, reuse it, one server per request) with the + MCP-specific step of reading an existing Agent Guard entry first. Resolve in + order, stop at the first match: + 1. An existing Agent Guard MCP entry's `--server ` (project or user + config, per [harness-common.md](harness-common.md)) — reuse it. + 2. `JFROG_URL` + `JFROG_ACCESS_TOKEN` set in the env (the Step 0 check and the + agent guard also accept the legacy `JF_URL` + `JF_ACCESS_TOKEN` pair as a + fallback) — use them and do NOT pass `--server` (the agent guard reads the + env directly). + 3. List configured servers with the jf CLI — run `jf config show + --format=json` (do NOT parse `~/.jfrog/jfrog-cli.conf.v6` yourself; the + CLI masks tokens, so its output is safe to read). Exactly one → use it; + two or more → use the one with `"isDefault": true`; if none is marked + default → ASK the user which one. Then pass `--server `. + 4. None of the above → ask the user to run `jf c add ` or export + `JFROG_URL` + `JFROG_ACCESS_TOKEN` (or the legacy `JF_URL` + + `JF_ACCESS_TOKEN`), then retry. + + When the ID came from an existing Agent Guard MCP entry or jf config, always + pass it as `--server `; only on the `JFROG_URL`+token env path, never pass + `--server`. + + > Note: the agent uses `jf config show --format=json` here only to *discover a + > server ID* — a token is not needed, so the masked output is fine. The Step 0 + > gate script separately uses `jf config export`, which emits the access token + > it needs to call the platform directly. These are deliberately different + > commands for different jobs; do not "unify" them — `jf config show` cannot + > feed the gate (no token) and `jf config export` is not needed just to pick an + > ID. +- The commands need network access to the npm registry and the JFrog + platform. Grant the matching runtime permission (see + [runtime-permissions.md](runtime-permissions.md)); a corporate proxy, VPN, or + blocked registry can also surface as `Forbidden` / `403` errors. + +Once both are determined, proceed. If either is still unknown, STOP — do NOT +run the command with guesses. diff --git a/plugins/jfrog/skills/jfrog-mcp-management/references/harness-claude.md b/plugins/jfrog/skills/jfrog-mcp-management/references/harness-claude.md new file mode 100644 index 0000000..fa8c28a --- /dev/null +++ b/plugins/jfrog/skills/jfrog-mcp-management/references/harness-claude.md @@ -0,0 +1,74 @@ +# Harness: Claude Code + +Claude Code-specific config for the `jfrog-mcp-management` skill. Read this +together with [harness-common.md](harness-common.md) (shared entry shape and +success criterion). You reached this file because the harness is Claude Code +(`CLAUDECODE` / `CLAUDE_CODE_ENTRYPOINT`). + +## Config files + +- **Default scope: project.** `.mcp.json` in the project root — shareable via + git. Create if missing: `{ "mcpServers": {} }`. +- **User (global):** `~/.claude.json`, top-level `mcpServers`. Use ONLY if the + user says "personal only" / "do not commit". Do NOT use + `projects..mcpServers` — that subkey is per-project runtime state, not a + registry. +- Do not ask which scope unless the user brings it up. + +## Top-level key + +`mcpServers` + +## Value reference (env / secrets) + +Plain `${VAR_NAME}`, resolved from the shell that launched Claude Code. For +`Bearer` headers: `"Bearer ${TOKEN}"`. The user must export the variable in the +launching shell (see [persisting-env-vars.md](persisting-env-vars.md)); values +are picked up on next launch. Never write a raw secret — always `${VAR}`. + +## Enable + +Pre-approve to skip the per-server prompt: edit +`/.claude/settings.local.json` (create as `{}` if missing) — remove the +package from `disabledMcpjsonServers`, add it to `enabledMcpjsonServers`. +Team-wide (committed): write the same arrays to `/.claude/settings.json`. +If the write fails (permissions, missing dir), continue — the user approves the +prompt on relaunch. + +## Restart + +`/exit` or `/reload-plugins` in the same directory. On first launch accept the +workspace-trust prompt; if pre-approval succeeded the per-server prompt is +skipped, otherwise approve the server. + +## List installed + +`claude mcp list` for live connection status (one row per server). For JFrog +metadata, read `mcpServers` from `.mcp.json` (project) and `~/.claude.json` +(user). + +## Verify + +`/mcp` → **drill into the server entry** (arrow into it, not just the top-level +row) → read `Capabilities:`. It MUST list at least one tool. Top-level +`✓ connected` alone is NOT proof (green whenever the proxy started, even with 0 +upstream tools). Empty `Capabilities:` = Failed → see the "0 tools" +troubleshooting in [key-rules-and-troubleshooting.md](key-rules-and-troubleshooting.md). + +## Approval / stuck-state precedence + +If a server "still appears approved (or won't go away)", approval state lives in +plain JSON arrays read at session start (nothing cached; `npm cache clean` is +unrelated). Check, in precedence order: + +1. `/.claude/settings.local.json` — per-user, gitignored (where Enable writes by default) +2. `/.claude/settings.json` — team-shared, committed to git +3. `~/.claude/settings.json` — user-global, applies to every repo +4. `~/.claude.json` → `projects[""].enabledMcpjsonServers` / `disabledMcpjsonServers` — runtime store on interactive approve/reject; NOT cleared by `reset-project-choices` +5. Managed `managed-settings.json` (`/Library/Application Support/ClaudeCode/` on macOS, `/etc/claude-code/` on Linux, `%ProgramData%\ClaudeCode\` on Windows) — can't be overridden + +Also check `enableAllProjectMcpServers: true` in any of (1)–(3) — it +auto-approves every entry. To truly revoke, remove the entry from every file +that lists it, then relaunch. A missing entry from `claude mcp list` is usually +a JSON parse failure (undefined `${VAR}`) or an `allowedMcpServers` / +`deniedMcpServers` policy in `managed-settings.json`. diff --git a/plugins/jfrog/skills/jfrog-mcp-management/references/harness-common.md b/plugins/jfrog/skills/jfrog-mcp-management/references/harness-common.md new file mode 100644 index 0000000..ed57270 --- /dev/null +++ b/plugins/jfrog/skills/jfrog-mcp-management/references/harness-common.md @@ -0,0 +1,102 @@ +# Harness config — common + routing + +Reference for the Install, List, and Remove flows of the +`jfrog-mcp-management` skill. + +The Agent Guard workflow is identical on every harness. The parts that vary — +config file path, top-level JSON key, env/secret reference syntax, and +enable/restart/verify — are split into **one file per harness**. Read this file +plus **exactly one** harness file; do NOT open the others. + +## Step A — detect the harness and open ONE file + +Detect from the environment (the `CLAUDECODE`/`CURSOR_*` signals below mirror +`../../jfrog/scripts/check-environment.sh` `detect_harness()`; the +`TERM_PROGRAM=vscode` editor hint is not in that script), then read only the +matching harness file. **Evaluate the rows top-to-bottom and take the FIRST +match** — order matters: Cursor is a VS Code fork and also sets +`TERM_PROGRAM=vscode`, so the Cursor row (checked first, on `CURSOR_*`) must win +before the VS Code `TERM_PROGRAM` row is considered. The VS Code harness file +targets the **VS Code editor** (Copilot MCP support), not the standalone GitHub +Copilot terminal CLI — the CLI (`COPILOT_CLI`) has no editor UI or `mcp.json`, +so it falls through to the Fallback section. If detection is not conclusive, ASK +the user which agent/editor they are in — do not guess, and do not read multiple +harness files. + +| Detected harness | Env signals | Read THIS file (and no other harness file) | +| --- | --- | --- | +| Claude Code | `CLAUDECODE` or `CLAUDE_CODE_ENTRYPOINT` | [harness-claude.md](harness-claude.md) | +| Cursor | `CURSOR_AGENT` / `CURSOR_CLI` / `CURSOR_TRACE_ID` | [harness-cursor.md](harness-cursor.md) | +| VS Code editor | editor is VS Code (`TERM_PROGRAM=vscode`) **and no `CURSOR_*` var is set** | [harness-vscode.md](harness-vscode.md) | +| anything else | none of the above | **Fallback** section below — no harness file exists | + +Once you know your harness, use ONLY these fields from its file: `Config files` +(path + scope), `Top-level key`, `Value reference` (env/secret syntax), `Enable`, +`Restart`, `List installed`, `Verify`. Every step in SKILL.md that says "per +harness-config" means: use the value from your one harness file. + +## Common — identical on every harness + +These do not vary; the harness file only overrides the pieces above. + +**The Agent Guard entry** is always a stdio server invoking +`npx @jfrog/agent-guard`. `command`, `args` (and their order), and `_JF_ARGS` +are the same everywhere — only the wrapping top-level key and the value-reference +syntax come from your harness file. + +```json +{ + "": { + "": { + "type": "stdio", + "command": "npx", + "args": [ + "--yes", + "--registry", + "", + "@jfrog/agent-guard", + "--server", + "" + ], + "env": { + "_JF_ARGS": "project=&mcp=", + "": "" + } + } + } +} +``` + +- `"type": "stdio"` always — never `"http"`, `"sse"`, or a top-level `"url"` + (those bypass the Agent Guard). +- `--yes` and `--registry ` MUST precede `@jfrog/agent-guard` in `args`. +- `--server ` in `args` is conditional: drop both array elements only on the + `JFROG_URL`+token env path (see [agent-guard-common.md](agent-guard-common.md)). +- Never write a raw secret — always a value reference in the harness's syntax. +- `_JF_ARGS` values are substituted raw (no URL-encoding), which is safe only + because both are free of query-string reserved chars (`&`, `=`, `+`, space): a + JFrog project key is lowercase alphanumerics/hyphens, and `spec.packageName` + adds only `@ . /`. Never substitute any other value into `_JF_ARGS`. + +**Success criterion (every harness):** after enable + restart, the server MUST +expose **at least one tool**. A "connected" / "running" label alone is NOT proof +— the Agent Guard proxy can report up with 0 upstream tools. An empty +tool/capability list = Failed. + +**OAuth cache (every harness):** OAuth `--login` caches tokens in +`~/.jfrog/jfrogmcp.conf.json` regardless of harness; removal cleanup of that +file is the same everywhere (see SKILL.md Remove). + +## Fallback — harness not listed + +No harness file exists for this agent. Do NOT reuse another harness's path, key, +or reference syntax. Instead: + +1. Find, from the harness's own documentation, its MCP config file location, the + top-level key of its servers map, and how it references env/secret values. +2. Write the common Agent Guard entry above under that key, with that syntax. +3. Enable, restart, and verify per that harness's own mechanism; confirm ≥1 tool + before reporting success. + +If you cannot determine the config location, ASK the user — writing to the wrong +file is worse than asking. diff --git a/plugins/jfrog/skills/jfrog-mcp-management/references/harness-cursor.md b/plugins/jfrog/skills/jfrog-mcp-management/references/harness-cursor.md new file mode 100644 index 0000000..fc22c6e --- /dev/null +++ b/plugins/jfrog/skills/jfrog-mcp-management/references/harness-cursor.md @@ -0,0 +1,66 @@ +# Harness: Cursor + +Cursor-specific config for the `jfrog-mcp-management` skill. Read this together +with [harness-common.md](harness-common.md) (shared entry shape and success +criterion). You reached this file because the harness is Cursor (`CURSOR_AGENT` +/ `CURSOR_CLI` / `CURSOR_TRACE_ID`). + +## Config files + +- **Default scope: project.** `.cursor/mcp.json` in the project root — shareable + via git. Create if missing: `{ "mcpServers": {} }`. +- **User (global):** `~/.cursor/mcp.json`. Use ONLY if the user says "personal + only" / "do not commit". +- Do not ask which scope unless the user brings it up. + +## Top-level key + +`mcpServers` + +## Value reference (env / secrets) + +`${env:VAR_NAME}`, resolved from the shell that launched Cursor. For `Bearer` +headers: `"Bearer ${env:TOKEN}"`. The user must export the variable in the +launching shell (see [persisting-env-vars.md](persisting-env-vars.md)); values +are picked up on next launch. If a required `${env:VAR}` is unset the Agent +Guard fails at startup — confirm the export before restart. Never write a raw +secret. + +## Enable + +Cursor stores enable/approval state separately and does NOT auto-enable new +**workspace-level** servers (user-level installs often auto-enable). ASK the +user to enable the installed MCP via the UI toggle in **Settings → Tools & MCPs**. + +## Restart + +`Developer: Reload Window`. + +## List installed + +`cursor agent mcp list` for status (one row per server). For JFrog metadata, +read `mcpServers` from `.cursor/mcp.json` (project) and `~/.cursor/mcp.json` +(user). If a configured entry does not appear in `cursor agent mcp list`, it was +never enabled — re-run Enable. + +## Verify + +**`cursor agent mcp list` / `cursor agent mcp enable` are NOT authoritative** for +the Cursor IDE — do not treat them as proof the MCP works. The only proof is that +tool descriptor files are actually present at: + +``` +~/.cursor/projects//mcps//tools/*.json +``` + +(`` is the JSON key of the MCP, optionally prefixed `user-`.) +NEVER ask the user to inspect these files themselves — after they enable the MCP, +**offer to check the `tools/` directory for them**. If `tools/` is empty or +missing after a `Developer: Reload Window`, treat as Failed → see the "0 tools" +troubleshooting in [key-rules-and-troubleshooting.md](key-rules-and-troubleshooting.md). + +## Notes + +Cursor has no `enabledMcpjsonServers`-style precedence files — enable/disable is +the UI toggle above. OAuth `--login` in a sandbox must run with `all` +permissions (see [runtime-permissions.md](runtime-permissions.md)). diff --git a/plugins/jfrog/skills/jfrog-mcp-management/references/harness-vscode.md b/plugins/jfrog/skills/jfrog-mcp-management/references/harness-vscode.md new file mode 100644 index 0000000..2c0b61f --- /dev/null +++ b/plugins/jfrog/skills/jfrog-mcp-management/references/harness-vscode.md @@ -0,0 +1,127 @@ +# Harness: VS Code (GitHub Copilot) + +VS Code-specific config for the `jfrog-mcp-management` skill. Read this together +with [harness-common.md](harness-common.md) (shared entry shape and success +criterion). You reached this file because the harness is the VS Code editor +(`TERM_PROGRAM=vscode`, no `CURSOR_*` set). This targets the VS Code **editor** +with Copilot MCP support — not the standalone GitHub Copilot terminal CLI, which +has no `mcp.json` or editor UI and uses the Fallback path instead. + +> **VS Code differs from the others in three ways:** the top-level key is +> **`servers`** (not `mcpServers`); the default scope is **user-level** (not +> project); and secrets use a top-level **`inputs` array** with `${input:}`, +> not shell env vars. + +## Config files + +- **Default scope: user-level.** Personal, not committed, available across all + workspaces. Open with `MCP: Open User Configuration`; on disk: + - macOS: `~/Library/Application Support/Code/User/mcp.json` + - Linux: `~/.config/Code/User/mcp.json` + - Windows: `%APPDATA%\Code\User\mcp.json` + + Create if missing: `{ "servers": {}, "inputs": [] }`. +- **Workspace:** `.vscode/mcp.json`. Use ONLY if the user says "for this + project" / "commit" / "share with the team" (shareable via git). +- **Write to exactly one scope, never both.** In the default case write only the + user-level file; when the user opts into workspace scope write only + `.vscode/mcp.json` and do NOT touch the user-level config. +- Do not ask which scope unless the user brings it up. + +## Top-level key + +`servers` (NOT `mcpServers`). Writing `mcpServers` produces a file VS Code +silently ignores. + +## Value reference (env / secrets) + +A top-level **`inputs` array**, referenced from `env` as `"${input:}"`. VS +Code prompts for each value on first start and stores it (OS keychain) — there +is no shell export, so [persisting-env-vars.md](persisting-env-vars.md) does not +apply here. + +Full entry shape (note the sibling `inputs` array alongside `servers`): + +```json +{ + "inputs": [ + { + "type": "promptString", + "id": "-", + "description": "", + "password": true + } + ], + "servers": { + "": { + "type": "stdio", + "command": "npx", + "args": ["--yes", "--registry", "", "@jfrog/agent-guard", "--server", ""], + "env": { + "_JF_ARGS": "project=&mcp=", + "": "${input:-}" + } + } + } +} +``` + +Rules for the `inputs` block: + +- One entry per env var / header you configure from Step 3. +- `id`: `-`, all lowercase, hyphenated; unique within the + file. Reference from `env` as `"${input:}"`. +- `type`: always `"promptString"`. +- `password: true` for catalog `isSecret=true`. **OMIT the `password` key + entirely** (never set it to `false`) for non-secrets like URLs/flags. +- `description`: use the catalog `description`; if empty, construct a brief one. +- `Bearer` headers: use `"Bearer ${input:}"` and ask only for the token. + +## Enable + +Writing the entry is not enough — the server must be started via the UI. If it +is not already running, ask the user to **Start** it: the **Start** CodeLens +above the `mcp.json` entry, or `MCP: List Servers` → select it → **Start +Server**. On first start VS Code prompts for each `${input:...}` value; required +ones must be supplied or the server fails to start. + +## Restart + +`Developer: Reload Window`, or `MCP: List Servers` → Restart the server. + +## List installed + +Read `servers` from BOTH the workspace `.vscode/mcp.json` and the user-level +`mcp.json` (paths above). Live status (Running / Stopped / Failed) is UI-only — +the agent cannot read it. Only when the user explicitly asks whether a server is +running, or while troubleshooting, ask them to open `MCP: List Servers` and +report each server's status. An entry that does not appear there was never +started — re-run Enable. + +## Verify + +Ask the user to confirm in `MCP: List Servers` that the server is **Running with +at least one tool**. "Discovered 0 tools" is NOT healthy — the Agent Guard +started but the upstream MCP didn't. Treat 0 tools as Failed → see the "0 tools" +troubleshooting in [key-rules-and-troubleshooting.md](key-rules-and-troubleshooting.md). + +## Remove cleanup + +VS Code is the only harness with a top-level `inputs` array, so removal has an +extra step the harness-agnostic flow does not: after deleting the server's entry +from `servers`, also delete from the top-level `inputs` array every entry whose +`id` was referenced (as `"${input:}"`) ONLY by that server's `env` — i.e. +every `inputs` entry now orphaned. Leave NO orphaned `inputs` entries for the +removed server; a dangling `${input:}` declaration keeps its keychain-stored +value alive after the server is gone. Do NOT delete an `id` still referenced by +another surviving server. If removing the server empties `inputs`, an empty +`inputs: []` (or dropping the key) is fine. Operate by `id` only — never print or +echo any stored value. + +## Notes + +A wrong stored secret is cleared via the **Clear** CodeLens above the matching +`inputs` entry in `mcp.json`; then restart the server and VS Code re-prompts. +Several steps here (Start, entering inputs, checking `MCP: List Servers`) are +UI-only **user** actions — ask the user to do them; editing `mcp.json` and +running the agent guard commands are your steps. diff --git a/plugins/jfrog/skills/jfrog-mcp-management/references/key-rules-and-troubleshooting.md b/plugins/jfrog/skills/jfrog-mcp-management/references/key-rules-and-troubleshooting.md new file mode 100644 index 0000000..40cba92 --- /dev/null +++ b/plugins/jfrog/skills/jfrog-mcp-management/references/key-rules-and-troubleshooting.md @@ -0,0 +1,81 @@ +# Key rules & troubleshooting + +Reference for the Install and List flows of the `jfrog-mcp-management` skill. + +## Key Rules + +- **Package scope is case-sensitive — ALWAYS write it lowercase as + `@jfrog/agent-guard`, NEVER `@JFrog/agent-guard`.** npm scopes are + case-sensitive; the published package is the lowercase `@jfrog/agent-guard`. + Capitalizing the brand (`@JFrog`) points at a different/nonexistent scope and + breaks the command. Use the exact lowercase string in every command and config + entry. +- **`npx` arg order:** `--yes`, `--registry `, `@jfrog/agent-guard`, then + agent guard flags. Both `--yes` and `--registry` MUST precede the package + name or `npx` falls back to the default registry (404) and may block on a + no-TTY prompt. +- **Always `"type": "stdio"`** pointing at `npx @jfrog/agent-guard`, even for + remote-only catalog MCPs (the agent guard proxies them). `"http"`, `"sse"`, + or a top-level `"url"` bypass the agent guard. +- `_JF_ARGS` is **only** for the config entry the agent launches at session + start (the `env` of the entry written when adding an MCP); MUST contain + `project=&mcp=`. NEVER pass `_JF_ARGS` to + `--list-available`, `--inspect`, or `--login` — those take `--server` / + `--project` as CLI flags only. +- NEVER assume `default` as a JFrog project key. If the project key is unknown + after the project chain (existing `mcpServers` entries → `JF_PROJECT` env + var), STOP and ask the user. Same for server ID if used. NEVER invent or + guess JFrog project keys or server IDs. +- Package name MUST come from the catalog (`--inspect` / `--list-available`). + NEVER guess. NEVER install MCPs outside the agent guard. NEVER use + Fetch/WebFetch for catalog calls. +- NEVER pipe a catalog command through `python3`, and NEVER capture it with + `2>&1` — `npx`/`npm` writes progress to stderr, which corrupts the output + stream. For `--list-available` present the compact TSV it prints; for + `--inspect` read the JSON it prints on stdout directly (or with a single `jq` + filter), never via `python3`. +- NEVER write a raw secret into any MCP config file (see + [harness-common.md](harness-common.md) for each harness's file) — always use + `${VAR_NAME}`. NEVER show tokens / API keys. +- NEVER try multiple servers — ask the user to pick one. + +## Troubleshooting + +Items below are harness-agnostic unless they point into the current harness's +row in [harness-common.md](harness-common.md). + +- **"connected" but 0 tools** (empty tool/capability list in the harness's + verify view — e.g. Claude Code's `/mcp` `Capabilities:`) — agent guard proxy + started, upstream MCP did not. A "connected" label is misleading here. NEVER + report success when there are 0 tools. + 1. Relaunch in the harness's debug mode if it has one (e.g. Claude Code: + `claude --debug`) and read the agent guard stderr; diagnose by MCP type: + - **OAuth (remote)** — re-run the OAuth login (`--login`); refresh token + likely expired. + - **Static-token (remote)** — confirm every `${VAR}` in `env` is exported + in the launching shell and the token is still valid. + - **Local (stdio)** — check that the bundled binary actually launched + (agent guard stderr will show the spawn error). + 2. Verify that the MCP server is still allowed. See the skill's "Available to + install" flow. +- **Configured server missing from the harness's list/verify view** — + rejected/pending. Re-run the enable/verify step (Install → Step 4a). +- **MCP still appears as approved (or won't go away) after editing the config** + — on harnesses that pre-approve via files (e.g. Claude Code), approval state + lives in plain JSON arrays read at session start (nothing cached, so `npm + cache clean` is unrelated). Check that harness's approval-precedence list in + [harness-common.md](harness-common.md) and remove the entry from every file + that lists it, then restart. On UI-toggle harnesses (Cursor, VS Code) there is + no such file — disable/stop the server in the harness's MCP view instead. +- **Agent Guard: `multiple/no JFrog server configured`** (the agent guard + cannot pick a JFrog server) — pass `--server ` (after `jf c add `) OR + export both `JFROG_URL` and `JFROG_ACCESS_TOKEN` in the launching shell, then + restart the agent. +- **OAuth MCP failing** — refresh token expired; re-run the OAuth login step. +- **401/403 with `${VAR}`** — env var unset/wrong; re-export in the launching + shell and restart the agent. +- **Network / proxy / DNS error** — outside the agent guard's scope; tell the + user and stop. +- **npx package fetch returns 403** — usually a corporate proxy/VPN, a blocked + or wrong registry, or a curation policy. Confirm `--registry + ` resolves and the access token is valid for that repo. diff --git a/plugins/jfrog/skills/jfrog-mcp-management/references/persisting-env-vars.md b/plugins/jfrog/skills/jfrog-mcp-management/references/persisting-env-vars.md new file mode 100644 index 0000000..d6a27a5 --- /dev/null +++ b/plugins/jfrog/skills/jfrog-mcp-management/references/persisting-env-vars.md @@ -0,0 +1,74 @@ +# Persisting environment variables + +Read this for **shell-based harnesses (Claude Code, Cursor)** when a Step 3 +input needs to be exported so its value reference (`${VAR}` for Claude Code, +`${env:VAR}` for Cursor) resolves — i.e. any secret, or a non-secret you chose +to keep out of the config as a reference. (VS Code does not use shell env for +this — it prompts for `inputs` values and stores them itself; skip this file.) + +These references resolve from the shell that launched the agent, so the variable +has to be exported in that shell and persisted across relaunches. Don't rely on +a fixed list of shells/rc files — detect the syntax family and the actual +startup file the running shell uses, and fall back to asking the user whenever +either is ambiguous. + +## 1. Determine the syntax family + +```bash +echo "$SHELL" +``` + +`$SHELL` reports the user's default *login* shell, which is not necessarily the +shell that launched the agent (e.g. a bash session started from a zsh login +shell). Prefer detecting the actual running/parent shell when you can (e.g. the +process that started Claude); use `$SHELL` only as a fallback, and **ask the +user** whenever the running shell — or its startup file — can't be determined +unambiguously. + +- Basename ends in `sh` (`bash`, `zsh`, `ksh`, `dash`, `ash`, `sh`, ...) or any + other POSIX-compatible shell → **POSIX family**: `export VAR_NAME=""`. + This covers virtually every Unix shell except fish, so don't special-case + bash vs. zsh vs. anything else in this family — the export syntax is + identical. +- Basename is `fish` → **fish family**: `set -gx VAR_NAME ""`. +- No `$SHELL` (native Windows session, PowerShell/CMD) → **Windows**: for + **non-secret** values persist with `setx VAR_NAME ""` (sets it for + future sessions; the current one still needs the in-session equivalent, + `$env:VAR_NAME` / `set VAR_NAME`). Do **not** use `setx` for secrets — it + puts the value on the command line (visible in process listings / command + history). For secret values, direct the user to set it via the Windows + environment-variable UI (System Properties → Environment Variables) or a + secret manager, and keep the in-session example session-scoped. +- Anything that doesn't clearly match one of the above → ask the user which + family applies rather than guessing. + +## 2. Find the startup file to persist it in + +Start from the family's canonical default, then verify it's actually the file +in play before writing to it: + +| Family | Canonical default | +|--------|-------------------| +| POSIX (bash) | `~/.bashrc` (macOS login shells, e.g. Terminal.app, instead read `~/.bash_profile`, which usually sources `~/.bashrc`) | +| POSIX (zsh) | `~/.zshrc` | +| POSIX (other: ksh, dash, ash, sh, ...) | ask the user — these don't have one universal convention | +| fish | `~/.config/fish/config.fish` | +| Windows | persistent user env (`setx`), no file to edit | + +- **Verify before writing**, don't assume the default is correct: `test -f + && echo exists`. If it's missing, or a dotfiles manager / + framework (oh-my-zsh, starship, chezmoi, etc.) is in play — which often + generates or `source`s rc files from elsewhere — a hardcoded guess can + silently miss the file the shell actually reads. Ask the user to confirm or + name the right file rather than writing blind. +- **If in doubt at any point, ask the user directly** which file to edit — do + not silently pick one from memory of "common" shells. + +## Rules + +- **Security:** NEVER take secrets in the chat, echo them back, or write raw + secret values into a config file. For secret values, instruct the user to add + the line themselves (e.g. via `read -rs VAR_NAME && export VAR_NAME` for the + current session) — you never see or type the value. +- After exporting, the user must **relaunch the agent** so `${VAR}` + references resolve. diff --git a/plugins/jfrog/skills/jfrog-mcp-management/references/runtime-permissions.md b/plugins/jfrog/skills/jfrog-mcp-management/references/runtime-permissions.md new file mode 100644 index 0000000..39a2570 --- /dev/null +++ b/plugins/jfrog/skills/jfrog-mcp-management/references/runtime-permissions.md @@ -0,0 +1,19 @@ +# Runtime permissions + +The Step 0 Agent Guard check and the agent guard commands make outbound HTTPS +calls, and some operations also write under `~/.jfrog/`. Grant the matching +runtime access, or the commands fail (`Forbidden`, empty output) or the Step 0 +check returns a false "disabled" result. + +| Operation | What it needs | +| --- | --- | +| Step 0 check, `--inspect`, `--list-available` | Network: outbound HTTPS to the npm registry and the JFrog platform | +| OAuth `--login`, removing a cached entry | Network + write access to `~/.jfrog/` (`jfrogmcp.conf.json`) | + +How that access is granted depends on the agent. Some agents (e.g. Claude Code) +read the skill's optional `allowed-tools` frontmatter to pre-approve the +specific commands the skill runs, so the user is not prompted per call; others +prompt for approval or use their own permission model. Either way the skill +works — an agent that does not honor `allowed-tools` just asks the user to +approve the command. Do NOT treat `allowed-tools` as the permission mechanism; +it is only a convenience where supported. diff --git a/plugins/jfrog/skills/jfrog-mcp-management/scripts/jfrog-agent-guard-check.mjs b/plugins/jfrog/skills/jfrog-mcp-management/scripts/jfrog-agent-guard-check.mjs new file mode 100644 index 0000000..0024321 --- /dev/null +++ b/plugins/jfrog/skills/jfrog-mcp-management/scripts/jfrog-agent-guard-check.mjs @@ -0,0 +1,270 @@ +#!/usr/bin/env node +// Copyright (c) JFrog Ltd. 2026 +// Licensed under the Apache License, Version 2.0 +// https://www.apache.org/licenses/LICENSE-2.0 +// +// JFrog Agent Guard activation check. +// +// Silent gate used by Step 0 of the jfrog-mcp-management skill. Determines +// whether the JFrog Agent Guard is enabled for the current environment. +// +// Contract: +// - exit 0 -> Agent Guard ENABLED (skill may proceed) +// - exit 2 -> reachable but the platform has the MCP registry DISABLED +// (skill should tell the user to contact their admin / IT) +// - exit 1 -> DISABLED for any other reason: no credentials, timeout, +// network/DNS error (skill must silently abort) +// - a single human-readable reason line is written to stdout for +// diagnostics only; Step 0 keys off the EXIT CODE, not the text. +// +// Set JF_AGENT_GUARD_DEBUG=true for verbose tracing on stderr. + +import { execFileSync } from "node:child_process"; +import process from "node:process"; + +const SETTINGS_PATH = + "/ml/core/api/v1/administration/account-settings/mcp_gateway_plugin_enabled"; +const REQUEST_TIMEOUT_MS = 5000; + +const debugEnabled = process.env.JF_AGENT_GUARD_DEBUG === "true"; +const debug = (message) => { + if (debugEnabled) console.error(`[jfrog-agent-guard] ${message}`); +}; + +// New JFROG_* env vars take precedence over the legacy JF_* names. +const env = (newName, oldName) => + process.env[newName] ?? (oldName ? process.env[oldName] : undefined); + +const enabled = (reason) => { + process.stdout.write(`Enabled: ${reason}\n`); + process.exit(0); +}; + +const disabled = (reason) => { + process.stdout.write(`Disabled: ${reason}\n`); + process.exit(1); +}; + +// Reachable platform that reports the MCP registry turned off. Distinct exit +// code so the skill can tell the user to contact their admin / IT. +const registryDisabled = (reason) => { + process.stdout.write(`RegistryDisabled: ${reason}\n`); + process.exit(2); +}; + +// Resolve credentials from Path A (environment variables) or Path B +// (the default JFrog CLI configuration). Returns { baseUrl, token, source } +// or null when neither path yields a usable URL + access token. +function resolveCredentials() { + const explicitServerId = process.argv[2]; + // With an explicit server ID, try the named jf-config server FIRST so the + // gate checks THAT JPD, not the ambient default. But if it does not resolve + // (server not in jf config, jf absent/old), fall back to env credentials + // rather than reporting a false "disabled" — the platform may be fully + // reachable via exported JFROG_URL + token even with no matching jf server. + if (explicitServerId) { + const fromCli = resolveFromCliConfig(); + if (fromCli) return fromCli; + debug( + "Explicit server ID did not resolve via jf config; falling back to env credentials.", + ); + } + + // Path A — environment variables. + const envUrl = env("JFROG_URL", "JF_URL"); + const envToken = env("JFROG_ACCESS_TOKEN", "JF_ACCESS_TOKEN"); + if (envUrl && envToken) { + debug("Using credentials from environment variables (Path A)."); + return { baseUrl: envUrl, token: envToken, source: "environment variables" }; + } + debug( + "Environment credentials incomplete; trying JFrog CLI config (Path B).", + ); + + // Path B — default server from the local JFrog CLI configuration. If an + // explicit ID was given we already tried the CLI above (and env fell through), + // so there is nothing left to resolve. + if (explicitServerId) return null; + return resolveFromCliConfig(); +} + +function resolveFromCliConfig() { + // `jf config export [server ID]` emits the server as a base64-encoded JSON + // blob containing url, accessToken, and serverId. An optional server ID may + // be passed as argv[2]; without it the CLI's default server is used. We use + // the CLI rather than reading ~/.jfrog/jfrog-cli.conf.v6 directly because + // newer CLIs do not persist the access token in that file (and the platform + // URL may be stored only as an /artifactory-suffixed URL there, which is + // wrong for /ml/core). + const serverId = process.argv[2]; + const exportArgs = serverId ? ["config", "export", serverId] : ["config", "export"]; + let exported; + try { + exported = execFileSync("jf", exportArgs, { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + timeout: 2000, + }).trim(); + } catch (error) { + debug( + `'jf config export' failed (jf not on PATH or no server configured): ${error?.message}`, + ); + return null; + } + + let cfg; + try { + cfg = JSON.parse(Buffer.from(exported, "base64").toString("utf8")); + } catch (error) { + debug(`Could not decode the jf config export token: ${error?.message}`); + return null; + } + + // `url` is the platform/JPD root — the base the /ml/core settings path needs. + const baseUrl = cfg?.url; + const token = cfg?.accessToken; + if (!baseUrl) { + debug("Exported JFrog CLI config has no platform URL."); + return null; + } + if (!token) { + debug("Exported JFrog CLI config has no access token (bearer auth needed)."); + return null; + } + + const id = cfg?.serverId ?? "default"; + return { baseUrl, token, source: `JF CLI config (server '${id}')` }; +} + +async function isGatewayPluginEnabled(baseUrl, token) { + // Normalize to the platform root: drop trailing slashes and a trailing + // `/artifactory` segment. Users commonly export JFROG_URL as + // `https://myco.jfrog.io/artifactory`, but the settings path lives under + // `/ml/core` off the platform root — without this, Path A would build + // `.../artifactory/ml/core/...` and 404 into a false "disabled" (exit 1). + const root = baseUrl.replace(/\/+$/, "").replace(/\/artifactory$/, ""); + const url = root + SETTINGS_PATH; + debug(`Fetching gateway plugin setting from ${url}`); + + // Trade-off: we use a direct fetch() rather than `jf api` (the pattern other + // scripts in this repo use for authenticated JFrog REST calls) because this + // gate keys off exact HTTP status codes — 200+value:false vs 401/403 vs + // unreachable each map to a different exit code — and parsing `jf api`'s + // "[Warn] ... returned NNN" / "Http Status: NNN" stderr convention for that + // is brittle. The cost: this call does NOT inherit any corporate-proxy or + // custom-CA settings baked into the user's `jf` config, so an env that only + // works through jf's transport can surface here as an unreachable/timeout + // (exit 1). If that becomes common, switch to `jf api` and parse its status. + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); + try { + const response = await fetch(url, { + method: "GET", + headers: { + Accept: "application/json", + Authorization: `Bearer ${token}`, + }, + signal: controller.signal, + }); + if (!response.ok) { + debug(`Settings request returned HTTP ${response.status}.`); + // Non-OK (incl. 401/403) means an auth/permission/transport problem, NOT + // a deliberately-disabled registry — stay silent (exit 1) rather than + // sending the user to IT. Only HTTP 200 + value:false is "disabled". + return { + ok: false, + reason: `settings endpoint returned HTTP ${response.status}`, + }; + } + const data = await response.json(); + // Be tolerant about where and how the flag is carried, so a shape/casing + // change on the platform side can't turn a genuinely-enabled registry into + // a false "disabled" (exit 1). The endpoint URL already names the setting + // (`.../mcp_gateway_plugin_enabled`), so the body may arrive as any of: + // - `{ settings: { mcpGatewayPluginEnabled: } }` (wrapped); + // - the same at the top level, un-wrapped; + // - `{ value: }` (bare wrapper, key implied by the URL); + // - a bare boolean `true` / `false`. + // Casing: the path segment is snake_case while JFrog JSON bodies are + // typically camelCase — accept either. + const unwrap = (v) => + v !== null && typeof v === "object" ? v?.value : v; + const container = data?.settings ?? data; + const named = + container?.mcpGatewayPluginEnabled ?? + container?.mcp_gateway_plugin_enabled; + // `named` first (explicit key), then the bare wrapper / bare boolean forms. + const value = + typeof data === "boolean" + ? data + : named !== undefined + ? unwrap(named) + : unwrap(container); + debug(`Settings response indicates gateway plugin enabled=${value}.`); + if (value === true) return { ok: true }; + if (value === false) { + return { + ok: false, + registryOff: true, + reason: "mcp gateway plugin setting returned false", + }; + } + return { + ok: false, + reason: "settings endpoint returned an invalid gateway-plugin setting", + }; + } catch (error) { + const reason = + error?.name === "AbortError" ? "timeout" : error?.message ?? "unknown error"; + debug(`Settings request failed: ${reason}`); + return { ok: false, reason: `settings endpoint unreachable (${reason})` }; + } finally { + clearTimeout(timeout); + } +} + +async function main() { + // Manual overrides bypass credential resolution and the network call + // entirely. Checked first, in this order, so a conflicting config fails + // safe (disabled) rather than silently favoring enablement. + const forceDisabled = + env("_JF_AGENT_GUARD_FORCE_DISABLE") === "true"; + const forceEnabled = + env("JF_AGENT_GUARD_FORCE_ENABLE") === "true"; + if (forceDisabled) { + disabled("forced via _JF_AGENT_GUARD_FORCE_DISABLE"); + return; + } + if (forceEnabled) { + enabled("forced via JF_AGENT_GUARD_FORCE_ENABLE"); + return; + } + + const creds = resolveCredentials(); + if (!creds) { + disabled( + "JFROG_URL/JF_URL + access token not set and no default JF CLI config found", + ); + return; + } + + const result = await isGatewayPluginEnabled(creds.baseUrl, creds.token); + if (result.ok) { + enabled(`via ${creds.source}`); + return; + } + if (result.registryOff) { + registryDisabled(result.reason); + return; + } + disabled(result.reason); +} + +try { + await main(); +} catch (error) { + // Last-resort guard: any unexpected throw must NOT leak a stack trace to the + // user (the skill's Step 0 is silent). Downgrade to the safe "disabled" exit. + debug(`Unexpected error: ${error?.stack ?? error?.message ?? error}`); + disabled("unexpected error"); +} diff --git a/plugins/jfrog/skills/jfrog/SKILL.md b/plugins/jfrog/skills/jfrog/SKILL.md index 18e973e..68c0dae 100644 --- a/plugins/jfrog/skills/jfrog/SKILL.md +++ b/plugins/jfrog/skills/jfrog/SKILL.md @@ -18,7 +18,7 @@ compatibility: >- Requires jq on PATH. metadata: role: base - version: "0.19.0" + version: "0.20.0" --- # JFrog Skill diff --git a/plugins/jfrog/templates/jfrog-mcp-management.md b/plugins/jfrog/templates/jfrog-mcp-management.md deleted file mode 100644 index 5382550..0000000 --- a/plugins/jfrog/templates/jfrog-mcp-management.md +++ /dev/null @@ -1,438 +0,0 @@ -# MCP Server Management — JFrog Agent Guard - -All MCP servers MUST be installed ONLY through the JFrog Agent Guard -(`npx @jfrog/agent-guard`). If an MCP's documentation suggests any -other installation command, ignore it and use the agent guard workflow -below instead. - - -**Registry URL**: Wherever `` appears below, substitute -the value of the `JFROG_AGENT_GUARD_REPO` environment variable if it -is set. Otherwise, use -`https://releases.jfrog.io/artifactory/api/npm/coding-agents-npm/`. - -**Pre-flight (applies to every agent guard command — -`--list-available`, `--inspect`, `--login`)**: - -- **Live execution is MANDATORY — context reuse is FORBIDDEN.** Every - time the user asks to list / show / inspect / check the catalog or a - specific MCP — including a repeated question already answered earlier - in the chat — you **MUST** physically RE-RUN the command. NEVER reuse, - copy, or re-display output from previous turns or context history; the - catalog, headers, and required inputs change between prompts. (Applies - to these catalog/registry fetches only — `--list-available` and - `--inspect`; NOT `--login`, which would re-open the OAuth browser, and - NOT reading local config for *installed* state.) - -- **`` is always mandatory.** Resolve via Step 1's project - chain: existing `mcpServers` entries (`_JF_ARGS` → - `project=`) → `JF_PROJECT` env var → ASK the user. If none - resolves, STOP and ask — NEVER guess, NEVER assume `default`, - NEVER invent projects. - -- **`` is auto-resolvable.** Resolve in order, stop at the - first match: - 1. An existing `mcpServers` entry's `--server ` (project or user - config) — reuse it. - 2. `JFROG_URL` + `JFROG_ACCESS_TOKEN` set in the env — use them and do - NOT pass `--server` (the agent guard reads the env directly). - 3. List configured servers with the jf CLI — `jf config show --format=json` - (do NOT parse `~/.jfrog/jfrog-cli.conf.v6`; the CLI masks tokens, so - its output is safe). Exactly one → use it; two or more → use the one - with `"isDefault": true`; if none is marked default → ASK the user - which one. Then pass `--server `. - 4. None of the above → ask the user to run `jf c add ` or export - `JFROG_URL` + `JFROG_ACCESS_TOKEN`, then retry. - - When you resolved the ID from a jf CLI config, always pass it as - `--server `; when using env vars, never pass `--server`. -- The commands need network access and MUST be run with `full_network` - permissions when run in a sandbox. Otherwise `Forbidden` errors will - be thrown. - -Once both are determined, proceed. If either is still unknown, -STOP — do NOT run the command with guesses. - -## Adding an MCP - -**Did the user name a specific MCP package?** ("add `foo-mcp`", -"install `@scope/bar`"). If NOT — they said something like "yes", -"add an MCP", "what can I install" — your FIRST action is to show -them the catalog so they can pick: - -1. Resolve server (Server ID `` or URL `JFROG_URL`) - and `` per the Pre-flight rule at the top of this document. - Server: auto-use the single jf CLI configs serverId as the server ID - or the `JFROG_URL` env var as the URL if unambiguous; only ask when - there are multiple or no jf configs and no env vars. - Project: Ask unless `JF_PROJECT` is set, or it's already in an - existing `mcpServers` entry. -2. Run "Listing MCPs > Available to install" with that server + - project and present the result as a numbered table. -3. Wait for the user to pick. Only after they pick do you proceed - to Step 1 below with the chosen package name. - -NEVER ask "which package would you like?" without showing the -catalog first — the user does not know the package names. - -Once you have a specific MCP package name, do ALL of the following -autonomously — do NOT ask for project, server, or package name -unless absolutely necessary: - -### Step 1: Determine project, server, and target config file - -**Server ID** - -1. Any existing `mcpServers` entry in `.cursor/mcp.json` (project) - or `~/.cursor/mcp.json` (user) — take the value after `--server` - in `args`. -2. Else `JFROG_URL` env var set (with `JFROG_ACCESS_TOKEN`) — the - agent guard can resolve credentials from these directly; - DO NOT pass `--server` as that would make the agent guard try to - parse the server details from the jf cli configuration. -3. Else list configured servers with the jf CLI — run - `jf config show --format=json` (do NOT parse - `~/.jfrog/jfrog-cli.conf.v6` yourself; the CLI masks tokens, so its - output is safe to read). From the result: - - exactly one server → use it without asking. - - two or more → use the one with `"isDefault": true`; if none is - marked default, list the `serverId`s and ASK the user which one. -4. Else (file missing, empty, or unreadable, and no `JFROG_URL`) - ask the user to either run `jf c add ` or export - `JFROG_URL` + `JFROG_ACCESS_TOKEN`, then retry. - -NEVER try multiple servers — pick one. When you resolved the ID from a -jf CLI config, always pass it as `--server ` in every agent guard -invocation; when using env vars, never pass `--server`. - -**Project** - -1. From existing `mcpServers` entries, `_JF_ARGS` → - `project=` value. -2. Else `JF_PROJECT` env var. -3. Else ask. NEVER guess, NEVER assume "default", NEVER use the server ID, - NEVER infer the project from other sources, NEVER make up projects, - ALWAYS ask. - -**Target config file** - -- **Default: `.cursor/mcp.json` in the project root.** Create it if - missing (`{ "mcpServers": {} }`). Shareable via git. -- Use `~/.cursor/mcp.json` ONLY if the user says "personal only" / - "do not commit". -- Do not ask which scope unless the user brings it up. - -### Step 2: Inspect the MCP in the catalog - -Step 2 needs a specific MCP name. If the user did NOT name one, do -not call `--inspect` — go to "Listing MCPs > Available to install" -instead, show the catalog, have them pick, then come back to Step 2 -with the chosen name. - -Once you have a name, run a SINGLE command — no Fetch/WebFetch, no -custom curl/Python, no direct JFrog API calls: - -``` -npx --yes \ - --registry \ - @jfrog/agent-guard \ - --inspect \ - --server \ - --project \ - --mcp -``` - -From the output JSON, extract (keep BOTH required AND optional): - -- `spec.packageName` — exact package name for the config. -- `spec.mcpServerType.local.bootParams.environmentVariables[]` for - local MCPs (each has `name`, `description`, `isRequired`, `isSecret`). -- `spec.mcpServerType.remote.endpoints[].headers[]` for remote MCPs - (each has `name` plus `mcpInput.mcpInputDetails` with the same - fields). - -On non-zero exit (typo, MCP not in catalog, network error, etc.), -show the error verbatim, then run `--list-available` (see "Listing -MCPs") so the user can pick a valid name and retry. - -### Step 3: Plan inputs - -Every `env` value is either a literal or a `${env:VAR_NAME}` -reference resolved from the shell that launched Cursor — there is -no interactive secret prompt. - -Split Step 2 inputs by `isRequired`: - -1. **Required** — always include in Step 4. -2. **Optional** — if even ONE exists, STOP and ask. List required - inputs first (informational), then each optional one by name + - description and ask which to configure. Do NOT decide for the - user. -3. No inputs → skip this step. - -For each input in Step 4: - -- **Secrets** (`isSecret=true`): use `${env:VAR_NAME}` in the config; - tell the user to export it for the current session via - `read -rs VAR_NAME && export VAR_NAME && echo exported`. - For persistence, the right startup file depends on the user's - **shell**, not their OS — macOS and Linux both commonly run zsh or - bash. Detect the shell (e.g. `echo "$SHELL"`) and add the export to - the file that shell loads on startup: - - **zsh** (the macOS default) → `~/.zshrc` - - **bash** → `~/.bashrc`; note macOS login shells read - `~/.bash_profile`, which usually sources `~/.bashrc` - - **fish** → `~/.config/fish/config.fish` (use `set -gx`) - - **Windows** → use `setx VAR_NAME ""` (PowerShell/CMD) - instead of the `read`/`export` snippet - If unsure which file the shell sources, ask the user. Values are - picked up on next launch (4a). NEVER take secrets in chat, echo them - back, or write raw values into config. -- **Non-secrets**: literal in `env` or `${env:VAR_NAME}` — ask if - unclear. - -### Step 4: Write the config entry - -Add the entry under `mcpServers` in the target config (default -`.cursor/mcp.json` — see Step 1). -**Both `--yes` and `--registry ` MUST come BEFORE -`@jfrog/agent-guard`** or `npx` falls back to the default -registry (404) and may block on a no-TTY prompt. Use -`"type": "stdio"` — never `"http"`, `"sse"`, or a top-level `"url"` -(those bypass the agent guard). - -```json -{ - "mcpServers": { - "": { - "type": "stdio", - "command": "npx", - "args": [ - "--yes", - "--registry", - "", - "@jfrog/agent-guard", - "--server", - "" - ], - "env": { - "_JF_ARGS": "project=&mcp=", - "": "${env:}" - } - } - } -} -``` - -Notes: - -- If a required `${env:VAR}` is unset, the agent guard fails at startup. - Confirm the user exported it before they restart. - If any env vars are missing, ASK the user to export them and restart Cursor. -- For `Bearer`-prefixed headers, either include the prefix in the env - var or hard-code it: `"Bearer ${env:TOKEN}"`. - -### 4a: Enable and verify the entry (mandatory) - -Adding the entry to `mcp.json` is not enough — Cursor stores -enable/approval state separately and does not auto-enable new -servers in workspace level installations. User level installations -often do get auto enabled. - -ALWAYS ask the user to verify the installation and enable the installed -MCP in the Cursor settings under "Tools & MCPs" via the UI toggle. - -**Verify (mandatory):** Being able to discover the MCP is not enough. -`cursor agent mcp list` and `cursor agent mcp enable` are not authoritative for -the cursor IDE. Do not trust these commands as proof for the MCP working or -not working. The only proof is to see if the **tool descriptors are actually -present** in -`~/.cursor/projects//mcps//tools/*.json` (the -mcp-server-name is the JSON key of the mcp, optionally prefixed `user-`). -NEVER ask the user to verify the tool descriptor files. ALWAYS Offer to check the -tools for the user after they enabled the MCP. -If `tools/` is empty (or the directory is missing) after a `Developer: Reload -Window`, treat as Failed and follow Troubleshooting "`ready` but 0 -tools". - -### Step 5: Authenticate OAuth MCPs (auto, after Step 4) - -Run ONLY for OAuth-style remote MCPs — i.e. `--inspect` showed a -`remote` section with `type: "http"` AND Step 4 wrote no static auth -header into `env`. Skip for local MCPs and for remote MCPs whose -auth comes from a static token in `env`. - -`--login` opens the browser, runs OAuth, caches tokens in -`~/.jfrog/jfrogmcp.conf.json`. Warn the user "I'm going to open your -browser to sign you in to ``" before: - -``` -npx --yes \ - --registry \ - @jfrog/agent-guard \ - --login \ - --server \ - --project \ - --mcp -``` - -Note: This must run with `all` permissions when run in a sandbox. - -Outcomes: - -- **Exit 0** — OAuth completed; tokens cached; server ready. -- **`expected 401, got 200`** — MCP is anonymous (no auth needed); - ignore. -- **Any other error** — paste it to the user verbatim and stop. - -## Removing an MCP - -1. Delete the entry from `mcpServers` in the file it was installed - in (`.cursor/mcp.json` or `~/.cursor/mcp.json`). -2. If OAuth was used (Step 5), also remove its entry from - `~/.jfrog/jfrogmcp.conf.json`. -3. Tell the user to reload Cursor (`Developer: Reload Window`) so - the removed entry stops loading (`mcp.json` is read at session - start only). - -## Listing MCPs - -**Route the request first** — pick which subsection to run BEFORE -touching any file or shell: - -| User said… | Run | -| --- | --- | -| "available", "what can I install", "what's in the catalog", "list MCPs" without other context | **Available to install** below — go straight to `--list-available`; do NOT inspect local files first | -| "installed", "configured", "connected", "running", "what MCPs do I have" | **Currently installed** below | -| ambiguous / both | run **both** subsections in order: Currently installed first, then Available to install, and present them as separate tables | - -NEVER invent MCP integrations from outside the catalog. The only -authoritative source for what's available is `--list-available` -against the configured server + project. If that command returns -nothing or errors, say so — do not pad the answer with names from -elsewhere. - -### Currently installed - -1. Run `cursor agent mcp list` for connection status (one row per - server). -2. For JFrog metadata, read `mcpServers` directly from - `.cursor/mcp.json` (project) and `~/.cursor/mcp.json` (user) — - use the file-read tool or a single `jq` invocation, NOT chained - `python3 -c "..."` pipes. For each entry whose `command` is `npx` - and whose `args` include `@jfrog/agent-guard`, show: display name - (the JSON key), package (`mcp=` in `_JF_ARGS`), server - ID (value after `--server`), scope (project / user). -3. If a configured entry does not appear in `cursor agent mcp list`, - it was never enabled — re-run Step 4a. - -### Available to install - -1. Determine **server** and **project** per the Pre-flight rule at - the top of this document. `--list-available` does NOT require - any existing `mcpServers` entry or pre-installed agent guard — - `npx --yes` fetches the agent guard on demand, so this works on a - fresh machine too. -2. Run EXACTLY this command — `--project` is passed as a CLI flag - To configure the server, either use the serverId from a jf cli - config with `--server` or omit `--server` if env vars are used to - configure URL and Access Token. **no additional env vars needed**: - -``` -npx --yes \ - --registry \ - @jfrog/agent-guard \ - --list-available \ - --project \ - [--server ] -``` - -The output is a compact TSV: a header line, then one server per line, -tab-separated: `nametypeversiondescription`. -Run the command ONCE and present the rows directly as a numbered -table — do NOT re-run it, redirect it, or parse it with `python3`/`jq`. -The `name` column is the install identifier (the value you pass to -`--inspect --mcp` and to install); `packageName` is NOT a separate -column — for remote/http MCPs there is no package name, so `name` is -the display name. - -3. Filter out any `name` already present in the installed list - (compare against `mcp=` in `_JF_ARGS`). Mark the rest as - available to install. - -## Key Rules - -- **Package scope is case-sensitive — ALWAYS write it lowercase as - `@jfrog/agent-guard`, NEVER `@JFrog/agent-guard`.** npm scopes are - case-sensitive; the published package is the lowercase - `@jfrog/agent-guard`. Capitalizing the brand (`@JFrog`) points at a - different/nonexistent scope and breaks the command. Use the exact - lowercase string in every command and config entry. -- **`npx` arg order:** `--yes`, `--registry `, - `@jfrog/agent-guard`, then agent guard flags. Both `--yes` and - `--registry` MUST precede the package name or `npx` falls back to - the default registry (404) and may block on a no-TTY prompt. -- **Always `"type": "stdio"`** pointing at `npx @jfrog/agent-guard`, - even for remote-only catalog MCPs (the agent guard proxies them). - `"http"`, `"sse"`, or a top-level `"url"` bypass the agent guard. -- `_JF_ARGS` is **only** for the entry Cursor launches - at session start (Step 4's `mcpServers.*.env`); MUST contain - `project=&mcp=`. - NEVER pass `_JF_ARGS` to `--list-available`, - `--inspect`, or `--login` — those take `--server` / `--project` - as CLI flags only. -- NEVER assume `default` as a project name. If the project is unknown - after Step 1's chain (existing `mcpServers` entries → `JF_PROJECT` - env var), STOP and ask the user. Same for server ID if used. - NEVER invent or guess projects or server IDs. -- Package name MUST come from the catalog (`--inspect` / - `--list-available`). NEVER guess. NEVER install MCPs outside the - agent guard. NEVER use Fetch/WebFetch for catalog calls. -- NEVER pipe a catalog command through `python3`, and NEVER capture it - with `2>&1` — `npx`/`npm` writes progress to stderr, which corrupts - the output stream. For `--list-available` present the compact TSV it - prints; for `--inspect` read the JSON it prints on stdout - directly (or with a single `jq` filter), never via `python3`. -- NEVER write a raw secret into `mcp.json` — always use - `${env:VAR_NAME}`. NEVER show tokens / API keys. - - NEVER try multiple servers — ask the user to pick one. - -## Troubleshooting - -- **`ready` but 0 tools (empty `mcps//tools/` after a - Command Palette `Developer: Reload Window`)** — agent guard proxy - started, upstream MCP did not. The top-level `ready` label is - misleading here. NEVER report success when there are 0 tools. - 1. Open Cursor's MCP / Output panel for the - agent guard stderr; diagnose by MCP type: - - **OAuth (remote)** — re-run Step 5 (`--login`); refresh token - likely expired. - - **Static-token (remote)** — confirm every `${env:VAR}` in `env` - is exported in the shell that launched Cursor and the token is - still valid. - - **Local (stdio)** — check that the bundled binary actually - launched (agent guard stderr will show the spawn error). - 2. Verify that the mcp server is still allowed. - See "Listing MCPs > Available to install". -- **`mcp.json` server missing from `cursor agent mcp list` / - Tools & MCP** — never enabled. Re-run Step 4a - (`cursor agent mcp enable `); if the entry is brand-new, - also `Developer: Reload Window` so Cursor picks up the file. -- **Agent Guard: `multiple/no JFrog server configured`** (the agent guard - cannot pick a JFrog server) — pass `--server ` (after - `jf c add `) OR export both `JFROG_URL` and - `JFROG_ACCESS_TOKEN` in the launching shell, then relaunch Cursor. -- **OAuth MCP failing** — refresh token expired; re-run Step 5. -- **401/403 with `${env:VAR}`** — env var unset/wrong; re-export in - the launching shell and relaunch Cursor. -- **`cursor: command not found`** — the Cursor shell command is not - on `PATH`. Use the absolute binary for `enable` (see Step 4a - resolution order) and tell the user to install the shell command - via `Cmd+Shift+P` → `Shell Command: Install 'cursor' command in - PATH`, or symlink it themselves: - `ln -s /Applications/Cursor.app/Contents/Resources/app/bin/cursor - ~/.local/bin/cursor`. -- **npx package fetch returns 403 in-agent**: Often caused by a - Cursor network sandbox/egress policy. Run with `full_network`. - If it still fails with full_network, troubleshoot - registry/auth/package/curation policy as usual. diff --git a/scripts/validate-hook-injector.mjs b/scripts/validate-hook-injector.mjs deleted file mode 100644 index fd488bc..0000000 --- a/scripts/validate-hook-injector.mjs +++ /dev/null @@ -1,199 +0,0 @@ -#!/usr/bin/env node - -// Copyright (c) JFrog Ltd. 2026 -// Licensed under the Apache License, Version 2.0 -// https://www.apache.org/licenses/LICENSE-2.0 - -// Smoke test for the sessionStart injector + plugin packaging, grouped into: -// Syntax — the injector exists and parses. -// Lint — plugin.json / hooks.json / template wiring is internally -// consistent (name, paths). -// Format — running the injector emits a well-formed sessionStart -// payload (valid JSON, correct shape). -// Injection logic — the payload actually carries the real template, and -// fail-closed paths emit {}. -// A template-filename / read-path mismatch makes the injector silently emit -// nothing (it catches the read error and exits 0); these checks turn that -// silent failure into a hard error. - -import { execFileSync, spawnSync } from "node:child_process"; -import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import process from "node:process"; -import { fileURLToPath } from "node:url"; - -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const pluginDir = path.join(repoRoot, "plugins", "jfrog"); -const injector = path.join(pluginDir, "scripts", "inject-instructions.mjs"); -const templatesDir = path.join(pluginDir, "templates"); -const hooksFile = path.join(pluginDir, "hooks", "hooks.json"); -const pluginManifestFile = path.join(pluginDir, ".cursor-plugin", "plugin.json"); - -const failures = []; - -function section(title) { - console.log(`\n${title}`); -} - -function check(label, fn) { - try { - fn(); - console.log(` ok ${label}`); - } catch (error) { - failures.push(label); - console.log(` FAIL ${label}\n ${error.message}`); - } -} - -// Run the injector with a clean copy of the env plus the given overrides, so an -// inherited force-flag or real JFrog credentials can't skew the result. -function runInjector(overrides) { - const env = { ...process.env }; - delete env._JF_AGENT_GUARD_FORCE_DISABLE; - delete env.JF_AGENT_GUARD_FORCE_ENABLE; - return execFileSync(process.execPath, [injector], { - encoding: "utf8", - env: { ...env, ...overrides }, - }); -} - -// Same as runInjector, but also captures stderr and clears any JFrog env vars -// so the jf-CLI fallback in resolveCredentials() is actually reachable. -function runInjectorWithDebug(overrides) { - const env = { ...process.env }; - delete env._JF_AGENT_GUARD_FORCE_DISABLE; - delete env.JF_AGENT_GUARD_FORCE_ENABLE; - delete env.JFROG_URL; - delete env.JF_URL; - delete env.JFROG_ACCESS_TOKEN; - delete env.JF_ACCESS_TOKEN; - const result = spawnSync(process.execPath, [injector], { - encoding: "utf8", - env: { ...env, JF_AGENT_GUARD_DEBUG: "true", ...overrides }, - }); - return { stdout: result.stdout ?? "", stderr: result.stderr ?? "" }; -} - -// Stubs a fake `jf` executable on PATH that emits the given config token, so -// the CLI-fallback path can be exercised without a real JFrog CLI install. -function withFakeJfOnPath(configToken, fn) { - const bin = mkdtempSync(path.join(tmpdir(), "fake-jf-")); - const jfPath = path.join(bin, "jf"); - writeFileSync(jfPath, `#!/usr/bin/env node\nprocess.stdout.write(${JSON.stringify(configToken)});\n`); - chmodSync(jfPath, 0o755); - try { - return fn(`${bin}${path.delimiter}${process.env.PATH}`); - } finally { - rmSync(bin, { recursive: true, force: true }); - } -} - -function main() { - console.log("Validating sessionStart injector + plugin packaging…"); - - // ---- Syntax: the injector exists and is parseable JS ---- - section("Syntax"); - check("injector source exists", () => { - if (!existsSync(injector)) throw new Error(`missing: ${injector}`); - }); - check("injector parses (node --check)", () => { - execFileSync(process.execPath, ["--check", injector], { stdio: "pipe" }); - }); - - // ---- Lint: manifest, hook wiring, and template read-path are consistent ---- - section("Lint (manifest & wiring)"); - - check("plugin.json is named the jfrog plugin", () => { - const pluginManifest = JSON.parse(readFileSync(pluginManifestFile, "utf8")); - if (pluginManifest.name !== "jfrog") { - throw new Error(`plugin.json name "${pluginManifest.name}" is not "jfrog"`); - } - if (!/^\d+\.\d+\.\d+$/.test(pluginManifest.version ?? "")) { - throw new Error(`plugin.json version is missing or not semver: ${JSON.stringify(pluginManifest.version)}`); - } - }); - - check("hooks.json wires sessionStart to the injector", () => { - const hooks = JSON.parse(readFileSync(hooksFile, "utf8")); - const entries = hooks?.hooks?.sessionStart; - if (!Array.isArray(entries) || entries.length === 0) { - throw new Error("hooks.json has no sessionStart hooks"); - } - const commands = entries.map((e) => e.command ?? ""); - if (!commands.some((c) => c.includes("inject-instructions.mjs"))) { - throw new Error("no sessionStart command references inject-instructions.mjs"); - } - }); - - // The filename the injector reads must match a real, non-empty template. - let templateName; - check("injector reads an existing template file", () => { - const src = readFileSync(injector, "utf8"); - const match = src.match(/"templates"\s*,\s*"([^"]+)"/); - if (!match) throw new Error("could not find the templates/ read path in the injector"); - templateName = match[1]; - const templatePath = path.join(templatesDir, templateName); - if (!existsSync(templatePath)) { - throw new Error(`injector reads "${templateName}" but it does not exist in plugins/jfrog/templates/`); - } - if (statSync(templatePath).size === 0) { - throw new Error(`template "${templateName}" is empty`); - } - }); - - // ---- Format: force-enable emits a well-formed sessionStart payload ---- - section("Format (injected payload shape)"); - let injectedContext; - check("force-enable emits valid JSON with a non-empty additional_context", () => { - const stdout = runInjector({ JF_AGENT_GUARD_FORCE_ENABLE: "true", JFROG_URL: "https://example.jfrog.io" }); - if (!stdout.trim()) throw new Error("stdout was empty"); - let payload; - try { - payload = JSON.parse(stdout); - } catch (error) { - throw new Error(`stdout did not parse as JSON: ${error.message}`); - } - if (typeof payload?.additional_context !== "string" || payload.additional_context.trim().length === 0) { - throw new Error("additional_context is missing or empty"); - } - injectedContext = payload.additional_context; - }); - - // ---- Injection logic: the payload is the real template; fail-closed works ---- - section("Injection logic"); - check("force-enable injects the actual template, byte-for-byte", () => { - if (injectedContext === undefined) throw new Error("force-enable payload not captured (see Format check)"); - if (!templateName) throw new Error("template name was not resolved (see Lint check)"); - const expected = readFileSync(path.join(templatesDir, templateName), "utf8"); - if (injectedContext !== expected) { - throw new Error("injected additionalContext does not match the template file content"); - } - }); - // ---- jf CLI fallback: resolveCredentials() falls back to `jf config export` ---- - section("jf CLI fallback"); - check("resolves credentials via 'jf config export' when env vars are unset", () => { - const token = Buffer.from( - JSON.stringify({ url: "https://example.jfrog.io", accessToken: "fake-token", serverId: "test-server" }), - ).toString("base64"); - withFakeJfOnPath(token, (fakePath) => { - const { stderr } = runInjectorWithDebug({ PATH: fakePath }); - if (!stderr.includes("Resolved credentials via 'jf config export'")) { - throw new Error(`expected debug log confirming jf CLI fallback, got:\n${stderr}`); - } - }); - }); - - check("force-disable emits {} (fail-closed)", () => { - const stdout = runInjector({ _JF_AGENT_GUARD_FORCE_DISABLE: "true" }).trim(); - if (stdout !== "{}") throw new Error(`expected "{}", got ${JSON.stringify(stdout)}`); - }); - - if (failures.length > 0) { - console.error(`\n${failures.length} check(s) failed.`); - process.exit(1); - } - console.log("\nAll checks passed."); -} - -main();