From f631612042a50c5340993e79cb607d17253791bf Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:00:44 +0300 Subject: [PATCH 01/13] AX-1705 - Add JFrog CLI credential support to inject-instructions Co-Authored-By: Claude Sonnet 4.6 --- plugins/jfrog/scripts/inject-instructions.mjs | 58 ++++++++++++++++--- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/plugins/jfrog/scripts/inject-instructions.mjs b/plugins/jfrog/scripts/inject-instructions.mjs index 8f6438f..dd8ef2e 100755 --- a/plugins/jfrog/scripts/inject-instructions.mjs +++ b/plugins/jfrog/scripts/inject-instructions.mjs @@ -3,6 +3,7 @@ // Licensed under the Apache License, Version 2.0 // https://www.apache.org/licenses/LICENSE-2.0 +import { execFileSync } from "node:child_process"; import { readFileSync } from "node:fs"; import path from "node:path"; import process from "node:process"; @@ -24,17 +25,52 @@ const forceDisabled = const forceEnabled = env("JF_AGENT_GUARD_FORCE_ENABLE") === "true"; -async function isAgentGuardEnabledViaSettings() { +// Resolve {baseUrl, token} from env vars, falling back to the JFrog CLI's +// default server. Returns null when nothing resolves. +function resolveCredentials() { const baseUrl = env("JFROG_URL", "JF_URL"); const token = env("JFROG_ACCESS_TOKEN", "JF_ACCESS_TOKEN"); - if (!baseUrl) { - debug("JFROG_URL/JF_URL is not set; skipping settings check"); - return false; + if (baseUrl && token) { + debug("Resolved credentials from environment variables"); + return { baseUrl, token }; } - if (!token) { - debug("JFROG_ACCESS_TOKEN/JF_ACCESS_TOKEN is not set; skipping settings check"); + + // `jf config export` emits the default server as a base64-encoded JSON token. + let configToken; + try { + configToken = execFileSync("jf", ["config", "export"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + } catch (error) { + debug(`'jf config export' failed (jf not on PATH or no server configured): ${error.message}`); + return null; + } + + let cfg; + try { + cfg = JSON.parse(Buffer.from(configToken, "base64").toString("utf8")); + } catch (error) { + debug(`Could not decode the jf Config Token: ${error.message}`); + return null; + } + + if (!cfg?.url || !cfg?.accessToken) { + debug("jf Config Token did not contain a usable url + accessToken"); + return null; + } + + debug(`Resolved credentials via 'jf config export' (serverId: ${cfg.serverId ?? ""})`); + return { baseUrl: cfg.url, token: cfg.accessToken }; +} + +async function isAgentGuardEnabledViaSettings() { + const credentials = resolveCredentials(); + if (!credentials) { + debug("No JFrog credentials resolved; skipping settings check"); return false; } + const { baseUrl, token } = credentials; const url = baseUrl.replace(/\/+$/, "") + @@ -92,13 +128,17 @@ try { path.join(root, "templates", "jfrog-mcp-management.md"), "utf8", ); -} catch { - process.stdout.write("{}"); +} catch (error) { + debug(`Could not read instructions template: ${error.message}`); process.exit(0); } +// The IDE consumes hookSpecificOutput.additionalContext from a SessionStart hook. process.stdout.write( JSON.stringify({ - additional_context: template, + hookSpecificOutput: { + hookEventName: "SessionStart", + additionalContext: template, + }, }), ); From acd2750dd321241996b127ecd14f54b39bc7fff2 Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:14:53 +0300 Subject: [PATCH 02/13] Remove license URL comment Co-Authored-By: Claude Sonnet 4.6 --- plugins/jfrog/scripts/inject-instructions.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/jfrog/scripts/inject-instructions.mjs b/plugins/jfrog/scripts/inject-instructions.mjs index dd8ef2e..96f5e68 100755 --- a/plugins/jfrog/scripts/inject-instructions.mjs +++ b/plugins/jfrog/scripts/inject-instructions.mjs @@ -1,7 +1,6 @@ #!/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 import { execFileSync } from "node:child_process"; import { readFileSync } from "node:fs"; From cbdf651501da538967013d9972f74150d1742b43 Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:27:36 +0300 Subject: [PATCH 03/13] AX-1705 - Align jfrog MCP template/injector with jf CLI + add hook injection validation - inject-instructions.mjs: add 3s timeout on 'jf config export'; emit {} on template read failure (fail-closed, well-formed empty payload) - jfrog-mcp-management.md: resolve servers via 'jf config show --format=json' instead of parsing ~/.jfrog/jfrog-cli.conf.v6; add Live-execution pre-flight rule; fix two typos - add scripts/validate-hook-injector.mjs smoke test + CI workflow Co-Authored-By: Claude Opus 4.8 (1M context) --- .../validate-inject-instructions.yml | 34 ++++ plugins/jfrog/scripts/inject-instructions.mjs | 2 + .../jfrog/templates/jfrog-mcp-management.md | 30 +++- scripts/validate-hook-injector.mjs | 156 ++++++++++++++++++ 4 files changed, 213 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/validate-inject-instructions.yml create mode 100644 scripts/validate-hook-injector.mjs diff --git a/.github/workflows/validate-inject-instructions.yml b/.github/workflows/validate-inject-instructions.yml new file mode 100644 index 0000000..37edcf1 --- /dev/null +++ b/.github/workflows/validate-inject-instructions.yml @@ -0,0 +1,34 @@ +# Copyright (c) JFrog Ltd. 2026 +# Licensed under the Apache License, Version 2.0 + +name: Validate hook injection + +on: + pull_request: + branches: [main] + paths: + - "plugins/jfrog/scripts/inject-instructions.mjs" + - "plugins/jfrog/templates/jfrog-mcp-management.md" + - "plugins/jfrog/hooks/hooks.json" + - "plugins/jfrog/.cursor-plugin/plugin.json" + - "scripts/validate-hook-injector.mjs" + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + name: Validate hook injection + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Run injector validation + run: node scripts/validate-hook-injector.mjs diff --git a/plugins/jfrog/scripts/inject-instructions.mjs b/plugins/jfrog/scripts/inject-instructions.mjs index 96f5e68..1c3141c 100755 --- a/plugins/jfrog/scripts/inject-instructions.mjs +++ b/plugins/jfrog/scripts/inject-instructions.mjs @@ -40,6 +40,7 @@ function resolveCredentials() { configToken = execFileSync("jf", ["config", "export"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], + timeout: 3000, }).trim(); } catch (error) { debug(`'jf config export' failed (jf not on PATH or no server configured): ${error.message}`); @@ -129,6 +130,7 @@ try { ); } catch (error) { debug(`Could not read instructions template: ${error.message}`); + process.stdout.write("{}"); process.exit(0); } diff --git a/plugins/jfrog/templates/jfrog-mcp-management.md b/plugins/jfrog/templates/jfrog-mcp-management.md index 7792187..959796b 100644 --- a/plugins/jfrog/templates/jfrog-mcp-management.md +++ b/plugins/jfrog/templates/jfrog-mcp-management.md @@ -8,12 +8,22 @@ below instead. **Registry URL**: Wherever `` appears below, substitute the value of the `JFROG_MCP_GATEWAY_REPO` environment variable if it -is set. Otherwise use +is set. Otherwise, use `https://releases.jfrog.io/artifactory/api/npm/coding-agents-npm/`. **Pre-flight (applies to every gateway command — `--list-available`, `--inspect`, `--login`)**: +- **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 **MUST** physically RE-RUN the command. NEVER reuse, + copy, or re-display output from previous turns or context history; the + catalog, headers, and required inputs change between prompts. (Applies + to these catalog/registry fetches only — `--list-available` and + `--inspect`; NOT `--login`, which would re-open the OAuth browser, and + NOT reading local config for *installed* state.) + - **`` is always mandatory.** Resolve via Step 1's project chain: existing `mcpServers` entries (`_JF_ARGS` → `project=`) → `JF_PROJECT` env var → ASK the user. If none @@ -22,7 +32,10 @@ is set. Otherwise use - **`` is auto-resolvable.** Resolve via Step 1's server chain: existing `mcpServers` entries (value after `--server` in - `args`) → `~/.jfrog/jfrog-cli.conf.v6`: + `args`) → list configured servers with the jf CLI + (`jf config show --format=json`; do NOT parse + `~/.jfrog/jfrog-cli.conf.v6`; the CLI masks tokens, so its output is + safe): - Exactly one jf CLI server configured → use it without asking; pass it as `--server `. The gateway would auto-resolve to the same value if `--server` were omitted, but we pass it explicitly for @@ -50,11 +63,11 @@ STOP — do NOT run the command with guesses. "add an MCP", "what can I install" — your FIRST action is to show them the catalog so they can pick: -1. Resolve server (Server ID`` or URL `JFROG_URL`) +1. Resolve server (Server ID `` or URL `JFROG_URL`) and `` per the Pre-flight rule at the top of this document. Server: auto-use the single jf CLI configs serverId as the server ID or the `JFROG_URL` env var as the URL if unambiguous; only ask when - there are multiple or no jf configs and not env vars. + there are multiple or no jf configs and no env vars. Project: Ask unless `JF_PROJECT` is set, or it's already in an existing `mcpServers` entry. 2. Run "Listing MCPs > Available to install" with that server + @@ -80,11 +93,10 @@ unless absolutely necessary: gateway can resolve credentials from these directly; DO NOT pass `--server` as that would make the gateway try to parse the server details from the jf cli configuration. -3. Else read `~/.jfrog/jfrog-cli.conf.v6` - (`%USERPROFILE%\.jfrog\jfrog-cli.conf.v6` on Windows) via a - terminal command (file-search skips hidden dirs) - NEVER print the full file contents as it can contain secrets. - Use the serverId subkeys:: +3. Else list configured servers with the jf CLI — run + `jf config show --format=json` (do NOT parse + `~/.jfrog/jfrog-cli.conf.v6` yourself; the CLI masks tokens, so its + output is safe to read). From the result: - exactly one server → use it without asking. - two or more → list the `serverId`s and ASK the user which one. 4. Else (file missing, empty, or unreadable, and no `JFROG_URL`) diff --git a/scripts/validate-hook-injector.mjs b/scripts/validate-hook-injector.mjs new file mode 100644 index 0000000..41eb66d --- /dev/null +++ b/scripts/validate-hook-injector.mjs @@ -0,0 +1,156 @@ +#!/usr/bin/env node + +// Copyright (c) JFrog Ltd. 2026 +// Licensed under the Apache License, Version 2.0 + +// Smoke test for the sessionStart injector + plugin packaging, grouped into: +// Syntax — the injector exists and parses. +// Lint — plugin.json / hooks.json / template wiring is internally +// consistent (name, paths). +// Format — running the injector emits a well-formed SessionStart +// payload (valid JSON, correct shape). +// Injection logic — the payload actually carries the real template, and +// fail-closed paths emit {}. +// A template-filename / read-path mismatch makes the injector silently emit +// nothing (it catches the read error and exits 0); these checks turn that +// silent failure into a hard error. + +import { execFileSync } from "node:child_process"; +import { existsSync, readFileSync, statSync } from "node:fs"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const pluginDir = path.join(repoRoot, "plugins", "jfrog"); +const injector = path.join(pluginDir, "scripts", "inject-instructions.mjs"); +const templatesDir = path.join(pluginDir, "templates"); +const hooksFile = path.join(pluginDir, "hooks", "hooks.json"); +const pluginManifestFile = path.join(pluginDir, ".cursor-plugin", "plugin.json"); + +const failures = []; + +function section(title) { + console.log(`\n${title}`); +} + +function check(label, fn) { + try { + fn(); + console.log(` ok ${label}`); + } catch (error) { + failures.push(label); + console.log(` FAIL ${label}\n ${error.message}`); + } +} + +// Run the injector with a clean copy of the env plus the given overrides, so an +// inherited force-flag or real JFrog credentials can't skew the result. +function runInjector(overrides) { + const env = { ...process.env }; + delete env._JF_AGENT_GUARD_FORCE_DISABLE; + delete env.JF_AGENT_GUARD_FORCE_ENABLE; + return execFileSync(process.execPath, [injector], { + encoding: "utf8", + env: { ...env, ...overrides }, + }); +} + +function main() { + console.log("Validating sessionStart injector + plugin packaging…"); + + // ---- Syntax: the injector exists and is parseable JS ---- + section("Syntax"); + check("injector source exists", () => { + if (!existsSync(injector)) throw new Error(`missing: ${injector}`); + }); + check("injector parses (node --check)", () => { + execFileSync(process.execPath, ["--check", injector], { stdio: "pipe" }); + }); + + // ---- Lint: manifest, hook wiring, and template read-path are consistent ---- + section("Lint (manifest & wiring)"); + + check("plugin.json is named the jfrog plugin", () => { + const pluginManifest = JSON.parse(readFileSync(pluginManifestFile, "utf8")); + if (pluginManifest.name !== "jfrog") { + throw new Error(`plugin.json name "${pluginManifest.name}" is not "jfrog"`); + } + if (!/^\d+\.\d+\.\d+$/.test(pluginManifest.version ?? "")) { + throw new Error(`plugin.json version is missing or not semver: ${JSON.stringify(pluginManifest.version)}`); + } + }); + + check("hooks.json wires sessionStart to the injector", () => { + const hooks = JSON.parse(readFileSync(hooksFile, "utf8")); + const entries = hooks?.hooks?.sessionStart; + if (!Array.isArray(entries) || entries.length === 0) { + throw new Error("hooks.json has no sessionStart hooks"); + } + const commands = entries.map((e) => e.command ?? ""); + if (!commands.some((c) => c.includes("inject-instructions.mjs"))) { + throw new Error("no sessionStart command references inject-instructions.mjs"); + } + }); + + // The filename the injector reads must match a real, non-empty template. + let templateName; + check("injector reads an existing template file", () => { + const src = readFileSync(injector, "utf8"); + const match = src.match(/"templates"\s*,\s*"([^"]+)"/); + if (!match) throw new Error("could not find the templates/ read path in the injector"); + templateName = match[1]; + const templatePath = path.join(templatesDir, templateName); + if (!existsSync(templatePath)) { + throw new Error(`injector reads "${templateName}" but it does not exist in plugins/jfrog/templates/`); + } + if (statSync(templatePath).size === 0) { + throw new Error(`template "${templateName}" is empty`); + } + }); + + // ---- Format: force-enable emits a well-formed SessionStart payload ---- + section("Format (injected payload shape)"); + let injectedContext; + check("force-enable emits valid JSON with a SessionStart additionalContext", () => { + const stdout = runInjector({ JF_AGENT_GUARD_FORCE_ENABLE: "true" }); + if (!stdout.trim()) throw new Error("stdout was empty"); + let payload; + try { + payload = JSON.parse(stdout); + } catch (error) { + throw new Error(`stdout did not parse as JSON: ${error.message}`); + } + const hook = payload?.hookSpecificOutput; + if (hook?.hookEventName !== "SessionStart") { + throw new Error(`expected hookSpecificOutput.hookEventName === "SessionStart", got ${JSON.stringify(hook?.hookEventName)}`); + } + if (typeof hook.additionalContext !== "string" || hook.additionalContext.trim().length === 0) { + throw new Error("hookSpecificOutput.additionalContext is missing or empty"); + } + injectedContext = hook.additionalContext; + }); + + // ---- Injection logic: the payload is the real template; fail-closed works ---- + section("Injection logic"); + check("force-enable injects the actual template, byte-for-byte", () => { + if (injectedContext === undefined) throw new Error("force-enable payload not captured (see Format check)"); + if (!templateName) throw new Error("template name was not resolved (see Lint check)"); + const expected = readFileSync(path.join(templatesDir, templateName), "utf8"); + if (injectedContext !== expected) { + throw new Error("injected additionalContext does not match the template file content"); + } + }); + check("force-disable emits {} (fail-closed)", () => { + const stdout = runInjector({ _JF_AGENT_GUARD_FORCE_DISABLE: "true" }).trim(); + if (stdout !== "{}") throw new Error(`expected "{}", got ${JSON.stringify(stdout)}`); + }); + + if (failures.length > 0) { + console.error(`\n${failures.length} check(s) failed.`); + process.exit(1); + } + console.log("\nAll checks passed."); +} + +main(); From b7832792b55bb33dc7a3d71c995ada6427f3bf71 Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:34:48 +0300 Subject: [PATCH 04/13] AX-1705 - Align injector/template with vscode-plugin - Add Apache license URL line to inject-instructions.mjs and validate-hook-injector.mjs headers. - Template: rename "gateway" -> "agent guard" throughout and JFROG_MCP_GATEWAY_REPO -> JFROG_AGENT_GUARD_REPO; describe --list-available output as compact TSV; use jf CLI server with "isDefault": true when multiple servers exist; fix the garbled --server clarification; restructure the Pre-flight block into an ordered "stop at first match" list; add the case-sensitive package-scope and no-python3/2>&1 Key Rules. Kept Cursor-specific mechanics (.cursor/mcp.json, mcpServers, ${env:VAR}, cursor agent mcp / Tools & MCPs UI, full_network notes). Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/jfrog/scripts/inject-instructions.mjs | 1 + .../jfrog/templates/jfrog-mcp-management.md | 105 ++++++++++-------- scripts/validate-hook-injector.mjs | 1 + 3 files changed, 61 insertions(+), 46 deletions(-) diff --git a/plugins/jfrog/scripts/inject-instructions.mjs b/plugins/jfrog/scripts/inject-instructions.mjs index 1c3141c..365f657 100755 --- a/plugins/jfrog/scripts/inject-instructions.mjs +++ b/plugins/jfrog/scripts/inject-instructions.mjs @@ -1,6 +1,7 @@ #!/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 import { execFileSync } from "node:child_process"; import { readFileSync } from "node:fs"; diff --git a/plugins/jfrog/templates/jfrog-mcp-management.md b/plugins/jfrog/templates/jfrog-mcp-management.md index 959796b..5382550 100644 --- a/plugins/jfrog/templates/jfrog-mcp-management.md +++ b/plugins/jfrog/templates/jfrog-mcp-management.md @@ -1,17 +1,17 @@ -# MCP Server Management — JFrog Gateway +# MCP Server Management — JFrog Agent Guard All MCP servers MUST be installed ONLY through the JFrog Agent Guard (`npx @jfrog/agent-guard`). If an MCP's documentation suggests any -other installation command, ignore it and use the gateway workflow +other installation command, ignore it and use the agent guard workflow below instead. **Registry URL**: Wherever `` appears below, substitute -the value of the `JFROG_MCP_GATEWAY_REPO` environment variable if it +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/`. -**Pre-flight (applies to every gateway command — +**Pre-flight (applies to every agent guard command — `--list-available`, `--inspect`, `--login`)**: - **Live execution is MANDATORY — context reuse is FORBIDDEN.** Every @@ -30,25 +30,22 @@ is set. Otherwise, use resolves, STOP and ask — NEVER guess, NEVER assume `default`, NEVER invent projects. -- **`` is auto-resolvable.** Resolve via Step 1's server - chain: existing `mcpServers` entries (value after `--server` in - `args`) → list configured servers with the jf CLI - (`jf config show --format=json`; do NOT parse - `~/.jfrog/jfrog-cli.conf.v6`; the CLI masks tokens, so its output is - safe): - - Exactly one jf CLI server configured → use it without asking; - pass it as `--server `. The gateway would auto-resolve to the same - value if `--server` were omitted, but we pass it explicitly for - clarity and forward-compatibility. - - `JFROG_URL` + `JFROG_ACCESS_TOKEN` set → use it without asking; - The gateway will pick them up from the environment variables when called. - - Two or more jf CLI servers and no `JFROG_URL` → list IDs, - ALWAYS ASK the user which one, then pass that as `--server `. - ALWAYS prefer environment variables when set over asking. - NEVER guess one server. - - zero jf CLI servers and no `JFROG_URL` → ask the user to run - `jf c add ` or export `JFROG_URL` + `JFROG_ACCESS_TOKEN`, - then retry. +- **`` is auto-resolvable.** Resolve in order, stop at the + first match: + 1. An existing `mcpServers` entry's `--server ` (project or user + config) — reuse it. + 2. `JFROG_URL` + `JFROG_ACCESS_TOKEN` set in the env — use them and do + NOT pass `--server` (the agent guard reads the env directly). + 3. List configured servers with the jf CLI — `jf config show --format=json` + (do NOT parse `~/.jfrog/jfrog-cli.conf.v6`; the CLI masks tokens, so + its output is safe). Exactly one → use it; two or more → use the one + with `"isDefault": true`; if none is marked default → ASK the user + which one. Then pass `--server `. + 4. None of the above → ask the user to run `jf c add ` or export + `JFROG_URL` + `JFROG_ACCESS_TOKEN`, then retry. + + When you resolved the ID from a jf CLI config, always pass it as + `--server `; when using env vars, never pass `--server`. - The commands need network access and MUST be run with `full_network` permissions when run in a sandbox. Otherwise `Forbidden` errors will be thrown. @@ -90,24 +87,23 @@ unless absolutely necessary: or `~/.cursor/mcp.json` (user) — take the value after `--server` in `args`. 2. Else `JFROG_URL` env var set (with `JFROG_ACCESS_TOKEN`) — the - gateway can resolve credentials from these directly; - DO NOT pass `--server` as that would make the gateway try to + agent guard can resolve credentials from these directly; + DO NOT pass `--server` as that would make the agent guard try to parse the server details from the jf cli configuration. 3. Else list configured servers with the jf CLI — run `jf config show --format=json` (do NOT parse `~/.jfrog/jfrog-cli.conf.v6` yourself; the CLI masks tokens, so its output is safe to read). From the result: - exactly one server → use it without asking. - - two or more → list the `serverId`s and ASK the user which one. + - two or more → use the one with `"isDefault": true`; if none is + marked default, list the `serverId`s and ASK the user which one. 4. Else (file missing, empty, or unreadable, and no `JFROG_URL`) ask the user to either run `jf c add ` or export `JFROG_URL` + `JFROG_ACCESS_TOKEN`, then retry. -NEVER try multiple servers — pick one. Once chosen, pass it -If a server from the jf cli configuration is supposed to be used: -Always explicitly as `--server ` in every gateway invocation. -Otherwise, if environment variables for `JFROG_URL` and `JFROG_ACCESS_TOKEN` -are used: Do NOT pass `--server ` +NEVER try multiple servers — pick one. When you resolved the ID from a +jf CLI config, always pass it as `--server ` in every agent guard +invocation; when using env vars, never pass `--server`. **Project** @@ -203,7 +199,7 @@ Add the entry under `mcpServers` in the target config (default `@jfrog/agent-guard`** or `npx` falls back to the default registry (404) and may block on a no-TTY prompt. Use `"type": "stdio"` — never `"http"`, `"sse"`, or a top-level `"url"` -(those bypass the gateway). +(those bypass the agent guard). ```json { @@ -230,7 +226,7 @@ registry (404) and may block on a no-TTY prompt. Use Notes: -- If a required `${env:VAR}` is unset, the gateway fails at startup. +- If a required `${env:VAR}` is unset, the agent guard fails at startup. Confirm the user exported it before they restart. If any env vars are missing, ASK the user to export them and restart Cursor. - For `Bearer`-prefixed headers, either include the prefix in the env @@ -334,8 +330,8 @@ elsewhere. 1. Determine **server** and **project** per the Pre-flight rule at the top of this document. `--list-available` does NOT require - any existing `mcpServers` entry or pre-installed gateway — - `npx --yes` fetches the gateway on demand, so this works on a + any existing `mcpServers` entry or pre-installed agent guard — + `npx --yes` fetches the agent guard on demand, so this works on a fresh machine too. 2. Run EXACTLY this command — `--project` is passed as a CLI flag To configure the server, either use the serverId from a jf cli @@ -351,22 +347,34 @@ npx --yes \ [--server ] ``` -Output is a JSON array; each element has `name`, `packageName`, -`description`, `type`, `packageVersion`, optional `env[]`. +The output is a compact TSV: a header line, then one server per line, +tab-separated: `nametypeversiondescription`. +Run the command ONCE and present the rows directly as a numbered +table — do NOT re-run it, redirect it, or parse it with `python3`/`jq`. +The `name` column is the install identifier (the value you pass to +`--inspect --mcp` and to install); `packageName` is NOT a separate +column — for remote/http MCPs there is no package name, so `name` is +the display name. -3. Filter out any `packageName` already present in the installed list +3. Filter out any `name` already present in the installed list (compare against `mcp=` in `_JF_ARGS`). Mark the rest as available to install. ## Key Rules +- **Package scope is case-sensitive — ALWAYS write it lowercase as + `@jfrog/agent-guard`, NEVER `@JFrog/agent-guard`.** npm scopes are + case-sensitive; the published package is the lowercase + `@jfrog/agent-guard`. Capitalizing the brand (`@JFrog`) points at a + different/nonexistent scope and breaks the command. Use the exact + lowercase string in every command and config entry. - **`npx` arg order:** `--yes`, `--registry `, - `@jfrog/agent-guard`, then gateway flags. Both `--yes` and + `@jfrog/agent-guard`, then agent guard flags. Both `--yes` and `--registry` MUST precede the package name or `npx` falls back to the default registry (404) and may block on a no-TTY prompt. - **Always `"type": "stdio"`** pointing at `npx @jfrog/agent-guard`, - even for remote-only catalog MCPs (the gateway proxies them). - `"http"`, `"sse"`, or a top-level `"url"` bypass the gateway. + even for remote-only catalog MCPs (the agent guard proxies them). + `"http"`, `"sse"`, or a top-level `"url"` bypass the agent guard. - `_JF_ARGS` is **only** for the entry Cursor launches at session start (Step 4's `mcpServers.*.env`); MUST contain `project=&mcp=`. @@ -379,7 +387,12 @@ Output is a JSON array; each element has `name`, `packageName`, NEVER invent or guess projects or server IDs. - Package name MUST come from the catalog (`--inspect` / `--list-available`). NEVER guess. NEVER install MCPs outside the - gateway. NEVER use Fetch/WebFetch for catalog calls. + agent guard. NEVER use Fetch/WebFetch for catalog calls. +- NEVER pipe a catalog command through `python3`, and NEVER capture it + with `2>&1` — `npx`/`npm` writes progress to stderr, which corrupts + the output stream. For `--list-available` present the compact TSV it + prints; for `--inspect` read the JSON it prints on stdout + directly (or with a single `jq` filter), never via `python3`. - NEVER write a raw secret into `mcp.json` — always use `${env:VAR_NAME}`. NEVER show tokens / API keys. - NEVER try multiple servers — ask the user to pick one. @@ -387,25 +400,25 @@ Output is a JSON array; each element has `name`, `packageName`, ## Troubleshooting - **`ready` but 0 tools (empty `mcps//tools/` after a - Command Palette `Developer: Reload Window`)** — gateway proxy + Command Palette `Developer: Reload Window`)** — agent guard proxy started, upstream MCP did not. The top-level `ready` label is misleading here. NEVER report success when there are 0 tools. 1. Open Cursor's MCP / Output panel for the - gateway stderr; diagnose by MCP type: + agent guard stderr; diagnose by MCP type: - **OAuth (remote)** — re-run Step 5 (`--login`); refresh token likely expired. - **Static-token (remote)** — confirm every `${env:VAR}` in `env` is exported in the shell that launched Cursor and the token is still valid. - **Local (stdio)** — check that the bundled binary actually - launched (gateway stderr will show the spawn error). + launched (agent guard stderr will show the spawn error). 2. Verify that the mcp server is still allowed. See "Listing MCPs > Available to install". - **`mcp.json` server missing from `cursor agent mcp list` / Tools & MCP** — never enabled. Re-run Step 4a (`cursor agent mcp enable `); if the entry is brand-new, also `Developer: Reload Window` so Cursor picks up the file. -- **Gateway: `multiple/no JFrog server configured`** (the gateway +- **Agent Guard: `multiple/no JFrog server configured`** (the agent guard cannot pick a JFrog server) — pass `--server ` (after `jf c add `) OR export both `JFROG_URL` and `JFROG_ACCESS_TOKEN` in the launching shell, then relaunch Cursor. diff --git a/scripts/validate-hook-injector.mjs b/scripts/validate-hook-injector.mjs index 41eb66d..15c657c 100644 --- a/scripts/validate-hook-injector.mjs +++ b/scripts/validate-hook-injector.mjs @@ -2,6 +2,7 @@ // Copyright (c) JFrog Ltd. 2026 // Licensed under the Apache License, Version 2.0 +// https://www.apache.org/licenses/LICENSE-2.0 // Smoke test for the sessionStart injector + plugin packaging, grouped into: // Syntax — the injector exists and parses. From 79a21d735d3b3da78a1decb5a5e972e20b3d6701 Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:54:38 +0300 Subject: [PATCH 05/13] AX-1705 - Align injector with claude-plugin JFROG_URL validation Port the early JFROG_URL validation block from claude-plugin: warn and exit with a single SessionStart payload when JFROG_URL is unset or has a trailing slash, surfacing the misconfig before the MCP server fails with a confusing DNS/double-slash error. Wording adapted to "restart Cursor". Supply a JFROG_URL override in the validator's force-enable test so it exercises the real template-injection path past the new URL check. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/jfrog/scripts/inject-instructions.mjs | 32 +++++++++++++++++-- scripts/validate-hook-injector.mjs | 2 +- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/plugins/jfrog/scripts/inject-instructions.mjs b/plugins/jfrog/scripts/inject-instructions.mjs index 365f657..98ec605 100755 --- a/plugins/jfrog/scripts/inject-instructions.mjs +++ b/plugins/jfrog/scripts/inject-instructions.mjs @@ -41,7 +41,7 @@ function resolveCredentials() { configToken = execFileSync("jf", ["config", "export"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], - timeout: 3000, + timeout: 1500, }).trim(); } catch (error) { debug(`'jf config export' failed (jf not on PATH or no server configured): ${error.message}`); @@ -80,7 +80,7 @@ async function isAgentGuardEnabledViaSettings() { debug(`Fetching agent guard setting from ${url}`); const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 5000); + const timeout = setTimeout(() => controller.abort(), 4000); try { const response = await fetch(url, { method: "GET", @@ -112,7 +112,33 @@ if (forceDisabled) { debug("Force-disable flag is set."); process.stdout.write("{}"); process.exit(0); -} else if (forceEnabled) { +} + +// Validate JFROG_URL early to surface misconfigurations before the MCP server +// attempts to connect and fails with a confusing DNS or double-slash error. +if (!process.env.JFROG_URL) { + const missingUrlWarning = "WARNING: JFROG_URL is not set. The JFrog MCP server will be unreachable — set JFROG_URL to your Artifactory base URL (e.g. https://mycompany.jfrog.io) and restart Cursor."; + log(missingUrlWarning); + process.stdout.write(JSON.stringify({ + hookSpecificOutput: { + hookEventName: "SessionStart", + additionalContext: `IMPORTANT: Immediately tell the user: "${missingUrlWarning}"`, + }, + })); + process.exit(0); +} else if (process.env.JFROG_URL.endsWith("/")) { + const trailingSlashWarning = "WARNING: JFROG_URL has a trailing slash. This produces a double-slash in the MCP URL and will silently fail — remove the trailing slash and restart Cursor."; + log(trailingSlashWarning); + process.stdout.write(JSON.stringify({ + hookSpecificOutput: { + hookEventName: "SessionStart", + additionalContext: `IMPORTANT: Immediately tell the user: "${trailingSlashWarning}"`, + }, + })); + process.exit(0); +} + +if (forceEnabled) { debug("Force-enable flag is set."); } else if (!(await isAgentGuardEnabledViaSettings())) { debug("Agent Guard not enabled; exiting without injecting instructions"); diff --git a/scripts/validate-hook-injector.mjs b/scripts/validate-hook-injector.mjs index 15c657c..175ed3d 100644 --- a/scripts/validate-hook-injector.mjs +++ b/scripts/validate-hook-injector.mjs @@ -114,7 +114,7 @@ function main() { section("Format (injected payload shape)"); let injectedContext; check("force-enable emits valid JSON with a SessionStart additionalContext", () => { - const stdout = runInjector({ JF_AGENT_GUARD_FORCE_ENABLE: "true" }); + const stdout = runInjector({ JF_AGENT_GUARD_FORCE_ENABLE: "true", JFROG_URL: "https://example.jfrog.io" }); if (!stdout.trim()) throw new Error("stdout was empty"); let payload; try { From ac9e6c46d0fca5e63759385b5a4a178a550acf4a Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Mon, 6 Jul 2026 12:19:10 +0300 Subject: [PATCH 06/13] Add license URL comment to validate-inject-instructions.yml --- .github/workflows/validate-inject-instructions.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/validate-inject-instructions.yml b/.github/workflows/validate-inject-instructions.yml index 37edcf1..020ecf5 100644 --- a/.github/workflows/validate-inject-instructions.yml +++ b/.github/workflows/validate-inject-instructions.yml @@ -1,5 +1,6 @@ # Copyright (c) JFrog Ltd. 2026 # Licensed under the Apache License, Version 2.0 +#https://www.apache.org/licenses/LICENSE-2.0 name: Validate hook injection From 32e60b758f2fd0eb76aaec99139f4abc190bacfc Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:07:48 +0300 Subject: [PATCH 07/13] AX-1705 - Refactor JFrog credentials resolution and enhance error handling --- plugins/jfrog/scripts/inject-instructions.mjs | 88 +++---------------- 1 file changed, 10 insertions(+), 78 deletions(-) diff --git a/plugins/jfrog/scripts/inject-instructions.mjs b/plugins/jfrog/scripts/inject-instructions.mjs index 98ec605..8f6438f 100755 --- a/plugins/jfrog/scripts/inject-instructions.mjs +++ b/plugins/jfrog/scripts/inject-instructions.mjs @@ -3,7 +3,6 @@ // Licensed under the Apache License, Version 2.0 // https://www.apache.org/licenses/LICENSE-2.0 -import { execFileSync } from "node:child_process"; import { readFileSync } from "node:fs"; import path from "node:path"; import process from "node:process"; @@ -25,53 +24,17 @@ const forceDisabled = const forceEnabled = env("JF_AGENT_GUARD_FORCE_ENABLE") === "true"; -// Resolve {baseUrl, token} from env vars, falling back to the JFrog CLI's -// default server. Returns null when nothing resolves. -function resolveCredentials() { +async function isAgentGuardEnabledViaSettings() { const baseUrl = env("JFROG_URL", "JF_URL"); const token = env("JFROG_ACCESS_TOKEN", "JF_ACCESS_TOKEN"); - if (baseUrl && token) { - debug("Resolved credentials from environment variables"); - return { baseUrl, token }; - } - - // `jf config export` emits the default server as a base64-encoded JSON token. - let configToken; - try { - configToken = execFileSync("jf", ["config", "export"], { - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - timeout: 1500, - }).trim(); - } catch (error) { - debug(`'jf config export' failed (jf not on PATH or no server configured): ${error.message}`); - return null; - } - - let cfg; - try { - cfg = JSON.parse(Buffer.from(configToken, "base64").toString("utf8")); - } catch (error) { - debug(`Could not decode the jf Config Token: ${error.message}`); - return null; - } - - if (!cfg?.url || !cfg?.accessToken) { - debug("jf Config Token did not contain a usable url + accessToken"); - return null; + if (!baseUrl) { + debug("JFROG_URL/JF_URL is not set; skipping settings check"); + return false; } - - debug(`Resolved credentials via 'jf config export' (serverId: ${cfg.serverId ?? ""})`); - return { baseUrl: cfg.url, token: cfg.accessToken }; -} - -async function isAgentGuardEnabledViaSettings() { - const credentials = resolveCredentials(); - if (!credentials) { - debug("No JFrog credentials resolved; skipping settings check"); + if (!token) { + debug("JFROG_ACCESS_TOKEN/JF_ACCESS_TOKEN is not set; skipping settings check"); return false; } - const { baseUrl, token } = credentials; const url = baseUrl.replace(/\/+$/, "") + @@ -80,7 +43,7 @@ async function isAgentGuardEnabledViaSettings() { debug(`Fetching agent guard setting from ${url}`); const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 4000); + const timeout = setTimeout(() => controller.abort(), 5000); try { const response = await fetch(url, { method: "GET", @@ -112,33 +75,7 @@ if (forceDisabled) { debug("Force-disable flag is set."); process.stdout.write("{}"); process.exit(0); -} - -// Validate JFROG_URL early to surface misconfigurations before the MCP server -// attempts to connect and fails with a confusing DNS or double-slash error. -if (!process.env.JFROG_URL) { - const missingUrlWarning = "WARNING: JFROG_URL is not set. The JFrog MCP server will be unreachable — set JFROG_URL to your Artifactory base URL (e.g. https://mycompany.jfrog.io) and restart Cursor."; - log(missingUrlWarning); - process.stdout.write(JSON.stringify({ - hookSpecificOutput: { - hookEventName: "SessionStart", - additionalContext: `IMPORTANT: Immediately tell the user: "${missingUrlWarning}"`, - }, - })); - process.exit(0); -} else if (process.env.JFROG_URL.endsWith("/")) { - const trailingSlashWarning = "WARNING: JFROG_URL has a trailing slash. This produces a double-slash in the MCP URL and will silently fail — remove the trailing slash and restart Cursor."; - log(trailingSlashWarning); - process.stdout.write(JSON.stringify({ - hookSpecificOutput: { - hookEventName: "SessionStart", - additionalContext: `IMPORTANT: Immediately tell the user: "${trailingSlashWarning}"`, - }, - })); - process.exit(0); -} - -if (forceEnabled) { +} else if (forceEnabled) { debug("Force-enable flag is set."); } else if (!(await isAgentGuardEnabledViaSettings())) { debug("Agent Guard not enabled; exiting without injecting instructions"); @@ -155,18 +92,13 @@ try { path.join(root, "templates", "jfrog-mcp-management.md"), "utf8", ); -} catch (error) { - debug(`Could not read instructions template: ${error.message}`); +} catch { process.stdout.write("{}"); process.exit(0); } -// The IDE consumes hookSpecificOutput.additionalContext from a SessionStart hook. process.stdout.write( JSON.stringify({ - hookSpecificOutput: { - hookEventName: "SessionStart", - additionalContext: template, - }, + additional_context: template, }), ); From 67bba50f6dd78a7b86c37ea1e0e25bad410ba26e Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Mon, 6 Jul 2026 21:54:38 +0300 Subject: [PATCH 08/13] Fix validate-hook-injector.mjs to match real injector payload shape The injector emits a flat { additional_context } object, not a hookSpecificOutput/SessionStart wrapper, so the CI validation script was failing on every run. Co-Authored-By: Claude Sonnet 5 --- scripts/validate-hook-injector.mjs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/scripts/validate-hook-injector.mjs b/scripts/validate-hook-injector.mjs index 175ed3d..e1a7fb0 100644 --- a/scripts/validate-hook-injector.mjs +++ b/scripts/validate-hook-injector.mjs @@ -8,7 +8,7 @@ // Syntax — the injector exists and parses. // Lint — plugin.json / hooks.json / template wiring is internally // consistent (name, paths). -// Format — running the injector emits a well-formed SessionStart +// Format — running the injector emits a well-formed sessionStart // payload (valid JSON, correct shape). // Injection logic — the payload actually carries the real template, and // fail-closed paths emit {}. @@ -110,10 +110,10 @@ function main() { } }); - // ---- Format: force-enable emits a well-formed SessionStart payload ---- + // ---- Format: force-enable emits a well-formed sessionStart payload ---- section("Format (injected payload shape)"); let injectedContext; - check("force-enable emits valid JSON with a SessionStart additionalContext", () => { + check("force-enable emits valid JSON with a non-empty additional_context", () => { const stdout = runInjector({ JF_AGENT_GUARD_FORCE_ENABLE: "true", JFROG_URL: "https://example.jfrog.io" }); if (!stdout.trim()) throw new Error("stdout was empty"); let payload; @@ -122,14 +122,10 @@ function main() { } catch (error) { throw new Error(`stdout did not parse as JSON: ${error.message}`); } - const hook = payload?.hookSpecificOutput; - if (hook?.hookEventName !== "SessionStart") { - throw new Error(`expected hookSpecificOutput.hookEventName === "SessionStart", got ${JSON.stringify(hook?.hookEventName)}`); + if (typeof payload?.additional_context !== "string" || payload.additional_context.trim().length === 0) { + throw new Error("additional_context is missing or empty"); } - if (typeof hook.additionalContext !== "string" || hook.additionalContext.trim().length === 0) { - throw new Error("hookSpecificOutput.additionalContext is missing or empty"); - } - injectedContext = hook.additionalContext; + injectedContext = payload.additional_context; }); // ---- Injection logic: the payload is the real template; fail-closed works ---- From d3835f74cc505d8bd87ddc73234f85ed38fa2c1f Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Tue, 7 Jul 2026 07:15:46 +0300 Subject: [PATCH 09/13] Add support for resolving JFrog credentials from environment variables and CLI config --- plugins/jfrog/scripts/inject-instructions.mjs | 49 ++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/plugins/jfrog/scripts/inject-instructions.mjs b/plugins/jfrog/scripts/inject-instructions.mjs index 8f6438f..0651872 100755 --- a/plugins/jfrog/scripts/inject-instructions.mjs +++ b/plugins/jfrog/scripts/inject-instructions.mjs @@ -3,6 +3,7 @@ // Licensed under the Apache License, Version 2.0 // https://www.apache.org/licenses/LICENSE-2.0 +import { execFileSync } from "node:child_process"; import { readFileSync } from "node:fs"; import path from "node:path"; import process from "node:process"; @@ -24,17 +25,53 @@ const forceDisabled = const forceEnabled = env("JF_AGENT_GUARD_FORCE_ENABLE") === "true"; -async function isAgentGuardEnabledViaSettings() { +// Resolve {baseUrl, token} from env vars, falling back to the JFrog CLI's +// default server. Returns null when nothing resolves. +function resolveCredentials() { const baseUrl = env("JFROG_URL", "JF_URL"); const token = env("JFROG_ACCESS_TOKEN", "JF_ACCESS_TOKEN"); - if (!baseUrl) { - debug("JFROG_URL/JF_URL is not set; skipping settings check"); - return false; + if (baseUrl && token) { + debug("Resolved credentials from environment variables"); + return { baseUrl, token }; + } + + // `jf config export` emits the default server as a base64-encoded JSON token. + let configToken; + try { + configToken = execFileSync("jf", ["config", "export"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + timeout: 3000, + }).trim(); + } catch (error) { + debug(`'jf config export' failed (jf not on PATH or no server configured): ${error.message}`); + return null; + } + + let cfg; + try { + cfg = JSON.parse(Buffer.from(configToken, "base64").toString("utf8")); + } catch (error) { + debug(`Could not decode the jf Config Token: ${error.message}`); + return null; + } + + if (!cfg?.url || !cfg?.accessToken) { + debug("jf Config Token did not contain a usable url + accessToken"); + return null; } - if (!token) { - debug("JFROG_ACCESS_TOKEN/JF_ACCESS_TOKEN is not set; skipping settings check"); + + debug(`Resolved credentials via 'jf config export' (serverId: ${cfg.serverId ?? ""})`); + return { baseUrl: cfg.url, token: cfg.accessToken }; +} + +async function isAgentGuardEnabledViaSettings() { + const credentials = resolveCredentials(); + if (!credentials) { + debug("No JFrog credentials resolved; skipping settings check"); return false; } + const { baseUrl, token } = credentials; const url = baseUrl.replace(/\/+$/, "") + From 79ea11a235abe8e0f85ddff397d8d1552b4d70e6 Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:12:46 +0300 Subject: [PATCH 10/13] Bump version to 0.5.6 for JFrog Platform plugins --- .cursor-plugin/marketplace.json | 2 +- plugins/jfrog/.cursor-plugin/plugin.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 964ec6f..e30f8ae 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -6,7 +6,7 @@ }, "metadata": { "description": "JFrog Platform plugins for Cursor", - "version": "0.5.5", + "version": "0.5.6", "pluginRoot": "plugins" }, "plugins": [ diff --git a/plugins/jfrog/.cursor-plugin/plugin.json b/plugins/jfrog/.cursor-plugin/plugin.json index 13fa44b..3aa316a 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.5.5", + "version": "0.5.6", "description": "JFrog Platform integration with MCP, security skills, supply-chain best practices, and JFrog Agent Guard governance for adding, removing, and listing MCP servers.", "author": { "name": "JFrog", From 7add03c21a2d553908a4a0a1dbac11d5adf6271b Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:39:58 +0300 Subject: [PATCH 11/13] Enhance credential resolution and debugging for JFrog CLI integration --- .../validate-inject-instructions.yml | 2 +- plugins/jfrog/scripts/inject-instructions.mjs | 10 ++-- scripts/validate-hook-injector.mjs | 50 ++++++++++++++++++- 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/.github/workflows/validate-inject-instructions.yml b/.github/workflows/validate-inject-instructions.yml index 020ecf5..0e2a041 100644 --- a/.github/workflows/validate-inject-instructions.yml +++ b/.github/workflows/validate-inject-instructions.yml @@ -1,6 +1,6 @@ # Copyright (c) JFrog Ltd. 2026 # Licensed under the Apache License, Version 2.0 -#https://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 name: Validate hook injection diff --git a/plugins/jfrog/scripts/inject-instructions.mjs b/plugins/jfrog/scripts/inject-instructions.mjs index 0651872..15d5242 100755 --- a/plugins/jfrog/scripts/inject-instructions.mjs +++ b/plugins/jfrog/scripts/inject-instructions.mjs @@ -25,8 +25,10 @@ const forceDisabled = const forceEnabled = env("JF_AGENT_GUARD_FORCE_ENABLE") === "true"; -// Resolve {baseUrl, token} from env vars, falling back to the JFrog CLI's -// default server. Returns null when nothing resolves. +// Resolve {baseUrl, token}: environment variables (JFROG_URL/JFROG_ACCESS_TOKEN, +// or legacy JF_*) are checked first; if either is missing, fall back to the +// JFrog CLI's default configured server via `jf config export`. Returns null +// when neither source yields usable credentials. function resolveCredentials() { const baseUrl = env("JFROG_URL", "JF_URL"); const token = env("JFROG_ACCESS_TOKEN", "JF_ACCESS_TOKEN"); @@ -41,7 +43,7 @@ function resolveCredentials() { configToken = execFileSync("jf", ["config", "export"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], - timeout: 3000, + timeout: 2000, }).trim(); } catch (error) { debug(`'jf config export' failed (jf not on PATH or no server configured): ${error.message}`); @@ -80,7 +82,7 @@ async function isAgentGuardEnabledViaSettings() { debug(`Fetching agent guard setting from ${url}`); const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 5000); + const timeout = setTimeout(() => controller.abort(), 4000); try { const response = await fetch(url, { method: "GET", diff --git a/scripts/validate-hook-injector.mjs b/scripts/validate-hook-injector.mjs index e1a7fb0..fd488bc 100644 --- a/scripts/validate-hook-injector.mjs +++ b/scripts/validate-hook-injector.mjs @@ -16,8 +16,9 @@ // nothing (it catches the read error and exits 0); these checks turn that // silent failure into a hard error. -import { execFileSync } from "node:child_process"; -import { existsSync, readFileSync, statSync } from "node:fs"; +import { execFileSync, spawnSync } from "node:child_process"; +import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import path from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; @@ -57,6 +58,37 @@ function runInjector(overrides) { }); } +// Same as runInjector, but also captures stderr and clears any JFrog env vars +// so the jf-CLI fallback in resolveCredentials() is actually reachable. +function runInjectorWithDebug(overrides) { + const env = { ...process.env }; + delete env._JF_AGENT_GUARD_FORCE_DISABLE; + delete env.JF_AGENT_GUARD_FORCE_ENABLE; + delete env.JFROG_URL; + delete env.JF_URL; + delete env.JFROG_ACCESS_TOKEN; + delete env.JF_ACCESS_TOKEN; + const result = spawnSync(process.execPath, [injector], { + encoding: "utf8", + env: { ...env, JF_AGENT_GUARD_DEBUG: "true", ...overrides }, + }); + return { stdout: result.stdout ?? "", stderr: result.stderr ?? "" }; +} + +// Stubs a fake `jf` executable on PATH that emits the given config token, so +// the CLI-fallback path can be exercised without a real JFrog CLI install. +function withFakeJfOnPath(configToken, fn) { + const bin = mkdtempSync(path.join(tmpdir(), "fake-jf-")); + const jfPath = path.join(bin, "jf"); + writeFileSync(jfPath, `#!/usr/bin/env node\nprocess.stdout.write(${JSON.stringify(configToken)});\n`); + chmodSync(jfPath, 0o755); + try { + return fn(`${bin}${path.delimiter}${process.env.PATH}`); + } finally { + rmSync(bin, { recursive: true, force: true }); + } +} + function main() { console.log("Validating sessionStart injector + plugin packaging…"); @@ -138,6 +170,20 @@ function main() { throw new Error("injected additionalContext does not match the template file content"); } }); + // ---- jf CLI fallback: resolveCredentials() falls back to `jf config export` ---- + section("jf CLI fallback"); + check("resolves credentials via 'jf config export' when env vars are unset", () => { + const token = Buffer.from( + JSON.stringify({ url: "https://example.jfrog.io", accessToken: "fake-token", serverId: "test-server" }), + ).toString("base64"); + withFakeJfOnPath(token, (fakePath) => { + const { stderr } = runInjectorWithDebug({ PATH: fakePath }); + if (!stderr.includes("Resolved credentials via 'jf config export'")) { + throw new Error(`expected debug log confirming jf CLI fallback, got:\n${stderr}`); + } + }); + }); + check("force-disable emits {} (fail-closed)", () => { const stdout = runInjector({ _JF_AGENT_GUARD_FORCE_DISABLE: "true" }).trim(); if (stdout !== "{}") throw new Error(`expected "{}", got ${JSON.stringify(stdout)}`); From 09ec92b7e3cc2cdf2e92b6c927363db5ca8ac65b Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:56:55 +0300 Subject: [PATCH 12/13] Explain the jf Config Token decode step in resolveCredentials Reviewer asked for a comment on what this part does, in addition to the function-level docstring covering the env-vars-first/jf-CLI -fallback order. Co-Authored-By: Claude Sonnet 5 --- plugins/jfrog/scripts/inject-instructions.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/jfrog/scripts/inject-instructions.mjs b/plugins/jfrog/scripts/inject-instructions.mjs index 15d5242..0e37952 100755 --- a/plugins/jfrog/scripts/inject-instructions.mjs +++ b/plugins/jfrog/scripts/inject-instructions.mjs @@ -50,6 +50,8 @@ function resolveCredentials() { return null; } + // The token is a base64-encoded JSON blob containing the server's url, + // accessToken, and serverId — decode and validate it before using it. let cfg; try { cfg = JSON.parse(Buffer.from(configToken, "base64").toString("utf8")); From d653d43474f23a9a8fa502c3708c8ef0cabef145 Mon Sep 17 00:00:00 2001 From: Matan Eden <57892946+MatanEden1@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:19:48 +0300 Subject: [PATCH 13/13] Document the JFrog CLI >= 2.105.0 requirement for Agent Guard's jf-CLI fallback jf config show/export only support --format=json from CLI 2.105.0 onward; older CLIs error on it. Reviewer flagged this as an undocumented new hard requirement introduced by the jf-CLI credential fallback. Co-Authored-By: Claude Sonnet 5 --- README.md | 1 + plugins/jfrog/README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 53f3739..f10d3df 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Before installing, make sure you have: - **Node.js** (≥ 14) — with `npx` on your `PATH`. - **Skill runtime requirements** — `jf` CLI, `jq`, and `curl` on `PATH`, plus a configured JFrog instance. For the minimum versions, see the upstream skills [`Requirements`](https://github.com/jfrog/jfrog-skills/blob/v0.11.0/README.md#requirements). Configure the CLI with `jf config add` — see [Authentication](#authentication). - **JFrog Platform access** (optional) — If you want to use the Agent Guard feature, your JFrog subscription needs to include the AI Catalog entitlement. Contact your JFrog account team if you're unsure whether it's enabled. +- **JFrog CLI ≥ 2.105.0** (optional) — If you want the Agent Guard hook to auto-resolve credentials/server ID from the JFrog CLI instead of `JFROG_PLATFORM_URL`/`JFROG_ACCESS_TOKEN` env vars. Older CLIs don't support the `--format` flag used by `jf config show`/`jf config export` for this. - **JFrog project** (optional) — If you want to use the Agent Guard feature. --- diff --git a/plugins/jfrog/README.md b/plugins/jfrog/README.md index 3f2b254..e626d0e 100644 --- a/plugins/jfrog/README.md +++ b/plugins/jfrog/README.md @@ -10,6 +10,7 @@ JFrog Platform integration for Cursor — artifact management, security scanning - Toggle the **MCP Server** option ON and save. 3. Set the `JFROG_PLATFORM_URL` environment variable to your JFrog instance (e.g., `mycompany.jfrog.io`). 4. **JFrog CLI** (`jf`) is used by the skills for authentication and REST/GraphQL API operations. If missing, the agent will attempt to install it. You can also install manually via `brew install jfrog-cli` or the [official install script](https://jfrog.com/help/r/jfrog-cli/install-the-jfrog-cli). +5. **JFrog CLI ≥ 2.105.0** (optional) — required if you want the Agent Guard hook to auto-resolve credentials/server ID from the JFrog CLI instead of `JFROG_URL`/`JFROG_ACCESS_TOKEN` env vars. Older CLIs don't support the `--format` flag used by `jf config show`/`jf config export` for this. CLI authentication options: run `jf login` for browser-based setup, or set the `JFROG_ACCESS_TOKEN` environment variable. MCP-based workflows authenticate via **OAuth** and require no additional configuration.