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 f10d3df..745e63a 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,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 3aa316a..c7962c9 100644 --- a/plugins/jfrog/.cursor-plugin/plugin.json +++ b/plugins/jfrog/.cursor-plugin/plugin.json @@ -20,11 +20,5 @@ "artifacts", "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" + "logo": "assets/logo.svg" } diff --git a/plugins/jfrog/README.md b/plugins/jfrog/README.md index 88da7e4..be61a51 100644 --- a/plugins/jfrog/README.md +++ b/plugins/jfrog/README.md @@ -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 | ### Skills diff --git a/plugins/jfrog/hooks/hooks.json b/plugins/jfrog/hooks/hooks.json deleted file mode 100644 index 36ff5ae..0000000 --- a/plugins/jfrog/hooks/hooks.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": 1, - "hooks": { - "sessionStart": [ - { - "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/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();