diff --git a/.devin-plugin/plugin.json b/.devin-plugin/plugin.json index 206d958..119e26d 100644 --- a/.devin-plugin/plugin.json +++ b/.devin-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "jfrog", "displayName": "JFrog Platform", - "version": "0.3.2", + "version": "0.3.3", "description": "Official JFrog plugin for Devin. Ships the JFrog skills bundle (platform ops, init, MCP management, AI Catalog, package safety, reference architecture, package-manager setup) and registers the JFrog Platform MCP server (remote HTTP + OAuth).", "author": { "name": "JFrog Ltd.", diff --git a/.github/scripts/sync-skills-vendor.json b/.github/scripts/sync-skills-vendor.json index acab867..19c5843 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.30.0", + "pin": "v0.32.0", "paths": [ "skills" ] diff --git a/skills/jfrog-ai-catalog/references/installing-skills.md b/skills/jfrog-ai-catalog/references/installing-skills.md index 5566250..8c105ec 100644 --- a/skills/jfrog-ai-catalog/references/installing-skills.md +++ b/skills/jfrog-ai-catalog/references/installing-skills.md @@ -52,6 +52,13 @@ its stdout as ``. Parse the `tool=` field from `` and map it to a If `tool` is `unknown`, empty, or not in the table — do **not** guess. Ask the user for the desired install path and use `--path ` instead. +**Exception — Kiro (install only):** if you're self-identified as Kiro (IDE +or `kiro-cli`, per your system prompt — `check-environment.sh` doesn't +detect it), `--harness kiro` is rejected by `jf`, so skip asking and use +`--path` with `.kiro/skills` (project) / `~/.kiro/skills` (global, or +`$KIRO_HOME/skills` if `KIRO_HOME` is set) directly. This exception does not +extend to `jf skills list` — see *List currently installed skills* in +`managing-installed-skills.md`. Choose exactly one install target (these are mutually exclusive): diff --git a/skills/jfrog-ai-catalog/references/managing-installed-skills.md b/skills/jfrog-ai-catalog/references/managing-installed-skills.md index 026c683..f51be99 100644 --- a/skills/jfrog-ai-catalog/references/managing-installed-skills.md +++ b/skills/jfrog-ai-catalog/references/managing-installed-skills.md @@ -16,6 +16,14 @@ jf skills list --server-id "" --harness "" --check-updates ``` Resolve `` to the current agent (see `installing-skills.md`). +**Exception — Kiro:** `--harness kiro` errors `unknown agent`, and `list` has +no `--path` flag. List directly from the filesystem instead: skill directory +names under `.kiro/skills` (project) / `~/.kiro/skills` (global, or +`$KIRO_HOME/skills`), each containing a `SKILL.md`. Version/description +aren't available this way — omit those columns. This is a filesystem +inventory only: a directory with a `SKILL.md` cannot confirm the skill was +installed via `jf skills install` — a manually added skill looks identical. +Present it as such rather than implying AI Catalog provenance. **Never run a bare `jf skills list`** because it errors. Always pass `--harness ` (installed skills) or `--repo ` (registry contents). `--check-updates` is only supported with `--harness` (not with `--repo`). Merge diff --git a/skills/jfrog-mcp-management/SKILL.md b/skills/jfrog-mcp-management/SKILL.md index d70fbe5..c49aded 100644 --- a/skills/jfrog-mcp-management/SKILL.md +++ b/skills/jfrog-mcp-management/SKILL.md @@ -11,19 +11,26 @@ description: >- 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. -allowed-tools: Bash(claude mcp list) Bash(cursor agent mcp list) Bash(codex mcp list) Bash(opencode mcp list) Read Edit Write + Requires network access to the JFrog Agent Guard npm registry (not public + npm) and the JFrog platform, and ~/.jfrog/ write access for OAuth login and + cache cleanup. +allowed-tools: Bash(claude mcp list) Bash(cursor agent mcp list) Bash(codex mcp list) Bash(opencode mcp list) Bash(kiro-cli mcp list) Bash(kiro-cli mcp list workspace) Bash(kiro-cli mcp list global) Bash(kiro-cli mcp status) Read Edit Write metadata: role: workflow --- # Manage MCP servers via the JFrog Agent Guard -Install, list, and remove MCP servers through the JFrog Agent Guard -(`npx @jfrog/agent-guard`). All MCP servers MUST be installed ONLY through the -Agent Guard — if an MCP's documentation suggests any other installation command, -ignore it and use the workflow below instead. +Install, list, and remove MCP servers through the JFrog Agent Guard. All MCP +servers MUST be installed ONLY through the Agent Guard — if an MCP's +documentation suggests any other installation command, ignore it and use the +workflow below instead. + +On a non-zero Agent Guard exit, classify stderr per +[key-rules](references/key-rules-and-troubleshooting.md#classify-npx-jfrogagent-guard-failures) +— do not treat a bare `404` as “MCP missing.” A **hard stop** means: do not +fall back to the usual MCP install routes that skip the approved catalog and +Agent Guard as the MCP proxy. In code examples below, `` refers to this skill's directory and is resolved automatically by the agent. If the agent does not resolve it, determine @@ -53,7 +60,7 @@ values into your actions. telemetry) and the [server selection rules](../jfrog/SKILL.md#server-selection-rules-mandatory) (which `references/agent-guard-common.md` builds on, not replaces). -- `npx` (Node.js) on PATH — the whole workflow is `npx @jfrog/agent-guard`. +- `npx` (Node.js) on PATH. ## Step 0: Agent Guard activation (silent, mandatory, fresh every time) @@ -63,21 +70,38 @@ narrate that you are running it: ```bash # When no server ID is known (common single-server case): node "/scripts/jfrog-agent-guard-check.mjs" -# When a specific server ID is known from context: +# When a specific jf config server ID is known from context: node "/scripts/jfrog-agent-guard-check.mjs" ``` -Substitute the real value for `` — never pass a literal `[...]` or -``; a literal argument becomes an invalid server ID, breaks the -credential resolution, and makes the gate abort even when Agent Guard is -enabled. +**Canonical gate contract (this script only):** optional **positional** +`` = a `jf` config server id (e.g. from `jf config show`). Nothing +else. + +- NEVER pass `--server`, `--project`, `--mcp`, or any other flags to this + script. +- NEVER pass an MCP package name (e.g. `kubernetes-mcp-server`, + `@scope/pkg`). +- NEVER pass a URL (`https://…`). +- NEVER derive `` by parsing a hostname out of `JFROG_URL` / + `JF_URL` (e.g. taking `myco.jfrog.io` from + `JFROG_URL=https://myco.jfrog.io`). If `jf config show` legitimately + names a server `myco.jfrog.io`, that is fine — the ban is on deriving + the id from the URL, not on hostname-shaped values from `jf config` + itself. +- Substitute the real jf server id for `` — never a literal + `[...]` or ``. Any unsupported or unknown argument stops + the gate (`Disabled: …`); it is NOT silently ignored and does NOT + fall back to env credentials or the default `jf` server. In a + multi-JPD setup that fallback would report the wrong platform. Pass `` when it is already known from context (an existing Agent -Guard MCP entry, or one the user named) so the gate checks the JPD you will -actually target, not the CLI default — this matters only in multi-JPD setups. -Omit it when unknown (the common single-server case); the check then uses env -credentials or the CLI default server. Do NOT block on resolving a server just -to run this gate. +Guard MCP entry's `--server` value, or one the user named) so the gate checks +the JPD you will actually target, not the CLI default — this matters only in +multi-JPD setups. Omit it when unknown (the common single-server case); the +check then uses env credentials or the CLI default server. Do NOT invent a +server id from the MCP name or from `JF_URL`. Do NOT block on resolving a +server just to run this gate. Interpret the exit code per [references/agent-guard-activation.md](references/agent-guard-activation.md). @@ -97,9 +121,10 @@ Run it FRESH on every activation — never cache or reuse a previous result. ## Pre-flight (Install and List → Available to install only) Read [references/agent-guard-common.md](references/agent-guard-common.md) for the -`` substitution and the rules for resolving `` -and `` before running any `npx @jfrog/agent-guard` command. Removal -and List → Currently installed read only local config, so they skip this. +canonical invocation (`npx --yes --registry @jfrog/agent-guard`), +the `` substitution, and the rules for resolving +`` and ``. Removal and List → Currently installed +read only local config, so they skip this. **Route the request**, then jump to the matching section: @@ -129,7 +154,8 @@ do NOT ask for JFrog project key, server, or package name unless necessary. [references/agent-guard-common.md](references/agent-guard-common.md). Pass `--server ` in every Agent Guard invocation whenever the ID came from an existing Agent Guard MCP entry or jf config; omit `--server` only on the -`JFROG_URL`+token env path. NEVER guess or assume `default` for the project key. +URL+token env path (`JFROG_URL`+`JFROG_ACCESS_TOKEN`, or legacy +`JF_URL`+`JF_ACCESS_TOKEN`). NEVER guess or assume `default` for the project key. **Target config file** - Use the current harness's row in @@ -170,9 +196,12 @@ npx --yes \ --mcp ``` +(never omit `--registry`; URL in [agent-guard-common](references/agent-guard-common.md)) + **`--server` is conditional** — include it per the Step 1 rule (from an -existing Agent Guard MCP entry or jf config; omit only on the `JFROG_URL`+token -env path). Same rule applies to `--login` and the config entry below. +existing Agent Guard MCP entry or jf config; omit only on the URL+token env +path — `JFROG_URL`+`JFROG_ACCESS_TOKEN`, or legacy `JF_URL`+`JF_ACCESS_TOKEN`). +Same rule applies to `--login` and the config entry below. From the output JSON, extract (keep BOTH required AND optional): - `spec.packageName` — exact package name for the config. @@ -181,9 +210,10 @@ From the output JSON, extract (keep BOTH required AND optional): `spec.mcpServerType.remote.endpoints[].headers[]` (via `mcpInput.mcpInputDetails`). Each carries `name`, `description`, `isRequired`, `isSecret`. -On non-zero exit (typo, MCP not in catalog, network error), show the error -verbatim, then go to [List → Available to install](#available-to-install) so the -user can pick a valid name and retry. +On non-zero exit, show the error verbatim, then classify per +[key-rules](references/key-rules-and-troubleshooting.md#classify-npx-jfrogagent-guard-failures). +Do not fall back to the usual MCP install routes that skip the approved catalog +and Agent Guard as the MCP proxy. ## Step 3: Plan inputs @@ -201,25 +231,36 @@ 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, OpenCode), how the user -exports/persists the variable, see the harness file and +shell-based harnesses (Claude Code, Cursor, Codex, Devin, Kiro, 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.) ## Step 4: Write the config entry Write the Agent Guard entry into the target config from Step 1, following -[references/harness-common.md](references/harness-common.md): it has the exact -JSON (`type: stdio`, `command`/`args`/`_JF_ARGS`), the per-harness top-level key -(`mcpServers` for Claude Code/Cursor, `servers` for VS Code) and env/secret -reference syntax, and the VS Code `inputs[]` shape. +[references/harness-common.md](references/harness-common.md) for the **shared +entry shape** (`type: stdio`, `command`/`args`/`_JF_ARGS`). Use your one +harness file only for path, top-level key, value-reference syntax, and any +"Full entry shape" override (Codex/OpenCode). Do not invent a different +`args`/`env` layout. + +**Config vs CLI (do not mix):** +- Config entry: project + MCP go in `env._JF_ARGS` as + `project=&mcp=`. +- Catalog CLI (`--inspect` / `--list-available` / `--login`): use `--project` + and `--mcp` as flags — those flags must **not** appear in the config + entry's `args`. Guardrails (identical everywhere): - `--yes` and `--registry ` MUST precede `@jfrog/agent-guard` in `args` (else npx hits the default registry → 404 / no-TTY hang). - `"type": "stdio"` only — never `"http"`, `"sse"`, or a top-level `"url"`. -- `--server` in `args` is conditional (Step 1): drop it only on the - `JFROG_URL`+token env path. +- `--server` in `args` is conditional (Step 1): drop it only on the URL+token + env path (`JFROG_URL`+`JFROG_ACCESS_TOKEN`, or legacy + `JF_URL`+`JF_ACCESS_TOKEN`). When present, its value is a jf config server + id — never an MCP name or a hostname from `JF_URL`. +- NEVER put `--project` or `--mcp` in config `args`. - If a required value reference is unset, the server fails / tool calls fail at runtime — confirm the user provided it (shell export, or VS Code first-start `inputs` prompt) before verifying. @@ -235,8 +276,10 @@ relaunch. Then tell the user: 1. Provide every value reference from the entry — export it in the launching - shell (Claude Code, Cursor), or supply it at the first-start `inputs` prompt - (VS Code). Unset values cause warnings and runtime failures. + shell (Claude Code, Cursor, Kiro, Devin, Codex, OpenCode — see + [references/persisting-env-vars.md](references/persisting-env-vars.md)), or + supply it at the first-start `inputs` prompt (VS Code). Unset values cause + warnings and runtime failures. 2. Restart per the harness's **Restart** column. 3. Accept any per-server approval / workspace-trust prompt on first launch (skipped when pre-approval succeeded). @@ -267,8 +310,12 @@ npx --yes \ Outcomes: - **Exit 0** — OAuth completed; tokens cached; server ready. -- **`expected 401, got 200`** — MCP is anonymous (no auth needed); ignore. -- **Any other error** — paste it to the user verbatim and stop. +- **`expected 401, got 200`** — MCP is anonymous (no auth needed); ignore + (even if the process exit is non-zero). Do not run the unmatched hard-stop. +- **Non-zero** — classify per + [key-rules](references/key-rules-and-troubleshooting.md#classify-npx-jfrogagent-guard-failures). + Do not fall back to the usual MCP install routes that skip the approved + catalog and Agent Guard as the MCP proxy. See [references/key-rules-and-troubleshooting.md](references/key-rules-and-troubleshooting.md) for key rules and troubleshooting. @@ -319,11 +366,11 @@ add-on where the agent provides it. 1. Determine **server** and **JFrog project key** per the Pre-flight rules. `--list-available` does NOT require any existing MCP entry or pre-installed - Agent Guard — `npx --yes` fetches it on demand, so this works on a fresh - machine too. + Agent Guard — `npx --yes --registry @jfrog/agent-guard` + fetches it on demand, so this works on a fresh machine too. 2. Run this ONCE — do not emit literal `[ ]` brackets. Append `--server - ` per the Step 1 rule (omit it only on the `JFROG_URL`+token env - path): + ` per the Step 1 rule (omit only on the URL+token env path — + `JFROG_URL`+`JFROG_ACCESS_TOKEN`, or legacy `JF_URL`+`JF_ACCESS_TOKEN`): ``` npx --yes \ --registry \ @@ -333,6 +380,13 @@ npx --yes \ --server ``` +On non-zero exit, classify per +[key-rules](references/key-rules-and-troubleshooting.md#classify-npx-jfrogagent-guard-failures). +Do not fall back to the usual MCP install routes that skip the approved catalog +and Agent Guard as the MCP proxy. +Exit 0 with only a TSV header (or `--format json` stdout `null`) is an empty +catalog — say so; do not invent names. + Output is a compact TSV — a header line, then one server per line: `nametypeversiondescription`. Present the rows directly as a numbered table — do NOT re-run, redirect, or parse with `python3`/`jq`. `name` diff --git a/skills/jfrog-mcp-management/references/agent-guard-activation.md b/skills/jfrog-mcp-management/references/agent-guard-activation.md index 86d0a5e..404a6ed 100644 --- a/skills/jfrog-mcp-management/references/agent-guard-activation.md +++ b/skills/jfrog-mcp-management/references/agent-guard-activation.md @@ -11,7 +11,7 @@ Guard or this script. Key off the EXIT CODE, not the text it prints. | --- | --- | --- | | 0 | Agent Guard enabled | Proceed with the skill. | | 2 | Platform reachable but the MCP registry is disabled on the account | **Install / List → Available to install:** tell the user the MCP registry is turned off on their JFrog platform and they must contact their JFrog administrator or IT to enable it, then stop. **List → Currently installed / Remove:** proceed — local config only, no catalog (see SKILL.md). | -| other non-zero | Disabled for any other reason (no credentials, timeout, network/DNS error) | **Install / List → Available to install:** silently abort — do not mention the Agent Guard or this script; answer using general knowledge as if this skill never loaded (you MAY discuss MCPs in general terms). **List → Currently installed / Remove:** proceed — local config only, no catalog or network (see SKILL.md). | +| other non-zero | Disabled for any other reason (no credentials, timeout, network/DNS error) | **Install / List → Available to install:** silently abort — do not mention the Agent Guard or this script. You MAY discuss MCPs in general terms. **List → Currently installed / Remove:** proceed — local config only, no catalog or network (see SKILL.md). | ## Local-only flows proceed on every exit code diff --git a/skills/jfrog-mcp-management/references/agent-guard-common.md b/skills/jfrog-mcp-management/references/agent-guard-common.md index 1d1e1e3..ea9ce04 100644 --- a/skills/jfrog-mcp-management/references/agent-guard-common.md +++ b/skills/jfrog-mcp-management/references/agent-guard-common.md @@ -1,7 +1,8 @@ # Agent guard common — registry URL & pre-flight Reference for the Install and List flows of the `jfrog-mcp-management` skill. -Read this before running any `npx @jfrog/agent-guard` command +Read this before running any +`npx --yes --registry @jfrog/agent-guard` command (`--list-available`, `--inspect`, `--login`). Terminology used throughout these skills: @@ -17,7 +18,12 @@ Terminology used throughout these skills: 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/`. +`https://releases.jfrog.io/artifactory/api/npm/coding-agents-npm/` — JFrog's +publicly accessible Releases Artifactory instance. It allows anonymous access +and hosts Agent Guard releases. + +Canonical invocation (every catalog / login command; never omit `--registry`): +`npx --yes --registry @jfrog/agent-guard` `@jfrog/agent-guard` is not published to the public npm registry; resolve it with `--registry ` above rather than the default npm registry. @@ -77,6 +83,14 @@ node "/scripts/jfrog-agent-guard-env-probe.mjs" pass it as `--server `; only on the `JFROG_URL`+token env path, never pass `--server`. + **`` is a `jf config` server id only.** NEVER invent it from an + MCP package name, and NEVER parse a hostname out of `JFROG_URL` / + `JF_URL` (or any other URL) to use as `--server` or as the Step 0 gate + positional argument. Hostname-shaped ids from `jf config show` itself + are fine — the ban is on deriving the id from the URL, not on the + shape of the value. If env URL+token is set, omit `--server` entirely + (path 2 above) — do not derive a fake server id from the URL. + > Note: the agent uses `jf config show --format=json` here only to *discover a > server ID* — a token is not needed, so the masked output is fine. The Step 0 > gate script separately uses `jf config export`, which emits the access token diff --git a/skills/jfrog-mcp-management/references/harness-common.md b/skills/jfrog-mcp-management/references/harness-common.md index febf55a..00ec744 100644 --- a/skills/jfrog-mcp-management/references/harness-common.md +++ b/skills/jfrog-mcp-management/references/harness-common.md @@ -12,8 +12,8 @@ plus **exactly one** harness file; do NOT open the others. The `CLAUDECODE` / `CURSOR_*` / `CODEX_*` / `OPENCODE` signals below mirror `../../jfrog/scripts/check-environment.sh` `detect_harness()`; the -`TERM_PROGRAM=vscode` editor hint is **not** in that script, and Devin is -**not** detected by the script. Each row's signal is **self-contained and +`TERM_PROGRAM=vscode` editor hint is **not** in that script, and Devin and +Kiro are **not** detected by the script. Each row's signal is **self-contained and non-overlapping**, so detection does not depend on evaluation order. The VS Code harness file targets the **VS Code editor** (Copilot MCP support), not the standalone GitHub Copilot terminal CLI — the CLI (`COPILOT_CLI`) has no @@ -39,7 +39,8 @@ editor UI or `mcp.json`, so it falls through to the Fallback section. | 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 | 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) | +| Kiro | Your system prompt / system instructions identify you as **Kiro** (Kiro IDE / `kiro-cli`). | [harness-kiro.md](harness-kiro.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 or Kiro | [harness-vscode.md](harness-vscode.md) | | anything else | none of the above | **Fallback** section below — no harness file exists | Once you know your harness, use ONLY these fields from its file: `Config files` @@ -51,7 +52,8 @@ harness-config" means: use the value from your one harness file. These do not vary; the harness file only overrides the pieces above. -**The Agent Guard entry** always invokes `npx @jfrog/agent-guard` with the same +**The Agent Guard entry** always invokes +`npx --yes --registry @jfrog/agent-guard` with the same argument tokens (in the same order) and the same `_JF_ARGS`. What varies per harness is **how the entry is written** — the wrapping top-level key, the value-reference syntax, and the entry *shape* itself (the transport field, and @@ -86,8 +88,22 @@ case; harnesses whose config is not JSON differ — e.g. **Codex** uses TOML wit - `"type": "stdio"` always — never `"http"`, `"sse"`, or a top-level `"url"` (those bypass the Agent Guard). - `--yes` and `--registry ` MUST precede `@jfrog/agent-guard` in `args`. -- `--server ` in `args` is conditional: drop both array elements only on the - `JFROG_URL`+token env path (see [agent-guard-common.md](agent-guard-common.md)). +- `--server ` in `args` is conditional: drop both array elements only on + the URL+token env path (`JFROG_URL`+`JFROG_ACCESS_TOKEN`, or legacy + `JF_URL`+`JF_ACCESS_TOKEN`) — see + [agent-guard-common.md](agent-guard-common.md). + `` is a `jf config` server id — never an MCP name, never a URL, and + never a hostname you parsed out of `JFROG_URL` / `JF_URL`. If a real id + in `jf config show` happens to be hostname-shaped (`myco.jfrog.io`), + use it as-is; the ban is on deriving the id from the URL, not on the + shape of the value. +- **NEVER put `--project` or `--mcp` in `args`.** Those flags are for catalog + CLI calls only (`--inspect` / `--list-available` / `--login`). In the config + entry, project + package belong exclusively in `_JF_ARGS`: + `project=&mcp=`. + - Wrong: `"args": […, "@jfrog/agent-guard", "--project", "da", "--mcp", "kubernetes-mcp-server"]` + - Right: `"args": […, "@jfrog/agent-guard", "--server", ""]` with + `"env": { "_JF_ARGS": "project=da&mcp=kubernetes-mcp-server", … }` - Never write a raw secret — always a value reference in the harness's syntax. - `_JF_ARGS` values are substituted raw (no URL-encoding), which is safe only because both are free of query-string reserved chars (`&`, `=`, `+`, space): a diff --git a/skills/jfrog-mcp-management/references/harness-kiro.md b/skills/jfrog-mcp-management/references/harness-kiro.md new file mode 100644 index 0000000..fa802d4 --- /dev/null +++ b/skills/jfrog-mcp-management/references/harness-kiro.md @@ -0,0 +1,76 @@ +# Harness: Kiro + +Kiro-specific config for the `jfrog-mcp-management` skill (Kiro IDE and +`kiro-cli`). Read this together with [harness-common.md](harness-common.md) +(shared entry shape and success criterion). You reached this file because +Step A matched **Kiro**: your system prompt / system instructions identify +you as Kiro (Kiro IDE or `kiro-cli`). The environment script does not detect +Kiro. Config, key, and env syntax are identical on both surfaces — no need to +tell them apart for this workflow. + +## Config files + +- **Kiro IDE only:** enable MCP support (`chat.mcp.enabled` setting) before + writing `mcp.json` — `kiro-cli` does not need this setting. +- **Default scope: project.** `.kiro/settings/mcp.json` in the workspace root + — shareable via git. Create if missing: `{ "mcpServers": {} }`. +- **User (global):** `~/.kiro/settings/mcp.json` (or `$KIRO_HOME/settings/mcp.json` + if `KIRO_HOME` is set). Use ONLY if the user says "personal only" / "do not + commit". Not always present — create if missing: `{ "mcpServers": {} }`. + Kiro merges both automatically at startup, workspace taking precedence on + conflicts. +- Do not ask which scope unless the user brings it up. + +## Top-level key + +`mcpServers` + +## Value reference (env / secrets) + +`${VAR_NAME}`, resolved from the shell that launched Kiro. For `Bearer` +headers: `"Bearer ${TOKEN}"`. The user must export the variable in the +launching shell (see [persisting-env-vars.md](persisting-env-vars.md)); values +are picked up on next launch. Never write a raw secret — always `${VAR}`. + +Kiro also gates env var expansion: an unapproved `${VAR_NAME}` triggers a +one-time approval popup (setting **Mcp Approved Env Vars**) before the value +is substituted. If a server starts with the value missing, tell the user to +approve it there. + +## Enable + +Every entry not marked `"disabled": true` runs automatically — writing the +entry is enough, there is no separate approval step. To disable without +deleting, set `"disabled": true` on that server's entry. + +## Restart + +Editing `mcp.json` needs no restart — both surfaces hot-reload it on save and +reconnect affected servers automatically. But a **newly exported env var** +needs Kiro relaunched: it only reads the shell environment at launch, so a +var exported after Kiro is already running won't resolve until you relaunch. + +## List installed + +- **`kiro-cli`:** `kiro-cli mcp list workspace` / `kiro-cli mcp list global` for + live status per scope (`kiro-cli mcp list` alone lists the merged/default + view). `kiro-cli mcp status --name ` for one server. +- **Kiro IDE:** tell the user to type `/mcp` in the chat for live server + + tool status — do not invoke it as a tool yourself. +- For JFrog metadata on either surface, read `mcpServers` directly from + `.kiro/settings/mcp.json` (project) and `~/.kiro/settings/mcp.json` (user, + or `$KIRO_HOME/settings/mcp.json` if `KIRO_HOME` is set). + +## Verify + +- **`kiro-cli`:** `kiro-cli mcp status --name ` for connection status, + then confirm real tools via `/mcp` in the same chat session (lists each + active server's tools). +- **Kiro IDE:** tell the user to type `/mcp` in the chat — it lists each + active server's tools directly, so they can drill into the target server. + Do not invoke it as a tool yourself. + +A connected/active status alone is NOT proof — the Agent Guard proxy can +report up with 0 upstream tools. Empty tool list = Failed, see the "0 tools" +troubleshooting in +[key-rules-and-troubleshooting.md](key-rules-and-troubleshooting.md). diff --git a/skills/jfrog-mcp-management/references/key-rules-and-troubleshooting.md b/skills/jfrog-mcp-management/references/key-rules-and-troubleshooting.md index 40cba92..656f715 100644 --- a/skills/jfrog-mcp-management/references/key-rules-and-troubleshooting.md +++ b/skills/jfrog-mcp-management/references/key-rules-and-troubleshooting.md @@ -10,18 +10,32 @@ Reference for the Install and List flows of the `jfrog-mcp-management` skill. Capitalizing the brand (`@JFrog`) points at a different/nonexistent scope and breaks the command. Use the exact lowercase string in every command and config entry. -- **`npx` arg order:** `--yes`, `--registry `, `@jfrog/agent-guard`, then - agent guard flags. Both `--yes` and `--registry` MUST precede the package - name or `npx` falls back to the default registry (404) and may block on a - no-TTY prompt. -- **Always `"type": "stdio"`** pointing at `npx @jfrog/agent-guard`, even for +- **`npx` arg order:** `--yes`, `--registry `, `@jfrog/agent-guard`, then + agent guard flags. Canonical invocation: + `npx --yes --registry @jfrog/agent-guard`. Both `--yes` and + `--registry` MUST precede the package name or `npx` falls back to the default + registry (404) and may block on a no-TTY prompt. +- **Always `"type": "stdio"`** pointing at + `npx --yes --registry @jfrog/agent-guard`, even for remote-only catalog MCPs (the agent guard proxies them). `"http"`, `"sse"`, or a top-level `"url"` bypass the agent guard. - `_JF_ARGS` is **only** for the config entry the agent launches at session start (the `env` of the entry written when adding an MCP); MUST contain `project=&mcp=`. NEVER pass `_JF_ARGS` to `--list-available`, `--inspect`, or `--login` — those take `--server` / - `--project` as CLI flags only. + `--project` / `--mcp` as CLI flags only. Conversely, NEVER put `--project` + or `--mcp` in the config entry's `args` — that is CLI-only; config uses + `_JF_ARGS`. +- **Three invocation contracts — do not mix:** (1) Step 0 gate + `jfrog-agent-guard-check.mjs` — optional positional jf `` only, + no flags; (2) catalog CLI — `--server` / `--project` / `--mcp` flags; + (3) config stdio entry — `_JF_ARGS=project=…&mcp=…`, optional `--server` + in `args` only when not on the env URL+token path. +- `` is always a `jf config` server id. NEVER an MCP package + name, NEVER a URL, and NEVER a hostname you derived from `JFROG_URL` / + `JF_URL`. Hostname-shaped ids from `jf config show` itself are fine — + the ban is on parsing an id out of the URL, not on the shape of the + value. - NEVER assume `default` as a JFrog project key. If the project key is unknown after the project chain (existing `mcpServers` entries → `JF_PROJECT` env var), STOP and ask the user. Same for server ID if used. NEVER invent or @@ -29,6 +43,11 @@ Reference for the Install and List flows of the `jfrog-mcp-management` skill. - Package name MUST come from the catalog (`--inspect` / `--list-available`). NEVER guess. NEVER install MCPs outside the agent guard. NEVER use Fetch/WebFetch for catalog calls. +- **Non-zero Agent Guard exit → classify, never invent a fallback install.** + Retryable fingerprints retry; catalog miss → List; **any other stderr is a + hard stop** (show it, do not retry, do not fall back to the usual MCP install + routes that skip the approved catalog and Agent Guard as the MCP proxy). See + [Classify npx @jfrog/agent-guard failures](#classify-npx-jfrogagent-guard-failures). - NEVER pipe a catalog command through `python3`, and NEVER capture it with `2>&1` — `npx`/`npm` writes progress to stderr, which corrupts the output stream. For `--list-available` present the compact TSV it prints; for @@ -39,6 +58,41 @@ Reference for the Install and List flows of the `jfrog-mcp-management` skill. `${VAR_NAME}`. NEVER show tokens / API keys. - NEVER try multiple servers — ask the user to pick one. +## Classify npx @jfrog/agent-guard failures + +Show the error verbatim. Ignore `npm warn` noise — except `npm warn invalid +config registry=…`, which names the cause of a self-inflicted E404. Match +**one** bucket from stderr. Fingerprints below are the live strings; if a +match fails, re-check +`npx --yes --registry @jfrog/agent-guard --version` rather +than assuming a pinned release. A **hard stop** means: do not fall back to +the usual MCP install routes that skip the approved catalog and Agent Guard +as the MCP proxy. + +1. **Package unreachable (hard stop).** `npm error code E404`, `npm error 404`, + npx package-fetch **403**, DNS/`ENOTFOUND`, timeout/`ETIMEDOUT`, connection + refused/`ECONNREFUSED`. Agent Guard never started. Two self-inflicted causes + look identical to a real outage, so rule both out first: (a) `--yes` and + `--registry ` must precede `@jfrog/agent-guard`, and (b) + `` must be substituted with a real URL — npm discards an + invalid `--registry` value, falls back to the default registry, and returns + the same E404, so compare it against the URL npm reports contacting. Fix and + retry if either is wrong. Otherwise tell the user the registry could not be + reached, point them at [Troubleshooting](#troubleshooting) (proxy/VPN, + blocked or wrong registry, curation policy), and **stop**. +2. **Catalog miss (retry via List).** `not found in curated list` — the catalog + responded; that MCP name is not approved. Go to List → Available to install. + Same fingerprint on `--login`. A wrong `--project` key can surface as this + miss on the MCP name — re-resolve the project key if List is empty. +3. **Retryable CLI/config.** `--project flag is required`, `--mcp flag is required`, + `Server ID '…' does not exist.`, `multiple/no JFrog server configured`. Fix + per Troubleshooting / Pre-flight and retry the **same** Agent Guard command. +4. **Any other non-zero (hard stop).** Unrecognized stderr (including + `Unauthorized`, `failed to fetch catalog`, `_JF_ARGS environment variable is not set`, + 5xx). Show it verbatim. Do not retry List. Do not guess another bucket. + Carve-out: `--login` `expected 401, got 200` is anonymous MCP — ignore, not + this bucket. + ## Troubleshooting Items below are harness-agnostic unless they point into the current harness's @@ -71,11 +125,25 @@ row in [harness-common.md](harness-common.md). cannot pick a JFrog server) — pass `--server ` (after `jf c add `) OR export both `JFROG_URL` and `JFROG_ACCESS_TOKEN` in the launching shell, then restart the agent. +- **`Server ID '' does not exist.`** — `--server` is not a configured jf + CLI server. Re-resolve the id (Pre-flight) or `jf c add`, then retry. Not a + catalog miss and not package-unreachable. +- **`--project flag is required` / `--mcp flag is required`** — empty or + omitted flag; fill from Pre-flight / the MCP name and retry. - **OAuth MCP failing** — refresh token expired; re-run the OAuth login step. - **401/403 with `${VAR}`** — env var unset/wrong; re-export in the launching shell and restart the agent. - **Network / proxy / DNS error** — outside the agent guard's scope; tell the - user and stop. -- **npx package fetch returns 403** — usually a corporate proxy/VPN, a blocked - or wrong registry, or a curation policy. Confirm `--registry - ` resolves and the access token is valid for that repo. + user and stop. This is package-unreachable per + [Classify npx @jfrog/agent-guard failures](#classify-npx-jfrogagent-guard-failures) + — never install the MCP by any other means as a workaround. +- **npx package fetch returns 403 or 404** — usually a corporate proxy/VPN, a + blocked or wrong registry, the JFrog registry being unreachable, or a + curation policy — not a missing package. The default + `` (`https://releases.jfrog.io/artifactory/api/npm/coding-agents-npm/`) + is JFrog's publicly accessible Releases Artifactory instance: anonymous + access, hosts Agent Guard releases. Confirm `--registry ` + resolves (and, if using a private override via `JFROG_AGENT_GUARD_REPO`, that + the access token is valid for that repo). Same hard-stop rule applies: do + not fall back to the usual MCP install routes that skip the approved catalog + and Agent Guard as the MCP proxy. diff --git a/skills/jfrog-mcp-management/references/persisting-env-vars.md b/skills/jfrog-mcp-management/references/persisting-env-vars.md index 7c0c49c..436707c 100644 --- a/skills/jfrog-mcp-management/references/persisting-env-vars.md +++ b/skills/jfrog-mcp-management/references/persisting-env-vars.md @@ -6,6 +6,8 @@ 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 / Devin CLI** — a `${env:VAR}` reference in the config. +- **Kiro (IDE / `kiro-cli`)** — a `${VAR}` reference in the config, gated by a + one-time approval popup (**Mcp Approved Env Vars**). - **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 0024321..999c52f 100644 --- a/skills/jfrog-mcp-management/scripts/jfrog-agent-guard-check.mjs +++ b/skills/jfrog-mcp-management/scripts/jfrog-agent-guard-check.mjs @@ -52,21 +52,57 @@ const registryDisabled = (reason) => { process.exit(2); }; +// Exactly one positional argv[2]. Extras (argv[3..]), flags, and URLs are +// ALWAYS caller bugs — stop the gate immediately so a multi-JPD setup does +// not report the wrong platform's state. Everything else is treated as a +// candidate jf config server id — including hostname-shaped values and ids +// with spaces — because a text pattern cannot separate a real jf id from +// an MCP package name. jf config is the source of truth (see +// resolveCredentials). +function readGateServerId() { + const extra = process.argv.slice(3); + if (extra.length > 0) { + disabled( + `expected zero or one positional jf config server id, got extra ` + + `argument(s) ${JSON.stringify(extra)} — the gate accepts only ` + + `\`\` positionally, with no flags or additional values ` + + `after it`, + ); + } + const raw = process.argv[2]; + if (raw === undefined) return undefined; + const id = String(raw).trim(); + if (!id) return undefined; + if (id.startsWith("-")) { + disabled( + `expected a jf config server id (positional), got flag ` + + `${JSON.stringify(id)} — pass \`\` positionally, not as \`--server\``, + ); + } + if (/:\/\//.test(id)) { + disabled( + `expected a jf config server id (positional), got URL ` + + `${JSON.stringify(id)} — do not derive an id from \`JFROG_URL\` / \`JF_URL\``, + ); + } + return id; +} + // Resolve credentials from Path A (environment variables) or Path B // (the default JFrog CLI configuration). Returns { baseUrl, token, source } // or null when neither path yields a usable URL + access token. function resolveCredentials() { - const explicitServerId = process.argv[2]; - // With an explicit server ID, try the named jf-config server FIRST so the - // gate checks THAT JPD, not the ambient default. But if it does not resolve - // (server not in jf config, jf absent/old), fall back to env credentials - // rather than reporting a false "disabled" — the platform may be fully - // reachable via exported JFROG_URL + token even with no matching jf server. + const explicitServerId = readGateServerId(); + // When the caller names a specific server, honor it or stop. Do not fall + // back to env credentials or the default jf server, that would check a + // different JPD in a multi-server setup when the named id is wrong (typo, + // MCP package name, unknown id). if (explicitServerId) { - const fromCli = resolveFromCliConfig(); + const fromCli = resolveFromCliConfig(explicitServerId); if (fromCli) return fromCli; - debug( - "Explicit server ID did not resolve via jf config; falling back to env credentials.", + disabled( + `server id ${JSON.stringify(explicitServerId)} is not configured in ` + + `\`jf config\` (or \`jf\` is unavailable) — refusing to check a different JPD`, ); } @@ -81,22 +117,18 @@ function resolveCredentials() { "Environment credentials incomplete; trying JFrog CLI config (Path B).", ); - // Path B — default server from the local JFrog CLI configuration. If an - // explicit ID was given we already tried the CLI above (and env fell through), - // so there is nothing left to resolve. - if (explicitServerId) return null; - return resolveFromCliConfig(); + // Path B — default server from the local JFrog CLI configuration. + return resolveFromCliConfig(undefined); } -function resolveFromCliConfig() { +function resolveFromCliConfig(serverId) { // `jf config export [server ID]` emits the server as a base64-encoded JSON // blob containing url, accessToken, and serverId. An optional server ID may - // be passed as argv[2]; without it the CLI's default server is used. We use - // the CLI rather than reading ~/.jfrog/jfrog-cli.conf.v6 directly because - // newer CLIs do not persist the access token in that file (and the platform - // URL may be stored only as an /artifactory-suffixed URL there, which is - // wrong for /ml/core). - const serverId = process.argv[2]; + // be passed; without it the CLI's default server is used. We use the CLI + // rather than reading ~/.jfrog/jfrog-cli.conf.v6 directly because newer CLIs + // do not persist the access token in that file (and the platform URL may be + // stored only as an /artifactory-suffixed URL there, which is wrong for + // /ml/core). const exportArgs = serverId ? ["config", "export", serverId] : ["config", "export"]; let exported; try { 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 902d453..377deb1 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.30.0" + version: "0.32.0" --- # 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) diff --git a/skills/jfrog/scripts/check-environment.sh b/skills/jfrog/scripts/check-environment.sh index 0941798..0712ba6 100755 --- a/skills/jfrog/scripts/check-environment.sh +++ b/skills/jfrog/scripts/check-environment.sh @@ -201,6 +201,140 @@ sanitize_token() { printf '%s' "${s:0:64}" } +# askpass_path_contains reports whether a VS Code-fork askpass path is that +# editor's install, not an unrelated substring (a Windows login named "cursor" +# would otherwise match). GIT_ASKPASS is generic git and is excluded. +# Mirrors jfrog-cli-core askpassPathContains. +askpass_path_contains() { + local app="$1" p + for key in VSCODE_GIT_ASKPASS_MAIN VSCODE_GIT_ASKPASS_NODE; do + p="$(printf '%s' "${!key:-}" | tr '[:upper:]' '[:lower:]' | tr '\\' '/')" + [[ -z "$p" ]] && continue + if [[ "$p" == *"/${app}.app"* || "$p" == *"/${app}/resources"* || "$p" == *"/.$app-server"* ]]; then + return 0 + fi + done + return 1 +} + +# Positive upstream VS Code install only. A non-empty VSCODE_GIT_ASKPASS_* var +# is not enough — every fork sets those. Mirrors askpassLooksLikeStockVSCode. +askpass_looks_like_stock_vscode() { + local p + for key in VSCODE_GIT_ASKPASS_MAIN VSCODE_GIT_ASKPASS_NODE; do + p="$(printf '%s' "${!key:-}" | tr '[:upper:]' '[:lower:]' | tr '\\' '/')" + [[ -z "$p" ]] && continue + if [[ "$p" == *"/visual studio code"* || "$p" == *"/microsoft vs code"* \ + || "$p" == *"/.vscode-server"* || "$p" == *"/code/resources"* ]]; then + return 0 + fi + done + return 1 +} + +# Short product names for known terminals. Unmapped values (including inherited +# TERM_PROGRAM=vscode) stay empty so client stays on the known-app allowlist. +canonical_terminal_name() { + local name + name="$(sanitize_token "${1:-}")" + case "$name" in + iterm.app) echo "iterm"; return ;; + esac + name="${name%.app}" + case "$name" in + "") ;; + iterm) echo "iterm" ;; + apple_terminal) echo "terminal" ;; + warpterminal|warp) echo "warp" ;; + tmux) echo "tmux" ;; + wezterm) echo "wezterm" ;; + alacritty) echo "alacritty" ;; + kitty) echo "kitty" ;; + ghostty) echo "ghostty" ;; + hyper) echo "hyper" ;; + esac +} + +fallback_terminal_name() { + if [[ -n "${TMUX:-}" ]]; then + echo "tmux" + elif [[ -n "${WT_SESSION:-}" ]]; then + echo "windows-terminal" + elif [[ "${TERM:-}" == "xterm-ghostty" ]]; then + echo "ghostty" + elif [[ -n "${KITTY_WINDOW_ID:-}" ]]; then + echo "kitty" + elif [[ -n "${ALACRITTY_LOG:-}" ]]; then + echo "alacritty" + fi +} + +host_terminal_name() { + local name + name="$(canonical_terminal_name "${TERM_PROGRAM:-}")" + if [[ -n "$name" ]]; then + echo "$name" + else + fallback_terminal_name + fi +} + +fold_agent_name() { + local name + name="$(printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr '[:upper:]' '[:lower:]')" + name="${name%%@*}" + printf '%s' "$name" +} + +is_copilot_vscode_plugin_alias() { + [[ "$(fold_agent_name "${AI_AGENT:-}")" == "github_copilot_vscode_agent" ]] \ + || [[ "$(fold_agent_name "${AGENT:-}")" == "github_copilot_vscode_agent" ]] +} + +# detect_host_client prints the app hosting this session. Mirrors detectClient() +# in jfrog-cli-core/common/commands/execution_context.go — keep the two in sync. +# +# Deliberately NOT part of detect_harness(): that table drives Agent Guard +# routing and mcp-management's tool= parsing and stays frozen. This only feeds +# the client= key. +# +# Order matters: every VS Code fork inherits the VSCODE_* vars from upstream, so +# a fork must resolve before anything reports vscode. CURSOR_AGENT does not +# prove the window (standalone CLI in iTerm / CI). +detect_host_client() { + local name + if [[ -n "${ZED_TERM:-}" ]]; then + echo "zed" + elif [[ "${TERMINAL_EMULATOR:-}" == "JetBrains-JediTerm" ]]; then + echo "jetbrains" + elif [[ -n "${CURSOR_TRACE_ID:-}" ]] || askpass_path_contains cursor; then + echo "cursor" + elif [[ -n "${WINDSURF_CASCADE_TERMINAL:-}" ]] || askpass_path_contains windsurf; then + echo "windsurf" + elif [[ -n "${ANTIGRAVITY_AGENT:-}" ]] || askpass_path_contains antigravity; then + echo "antigravity" + elif [[ -n "${TRAE_AI_SHELL_ID:-}" ]] || askpass_path_contains trae; then + echo "trae" + elif askpass_path_contains vscodium || askpass_path_contains codium; then + echo "codium" + elif [[ -n "${VisualStudioVersion:-}" ]]; then + echo "visualstudio" + elif askpass_looks_like_stock_vscode; then + echo "vscode" + elif [[ "${COPILOT_AGENT:-}" == "1" ]] || is_copilot_vscode_plugin_alias; then + name="$(host_terminal_name)" + if [[ -n "$name" ]]; then + echo "$name" + else + echo "vscode" + fi + elif [[ -n "${CLAUDE_CODE_CHILD_SESSION:-}" || -n "${CLAUDE_CODE_IS_COWORK:-}" ]]; then + echo "claude" + else + host_terminal_name + fi +} + # Map a generic AI_AGENT/AGENT value (agents.md proposal, @vercel/detect-agent) # to a canonical name. Strips a version suffix (e.g. "goose@1.2.3") and lowercases. # Empty input → nothing; unrecognized non-empty → "unknown". @@ -318,10 +452,11 @@ emit_skill_env() { harness="unknown" harness_from_model_fallback=true fi - # Client (TERM_PROGRAM): app hosting the session. Omitted on new CLI when the - # CLI will emit ai-client/ itself (not on the model-slug fallback path). + # Client: the editor window hosting the session (see detect_host_client). + # Omitted on new CLI when the CLI will emit ai-client/ itself (not on the + # model-slug fallback path). local client - client="$(sanitize_token "${TERM_PROGRAM:-}")" + client="$(detect_host_client)" local carry_client_ua="false" if [[ "$cli_version" == "unknown" ]] || version_lt "$cli_version" "$AGENT_UA_MIN_CLI_VERSION" || [[ "$harness_from_model_fallback" == "true" ]]; then carry_client_ua="true"