diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 508cc38..ce7c20b 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "jfrog", "displayName": "JFrog", "description": "Official JFrog plugin. Connect Claude Code to JFrog to manage, secure, and govern your software supply chain. Give agents the context to build secure, compliant software.", - "version": "0.3.0", + "version": "0.3.1", "author": { "name": "JFrog Ltd.", "email": "devrel@jfrog.com", 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 c4e69f9..70083b3 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,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 `${JFROG_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. | | **Hook** | Plugin MCP rewrite | On SessionStart / FileChanged, rewrite discovered installed-plugin `.mcp.json` files through Agent Guard (`--rewrite-mcp-json`) so stdio MCP entries launch via `@jfrog/agent-guard`. | | **Hook** | Skill Governance (Preview) | Evaluates the skills Claude invokes against your organization's JFrog skill governance policies, and blocks the ones that violate them — showing which policies were violated and the command to request a waiver. Enforcement runs in the JFrog Agent Guard; the hook only carries the event to it. | @@ -41,7 +41,7 @@ Before installing, make sure you have: - **Claude Code CLI** (≥ 1.0) — The Claude Code CLI. - **Node.js** (≥ 18) — with `npx` on your `PATH` (used by the Agent Guard). Without it Skill Governance cannot run, and governed actions are allowed unchecked. - **Git Bash on Windows** — the Skill Governance hook runs as a Bash command so that it behaves identically on every platform. Install [Git for Windows](https://git-scm.com/downloads/win); without it the hook cannot run and governed actions are allowed unchecked. Not needed on macOS or Linux. -- **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 AI Catalog** (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_URL`/`JFROG_ACCESS_TOKEN` env vars. Older CLIs don't support the `--format` flag used by `jf config show` for this. - **JFrog project** (optional) — If you want to use the Agent Guard feature. @@ -151,7 +151,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 0c48fcc..ae0d63a 100644 --- a/VENDOR.md +++ b/VENDOR.md @@ -7,7 +7,7 @@ The skill packages under `skills/` are vendored from **[jfrog/jfrog-skills](http | **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/skills/jfrog-init/SKILL.md b/skills/jfrog-init/SKILL.md index 6fbde42..80760bd 100644 --- a/skills/jfrog-init/SKILL.md +++ b/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/skills/jfrog-init/references/flow-diagram.md b/skills/jfrog-init/references/flow-diagram.md index 4a1cb54..6f77852 100644 --- a/skills/jfrog-init/references/flow-diagram.md +++ b/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/skills/jfrog-init/references/how-to-ask-user.md b/skills/jfrog-init/references/how-to-ask-user.md new file mode 100644 index 0000000..db3cbf2 --- /dev/null +++ b/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/skills/jfrog-init/references/mcp-plugin-config.md b/skills/jfrog-init/references/mcp-plugin-config.md index 01a818f..39ac6dc 100644 --- a/skills/jfrog-init/references/mcp-plugin-config.md +++ b/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/skills/jfrog-init/references/node-install-prompt.md b/skills/jfrog-init/references/node-install-prompt.md index db8baa2..d58bd79 100644 --- a/skills/jfrog-init/references/node-install-prompt.md +++ b/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/skills/jfrog-init/references/script-invocation.md b/skills/jfrog-init/references/script-invocation.md index e1c6def..e6595d4 100644 --- a/skills/jfrog-init/references/script-invocation.md +++ b/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/skills/jfrog-init/references/server-picker.md b/skills/jfrog-init/references/server-picker.md index 0e46e2d..b5be11b 100644 --- a/skills/jfrog-init/references/server-picker.md +++ b/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/skills/jfrog-init/scripts/jfrog-detect-jfrog-mcp.mjs b/skills/jfrog-init/scripts/jfrog-detect-jfrog-mcp.mjs index 510457f..35f1351 100755 --- a/skills/jfrog-init/scripts/jfrog-detect-jfrog-mcp.mjs +++ b/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/skills/jfrog-init/scripts/jfrog-reinstall-jfrog-plugin.mjs b/skills/jfrog-init/scripts/jfrog-reinstall-jfrog-plugin.mjs index 062785c..f115185 100755 --- a/skills/jfrog-init/scripts/jfrog-reinstall-jfrog-plugin.mjs +++ b/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/skills/jfrog-init/scripts/jfrog-resolve-mcp-config.mjs b/skills/jfrog-init/scripts/jfrog-resolve-mcp-config.mjs index f38878b..14d7e0e 100755 --- a/skills/jfrog-init/scripts/jfrog-resolve-mcp-config.mjs +++ b/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/skills/jfrog-init/scripts/jfrog-substitute-mcp-placeholders.mjs b/skills/jfrog-init/scripts/jfrog-substitute-mcp-placeholders.mjs index 8bd635e..c75b6f0 100755 --- a/skills/jfrog-init/scripts/jfrog-substitute-mcp-placeholders.mjs +++ b/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/skills/jfrog-init/scripts/lib/jf.mjs b/skills/jfrog-init/scripts/lib/jf.mjs index 3a1206a..34d3684 100755 --- a/skills/jfrog-init/scripts/lib/jf.mjs +++ b/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/skills/jfrog-mcp-management/SKILL.md b/skills/jfrog-mcp-management/SKILL.md index 8f38454..d70fbe5 100644 --- a/skills/jfrog-mcp-management/SKILL.md +++ b/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/skills/jfrog-mcp-management/references/agent-guard-common.md b/skills/jfrog-mcp-management/references/agent-guard-common.md index f060bf9..1d1e1e3 100644 --- a/skills/jfrog-mcp-management/references/agent-guard-common.md +++ b/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/skills/jfrog-mcp-management/references/harness-common.md b/skills/jfrog-mcp-management/references/harness-common.md index 850a365..febf55a 100644 --- a/skills/jfrog-mcp-management/references/harness-common.md +++ b/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/skills/jfrog-mcp-management/references/harness-devin.md b/skills/jfrog-mcp-management/references/harness-devin.md index 49616c1..d4800ca 100644 --- a/skills/jfrog-mcp-management/references/harness-devin.md +++ b/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/skills/jfrog-mcp-management/references/persisting-env-vars.md b/skills/jfrog-mcp-management/references/persisting-env-vars.md index 1460227..7c0c49c 100644 --- a/skills/jfrog-mcp-management/references/persisting-env-vars.md +++ b/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/skills/jfrog-mcp-management/scripts/jfrog-agent-guard-check.mjs b/skills/jfrog-mcp-management/scripts/jfrog-agent-guard-check.mjs index 51f142d..0024321 100644 --- a/skills/jfrog-mcp-management/scripts/jfrog-agent-guard-check.mjs +++ b/skills/jfrog-mcp-management/scripts/jfrog-agent-guard-check.mjs @@ -32,16 +32,8 @@ const debug = (message) => { }; // New JFROG_* env vars take precedence over the legacy JF_* names. -const env = (newName, oldName) => { - const pick = (name) => { - if (!name) return undefined; - const raw = process.env[name]; - if (typeof raw !== "string") return undefined; - const trimmed = raw.trim(); - return trimmed || undefined; - }; - return pick(newName) ?? pick(oldName); -}; +const env = (newName, oldName) => + process.env[newName] ?? (oldName ? process.env[oldName] : undefined); const enabled = (reason) => { process.stdout.write(`Enabled: ${reason}\n`); diff --git a/skills/jfrog-mcp-management/scripts/jfrog-agent-guard-env-probe.mjs b/skills/jfrog-mcp-management/scripts/jfrog-agent-guard-env-probe.mjs new file mode 100644 index 0000000..14f832e --- /dev/null +++ b/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/skills/jfrog-package-safety-and-download/SKILL.md b/skills/jfrog-package-curation/SKILL.md similarity index 80% rename from skills/jfrog-package-safety-and-download/SKILL.md rename to skills/jfrog-package-curation/SKILL.md index 0adb9cf..300f785 100644 --- a/skills/jfrog-package-safety-and-download/SKILL.md +++ b/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/skills/jfrog-package-curation/references/rca-procedure.md b/skills/jfrog-package-curation/references/rca-procedure.md new file mode 100644 index 0000000..3a8f24b --- /dev/null +++ b/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/skills/jfrog-reference-architecture/SKILL.md b/skills/jfrog-reference-architecture/SKILL.md index 394d000..b331b24 100644 --- a/skills/jfrog-reference-architecture/SKILL.md +++ b/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/skills/jfrog/SKILL.md b/skills/jfrog/SKILL.md index 2341fe3..460d111 100644 --- a/skills/jfrog/SKILL.md +++ b/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/skills/jfrog/references/INDEX.md b/skills/jfrog/references/INDEX.md index c005c53..b40a61f 100644 --- a/skills/jfrog/references/INDEX.md +++ b/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)