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