From 83a7900b27bd1fb7099707049240d48df5025665 Mon Sep 17 00:00:00 2001 From: "jfrog-agentic-release-bot[bot]" <276080306+jfrog-agentic-release-bot[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 08:11:08 +0000 Subject: [PATCH] [patch] chore: sync skills to v0.30.0 --- .devin-plugin/plugin.json | 2 +- .github/scripts/sync-skills-vendor.json | 2 +- .../references/installing-plugins.md | 20 +- skills/jfrog-init/SKILL.md | 21 +- skills/jfrog-init/references/flow-diagram.md | 2 +- .../jfrog-init/references/how-to-ask-user.md | 19 ++ .../references/mcp-plugin-config.md | 37 ++- .../references/node-install-prompt.md | 28 +-- .../references/script-invocation.md | 2 +- skills/jfrog-init/references/server-picker.md | 33 +-- .../scripts/jfrog-detect-jfrog-mcp.mjs | 17 +- .../scripts/jfrog-reinstall-jfrog-plugin.mjs | 14 +- .../scripts/jfrog-resolve-mcp-config.mjs | 204 +++++++++++------ .../jfrog-substitute-mcp-placeholders.mjs | 15 +- skills/jfrog-init/scripts/lib/jf.mjs | 40 ++-- skills/jfrog-mcp-management/SKILL.md | 19 +- .../references/agent-guard-common.md | 17 ++ .../references/harness-common.md | 2 +- .../references/harness-devin.md | 213 +++++++++--------- .../references/persisting-env-vars.md | 2 +- .../scripts/jfrog-agent-guard-env-probe.mjs | 33 +++ skills/jfrog/SKILL.md | 2 +- skills/jfrog/scripts/check-environment.sh | 4 +- 23 files changed, 460 insertions(+), 288 deletions(-) create mode 100644 skills/jfrog-init/references/how-to-ask-user.md create mode 100644 skills/jfrog-mcp-management/scripts/jfrog-agent-guard-env-probe.mjs diff --git a/.devin-plugin/plugin.json b/.devin-plugin/plugin.json index a9ed151..206d958 100644 --- a/.devin-plugin/plugin.json +++ b/.devin-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "jfrog", "displayName": "JFrog Platform", - "version": "0.3.1", + "version": "0.3.2", "description": "Official JFrog plugin for Devin. Ships the JFrog skills bundle (platform ops, init, MCP management, AI Catalog, package safety, reference architecture, package-manager setup) and registers the JFrog Platform MCP server (remote HTTP + OAuth).", "author": { "name": "JFrog Ltd.", diff --git a/.github/scripts/sync-skills-vendor.json b/.github/scripts/sync-skills-vendor.json index 2e9a74b..acab867 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.28.0", + "pin": "v0.30.0", "paths": [ "skills" ] diff --git a/skills/jfrog-ai-catalog/references/installing-plugins.md b/skills/jfrog-ai-catalog/references/installing-plugins.md index 88f66de..b3f38e3 100644 --- a/skills/jfrog-ai-catalog/references/installing-plugins.md +++ b/skills/jfrog-ai-catalog/references/installing-plugins.md @@ -33,14 +33,18 @@ every choice (`--repo`, target) up front. **Resolve `` from the environment check script — never from your model name.** If `` is not already known from this session, run `bash /../jfrog/scripts/check-environment.sh ` now and capture -its stdout as ``. Parse the `tool=` field from `` and pass it straight -through as `--harness `. - -If `tool` is `unknown` or empty, do **not** guess — ask the user for the -desired install path and use `--path ` instead. - -If the CLI rejects the harness with `unknown agent`, fall back to asking the -user for `--path `, the same as the unknown/empty case above. +its stdout as ``. Parse the `tool=` field from `` and map it to a +`jf` harness name: + +| `tool=` value in `` | `--harness` for `jf agent plugins` | +|-------------------------|-------------------------------------| +| `claude` | `claude` | +| `cursor` | `cursor` | +| `copilot` | `vscode` | +| `unknown`, empty, or any other | Ask the user | + +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. Choose exactly one install target (these are mutually exclusive): diff --git a/skills/jfrog-init/SKILL.md b/skills/jfrog-init/SKILL.md index 6fbde42..80760bd 100644 --- a/skills/jfrog-init/SKILL.md +++ b/skills/jfrog-init/SKILL.md @@ -11,6 +11,8 @@ metadata: # /jfrog-init — verify and guide JFrog plugin readiness +**First output must be a tool call, not text.** No "I'll start..." preamble. + Walks a fixed, ordered checklist and stops at the first red result, guiding the user through the matching fix before re-checking. Every detector in `scripts/` is idempotent, read-only, JSON-emitting, and implemented in @@ -50,8 +52,8 @@ substitutes it automatically, identically, in both this text and the `allowed-tools` Bash rules above — write it literally rather than resolving it yourself, so the two stay byte-for-byte consistent regardless of install depth (see `references/script-invocation.md`). On -a harness that doesn't perform this substitution (e.g. Cursor, which -doesn't consult `allowed-tools` for approval at all — every command +a harness that doesn't perform this substitution (e.g. Cursor or Codex, +neither of which consults `allowed-tools` for approval — every command below still raises its own prompt there), replace it with the real absolute path of this file's directory yourself, same as before. @@ -140,10 +142,12 @@ user needs to know or act on: - **Do not** announce that you're about to run the checklist, or name which check comes first — not even generically ("I'll run the setup checklist silently, starting with the JFrog CLI check" is itself a - violation: it names a step while claiming to be silent). Silently - means no preamble message at all. Say nothing until you have - something the user needs to act on (an ask, a red result) or the - final summary. + violation: it names a step while claiming to be silent). The same + applies to reading reference docs: "I'll start by reading the flow + docs" is a preamble. Silently means no preamble message at all — not + before running commands, not before reading files. Say nothing until + you have something the user needs to act on (an ask, a red result) + or the final summary. Instead: @@ -251,6 +255,11 @@ Read the output yourself, no JSON to parse: - `node --version` ≥ 18 **and** `npx --version` succeeds → **green** → proceed to Step 2. +**Never paste the raw shell output.** Translate to plain English — +"npx is not installed" not `` `command not found` ``, "Node.js v16 is +too old" not the version string verbatim. The raw output is for your +reasoning, not for the user. + On red, **stop and read `references/node-install-prompt.md` in full before responding to the user.** It has the exact `AskUserQuestion` payload, the forbidden phrases, and the install commands — required diff --git a/skills/jfrog-init/references/flow-diagram.md b/skills/jfrog-init/references/flow-diagram.md index 4a1cb54..6f77852 100644 --- a/skills/jfrog-init/references/flow-diagram.md +++ b/skills/jfrog-init/references/flow-diagram.md @@ -63,7 +63,7 @@ flowchart TD S4 -->|unreachable/timeout/other| STOP2["STOP: show raw error (network/URL hint included)"]:::stopBox S4 -->|yes| S5 - S5["5. Plugin mcp.json has mcpServers.jfrog? (auto-substitutes a JFROG_PLATFORM_URL/JFROG_URL placeholder inline, if present)"]:::stepBox + S5["5. Plugin mcp.json has a valid jfrog entry? (auto-substitutes a JFROG_PLATFORM_URL/JFROG_URL placeholder inline, if present)"]:::stepBox S5 -->|substitution needed, server-id ambiguous| ASKSRV5["AskUserQuestion: pick server-id"]:::fixBox ASKSRV5 --> S5 S5 -->|missing/invalid/no entry, incl. substitution failure| F5["Note: reinstall or update the JFrog plugin, or resolve jf config (non-blocking)"]:::fixBox diff --git a/skills/jfrog-init/references/how-to-ask-user.md b/skills/jfrog-init/references/how-to-ask-user.md new file mode 100644 index 0000000..db3cbf2 --- /dev/null +++ b/skills/jfrog-init/references/how-to-ask-user.md @@ -0,0 +1,19 @@ +# How to ask the user questions + +When the skill needs a Yes/No answer, a selection, or any other input +from the user, use the **native interactive prompt tool** built into +your harness so the user can click or select rather than type: + +| Harness | Preferred tool | +|-------------|------------------------| +| Claude Code | `AskUserQuestion` | +| Codex | `request_user_input` | + +Each reference file specifies the question text and option labels; use +your harness's native tool to present them. Native prompt tools already +offer a free-text "Other" fallback for values not in the list — don't +add a duplicate "Other" option yourself. + +**Fallback**: if no native prompt tool is available, or the tool +returns without a selection, surface the question as plain text in +your reply — never silently stop without presenting it. diff --git a/skills/jfrog-init/references/mcp-plugin-config.md b/skills/jfrog-init/references/mcp-plugin-config.md index 01a818f..39ac6dc 100644 --- a/skills/jfrog-init/references/mcp-plugin-config.md +++ b/skills/jfrog-init/references/mcp-plugin-config.md @@ -13,21 +13,31 @@ resolved at runtime from an env var: {"mcpServers": {"jfrog": {"url": "https://${JFROG_PLATFORM_URL}/mcp"}}} ``` +Codex's plugin ships the same idea in a different shape — no +`mcpServers` wrapper, and angle brackets instead of `${...}`: + +```json +{"jfrog": {"url": "https:///mcp"}} +``` + Because we have that URL sitting in `jf config`, and because leaving the placeholder in place means the MCP silently fails to load in the IDE / agent, Step 5 auto-substitutes it. If the detector finds the placeholder pattern anywhere in the file, it calls `jfrog-substitute-mcp-placeholders.mjs`, which: -1. Parses the file as JSON and looks **only** at - `mcpServers.jfrog.url` — never a file-wide text replace, so an - unrelated MCP server entry or JSON value that happens to contain the - same placeholder text is never touched. +1. Parses the file as JSON and looks **only** at the `jfrog` entry's + `url` (nested under `mcpServers` on every harness but Codex, which + has no wrapper) — never a file-wide text replace, so an unrelated + MCP server entry or JSON value that happens to contain the same + placeholder text is never touched. 2. Reads the JPD URL from `jf config` (default server, or the one passed as arg 2), normalizes it to the JPD root, and substitutes it into that one `url` string. -3. Handles both the `https://${...}` form (where our own scheme would - double up) and the bare `${...}` form. +3. Replaces in two passes — first a placeholder preceded by a scheme + (`https://${...}`, where our own scheme would otherwise double up), + then a bare one. Each pass recognizes all three syntaxes: `${VAR}`, + `$VAR`, and Codex's ``. 4. Re-serializes the whole file (`JSON.stringify(parsed, null, 2)`) and writes atomically (temp file + rename) so a partial write cannot corrupt the file. Original formatting/whitespace elsewhere in the @@ -44,16 +54,19 @@ This is the ONLY place `/jfrog-init` writes to the plugin-owned | Cursor | `~/.cursor/plugins/cache/cursor-public/jfrog//mcp.json` (glob → newest) | | VS Code | `~/.vscode/agent-plugins/github.com/jfrog/vscode-plugin/plugin/.mcp.json` | | Claude Code | `~/.claude/plugins/cache//jfrog//.mcp.json` (glob) | +| Codex | `$CODEX_HOME/plugins/cache/codex-plugin/jfrog//.mcp.json` (glob → newest; `$CODEX_HOME` defaults to `~/.codex`) | -Harness detection: `CLAUDECODE` / `CURSOR_TRACE_ID` / `VSCODE_PID` / -`TERM_PROGRAM`. Override with `JFROG_INIT_HARNESS=claude|cursor|vscode` -or a specific file via `JFROG_INIT_MCP_CONFIG=/abs/path`. +Harness detection (in priority order): `CODEX_SANDBOX` / `CLAUDECODE` / +`CURSOR_TRACE_ID` / `VSCODE_PID` / `TERM_PROGRAM`. Override with +`JFROG_INIT_HARNESS=claude|cursor|vscode|codex` or a specific file via +`JFROG_INIT_MCP_CONFIG=/abs/path`. **What the detector verifies** (three things): 1. Plugin file exists and is non-empty at its harness-specific path. 2. Parses as valid JSON. -3. Contains an `mcpServers.jfrog` entry with a non-empty `url`. +3. Contains a `jfrog` entry (nested under `mcpServers` on every harness + but Codex, which has no wrapper) with a non-empty `url`. It does NOT enforce any other `type`/`url` shape (each plugin owns its own schema) and it does NOT probe the endpoint — a mis-configured MCP @@ -67,12 +80,12 @@ reachable. - **Exit 1 (red)** or **Exit 3 (error)** → **non-blocking** — proceed to Step 6 as if green, but remember the cause for the Final Summary. Steps 6 and 7 call the JPD's REST APIs directly with `jf config` - credentials, never through `mcpServers.jfrog`, so a broken or + credentials, never through the JFrog MCP, so a broken or missing plugin `mcp.json` doesn't affect whether those checks are accurate — there's nothing to gain by stopping the walk over it. Tell the two red causes apart from the detector's `detail` for the Final Summary note: - - Plugin file missing / empty / lacks `mcpServers.jfrog`. Fix: + - Plugin file missing / empty / lacks a valid `jfrog` entry. Fix: **reinstall or update the JFrog plugin.** If the user asks why or how to fix it, run: diff --git a/skills/jfrog-init/references/node-install-prompt.md b/skills/jfrog-init/references/node-install-prompt.md index db8baa2..d58bd79 100644 --- a/skills/jfrog-init/references/node-install-prompt.md +++ b/skills/jfrog-init/references/node-install-prompt.md @@ -1,26 +1,16 @@ # Step 1 — the Node.js install prompt **Required behavior for Step 1's red branch, not optional background.** -When `node --version` is missing, unparseable, or its major version is -`< 18`, call `AskUserQuestion` with this exact payload shape (fill in -`` with either `isn't installed` or the specific `` `` -is too old (need ≥ 18) ``, matching whichever is actually true): +When `node --version` is missing/unparseable/`< 18`, or when `npx +--version` fails (node present but npx absent — same red branch), ask +the user (see `how-to-ask-user.md` for native tool and fallback rules): -```json -{ - "questions": [ - { - "question": "Node.js . Install it now?", - "header": "Install Node", - "multiSelect": false, - "options": [ - {"label": "Yes", "description": "Install Node.js now. Adds a line to your shell startup file so future terminals can find it."}, - {"label": "No", "description": "Cancel /jfrog-init."} - ] - } - ] -} -``` +- **Question**: "Node.js ``. Install it now?" + — where `` is a brief, accurate description of what the tool + output actually showed (e.g. "isn't installed", "`v16.4.0` is too old + (need ≥ 18)", "is installed but npx is missing"). +- **Yes**: "Install Node.js now. Adds a line to your shell startup file so future terminals can find it." +- **No**: "Cancel /jfrog-init." **Do not** mention any install method (nvm, winget), a version manager name, or any URL — not in the question, not in an option description. diff --git a/skills/jfrog-init/references/script-invocation.md b/skills/jfrog-init/references/script-invocation.md index e1c6def..e6595d4 100644 --- a/skills/jfrog-init/references/script-invocation.md +++ b/skills/jfrog-init/references/script-invocation.md @@ -86,7 +86,7 @@ lookups), `node commands, not the function calls a pre-approved script makes once running, so the mutation executes with no prompt whenever Step 5 finds a placeholder. This is intentional, not an oversight: the write is - narrowly scoped to `mcpServers.jfrog.url`, atomic (temp file + rename), + narrowly scoped to the jfrog entry's `url` field, atomic (temp file + rename), and idempotent — see `mcp-plugin-config.md` for why that scope was judged safe to run unattended, unlike the two mutations below. - `jfrog-state-file.mjs`'s **`set`** mode (writes `~/.jfrog/setup.json` diff --git a/skills/jfrog-init/references/server-picker.md b/skills/jfrog-init/references/server-picker.md index 0e46e2d..b5be11b 100644 --- a/skills/jfrog-init/references/server-picker.md +++ b/skills/jfrog-init/references/server-picker.md @@ -23,31 +23,20 @@ triggered this — is reasoning for you to follow silently, never to narrate.** The user never sees why they're being asked, only the `AskUserQuestion` payload itself. -Call `AskUserQuestion` with the **first two** entries of `candidates` -(in the order the detector returned them — never reordered, never -chosen by matching a hostname, git identity, or any other signal) as -the two options, and rely on the tool's built-in "Other" for typing a -different server-id: +Ask the user (see `how-to-ask-user.md` for native tool and fallback +rules) with the **first two** entries of `candidates` (in the order +the detector returned them — never reordered, never chosen by matching +a hostname, git identity, or any other signal) as the two options, and +include a free-text escape hatch for typing a different server-id: -```json -{ - "questions": [ - { - "question": "Which JFrog server do you want to use?", - "header": "Server", - "multiSelect": false, - "options": [ - {"label": "", "description": "Server ID: "}, - {"label": "", "description": "Server ID: "} - ] - } - ] -} -``` +- **Question**: "Which JFrog server do you want to use?" +- **Option 1**: `` (description: "Server ID: ``") +- **Option 2**: `` (description: "Server ID: ``") +- **Other**: let the user type a different server-id if needed **Never surface the full candidate list or a count** to the user in -any case — the picker's two options (plus "Other") are the entire -user-facing surface, same rule as the project picker. +any case — the picker's two options (plus the free-text escape hatch) +are the entire user-facing surface, same rule as the project picker. On picking option 1 or 2, or typing a value via **Other** → re-invoke **the same detector that emitted the ask** (never a different one) diff --git a/skills/jfrog-init/scripts/jfrog-detect-jfrog-mcp.mjs b/skills/jfrog-init/scripts/jfrog-detect-jfrog-mcp.mjs index 510457f..35f1351 100755 --- a/skills/jfrog-init/scripts/jfrog-detect-jfrog-mcp.mjs +++ b/skills/jfrog-init/scripts/jfrog-detect-jfrog-mcp.mjs @@ -1,6 +1,7 @@ #!/usr/bin/env node // Verifies the JFrog PLUGIN'S OWN mcp.json (per harness) exists at its -// installed path AND contains an mcpServers.jfrog entry. This file is +// installed path AND contains a jfrog entry (under mcpServers.jfrog on every +// harness but Codex, which has a bare top-level jfrog key). This file is // owned by the plugin — we NEVER write to it, with one exception: // automatic placeholder substitution (see jfrog-substitute-mcp-placeholders.mjs). // If it's missing, malformed, or lacks the jfrog entry, the correct fix @@ -98,13 +99,9 @@ export function detectJfrogMcp(serverIdArg) { return 3; } - // Auto-substitute any `${JFROG_PLATFORM_URL}` / `${JFROG_URL}` placeholder - // with the real JPD URL from `jf config`. Left in place, the MCP would - // fail to load in the IDE/agent since the env var doesn't exist. - // Checked against mcpServers.jfrog.url specifically (not the raw file - // text) so a placeholder-shaped string elsewhere in the plugin's - // mcp.json — an unrelated MCP entry, say — can't trigger substitution - // for a jfrog.url that has none. + // Substitute a placeholder in the jfrog entry's own url with the real + // JPD URL from `jf config` — checked there specifically, not the raw + // file text, so an unrelated MCP entry can't trigger it. const preSubstitutionUrl = jfrogMcpUrl(parsed); if (typeof preSubstitutionUrl === "string" && hasMcpPlaceholder(preSubstitutionUrl)) { const result = substituteMcpPlaceholders(target, SERVER_ID); @@ -134,11 +131,11 @@ export function detectJfrogMcp(serverIdArg) { const url = jfrogMcpUrl(parsed); const hasUrl = typeof url === "string" && url.trim() !== ""; if (!hasUrl) { - emit("red", target, "plugin mcp.json has no valid mcpServers.jfrog entry (missing or empty url) — reinstall or update the JFrog plugin"); + emit("red", target, "plugin mcp.json has no valid jfrog entry (missing or empty url) — reinstall or update the JFrog plugin"); return 1; } - emit("green", target, "plugin mcp.json present with mcpServers.jfrog entry"); + emit("green", target, "plugin mcp.json present with a jfrog entry"); return 0; } diff --git a/skills/jfrog-init/scripts/jfrog-reinstall-jfrog-plugin.mjs b/skills/jfrog-init/scripts/jfrog-reinstall-jfrog-plugin.mjs index 062785c..f115185 100755 --- a/skills/jfrog-init/scripts/jfrog-reinstall-jfrog-plugin.mjs +++ b/skills/jfrog-init/scripts/jfrog-reinstall-jfrog-plugin.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node // This script itself never writes to the plugin's mcp.json — the file is -// owned by the JFrog plugin (Cursor / VS Code / Claude). It only prints +// owned by the JFrog plugin (Cursor / VS Code / Claude / Codex). It only prints // the diagnosis and the correct remedy for a plugin file that's missing // or invalid: reinstall or update the plugin, with per-harness commands // so the user isn't left guessing. (A placeholder-only problem — e.g. an @@ -47,11 +47,21 @@ After install, restart Claude Code, then re-run /jfrog-init.`); Restart VS Code, then re-run /jfrog-init.`); break; + case "codex": + console.log(`Codex: + codex plugin remove jfrog@codex-plugin # if already installed + codex plugin marketplace add jfrog/codex-plugin # skip if already configured + codex plugin marketplace upgrade codex-plugin + codex plugin add jfrog@codex-plugin + +Restart Codex, then re-run /jfrog-init.`); + break; default: console.log(`Reinstall the JFrog plugin in whichever IDE you're using: Cursor: Settings → Plugins → search "JFrog" → reinstall. VS Code: code --install-extension JFrog.jfrog-vscode-extension --force Claude Code: claude plugin install jfrog-beta/jfrog + Codex: codex plugin marketplace add jfrog/codex-plugin && codex plugin add jfrog@codex-plugin Restart the IDE afterwards, then re-run /jfrog-init.`); } @@ -62,6 +72,8 @@ Expected plugin-owned paths (for reference): Cursor: ~/.cursor/plugins/cache/cursor-public/jfrog//mcp.json VS Code: ~/.vscode/agent-plugins/github.com/jfrog/vscode-plugin/plugin/.mcp.json Claude: ~/.claude/plugins/cache//jfrog//.mcp.json + Codex: $CODEX_HOME/plugins/cache/codex-plugin/jfrog//.mcp.json + ($CODEX_HOME defaults to ~/.codex) `); if (resolved.path && existsSync(resolved.path)) { diff --git a/skills/jfrog-init/scripts/jfrog-resolve-mcp-config.mjs b/skills/jfrog-init/scripts/jfrog-resolve-mcp-config.mjs index f38878b..14d7e0e 100755 --- a/skills/jfrog-init/scripts/jfrog-resolve-mcp-config.mjs +++ b/skills/jfrog-init/scripts/jfrog-resolve-mcp-config.mjs @@ -13,6 +13,9 @@ // Claude: ~/.claude/plugins/cache//jfrog//.mcp.json // (glob across any marketplace + version; most-recently- // modified wins.) +// Codex: $CODEX_HOME/plugins/cache/codex-plugin/jfrog//.mcp.json +// (multiple dirs may exist; most-recently-modified +// wins. $CODEX_HOME defaults to ~/.codex.) // // NOTE (Claude): the current released Claude plugin (jfrog-beta/0.3.0-beta.1) // does NOT ship a .mcp.json — the source repo has one, but the packager @@ -21,17 +24,25 @@ // converts into a clear red / "reinstall the JFrog plugin" instruction. // // Harness detection (env-var signals, in order): -// 1. Claude Code -> $CLAUDECODE / $CLAUDE_CODE_* set -// 2. Cursor -> $CURSOR_AGENT / $CURSOR_CLI / $CURSOR_TRACE_ID set, +// 1. Codex -> $CODEX_SANDBOX / $CODEX_THREAD_ID / $CODEX_CI set +// 2. Claude Code -> $CLAUDECODE / $CLAUDE_CODE_* set +// 3. Cursor -> $CURSOR_AGENT / $CURSOR_CLI / $CURSOR_TRACE_ID set, // or TERM_PROGRAM=cursor -// 3. VS Code -> $VSCODE_PID set, or TERM_PROGRAM=vscode -// The Cursor signals mirror the base skill's check-environment.sh -// detect_harness() and harness-common.md's routing table. +// 4. VS Code -> $VSCODE_PID set, or TERM_PROGRAM=vscode +// Codex is listed first because a Codex session launched from inside +// another harness's terminal still carries that host's own signal — and +// nesting goes both ways, so more than one signal can be present at once. +// When that happens, detectHarness() below walks the process ancestry to +// find which harness actually spawned this invocation. +// detectHarness() is the single JS implementation — exported and reused +// by every other script in this skill that needs harness information. // // Overrides: -// - JFROG_INIT_HARNESS=claude|cursor|vscode forces one specific harness. -// - JFROG_INIT_MCP_CONFIG=/abs/path forces one specific path. +// - JFROG_INIT_HARNESS=claude|cursor|vscode|codex forces one specific harness. +// - JFROG_INIT_MCP_CONFIG=/abs/path forces one specific path. // (Escape hatch — bypasses the plugin-path resolution entirely.) +// - CODEX_HOME=/abs/path Codex's own var, honored by +// the codex branch below; defaults to ~/.codex. // // CLI usage: node jfrog-resolve-mcp-config.mjs // Prints only the path on stdout on success. @@ -39,32 +50,66 @@ // Exit 1 -> could not detect the current harness // Exit 2 -> harness detected, but the plugin's mcp.json is not installed +import { execFileSync } from "node:child_process"; import { existsSync, readdirSync, statSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import { isMainModule } from "./lib/jf.mjs"; -const VALID_HARNESSES = new Set(["claude", "cursor", "vscode"]); +const VALID_HARNESSES = new Set(["claude", "cursor", "vscode", "codex"]); -// JFROG_INIT_HARNESS is matched case-insensitively (e.g. "Claude", "CURSOR") -// so the documented override doesn't silently fail on a case mismatch. -export function detectHarness() { +// One entry per harness, in priority order (see doc comment above) — used +// both as the signal check and as the static fallback when the ancestry +// tie-break can't resolve it. Adding a harness (Kiro, OpenCode, ...) is +// just a new entry here. +const HARNESS_SIGNALS = [ + { name: "codex", signaled: () => process.env.CODEX_SANDBOX || process.env.CODEX_THREAD_ID || process.env.CODEX_CI }, + { name: "claude", signaled: () => process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT || process.env.CLAUDE_CODE_SESSION_ID }, + // Checked before VS Code: Cursor's CLI/agent surfaces can report TERM_PROGRAM=vscode. + { name: "cursor", signaled: () => process.env.CURSOR_AGENT || process.env.CURSOR_CLI || process.env.CURSOR_TRACE_ID || process.env.TERM_PROGRAM === "cursor" }, + { name: "vscode", signaled: () => process.env.VSCODE_PID || process.env.TERM_PROGRAM === "vscode" }, +]; + +// Breaks ties when multiple harness signals fire at once: env vars are +// inherited by child processes regardless of nesting direction, so +// presence alone can't tell them apart. Walks up from the immediate +// parent — skipping the shell/node layers each harness spawns to run a +// command (e.g. codex -> bash -> claude -> bash -> node) — until a +// process name matches a candidate, or maxDepth is hit. Unix-only (ps); +// returns [] on failure (e.g. Windows), which falls through to the +// static priority order. +function getAncestorChain(maxDepth = 12) { + const chain = []; + let pid = process.ppid; + for (let i = 0; i < maxDepth && pid > 1; i++) { + let line; + try { + line = execFileSync("ps", ["-o", "ppid=,comm=", "-p", String(pid)]).toString().trim(); + } catch { + break; + } + const match = line.match(/^(\d+)\s+(.*)$/); + if (!match) break; + chain.push(match[2].toLowerCase()); + pid = Number(match[1]); + } + return chain; +} + +// JFROG_INIT_HARNESS is matched case-insensitively so the documented +// override doesn't silently fail on a case mismatch. getAncestors is +// injectable so tests can stub the tie-break without spawning `ps`. +export function detectHarness(getAncestors = getAncestorChain) { if (process.env.JFROG_INIT_HARNESS) return process.env.JFROG_INIT_HARNESS.trim().toLowerCase(); - if (process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT || process.env.CLAUDE_CODE_SESSION_ID) return "claude"; - // CURSOR_AGENT / CURSOR_CLI are as much a Cursor signal as - // CURSOR_TRACE_ID — all three are what the base skill's - // check-environment.sh detect_harness() and harness-common.md's routing - // table treat as Cursor, and this function has to agree with them or - // the two disagree about which harness the same session is running in. - // Order matters as much as the signals: Cursor's CLI/agent surfaces can - // report TERM_PROGRAM=vscode, so the Cursor test has to run before the - // VS Code one below or those surfaces resolve to the VS Code plugin - // path and the detector reports the JFrog plugin as missing. - if (process.env.CURSOR_AGENT || process.env.CURSOR_CLI || process.env.CURSOR_TRACE_ID || process.env.TERM_PROGRAM === "cursor") { - return "cursor"; + const candidates = HARNESS_SIGNALS.filter((h) => h.signaled()).map((h) => h.name); + if (candidates.length <= 1) return candidates[0] || ""; + // Multiple signals at once means nested harnesses — resolve via + // ancestry, else fall back to the static priority order. + for (const comm of getAncestors()) { + const match = candidates.find((name) => comm.includes(name)); + if (match) return match; } - if (process.env.VSCODE_PID || process.env.TERM_PROGRAM === "vscode") return "vscode"; - return ""; + return candidates[0]; } // Picks the newest file matching `/*/<...tailParts>` by mtime. @@ -131,6 +176,59 @@ function newestClaudeMatch() { return best; } +function resolveClaudePath() { + const match = newestClaudeMatch(); + if (!match) { + return { + error: + "JFrog Claude plugin does not ship a .mcp.json at ~/.claude/plugins/cache/*/jfrog/*/.mcp.json\n" + + " reinstall or update the JFrog plugin so it includes the file.", + code: 2, + }; + } + return { path: match }; +} + +function resolveCursorPath() { + const match = newestMatch(join(homedir(), ".cursor", "plugins", "cache", "cursor-public", "jfrog"), ["mcp.json"]); + if (!match) { + return { + error: + "JFrog Cursor plugin's mcp.json not found under ~/.cursor/plugins/cache/cursor-public/jfrog/\n" + + " install the JFrog plugin in Cursor to make it available.", + code: 2, + }; + } + return { path: match }; +} + +function resolveVscodePath() { + const p = join(homedir(), ".vscode", "agent-plugins", "github.com", "jfrog", "vscode-plugin", "plugin", ".mcp.json"); + if (!existsSync(p)) { + return { + error: `JFrog VS Code plugin's .mcp.json not found at ${p}\n install the JFrog plugin in VS Code to make it available.`, + code: 2, + }; + } + return { path: p }; +} + +function resolveCodexPath() { + const codexHome = process.env.CODEX_HOME || join(homedir(), ".codex"); + const codexPluginDir = join(codexHome, "plugins", "cache", "codex-plugin", "jfrog"); + const match = newestMatch(codexPluginDir, [".mcp.json"]); + if (!match) { + return { + error: + `JFrog Codex plugin's .mcp.json not found under ${codexPluginDir}/\n` + + " run `codex plugin marketplace add jfrog/codex-plugin` then\n" + + " `codex plugin add jfrog@codex-plugin` to make it available.", + code: 2, + }; + } + return { path: match }; +} + export function resolveMcpConfig() { if (process.env.JFROG_INIT_MCP_CONFIG) { return { path: process.env.JFROG_INIT_MCP_CONFIG }; @@ -144,55 +242,29 @@ export function resolveMcpConfig() { // set the very variable they already set. if (process.env.JFROG_INIT_HARNESS && !VALID_HARNESSES.has(harness)) { return { - error: `JFROG_INIT_HARNESS=${process.env.JFROG_INIT_HARNESS} is not one of: claude, cursor, vscode.`, + error: `JFROG_INIT_HARNESS=${process.env.JFROG_INIT_HARNESS} is not one of: claude, cursor, vscode, codex.`, code: 1, }; } - if (harness === "claude") { - const match = newestClaudeMatch(); - if (!match) { - return { - error: - "JFrog Claude plugin does not ship a .mcp.json at ~/.claude/plugins/cache/*/jfrog/*/.mcp.json\n" + - " reinstall or update the JFrog plugin so it includes the file.", - code: 2, - }; - } - return { path: match }; - } - - if (harness === "cursor") { - const match = newestMatch(join(homedir(), ".cursor", "plugins", "cache", "cursor-public", "jfrog"), ["mcp.json"]); - if (!match) { + switch (harness) { + case "claude": + return resolveClaudePath(); + case "cursor": + return resolveCursorPath(); + case "vscode": + return resolveVscodePath(); + case "codex": + return resolveCodexPath(); + default: return { error: - "JFrog Cursor plugin's mcp.json not found under ~/.cursor/plugins/cache/cursor-public/jfrog/\n" + - " install the JFrog plugin in Cursor to make it available.", - code: 2, - }; - } - return { path: match }; - } - - if (harness === "vscode") { - const p = join(homedir(), ".vscode", "agent-plugins", "github.com", "jfrog", "vscode-plugin", "plugin", ".mcp.json"); - if (!existsSync(p)) { - return { - error: `JFrog VS Code plugin's .mcp.json not found at ${p}\n install the JFrog plugin in VS Code to make it available.`, - code: 2, + "could not detect current harness (Claude Code / Cursor / VS Code / Codex).\n" + + " Set JFROG_INIT_HARNESS=claude|cursor|vscode|codex, or\n" + + " JFROG_INIT_MCP_CONFIG=/absolute/path/to/mcp.json to override.", + code: 1, }; - } - return { path: p }; } - - return { - error: - "could not detect current harness (Claude Code / Cursor / VS Code).\n" + - " Set JFROG_INIT_HARNESS=claude|cursor|vscode, or\n" + - " JFROG_INIT_MCP_CONFIG=/absolute/path/to/mcp.json to override.", - code: 1, - }; } if (isMainModule(import.meta.url)) { diff --git a/skills/jfrog-init/scripts/jfrog-substitute-mcp-placeholders.mjs b/skills/jfrog-init/scripts/jfrog-substitute-mcp-placeholders.mjs index 8bd635e..c75b6f0 100755 --- a/skills/jfrog-init/scripts/jfrog-substitute-mcp-placeholders.mjs +++ b/skills/jfrog-init/scripts/jfrog-substitute-mcp-placeholders.mjs @@ -1,14 +1,15 @@ #!/usr/bin/env node // Rewrites a placeholder-style env-var reference in the JFrog plugin's -// mcp.json — specifically `mcpServers.jfrog.url`, nothing else in the -// file — with the real JPD URL from `jf config`. This is the ONLY code -// path in /jfrog-init that writes to the plugin-owned mcp.json. +// mcp.json — specifically the `jfrog` entry's `.url` (nested under +// `mcpServers` on every harness but Codex, which has no wrapper) — with +// the real JPD URL from `jf config`. This is the ONLY code path in +// /jfrog-init that writes to the plugin-owned mcp.json. // // Scoped to that one field (rather than a file-wide text replace) so an // unrelated MCP server entry or JSON value that happens to contain the // same placeholder text is never touched. // -// Placeholders handled (both `$VAR` and `${VAR}` forms): +// Placeholders handled (`$VAR`, `${VAR}`, and Codex's `` forms): // - JFROG_PLATFORM_URL // - JFROG_URL // @@ -37,7 +38,7 @@ // Exit 3 -> read/write error, or jf missing import { existsSync, readFileSync, writeFileSync, renameSync, statSync, chmodSync, unlinkSync } from "node:fs"; -import { emit as emitJf, isMainModule, jfAvailable, jfConfigShow, urlForServer, normalizeJpdUrl, mcpPlaceholderRegexes, jfrogMcpUrl, hasMcpPlaceholder, askServerResult, describeJfUnavailable } from "./lib/jf.mjs"; +import { emit as emitJf, isMainModule, jfAvailable, jfConfigShow, urlForServer, normalizeJpdUrl, mcpPlaceholderRegexes, jfrogMcpEntry, jfrogMcpUrl, hasMcpPlaceholder, askServerResult, describeJfUnavailable } from "./lib/jf.mjs"; import { resolveJfServer } from "./jfrog-resolve-jf-server.mjs"; // Result shape: { exitCode, status, detail, candidates? } — mirrors the @@ -69,7 +70,7 @@ export function substituteMcpPlaceholders(target, serverIdOverride) { const currentUrl = jfrogMcpUrl(parsed); if (currentUrl === null) { - return { exitCode: 0, status: "green", detail: "no mcpServers.jfrog.url present — nothing to substitute" }; + return { exitCode: 0, status: "green", detail: "no jfrog entry url present — nothing to substitute" }; } // Checked before resolving a jf server at all — an unresolvable/ambiguous @@ -106,7 +107,7 @@ export function substituteMcpPlaceholders(target, serverIdOverride) { let newUrl = currentUrl.replace(withScheme, () => jpdUrl); newUrl = newUrl.replace(bare, () => jpdUrl); - parsed.mcpServers.jfrog.url = newUrl; + jfrogMcpEntry(parsed).url = newUrl; const rewritten = JSON.stringify(parsed, null, 2) + "\n"; const tmp = `${target}.tmp.${process.pid}`; diff --git a/skills/jfrog-init/scripts/lib/jf.mjs b/skills/jfrog-init/scripts/lib/jf.mjs index 3a1206a..34d3684 100755 --- a/skills/jfrog-init/scripts/lib/jf.mjs +++ b/skills/jfrog-init/scripts/lib/jf.mjs @@ -398,31 +398,35 @@ export function emit(obj) { process.stdout.write(JSON.stringify(obj) + "\n"); } -// The four supported placeholder forms — `${VAR}` or bare `$VAR` followed -// by a non-identifier character or end of string — and nothing looser. -// Independently-optional braces (`\{?...\}?`) would also match malformed -// or unrelated text like `${JFROG_URL_SUFFIX}` or an unclosed `${JFROG_URL`; -// the `\b` after the bare form and the exact `\{...\}` pairing rule both -// out. Shared by the detector (jfrog-detect-jfrog-mcp.mjs) and the -// substituter (jfrog-substitute-mcp-placeholders.mjs) so "is there a -// placeholder?" and "replace the placeholder" agree on what counts as one. -const MCP_PLACEHOLDER_PATTERN = "\\$\\{(?:JFROG_PLATFORM_URL|JFROG_URL)\\}|\\$(?:JFROG_PLATFORM_URL|JFROG_URL)\\b"; +// `${VAR}`, bare `$VAR`, or Codex's `` — strict pairing so it doesn't +// also match malformed/unrelated text like `${JFROG_URL_SUFFIX}`. Shared +// by the detector and substituter so both agree on what counts as one. +const MCP_PLACEHOLDER_PATTERN = "\\$\\{(?:JFROG_PLATFORM_URL|JFROG_URL)\\}|\\$(?:JFROG_PLATFORM_URL|JFROG_URL)\\b|<(?:JFROG_PLATFORM_URL|JFROG_URL)>"; export function hasMcpPlaceholder(text) { return new RegExp(MCP_PLACEHOLDER_PATTERN).test(text); } -// Shared "is `mcpServers.jfrog` a valid object, and what's its `.url`?" -// check — used by the detector (jfrog-detect-jfrog-mcp.mjs, to decide if -// there's a url worth validating) and the substituter -// (jfrog-substitute-mcp-placeholders.mjs, to decide if there's a url worth -// rewriting) so the two agree on what counts as a valid entry, the same -// way MCP_PLACEHOLDER_PATTERN keeps "is there a placeholder?" in sync. +// Every harness but Codex nests the entry under `mcpServers.jfrog`; Codex's +// plugin mcp.json has no wrapper — bare top-level `jfrog`. Branch keyed on +// the KEY's presence, not truthiness, so `{ "mcpServers": null, "jfrog": +// {...} }` isn't mistaken for Codex-shaped, and JFROG_INIT_MCP_CONFIG can +// still point at a Codex-shaped file from any harness. +// +// Returns the live entry object (mutations land on `parsed`), or null. +export function jfrogMcpEntry(parsed) { + if (parsed === null || typeof parsed !== "object") return null; + const wrapped = "mcpServers" in parsed; + const entry = wrapped ? parsed.mcpServers?.jfrog : parsed.jfrog; + if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return null; + if (!wrapped && !("url" in entry)) return null; + return entry; +} + // Returns the url string (possibly empty) on a valid entry, null otherwise. export function jfrogMcpUrl(parsed) { - const entry = parsed?.mcpServers?.jfrog; - if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return null; - return typeof entry.url === "string" ? entry.url : null; + const entry = jfrogMcpEntry(parsed); + return entry && typeof entry.url === "string" ? entry.url : null; } // Fresh RegExp instances every call — a shared module-level `g`-flagged diff --git a/skills/jfrog-mcp-management/SKILL.md b/skills/jfrog-mcp-management/SKILL.md index 8f38454..d70fbe5 100644 --- a/skills/jfrog-mcp-management/SKILL.md +++ b/skills/jfrog-mcp-management/SKILL.md @@ -1,10 +1,15 @@ --- name: jfrog-mcp-management description: >- - Use to install, list, or remove MCP servers through the JFrog Agent Guard (npx - @jfrog/agent-guard), and browse the JFrog MCP catalog. Use whenever the user - wants to add/enable/list/remove/uninstall an MCP server or tool — even without - saying "MCP" — and never install one any other way. + Use to install, list, or remove MCP servers, and to discover which MCPs the + user can install — including questions about available, approved, or allowed + MCPs for a project. That governed catalog is the authoritative answer; do + not answer those questions from local IDE/settings allowlists alone. Use + whenever the user wants to add/enable/install/list/remove/uninstall/configure + an MCP or write/update the agent's MCP config — even if they name a package + like `@scope/pkg` and even without saying "MCP" or "JFrog". Never install an + MCP any other way. All of this goes through the JFrog Agent Guard (npx + @jfrog/agent-guard). compatibility: >- Requires network access to the npm registry and the JFrog platform, and ~/.jfrog/ write access for OAuth login and cache cleanup. @@ -101,7 +106,7 @@ and List → Currently installed read only local config, so they skip this. | User intent | Section | | --- | --- | | add / install / set up / enable / configure an MCP | [Install](#install-an-mcp) | -| list / show / what can I install / what's set up / connected | [List](#list-mcps) | +| list / show / what can I install / allowed / approved / available / catalog / what's set up / connected in the context of MCPs | [List](#list-mcps) | | remove / uninstall / delete / disconnect / turn off an MCP | [Remove](#remove-an-mcp) | --- @@ -196,7 +201,7 @@ Split Step 2 inputs by `isRequired`: Handling: **secrets** (`isSecret=true`) MUST be a value reference, NEVER a raw value — never take a secret in chat, echo it, or write it into config. **Non-secrets** may be a literal or a reference. For the exact syntax and, on -shell-based harnesses (Claude Code, Cursor, Codex, Devin Desktop, OpenCode), how the user +shell-based harnesses (Claude Code, Cursor, Codex, Devin, OpenCode), how the user exports/persists the variable, see the harness file and [references/persisting-env-vars.md](references/persisting-env-vars.md). (VS Code prompts for `inputs` values on first start — no shell export.) @@ -277,7 +282,7 @@ file or shell: | User said… | Run | | --- | --- | -| "available", "what can I install", "what's in the catalog", "list MCPs" without other context | **Available to install** — go straight to `--list-available`; do NOT inspect local files first | +| "available", "what can I install", "what's in the catalog", "list MCPs", "allowed to install", "approved" without other context | **Available to install** — go straight to `--list-available`; do NOT inspect local files / IDE allowlists first. Do NOT ask whether to check the catalog. | | "installed", "configured", "connected", "running", "what MCPs do I have" | **Currently installed** | | ambiguous / both | run **both** in order: Currently installed first, then Available to install, as separate tables | diff --git a/skills/jfrog-mcp-management/references/agent-guard-common.md b/skills/jfrog-mcp-management/references/agent-guard-common.md index f060bf9..1d1e1e3 100644 --- a/skills/jfrog-mcp-management/references/agent-guard-common.md +++ b/skills/jfrog-mcp-management/references/agent-guard-common.md @@ -19,8 +19,25 @@ Wherever `` appears, substitute the value of the `JFROG_AGENT_GUARD_REPO` environment variable if it is set. Otherwise use `https://releases.jfrog.io/artifactory/api/npm/coding-agents-npm/`. +`@jfrog/agent-guard` is not published to the public npm registry; resolve it +with `--registry ` above rather than the default npm registry. + ## Pre-flight (applies to every agent guard command — `--list-available`, `--inspect`, `--login`) +**Environment probe (run once before resolving the project key and server).** +Before resolving credentials and the JFrog project key, run the skill’s env +probe so each var is printed on its own line (chained `printenv` or truncating +with `head` can merge lines and confuse the read). Prefer this over inventing +an env check; do not print raw token values — report tokens as `present` only; +prefer reporting `JFROG_URL` / `JF_URL` as `present` only as well; an empty +value after the label means the variable is unset. Print real values for +`JF_PROJECT` and `JFROG_AGENT_GUARD_REPO` (needed for `--project` and +enforceable MCP entries): + +```bash +node "/scripts/jfrog-agent-guard-env-probe.mjs" +``` + - **Live execution is MANDATORY — context reuse is FORBIDDEN.** Every time the user asks to list / show / inspect / check the catalog or a specific MCP — including a repeated question already answered earlier in the chat — you diff --git a/skills/jfrog-mcp-management/references/harness-common.md b/skills/jfrog-mcp-management/references/harness-common.md index 850a365..febf55a 100644 --- a/skills/jfrog-mcp-management/references/harness-common.md +++ b/skills/jfrog-mcp-management/references/harness-common.md @@ -38,7 +38,7 @@ editor UI or `mcp.json`, so it falls through to the Fallback section. | Codex | `CODEX_SANDBOX` / `CODEX_THREAD_ID` / `CODEX_CI` | [harness-codex.md](harness-codex.md) | | Cursor | `CURSOR_AGENT` / `CURSOR_CLI` / `CURSOR_TRACE_ID` env var | [harness-cursor.md](harness-cursor.md) | | OpenCode | `OPENCODE` | [harness-opencode.md](harness-opencode.md) | -| Devin Desktop | Your system prompt / system instructions identify you as **Devin** (Devin Desktop / Devin Local / Cognition). That alone is enough. Optionally confirm with `VSCODE_IPC_HOOK` set to the Devin Desktop IPC socket (full path), e.g. macOS: `~/Library/Application Support/Devin/-main.sock` — the expanded path contains `/Devin/`. The path alone is **not** enough. | [harness-devin.md](harness-devin.md) | +| Devin | Your system prompt / system instructions identify you as **Devin** (Devin Desktop / Devin Local / Devin CLI / Cognition). | [harness-devin.md](harness-devin.md) | | VS Code editor | `TERM_PROGRAM=vscode` **and no `CURSOR_*` var is set** **and no `OPENCODE` var is set** **and no `CODEX_*` var is set** **and no `CLAUDECODE`/`CLAUDE_CODE_ENTRYPOINT` var is set** **and no `GEMINI_CLI` / `GOOSE_TERMINAL` / `COPILOT_CLI` var is set** **and** your system prompt / system instructions do **not** identify you as Devin | [harness-vscode.md](harness-vscode.md) | | anything else | none of the above | **Fallback** section below — no harness file exists | diff --git a/skills/jfrog-mcp-management/references/harness-devin.md b/skills/jfrog-mcp-management/references/harness-devin.md index 49616c1..d4800ca 100644 --- a/skills/jfrog-mcp-management/references/harness-devin.md +++ b/skills/jfrog-mcp-management/references/harness-devin.md @@ -1,37 +1,59 @@ -# Harness: Devin **Desktop** - -Devin Desktop-specific config for the `jfrog-mcp-management` skill. Read this -together with [harness-common.md](harness-common.md) (shared entry shape and -success criterion). You reached this file because Step A matched **Devin**: -your system prompt / system instructions identify you as Devin. You may -optionally confirm with `VSCODE_IPC_HOOK` under the Devin user-data dir (e.g. -`~/Library/Application Support/Devin/-main.sock`). The environment +# Harness: Devin + +Devin-specific config for the `jfrog-mcp-management` skill (Devin CLI and +Devin Local in Devin Desktop). Read this together with +[harness-common.md](harness-common.md) (shared entry shape and success +criterion). You reached this file because Step A matched **Devin**: your +system prompt / system instructions identify you as Devin. The environment script does not detect Devin. -Devin Desktop is a VS Code-family Electron shell that runs the Cascade / Devin -Local agent. It stores MCP configuration in the Windsurf config file used by -the underlying platform. +This harness targets the **Devin plugin** path only. + +## Detect the Devin surface + +Run this from **your** agent environment (not a terminal the user typed into — +the two can differ) before choosing restart or verify steps: + +```bash +printf 'TERM_PROGRAM=%s\n' "${TERM_PROGRAM-}" +printf 'VSCODE_IPC_HOOK=%s\n' "${VSCODE_IPC_HOOK-}" +``` + +Classify from the result. **Environment markers win over system-prompt +wording**, because Devin Local's system prompt also describes an "interactive +command line agent" and must NOT be treated as CLI: + +- **Devin Desktop (Devin Local):** `VSCODE_IPC_HOOK` is set to a path inside a + Devin user-data directory (contains `/Devin/` on macOS/Linux, `\Devin\` on + Windows). Optionally the system prompt identifies Devin Desktop / + Devin Local. +- **Devin CLI:** no Devin Desktop marker above — `VSCODE_IPC_HOOK` unset (or + not under a Devin user-data dir). This holds even when the system prompt + calls you an "interactive command line agent". +- If a Desktop marker is present, choose Desktop even if the prompt reads + CLI-like. If nothing is conclusive, ASK the user — do not guess. + +Use the matching Desktop or CLI instructions below for restart, list, and +verify. **Config write path is the same for both** (see Config files). Do not +mix restart or verification surfaces. ## Config files +Both Devin CLI and Devin Local use the same Devin MCP config files. + - **Default scope: user-level.** Personal, not committed, available across all - workspaces. **Prefer Windsurf** — the same file Cascade uses and that the - JFrog Desktop extension writes the `jfrog` MCP into: - - macOS/Linux: `~/.codeium/windsurf/mcp_config.json` - - Windows: `%APPDATA%\.codeium\windsurf\mcp_config.json` + workspaces: + - macOS/Linux: `~/.config/devin/mcp_config.json` + - Windows: `%APPDATA%\devin\mcp_config.json` Create the parent directory first (`mkdir -p` / platform equivalent), then - create the file if missing: `{ "mcpServers": {} }`. Devin Local imports this - file when `read_config_from.windsurf` is not `false` in - `~/.config/devin/config.json` (default) — so one write serves Cascade and Local. -- **Exception — migrated native store:** If `~/.config/devin/mcp_config.json` - **already exists** (user accepted **Migrate MCP config** / Copy), Devin Local - uses that file instead of Windsurf import. For Local, merge entries **there** - and do **not** require `read_config_from.windsurf`. Cascade never reads the - native file — if the entry must also appear in Cascade, merge into Windsurf - as well. Prefer **Cancel** on migrate so both agents stay on Windsurf. -- **Project scope:** Not supported by Devin Desktop's Cascade / Windsurf config. -- Do not ask which scope unless the user brings it up. + create the file if missing: `{ "mcpServers": {} }`. +- **Project scope** (only if the user asks): `.devin/mcp_config.json` in the + project root (shared / commit-able). +- **Local project override** (only if the user asks): `.devin/mcp_config.local.json` + (gitignored; personal keys). +- Do not ask which scope unless the user brings it up; use the user-level + default above. ## Top-level key @@ -39,106 +61,91 @@ the underlying platform. ## Value reference (env / secrets) -`${env:VAR_NAME}`, resolved from the environment that launched Devin Desktop. -For `Bearer` headers: `"Bearer ${env:TOKEN}"`. Devin Desktop also supports -`${file:~/path/to/file}` to inline a file's trimmed contents. The user must -export the variable in the environment that launches Devin Desktop (see -[persisting-env-vars.md](persisting-env-vars.md)); values are picked up on -next launch. If a required `${env:VAR}` is unset the Agent Guard fails at -startup — confirm the export before restart. Never write a raw secret. +`${env:VAR_NAME}`, resolved from the environment that launches the current +surface (Devin Desktop or Devin CLI). For `Bearer` headers: +`"Bearer ${env:TOKEN}"`. Also supports `${file:~/path/to/file}` to inline a +file's trimmed contents. The user must export the variable in the launching +environment (see [persisting-env-vars.md](persisting-env-vars.md)); values are +picked up on next launch / new session. If a required `${env:VAR}` is unset +the upstream MCP may fail at startup — confirm the export before restart. +Never write a raw secret. `${env:…}` / `${file:…}` are for the upstream MCP's own secrets and inputs — never for JFrog Agent Guard credentials (see below). ## JFrog credentials - from the `jf` config -**Include `--server ` by default.** It reads that server's URL + token -from the on-disk `jf` CLI config, is unambiguous, and keeps working if the user -later adds more servers. Resolve `` per the agent-guard-common -Pre-flight rules; never emit an empty `--server`. - -`--server` can be **omitted only when exactly one `jf` server is configured** - in -that case the Agent Guard auto-resolves it. With **multiple** `jf` servers, -omitting `--server` fails: the Agent Guard cannot choose between them and does NOT -fall back to the `jf` default, so `--server` is required. (When in doubt, include -it.) - -**Devin exception to the shared rule.** [SKILL.md](../SKILL.md) treats `--server` -as conditional and permits dropping it on the `JFROG_URL`+token env path (see its -Step 4 Guardrails, "`--server` … drop it only on the `JFROG_URL`+token env -path"). **That env path does NOT apply on Devin Desktop** - do NOT authenticate -JFrog via env-var credentials, even though Devin Desktop would resolve -`${env:JFROG_URL}` / `${env:JFROG_ACCESS_TOKEN}` (or inherit them from the -launch environment) into the Agent Guard process. Use `--server ` -(or a single configured `jf` server) as described above. If there is no usable -`jf` server, ask the user to add one (`jf c add `, or `jf login`) before -continuing. - -If credentials cannot be resolved (no `--server` and either zero or multiple `jf` -servers), the entry fails to start and the server connects with no tools. +On Devin (CLI and Desktop Local), authenticate Agent Guard only through the +on-disk `jf` CLI config. **Always include `--server `** in every +Agent Guard command and written MCP config entry — resolve `` per +the agent-guard-common Pre-flight rules, never emit an empty `--server`, and do +**not** omit `--server` even when only one `jf` server is configured (explicit +server ID matches plugin enforcement). + +Do **not** use the shared [SKILL.md](../SKILL.md) env-var auth path +(`JFROG_URL` / `JFROG_ACCESS_TOKEN`, or legacy `JF_URL` / `JF_ACCESS_TOKEN`) on +Devin, even though Devin would resolve or forward them into Agent Guard. If +there is no usable `jf` server, ask the user to add one (`jf c add `, or +`jf login`) before continuing. + +If credentials cannot be resolved (no `--server ` in the entry, or no +usable `jf` server to resolve one from), the entry fails to start and the server +connects with no tools. ## Enable -Devin Desktop loads every non-disabled entry in `mcpServers` automatically on -window load; there is no per-server approval prompt to pre-approve. If the -entry carries `"disabled": true`, remove it so the server runs. Otherwise -nothing to do here. +Both surfaces start every server under `mcpServers` that is not marked +`"disabled": true` on that server's own entry (per-server flag in the config — +same idea as `devin mcp disable` / `enable`). If `` has +`"disabled": true`, remove that flag so the server can run. Approving MCP tool +calls in chat is separate from enablement. ## Restart -`Developer: Reload Window` (or fully quit and reopen Devin Desktop). Devin -Desktop re-reads `mcp_config.json` on window load and reconnects each server. +- **Devin Desktop (Local):** tell the user to run `Developer: Reload Window` (or fully quit and + reopen Devin Desktop). Desktop re-reads MCP config on window / session load. +- **Devin CLI:** tell the user to start a new Devin CLI session — exit and run + `devin` again in the same directory — so the added/removed entry takes + effect (user, project, and local MCP config files are read at session start). ## List installed -Open the **MCP servers** panel (Cascade panel toolbar, or -`Devin Settings → Cascade → MCP Servers`), or **Open customizations** on a -Devin Local session — each configured server is listed with its live -connection state. Servers and their tools are also reachable via `@` in the -chat input. Do **not** use `/mcp` here: that slash command is Devin CLI only; -in Desktop `/` lists workflows, so `/mcp` can fuzzy-match a skill and mislead. -Confirm via the MCP servers panel / Open customizations, or by checking that -`` exists under `mcpServers` in the active store (Windsurf by default; -native `~/.config/devin/mcp_config.json` only when that file already exists — -see Config files). When reading the file, do not report secret values — env -**key names** only; never display resolved `${env:…}` or `${file:…}` contents. +Read `mcpServers` from `~/.config/devin/mcp_config.json` (or the project/local +file if that scope was used). Do not report secret values — env **key names** +only. + +- **Devin CLI and Devin Desktop (Local):** run `devin mcp list` for + live connection status. +- If the config and `devin mcp list` are not enough, tell the user to run + `/mcp` for the interactive status panel. On **Devin Desktop (Local)** only, + they can also open **Open customizations** (MCP list) and report each + server's status. ## Verify -Before treating a missing server as Failed: confirm the entry is in the active -store (Windsurf by default; native only when that file already exists). For -Devin Local on Windsurf, also confirm `read_config_from.windsurf` is not -`false`. Skip that flag check when Local is on the native file. - -Ask which MCP servers are available, or open the MCP servers panel / Open -customizations, and confirm `` is listed and connected. Then ask the -agent to list that server's tools (or reach it via `@`); the server MUST -expose **at least one tool**. A connected indicator alone is NOT proof — the -Agent Guard proxy can report connected with 0 upstream tools. Empty tool -list = Failed → see the "0 tools" troubleshooting in +Before treating a missing server as Failed, confirm the entry is in the active +store for this harness (user `~/.config/devin/mcp_config.json` by default). + +After the user completes Restart (see Restart), run `devin mcp list` for connection status, +then **list that server's live tools** through the connected MCP (Devin CLI and +Devin Desktop Local). + +The server MUST expose **at least one tool**. A connected indicator alone is +NOT proof — the Agent Guard proxy can report connected with 0 upstream tools. +Empty tool list = Failed → see the "0 tools" troubleshooting in [key-rules-and-troubleshooting.md](key-rules-and-troubleshooting.md). +Do **not** treat a tool list scraped from npm / GitHub docs as verification — +only a live tool list from the connected server counts. + On first connect without cached OAuth, Devin opens a browser to sign in; later -runs reuse stored credentials. Treat **Output → MCP** as authentication / -connection status only — never as a source of token values. Devin Local may -also prompt to approve each MCP tool call by default — grant the prompt before -treating an empty list as a failure. +runs reuse stored credentials. Devin Local and Devin CLI may prompt to approve +each MCP tool call by default — grant the prompt before treating an empty list +as a failure. ## Notes -- Cascade always reads `~/.codeium/windsurf/mcp_config.json`. Devin Local - imports that same file when `read_config_from.windsurf` is enabled in - `~/.config/devin/config.json` (default). If Local is on Windsurf and that - flag is `false`, Local will not see Windsurf entries even though the file on - disk is unchanged. -- Some Devin Desktop builds prompt to copy Windsurf MCP config to - `~/.config/devin/mcp_config.json` (**Migrate MCP config**). Prefer **Cancel** - unless the user wants to migrate: once the native file exists, Local uses it - as its store (no Windsurf-import requirement) while Cascade continues to use - Windsurf only — installs then diverge unless you write both. +- Devin CLI and Devin Local share the same MCP config paths. An install from + either surface is visible to the other after the appropriate restart. - OAuth `--login` caches tokens in `~/.jfrog/jfrogmcp.conf.json` (same as all harnesses); removal cleanup of that file is the same everywhere. -- Devin Desktop is distinct from **Devin CLI** (the `devin` terminal agent): - the CLI has its own config at `.devin/config.json` / `.devin/config.local.json` - and is not covered by this harness file. CLI-only surfaces such as `/mcp` - do not apply here. diff --git a/skills/jfrog-mcp-management/references/persisting-env-vars.md b/skills/jfrog-mcp-management/references/persisting-env-vars.md index 1460227..7c0c49c 100644 --- a/skills/jfrog-mcp-management/references/persisting-env-vars.md +++ b/skills/jfrog-mcp-management/references/persisting-env-vars.md @@ -5,7 +5,7 @@ so its value takes effect. How each harness picks up the exported variable: - **Claude Code** — a `${VAR}` reference in the config. - **Cursor** — a `${env:VAR}` reference in the config. -- **Devin Desktop** — a `${env:VAR}` reference in the config. +- **Devin Desktop / Devin CLI** — a `${env:VAR}` reference in the config. - **Codex** — a variable name listed in the `env_vars` allow-list; Codex forwards that named variable's value from the launching shell to the server (e.g. an env var like `Authorization`). diff --git a/skills/jfrog-mcp-management/scripts/jfrog-agent-guard-env-probe.mjs b/skills/jfrog-mcp-management/scripts/jfrog-agent-guard-env-probe.mjs new file mode 100644 index 0000000..14f832e --- /dev/null +++ b/skills/jfrog-mcp-management/scripts/jfrog-agent-guard-env-probe.mjs @@ -0,0 +1,33 @@ +#!/usr/bin/env node +// Copyright (c) JFrog Ltd. 2026 +// Licensed under the Apache License, Version 2.0 +// https://www.apache.org/licenses/LICENSE-2.0 +// +// JFrog Agent Guard environment probe. +// +// Safe cross-platform env dump used by the jfrog-mcp-management skill pre-flight +// (before resolving project key / server). Prints one labeled line per var so +// agents do not invent chained printenv / head probes that merge lines. +// +// Contract: +// - Exit 0 after writing all lines (do not call process.exit — let Node +// flush stdout, especially under pipes) +// - Tokens (JFROG_ACCESS_TOKEN / JF_ACCESS_TOKEN): "present" or empty +// - URLs (JFROG_URL / JF_URL): "present" or empty (prefer presence-only) +// - JF_PROJECT / JFROG_AGENT_GUARD_REPO: real value or empty when unset +// - Never print raw token values + +import process from "node:process"; + +const presentOrEmpty = (value) => (value ? "present" : ""); + +const lines = [ + `JFROG_URL: ${presentOrEmpty(process.env.JFROG_URL)}`, + `JFROG_ACCESS_TOKEN: ${presentOrEmpty(process.env.JFROG_ACCESS_TOKEN)}`, + `JF_URL: ${presentOrEmpty(process.env.JF_URL)}`, + `JF_ACCESS_TOKEN: ${presentOrEmpty(process.env.JF_ACCESS_TOKEN)}`, + `JF_PROJECT: ${process.env.JF_PROJECT ?? ""}`, + `JFROG_AGENT_GUARD_REPO: ${process.env.JFROG_AGENT_GUARD_REPO ?? ""}`, +]; + +process.stdout.write(`${lines.join("\n")}\n`); diff --git a/skills/jfrog/SKILL.md b/skills/jfrog/SKILL.md index 8cd0cf5..902d453 100644 --- a/skills/jfrog/SKILL.md +++ b/skills/jfrog/SKILL.md @@ -19,7 +19,7 @@ compatibility: >- (CLI) and Tier 3 (jf api) operations; without it, only MCP (Tier 1) is available. metadata: role: base - version: "0.28.0" + version: "0.30.0" --- # JFrog Skill diff --git a/skills/jfrog/scripts/check-environment.sh b/skills/jfrog/scripts/check-environment.sh index 04c23b3..0941798 100755 --- a/skills/jfrog/scripts/check-environment.sh +++ b/skills/jfrog/scripts/check-environment.sh @@ -217,7 +217,7 @@ canonical_agent_name() { gemini-cli|gemini) echo "gemini" ;; goose) echo "goose" ;; cursor-cli|cursor) echo "cursor" ;; - github-copilot|copilot-cli|copilot) echo "copilot" ;; + github-copilot|copilot-cli|copilot|github_copilot_vscode_agent) echo "copilot" ;; kilocode) echo "kilocode" ;; roo-code|roo_code) echo "roo_code" ;; codex) echo "codex" ;; @@ -255,7 +255,7 @@ detect_harness() { echo "goose" elif [[ -n "${CURSOR_AGENT:-}" || "${CURSOR_EXTENSION_HOST_ROLE:-}" == "agent-exec" || -n "${CURSOR_CLI:-}" || -n "${CURSOR_TRACE_ID:-}" ]]; then echo "cursor" - elif [[ -n "${COPILOT_CLI:-}" || -n "${COPILOT_AGENT_SESSION_ID:-}" ]]; then + elif [[ -n "${COPILOT_CLI:-}" || -n "${COPILOT_AGENT_SESSION_ID:-}" || -n "${COPILOT_AGENT:-}" ]]; then echo "copilot" elif [[ -n "${KILOCODE_FEATURE:-}" || -n "${KILO_PID:-}" ]]; then echo "kilocode"