diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 3ddf459..76f7b9e 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.18", + "version": "0.5.19", "pluginRoot": "plugins" }, "plugins": [ diff --git a/.github/scripts/sync-skills-vendor.json b/.github/scripts/sync-skills-vendor.json index d1f57db..bf58e43 100644 --- a/.github/scripts/sync-skills-vendor.json +++ b/.github/scripts/sync-skills-vendor.json @@ -1,6 +1,6 @@ { "repo": "jfrog/jfrog-skills", - "pin": "v0.29.0", + "pin": "v0.31.1", "paths": [ "skills" ] diff --git a/README.md b/README.md index 17633a3..9c088fb 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ JFrog plugin for [Cursor](https://cursor.com): artifact management, security sca ## What's new - **Agent Package Resolution (Preview).** A hook automatically routes the packages your AI agent installs through your JFrog Artifactory instead of public registries. See [Agent Package Resolution](#agent-package-resolution-preview). -- **AI Catalog skill.** New `jfrog-ai-catalog-skills` skill to discover, install, update, and publish agent skills hosted in the JFrog AI Catalog. +- **AI Catalog skill.** New `jfrog-ai-catalog` skill to discover, install, update, and publish agent skills hosted in the JFrog AI Catalog. --- ## Features @@ -16,7 +16,7 @@ The JFrog plugin provides the following capabilities, grouped by component: | --- | --- | --- | | **MCP** | JFrog MCP server | Remote JFrog MCP server auto-attached to every session via `mcp.json` at `https://${JFROG_PLATFORM_URL}/mcp` (OAuth, no API keys). | | **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. | +| **Skill** | Package curation | 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. | | **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. | @@ -29,7 +29,7 @@ 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** (≥ 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). +- **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.31.1/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 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. @@ -92,7 +92,7 @@ Once configured, interact with the JFrog plugin through natural language. Exampl | "Create a scoped access token for CI." | Creates an access token with the requested scope. | | "Promote this release bundle to production." | Uses Lifecycle / Distribution APIs to promote the bundle. | -### Package safety & download skill +### Package curation skill | Ask the agent… | What happens | | --- | --- | diff --git a/VENDOR.md b/VENDOR.md index 11dd963..423f0dd 100644 --- a/VENDOR.md +++ b/VENDOR.md @@ -7,7 +7,7 @@ The skill packages under `plugins/jfrog/skills/` are vendored from **[jfrog/jfro | **Repository** | https://github.com/jfrog/jfrog-skills | | **Pinned release** | see `pin` in [`.github/scripts/sync-skills-vendor.json`](.github/scripts/sync-skills-vendor.json) | -Included directories: `jfrog/`, `jfrog-ai-catalog-skills/`, `jfrog-package-safety-and-download/`, `jfrog-reference-architecture/`, `jfrog-setup-package-managers/` (as of the pinned release). +Included directories: `jfrog/`, `jfrog-ai-catalog/`, `jfrog-package-curation/`, `jfrog-reference-architecture/`, `jfrog-setup-package-managers/` (as of the pinned release). ## Refreshing diff --git a/plugins/jfrog/.cursor-plugin/plugin.json b/plugins/jfrog/.cursor-plugin/plugin.json index 60a1354..c61934d 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.18", + "version": "0.5.19", "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", diff --git a/plugins/jfrog/README.md b/plugins/jfrog/README.md index f32cf8e..f4d612d 100644 --- a/plugins/jfrog/README.md +++ b/plugins/jfrog/README.md @@ -27,14 +27,14 @@ CLI authentication options: run `jf login` for browser-based setup, or set the ` | Skill | Triggers when you mention... | |-------|------------------------------| | **jfrog** | any JFrog product, artifactory, xray, security, access token, curation, distribution, release bundle, apptrust, runtime, mission control, worker, jf command, or best practice | -| **jfrog-ai-catalog-skills** | discovering/installing/updating/publishing agent skills, JFrog AI Catalog, `jf skills`, Agent Guard | -| **jfrog-package-safety-and-download** | package safety, curation, allowed/blocked packages, downloading packages via JFrog | +| **jfrog-ai-catalog** | discovering/installing/updating/publishing agent skills, JFrog AI Catalog, `jf skills`, Agent Guard | +| **jfrog-package-curation** | package safety, curation, allowed/blocked packages, downloading packages via JFrog | The **jfrog** skill (`skills/jfrog/`) provides platform-wide coverage via MCP tools, JFrog CLI commands, and `jf api` REST/GraphQL. It includes 24 reference files under `references/` and 3 automation scripts under `scripts/` covering Artifactory, Security/Xray, Access, Distribution, Curation, AppTrust, Mission Control, Workers, and architectural patterns. -The **jfrog-ai-catalog-skills** skill (`skills/jfrog-ai-catalog-skills/`) discovers, installs, manages, and publishes agent skills hosted in the JFrog AI Catalog via `jf skills` and Agent Guard. +The **jfrog-ai-catalog** skill (`skills/jfrog-ai-catalog/`) discovers, installs, manages, and publishes agent skills hosted in the JFrog AI Catalog via `jf skills` and Agent Guard. -The **jfrog-package-safety-and-download** skill (`skills/jfrog-package-safety-and-download/`) handles package safety checks — querying the JFrog Public Catalog, interpreting security signals, checking curation policies, and downloading packages through Artifactory remote caches. +The **jfrog-package-curation** skill (`skills/jfrog-package-curation/`) handles package safety checks — querying the JFrog Public Catalog, interpreting security signals, checking curation policies, and downloading packages through Artifactory remote caches. ## Agent Package Resolution (Preview) diff --git a/plugins/jfrog/skills/jfrog-init/SKILL.md b/plugins/jfrog/skills/jfrog-init/SKILL.md index 6fbde42..80760bd 100644 --- a/plugins/jfrog/skills/jfrog-init/SKILL.md +++ b/plugins/jfrog/skills/jfrog-init/SKILL.md @@ -11,6 +11,8 @@ metadata: # /jfrog-init — verify and guide JFrog plugin readiness +**First output must be a tool call, not text.** No "I'll start..." preamble. + Walks a fixed, ordered checklist and stops at the first red result, guiding the user through the matching fix before re-checking. Every detector in `scripts/` is idempotent, read-only, JSON-emitting, and implemented in @@ -50,8 +52,8 @@ substitutes it automatically, identically, in both this text and the `allowed-tools` Bash rules above — write it literally rather than resolving it yourself, so the two stay byte-for-byte consistent regardless of install depth (see `references/script-invocation.md`). On -a harness that doesn't perform this substitution (e.g. Cursor, which -doesn't consult `allowed-tools` for approval at all — every command +a harness that doesn't perform this substitution (e.g. Cursor or Codex, +neither of which consults `allowed-tools` for approval — every command below still raises its own prompt there), replace it with the real absolute path of this file's directory yourself, same as before. @@ -140,10 +142,12 @@ user needs to know or act on: - **Do not** announce that you're about to run the checklist, or name which check comes first — not even generically ("I'll run the setup checklist silently, starting with the JFrog CLI check" is itself a - violation: it names a step while claiming to be silent). Silently - means no preamble message at all. Say nothing until you have - something the user needs to act on (an ask, a red result) or the - final summary. + violation: it names a step while claiming to be silent). The same + applies to reading reference docs: "I'll start by reading the flow + docs" is a preamble. Silently means no preamble message at all — not + before running commands, not before reading files. Say nothing until + you have something the user needs to act on (an ask, a red result) + or the final summary. Instead: @@ -251,6 +255,11 @@ Read the output yourself, no JSON to parse: - `node --version` ≥ 18 **and** `npx --version` succeeds → **green** → proceed to Step 2. +**Never paste the raw shell output.** Translate to plain English — +"npx is not installed" not `` `command not found` ``, "Node.js v16 is +too old" not the version string verbatim. The raw output is for your +reasoning, not for the user. + On red, **stop and read `references/node-install-prompt.md` in full before responding to the user.** It has the exact `AskUserQuestion` payload, the forbidden phrases, and the install commands — required diff --git a/plugins/jfrog/skills/jfrog-init/references/flow-diagram.md b/plugins/jfrog/skills/jfrog-init/references/flow-diagram.md index 4a1cb54..6f77852 100644 --- a/plugins/jfrog/skills/jfrog-init/references/flow-diagram.md +++ b/plugins/jfrog/skills/jfrog-init/references/flow-diagram.md @@ -63,7 +63,7 @@ flowchart TD S4 -->|unreachable/timeout/other| STOP2["STOP: show raw error (network/URL hint included)"]:::stopBox S4 -->|yes| S5 - S5["5. Plugin mcp.json has mcpServers.jfrog? (auto-substitutes a JFROG_PLATFORM_URL/JFROG_URL placeholder inline, if present)"]:::stepBox + S5["5. Plugin mcp.json has a valid jfrog entry? (auto-substitutes a JFROG_PLATFORM_URL/JFROG_URL placeholder inline, if present)"]:::stepBox S5 -->|substitution needed, server-id ambiguous| ASKSRV5["AskUserQuestion: pick server-id"]:::fixBox ASKSRV5 --> S5 S5 -->|missing/invalid/no entry, incl. substitution failure| F5["Note: reinstall or update the JFrog plugin, or resolve jf config (non-blocking)"]:::fixBox diff --git a/plugins/jfrog/skills/jfrog-init/references/how-to-ask-user.md b/plugins/jfrog/skills/jfrog-init/references/how-to-ask-user.md new file mode 100644 index 0000000..db3cbf2 --- /dev/null +++ b/plugins/jfrog/skills/jfrog-init/references/how-to-ask-user.md @@ -0,0 +1,19 @@ +# How to ask the user questions + +When the skill needs a Yes/No answer, a selection, or any other input +from the user, use the **native interactive prompt tool** built into +your harness so the user can click or select rather than type: + +| Harness | Preferred tool | +|-------------|------------------------| +| Claude Code | `AskUserQuestion` | +| Codex | `request_user_input` | + +Each reference file specifies the question text and option labels; use +your harness's native tool to present them. Native prompt tools already +offer a free-text "Other" fallback for values not in the list — don't +add a duplicate "Other" option yourself. + +**Fallback**: if no native prompt tool is available, or the tool +returns without a selection, surface the question as plain text in +your reply — never silently stop without presenting it. diff --git a/plugins/jfrog/skills/jfrog-init/references/mcp-plugin-config.md b/plugins/jfrog/skills/jfrog-init/references/mcp-plugin-config.md index 01a818f..39ac6dc 100644 --- a/plugins/jfrog/skills/jfrog-init/references/mcp-plugin-config.md +++ b/plugins/jfrog/skills/jfrog-init/references/mcp-plugin-config.md @@ -13,21 +13,31 @@ resolved at runtime from an env var: {"mcpServers": {"jfrog": {"url": "https://${JFROG_PLATFORM_URL}/mcp"}}} ``` +Codex's plugin ships the same idea in a different shape — no +`mcpServers` wrapper, and angle brackets instead of `${...}`: + +```json +{"jfrog": {"url": "https:///mcp"}} +``` + Because we have that URL sitting in `jf config`, and because leaving the placeholder in place means the MCP silently fails to load in the IDE / agent, Step 5 auto-substitutes it. If the detector finds the placeholder pattern anywhere in the file, it calls `jfrog-substitute-mcp-placeholders.mjs`, which: -1. Parses the file as JSON and looks **only** at - `mcpServers.jfrog.url` — never a file-wide text replace, so an - unrelated MCP server entry or JSON value that happens to contain the - same placeholder text is never touched. +1. Parses the file as JSON and looks **only** at the `jfrog` entry's + `url` (nested under `mcpServers` on every harness but Codex, which + has no wrapper) — never a file-wide text replace, so an unrelated + MCP server entry or JSON value that happens to contain the same + placeholder text is never touched. 2. Reads the JPD URL from `jf config` (default server, or the one passed as arg 2), normalizes it to the JPD root, and substitutes it into that one `url` string. -3. Handles both the `https://${...}` form (where our own scheme would - double up) and the bare `${...}` form. +3. Replaces in two passes — first a placeholder preceded by a scheme + (`https://${...}`, where our own scheme would otherwise double up), + then a bare one. Each pass recognizes all three syntaxes: `${VAR}`, + `$VAR`, and Codex's ``. 4. Re-serializes the whole file (`JSON.stringify(parsed, null, 2)`) and writes atomically (temp file + rename) so a partial write cannot corrupt the file. Original formatting/whitespace elsewhere in the @@ -44,16 +54,19 @@ This is the ONLY place `/jfrog-init` writes to the plugin-owned | Cursor | `~/.cursor/plugins/cache/cursor-public/jfrog//mcp.json` (glob → newest) | | VS Code | `~/.vscode/agent-plugins/github.com/jfrog/vscode-plugin/plugin/.mcp.json` | | Claude Code | `~/.claude/plugins/cache//jfrog//.mcp.json` (glob) | +| Codex | `$CODEX_HOME/plugins/cache/codex-plugin/jfrog//.mcp.json` (glob → newest; `$CODEX_HOME` defaults to `~/.codex`) | -Harness detection: `CLAUDECODE` / `CURSOR_TRACE_ID` / `VSCODE_PID` / -`TERM_PROGRAM`. Override with `JFROG_INIT_HARNESS=claude|cursor|vscode` -or a specific file via `JFROG_INIT_MCP_CONFIG=/abs/path`. +Harness detection (in priority order): `CODEX_SANDBOX` / `CLAUDECODE` / +`CURSOR_TRACE_ID` / `VSCODE_PID` / `TERM_PROGRAM`. Override with +`JFROG_INIT_HARNESS=claude|cursor|vscode|codex` or a specific file via +`JFROG_INIT_MCP_CONFIG=/abs/path`. **What the detector verifies** (three things): 1. Plugin file exists and is non-empty at its harness-specific path. 2. Parses as valid JSON. -3. Contains an `mcpServers.jfrog` entry with a non-empty `url`. +3. Contains a `jfrog` entry (nested under `mcpServers` on every harness + but Codex, which has no wrapper) with a non-empty `url`. It does NOT enforce any other `type`/`url` shape (each plugin owns its own schema) and it does NOT probe the endpoint — a mis-configured MCP @@ -67,12 +80,12 @@ reachable. - **Exit 1 (red)** or **Exit 3 (error)** → **non-blocking** — proceed to Step 6 as if green, but remember the cause for the Final Summary. Steps 6 and 7 call the JPD's REST APIs directly with `jf config` - credentials, never through `mcpServers.jfrog`, so a broken or + credentials, never through the JFrog MCP, so a broken or missing plugin `mcp.json` doesn't affect whether those checks are accurate — there's nothing to gain by stopping the walk over it. Tell the two red causes apart from the detector's `detail` for the Final Summary note: - - Plugin file missing / empty / lacks `mcpServers.jfrog`. Fix: + - Plugin file missing / empty / lacks a valid `jfrog` entry. Fix: **reinstall or update the JFrog plugin.** If the user asks why or how to fix it, run: diff --git a/plugins/jfrog/skills/jfrog-init/references/node-install-prompt.md b/plugins/jfrog/skills/jfrog-init/references/node-install-prompt.md index db8baa2..d58bd79 100644 --- a/plugins/jfrog/skills/jfrog-init/references/node-install-prompt.md +++ b/plugins/jfrog/skills/jfrog-init/references/node-install-prompt.md @@ -1,26 +1,16 @@ # Step 1 — the Node.js install prompt **Required behavior for Step 1's red branch, not optional background.** -When `node --version` is missing, unparseable, or its major version is -`< 18`, call `AskUserQuestion` with this exact payload shape (fill in -`` with either `isn't installed` or the specific `` `` -is too old (need ≥ 18) ``, matching whichever is actually true): +When `node --version` is missing/unparseable/`< 18`, or when `npx +--version` fails (node present but npx absent — same red branch), ask +the user (see `how-to-ask-user.md` for native tool and fallback rules): -```json -{ - "questions": [ - { - "question": "Node.js . Install it now?", - "header": "Install Node", - "multiSelect": false, - "options": [ - {"label": "Yes", "description": "Install Node.js now. Adds a line to your shell startup file so future terminals can find it."}, - {"label": "No", "description": "Cancel /jfrog-init."} - ] - } - ] -} -``` +- **Question**: "Node.js ``. Install it now?" + — where `` is a brief, accurate description of what the tool + output actually showed (e.g. "isn't installed", "`v16.4.0` is too old + (need ≥ 18)", "is installed but npx is missing"). +- **Yes**: "Install Node.js now. Adds a line to your shell startup file so future terminals can find it." +- **No**: "Cancel /jfrog-init." **Do not** mention any install method (nvm, winget), a version manager name, or any URL — not in the question, not in an option description. diff --git a/plugins/jfrog/skills/jfrog-init/references/script-invocation.md b/plugins/jfrog/skills/jfrog-init/references/script-invocation.md index e1c6def..e6595d4 100644 --- a/plugins/jfrog/skills/jfrog-init/references/script-invocation.md +++ b/plugins/jfrog/skills/jfrog-init/references/script-invocation.md @@ -86,7 +86,7 @@ lookups), `node commands, not the function calls a pre-approved script makes once running, so the mutation executes with no prompt whenever Step 5 finds a placeholder. This is intentional, not an oversight: the write is - narrowly scoped to `mcpServers.jfrog.url`, atomic (temp file + rename), + narrowly scoped to the jfrog entry's `url` field, atomic (temp file + rename), and idempotent — see `mcp-plugin-config.md` for why that scope was judged safe to run unattended, unlike the two mutations below. - `jfrog-state-file.mjs`'s **`set`** mode (writes `~/.jfrog/setup.json` diff --git a/plugins/jfrog/skills/jfrog-init/references/server-picker.md b/plugins/jfrog/skills/jfrog-init/references/server-picker.md index 0e46e2d..b5be11b 100644 --- a/plugins/jfrog/skills/jfrog-init/references/server-picker.md +++ b/plugins/jfrog/skills/jfrog-init/references/server-picker.md @@ -23,31 +23,20 @@ triggered this — is reasoning for you to follow silently, never to narrate.** The user never sees why they're being asked, only the `AskUserQuestion` payload itself. -Call `AskUserQuestion` with the **first two** entries of `candidates` -(in the order the detector returned them — never reordered, never -chosen by matching a hostname, git identity, or any other signal) as -the two options, and rely on the tool's built-in "Other" for typing a -different server-id: +Ask the user (see `how-to-ask-user.md` for native tool and fallback +rules) with the **first two** entries of `candidates` (in the order +the detector returned them — never reordered, never chosen by matching +a hostname, git identity, or any other signal) as the two options, and +include a free-text escape hatch for typing a different server-id: -```json -{ - "questions": [ - { - "question": "Which JFrog server do you want to use?", - "header": "Server", - "multiSelect": false, - "options": [ - {"label": "", "description": "Server ID: "}, - {"label": "", "description": "Server ID: "} - ] - } - ] -} -``` +- **Question**: "Which JFrog server do you want to use?" +- **Option 1**: `` (description: "Server ID: ``") +- **Option 2**: `` (description: "Server ID: ``") +- **Other**: let the user type a different server-id if needed **Never surface the full candidate list or a count** to the user in -any case — the picker's two options (plus "Other") are the entire -user-facing surface, same rule as the project picker. +any case — the picker's two options (plus the free-text escape hatch) +are the entire user-facing surface, same rule as the project picker. On picking option 1 or 2, or typing a value via **Other** → re-invoke **the same detector that emitted the ask** (never a different one) diff --git a/plugins/jfrog/skills/jfrog-init/scripts/jfrog-detect-jfrog-mcp.mjs b/plugins/jfrog/skills/jfrog-init/scripts/jfrog-detect-jfrog-mcp.mjs index 510457f..35f1351 100755 --- a/plugins/jfrog/skills/jfrog-init/scripts/jfrog-detect-jfrog-mcp.mjs +++ b/plugins/jfrog/skills/jfrog-init/scripts/jfrog-detect-jfrog-mcp.mjs @@ -1,6 +1,7 @@ #!/usr/bin/env node // Verifies the JFrog PLUGIN'S OWN mcp.json (per harness) exists at its -// installed path AND contains an mcpServers.jfrog entry. This file is +// installed path AND contains a jfrog entry (under mcpServers.jfrog on every +// harness but Codex, which has a bare top-level jfrog key). This file is // owned by the plugin — we NEVER write to it, with one exception: // automatic placeholder substitution (see jfrog-substitute-mcp-placeholders.mjs). // If it's missing, malformed, or lacks the jfrog entry, the correct fix @@ -98,13 +99,9 @@ export function detectJfrogMcp(serverIdArg) { return 3; } - // Auto-substitute any `${JFROG_PLATFORM_URL}` / `${JFROG_URL}` placeholder - // with the real JPD URL from `jf config`. Left in place, the MCP would - // fail to load in the IDE/agent since the env var doesn't exist. - // Checked against mcpServers.jfrog.url specifically (not the raw file - // text) so a placeholder-shaped string elsewhere in the plugin's - // mcp.json — an unrelated MCP entry, say — can't trigger substitution - // for a jfrog.url that has none. + // Substitute a placeholder in the jfrog entry's own url with the real + // JPD URL from `jf config` — checked there specifically, not the raw + // file text, so an unrelated MCP entry can't trigger it. const preSubstitutionUrl = jfrogMcpUrl(parsed); if (typeof preSubstitutionUrl === "string" && hasMcpPlaceholder(preSubstitutionUrl)) { const result = substituteMcpPlaceholders(target, SERVER_ID); @@ -134,11 +131,11 @@ export function detectJfrogMcp(serverIdArg) { const url = jfrogMcpUrl(parsed); const hasUrl = typeof url === "string" && url.trim() !== ""; if (!hasUrl) { - emit("red", target, "plugin mcp.json has no valid mcpServers.jfrog entry (missing or empty url) — reinstall or update the JFrog plugin"); + emit("red", target, "plugin mcp.json has no valid jfrog entry (missing or empty url) — reinstall or update the JFrog plugin"); return 1; } - emit("green", target, "plugin mcp.json present with mcpServers.jfrog entry"); + emit("green", target, "plugin mcp.json present with a jfrog entry"); return 0; } diff --git a/plugins/jfrog/skills/jfrog-init/scripts/jfrog-reinstall-jfrog-plugin.mjs b/plugins/jfrog/skills/jfrog-init/scripts/jfrog-reinstall-jfrog-plugin.mjs index 062785c..f115185 100755 --- a/plugins/jfrog/skills/jfrog-init/scripts/jfrog-reinstall-jfrog-plugin.mjs +++ b/plugins/jfrog/skills/jfrog-init/scripts/jfrog-reinstall-jfrog-plugin.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node // This script itself never writes to the plugin's mcp.json — the file is -// owned by the JFrog plugin (Cursor / VS Code / Claude). It only prints +// owned by the JFrog plugin (Cursor / VS Code / Claude / Codex). It only prints // the diagnosis and the correct remedy for a plugin file that's missing // or invalid: reinstall or update the plugin, with per-harness commands // so the user isn't left guessing. (A placeholder-only problem — e.g. an @@ -47,11 +47,21 @@ After install, restart Claude Code, then re-run /jfrog-init.`); Restart VS Code, then re-run /jfrog-init.`); break; + case "codex": + console.log(`Codex: + codex plugin remove jfrog@codex-plugin # if already installed + codex plugin marketplace add jfrog/codex-plugin # skip if already configured + codex plugin marketplace upgrade codex-plugin + codex plugin add jfrog@codex-plugin + +Restart Codex, then re-run /jfrog-init.`); + break; default: console.log(`Reinstall the JFrog plugin in whichever IDE you're using: Cursor: Settings → Plugins → search "JFrog" → reinstall. VS Code: code --install-extension JFrog.jfrog-vscode-extension --force Claude Code: claude plugin install jfrog-beta/jfrog + Codex: codex plugin marketplace add jfrog/codex-plugin && codex plugin add jfrog@codex-plugin Restart the IDE afterwards, then re-run /jfrog-init.`); } @@ -62,6 +72,8 @@ Expected plugin-owned paths (for reference): Cursor: ~/.cursor/plugins/cache/cursor-public/jfrog//mcp.json VS Code: ~/.vscode/agent-plugins/github.com/jfrog/vscode-plugin/plugin/.mcp.json Claude: ~/.claude/plugins/cache//jfrog//.mcp.json + Codex: $CODEX_HOME/plugins/cache/codex-plugin/jfrog//.mcp.json + ($CODEX_HOME defaults to ~/.codex) `); if (resolved.path && existsSync(resolved.path)) { diff --git a/plugins/jfrog/skills/jfrog-init/scripts/jfrog-resolve-mcp-config.mjs b/plugins/jfrog/skills/jfrog-init/scripts/jfrog-resolve-mcp-config.mjs index f38878b..14d7e0e 100755 --- a/plugins/jfrog/skills/jfrog-init/scripts/jfrog-resolve-mcp-config.mjs +++ b/plugins/jfrog/skills/jfrog-init/scripts/jfrog-resolve-mcp-config.mjs @@ -13,6 +13,9 @@ // Claude: ~/.claude/plugins/cache//jfrog//.mcp.json // (glob across any marketplace + version; most-recently- // modified wins.) +// Codex: $CODEX_HOME/plugins/cache/codex-plugin/jfrog//.mcp.json +// (multiple dirs may exist; most-recently-modified +// wins. $CODEX_HOME defaults to ~/.codex.) // // NOTE (Claude): the current released Claude plugin (jfrog-beta/0.3.0-beta.1) // does NOT ship a .mcp.json — the source repo has one, but the packager @@ -21,17 +24,25 @@ // converts into a clear red / "reinstall the JFrog plugin" instruction. // // Harness detection (env-var signals, in order): -// 1. Claude Code -> $CLAUDECODE / $CLAUDE_CODE_* set -// 2. Cursor -> $CURSOR_AGENT / $CURSOR_CLI / $CURSOR_TRACE_ID set, +// 1. Codex -> $CODEX_SANDBOX / $CODEX_THREAD_ID / $CODEX_CI set +// 2. Claude Code -> $CLAUDECODE / $CLAUDE_CODE_* set +// 3. Cursor -> $CURSOR_AGENT / $CURSOR_CLI / $CURSOR_TRACE_ID set, // or TERM_PROGRAM=cursor -// 3. VS Code -> $VSCODE_PID set, or TERM_PROGRAM=vscode -// The Cursor signals mirror the base skill's check-environment.sh -// detect_harness() and harness-common.md's routing table. +// 4. VS Code -> $VSCODE_PID set, or TERM_PROGRAM=vscode +// Codex is listed first because a Codex session launched from inside +// another harness's terminal still carries that host's own signal — and +// nesting goes both ways, so more than one signal can be present at once. +// When that happens, detectHarness() below walks the process ancestry to +// find which harness actually spawned this invocation. +// detectHarness() is the single JS implementation — exported and reused +// by every other script in this skill that needs harness information. // // Overrides: -// - JFROG_INIT_HARNESS=claude|cursor|vscode forces one specific harness. -// - JFROG_INIT_MCP_CONFIG=/abs/path forces one specific path. +// - JFROG_INIT_HARNESS=claude|cursor|vscode|codex forces one specific harness. +// - JFROG_INIT_MCP_CONFIG=/abs/path forces one specific path. // (Escape hatch — bypasses the plugin-path resolution entirely.) +// - CODEX_HOME=/abs/path Codex's own var, honored by +// the codex branch below; defaults to ~/.codex. // // CLI usage: node jfrog-resolve-mcp-config.mjs // Prints only the path on stdout on success. @@ -39,32 +50,66 @@ // Exit 1 -> could not detect the current harness // Exit 2 -> harness detected, but the plugin's mcp.json is not installed +import { execFileSync } from "node:child_process"; import { existsSync, readdirSync, statSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import { isMainModule } from "./lib/jf.mjs"; -const VALID_HARNESSES = new Set(["claude", "cursor", "vscode"]); +const VALID_HARNESSES = new Set(["claude", "cursor", "vscode", "codex"]); -// JFROG_INIT_HARNESS is matched case-insensitively (e.g. "Claude", "CURSOR") -// so the documented override doesn't silently fail on a case mismatch. -export function detectHarness() { +// One entry per harness, in priority order (see doc comment above) — used +// both as the signal check and as the static fallback when the ancestry +// tie-break can't resolve it. Adding a harness (Kiro, OpenCode, ...) is +// just a new entry here. +const HARNESS_SIGNALS = [ + { name: "codex", signaled: () => process.env.CODEX_SANDBOX || process.env.CODEX_THREAD_ID || process.env.CODEX_CI }, + { name: "claude", signaled: () => process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT || process.env.CLAUDE_CODE_SESSION_ID }, + // Checked before VS Code: Cursor's CLI/agent surfaces can report TERM_PROGRAM=vscode. + { name: "cursor", signaled: () => process.env.CURSOR_AGENT || process.env.CURSOR_CLI || process.env.CURSOR_TRACE_ID || process.env.TERM_PROGRAM === "cursor" }, + { name: "vscode", signaled: () => process.env.VSCODE_PID || process.env.TERM_PROGRAM === "vscode" }, +]; + +// Breaks ties when multiple harness signals fire at once: env vars are +// inherited by child processes regardless of nesting direction, so +// presence alone can't tell them apart. Walks up from the immediate +// parent — skipping the shell/node layers each harness spawns to run a +// command (e.g. codex -> bash -> claude -> bash -> node) — until a +// process name matches a candidate, or maxDepth is hit. Unix-only (ps); +// returns [] on failure (e.g. Windows), which falls through to the +// static priority order. +function getAncestorChain(maxDepth = 12) { + const chain = []; + let pid = process.ppid; + for (let i = 0; i < maxDepth && pid > 1; i++) { + let line; + try { + line = execFileSync("ps", ["-o", "ppid=,comm=", "-p", String(pid)]).toString().trim(); + } catch { + break; + } + const match = line.match(/^(\d+)\s+(.*)$/); + if (!match) break; + chain.push(match[2].toLowerCase()); + pid = Number(match[1]); + } + return chain; +} + +// JFROG_INIT_HARNESS is matched case-insensitively so the documented +// override doesn't silently fail on a case mismatch. getAncestors is +// injectable so tests can stub the tie-break without spawning `ps`. +export function detectHarness(getAncestors = getAncestorChain) { if (process.env.JFROG_INIT_HARNESS) return process.env.JFROG_INIT_HARNESS.trim().toLowerCase(); - if (process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT || process.env.CLAUDE_CODE_SESSION_ID) return "claude"; - // CURSOR_AGENT / CURSOR_CLI are as much a Cursor signal as - // CURSOR_TRACE_ID — all three are what the base skill's - // check-environment.sh detect_harness() and harness-common.md's routing - // table treat as Cursor, and this function has to agree with them or - // the two disagree about which harness the same session is running in. - // Order matters as much as the signals: Cursor's CLI/agent surfaces can - // report TERM_PROGRAM=vscode, so the Cursor test has to run before the - // VS Code one below or those surfaces resolve to the VS Code plugin - // path and the detector reports the JFrog plugin as missing. - if (process.env.CURSOR_AGENT || process.env.CURSOR_CLI || process.env.CURSOR_TRACE_ID || process.env.TERM_PROGRAM === "cursor") { - return "cursor"; + const candidates = HARNESS_SIGNALS.filter((h) => h.signaled()).map((h) => h.name); + if (candidates.length <= 1) return candidates[0] || ""; + // Multiple signals at once means nested harnesses — resolve via + // ancestry, else fall back to the static priority order. + for (const comm of getAncestors()) { + const match = candidates.find((name) => comm.includes(name)); + if (match) return match; } - if (process.env.VSCODE_PID || process.env.TERM_PROGRAM === "vscode") return "vscode"; - return ""; + return candidates[0]; } // Picks the newest file matching `/*/<...tailParts>` by mtime. @@ -131,6 +176,59 @@ function newestClaudeMatch() { return best; } +function resolveClaudePath() { + const match = newestClaudeMatch(); + if (!match) { + return { + error: + "JFrog Claude plugin does not ship a .mcp.json at ~/.claude/plugins/cache/*/jfrog/*/.mcp.json\n" + + " reinstall or update the JFrog plugin so it includes the file.", + code: 2, + }; + } + return { path: match }; +} + +function resolveCursorPath() { + const match = newestMatch(join(homedir(), ".cursor", "plugins", "cache", "cursor-public", "jfrog"), ["mcp.json"]); + if (!match) { + return { + error: + "JFrog Cursor plugin's mcp.json not found under ~/.cursor/plugins/cache/cursor-public/jfrog/\n" + + " install the JFrog plugin in Cursor to make it available.", + code: 2, + }; + } + return { path: match }; +} + +function resolveVscodePath() { + const p = join(homedir(), ".vscode", "agent-plugins", "github.com", "jfrog", "vscode-plugin", "plugin", ".mcp.json"); + if (!existsSync(p)) { + return { + error: `JFrog VS Code plugin's .mcp.json not found at ${p}\n install the JFrog plugin in VS Code to make it available.`, + code: 2, + }; + } + return { path: p }; +} + +function resolveCodexPath() { + const codexHome = process.env.CODEX_HOME || join(homedir(), ".codex"); + const codexPluginDir = join(codexHome, "plugins", "cache", "codex-plugin", "jfrog"); + const match = newestMatch(codexPluginDir, [".mcp.json"]); + if (!match) { + return { + error: + `JFrog Codex plugin's .mcp.json not found under ${codexPluginDir}/\n` + + " run `codex plugin marketplace add jfrog/codex-plugin` then\n" + + " `codex plugin add jfrog@codex-plugin` to make it available.", + code: 2, + }; + } + return { path: match }; +} + export function resolveMcpConfig() { if (process.env.JFROG_INIT_MCP_CONFIG) { return { path: process.env.JFROG_INIT_MCP_CONFIG }; @@ -144,55 +242,29 @@ export function resolveMcpConfig() { // set the very variable they already set. if (process.env.JFROG_INIT_HARNESS && !VALID_HARNESSES.has(harness)) { return { - error: `JFROG_INIT_HARNESS=${process.env.JFROG_INIT_HARNESS} is not one of: claude, cursor, vscode.`, + error: `JFROG_INIT_HARNESS=${process.env.JFROG_INIT_HARNESS} is not one of: claude, cursor, vscode, codex.`, code: 1, }; } - if (harness === "claude") { - const match = newestClaudeMatch(); - if (!match) { - return { - error: - "JFrog Claude plugin does not ship a .mcp.json at ~/.claude/plugins/cache/*/jfrog/*/.mcp.json\n" + - " reinstall or update the JFrog plugin so it includes the file.", - code: 2, - }; - } - return { path: match }; - } - - if (harness === "cursor") { - const match = newestMatch(join(homedir(), ".cursor", "plugins", "cache", "cursor-public", "jfrog"), ["mcp.json"]); - if (!match) { + switch (harness) { + case "claude": + return resolveClaudePath(); + case "cursor": + return resolveCursorPath(); + case "vscode": + return resolveVscodePath(); + case "codex": + return resolveCodexPath(); + default: return { error: - "JFrog Cursor plugin's mcp.json not found under ~/.cursor/plugins/cache/cursor-public/jfrog/\n" + - " install the JFrog plugin in Cursor to make it available.", - code: 2, - }; - } - return { path: match }; - } - - if (harness === "vscode") { - const p = join(homedir(), ".vscode", "agent-plugins", "github.com", "jfrog", "vscode-plugin", "plugin", ".mcp.json"); - if (!existsSync(p)) { - return { - error: `JFrog VS Code plugin's .mcp.json not found at ${p}\n install the JFrog plugin in VS Code to make it available.`, - code: 2, + "could not detect current harness (Claude Code / Cursor / VS Code / Codex).\n" + + " Set JFROG_INIT_HARNESS=claude|cursor|vscode|codex, or\n" + + " JFROG_INIT_MCP_CONFIG=/absolute/path/to/mcp.json to override.", + code: 1, }; - } - return { path: p }; } - - return { - error: - "could not detect current harness (Claude Code / Cursor / VS Code).\n" + - " Set JFROG_INIT_HARNESS=claude|cursor|vscode, or\n" + - " JFROG_INIT_MCP_CONFIG=/absolute/path/to/mcp.json to override.", - code: 1, - }; } if (isMainModule(import.meta.url)) { diff --git a/plugins/jfrog/skills/jfrog-init/scripts/jfrog-substitute-mcp-placeholders.mjs b/plugins/jfrog/skills/jfrog-init/scripts/jfrog-substitute-mcp-placeholders.mjs index 8bd635e..c75b6f0 100755 --- a/plugins/jfrog/skills/jfrog-init/scripts/jfrog-substitute-mcp-placeholders.mjs +++ b/plugins/jfrog/skills/jfrog-init/scripts/jfrog-substitute-mcp-placeholders.mjs @@ -1,14 +1,15 @@ #!/usr/bin/env node // Rewrites a placeholder-style env-var reference in the JFrog plugin's -// mcp.json — specifically `mcpServers.jfrog.url`, nothing else in the -// file — with the real JPD URL from `jf config`. This is the ONLY code -// path in /jfrog-init that writes to the plugin-owned mcp.json. +// mcp.json — specifically the `jfrog` entry's `.url` (nested under +// `mcpServers` on every harness but Codex, which has no wrapper) — with +// the real JPD URL from `jf config`. This is the ONLY code path in +// /jfrog-init that writes to the plugin-owned mcp.json. // // Scoped to that one field (rather than a file-wide text replace) so an // unrelated MCP server entry or JSON value that happens to contain the // same placeholder text is never touched. // -// Placeholders handled (both `$VAR` and `${VAR}` forms): +// Placeholders handled (`$VAR`, `${VAR}`, and Codex's `` forms): // - JFROG_PLATFORM_URL // - JFROG_URL // @@ -37,7 +38,7 @@ // Exit 3 -> read/write error, or jf missing import { existsSync, readFileSync, writeFileSync, renameSync, statSync, chmodSync, unlinkSync } from "node:fs"; -import { emit as emitJf, isMainModule, jfAvailable, jfConfigShow, urlForServer, normalizeJpdUrl, mcpPlaceholderRegexes, jfrogMcpUrl, hasMcpPlaceholder, askServerResult, describeJfUnavailable } from "./lib/jf.mjs"; +import { emit as emitJf, isMainModule, jfAvailable, jfConfigShow, urlForServer, normalizeJpdUrl, mcpPlaceholderRegexes, jfrogMcpEntry, jfrogMcpUrl, hasMcpPlaceholder, askServerResult, describeJfUnavailable } from "./lib/jf.mjs"; import { resolveJfServer } from "./jfrog-resolve-jf-server.mjs"; // Result shape: { exitCode, status, detail, candidates? } — mirrors the @@ -69,7 +70,7 @@ export function substituteMcpPlaceholders(target, serverIdOverride) { const currentUrl = jfrogMcpUrl(parsed); if (currentUrl === null) { - return { exitCode: 0, status: "green", detail: "no mcpServers.jfrog.url present — nothing to substitute" }; + return { exitCode: 0, status: "green", detail: "no jfrog entry url present — nothing to substitute" }; } // Checked before resolving a jf server at all — an unresolvable/ambiguous @@ -106,7 +107,7 @@ export function substituteMcpPlaceholders(target, serverIdOverride) { let newUrl = currentUrl.replace(withScheme, () => jpdUrl); newUrl = newUrl.replace(bare, () => jpdUrl); - parsed.mcpServers.jfrog.url = newUrl; + jfrogMcpEntry(parsed).url = newUrl; const rewritten = JSON.stringify(parsed, null, 2) + "\n"; const tmp = `${target}.tmp.${process.pid}`; diff --git a/plugins/jfrog/skills/jfrog-init/scripts/lib/jf.mjs b/plugins/jfrog/skills/jfrog-init/scripts/lib/jf.mjs index 3a1206a..34d3684 100755 --- a/plugins/jfrog/skills/jfrog-init/scripts/lib/jf.mjs +++ b/plugins/jfrog/skills/jfrog-init/scripts/lib/jf.mjs @@ -398,31 +398,35 @@ export function emit(obj) { process.stdout.write(JSON.stringify(obj) + "\n"); } -// The four supported placeholder forms — `${VAR}` or bare `$VAR` followed -// by a non-identifier character or end of string — and nothing looser. -// Independently-optional braces (`\{?...\}?`) would also match malformed -// or unrelated text like `${JFROG_URL_SUFFIX}` or an unclosed `${JFROG_URL`; -// the `\b` after the bare form and the exact `\{...\}` pairing rule both -// out. Shared by the detector (jfrog-detect-jfrog-mcp.mjs) and the -// substituter (jfrog-substitute-mcp-placeholders.mjs) so "is there a -// placeholder?" and "replace the placeholder" agree on what counts as one. -const MCP_PLACEHOLDER_PATTERN = "\\$\\{(?:JFROG_PLATFORM_URL|JFROG_URL)\\}|\\$(?:JFROG_PLATFORM_URL|JFROG_URL)\\b"; +// `${VAR}`, bare `$VAR`, or Codex's `` — strict pairing so it doesn't +// also match malformed/unrelated text like `${JFROG_URL_SUFFIX}`. Shared +// by the detector and substituter so both agree on what counts as one. +const MCP_PLACEHOLDER_PATTERN = "\\$\\{(?:JFROG_PLATFORM_URL|JFROG_URL)\\}|\\$(?:JFROG_PLATFORM_URL|JFROG_URL)\\b|<(?:JFROG_PLATFORM_URL|JFROG_URL)>"; export function hasMcpPlaceholder(text) { return new RegExp(MCP_PLACEHOLDER_PATTERN).test(text); } -// Shared "is `mcpServers.jfrog` a valid object, and what's its `.url`?" -// check — used by the detector (jfrog-detect-jfrog-mcp.mjs, to decide if -// there's a url worth validating) and the substituter -// (jfrog-substitute-mcp-placeholders.mjs, to decide if there's a url worth -// rewriting) so the two agree on what counts as a valid entry, the same -// way MCP_PLACEHOLDER_PATTERN keeps "is there a placeholder?" in sync. +// Every harness but Codex nests the entry under `mcpServers.jfrog`; Codex's +// plugin mcp.json has no wrapper — bare top-level `jfrog`. Branch keyed on +// the KEY's presence, not truthiness, so `{ "mcpServers": null, "jfrog": +// {...} }` isn't mistaken for Codex-shaped, and JFROG_INIT_MCP_CONFIG can +// still point at a Codex-shaped file from any harness. +// +// Returns the live entry object (mutations land on `parsed`), or null. +export function jfrogMcpEntry(parsed) { + if (parsed === null || typeof parsed !== "object") return null; + const wrapped = "mcpServers" in parsed; + const entry = wrapped ? parsed.mcpServers?.jfrog : parsed.jfrog; + if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return null; + if (!wrapped && !("url" in entry)) return null; + return entry; +} + // Returns the url string (possibly empty) on a valid entry, null otherwise. export function jfrogMcpUrl(parsed) { - const entry = parsed?.mcpServers?.jfrog; - if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return null; - return typeof entry.url === "string" ? entry.url : null; + const entry = jfrogMcpEntry(parsed); + return entry && typeof entry.url === "string" ? entry.url : null; } // Fresh RegExp instances every call — a shared module-level `g`-flagged diff --git a/plugins/jfrog/skills/jfrog-mcp-management/SKILL.md b/plugins/jfrog/skills/jfrog-mcp-management/SKILL.md index 8f38454..d70fbe5 100644 --- a/plugins/jfrog/skills/jfrog-mcp-management/SKILL.md +++ b/plugins/jfrog/skills/jfrog-mcp-management/SKILL.md @@ -1,10 +1,15 @@ --- name: jfrog-mcp-management description: >- - Use to install, list, or remove MCP servers 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. + Use to install, list, or remove MCP servers, and to discover which MCPs the + user can install — including questions about available, approved, or allowed + MCPs for a project. That governed catalog is the authoritative answer; do + not answer those questions from local IDE/settings allowlists alone. Use + whenever the user wants to add/enable/install/list/remove/uninstall/configure + an MCP or write/update the agent's MCP config — even if they name a package + like `@scope/pkg` and even without saying "MCP" or "JFrog". Never install an + MCP any other way. All of this goes through the JFrog Agent Guard (npx + @jfrog/agent-guard). compatibility: >- Requires network access to the npm registry and the JFrog platform, and ~/.jfrog/ write access for OAuth login and cache cleanup. @@ -101,7 +106,7 @@ and List → Currently installed read only local config, so they skip this. | 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) | +| list / show / what can I install / allowed / approved / available / catalog / what's set up / connected in the context of MCPs | [List](#list-mcps) | | remove / uninstall / delete / disconnect / turn off an MCP | [Remove](#remove-an-mcp) | --- @@ -196,7 +201,7 @@ Split Step 2 inputs by `isRequired`: 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, Codex, Devin Desktop, OpenCode), how the user +shell-based harnesses (Claude Code, Cursor, Codex, Devin, OpenCode), 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.) @@ -277,7 +282,7 @@ 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 | +| "available", "what can I install", "what's in the catalog", "list MCPs", "allowed to install", "approved" without other context | **Available to install** — go straight to `--list-available`; do NOT inspect local files / IDE allowlists first. Do NOT ask whether to check the catalog. | | "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 | 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 index f060bf9..1d1e1e3 100644 --- a/plugins/jfrog/skills/jfrog-mcp-management/references/agent-guard-common.md +++ b/plugins/jfrog/skills/jfrog-mcp-management/references/agent-guard-common.md @@ -19,8 +19,25 @@ 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/`. +`@jfrog/agent-guard` is not published to the public npm registry; resolve it +with `--registry ` above rather than the default npm registry. + ## Pre-flight (applies to every agent guard command — `--list-available`, `--inspect`, `--login`) +**Environment probe (run once before resolving the project key and server).** +Before resolving credentials and the JFrog project key, run the skill’s env +probe so each var is printed on its own line (chained `printenv` or truncating +with `head` can merge lines and confuse the read). Prefer this over inventing +an env check; do not print raw token values — report tokens as `present` only; +prefer reporting `JFROG_URL` / `JF_URL` as `present` only as well; an empty +value after the label means the variable is unset. Print real values for +`JF_PROJECT` and `JFROG_AGENT_GUARD_REPO` (needed for `--project` and +enforceable MCP entries): + +```bash +node "/scripts/jfrog-agent-guard-env-probe.mjs" +``` + - **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 diff --git a/plugins/jfrog/skills/jfrog-mcp-management/references/harness-common.md b/plugins/jfrog/skills/jfrog-mcp-management/references/harness-common.md index 850a365..febf55a 100644 --- a/plugins/jfrog/skills/jfrog-mcp-management/references/harness-common.md +++ b/plugins/jfrog/skills/jfrog-mcp-management/references/harness-common.md @@ -38,7 +38,7 @@ editor UI or `mcp.json`, so it falls through to the Fallback section. | Codex | `CODEX_SANDBOX` / `CODEX_THREAD_ID` / `CODEX_CI` | [harness-codex.md](harness-codex.md) | | Cursor | `CURSOR_AGENT` / `CURSOR_CLI` / `CURSOR_TRACE_ID` env var | [harness-cursor.md](harness-cursor.md) | | OpenCode | `OPENCODE` | [harness-opencode.md](harness-opencode.md) | -| Devin Desktop | Your system prompt / system instructions identify you as **Devin** (Devin Desktop / Devin Local / Cognition). That alone is enough. Optionally confirm with `VSCODE_IPC_HOOK` set to the Devin Desktop IPC socket (full path), e.g. macOS: `~/Library/Application Support/Devin/-main.sock` — the expanded path contains `/Devin/`. The path alone is **not** enough. | [harness-devin.md](harness-devin.md) | +| Devin | Your system prompt / system instructions identify you as **Devin** (Devin Desktop / Devin Local / Devin CLI / Cognition). | [harness-devin.md](harness-devin.md) | | VS Code editor | `TERM_PROGRAM=vscode` **and no `CURSOR_*` var is set** **and no `OPENCODE` var is set** **and no `CODEX_*` var is set** **and no `CLAUDECODE`/`CLAUDE_CODE_ENTRYPOINT` var is set** **and no `GEMINI_CLI` / `GOOSE_TERMINAL` / `COPILOT_CLI` var is set** **and** your system prompt / system instructions do **not** identify you as Devin | [harness-vscode.md](harness-vscode.md) | | anything else | none of the above | **Fallback** section below — no harness file exists | diff --git a/plugins/jfrog/skills/jfrog-mcp-management/references/harness-devin.md b/plugins/jfrog/skills/jfrog-mcp-management/references/harness-devin.md index 49616c1..d4800ca 100644 --- a/plugins/jfrog/skills/jfrog-mcp-management/references/harness-devin.md +++ b/plugins/jfrog/skills/jfrog-mcp-management/references/harness-devin.md @@ -1,37 +1,59 @@ -# Harness: Devin **Desktop** - -Devin Desktop-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 Step A matched **Devin**: -your system prompt / system instructions identify you as Devin. You may -optionally confirm with `VSCODE_IPC_HOOK` under the Devin user-data dir (e.g. -`~/Library/Application Support/Devin/-main.sock`). The environment +# Harness: Devin + +Devin-specific config for the `jfrog-mcp-management` skill (Devin CLI and +Devin Local in Devin Desktop). Read this together with +[harness-common.md](harness-common.md) (shared entry shape and success +criterion). You reached this file because Step A matched **Devin**: your +system prompt / system instructions identify you as Devin. The environment script does not detect Devin. -Devin Desktop is a VS Code-family Electron shell that runs the Cascade / Devin -Local agent. It stores MCP configuration in the Windsurf config file used by -the underlying platform. +This harness targets the **Devin plugin** path only. + +## Detect the Devin surface + +Run this from **your** agent environment (not a terminal the user typed into — +the two can differ) before choosing restart or verify steps: + +```bash +printf 'TERM_PROGRAM=%s\n' "${TERM_PROGRAM-}" +printf 'VSCODE_IPC_HOOK=%s\n' "${VSCODE_IPC_HOOK-}" +``` + +Classify from the result. **Environment markers win over system-prompt +wording**, because Devin Local's system prompt also describes an "interactive +command line agent" and must NOT be treated as CLI: + +- **Devin Desktop (Devin Local):** `VSCODE_IPC_HOOK` is set to a path inside a + Devin user-data directory (contains `/Devin/` on macOS/Linux, `\Devin\` on + Windows). Optionally the system prompt identifies Devin Desktop / + Devin Local. +- **Devin CLI:** no Devin Desktop marker above — `VSCODE_IPC_HOOK` unset (or + not under a Devin user-data dir). This holds even when the system prompt + calls you an "interactive command line agent". +- If a Desktop marker is present, choose Desktop even if the prompt reads + CLI-like. If nothing is conclusive, ASK the user — do not guess. + +Use the matching Desktop or CLI instructions below for restart, list, and +verify. **Config write path is the same for both** (see Config files). Do not +mix restart or verification surfaces. ## Config files +Both Devin CLI and Devin Local use the same Devin MCP config files. + - **Default scope: user-level.** Personal, not committed, available across all - workspaces. **Prefer Windsurf** — the same file Cascade uses and that the - JFrog Desktop extension writes the `jfrog` MCP into: - - macOS/Linux: `~/.codeium/windsurf/mcp_config.json` - - Windows: `%APPDATA%\.codeium\windsurf\mcp_config.json` + workspaces: + - macOS/Linux: `~/.config/devin/mcp_config.json` + - Windows: `%APPDATA%\devin\mcp_config.json` Create the parent directory first (`mkdir -p` / platform equivalent), then - create the file if missing: `{ "mcpServers": {} }`. Devin Local imports this - file when `read_config_from.windsurf` is not `false` in - `~/.config/devin/config.json` (default) — so one write serves Cascade and Local. -- **Exception — migrated native store:** If `~/.config/devin/mcp_config.json` - **already exists** (user accepted **Migrate MCP config** / Copy), Devin Local - uses that file instead of Windsurf import. For Local, merge entries **there** - and do **not** require `read_config_from.windsurf`. Cascade never reads the - native file — if the entry must also appear in Cascade, merge into Windsurf - as well. Prefer **Cancel** on migrate so both agents stay on Windsurf. -- **Project scope:** Not supported by Devin Desktop's Cascade / Windsurf config. -- Do not ask which scope unless the user brings it up. + create the file if missing: `{ "mcpServers": {} }`. +- **Project scope** (only if the user asks): `.devin/mcp_config.json` in the + project root (shared / commit-able). +- **Local project override** (only if the user asks): `.devin/mcp_config.local.json` + (gitignored; personal keys). +- Do not ask which scope unless the user brings it up; use the user-level + default above. ## Top-level key @@ -39,106 +61,91 @@ the underlying platform. ## Value reference (env / secrets) -`${env:VAR_NAME}`, resolved from the environment that launched Devin Desktop. -For `Bearer` headers: `"Bearer ${env:TOKEN}"`. Devin Desktop also supports -`${file:~/path/to/file}` to inline a file's trimmed contents. The user must -export the variable in the environment that launches Devin Desktop (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. +`${env:VAR_NAME}`, resolved from the environment that launches the current +surface (Devin Desktop or Devin CLI). For `Bearer` headers: +`"Bearer ${env:TOKEN}"`. Also supports `${file:~/path/to/file}` to inline a +file's trimmed contents. The user must export the variable in the launching +environment (see [persisting-env-vars.md](persisting-env-vars.md)); values are +picked up on next launch / new session. If a required `${env:VAR}` is unset +the upstream MCP may fail at startup — confirm the export before restart. +Never write a raw secret. `${env:…}` / `${file:…}` are for the upstream MCP's own secrets and inputs — never for JFrog Agent Guard credentials (see below). ## JFrog credentials - from the `jf` config -**Include `--server ` by default.** It reads that server's URL + token -from the on-disk `jf` CLI config, is unambiguous, and keeps working if the user -later adds more servers. Resolve `` per the agent-guard-common -Pre-flight rules; never emit an empty `--server`. - -`--server` can be **omitted only when exactly one `jf` server is configured** - in -that case the Agent Guard auto-resolves it. With **multiple** `jf` servers, -omitting `--server` fails: the Agent Guard cannot choose between them and does NOT -fall back to the `jf` default, so `--server` is required. (When in doubt, include -it.) - -**Devin exception to the shared rule.** [SKILL.md](../SKILL.md) treats `--server` -as conditional and permits dropping it on the `JFROG_URL`+token env path (see its -Step 4 Guardrails, "`--server` … drop it only on the `JFROG_URL`+token env -path"). **That env path does NOT apply on Devin Desktop** - do NOT authenticate -JFrog via env-var credentials, even though Devin Desktop would resolve -`${env:JFROG_URL}` / `${env:JFROG_ACCESS_TOKEN}` (or inherit them from the -launch environment) into the Agent Guard process. Use `--server ` -(or a single configured `jf` server) as described above. If there is no usable -`jf` server, ask the user to add one (`jf c add `, or `jf login`) before -continuing. - -If credentials cannot be resolved (no `--server` and either zero or multiple `jf` -servers), the entry fails to start and the server connects with no tools. +On Devin (CLI and Desktop Local), authenticate Agent Guard only through the +on-disk `jf` CLI config. **Always include `--server `** in every +Agent Guard command and written MCP config entry — resolve `` per +the agent-guard-common Pre-flight rules, never emit an empty `--server`, and do +**not** omit `--server` even when only one `jf` server is configured (explicit +server ID matches plugin enforcement). + +Do **not** use the shared [SKILL.md](../SKILL.md) env-var auth path +(`JFROG_URL` / `JFROG_ACCESS_TOKEN`, or legacy `JF_URL` / `JF_ACCESS_TOKEN`) on +Devin, even though Devin would resolve or forward them into Agent Guard. If +there is no usable `jf` server, ask the user to add one (`jf c add `, or +`jf login`) before continuing. + +If credentials cannot be resolved (no `--server ` in the entry, or no +usable `jf` server to resolve one from), the entry fails to start and the server +connects with no tools. ## Enable -Devin Desktop loads every non-disabled entry in `mcpServers` automatically on -window load; there is no per-server approval prompt to pre-approve. If the -entry carries `"disabled": true`, remove it so the server runs. Otherwise -nothing to do here. +Both surfaces start every server under `mcpServers` that is not marked +`"disabled": true` on that server's own entry (per-server flag in the config — +same idea as `devin mcp disable` / `enable`). If `` has +`"disabled": true`, remove that flag so the server can run. Approving MCP tool +calls in chat is separate from enablement. ## Restart -`Developer: Reload Window` (or fully quit and reopen Devin Desktop). Devin -Desktop re-reads `mcp_config.json` on window load and reconnects each server. +- **Devin Desktop (Local):** tell the user to run `Developer: Reload Window` (or fully quit and + reopen Devin Desktop). Desktop re-reads MCP config on window / session load. +- **Devin CLI:** tell the user to start a new Devin CLI session — exit and run + `devin` again in the same directory — so the added/removed entry takes + effect (user, project, and local MCP config files are read at session start). ## List installed -Open the **MCP servers** panel (Cascade panel toolbar, or -`Devin Settings → Cascade → MCP Servers`), or **Open customizations** on a -Devin Local session — each configured server is listed with its live -connection state. Servers and their tools are also reachable via `@` in the -chat input. Do **not** use `/mcp` here: that slash command is Devin CLI only; -in Desktop `/` lists workflows, so `/mcp` can fuzzy-match a skill and mislead. -Confirm via the MCP servers panel / Open customizations, or by checking that -`` exists under `mcpServers` in the active store (Windsurf by default; -native `~/.config/devin/mcp_config.json` only when that file already exists — -see Config files). When reading the file, do not report secret values — env -**key names** only; never display resolved `${env:…}` or `${file:…}` contents. +Read `mcpServers` from `~/.config/devin/mcp_config.json` (or the project/local +file if that scope was used). Do not report secret values — env **key names** +only. + +- **Devin CLI and Devin Desktop (Local):** run `devin mcp list` for + live connection status. +- If the config and `devin mcp list` are not enough, tell the user to run + `/mcp` for the interactive status panel. On **Devin Desktop (Local)** only, + they can also open **Open customizations** (MCP list) and report each + server's status. ## Verify -Before treating a missing server as Failed: confirm the entry is in the active -store (Windsurf by default; native only when that file already exists). For -Devin Local on Windsurf, also confirm `read_config_from.windsurf` is not -`false`. Skip that flag check when Local is on the native file. - -Ask which MCP servers are available, or open the MCP servers panel / Open -customizations, and confirm `` is listed and connected. Then ask the -agent to list that server's tools (or reach it via `@`); the server MUST -expose **at least one tool**. A connected indicator alone is NOT proof — the -Agent Guard proxy can report connected with 0 upstream tools. Empty tool -list = Failed → see the "0 tools" troubleshooting in +Before treating a missing server as Failed, confirm the entry is in the active +store for this harness (user `~/.config/devin/mcp_config.json` by default). + +After the user completes Restart (see Restart), run `devin mcp list` for connection status, +then **list that server's live tools** through the connected MCP (Devin CLI and +Devin Desktop Local). + +The server MUST expose **at least one tool**. A connected indicator alone is +NOT proof — the Agent Guard proxy can report connected with 0 upstream tools. +Empty tool list = Failed → see the "0 tools" troubleshooting in [key-rules-and-troubleshooting.md](key-rules-and-troubleshooting.md). +Do **not** treat a tool list scraped from npm / GitHub docs as verification — +only a live tool list from the connected server counts. + On first connect without cached OAuth, Devin opens a browser to sign in; later -runs reuse stored credentials. Treat **Output → MCP** as authentication / -connection status only — never as a source of token values. Devin Local may -also prompt to approve each MCP tool call by default — grant the prompt before -treating an empty list as a failure. +runs reuse stored credentials. Devin Local and Devin CLI may prompt to approve +each MCP tool call by default — grant the prompt before treating an empty list +as a failure. ## Notes -- Cascade always reads `~/.codeium/windsurf/mcp_config.json`. Devin Local - imports that same file when `read_config_from.windsurf` is enabled in - `~/.config/devin/config.json` (default). If Local is on Windsurf and that - flag is `false`, Local will not see Windsurf entries even though the file on - disk is unchanged. -- Some Devin Desktop builds prompt to copy Windsurf MCP config to - `~/.config/devin/mcp_config.json` (**Migrate MCP config**). Prefer **Cancel** - unless the user wants to migrate: once the native file exists, Local uses it - as its store (no Windsurf-import requirement) while Cascade continues to use - Windsurf only — installs then diverge unless you write both. +- Devin CLI and Devin Local share the same MCP config paths. An install from + either surface is visible to the other after the appropriate restart. - OAuth `--login` caches tokens in `~/.jfrog/jfrogmcp.conf.json` (same as all harnesses); removal cleanup of that file is the same everywhere. -- Devin Desktop is distinct from **Devin CLI** (the `devin` terminal agent): - the CLI has its own config at `.devin/config.json` / `.devin/config.local.json` - and is not covered by this harness file. CLI-only surfaces such as `/mcp` - do not apply here. 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 index 1460227..7c0c49c 100644 --- a/plugins/jfrog/skills/jfrog-mcp-management/references/persisting-env-vars.md +++ b/plugins/jfrog/skills/jfrog-mcp-management/references/persisting-env-vars.md @@ -5,7 +5,7 @@ so its value takes effect. How each harness picks up the exported variable: - **Claude Code** — a `${VAR}` reference in the config. - **Cursor** — a `${env:VAR}` reference in the config. -- **Devin Desktop** — a `${env:VAR}` reference in the config. +- **Devin Desktop / Devin CLI** — a `${env:VAR}` reference in the config. - **Codex** — a variable name listed in the `env_vars` allow-list; Codex forwards that named variable's value from the launching shell to the server (e.g. an env var like `Authorization`). diff --git a/plugins/jfrog/skills/jfrog-mcp-management/scripts/jfrog-agent-guard-env-probe.mjs b/plugins/jfrog/skills/jfrog-mcp-management/scripts/jfrog-agent-guard-env-probe.mjs new file mode 100644 index 0000000..14f832e --- /dev/null +++ b/plugins/jfrog/skills/jfrog-mcp-management/scripts/jfrog-agent-guard-env-probe.mjs @@ -0,0 +1,33 @@ +#!/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 environment probe. +// +// Safe cross-platform env dump used by the jfrog-mcp-management skill pre-flight +// (before resolving project key / server). Prints one labeled line per var so +// agents do not invent chained printenv / head probes that merge lines. +// +// Contract: +// - Exit 0 after writing all lines (do not call process.exit — let Node +// flush stdout, especially under pipes) +// - Tokens (JFROG_ACCESS_TOKEN / JF_ACCESS_TOKEN): "present" or empty +// - URLs (JFROG_URL / JF_URL): "present" or empty (prefer presence-only) +// - JF_PROJECT / JFROG_AGENT_GUARD_REPO: real value or empty when unset +// - Never print raw token values + +import process from "node:process"; + +const presentOrEmpty = (value) => (value ? "present" : ""); + +const lines = [ + `JFROG_URL: ${presentOrEmpty(process.env.JFROG_URL)}`, + `JFROG_ACCESS_TOKEN: ${presentOrEmpty(process.env.JFROG_ACCESS_TOKEN)}`, + `JF_URL: ${presentOrEmpty(process.env.JF_URL)}`, + `JF_ACCESS_TOKEN: ${presentOrEmpty(process.env.JF_ACCESS_TOKEN)}`, + `JF_PROJECT: ${process.env.JF_PROJECT ?? ""}`, + `JFROG_AGENT_GUARD_REPO: ${process.env.JFROG_AGENT_GUARD_REPO ?? ""}`, +]; + +process.stdout.write(`${lines.join("\n")}\n`); diff --git a/plugins/jfrog/skills/jfrog-package-safety-and-download/SKILL.md b/plugins/jfrog/skills/jfrog-package-curation/SKILL.md similarity index 80% rename from plugins/jfrog/skills/jfrog-package-safety-and-download/SKILL.md rename to plugins/jfrog/skills/jfrog-package-curation/SKILL.md index 0adb9cf..300f785 100644 --- a/plugins/jfrog/skills/jfrog-package-safety-and-download/SKILL.md +++ b/plugins/jfrog/skills/jfrog-package-curation/SKILL.md @@ -1,25 +1,61 @@ --- -name: jfrog-package-safety-and-download +name: jfrog-package-curation description: >- - Check JFrog Public Catalog and stored packages for a version, interpret - catalog security signals, and download through Artifactory (JFrog Platform - locations, remote cache, curation-aware package managers, or repo proxy). - Use when the user asks whether a package is safe, allowed, curated, or - wants to download npm, Maven, PyPI, Go, or similar packages via JFrog. - Do NOT use for pure CVE or vulnerability lookups (e.g. "details on - CVE-2021-23337") — those are handled by the jfrog skill's Public security - domain queries without this workflow. + Check/download a package (npm, Maven, PyPI, Go...) via JFrog — safe, + allowed, curated? Or: package op fails/blocked (ETARGET, 403, blocked by + curation policy, missing version, waiver) — root cause it. Checks the JFrog Public + Catalog and stored packages for a version, interprets catalog security + signals, and downloads through Artifactory (JFrog Platform locations, + remote cache, curation-aware package managers, or repo proxy). Do NOT use + for pure CVE or vulnerability lookups (e.g. "details on CVE-2021-23337") + — those are handled by the jfrog skill's Public security domain queries + without this workflow. Do NOT use for installing, listing, or approving MCP + servers/tools (even when named like a package, e.g. `@scope/pkg`) — that's + `jfrog-mcp-management`. +compatibility: >- + "Check & download" needs only the JFrog CLI (`jf`). "Troubleshoot a + failure" needs the JFrog MCP curation tools (`jfs_curation_*`) — `jf` CLI + alone does not cover it; if unavailable, that section cannot run (see its + Preflight). Its reference file is local; outbound HTTPS is optional there + too (on-demand WebFetch of JFrog docs) — without it, it runs on the MCP + tools and local reference, degraded. metadata: role: workflow --- -# JFrog Package Safety and Download +# JFrog Package Curation ## Prerequisites - Read `../jfrog/SKILL.md` for JFrog Platform concepts, domain model, CLI setup, and API patterns. - **OneModel shapes drift by server version.** Before inventing GraphQL fields or `where` filters, read `../jfrog/references/onemodel-graphql.md` (schema fetch workflow) and `../jfrog/references/onemodel-query-examples.md` (**Public packages**, **Stored packages**). Regenerate or verify queries against `GET "$JFROG_URL/onemodel/api/v1/supergraph/schema"` when examples fail validation. +## Which path do I need? + +- **Package hasn't been checked or downloaded yet** (is it safe / allowed / curated? download it) → go to **Check & download** below. +- **A package operation already failed** — `ETARGET` (npm), "no matching distribution" (pip), `NU1102` (NuGet), a version missing/stripped from the registry, `403` / "blocked by curation policy", or an approved waiver not taking effect → go to **Troubleshoot a failure** below. + +--- + +# Troubleshoot a failure + +> **Requires the JFrog MCP curation tools** (`jfs_curation_*`); no `jf` CLI +> equivalent is used in this path. See the skill's `compatibility` note. + +Root-causing a curation-attributable failure (`ETARGET`, `403`, a version +missing from the index, a waiver not taking effect) is a multi-step +investigation — preflight checks, the two-gate model, symptom classification, +then a full RCA procedure. It doesn't belong inline here since a plain +download never touches it. **Open +[references/rca-procedure.md](references/rca-procedure.md) and follow it from +the top** — one file, covering preflight, the #1 RULE, the two-gate model, +symptom classification (Step 0), and the RCA itself (Steps 1–4, worked +examples, gotchas). + +--- + +# Check & download + ## Workflow overview ```mermaid @@ -36,7 +72,7 @@ flowchart TD I -->|Yes| J[Check curation policy via API] I -->|No| K[Download via remote repo] J -->|200 Allowed| K - J -->|403 Blocked| M[Report curation blocked — stop] + J -->|403 Blocked| M[Report curation blocked — see Troubleshoot a failure] ``` ### Parallelization opportunities @@ -207,7 +243,10 @@ fi download via a remote repo (same as Step 6b). - **`returned 403` on stderr** → package is **blocked** by a curation policy. The response body explains which policy rule blocked it. Report - the block reason to the user and stop — do not attempt to download. + the block reason to the user and stop — do not attempt to download. For + root-cause detail beyond the block reason (why this version specifically, + whether a waiver applies, what to use instead), see **Troubleshoot a + failure** above. - **Any other non-zero exit** → treat as an operational failure (auth, DNS, endpoint disabled) and report. diff --git a/plugins/jfrog/skills/jfrog-package-curation/references/rca-procedure.md b/plugins/jfrog/skills/jfrog-package-curation/references/rca-procedure.md new file mode 100644 index 0000000..3a8f24b --- /dev/null +++ b/plugins/jfrog/skills/jfrog-package-curation/references/rca-procedure.md @@ -0,0 +1,568 @@ +# Curation RCA procedure + +> **Requires the JFrog MCP curation tools** (`jfs_curation_*`); no `jf` CLI +> equivalent is used in this path. See the skill's `compatibility` note. + +> **#1 RULE — READ FIRST.** The **public catalog** +> (`catalog_packages_list_versions`) is the only authority on whether a version +> exists — quote its version list when you report existence. **Never** assert a +> version exists or doesn't from the curated registry (`npm view` / `pip index` +> against `/artifactory/...`, which is curation-filtered, so a CVS-blocked version +> is simply absent) **or from your own memory**. Decide existence from the catalog, +> and cross-check the **CVS audit** (`jfs_curation_query_cvs_audit_events`) for why a version was +> filtered. + +Root-cause a curation package failure: correlate the two places Curation acts, +then report an actionable conclusion. This file is the whole workflow — +preflight, the two-gate model, symptom classification, evidence gathering, +interpretation, and the conclusion — read it in order. + +This orchestrates the **JFrog MCP curation tools** (no `jf` CLI or raw +HTTP) and assumes the JFrog MCP server is connected and the tenant is +Curation-entitled. The curation tools are the **`jfs_curation_*`** family (e.g. +`jfs_curation_query_cvs_audit_events`); `catalog_packages_list_versions` +and `artifactory_repositories_get` are different tool families without the prefix. + +## Preflight + +**Right environment.** The MCP curation tools only ever query the +**currently connected** JFrog tenant, and a different env has different audit data — +an answer from the wrong tenant is worse than none. When the failing URL's host +matches the connected tenant, you're set — proceed. **Confirm with the user only +when it's ambiguous** (the host doesn't obviously match, or you can't tell which +tenant is connected); on a real mismatch, **stop** until the tools point at the +right tenant. Don't ask when it already matches. + +**Tools must be present.** These MCP tools are gated at registration +by platform version + entitlement (min Xray **3.153.0** for the CVS tools and +`jfs_curation_get_audit_event_policies`; **3.112.0** for `jfs_curation_query_audit_events`). If the tool you +need is **not in your available tools**, split the diagnosis on what else is +present: + +- **No JFrog MCP tools at all** (not even `catalog_packages_list_versions` / + `artifactory_repositories_get`) → the JFrog MCP server isn't connected. Say + so; never substitute a `jf` command or raw HTTP. +- **Other JFrog MCP tools work but `jfs_curation_*` is absent** → the + platform is below the required Xray version or the tenant isn't + Curation-entitled. **Stop gracefully:** report that the audit-based investigation + can't run here and that it needs Xray ≥ 3.153.0 with Curation enabled. + +Either way, do **not** fabricate calls or guess results. + +## How Curation works + +A package manager does two things: **resolve** a version, then **download** it. +Curation acts at both, with different strictness. + +| | **Gate 1 — Resolution** | **Gate 2 — Download** | +|---|---|---| +| Client action | asks Artifactory for the version list | requests the binary | +| Curation evaluates | versions newest-first, **stops at the first approved one** (max evaluated is configurable, typically 1) | **all** policies, against the one requested version | +| Returns | that version **plus everything older** — or `403` if none approved | the binary — or `403` | +| Strictness | best-effort filter to reduce downstream blocks; **not a guarantee** | **hard gate** — enforcement is final here | + +**The list is truncated at the top, not filtered throughout.** Versions *newer* +than the approved one are gone. Versions *older* are present but were **never +evaluated**. Every symptom below follows from that one sentence. + +```mermaid +sequenceDiagram + participant CLI as Package manager + participant RT as Artifactory + participant CUR as Curation + + CLI->>RT: 1. list versions + RT->>CUR: evaluate + CUR-->>RT: first approved version + all older + alt none approved + RT-->>CLI: 403 + else + RT-->>CLI: truncated list + end + Note over CLI: client resolves against this list only
a pin not in it fails here, in the client + + CLI->>RT: 2. download resolved version + RT->>CUR: evaluate all policies + alt blocked + RT-->>CLI: 403 + else + RT-->>CLI: binary + end +``` + +**Which audit records which gate:** + +- **Gate 1 — resolution** → the **CVS audit**: + `jfs_curation_query_cvs_audit_events` → `jfs_curation_get_cvs_audit_version_detail`. +- **Gate 2 — download** → the **package-download audit**: + `jfs_curation_query_audit_events` → `jfs_curation_get_audit_event_policies`. +- A **`403` with nothing resolvable** (gate 1, every version blocked) is recorded in + **both** audits — query both. + +**Symptom → gate:** + +| Symptom | Gate | Read it as | +|---|---|---| +| `ETARGET`, "no matching distribution", `NU1102` | 1 | pin is newer than the approved version, so it was never returned. The client failed locally — **Curation wrote no event for this failure** | +| `403` on a version the client could see | 2 | pin is older than the approved version: passed gate 1 unevaluated, blocked at the hard gate. Expected, not a contradiction | +| `403` with nothing resolvable | 1 | every version blocked (recorded in both audits) | +| Visible "latest" is lower than upstream | 1 | normal truncation, not a fault | + +Exact error text is **manager- and command-specific** — don't assume `ETARGET` +(npm `ETARGET`, pip "no matching distribution", NuGet `NU1102`, `E403`; `npm +install` vs `npm ci`, `pip install ` vs `==`). + +Gate 1 and gate 2 disagreeing is **by design** — say so rather than reconciling it. + +**"CVS disabled" ≠ no gate 1.** CVS is **enabled by default** for some package types: the +**malicious** and **immature** policies run at gate 1 even when CVS is switched off +(npm today; the set grows as types onboard — don't assume it's only npm). So a +gate-1 block whose `condition_name` is malicious or immature is expected despite a +"CVS is disabled" claim; a CVE / license / ban condition means CVS is actually on. +Treat the audit as ground truth — a gate-1 event contemporaneous with the failure +settles it. Read the `condition_name` and explain it; don't argue with the claim. + +> **Don't mix timelines.** The audit tools report the **past** (timestamped +> events). `jfs_curation_check_remote_package_compliance` returns a **live verdict now** — it +> runs a fresh CVS evaluation under current policy and may create a new audit row. +> Anchor the RCA on the audit timeline. Use a live compliance check only per +> Step 2, and label its result **current** — never as evidence of the +> failure-time state. + +**Docs to consult** — fetch on demand, once (if blocked/airgapped, say so and +reason from the tool outputs; if the fetch itself fails for any other reason, +say so and proceed on the tool outputs alone rather than stalling): + +- **CVS mechanism, supported types, waiver applicability, limits** — fetch at the + start of a gate-1 investigation: + `https://docs.jfrog.com/security/docs/compliant-version-selection` +- **Per-package-manager error text** — don't assume `ETARGET`; the pattern is + manager- and command-specific: + `https://docs.jfrog.com/security/docs/behavior-by-package` +- **Curation concepts** (what it is, features & capabilities), only to ground a + term you cannot otherwise resolve: + `https://docs.jfrog.com/security/docs/curation-intro` and + `https://docs.jfrog.com/security/docs/features-and-capabilities-getting-started-5` + +## Route by gate + +Classify the symptom → gate → tool using the tables above: + +- **Gate 1 — resolution** (not resolvable: `ETARGET`, "no matching distribution", + `NU1102`, missing from `npm view` / `pip index`) → **CVS audit**: + `jfs_curation_query_cvs_audit_events` → `jfs_curation_get_cvs_audit_version_detail`. +- **Gate 2 — download** (`403` on a version that *is* resolvable) → **package-download + audit**: `jfs_curation_query_audit_events` → `jfs_curation_get_audit_event_policies`. + Enough to root-cause the 403 on its own — don't reach for a compliance check by reflex. + +A `403` with **nothing** resolvable is gate 1 and is recorded in **both** audits — +query both. The gates can **disagree** (a resolvable version that still 403s) — +surface it, don't reconcile. + +## Step 0 — Capture the symptom + +Collect before touching any tool. Ask the user only for what is missing: + +- **package type** (npm, PyPI, Maven, Go, NuGet, …) — infer it from the failing + URL (`/api//…`, e.g. `/api/npm/`) or the command (`npm`/`pip`/…); ask the + user only if neither is available. `package_type` is **required** on the CVS + audit call. If the first audit comes back + empty, suspect a casing/token mismatch instead: canonical CVS tokens are + `npm, PyPI, Maven, Go, NuGet, Gems, Conda, Composer, Gradle, + AIEditorExtensions, SBT, Alpine, Docker, Cargo` (case-sensitive, not + normalised server-side) — retry with the canonical casing, or ask the user + to confirm the ecosystem rather than guessing. +- **package name** and the **version** in the error. +- **repository / registry** the client resolved through (from the failing URL / + client config), and whether it's a JFrog **curated remote**, a **virtual** (some + managers — e.g. Go — require one; npm can use either), or plain public. This + decides how Step 1 reads a "not found" (curated vs public) and how Step 2 + identifies the remote (the audits are keyed by the remote, never a virtual). You + often can't tell virtual from remote by name — capture it; if unknown, ask; do + not assume. +- the **exact error text** and the **approximate date + hour** — bounds the audit + window (Step 2). Investigate any past failure within **retention** (default ~30 days); + the 168h cap limits one query's **width**, not recency. For an older failure, + anchor the window near the failure time (not "now") so the query actually covers it. + +**Pin the timeline before running any audit.** Read the failure's **date + hour** +from the error text / log if it's there (e.g. a timestamped log line) — don't ask +for what the provided context already shows; ask the user only if it's missing +(and don't settle for "recently"). It bounds the audit window and anchors +`created_at_end` to the failure, not "now". Then, within that window, if Step 2's +survey turns up **more than one curated remote** for the package, ask which repo +they resolved through before interpreting — see Step 2's "Identify the curated +remote". + +Classify into a symptom class. This list is **open-ended** — match the closest +row; if nothing fits, use the last row (never refuse for lack of an exact match): + +| Symptom | Class | Start with | +|---|---|---| +| A version can't be resolved/installed — e.g. `ETARGET` (npm), "no matching distribution" (pip), `NU1102` (NuGet), or absent from `npm view` / `pip index`. The exact pattern is **package-manager-specific** — see behavior-by-package | **NOT-RESOLVABLE** | Step 1's client-side-config check, then Gate 1 — CVS audit | +| `403` / `Forbidden` / "blocked by curation policy" on a version that *is* listed | **DOWNLOAD-BLOCKED** | Gate 2 — package-download audit | +| Every version fails because nothing resolves at all (empty/near-empty listing) | **NOT-RESOLVABLE** (package-level) | This is a resolution failure, not a download-time block — Gate 1 + both audits | +| Every version 403s at download while the package is still listed | **PACKAGE-BLOCKED** | Gate 2; expect a package-level block | +| The blocked name is a dependency of what you installed | **TRANSITIVE** | Gate 2 on the *blocked* name, not the top-level one | +| Used to install, now blocked (or was blocked, now works) | **DRIFT / TIMELINE** | both audits across time; a live compliance check is the *only* "now" signal | +| Still blocked despite an approved waiver | **WAIVER-NOT-APPLIED** | Gate 2 + `jfs_curation_query_waiver_requests` (check scope, status, expiry) | +| "Pending Catalog update" reason, or intermittent/flaky blocks | **PENDING / TRANSIENT** | the relevant audit filtered by `reason` | +| Anything else curation-related, or unclear | **OTHER** | gather from **both** audits + the client error text, then map via the Step 4 RCA table | + +## Step 1 — Rule out the non-curation cause first + +Don't assume curation — and for **NOT-RESOLVABLE**, decide existence per the **#1 RULE**. + +**Rule out client-side config first.** A resolve failure can look identical to a +curation block while having nothing to do with curation — the package manager's +own local filters produce the same symptom shape (`ETARGET`, no matching +version). Before trusting the symptom table's default routing, check whether +the client config explains it: a date/version-age filter (npm's `before` / +`min-release-age`, surfaced via `npm config get before` and `npm config get +min-release-age`), a registry override in `.npmrc` / `pip.conf` / `nuget.config` +/ `GOPROXY`, or a CI-injected flag. This applies **regardless of the exact +error string** — don't wait for a table row that matches your specific +package manager's wording. + +Settle it with **two independent reads — run BOTH, and never gate one behind the +other**: + +1. **Does it exist?** Use **only** `catalog_packages_list_versions` (approved MCP + tool). **Never** query a public/upstream registry directly, **never** the + curated `/artifactory/...` registry, and **never** `artifactory_packages_get_versions` + (curated/cached = **filtered** listing — same trap). Two coverage notes: + - **Name translation:** the catalog token differs for some CVS types — **Go → + `golang`**, **Gems → `rubygems`**. npm / PyPI / Maven / NuGet are the same. + - The catalog covers only some types; for an **unsupported** type it returns + nothing — a catalog miss there is **not** proof of non-existence. Fall back + to the CVS audit (read 2); if that's also empty, say the existence check is + unavailable rather than guessing. +2. **What did Curation decide?** For a **NOT-RESOLVABLE** symptom, **always** query + the CVS audit (`jfs_curation_query_cvs_audit_events`, package + type) — do not + skip it because a listing looked empty. It is the authoritative record of whether + CVS inspected the version and blocked or approved it: it both proves the version + existed and gives the verdict. (For a **403 / download** symptom, the reverse + holds — do **not** query the CVS audit; use the download audit only. See Gate 2 + and the 403 worked example. A **403 with nothing resolvable** is Gate 1 + too — query **both** audits, per the two-gate model above.) + +Read the cross-product: + +- **In the catalog (or a CVS record exists) + CVS blocked it** → **curated-index + filtering** (the usual answer): CVS blocked the version **and** the client can't + resolve it. The audit proves *blocked* (a decision); the **client's ETARGET / + absence** is what proves it's missing from the listing — the audit does not state + listing membership, so don't claim omission unless the client listing actually + lacks the pin. Together → **Gate 1** for the policy detail and the compliant + version CVS selected. +- **In the catalog + no CVS record** → not (yet) curation — remote sync / virtual + repo / cache config; investigate outside CVS. +- **Not in the catalog (and the catalog supports this package type) + no CVS + record** → likely never existed / yanked → not curation; report the nearest + catalog versions (e.g. `1.18.1`, `0.19.0`) and stop. If the catalog does + **not** support this type, a miss is not proof of non-existence — report the + existence check as unavailable (Insufficient evidence) instead. + +A resolve-time ETARGET fails **in the client**: it mints **no** CVS or download +audit row and there is **no** 403 to find. The CVS evidence is a +**prior/historical** evaluation (when the package was last inspected) — never +expect a row created by this failed install, and never read its absence as +"curation not involved". + +For a **403** symptom the non-curation confounders are auth / permissions / a +disabled repo — rule those out, then Gate 2. + +## Step 2 — Gather evidence + +Pick the branch from Step 0. When in doubt, run both; they are independent. + +**Identify the curated remote.** The audits are keyed by the curated **remote**, +never a virtual — so the repo in the client URL is a lookup key, not the answer. +Capture it as `client_repo` (from the failing URL, e.g. `/api/npm//…`); a +virtual (some managers, e.g. Go, require one) is **never** the curated remote key, +so do **not** pre-filter the audit on it. Read the candidate remotes from the +returned events (`curated_repo_name` for CVS, `curated_repository_name` for the +download audit) within the failure window. + +When `client_repo` is a virtual, or more than one remote has a contemporaneous +event, resolve which remote in this order — **never infer from names** +(`npm-virtual` does not imply `npm-remote`): + +1. **Artifactory join (preferred).** Call `artifactory_repositories_get(client_repo)` + and intersect its member repositories with the candidate remotes from the audit. + A single match is the remote. +2. **Ask (fallback).** If that tool is not accessible (e.g. a 403/permission error), + or the intersection is empty or still more than one, **ask the user to confirm + the underlying remote** they resolved through. Do not guess. + +Until the remote is proven by (1) or (2), do **not** put a repo in the RCA +**Repository** field — and never the virtual name — and cap that field at +**Medium** confidence. The *what* (policy/verdict) can still be **High** when every +candidate remote carries the same verdict, even while the Repository stays Medium. + +**Window to the failure time** (from Step 0), not the 7-day default — far fewer +day-partitions scanned on a busy tenant, and required when the failure is >7 days old. +The survey bounds `created_at_start`/`created_at_end` are **RFC3339 strings** (e.g. +`2026-08-18T03:33:00Z`), **not** epoch — epoch ms is only for +`jfs_curation_get_cvs_audit_version_detail.created_at` (Gate 1 step 3); don't carry that +format into the survey or it 400s / returns empty. + +- **Gate 2 (403):** the block is contemporaneous with the failure, so window it + **as tightly as your timestamp precision allows** — with a precise time (e.g. from + the client log), use only a clock-skew margin (**±15 min**, at most ±1 h); widen + toward ±1 day **only** when the failure time is approximate ("sometime that day"). + A tight window also avoids sweeping in unrelated contemporaneous events (e.g. a + second remote's block for the same package), which is what forces the + remote-disambiguation step — so don't widen it past what the timestamp warrants. +- **Gate 1 (CVS):** the evaluation happens when Artifactory (re)builds the + **cached** curated metadata, so it can be **hours or days before** the failed + resolve — its timestamp has no fixed relation to the failure clock. Do **not** + window symmetrically or tightly: reach **back** — set `created_at_end` to just + after the failure, `created_at_start` to **failure − 7 days** (one 168h window), + `direction=desc`, and take the **latest event at or before the failure**. Cap the + reach-back at 7 days; do **not** page further. If that window is empty, report + **"no CVS record in the 7 days before the failure"** (the cached inspection is + older than 7 days) — not "no CVS" outright. + +### Gate 1 — NOT-RESOLVABLE (CVS audit) + +Target the **historical** CVS evaluation — the failed install created no new row. +The pin may be **absent** from `versions[]` because resolution stopped before it +(see **How Curation works** above), not because it was "never evaluated". + +1. **Survey** with `jfs_curation_query_cvs_audit_events` (do **not** set `include_versions` + yet). `package_name` and `package_type` are **required** (no unscoped + surveys). Add `inspection_results=no_compliant_versions,compliant_version_selected` + (comma-separated IN — both failure buckets in one call), and use `direction=desc` with a + small `num_of_rows` for the latest evaluation. **Never** survey + `all_versions_compliant` to explain a failure (~99% of the data); if you ever + query it, cap the window to **≤1 day**. Set `include_total` only when you need the + match count or an exact `next_offset` (to gauge volume or paginate) — it adds a + count query, so skip it for a plain latest-event lookup. + Read `packages[].summary`, `inspection_results`, `latest_inspected_version`. +2. **Drill down** on the event you care about: call again with the same + `package_name` + `package_type` plus `event_id=`, + `include_versions=true`, `num_of_rows` ≤ 10. Find the target version in + `packages[].versions[]` and read its `action` + blocking `policies[]`. +3. **Explain** one version with `jfs_curation_get_cvs_audit_version_detail` + (`version_id=versions[].id`, `created_at=` the **event's** `created_at` in + **epoch milliseconds** — copy it verbatim). + +### Gate 2 — DOWNLOAD-BLOCKED (package-download audit) + +1. **Search** `jfs_curation_query_audit_events` by `package_type` + `package_name` + (+ `package_version` if known) over the relevant window (add `include_total=true` + only if you want the match count). +2. **Explain** the blocking event with `jfs_curation_get_audit_event_policies` + (`event_id=data[i].id`) for the `explanation` + `remediation`. These fields can + come back **empty** (policy/condition names and verdict only) — that does **not** + lower confidence: when the audit's policy/condition agrees with the client's + block notice, the finding is still **High**. Fall back to the Step 4 generic + package-blocked remediation and note "no remediation text from the API". + +### Optional live compliance check — only when it earns its place + +`jfs_curation_check_remote_package_compliance` is **not** a step in the RCA. It is a live, +current-policy verdict that runs a fresh evaluation (see the +timeline note above), so it does not tell you what happened at the time of the +failure and it can muddy a clean audit-based conclusion. Call it **only** when: + +- the user asks "what can I install instead?" **and** the audit did not already + surface allowed/suggested versions (the client's own block notice often does); +- you specifically need a **current-vs-past drift** check (e.g. confirm the block + still stands under today's policy); or +- you are about to **change a dependency or recommend a pin** and must validate + the candidate live. + +Otherwise skip it. When you do call it, report `allowed_versions` and the verdict +as **current state**, kept separate from the audit-based root cause. + +## Step 3 — Interpret + +Each tool's own description lists its traps; read them before concluding. The +ones that most often flip an RCA: + +- **Empty CVS result → first rule out "curation not enabled".** The audit tools + are **not** feature-gated (they need only `VIEW_POLICIES`), so a disabled feature + writes no rows and returns `200` with empty `data` — indistinguishable from + "nothing blocked". +- **For a 403, the download audit is the authority; CVS coverage is partial.** + Because resolution stops at the first approved version (see **How Curation + works** above), absence from a CVS `versions[]` does **not** mean a version + was unevaluated, out of the index, or approved — and it must **never** weaken + or override a `jfs_curation_query_audit_events` finding. Versions CVS did not + (re)select stay in the index and are enforced by the download flow — that is + the 403. +- **Bad `inspection_results`/`reason` tokens are silently ignored** → a 200 + **unfiltered** result. Verify returned rows carry the value you filtered on. +- **Do not invent or generalize a mechanism.** The audit reports the *decision* + on a version (blocked/compliant + policy + reason), not why the client listing + contains or omits it. Never infer a general rule from a few events — e.g. + "CVE policies enforce at download, ban-label at resolve" from two rows. A + policy's category does not determine listing membership or enforcement stage; + read each event's own fields. If the behavior in question (listing/exposure, + enforcement stage) is not stated in the audit rows or the CVS doc, report it as + **not determined** — do not synthesize an explanation to fit the data points. + +## Before Step 4 — output gate + +No conclusion (even hedged) without these tool calls made and reflected in +Evidence: + +- Existence in question (NOT-RESOLVABLE, or any 403 with **nothing** + resolvable): `catalog_packages_list_versions` (the #1 RULE). +- A 403 on a version the client already **resolved/listed**: existence is + proven by that successful listing — the catalog call isn't required; go + straight to the download audit. +- NOT-RESOLVABLE, or any 403 with nothing resolvable: `jfs_curation_query_cvs_audit_events`. +- Any resolvable/listed 403 (DOWNLOAD-BLOCKED, PACKAGE-BLOCKED on a still-listed + name, TRANSITIVE): `jfs_curation_query_audit_events`. +- WAIVER-NOT-APPLIED: + `jfs_curation_query_waiver_requests`. + +Missing/unavailable tool ≠ license to guess — use Insufficient evidence instead. + +## Step 4 — Conclude the RCA + +Map the evidence to a root cause. Report the **first** row that matches. + +| Evidence | Root cause | Remediation | +|---|---|---| +| Version not in the public catalog (for a package type the catalog supports) and no CVS record | **Not curation** — version does not exist | Correct the version; nothing to unblock. | +| ETARGET on a curated registry; version present in the **public catalog** but absent from the **curated** listing; CVS history shows it filtered / not selected | **Curated-index filtering** — CVS omitted the version from the curated listing, so the client can't resolve it (fails at resolve, no 403, no fresh audit row) | Use a version present in the curated listing (one CVS selected / `allowed_versions`); or waiver and re-index. | +| CVS event: target version `action=blocked` + policy P | **CVS non-compliant** — CVS found the version non-compliant under policy P (this is the decision; it does not by itself explain listing membership) | Apply the version's `remediation` from `jfs_curation_get_cvs_audit_version_detail`; pin to an `action=approved` version from the same event (or `allowed_versions`). For waiver applicability under CVS, consult the CVS doc — do not assume. | +| CVS event: `inspection_results=no_compliant_versions` | **No compliant version found** in the audit | Check `jfs_curation_check_remote_package_compliance` for any allowed version; otherwise raise a waiver (see the CVS doc for applicability). Do not recommend changing the policy. | +| Package-audit event: version `action=blocked` + policy P | **Download-time block** — the 403 comes from policy P at download | Apply the policy's `remediation` from `jfs_curation_get_audit_event_policies`; use an allowed version; or raise a waiver. | +| Locked/older pin 403s at download; the recent CVS record selected a *newer* compliant version and has no (or only a stale/earlier) record for the pin | **Pin bypasses CVS selection** — CVS returned a newer compliant version and did not (re)select the pinned one, so the pin stays in the index and is enforced by the download-time policy (the 403). The CVS absence is expected, **not** evidence of anything | Use the compliant version CVS selected (or `allowed_versions`), or waiver the pin. The download audit + `jfs_curation_get_audit_event_policies` is the authority for the block. | +| Block covers the package itself, not a single version | **Package-blocked** — a policy blocks the whole package | Use a different package, or waiver the package if policy allows (see the block's `remediation`). | +| The blocked package name ≠ the one the user installed | **Transitive dependency block** — a dependency was blocked | Re-run the RCA on the *blocked* dependency; remediate it (pin/override the transitive) rather than the top-level package. | +| Approved waiver exists (`jfs_curation_query_waiver_requests`) yet the block persists | **Waiver not effective** — wrong scope/repo, expired, not yet applied, or the index has not refreshed | Verify the waiver's scope, status, and expiry against the failing repo/version. If it's valid, the index likely hasn't re-evaluated — no MCP tool here can force this; tell the **user** to reindex / zap the cache on the curated remote and retry after they do. | +| `reason = "Pending Catalog update"` | **Transient / pending** — the verdict awaits catalog data | Retry after the catalog updates; treat as not-final, not a hard block. | +| CVS `approved` **and** a download-time block exist for the same version | **Divergence** — the version is CVS-approved yet blocked at download; causes include a policy change between evaluation and download, or a different policy set on the download path (see the CVS doc) | Confirm current status via `jfs_curation_check_remote_package_compliance`; use a curation-compliant version or raise a waiver. Do not recommend changing the policy. | +| No CVS **and** no package-audit record, version exists upstream | **Likely not curation** (write lag, CVS disabled or not applicable, no scoped policy, unsupported type) | For the CVS audit, confirm the window already reached the full 7 days (Gate 1's cap) — do not widen past 168h or drop `package_name`/`package_type`. For the download audit, you may widen using the multi-window probe strategy and drop optional filters, but keep `package_type`. If still empty at those caps, investigate outside curation. | + +**Failure Reason lookup** — map the matched root cause above to the one **Failure +Reason** you print below (several root causes map to the same label): + +- **Version not available — removed by curation:** Curated-index filtering; CVS + non-compliant; No compliant version found. +- **Download blocked by a curation policy:** Download-time block; Pin bypasses CVS + selection; Divergence (CVS-approved yet blocked at download); Waiver not effective + (add "waiver not applied" in Failure Details). +- **Whole package blocked by curation:** Package-blocked. +- **Curation check still pending:** Transient / pending ("Pending Catalog update"). +- **Not caused by curation:** version does not exist; likely-not-curation (remote + sync / cache / no scoped policy / unsupported type); a client-side date/version + filter (`min-release-age` / `before`, registry override, CI flag) explains it. +- **No curation record found:** no CVS and no package-audit record in window. +- **Transitive** is not its own label — re-run the RCA on the *blocked dependency* + and use whichever Failure Reason its own root-cause row maps to. + +**If the root cause is "Not caused by curation" or "No curation record +found," skip the table below — use the "Not a curation problem" block after +Insufficient evidence instead.** The table's fields are curation-specific +(Repository, Audit event links) and don't apply to a non-curation cause. + +For the other four Failure Reasons, present the conclusion as a **table** — +render exactly these rows, in this order. Frame every value in **curation** +terms (this is a curation RCA, not a generic one): + +| Field | Value | +| --- | --- | +| **Confidence** | **High** / **Medium** / **Low** — ``. Report **Low** when a step returned an ambiguous empty result rather than a positive record; the *what* can stay **High** while **Repository** is **Medium** until the virtual→remote join is proven (Step 2). | +| **Package** | `` | +| **Version** | `` (or `all` / the failing pin) | +| **Failure Reason** | one of: **Version not available — removed by curation** · **Download blocked by a curation policy** · **Whole package blocked by curation** · **Curation check still pending** · **Not caused by curation** · **No curation record found** | +| **Failure Details** | Failed due to curation policy `` — `` (e.g. "Failed due to curation policy `cve` — cve more than 7 and Half") | +| **Repository** | the remote from whichever audit you used — `` (CVS) or `` (download audit) | +| **Failure Timestamp** | the exact failure timestamp, e.g. `2026-08-18T03:33:00Z` | +| **Audit event links** | the Curation **UI audit page** — `/ui/package-curation/audit` (`` = the platform host from the failing URL / connected tenant) — plus the event(s) that locate the record there: CVS `jfs_curation_query_cvs_audit_events` event `id=` (`created_at=`), download `jfs_curation_query_audit_events` event `id=` | + +Then, **below the table, as bullet points** (not table rows): + +- **Ruled out:** the non-curation causes you checked — auth / permissions, repo or remote config, a version that never existed. +- **Evidence:** discrete `key=value` tokens on one line, each individually searchable/copyable — e.g. `tool=jfs_curation_query_cvs_audit_events` `event_id=1290` `created_at=1786000000000` `action=blocked` `policy=cve`. Emit only the tokens you have. +- **Remediation** *(High confidence only — omit entirely at Medium/Low):* use a curation-**compliant** version (the one CVS selected, or an `allowed_versions` entry), or **raise a waiver** scoped to the curated remote(s) you resolve through. **Never** propose changing or loosening a curation policy (e.g. "lower the CVE threshold in policy `cve`") — a policy change is out of scope for this RCA; the waiver is the only sanctioned exception. + +**Insufficient evidence** *(use instead of the table above when the output +gate stopped you)*: state **Cannot conclude** — name the missing tool call(s) +or ambiguity (tenant, repo, tool unavailable) — and what's needed to proceed. +Never fill the table with a guess to avoid this. + +**Not a curation problem** *(use instead of the table above for "Not caused +by curation" / "No curation record found")*: state the actual cause and the +evidence that rules out curation — e.g. `npm config get before` / +`min-release-age` returned a value matching the error, or no CVS/download +record exists in a widened window. No curation-specific fields — Repository +and Audit event links don't apply here. + +**Consistency gate — before you finalize.** Re-check the conclusion against +**every** event you retrieved, not just the ones that fit. If any row contradicts +it, the conclusion is wrong — revise it to fit all the data. A conclusion that +requires an invented mechanism to reconcile two rows is a red flag: prefer "not +determined from the audit (see the CVS doc)" over a synthesized rule. + +## Worked examples (illustrative) + +Two end-to-end illustrations. They are **not** the supported set — apply the same +method (Step 0 → Step 4) to any curation symptom, including the classes above +that have no example here. + +### npm `ETARGET` + +> `No matching version found for @vitest/eslint-plugin@1.6.27` + +`ETARGET` → NOT-RESOLVABLE (Gate 1). **First** apply Step 1's two reads — do +**not** trust `npm view` against the curated registry (that's the filtered +view). (1) Existence via `catalog_packages_list_versions` (the only approved +oracle — do not query public npm directly) — absent there → "version does not +exist", report the nearest catalog versions, stop. (2) **Always** survey the CVS audit +`jfs_curation_query_cvs_audit_events(package_name, package_type="npm")` with +`direction=desc` for the latest **historical** evaluation (this failed install minted +no row). Drill down (`event_id`, `include_versions=true`) and look for `1.6.27`: +`action=blocked` → CVS found it non-compliant; combined with the client's ETARGET +(the pin is absent from the curated listing), that's curated-index filtering +— get `explanation`/`remediation` via `jfs_curation_get_cvs_audit_version_detail` and pin to +the version CVS selected (check the CVS doc before assuming a waiver applies); +**absent** from `versions[]` → likely CVS stopped after selecting its compliant +version(s) and never recorded this pin (Step 3), not "never evaluated". Conclude +per Step 4. + +### 403 on a version that `npm view` lists + +> `npm view` lists `axios@1.15.1`, but `npm install axios@1.15.1` fails with +> 403 "blocked by JFrog Packages Curation Service" (policy: CVE). User claim: +> "it's listed, so CVS approved it — why does download fail?" + +403 on a **resolvable** version → **DOWNLOAD-BLOCKED (Gate 2)**. The version +is already listed, so resolvability — and therefore existence, for this +purpose — is proven by the client's own successful listing; Step 1's +catalog/CVS existence check is for when resolvability itself is in question, +so it doesn't apply here. Then Gate 2 **alone**: +`jfs_curation_query_audit_events(package_type="npm", package_name="axios", package_version="1.15.1")` +returns the blocked download event with the policy and CVEs — that **is** the +root cause. If the user wants the remediation prose, add +`jfs_curation_get_audit_event_policies(event_id)`. + +Answer the misconception without a compliance call: a `npm view` listing is what +the client resolves against, **not** a per-version CVS approval — a locked/exact +pin to a non-compliant version is refused at download with no fallback (fetch the +CVS doc to cite this). You do **not** need `jfs_curation_check_remote_package_compliance` to +conclude this, and you should not query the CVS audit to "prove approval" — CVS +audit answering `all_versions_compliant` is a *different timeline and mechanism* +and will confuse the 403 RCA. Only if the user then asks "what should I install +instead?" and no allowed versions are already in hand does a compliance check (or +the client's own suggested-versions notice) earn its place. + +## Gotchas + +- **MCP tools only.** Never substitute a `jf` command or raw HTTP for any + `jfs_curation_*` call. If a curation tool specifically is unavailable while + other JFrog MCP tools work, that's a version/entitlement gap, not a + disconnected server — see Preflight. +- **Disjoint id-spaces / repo-name spelling.** CVS event ids go to + `jfs_curation_get_cvs_audit_version_detail`, package-audit ids to + `jfs_curation_get_audit_event_policies` (crossing them returns nothing). CVS uses + `curated_repo_name`; package audit uses `curated_repository_name`. +- **Don't over-attribute to curation.** The most common `ETARGET` cause is a + version that never existed, or a client-side config filter — always complete + Step 1 before blaming curation. diff --git a/plugins/jfrog/skills/jfrog-reference-architecture/SKILL.md b/plugins/jfrog/skills/jfrog-reference-architecture/SKILL.md index 394d000..b331b24 100644 --- a/plugins/jfrog/skills/jfrog-reference-architecture/SKILL.md +++ b/plugins/jfrog/skills/jfrog-reference-architecture/SKILL.md @@ -11,7 +11,7 @@ description: >- Do NOT use for artifact search or download, repository or permission admin, CVE or vulnerability lookups, live jf CLI operations against their instance, or package curation — use the jfrog base skill or - jfrog-package-safety-and-download instead. + jfrog-package-curation instead. compatibility: >- Requires outbound HTTPS (WebFetch or equivalent). Request full_network when the runtime blocks fetches. No jf CLI or configured JFrog instance required diff --git a/plugins/jfrog/skills/jfrog/SKILL.md b/plugins/jfrog/skills/jfrog/SKILL.md index 2341fe3..460d111 100644 --- a/plugins/jfrog/skills/jfrog/SKILL.md +++ b/plugins/jfrog/skills/jfrog/SKILL.md @@ -19,7 +19,7 @@ compatibility: >- (CLI) and Tier 3 (jf api) operations; without it, only MCP (Tier 1) is available. metadata: role: base - version: "0.29.0" + version: "0.31.1" --- # JFrog Skill diff --git a/plugins/jfrog/skills/jfrog/references/INDEX.md b/plugins/jfrog/skills/jfrog/references/INDEX.md index c005c53..b40a61f 100644 --- a/plugins/jfrog/skills/jfrog/references/INDEX.md +++ b/plugins/jfrog/skills/jfrog/references/INDEX.md @@ -50,7 +50,9 @@ for one operation. - **Watches, policies, violations, components, or vulnerability scanning (concepts)**: read `references/xray-entities.md` - **Exposures scanning results (secrets, IaC, service misconfigurations, application security risks)**: read `references/xray-entities.md` § Exposures (Advanced Security) -- **Curation audit events (approved/blocked packages, dry-run policy evaluations, curation export)**: read `references/xray-entities.md` § Curation audit events +- **Package install/resolve/download failure blamed on Curation — 403 from a curated remote, `ETARGET` / "no matching version", a version missing from the index, "why was package X blocked?", or a waiver after a block**: load the `jfrog-package-curation` workflow skill and follow its "Troubleshoot a failure" section (index-time CVS audit + download-time package audit → root cause + remediation, MCP only). Its "Check & download" section instead handles proactive "is this package safe to download?". +- **Is this package safe / allowed / curated? Downloading npm, Maven, PyPI, Go, or similar packages via JFrog**: load the `jfrog-package-curation` workflow skill and follow its "Check & download" section. +- **Curation audit events (approved/blocked packages, dry-run policy evaluations, curation export) without a failure to triage**: read `references/xray-entities.md` § Curation audit events ## Release lifecycle & distribution @@ -60,7 +62,7 @@ for one operation. ## Catalog - **Public or custom catalog, package metadata, vulnerability advisories, licenses, OpenSSF, or MCP services (concepts)**: if the JFrog MCP server exposes a catalog tool, prefer it for single-package lookups. For deeper queries, read `references/catalog-entities.md` -- **CVE details, vulnerability lookup by CVE ID, or severity/affected-packages/fix-versions for a specific CVE**: prefer an MCP vulnerability-lookup tool if the JFrog MCP server exposes one. Otherwise read `references/onemodel-query-examples.md` § *Public security domain* for the `searchVulnerabilities` query shape — this is self-contained; do not load the `jfrog-package-safety-and-download` skill for pure CVE lookups +- **CVE details, vulnerability lookup by CVE ID, or severity/affected-packages/fix-versions for a specific CVE**: prefer an MCP vulnerability-lookup tool if the JFrog MCP server exposes one. Otherwise read `references/onemodel-query-examples.md` § *Public security domain* for the `searchVulnerabilities` query shape — this is self-contained; do not load the `jfrog-package-curation` skill for pure CVE lookups ## OneModel (GraphQL)