Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "jfrog",
"version": "0.1.13",
"version": "0.1.14",
"description": "JFrog skills and the JFrog MCP server for Codex \u2014 interact with the JFrog Platform.",
"author": {
"name": "JFrog",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codex-plugin",
"version": "0.1.13",
"version": "0.1.14",
"private": true,
"type": "module",
"description": "JFrog skills and MCP server plugin for OpenAI Codex.",
Expand Down
2 changes: 1 addition & 1 deletion scripts/sync-skills-vendor.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"repo": "jfrog/jfrog-skills",
"pin": "v0.43.0",
"pin": "v0.44.0",
"paths": [
"skills"
]
Expand Down
2 changes: 1 addition & 1 deletion skills/jfrog-ai-catalog/references/installing-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ every choice (`--repo`, target) up front.

**Resolve `<harness>` from the environment check script — never from your model
name.** If `<UA>` is not already known from this session, run
`bash <skill_path>/../jfrog/scripts/check-environment.sh <model-slug>` now and capture
`node <skill_path>/../jfrog/scripts/check-environment.mjs <model-slug>` now and capture
its stdout as `<UA>`. Parse the `tool=<h>` field from `<UA>` and map it to a
`jf` harness name:

Expand Down
15 changes: 9 additions & 6 deletions skills/jfrog-ai-catalog/references/installing-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,29 +52,32 @@ up front.

**Resolve `<harness>` from the environment check script — never from your model
name.** If `<UA>` is not already known from this session, run
`bash <skill_path>/../jfrog/scripts/check-environment.sh <model-slug>` now and capture
`node <skill_path>/../jfrog/scripts/check-environment.mjs <model-slug>` now and capture
its stdout as `<UA>`. Parse the `tool=<h>` field from `<UA>` and map it to a
`jf` harness name:

| `tool=` value in `<UA>` | `--harness` for `jf skills` |
|-------------------------|------------------------------|
| `claude` | `claude-code` |
| `cursor` | `cursor` |
| `copilot` | `github-copilot` |
| `copilot` | `github-copilot` (plus `--global`, see below) |
| `unknown`, empty, or any other | Ask the user |

If `tool` is `unknown`, empty, or not in the table — do **not** guess. Ask
the user for the desired install path and use `--path <dir>` instead.
**Exception — Kiro and Junie (install only):** `jf` accepts neither
`--harness kiro` nor `--harness junie`, so if you're self-identified as either
(Kiro IDE / `kiro-cli`, or Junie — `check-environment.sh` detects Junie but not
(Kiro IDE / `kiro-cli`, or Junie — `check-environment.mjs` detects Junie but not
Kiro), skip asking and use `--path` directly: `.kiro/skills` (project) /
`~/.kiro/skills` (global, or `$KIRO_HOME/skills` if `KIRO_HOME` is set) for
Kiro; `.junie/skills` (project) / `~/.junie/skills` (global) for Junie. This
exception does not extend to `jf skills list` — see *List currently installed
skills* in `managing-installed-skills.md`.

Choose exactly one install target (these are mutually exclusive):
**Exception — Copilot:** also pass `--global` (installs to `~/.copilot/skills`);
without it, skills land under Copilot Chat's cwd, which VS Code may not scan.

Otherwise choose exactly one install target (these are mutually exclusive):

| Flag | Installs into |
|------|---------------|
Expand Down Expand Up @@ -315,8 +318,8 @@ jf skills update "<slug>" --server-id "<SID>" --harness "<harness>" --repo "<rep
```

Use the same install-target flag (`--harness`/`--global`/`--project-dir`/
`--path`) the skill was installed with. After updating, re-verify the `SKILL.md`
(see *Verify the install landed* above). If the update download 403s, handle it
`--path`) the skill was installed with (Copilot: plus `--global`). After
updating, re-verify the `SKILL.md` (see *Verify the install landed* above). If the update download 403s, handle it
as in *Handling a blocked download* above.

On success, reply using **this exact template**:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jf skills list --server-id "<SID>" --harness "<harness>" --check-updates
```

Resolve `<harness>` to the current agent (see `installing-skills.md`).
For Copilot, add `--global` to `--check-updates` too.
**Exception — Kiro and Junie:** neither `--harness kiro` nor `--harness junie`
works, and `list` has no `--path` flag, so list directly from the filesystem:
skill directory names under `.kiro/skills` / `~/.kiro/skills` (or
Expand Down
32 changes: 20 additions & 12 deletions skills/jfrog-init/scripts/jfrog-login-register-session.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { execFileSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import { jfApi, parseHttpStatus } from "./lib/jf-api.mjs";
import { isMainModule } from "./lib/jf.mjs";
import { resolveCommand } from "./lib/command.mjs";
import { SAFE_URL } from "./jfrog-login-save-credentials.mjs";

// Pins stdio so a failing/logging `jf` subprocess can't leak output to
Expand Down Expand Up @@ -102,18 +103,25 @@ export function registerSession(platformUrlRaw) {
const sessionUuid = randomUUID();
const verifyCode = sessionUuid.slice(-4);

// Register the session with the Access API
const register = jfApi([
"--url",
platformUrl,
"-X",
"POST",
"-H",
"Content-Type: application/json",
"-d",
JSON.stringify({ session: sessionUuid }),
"/access/api/v2/authentication/jfrog_client_login/request",
]);
// Register the session with the Access API. A `jf.cmd` shim runs under
// cmd.exe, which splits on spaces and reads `"` as a metacharacter (see
// jfApi()), so there the body goes over stdin and the header drops its
// space. Elsewhere the body stays a plain `-d` arg.
const body = JSON.stringify({ session: sessionUuid });
const { shell } = resolveCommand("jf");
const register = jfApi(
[
"--url",
platformUrl,
"-X",
"POST",
"-H",
shell ? "Content-Type:application/json" : "Content-Type: application/json",
...(shell ? ["--input", "-"] : ["-d", body]),
"/access/api/v2/authentication/jfrog_client_login/request",
],
shell ? { input: body } : {}
);
if (!register.ok) {
const code = parseHttpStatus(register.stderr);
process.stderr.write(`ERROR: Session registration failed (HTTP ${code})\n`);
Expand Down
35 changes: 28 additions & 7 deletions skills/jfrog-init/scripts/jfrog-login-save-credentials.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import { execFileSync } from "node:child_process";
import { jfApi, parseHttpStatus } from "./lib/jf-api.mjs";
import {
assertJfArgsShellSafe,
isMainModule,
jfConfigShow,
normalizeJpdUrl,
Expand All @@ -46,6 +47,9 @@ import { resolveCommand } from "./lib/command.mjs";
// so everything interpolated into a `jf` argument is screened first.
export const SAFE_URL = /^https?:\/\/[A-Za-z0-9.-]+(:\d+)?(\/[A-Za-z0-9._~/-]*)?$/;
const SAFE_TOKEN = /^[A-Za-z0-9._-]+$/;
// The session UUID goes into the token path, so only register-session's
// canonical randomUUID() form is accepted.
export const SAFE_SESSION_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

// Derive server ID from URL
// SaaS: https://mycompany.jfrog.io → mycompany
Expand Down Expand Up @@ -73,6 +77,13 @@ function execFileOpts(timeoutMs, needsShell, input) {
};
}

// Every direct `jf` spawn below goes through the same screen as runJf().
// An optional `input` is written to the child's stdin.
export function execJf(jfPath, needsShell, args, timeoutMs, input) {
assertJfArgsShellSafe(args, needsShell, "saveCredentials");
return execFileSync(jfPath, args, execFileOpts(timeoutMs, needsShell, input));
}

export function saveCredentials(platformUrlRaw, sessionUuid) {
if (!platformUrlRaw || !sessionUuid) {
process.stderr.write("Usage: node jfrog-login-save-credentials.mjs <platform-url> <session-uuid>\n");
Expand All @@ -83,12 +94,16 @@ export function saveCredentials(platformUrlRaw, sessionUuid) {
process.stderr.write("ERROR: platform URL contains unexpected characters.\n");
return 1;
}
if (!SAFE_SESSION_UUID.test(sessionUuid)) {
process.stderr.write("ERROR: session UUID is not a valid UUID.\n");
return 1;
}

selfHealPath();
const { target: jfPath, shell: needsShell } = resolveCommand("jf");

try {
execFileSync(jfPath, ["--version"], execFileOpts(10_000, needsShell));
execJf(jfPath, needsShell, ["--version"], 10_000);
} catch (err) {
if (err.code === "ENOENT") {
process.stderr.write("ERROR: jf is not installed\n");
Expand Down Expand Up @@ -151,19 +166,25 @@ export function saveCredentials(platformUrlRaw, sessionUuid) {

// Save credentials to jf config (writes to ~/.jfrog/, needs unrestricted filesystem)
try {
execFileSync(jfPath, ["config", "remove", serverId, "--quiet"], execFileOpts(10_000, needsShell));
execJf(jfPath, needsShell, ["config", "remove", serverId, "--quiet"], 10_000);
} catch {
// No existing entry to remove — fine.
}

// Token goes over stdin, never argv — argv is visible to other local users
// in process listings (and, under a `.cmd` shim, in cmd.exe's command line).
const addArgs = ["config", "add", serverId, `--url=${platformUrl}`, "--access-token-stdin", "--interactive=false"];
// Screened before the try so a rejected argument isn't reported below as a
// sandbox problem.
try {
assertJfArgsShellSafe(addArgs, needsShell, "saveCredentials");
} catch (err) {
process.stderr.write(`ERROR: ${err.message}\n`);
return 4;
}

try {
execFileSync(
jfPath,
["config", "add", serverId, `--url=${platformUrl}`, "--access-token-stdin", "--interactive=false"],
execFileOpts(15_000, needsShell, accessToken)
);
execJf(jfPath, needsShell, addArgs, 15_000, accessToken);
} catch {
process.stderr.write("ERROR: Failed to save credentials with jf config add.\n");
process.stderr.write("This may be caused by sandbox restrictions on ~/.jfrog/ writes.\n");
Expand Down
20 changes: 14 additions & 6 deletions skills/jfrog-init/scripts/lib/jf-api.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { execFileSync } from "node:child_process";
import { resolveCommand } from "./command.mjs";
import { assertJfArgsShellSafe } from "./jf.mjs";

// Matches jf rt ping's own network-call timeout (lib/jf.mjs's
// JF_CLI_TIMEOUT_MS) for the same reason: long enough that a slow JPD
Expand All @@ -28,29 +29,36 @@ export function parseHttpStatus(text) {
// Runs `jf api <...args>`, returning both streams and exit info instead of
// throwing — a non-zero exit (unreachable server, a 400 on an unfinished
// login, etc) is an expected outcome the caller branches on, not a script
// bug.
export function jfApi(args) {
// bug. A shell-unsafe arg under a `.cmd` shim is rejected before anything
// spawns and comes back as `{ ok: false }` too, with the reason in `stderr`
// (this function never throws). JSON never passes that screen, so a
// request body goes in via `input` (stdin, read by `--input -`).
export function jfApi(args, { input } = {}) {
const { target, shell } = resolveCommand("jf");
try {
const { target, shell } = resolveCommand("jf");
assertJfArgsShellSafe(args, shell, "jfApi");
const stdout = execFileSync(target, ["api", ...args], {
encoding: "utf8",
timeout: JF_API_TIMEOUT_MS,
shell,
input,
// Node's execFileSync/execSync echo the child's stderr to the
// parent's own stderr live by default ("stderr by default will be
// output to the parent's stderr unless stdio is specified" per the
// Node docs) — on top of still populating err.stderr for a failed
// call. Left at the default, every `jf api` info/warn log line
// (e.g. "Http Status: NNN") would leak straight to the terminal.
// Pinning stdio here keeps this silent: nothing is inherited,
// everything is still captured.
stdio: ["ignore", "pipe", "pipe"],
// everything is still captured. stdin is piped only to carry `input`.
stdio: [input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
env: { ...process.env, CI: "true" },
});
return { ok: true, stdout, stderr: "" };
} catch (err) {
const stdout = err.stdout ? err.stdout.toString() : "";
const stderr = err.stderr ? err.stderr.toString() : "";
// A rejected arg never spawned, so there is no child stderr: surface the
// rejection message instead.
const stderr = err.stderr ? err.stderr.toString() : /refusing shell-unsafe/.test(err.message) ? err.message : "";
return { ok: false, stdout, stderr, error: err };
}
}
17 changes: 11 additions & 6 deletions skills/jfrog-init/scripts/lib/jf.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,22 @@ export const JF_CLI_TIMEOUT_MS = 30_000;
// SHELL_UNSAFE, a `jf` arg (e.g. a server ID) may legitimately contain one.
export const JF_ARG_UNSAFE = /[&|;$<>`"'\\^\n]/;

// The screen every `jf` spawn with caller-built args applies first — runJf(),
// lib/jf-api.mjs's jfApi(), jfrog-login-save-credentials.mjs — so none drifts.
export function assertJfArgsShellSafe(args, shell, caller) {
if (!shell) return;
const unsafe = args.find((a) => JF_ARG_UNSAFE.test(a));
if (unsafe !== undefined) {
throw new Error(`${caller}: refusing shell-unsafe argument: ${JSON.stringify(unsafe)}`);
}
}

// The one place every `jf` spawn goes through. `timeoutMs` overrides the
// local-operation default for a network-bound caller.
export function runJf(args, { timeoutMs = JF_CLI_TIMEOUT_MS } = {}) {
selfHealPath();
const { target, shell } = resolveCommand("jf");
if (shell) {
const unsafe = args.find((a) => JF_ARG_UNSAFE.test(a));
if (unsafe !== undefined) {
throw new Error(`runJf: refusing shell-unsafe argument: ${JSON.stringify(unsafe)}`);
}
}
assertJfArgsShellSafe(args, shell, "runJf");
// Without this, execFileSync forwards jf's stderr to ours.
// Node's DEP0190 fires on every shell:true spawn with an args array,
// warning generically about unescaped-argument injection — the exact
Expand Down
4 changes: 2 additions & 2 deletions skills/jfrog-mcp-management/references/harness-common.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ plus **exactly one** harness file; do NOT open the others.
## Step A — detect the harness and open ONE file

The `CLAUDECODE` / `CURSOR_*` / `CODEX_*` / `OPENCODE` signals below
mirror `../../jfrog/scripts/check-environment.sh` `detect_harness()`; the
mirror `../../jfrog/scripts/check-environment.mjs` `detectHarness()`; the
`TERM_PROGRAM=vscode` editor hint is **not** in that script, and Devin and
Kiro are **not** detected by the script. Each row's signal is **self-contained and
non-overlapping**, so detection does not depend on evaluation order. The VS
Code harness file targets the **VS Code editor** (Copilot MCP support), not
the standalone GitHub Copilot terminal CLI — the CLI (`COPILOT_CLI`) has no
editor UI or `mcp.json`, so it falls through to the Fallback section.

1. Call `../../jfrog/scripts/check-environment.sh` and parse `tool=<name>` from
1. Run `node "<skill_path>/../jfrog/scripts/check-environment.mjs"` and parse `tool=<name>` from
the User-Agent line. When `tool` is `claude` or `cursor`, that matches the
Claude or Cursor row below — open that harness file. This call also
satisfies the Prerequisites environment check — capture/export
Expand Down
29 changes: 19 additions & 10 deletions skills/jfrog/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ description: >-
advanced security, exposures, sast, or any JFrog product name.
Do NOT use this skill to install, add, remove, list, or manage MCP servers.
compatibility: >-
Requires bash, jq, and awk on PATH. The JFrog CLI (jf) is required for Tier 2
(CLI) and Tier 3 (jf api) operations; without it, only MCP (Tier 1) is available.
Requires bash, jq, and awk on PATH (Git Bash or WSL on Windows), and Node.js 18
or newer for the environment-check scripts (bash fallback if Node is missing).
The JFrog CLI (jf) is required for Tier 2 (CLI) and Tier 3 (jf api)
operations; without it, only MCP (Tier 1) is available.
metadata:
role: base
version: "0.43.0"
version: "0.44.0"
---

# JFrog Skill
Expand Down Expand Up @@ -153,8 +155,13 @@ or Tier 3 (`jf`) operation this session, run the environment check. On exit
0/1, **remember its stdout line verbatim** as `<UA>` for the rest of the
session. Skip the CLI offer during `jfrog-init`.

Run `node --version` first. Node 18+ → use the `.mjs` scripts below. Missing or
older → use the `.sh` twin of every script below with `bash` (needs bash and jq;
Git Bash or WSL on Windows): same arguments incl. `--clear`/`--force`, same
output.

```bash
bash <skill_path>/scripts/check-environment.sh <model-slug>
node <skill_path>/scripts/check-environment.mjs <model-slug>
# exit 0/1 stdout: exactly one opaque line — that line IS <UA>. Copy it byte-for-byte.
# Do not parse, rebuild, or approximate the export value from this comment.
# stderr: JSON state (cached 24h at ${JFROG_CLI_HOME_DIR:-$HOME/.jfrog}/skills-cache/jfrog-skill-state.json)
Expand All @@ -163,16 +170,18 @@ bash <skill_path>/scripts/check-environment.sh <model-slug>
Then, on exit 0/1 only:

```bash
bash <skill_path>/scripts/cli-newer-version-offer.sh
node <skill_path>/scripts/cli-newer-version-offer.mjs
# No Node 18+ → bash <skill_path>/scripts/cli-newer-version-offer.sh
# SKIP → continue the original task; do not mention the offer.
# NEWER_AVAILABLE <cli_version> <latest> (suggest_upgrade) → never block or ask
# mid-task. Immediately run (before any other command):
# bash <skill_path>/scripts/cli-newer-version-offer.sh --clear
# node <skill_path>/scripts/cli-newer-version-offer.mjs --clear
# (no Node 18+: bash <skill_path>/scripts/cli-newer-version-offer.sh --clear)
# Then finish the original task (success or failure) and end the final reply
# with this line verbatim as its very last line (no other upgrade mention):
# JFrog CLI <latest> is available (you have <cli_version>). Upgrade? Yes / No
# Yes (later turn) → references/jfrog-cli-install-upgrade.md, then
# check-environment.sh --force (Tier 2/3 only on exit 0/1). No → silent.
# check-environment.mjs (or .sh) <model-slug> --force (Tier 2/3 only on exit 0/1). No → silent.
# Next offer = next new latest. JFROG_CLI_AVOID_NEW_VERSION_WARNING=true → SKIP.
```

Expand All @@ -192,7 +201,7 @@ invent, rebuild, or edit it). On exit 2/3 there is no `<UA>` — do not synthesi
one. Current stdout starts with `jfrog-skills/`, never with `model/`.

- **Parent session:** if `<UA>` is missing or starts with `model/` (legacy),
discard it and **re-run** `check-environment.sh`. Export the new exit 0/1
discard it and **re-run** `check-environment.mjs`. Export the new exit 0/1
stdout line only when it does **not** start with `model/`; otherwise **stop**
(do not invent).
- **Subagents:** use only the parent-passed exact `<UA>` — never re-run the
Expand Down Expand Up @@ -251,7 +260,7 @@ Use **`jq`** for all JSON parsing of CLI and API output (pipes, `-r`, filters).
`${JFROG_CLI_HOME_DIR:-$HOME/.jfrog}/skills-cache/` is **not** a general scratch
or temp directory. Use it **only** for these two artifacts:

1. **`jfrog-skill-state.json`** — written by `scripts/check-environment.sh`
1. **`jfrog-skill-state.json`** — written by `scripts/check-environment.mjs`
(24-hour CLI check cache).
2. **`onemodel-schema-${JFROG_SERVER_ID}.graphql`** — cached OneModel supergraph
schema (see `references/onemodel-graphql.md`).
Expand Down Expand Up @@ -481,7 +490,7 @@ reused); re-read for `jq` — never re-run the same network call to fix parsing.
only when the next action needs `jf api` / advanced CLI:

- [ ] `export JFROG_CLI_USER_AGENT='<UA>'` — exact stdout line from
`check-environment.sh` exit 0/1; CLI offer done or N/A (`jfrog-init`);
`check-environment.mjs` exit 0/1 (`.sh` if no Node 18+); CLI offer done or N/A (`jfrog-init`);
`NEWER_AVAILABLE` = end-of-reply note, never a mid-task stop
- [ ] network `jf`: `--server-id <SID>` after subcommand (not `jf --version` /
`jf config show` pre-SID)
Expand Down
Loading
Loading