From b413aaadc03094983ac4420e3aaba04b20e3dfec Mon Sep 17 00:00:00 2001 From: "jfrog-agentic-release-bot[bot]" <276080306+jfrog-agentic-release-bot[bot]@users.noreply.github.com> Date: Sun, 30 Aug 2026 15:45:36 +0000 Subject: [PATCH] [patch] chore: sync skills to v0.34.0 --- .cursor-plugin/marketplace.json | 2 +- .github/scripts/sync-skills-vendor.json | 2 +- plugins/jfrog/.cursor-plugin/plugin.json | 2 +- plugins/jfrog/skills/jfrog-init/SKILL.md | 43 +++-- .../jfrog-init/references/how-to-ask-user.md | 15 +- .../references/mcp-plugin-config.md | 41 ++++- .../jfrog-init/references/project-picker.md | 10 + .../references/project-resolution-branches.md | 7 + .../jfrog-init/scripts/jfrog-detect-all.mjs | 7 +- .../scripts/jfrog-reinstall-jfrog-plugin.mjs | 26 ++- .../scripts/jfrog-resolve-mcp-config.mjs | 171 +++++++++++++++++- .../jfrog-substitute-mcp-placeholders.mjs | 15 +- .../skills/jfrog-mcp-management/SKILL.md | 162 ++++++++++------- .../references/agent-guard-activation.md | 44 ----- .../references/harness-codex.md | 22 +-- .../references/harness-common.md | 2 +- .../scripts/jfrog-agent-guard-check.mjs | 59 +++--- plugins/jfrog/skills/jfrog/SKILL.md | 2 +- 18 files changed, 432 insertions(+), 200 deletions(-) delete mode 100644 plugins/jfrog/skills/jfrog-mcp-management/references/agent-guard-activation.md diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 3f26dc6..beaab50 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.1", + "version": "0.6.2", "pluginRoot": "plugins" }, "plugins": [ diff --git a/.github/scripts/sync-skills-vendor.json b/.github/scripts/sync-skills-vendor.json index 19c5843..7495664 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.32.0", + "pin": "v0.34.0", "paths": [ "skills" ] diff --git a/plugins/jfrog/.cursor-plugin/plugin.json b/plugins/jfrog/.cursor-plugin/plugin.json index def03d4..34a1221 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.1", + "version": "0.6.2", "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/skills/jfrog-init/SKILL.md b/plugins/jfrog/skills/jfrog-init/SKILL.md index 80760bd..de2727f 100644 --- a/plugins/jfrog/skills/jfrog-init/SKILL.md +++ b/plugins/jfrog/skills/jfrog-init/SKILL.md @@ -4,7 +4,7 @@ description: Set up and verify the JFrog plugin. Run on first install, to comple disable-model-invocation: true compatibility: >- Requires Node.js 18 or newer, and network access to the JFrog platform. -allowed-tools: Bash(node --version) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-catalog-runtime.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jf-cli.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jf-config.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jfrog-mcp.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-project.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-server-ping.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-re*.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-state-file.mjs" get*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-state-file.mjs" path*) Bash(node -e "import('${CLAUDE_SKILL_DIR}/scripts/jfrog-resolve-mcp-config.mjs').then(function(m){console.log(m.detectHarness())})") Bash(npx --version) Bash(uname:*) AskUserQuestion +allowed-tools: Bash(node --version) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-catalog-runtime.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jf-cli.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jf-config.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jfrog-mcp.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-project.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-server-ping.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-reinstall-jfrog-plugin.mjs"*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-state-file.mjs" get*) Bash(node "${CLAUDE_SKILL_DIR}/scripts/jfrog-state-file.mjs" path*) Bash(node -e "import('${CLAUDE_SKILL_DIR}/scripts/jfrog-resolve-mcp-config.mjs').then(function(m){console.log(m.detectHarness())})") Bash(npx --version) Bash(uname:*) AskUserQuestion metadata: role: workflow --- @@ -72,8 +72,9 @@ absolute path of this file's directory yourself, same as before. Node (Step 1) or `jf` (Step 2); `AskUserQuestion` picker for web-login vs. token (Step 3/4); `AskUserQuestion` picker for project selection (Step 6). Everything else is read-only except Step 5's - placeholder substitution, Step 8's `~/.netrc` write, and the Final - summary's state write. + placeholder substitution (plus, for kiro-cli, creating or merging the + `jfrog` entry into `~/.kiro/settings/mcp.json`), Step 8's `~/.netrc` + write, and the Final summary's state write. - **Never surface the checklist.** Run silently — no step narration, no raw JSON/exit codes, no branch-reasoning said out loud. See [Customer-facing output](#customer-facing-output). @@ -139,6 +140,11 @@ user needs to know or act on: follow silently, not to summarize out loud — the field names in it are never user-facing. The only output the user sees at an ask point is the prompt itself. +- **Do not** narrate whether the `AskUserQuestion` tool is available in + the current harness before falling back to the plain-text prompt + (e.g. "the AskUserQuestion tool isn't available here, I'll present + this as a plain question instead"). If it isn't available, silently + use the plain-text fallback already documented for that ask point. - **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 @@ -331,7 +337,14 @@ blocks) — required behavior, not optional background. ## Step 5: JFrog MCP plugin file has a jfrog entry? +If you are Kiro or Kiro CLI, prefix the command with +`JFROG_INIT_HARNESS=kiro` or `JFROG_INIT_HARNESS=kiro-cli` on the +same line — no other harness needs this. Silent, like every other +setup command in this walk. + ```bash +JFROG_INIT_HARNESS=kiro node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jfrog-mcp.mjs" "[server-id]"; rc=$?; true +JFROG_INIT_HARNESS=kiro-cli node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jfrog-mcp.mjs" "[server-id]"; rc=$?; true node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jfrog-mcp.mjs" "[server-id]"; rc=$?; true ``` @@ -342,7 +355,9 @@ it for the placeholder fix instead of re-resolving from scratch. **Read-only against the JFrog plugin's own `mcp.json` — with one exception: automatic placeholder substitution** of an unresolved `${JFROG_PLATFORM_URL}` / `${JFROG_URL}` with the real JPD URL from -`jf config`. +`jf config` (plus, for kiro-cli, adding the `jfrog` entry to +`~/.kiro/settings/mcp.json` when no plugin has put one there — see +`references/mcp-plugin-config.md`). **Stop and read `references/mcp-plugin-config.md` in full** — exactly how the substitution works, the per-harness plugin-config paths, and @@ -361,24 +376,8 @@ node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-project.mjs" "[server-id]" "[proj the exact "reuse ``?" `AskUserQuestion` and the jpdUrl-drift check this step requires, not optional background. -**Where the project list comes from.** `jfrog-detect-project.mjs` fetches -`GET /access/api/v1/projects` (the -[GetProjectsList](https://docs.jfrog.com/projects/reference/getprojectslist) -endpoint, authenticated with credentials from `jf config export`) once -per walk and caches it in memory for a short TTL (`lib/project-cache.mjs`) -— the interactive picker re-invokes this script once per user attempt, -and re-enumerating on every typed guess would be wasted network traffic. -This is the list every "enumerated project list" / `candidatesWithNames` -reference below draws from. - -**Name-or-key input.** The user answers with **either** the project's -canonical key OR its display name — whichever is easier for them. -`jfrog-detect-project.mjs` resolves it against the enumerated project -list (exact key, exact name, then progressively fuzzier tiers — see -`references/project-matching.md` for the exact algorithm), confirms -existence, and emits the canonical key on green in the JSON -`resolvedKey` field. An ambiguous input exits red with `candidates` -listing the tied keys. +Name-or-key input is resolved via exact match, then progressively +fuzzier tiers — see `references/project-matching.md` for the algorithm. **Picking a project, interactively.** Whenever the detector needs the user to choose — no input was passed, the typed input didn't match 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 index db3cbf2..bc8e526 100644 --- a/plugins/jfrog/skills/jfrog-init/references/how-to-ask-user.md +++ b/plugins/jfrog/skills/jfrog-init/references/how-to-ask-user.md @@ -4,10 +4,11 @@ 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` | +| Harness | Preferred tool | +|-----------------|------------------------| +| Claude Code | `AskUserQuestion` | +| Codex | `request_user_input` | +| Kiro / Kiro CLI | none — no native prompt tool exists; go straight to the plain-text fallback | Each reference file specifies the question text and option labels; use your harness's native tool to present them. Native prompt tools already @@ -17,3 +18,9 @@ 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. + +**Kiro / Kiro CLI**: check `$JFROG_INIT_HARNESS` (already exported by +Step 5) before reaching for `AskUserQuestion` — if it's `kiro` or +`kiro-cli`, skip the tool call entirely and use the plain-text fallback +directly. Calling it anyway surfaces a "tool does not exist" error to +the user before you fall back. 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 39ac6dc..59a52b2 100644 --- a/plugins/jfrog/skills/jfrog-init/references/mcp-plugin-config.md +++ b/plugins/jfrog/skills/jfrog-init/references/mcp-plugin-config.md @@ -44,8 +44,10 @@ placeholder pattern anywhere in the file, it calls file is not preserved byte-for-byte. 5. Is idempotent — subsequent runs find no placeholder and no-op. -This is the ONLY place `/jfrog-init` writes to the plugin-owned -`mcp.json`. Everything else in Step 5 is read-only. +This is the only place `/jfrog-init` writes to a harness's plugin-owned +`mcp.json` — with one further exception for Kiro CLI: ensuring a `jfrog` +entry exists in `~/.kiro/settings/mcp.json`, which no plugin ships (see +below). Everything else in Step 5 is read-only. **Per-harness plugin-owned config file:** @@ -55,11 +57,26 @@ This is the ONLY place `/jfrog-init` writes to the plugin-owned | 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`) | +| Kiro (IDE) | `~/.kiro/powers/installed/jfrog-kiro-power/mcp.json` (stable path) | +| Kiro CLI | `~/.kiro/settings/mcp.json` — Kiro's own global MCP config, not shipped by any plugin, so the `jfrog` entry is **created or merged in** with a placeholder url, then substituted like every other row above | + +The Kiro CLI merge is additive and never destructive: the file normally +holds the user's other MCP servers, so a `jfrog` entry that already has a +url is left untouched (a placeholder in it is the substitution step's +job), other servers and the file's mode are preserved, a symlinked config +stays a symlink, and a file that isn't valid JSON is reported rather than +rewritten. 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`. +`JFROG_INIT_HARNESS=claude|cursor|vscode|codex|kiro|kiro-cli` or a +specific file via `JFROG_INIT_MCP_CONFIG=/abs/path`. Neither Kiro target +has an auto-detect signal yet — both are reachable only via the +`JFROG_INIT_HARNESS=kiro` / `kiro-cli` overrides. + +`SKILL.md`'s Step 5 already has you export `JFROG_INIT_HARNESS=kiro` / +`kiro-cli` up front when you're running as one of those two — before +the detector ever runs, so Exit 3 below isn't the trigger for it. **What the detector verifies** (three things): @@ -83,7 +100,7 @@ reachable. 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 + Tell the red causes apart from the detector's `detail` for the Final Summary note: - Plugin file missing / empty / lacks a valid `jfrog` entry. Fix: **reinstall or update the JFrog plugin.** If the user asks why or @@ -98,8 +115,22 @@ reachable. - Plugin file has a placeholder and automatic substitution failed with no url set for the resolved server-id. Fix: **resolve `jf config`**. Reinstalling the plugin does not fix this. + - Kiro CLI only: it could not create or update its own + `~/.kiro/settings/mcp.json` — no plugin ships this file, so there's + nothing to reinstall. The detail names the actual cause (e.g. the + parent path blocked by a non-directory, or a permissions error). + Fix: **correct the file or parent-directory permissions/path**, + then re-run. - (Exit 3 only) Harness could not be detected, or plugin file is invalid JSON / unreadable. Show the raw detector error in the note. + **Do not react to this by guessing a harness or trying + `JFROG_INIT_HARNESS` values to see what resolves it.** If this is + Kiro or Kiro CLI, the override was already exported before the + detector's first run (top of Step 5), so it should not reach Exit + 3 for that cause at all. Otherwise this is Exit 3, non-blocking + like every other cause above: note it and move on to Step 6 in the + same turn, with zero visible pause — do not stop to read this file + or any other reference doc over it. - **Exit 2 (`ask`)** → the one outcome that still blocks: placeholder present, but the jf server-id is ambiguous — every step from here on needs a resolved server-id, so there's nothing to skip ahead to. diff --git a/plugins/jfrog/skills/jfrog-init/references/project-picker.md b/plugins/jfrog/skills/jfrog-init/references/project-picker.md index 9988505..c1e827c 100644 --- a/plugins/jfrog/skills/jfrog-init/references/project-picker.md +++ b/plugins/jfrog/skills/jfrog-init/references/project-picker.md @@ -102,6 +102,16 @@ over the generic first-two when present: accepted input format (no *"(name or key)"*, no *"you can type a key or name"*, no *"either the display name or key works"*). +- **If `AskUserQuestion` is not available as a tool at all** (some + harnesses don't expose it — check `$JFROG_INIT_HARNESS`: `kiro` / + `kiro-cli` never have it, so skip the tool call and go straight to + the fallback instead of calling it and surfacing its error), use the + plain-text fallback line above — + *"Which project do you want to use?"*, nothing else — regardless of + how many entries `candidatesWithNames` has. Never explain that the + tool is unavailable, never mention the candidate count or that a + picker would otherwise have been used; the user only ever sees the + one line. - **Never surface the full candidate list or a count** to the user in any case — the picker's two options (plus "Other") or the plain-text fallback are the entire user-facing surface. diff --git a/plugins/jfrog/skills/jfrog-init/references/project-resolution-branches.md b/plugins/jfrog/skills/jfrog-init/references/project-resolution-branches.md index 4e4d1d3..501c7c9 100644 --- a/plugins/jfrog/skills/jfrog-init/references/project-resolution-branches.md +++ b/plugins/jfrog/skills/jfrog-init/references/project-resolution-branches.md @@ -4,6 +4,13 @@ full whenever `jfrog-detect-project.mjs` returns anything other than a clean exit 0. +**Everything below — exit codes, `unresolved`, and which bullet you +land on — is reasoning for you to follow silently, never to narrate.** +Never repeat this table's own words back to the user (e.g. "Exit 2, +no `unresolved`" or "this is an ask with no input"). The only output +the user sees is the resulting prompt itself, the raw detector error +where one is shown, or the Final Summary. + - **Exit 2 (`ask`) with `"unresolved": "server"`** → not a project ask — the server-id is ambiguous. Follow "Resolving `` for Steps 4-7" in `SKILL.md` (prompt for a server from `candidates`), then diff --git a/plugins/jfrog/skills/jfrog-init/scripts/jfrog-detect-all.mjs b/plugins/jfrog/skills/jfrog-init/scripts/jfrog-detect-all.mjs index 7efdbcd..4c8c1c1 100755 --- a/plugins/jfrog/skills/jfrog-init/scripts/jfrog-detect-all.mjs +++ b/plugins/jfrog/skills/jfrog-init/scripts/jfrog-detect-all.mjs @@ -1,7 +1,10 @@ #!/usr/bin/env node // Runs the detectors in dependency order and reports one JSON summary line. -// Idempotent, read-only, zero mutation (aside from the state-file hint on -// green) — safe to run repeatedly. +// Idempotent and safe to run repeatedly. Mostly read-only — with two +// exceptions: (1) for the kiro-cli harness, Step 5 (detectJfrogMcp → +// resolveMcpConfig) creates or merges ~/.kiro/settings/mcp.json when the +// jfrog entry is absent, the same write that jfrog-detect-jfrog-mcp.mjs +// performs; (2) on green, a state-file hint is written to ~/.jfrog/setup.json. // // Usage: node jfrog-detect-all.mjs [server-id] [project-input] // 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 f115185..30a52ca 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 @@ -13,10 +13,17 @@ // Always exits 0 after printing. import { existsSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; import { detectHarness, resolveMcpConfig } from "./jfrog-resolve-mcp-config.mjs"; -const resolved = resolveMcpConfig(); const harness = detectHarness(); +// kiro-cli's resolveMcpConfig() writes to disk as a side effect (ensureKiroCliJfrogEntry), +// which this diagnostic-only script must never do — derive the path directly instead. +const resolved = + harness === "kiro-cli" + ? { path: join(homedir(), ".kiro", "settings", "mcp.json") } + : resolveMcpConfig(); console.log(`The JFrog MCP entry lives inside the JFrog plugin's own mcp.json file. This script never writes to it — it only diagnoses and prints the fix. @@ -56,12 +63,27 @@ Restart VS Code, then re-run /jfrog-init.`); Restart Codex, then re-run /jfrog-init.`); break; + case "kiro": + console.log(`Kiro IDE: + Open the Powers panel → Add Custom Power → Import from GitHub → + enter the JFrog Kiro Power repository URL. + Restart Kiro, then re-run /jfrog-init.`); + break; + case "kiro-cli": + console.log(`Kiro CLI: + No plugin reinstall needed — the jfrog entry in ~/.kiro/settings/mcp.json + is created automatically by /jfrog-init. Re-run /jfrog-init to recreate it. + If /jfrog-init reports the file is invalid, open ~/.kiro/settings/mcp.json, + fix the JSON (keep the other MCP server entries), 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 + Kiro: Powers panel → Add Custom Power → Import from GitHub. + Kiro CLI: no reinstall needed — entry is created automatically by /jfrog-init. Restart the IDE afterwards, then re-run /jfrog-init.`); } @@ -74,6 +96,8 @@ Expected plugin-owned paths (for reference): Claude: ~/.claude/plugins/cache//jfrog//.mcp.json Codex: $CODEX_HOME/plugins/cache/codex-plugin/jfrog//.mcp.json ($CODEX_HOME defaults to ~/.codex) + Kiro: ~/.kiro/powers/installed/jfrog-kiro-power/mcp.json + Kiro CLI: ~/.kiro/settings/mcp.json (not plugin-owned; created by /jfrog-init) `); 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 14d7e0e..b675d76 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 @@ -2,7 +2,8 @@ // Resolves the PLUGIN-OWNED mcp.json for the CURRENT harness and returns its // absolute path. This is the file the JFrog plugin ships with — NOT the // user's project- or user-scope MCP config. This skill never touches the -// customer's own mcp.json; only the one owned by the JFrog plugin. +// customer's own mcp.json; only the one owned by the JFrog plugin — with +// one exception, kiro-cli, which has no plugin behind it (see below). // // Plugin-owned paths per harness: // Cursor: ~/.cursor/plugins/cache/cursor-public/jfrog//mcp.json @@ -16,6 +17,11 @@ // Codex: $CODEX_HOME/plugins/cache/codex-plugin/jfrog//.mcp.json // (multiple dirs may exist; most-recently-modified // wins. $CODEX_HOME defaults to ~/.codex.) +// Kiro (IDE): ~/.kiro/powers/installed/jfrog-kiro-power/mcp.json (stable path) +// Kiro CLI: ~/.kiro/settings/mcp.json (not plugin-shipped; this is +// Kiro's own global MCP config, so the jfrog entry is +// created — or merged into an existing file — with a +// placeholder url. See ensureKiroCliJfrogEntry() below.) // // 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 @@ -37,8 +43,12 @@ // detectHarness() is the single JS implementation — exported and reused // by every other script in this skill that needs harness information. // +// Kiro (IDE and CLI) has no detect signal yet — reachable only via the +// JFROG_INIT_HARNESS=kiro / kiro-cli overrides below. +// // Overrides: -// - JFROG_INIT_HARNESS=claude|cursor|vscode|codex forces one specific harness. +// - JFROG_INIT_HARNESS=claude|cursor|vscode|codex|kiro|kiro-cli 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 @@ -51,17 +61,17 @@ // 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 { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; -import { join } from "node:path"; -import { isMainModule } from "./lib/jf.mjs"; +import { dirname, join } from "node:path"; +import { isMainModule, jfrogMcpEntry } from "./lib/jf.mjs"; -const VALID_HARNESSES = new Set(["claude", "cursor", "vscode", "codex"]); +const VALID_HARNESSES = new Set(["claude", "cursor", "vscode", "codex", "kiro", "kiro-cli"]); // 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. +// tie-break can't resolve it. Adding a harness (OpenCode, ...) is just a +// new entry here — Kiro has none yet, see the header note above. 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 }, @@ -229,6 +239,143 @@ function resolveCodexPath() { return { path: match }; } +function resolveKiroPath() { + const p = join(homedir(), ".kiro", "powers", "installed", "jfrog-kiro-power", "mcp.json"); + if (!existsSync(p)) { + return { + error: `JFrog Kiro Power's mcp.json not found at ${p}\n install the JFrog Power in Kiro (Powers panel -> Add Custom Power -> Import from GitHub) to make it available.`, + code: 2, + }; + } + return { path: p }; +} + +// The url every other harness's plugin ships; the generic substitution +// step rewrites it with the real JPD from `jf config`. +const KIRO_CLI_PLACEHOLDER_URL = "https://${JFROG_PLATFORM_URL}/mcp"; + +// Overwrites an existing file via temp+rename (preserves symlinks and mode). +function replaceKiroCliConfig(target, content) { + const real = realpathSync(target); + const tmp = `${real}.tmp.${process.pid}`; + try { + // "wx" refuses to follow/overwrite anything already at tmp. + writeFileSync(tmp, content, { flag: "wx", mode: 0o600 }); + chmodSync(tmp, statSync(real).mode & 0o777); + renameSync(tmp, real); + } catch (err) { + // A run killed between write and rename leaves tmp behind, and the + // name is only unique per PID — clean up so the next run isn't stuck + // on EEXIST forever. + try { + unlinkSync(tmp); + } catch { + // Never created, already renamed, or not ours to remove. + } + throw err; + } +} + +// kiro-cli is the one target with no plugin behind it: ~/.kiro/settings/mcp.json +// is Kiro's own global MCP config, so a missing jfrog entry is something to +// add rather than an install error. Additive only — the file normally holds +// the user's other MCP servers, and an existing jfrog entry is left exactly +// as it is (a placeholder in its url is the substitution step's job, not +// this one's). Returns an error result, or null when the file is ready. +// +// `retryAfterRace` guards the one path that can legitimately need a second +// look: see the EEXIST branch below. +function ensureKiroCliJfrogEntry(target, retryAfterRace = true) { + let raw = null; + try { + raw = readFileSync(target, "utf8"); + } catch (err) { + if (err.code !== "ENOENT") { + return { error: `could not read ${target}: ${err.message}`, code: 2 }; + } + } + + // Nothing usable on disk — write the whole document rather than merge. + // An empty file counts: Kiro treats it as no config, and JSON.parse of + // "" would only send us down the invalid-JSON path below. + if (raw === null || raw.trim() === "") { + const content = JSON.stringify({ mcpServers: { jfrog: { url: KIRO_CLI_PLACEHOLDER_URL } } }, null, 2) + "\n"; + try { + if (raw === null) { + mkdirSync(dirname(target), { recursive: true }); + writeFileSync(target, content, { flag: "wx", mode: 0o600 }); + } else { + replaceKiroCliConfig(target, content); + } + } catch (err) { + if (err.code !== "EEXIST") { + return { error: `could not write ${target}: ${err.message}`, code: 2 }; + } + // "wx" raises EEXIST for a race-created file or a dangling symlink; + // retry to tell them apart (a real file now parses; an unreadable path + // comes back here with the retry spent and is reported as an error). + if (retryAfterRace) return ensureKiroCliJfrogEntry(target, false); + return { error: `could not write ${target}: something already at that path cannot be read as a file`, code: 2 }; + } + return null; + } + + let parsed; + try { + parsed = JSON.parse(raw); + } catch { + return { error: `${target} is not valid JSON — refusing to modify it; fix the file, then re-run.`, code: 2 }; + } + if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + return { error: `${target} is not a JSON object — refusing to modify it; fix the file, then re-run.`, code: 2 }; + } + + // Kiro CLI reads only mcpServers.jfrog — a top-level jfrog key (Codex + // shape) is invisible to it. Use a direct lookup instead of jfrogMcpEntry() + // so a bare top-level entry doesn't falsely satisfy the check and skip the + // merge that would write the mcpServers shape Kiro CLI actually needs. + const existing = parsed.mcpServers && typeof parsed.mcpServers === "object" && !Array.isArray(parsed.mcpServers) + ? parsed.mcpServers.jfrog + : undefined; + if (existing && typeof existing === "object" && !Array.isArray(existing) && typeof existing.url === "string" && existing.url.trim() !== "") { + return null; + } + + if (!("mcpServers" in parsed) || parsed.mcpServers === undefined || parsed.mcpServers === null) { + parsed.mcpServers = {}; + } else if (typeof parsed.mcpServers !== "object" || Array.isArray(parsed.mcpServers)) { + return { error: `${target} has a non-object "mcpServers" — refusing to modify it; fix the file, then re-run.`, code: 2 }; + } + + // Spread rather than assign: a half-written entry may already carry + // fields of the user's own (e.g. "disabled") that aren't ours to drop. + const entry = parsed.mcpServers.jfrog; + parsed.mcpServers.jfrog = { + ...(entry !== null && typeof entry === "object" && !Array.isArray(entry) ? entry : {}), + url: KIRO_CLI_PLACEHOLDER_URL, + }; + + try { + replaceKiroCliConfig(target, JSON.stringify(parsed, null, 2) + "\n"); + } catch (err) { + return { error: `could not write ${target}: ${err.message}`, code: 2 }; + } + return null; +} + +function resolveKiroCliPath() { + const p = join(homedir(), ".kiro", "settings", "mcp.json"); + const err = ensureKiroCliJfrogEntry(p); + if (err) return err; + // Resolve after ensure so the substitution step downstream always + // receives the real path — never a symlink that renameSync would replace. + try { + return { path: realpathSync(p) }; + } catch (e) { + return { error: `could not resolve real path of ${p}: ${e.message}`, code: 2 }; + } +} + export function resolveMcpConfig() { if (process.env.JFROG_INIT_MCP_CONFIG) { return { path: process.env.JFROG_INIT_MCP_CONFIG }; @@ -242,7 +389,7 @@ 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, codex.`, + error: `JFROG_INIT_HARNESS=${process.env.JFROG_INIT_HARNESS} is not one of: claude, cursor, vscode, codex, kiro, kiro-cli.`, code: 1, }; } @@ -256,11 +403,15 @@ export function resolveMcpConfig() { return resolveVscodePath(); case "codex": return resolveCodexPath(); + case "kiro": + return resolveKiroPath(); + case "kiro-cli": + return resolveKiroCliPath(); default: return { error: "could not detect current harness (Claude Code / Cursor / VS Code / Codex).\n" + - " Set JFROG_INIT_HARNESS=claude|cursor|vscode|codex, or\n" + + " Set JFROG_INIT_HARNESS=claude|cursor|vscode|codex|kiro|kiro-cli, or\n" + " JFROG_INIT_MCP_CONFIG=/absolute/path/to/mcp.json to override.", code: 1, }; 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 c75b6f0..a058e3a 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 @@ -37,7 +37,7 @@ // server-id passed — ambiguous, caller must ask the user // Exit 3 -> read/write error, or jf missing -import { existsSync, readFileSync, writeFileSync, renameSync, statSync, chmodSync, unlinkSync } from "node:fs"; +import { existsSync, readFileSync, realpathSync, writeFileSync, renameSync, statSync, chmodSync, unlinkSync } from "node:fs"; 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"; @@ -110,20 +110,23 @@ export function substituteMcpPlaceholders(target, serverIdOverride) { jfrogMcpEntry(parsed).url = newUrl; const rewritten = JSON.stringify(parsed, null, 2) + "\n"; - const tmp = `${target}.tmp.${process.pid}`; + const real = realpathSync(target); + const tmp = `${real}.tmp.${process.pid}`; try { // "wx" refuses to follow/overwrite anything already at tmp (e.g. a // pre-planted symlink) — same symlink-safe pattern as - // lib/project-cache.mjs's writeCachedProjectList(). - writeFileSync(tmp, rewritten, { flag: "wx" }); + // lib/project-cache.mjs's writeCachedProjectList(). mode: 0o600 closes + // the window between writeFileSync and chmodSync where sibling MCP tokens + // in the same file would be world-readable. + writeFileSync(tmp, rewritten, { flag: "wx", mode: 0o600 }); // rename() replaces the target's inode wholesale, so without this the // file would silently pick up writeFileSync's default umask-derived // mode instead of the target's own — e.g. a 0600 mcp.json holding // another MCP server's secrets in its env block would come back 0644 // (world-readable) after a substitution that has nothing to do with // that other entry. - chmodSync(tmp, statSync(target).mode & 0o777); - renameSync(tmp, target); + chmodSync(tmp, statSync(real).mode & 0o777); + renameSync(tmp, real); } catch (err) { // A run killed between the write and the rename (Ctrl-C, OOM, harness // timeout) leaves tmp behind; the name is only unique per PID, so the diff --git a/plugins/jfrog/skills/jfrog-mcp-management/SKILL.md b/plugins/jfrog/skills/jfrog-mcp-management/SKILL.md index c49aded..7b2d630 100644 --- a/plugins/jfrog/skills/jfrog-mcp-management/SKILL.md +++ b/plugins/jfrog/skills/jfrog-mcp-management/SKILL.md @@ -14,69 +14,44 @@ compatibility: >- 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 +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 --name*) 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. 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. When Agent Guard is on (see Step 0), +do that only through the Agent Guard — ignore other install commands from an +MCP's docs. 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. +— do not treat a bare `404` as “MCP missing.” If Agent Guard is enabled, 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 the path by locating this SKILL.md file and using its parent directory. -**Harness config (resolve once, up front).** Where MCP config lives — the file -path, the top-level key, the config format (JSON or TOML), the env/secret -reference syntax, and how a server is enabled/verified/listed — varies per agent -(Claude Code, Codex, Cursor, OpenCode, VS Code, …). Those values are NOT -hard-coded in this skill. Read -[references/harness-common.md](references/harness-common.md) first: it holds the -shared entry shape and a routing table that tells you which SINGLE harness file -to open (`harness-claude.md`, `harness-codex.md`, `harness-cursor.md`, -`harness-opencode.md`, `harness-vscode.md`, …). -**Read common + exactly one harness file — do NOT open the others.** Then use -that harness file's fields wherever a step says "per harness-config". Adding a -new agent is a new `harness-.md` file, with no change to this workflow. -Never hard-code one harness's `.mcp.json` / `mcpServers` / `claude mcp list` -values into your actions. +## Runtime requirement -## Prerequisites - -- **Read the base [`../jfrog/SKILL.md`](../jfrog/SKILL.md) first** — it owns the - shared guards this skill reuses (does not repeat): the - [environment check](../jfrog/SKILL.md#environment-check) (run it before the - first `jf` call and `export JFROG_CLI_USER_AGENT` so these calls show in usage - 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. +Node.js on PATH — `node` runs the Step 0 check, `npx` runs `@jfrog/agent-guard`. -## Step 0: Agent Guard activation (silent, mandatory, fresh every time) +## Step 0: Agent Guard activation (silent, mandatory) -Before anything else, run the following and WAIT for its exit code — do not -narrate that you are running it: +Before anything else, run this check and WAIT for its exit code. Do not narrate +it. Do not reuse a result from an earlier turn in this session. ```bash -# When no server ID is known (common single-server case): node "/scripts/jfrog-agent-guard-check.mjs" -# When a specific jf config server ID is known from context: -node "/scripts/jfrog-agent-guard-check.mjs" ``` -**Canonical gate contract (this script only):** optional **positional** -`` = a `jf` config server id (e.g. from `jf config show`). Nothing -else. +Request network access for this command. + +**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. @@ -91,7 +66,7 @@ else. 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 + the gate (`Unknown: …`); 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. @@ -103,20 +78,74 @@ 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). -Run it FRESH on every activation — never cache or reuse a previous result. - -- **Install and List → Available to install** proceed only on Exit 0 (or a - listed disabled-state exception) — they call the catalog over the network. -- **List → Currently installed** reads only local config files (no catalog, no - network), so like Remove it proceeds on ANY exit code. Never let a non-zero - Step 0 stop a "what MCPs do I have installed?" request. -- **Remove** edits local config only and never calls the catalog or the network, - so it proceeds on ANY exit code — Exit 0, Exit 2 (registry disabled), and Exit - 1 (no credentials / offline / network error). The local cleanup still works - regardless. In fact Remove need not block on Step 0 at all; run it if - convenient, but never let a non-zero exit stop a removal. +```bash +# Only when certain of the JFrog platform server id: +node "/scripts/jfrog-agent-guard-check.mjs" +``` + +If the first run fails with a network error, timeout, or other spurious issue +(sandbox, DNS, HTTP 5xx), run the same command again with network access and use +that second exit code. Do not retry when the reason is missing credentials or +missing jf config. + +Interpret the exit code: + +### Exit 0 — Agent Guard enabled + +This organization uses Agent Guard. Install, list available, list currently +installed, and remove MCPs with this skill. No other approved path for those. + +### Exit 2 — Agent Guard disabled + +This organization does not use Agent Guard. Honor that. Do not mention Agent +Guard or this check unless asked. Do not tell the user to contact an admin or +enable a registry. + +- **Install** and **List → Available to install:** ignore those sections. Serve + those asks as you would if this skill had never loaded. +- **List → Currently installed** and **Remove:** proceed (local config only). + +Keep reading for Install / list-available only when the user explicitly asked +to use Agent Guard anyway (evaluating the catalog without enabling it for the +organization is legitimate). + +### Any other non-zero exit — status unknown + +The check did not reach a definitive platform answer (no credentials, timeout, +HTTP error, network/DNS). Treat it like Exit 2 for Install and List → Available +to install (ignore those sections; keep serving the user). List → Currently +installed and Remove still proceed. + +Mention once — as a side note while continuing — what failed and that the user +can use the `jfrog-init` command to fix any local configuration issues. Do not +ask them to decide, do not repeat it, and do not stop to fix it. + +## Harness config + +**Harness config (resolve once, up front).** Where MCP config lives — the file +path, the top-level key, the config format (JSON or TOML), the env/secret +reference syntax, and how a server is enabled/verified/listed — varies per agent +(Claude Code, Codex, Cursor, OpenCode, VS Code, …). Those values are NOT +hard-coded in this skill. Read +[references/harness-common.md](references/harness-common.md) first: it holds the +shared entry shape and a routing table that tells you which SINGLE harness file +to open (`harness-claude.md`, `harness-codex.md`, `harness-cursor.md`, +`harness-opencode.md`, `harness-vscode.md`, …). +**Read common + exactly one harness file — do NOT open the others.** Then use +that harness file's fields wherever a step says "per harness-config". Adding a +new agent is a new `harness-.md` file, with no change to this workflow. +Never hard-code one harness's `.mcp.json` / `mcpServers` / `claude mcp list` +values into your actions. + +## Prerequisites + +**Read the base [`../jfrog/SKILL.md`](../jfrog/SKILL.md) first** — it owns the +shared guards this skill reuses (does not repeat): the +[environment check](../jfrog/SKILL.md#environment-check) (run it before the first +`jf` call and `export JFROG_CLI_USER_AGENT` so these calls show in usage +telemetry) and the +[server selection rules](../jfrog/SKILL.md#server-selection-rules-mandatory) +(which `references/agent-guard-common.md` builds on, not replaces). ## Pre-flight (Install and List → Available to install only) @@ -138,6 +167,9 @@ read only local config, so they skip this. # Install an MCP +If Step 0 was exit 2 (or unknown treated as exit 2), ignore this section. +Continue serving the user without Agent Guard. + **Did the user name a specific MCP package?** ("add `foo-mcp`", "install `@scope/bar`"). If NOT — they said "yes", "add an MCP", "what can I install" — your FIRST action is to show the catalog (run [List → Available to @@ -333,10 +365,10 @@ file or shell: | "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 | -NEVER invent MCP integrations from outside the catalog. The only authoritative -source for what's available is `--list-available` against the configured server -+ JFrog project key. If that returns nothing or errors, say so — do not pad the -answer with names from elsewhere. +When Step 0 was exit 0: NEVER invent MCP integrations from outside the catalog. +The only authoritative source for what's available is `--list-available` against +the configured server + JFrog project key. If that returns nothing or errors, +say so — do not pad the answer with names from elsewhere. ## Currently installed @@ -364,6 +396,9 @@ add-on where the agent provides it. ## Available to install +If Step 0 was exit 2 (or unknown treated as exit 2), ignore this subsection. +Continue serving the user without Agent Guard. Currently installed still proceeds. + 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 --registry @jfrog/agent-guard` @@ -406,8 +441,11 @@ for key rules and troubleshooting. # Remove an MCP -Removal edits local config only and never calls the catalog, so it proceeds even -on Step 0 Exit 2 (registry disabled). +Removal edits local config only and never calls the catalog, so it proceeds on +ANY Step 0 exit code. +An MCP entry that runs `@jfrog/agent-guard` must always be removed with these +instructions, to make sure the local config is cleaned up and the OAuth cache is +cleared. 1. **Locate the entry across both scopes first.** Read the servers map from BOTH the project and user config files for the current harness (per 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 deleted file mode 100644 index 404a6ed..0000000 --- a/plugins/jfrog/skills/jfrog-mcp-management/references/agent-guard-activation.md +++ /dev/null @@ -1,44 +0,0 @@ -# Agent Guard activation — Step 0 exit codes - -Reference for the Step 0 check (`scripts/jfrog-agent-guard-check.mjs`) run by -the `jfrog-mcp-management` skill. - -Run the check FRESH on every activation — NEVER cache or reuse a previous -result. Do not tell the user you are running it, and never mention the Agent -Guard or this script. Key off the EXIT CODE, not the text it prints. - -| Exit | Meaning | Action | -| --- | --- | --- | -| 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. 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 - -Two flows are pure local-config reads/edits that NEVER call the catalog or the -network, so no exit code can make them fail for lack of platform access — they -proceed on Exit 0, Exit 2, and Exit 1 alike, and do not depend on Step 0: - -- **Remove** — delete the entry from the harness's MCP config, then clean the - OAuth cache key. -- **List → Currently installed** — read the servers map from the harness's - config files (live status is an optional add-on where the harness provides it). - -Only **Install** and **List → Available to install** are gated on Exit 0 (they -hit the catalog over the network); see the exceptions below. - -## Exceptions — Install / List → Available to install proceed even on a non-zero exit - -These exceptions apply ONLY to "other non-zero" exits (no credentials, -timeout, network/DNS error). For **Install / List → Available to install** they -do NOT apply to Exit 2: the platform explicitly reported the MCP registry is -disabled, so no agent guard command can succeed — stop after telling the user to -contact their admin/IT, even if an existing `mcpServers` entry is present. -(Remove and List → Currently installed are not gated at all — see above.) - -Continue with the skill when either holds: - -- The user explicitly asked to use the JFrog Agent Guard anyway; or -- The workspace is already on the Agent Guard — an existing entry in the - harness's MCP config (see [harness-common.md](harness-common.md)) runs - `@jfrog/agent-guard`. diff --git a/plugins/jfrog/skills/jfrog-mcp-management/references/harness-codex.md b/plugins/jfrog/skills/jfrog-mcp-management/references/harness-codex.md index 4538171..f2c0eaa 100644 --- a/plugins/jfrog/skills/jfrog-mcp-management/references/harness-codex.md +++ b/plugins/jfrog/skills/jfrog-mcp-management/references/harness-codex.md @@ -144,22 +144,12 @@ described above. If there is no usable `jf` server, ask the user to add one If credentials cannot be resolved (no `--server` and either zero or multiple `jf` servers), the entry fails to start with `connection closed: initialize response`. -## Step 0 activation check under Codex's sandbox - -Codex runs shell commands in a sandbox with **no outbound network by default**, -and the skill's Step 0 check (`scripts/jfrog-agent-guard-check.mjs`) probes the -JFrog settings endpoint over the network. So the first run can report `Disabled: -settings endpoint unreachable (fetch failed)` even when the `jf` credentials are -valid - that is the sandbox blocking the request, NOT a missing or unreachable -server. On Codex, treat a first-run `unreachable (fetch failed)` as -**inconclusive, not a Disabled result** - do NOT apply the Step 0 "silently -abort" handling from -[agent-guard-activation.md](agent-guard-activation.md) yet. First re-run the SAME -check with network access (approve the escalated command, or run it outside the -sandbox); only treat the platform as unreachable if it STILL fails with network. -A follow-up `Enabled: via JF CLI config (server '')` confirms it was only the -sandbox. Credentials resolve from the on-disk `jf` config regardless - only the -reachability probe needs network. +## Codex's sandbox + +Codex sandboxes shell with no outbound network by default. Step 0 already +requires a second run with network, the same is relevant for all commands that +run into network issues in codex. Approve the escalated command or run it +outside the sandbox, then re-run. ## Enable 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 00ec744..2144dcb 100644 --- a/plugins/jfrog/skills/jfrog-mcp-management/references/harness-common.md +++ b/plugins/jfrog/skills/jfrog-mcp-management/references/harness-common.md @@ -40,7 +40,7 @@ editor UI or `mcp.json`, so it falls through to the Fallback section. | OpenCode | `OPENCODE` | [harness-opencode.md](harness-opencode.md) | | Devin | Your system prompt / system instructions identify you as **Devin** (Devin Desktop / Devin Local / Devin CLI / Cognition). | [harness-devin.md](harness-devin.md) | | 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) | +| VS Code editor | `TERM_PROGRAM=vscode` **and `TERM_PROGRAM` is not `kiro`** **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` 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 999c52f..ed84da9 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 @@ -10,10 +10,9 @@ // // Contract: // - exit 0 -> Agent Guard ENABLED (skill may proceed) -// - exit 2 -> reachable but the platform has the MCP registry DISABLED -// (skill should tell the user to contact their admin / IT) -// - exit 1 -> DISABLED for any other reason: no credentials, timeout, -// network/DNS error (skill must silently abort) +// - exit 2 -> Agent Guard DISABLED (registry off, or _JF_AGENT_GUARD_FORCE_DISABLE) +// - exit 1 -> status UNKNOWN: no credentials, timeout, network/DNS error. +// Not sure if Enabled or not. // - a single human-readable reason line is written to stdout for // diagnostics only; Step 0 keys off the EXIT CODE, not the text. // @@ -35,21 +34,27 @@ const debug = (message) => { const env = (newName, oldName) => process.env[newName] ?? (oldName ? process.env[oldName] : undefined); +const GATE_DONE = Symbol("gateDone"); + const enabled = (reason) => { process.stdout.write(`Enabled: ${reason}\n`); - process.exit(0); + process.exitCode = 0; + throw GATE_DONE; }; -const disabled = (reason) => { - process.stdout.write(`Disabled: ${reason}\n`); - process.exit(1); +const unknown = (reason) => { + process.stdout.write(`Unknown: ${reason}\n`); + process.exitCode = 1; + throw GATE_DONE; }; // Reachable platform that reports the MCP registry turned off. Distinct exit -// code so the skill can tell the user to contact their admin / IT. +// code so the skill can tell this definitive answer apart from an undetermined +// status. const registryDisabled = (reason) => { process.stdout.write(`RegistryDisabled: ${reason}\n`); - process.exit(2); + process.exitCode = 2; + throw GATE_DONE; }; // Exactly one positional argv[2]. Extras (argv[3..]), flags, and URLs are @@ -62,7 +67,7 @@ const registryDisabled = (reason) => { function readGateServerId() { const extra = process.argv.slice(3); if (extra.length > 0) { - disabled( + unknown( `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 ` + @@ -74,13 +79,13 @@ function readGateServerId() { const id = String(raw).trim(); if (!id) return undefined; if (id.startsWith("-")) { - disabled( + unknown( `expected a jf config server id (positional), got flag ` + `${JSON.stringify(id)} — pass \`\` positionally, not as \`--server\``, ); } if (/:\/\//.test(id)) { - disabled( + unknown( `expected a jf config server id (positional), got URL ` + `${JSON.stringify(id)} — do not derive an id from \`JFROG_URL\` / \`JF_URL\``, ); @@ -100,7 +105,7 @@ function resolveCredentials() { if (explicitServerId) { const fromCli = resolveFromCliConfig(explicitServerId); if (fromCli) return fromCli; - disabled( + unknown( `server id ${JSON.stringify(explicitServerId)} is not configured in ` + `\`jf config\` (or \`jf\` is unavailable) — refusing to check a different JPD`, ); @@ -201,8 +206,8 @@ async function isGatewayPluginEnabled(baseUrl, token) { if (!response.ok) { debug(`Settings request returned HTTP ${response.status}.`); // Non-OK (incl. 401/403) means an auth/permission/transport problem, NOT - // a deliberately-disabled registry — stay silent (exit 1) rather than - // sending the user to IT. Only HTTP 200 + value:false is "disabled". + // a deliberately-disabled registry — report unknown (exit 1) rather than + // claiming disabled. Only HTTP 200 + value:false is "disabled". return { ok: false, reason: `settings endpoint returned HTTP ${response.status}`, @@ -264,7 +269,7 @@ async function main() { const forceEnabled = env("JF_AGENT_GUARD_FORCE_ENABLE") === "true"; if (forceDisabled) { - disabled("forced via _JF_AGENT_GUARD_FORCE_DISABLE"); + registryDisabled("forced via _JF_AGENT_GUARD_FORCE_DISABLE"); return; } if (forceEnabled) { @@ -274,7 +279,7 @@ async function main() { const creds = resolveCredentials(); if (!creds) { - disabled( + unknown( "JFROG_URL/JF_URL + access token not set and no default JF CLI config found", ); return; @@ -289,14 +294,22 @@ async function main() { registryDisabled(result.reason); return; } - disabled(result.reason); + unknown(result.reason); } try { await main(); } catch (error) { - // Last-resort guard: any unexpected throw must NOT leak a stack trace to the - // user (the skill's Step 0 is silent). Downgrade to the safe "disabled" exit. - debug(`Unexpected error: ${error?.stack ?? error?.message ?? error}`); - disabled("unexpected error"); + if (error === GATE_DONE) { + // Intentional halt after a diagnostic write. Lets stdout drain. + } else { + // Last-resort guard: any unexpected throw must NOT leak a stack trace to the + // user (the skill's Step 0 is silent). Downgrade to the safe "unknown" exit. + debug(`Unexpected error: ${error?.stack ?? error?.message ?? error}`); + try { + unknown("unexpected error"); + } catch (halt) { + if (halt !== GATE_DONE) throw halt; + } + } } diff --git a/plugins/jfrog/skills/jfrog/SKILL.md b/plugins/jfrog/skills/jfrog/SKILL.md index 377deb1..c47dffa 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.32.0" + version: "0.34.0" --- # JFrog Skill