diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 36117a0..a157216 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "jfrog", "displayName": "JFrog", "description": "Official JFrog plugin. Connect Claude Code to JFrog to manage, secure, and govern your software supply chain. Give agents the context to build secure, compliant software.", - "version": "0.2.3", + "version": "0.2.4", "author": { "name": "JFrog Ltd.", "email": "devrel@jfrog.com", diff --git a/.github/scripts/sync-skills-vendor.json b/.github/scripts/sync-skills-vendor.json new file mode 100644 index 0000000..b4c4d06 --- /dev/null +++ b/.github/scripts/sync-skills-vendor.json @@ -0,0 +1,5 @@ +{ + "repo": "jfrog/jfrog-skills", + "pin": "v0.11.0", + "paths": ["skills"] +} diff --git a/.github/scripts/sync-skills.mjs b/.github/scripts/sync-skills.mjs new file mode 100644 index 0000000..51ec45c --- /dev/null +++ b/.github/scripts/sync-skills.mjs @@ -0,0 +1,116 @@ +#!/usr/bin/env node +// Vendors skill content from the upstream jfrog/jfrog-skills repository +// into this plugin. Run manually when bumping the pin: bump `pin` in +// sync-skills-vendor.json, then run this +// script to regenerate `skills/`, then commit both alongside each other. +// +// Usage: +// node .github/scripts/sync-skills.mjs +// +// Steps the script performs: +// 1. Reads sync-skills-vendor.json to learn which repo + ref to pull. +// 2. Downloads that tarball from codeload.github.com (public, no auth). +// 3. Extracts it into a temp directory. +// 4. Copies the requested paths (e.g. "skills") into the repo root, +// replacing any existing tree. +// +// The pin in sync-skills-vendor.json is the single source of truth — +// there is no runtime override. To ship a different skill version, +// change the pin in a PR and commit the synced tree alongside it. + +import { promises as fs, createWriteStream } from "node:fs"; +import { Readable } from "node:stream"; +import { pipeline } from "node:stream/promises"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { tmpdir } from "node:os"; +import { fileURLToPath } from "node:url"; + +// filesystem helpers +async function readJson(filePath) { + return JSON.parse(await fs.readFile(filePath, "utf8")); +} + +async function fileExists(filePath) { + try { await fs.access(filePath); return true; } catch { return false; } +} + +// download the upstream tarball + +// codeload.github.com serves any public repo's archive over HTTPS +// without auth, accepting a tag, branch, or commit SHA as the ref. +async function downloadTarball(repo, ref, destPath) { + const url = `https://codeload.github.com/${repo}/tar.gz/${encodeURIComponent(ref)}`; + const res = await fetch(url, { redirect: "follow" }); + if (!res.ok) throw new Error(`Could not download ${repo}@${ref} (HTTP ${res.status})`); + await pipeline(Readable.fromWeb(res.body), createWriteStream(destPath)); + console.log(` fetched ${url}`); +} + +// extract the tarball + +// Shells out to the system `tar` instead of pulling in an npm tar library — +// keeps the script zero-dependency. +// +// GitHub tarballs always have exactly one top-level directory whose +// name encodes the repo + commit. We return that path so the caller +// knows where to find the extracted tree. +async function extractTarball(tarballPath, intoDir) { + await fs.mkdir(intoDir, { recursive: true }); + const result = spawnSync("tar", ["-xzf", tarballPath, "-C", intoDir], { stdio: "inherit" }); + if (result.status !== 0) throw new Error(`tar exited with status ${result.status}`); + const [topLevel] = await fs.readdir(intoDir); + return path.join(intoDir, topLevel); +} + +// copy one path from the extracted tree into the plugin + +// Removes the destination first so we never end up with stale leftovers +// from a previous sync, then creates the destination's parent directory then copies. +async function copyPath(fromDir, toDir, relativePath) { + const from = path.join(fromDir, relativePath); + const to = path.join(toDir, relativePath); + if (!(await fileExists(from))) { + throw new Error(`path missing in upstream tarball: ${relativePath}`); + } + await fs.rm(to, { recursive: true, force: true }); + await fs.mkdir(path.dirname(to), { recursive: true }); + await fs.cp(from, to, { recursive: true }); + console.log(` ${relativePath} -> ${path.relative(process.cwd(), to)}`); +} + +// Sync this plugin: read sync-skills-vendor.json, download + extract + copy. +// +// Paths are resolved relative to the script itself rather than CWD, so +// the script works regardless of where it's invoked from. The repo root +// is two levels up from .github/scripts/. +async function main() { + const scriptDir = path.dirname(fileURLToPath(import.meta.url)); + const repoRoot = path.resolve(scriptDir, "..", ".."); + const vendorPath = path.join(scriptDir, "sync-skills-vendor.json"); + if (!(await fileExists(vendorPath))) { + throw new Error(`missing sync-skills-vendor.json at ${vendorPath}`); + } + + const { repo, pin, paths } = await readJson(vendorPath); + if (!repo || !pin || !Array.isArray(paths) || paths.length === 0) { + throw new Error(`${vendorPath} must define 'repo', 'pin' and a non-empty 'paths' array`); + } + + console.log(`--- ${repo} (ref: ${pin}) ---`); + + const workDir = await fs.mkdtemp(path.join(tmpdir(), "sync-skills-")); + try { + // `slug` is just a unique filename for this tarball + extract dir. + const slug = `${repo.replace("/", "-")}-${pin.replace(/[^A-Za-z0-9._-]/g, "_")}`; + const tarball = path.join(workDir, `${slug}.tar.gz`); + await downloadTarball(repo, pin, tarball); + const extracted = await extractTarball(tarball, path.join(workDir, slug)); + for (const rel of paths) await copyPath(extracted, repoRoot, rel); + } finally { + await fs.rm(workDir, { recursive: true, force: true }); + } + console.log("done."); +} + +await main(); diff --git a/.gitignore b/.gitignore index 603ad37..82ac12b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ .env.* !.env.example +# IDE settings +.idea/ + # OS and editor .DS_Store *.swp @@ -13,8 +16,5 @@ tmp/ temp/ -# Local extract when refreshing vendored skills -.tmp-jfrog-skills/ - # Skill runtime caches (never commit) **/local-cache/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 26020fd..06018fd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,7 @@ All contributors must sign the [JFrog CLA](https://jfrog.com/cla/) before contri node scripts/validate-claude-plugin.mjs ``` -Before a release or directory submission, also run **`claude plugin validate`** (requires [Claude Code](https://code.claude.com/docs) CLI). +This checks `.claude-plugin/plugin.json` and walks every `skills/*/SKILL.md` for required YAML frontmatter. Before a release or directory submission, also run **`claude plugin validate`** (requires [Claude Code](https://code.claude.com/docs) CLI). 4. **Test** by loading the repository as the plugin (the repo root is the plugin root): @@ -29,17 +29,28 @@ Exercise the skills you changed (for example `/jfrog:`). Run `/reloa 5. **Commit** with a clear, descriptive message. 6. Open a **pull request** against `main` with a summary of what changed and why. +### Updating the vendored skills + +The `skills/` tree is vendored from [jfrog/jfrog-skills](https://github.com/jfrog/jfrog-skills) and committed to `main` — see [`VENDOR.md`](VENDOR.md) for the full flow. To regenerate the tree locally against the pin in [`.github/scripts/sync-skills-vendor.json`](.github/scripts/sync-skills-vendor.json): + +```bash +node .github/scripts/sync-skills.mjs +``` + +This downloads the pinned upstream tarball and replaces the contents of `skills/`. Commit the result alongside any pin/version bumps. + ## Pre-release checklist - [ ] `node scripts/validate-claude-plugin.mjs` passes. - [ ] `claude plugin validate` passes (before directory submission or major releases). - [ ] Version bumped in [`.claude-plugin/plugin.json`](.claude-plugin/plugin.json) when the plugin changes. - [ ] No secrets, credentials, or files under `**/local-cache/` committed. +- [ ] If the skill tree changed: `pin` in `.github/scripts/sync-skills-vendor.json` matches the upstream tag the new tree was generated from. - [ ] Smoke-test: `claude --plugin-dir .` from the repo root. ### Submitting to the Claude plugin directory -Use [Submitting your plugin](https://claude.com/docs/plugins/submit). Submit the **public GitHub URL** of this repository — the **repository root** is the plugin root (manifest in `.claude-plugin/`, skills in `skills/`). +Use [Submitting your plugin](https://claude.com/docs/plugins/submit). Submit the **public GitHub URL** of this repository — the **repository root** is the plugin root (manifest in `.claude-plugin/`, skills committed under `skills/`, vendored from [jfrog/jfrog-skills](https://github.com/jfrog/jfrog-skills)). Compliance: [Anthropic Software Directory Terms](https://support.claude.com/en/articles/13145338-anthropic-software-directory-terms), [Anthropic Software Directory Policy](https://support.claude.com/en/articles/13145358-anthropic-software-directory-policy). diff --git a/README.md b/README.md index dccccd6..3d1d95f 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,8 @@ Before installing, make sure you have: - **JFrog host URL and access token** — Your JFrog platform URL and a valid access token. - **Claude Code CLI** (≥ 1.0) — The Claude Code CLI. -- **Node.js** (≥ 14) — with `npx` on your `PATH`. -- **JFrog CLI** (≥ 2.x, optional) — Recommended for `jf config add` authentication (see [Authentication](#authentication)). +- **Node.js** (≥ 14) — with `npx` on your `PATH` (used by the Agent Guard hook). +- **Skill runtime requirements** — `jf` CLI, `jq`, and `curl` on `PATH`, plus a configured JFrog instance. For the minimum versions, see the upstream skills [`Requirements`](https://github.com/jfrog/jfrog-skills/blob/v0.11.0/README.md#requirements). Configure the CLI with `jf config add` — see [Authentication](#authentication). - **JFrog AI Catalog** (optional) — If you want to use the Agent Guard feature, your JFrog subscription needs to include the AI Catalog entitlement. Contact your JFrog account team if you're unsure whether it's enabled. - **JFrog project** (optional) — If you want to use the Agent Guard feature. @@ -117,6 +117,26 @@ See the [JFrog MCP Registry troubleshooting guide](https://docs.jfrog.com/ai-ml/ --- +## Updating the vendored skills + +The `skills/` tree is vendored from [`jfrog/jfrog-skills`](https://github.com/jfrog/jfrog-skills) at the version pinned in [`.github/scripts/sync-skills-vendor.json`](.github/scripts/sync-skills-vendor.json). To pull a newer upstream release into this repo: + +1. Bump `pin` in `.github/scripts/sync-skills-vendor.json` to the new tag (e.g. `v0.12.0`). +2. Run the sync script from the repo root: + + ```bash + node .github/scripts/sync-skills.mjs + ``` + + It downloads the pinned tarball from `codeload.github.com`, extracts it, and replaces the directories listed in `paths` (today: `skills/`). +3. Bump `version` in [`.claude-plugin/plugin.json`](.claude-plugin/plugin.json) so users actually receive the update — Claude Code skips installs whose resolved version hasn't changed. +4. Update the pinned-version link in the [Prerequisites](#prerequisites) section so the skill runtime requirements point at the new tag. +5. Commit the pin bump, the regenerated `skills/` tree, the version bump, and the README link bump together, and open a PR. + +See [`VENDOR.md`](VENDOR.md) for the full picture. + +--- + ## Contributing See [`CONTRIBUTING.md`](CONTRIBUTING.md) for development setup, coding conventions, and the pull-request process. diff --git a/VENDOR.md b/VENDOR.md new file mode 100644 index 0000000..eb7b1a5 --- /dev/null +++ b/VENDOR.md @@ -0,0 +1,26 @@ +# Vendored skills + +The skill packages under `skills/` are vendored from **[jfrog/jfrog-skills](https://github.com/jfrog/jfrog-skills)** and committed to `main`. + +| | | +| --- | --- | +| **Repository** | https://github.com/jfrog/jfrog-skills | +| **Pinned release** | see `pin` in [`.github/scripts/sync-skills-vendor.json`](.github/scripts/sync-skills-vendor.json) | + +Included directories: `jfrog/`, `jfrog-package-safety-and-download/` (as of the pinned release). + +## Refreshing + +When the upstream repo publishes a new release, refresh the vendored tree via a PR that: + +1. Bumps `pin` in [`.github/scripts/sync-skills-vendor.json`](.github/scripts/sync-skills-vendor.json) to the new tag. +2. Re-syncs and commits the refreshed `skills/` tree. +3. Bumps `version` in [`.claude-plugin/plugin.json`](.claude-plugin/plugin.json) so users actually receive the update (Claude Code skips installs whose resolved version hasn't changed). + +To regenerate the tree locally before opening the PR: + +```bash +node .github/scripts/sync-skills.mjs +``` + +The script reads its sibling [`sync-skills-vendor.json`](.github/scripts/sync-skills-vendor.json), downloads the pinned upstream tarball from `codeload.github.com`, and replaces the directories listed in `paths` (today: `skills/`). diff --git a/skills/VENDOR.md b/skills/VENDOR.md deleted file mode 100644 index 6b107b7..0000000 --- a/skills/VENDOR.md +++ /dev/null @@ -1,13 +0,0 @@ -# Vendored skills - -The skill packages in this directory are vendored from **[jfrog/jfrog-skills](https://github.com/jfrog/jfrog-skills)**. - -| | | -| --- | --- | -| **Repository** | https://github.com/jfrog/jfrog-skills | -| **Release** | [v0.3.0](https://github.com/jfrog/jfrog-skills/releases/tag/v0.3.0) | -| **Source commit** | `af1fb9d98d8b7230cc179a41c12d606aca3aac7e` | - -Included directories: `jfrog/`, `jfrog-package-safety-and-download/` (as of that release). - -To refresh: take the [latest release tarball](https://github.com/jfrog/jfrog-skills/releases/latest), replace those skill trees under `skills/`, and update this file with the new tag and commit SHA (`git rev-parse vX.Y.Z` in a clone, or the GitHub tag object). diff --git a/skills/jfrog-package-safety-and-download/SKILL.md b/skills/jfrog-package-safety-and-download/SKILL.md index 3762550..598318a 100644 --- a/skills/jfrog-package-safety-and-download/SKILL.md +++ b/skills/jfrog-package-safety-and-download/SKILL.md @@ -2,7 +2,7 @@ name: jfrog-package-safety-and-download description: >- Check JFrog Public Catalog and stored packages for a version, interpret - catalog security signals, and download through Artifactory (Jfrog Platform + catalog security signals, and download through Artifactory (JFrog Platform locations, remote cache, curation-aware package managers, or repo proxy). Use when the user asks whether a package is safe, allowed, curated, or wants to download npm, Maven, PyPI, Go, or similar packages via JFrog. @@ -37,12 +37,12 @@ When to read this file: flowchart TD A[User requests package check / download] --> B{Package in Public Catalog?} B -->|Yes| C[Get latest version from Catalog] - B -->|No| D{Package in Jfrog Platform Stored Packages?} + B -->|No| D{Package in JFrog Platform Stored Packages?} D -->|Yes| E[Get latest version from Stored Packages] D -->|No| F[Package not found — stop] - C --> G{Latest version in Jfrog Platform?} + C --> G{Latest version in JFrog Platform?} E --> G - G -->|Yes| H[Safe — download from Jfrog Platform] + G -->|Yes| H[Safe — download from JFrog Platform] G -->|No| I{Curation entitled?} I -->|Yes| J[Check curation policy via API] I -->|No| K[Download via remote repo] @@ -60,23 +60,23 @@ reduce total latency: parallel. Use whichever returns data; if the Public Catalog returns a hit, prefer its `latestVersion` for Step 2. - **Step 3 + Step 5**: After determining the version, query stored package - versions (Jfrog Platform check) and curation entitlement + versions (JFrog Platform check) and curation entitlement (`/api/system/version`) in parallel. Both are independent reads — the - curation result is needed immediately if the Jfrog Platform check returns + curation result is needed immediately if the JFrog Platform check returns empty. -When issuing parallel Shell calls, remember that credentials don't persist -across calls — use the jfrog skill to obtain the platform URL and credentials -in each parallel call. +When issuing parallel Shell calls, each `jf api` call authenticates +independently against the active `jf config` server; no shell state needs +to be passed between calls. ## Step 1: Find the package Search the **Public Catalog** first via OneModel GraphQL, then fall back to **Stored Packages** if not found. -Use the jfrog skill for credentials (Tier 3) and the GraphQL execution -pattern. Refer to `../jfrog/references/onemodel-query-examples.md` for query -shapes. +Execute the query through `jf api` as described in +`../jfrog/references/onemodel-graphql.md`; refer to +`../jfrog/references/onemodel-query-examples.md` for concrete query shapes. **When package type is known** (e.g. `npm`, `maven`, `pypi`), use `publicPackages.getPackage(type:, name:)` (see *Get a public package*). @@ -104,9 +104,9 @@ type they mean. | Source | Version field | |--------|--------------| | Public Catalog | `latestVersion.version` (object selection required) | -| Jfrog Platform Stored Packages | `latestVersionName` on `StoredPackage`, or highest entry from `versionsConnection` | +| JFrog Platform Stored Packages | `latestVersionName` on `StoredPackage`, or highest entry from `versionsConnection` | -## Step 3: Check if package + latest version exists in Jfrog Platform +## Step 3: Check if package + latest version exists in JFrog Platform Query stored package versions using `storedPackages.searchPackageVersions` with a `hasPackageWith` filter (see `../jfrog/references/onemodel-query-examples.md` @@ -114,51 +114,51 @@ with a `hasPackageWith` filter (see `../jfrog/references/onemodel-query-examples version from Step 2, and request `locationsConnection` to get repository details (`repositoryKey`, `repositoryType`, `leadArtifactPath`). -Use the jfrog skill for credentials and GraphQL execution. +Execute the query through `jf api` (see +`../jfrog/references/onemodel-graphql.md` for the invocation pattern). -- **Found with locations** → package is in the Jfrog Platform. Report as **safe to +- **Found with locations** → package is in the JFrog Platform. Report as **safe to download**. Proceed to Step 4. - **Not found** → proceed to Step 5. -## Step 4: Download from Jfrog Platform +## Step 4: Download from JFrog Platform -Use the location info from Step 3. Download command depends on repository -type. **`` must be a full file path** (e.g. +Use the location info from Step 3. Binary artifact downloads go through +`jf rt dl` — **not** `jf api`. `jf api` is the unified entry point for the +JFrog REST APIs (metadata, admin, curation, etc.) and does not expose the +`-L` / `-o` flags needed to stream binary content through a redirect chain. + +**`` must be a full file path** (e.g. `./downloads/lodash-4.18.1.tgz`), not a bare directory. `jf rt dl --flat` treats the target as a file name; passing a directory causes a misleading "open path: is a directory" error. | `repositoryType` | Strategy | |-------------------|----------| -| `local` or `federated` | Use `jf rt dl` — the artifact is stored locally and will always be found | -| `remote` | Use the proxy endpoint directly — `jf rt dl` only finds already-cached artifacts and returns 0 for uncached packages, wasting an API call | - -**local / federated download:** - -```bash -jf rt dl "/" --flat -``` +| `local` or `federated` | `jf rt dl "/" --flat` | +| `remote` | `jf rt dl` against the **base** remote repo (strip any trailing `-cache`) — it transparently triggers the remote fetch when the artifact is not yet cached | -**remote download — go straight to the proxy endpoint:** +**local / federated / remote download:** ```bash -jf rt curl -s -L -XGET "/api///" \ - -o +jf rt dl "/" --flat ``` -**Resolving the remote repo key for the proxy endpoint:** The `repositoryKey` -returned by OneModel for remote locations often already ends in `-cache` (e.g. -`devNPM-remote-cache`). The proxy endpoint needs the **base remote repo name** +**Resolving the remote repo key:** The `repositoryKey` returned by OneModel +for remote locations often already ends in `-cache` (e.g. +`devNPM-remote-cache`). `jf rt dl` needs the **base remote repo name** (without `-cache`). Strip the `-cache` suffix when present (e.g. -`devNPM-remote-cache` → `devNPM-remote`). If the key does not end in `-cache`, -use it as-is. +`devNPM-remote-cache` → `devNPM-remote`). If the key does not end in +`-cache`, use it as-is. -See the **Protocol endpoints** table below for `` and path format. +See the **Protocol endpoints** table below for the package-type-specific +path format inside the repo. ## Step 5: Check curation entitlement ```bash -jf rt curl -s -XGET /api/system/version | jq '.addons | index("curation") != null' +jf api /artifactory/api/system/version \ + | jq '.addons | index("curation") != null' ``` - `true` → curation is entitled. Proceed to Step 6a. @@ -171,81 +171,99 @@ package version is allowed across all repositories before downloading. ```bash RESPONSE_FILE="/tmp/curation-status-$$.json" -jf xr curl -s -XPOST "/api/v1/curation/package_status/all_repos" \ - -H "Content-Type: application/json" \ - -d "{\"packageType\":\"\",\"packageName\":\"\",\"packageVersion\":\"\"}" \ - -o "$RESPONSE_FILE" -w "\n%{http_code}" -echo "$RESPONSE_FILE" +PAYLOAD_FILE="/tmp/curation-payload-$$.json" +STDERR_FILE="/tmp/curation-err-$$.log" + +jq -n \ + --arg type "" \ + --arg name "" \ + --arg version "" \ + '{packageType:$type, packageName:$name, packageVersion:$version}' \ + > "$PAYLOAD_FILE" + +set +e +jf api /xray/api/v1/curation/package_status/all_repos \ + -X POST -H "Content-Type: application/json" \ + --input "$PAYLOAD_FILE" \ + > "$RESPONSE_FILE" 2> "$STDERR_FILE" +RC=$? +set -e +echo "RC=$RC"; echo "$RESPONSE_FILE" ``` Supported `packageType` values: `npm`, `pypi`, `maven`, `go`, `nuget`, `docker`, `gradle`. -To capture both the response body and the HTTP status code in one call, use -`-w "\n%{http_code}"` and parse the last line as the status code: +**Interpreting the result with `jf api`**: unlike plain `curl`, `jf api` +surfaces the HTTP result through its **exit code** and a +`" [Warn] ... returned 4xx/5xx"` line on **stderr** (not a +`%{http_code}` suffix in stdout). The response body is always written to +stdout. Parse both: ```bash -HTTP_CODE=$(tail -1 "$RESPONSE_FILE") -BODY=$(sed '$d' "$RESPONSE_FILE") -if [ "$HTTP_CODE" = "403" ]; then - echo "Blocked by curation policy:" - echo "$BODY" -elif [ "$HTTP_CODE" = "200" ]; then +if [ "$RC" -eq 0 ]; then echo "Package is allowed by curation." +elif grep -q 'returned 403' "$STDERR_FILE"; then + echo "Blocked by curation policy:" + cat "$RESPONSE_FILE" +else + echo "Curation check failed (rc=$RC):" + cat "$STDERR_FILE" fi ``` -**Evaluate the HTTP status code:** +**Evaluate the outcome:** -- **200** → package is **allowed** by curation policy. Proceed to download - via a remote repo (same as Step 6b). -- **403** → package is **blocked** by a curation policy. The response body - explains which policy rule blocked it. Report the block reason to the user - and stop — do not attempt to download. +- **exit 0** → package is **allowed** by curation policy. Proceed to + download via a remote repo (same as Step 6b). +- **`returned 403` on stderr** → package is **blocked** by a curation + policy. The response body explains which policy rule blocked it. Report + the block reason to the user and stop — do not attempt to download. +- **Any other non-zero exit** → treat as an operational failure (auth, DNS, + endpoint disabled) and report. ## Step 6b: Download without curation -When curation is not entitled and the package is not in the Jfrog Platform, +When curation is not entitled and the package is not in the JFrog Platform, download directly through a remote repo. 1. **Find a remote repo** of the right package type: ```bash - jf rt curl -s -XGET "/api/repositories?type=remote&packageType=" \ + jf api \ + "/artifactory/api/repositories?type=remote&packageType=" \ | jq '.[].key' ``` -2. **Download:** +2. **Download** — use `jf rt dl` against the base remote repo (without + `-cache`); it handles both cached and uncached artifacts: ```bash - jf rt dl "-cache/" --flat + jf rt dl "/" --flat ``` - If 0 results (not cached), fetch through the remote proxy: - - ```bash - jf rt curl -s -L -XGET "/api///" \ - -o - ``` +## Artifact paths by package type -## Protocol endpoints by package type +Use these path patterns when `leadArtifactPath` is not available from +OneModel. The leading `/` is the base repo key you pass to `jf rt dl`. -| Type | Protocol prefix | Artifact path pattern | -|------|----------------|----------------------| -| `npm` | `/api/npm/` | `/-/-.tgz` | -| `pypi` | `/api/pypi//packages` | `//-.tar.gz` | -| `maven` | `/` | `///-.jar` | -| `go` | `/api/go/` | `/@v/.zip` | +| Type | `jf rt dl` target pattern | +|--------|-------------------------------------------------------------------------| +| `npm` | `//-/-.tgz` | +| `pypi` | `///-.tar.gz` | +| `maven`| `////-.jar` | +| `go` | `//@v/.zip` | ## Gotchas -- **`jf rt dl` and remote repos**: `jf rt dl` only finds artifacts already - present in the `-cache` repo. For `remote` repository types, Step 4 - instructs to skip `jf rt dl` entirely and use the proxy endpoint directly, - avoiding a wasted round-trip for uncached packages. -- **Redirects**: `jf rt curl` does not follow HTTP redirects by default. - Always pass `-L` when downloading binary artifacts through remote repo - proxy endpoints. +- **Binary downloads vs. `jf api`**: `jf api` is for REST APIs, not binary + content. It does not follow redirects transparently into a binary payload + and does not expose `-L` / `-o`. Always use `jf rt dl` (against the base + remote repo, not the `-cache` one) for the actual artifact download. +- **`jf rt dl` and uncached remotes**: `jf rt dl "/"` — + targeting the **base** remote repo rather than `-cache/` — + transparently triggers the remote fetch and caches the artifact. Do not + try to pre-query the proxy via `jf api`. - **`jf rt dl --flat` target must be a file path**: When downloading a single artifact, pass a full output **file** path (e.g. `./downloads/lodash-4.18.1.tgz`), not a directory. The CLI opens the target @@ -255,8 +273,14 @@ download directly through a remote repo. - **Package type detection**: If the user doesn't specify the package type, the Public Catalog search by name alone may return multiple types. Ask the user to disambiguate before proceeding. -- **Curation API uses `jf xr curl`**: The curation package status endpoint - is under Xray, not Artifactory. Use `jf xr curl`, not `jf rt curl`. +- **Curation endpoint lives under Xray**: use + `/xray/api/v1/curation/package_status/all_repos` (via `jf api`). Do not + prefix it with `/artifactory`. +- **Curation result discrimination with `jf api`**: the 200/403 signal comes + from `jf api`'s **exit code** plus a `returned NNN` line on **stderr**, + not from a `%{http_code}` appended to stdout. Capture stderr to a file + (`2> "$STDERR_FILE"`) and branch on `RC` + `grep 'returned 403'` as shown + in Step 6a. - **Curation API package type values**: Must be lowercase and match one of `npm`, `pypi`, `maven`, `go`, `nuget`, `docker`, `gradle`. Other values will return an error. diff --git a/skills/jfrog/.gitignore b/skills/jfrog/.gitignore deleted file mode 100644 index 3215b96..0000000 --- a/skills/jfrog/.gitignore +++ /dev/null @@ -1 +0,0 @@ -local-cache/ diff --git a/skills/jfrog/SKILL.md b/skills/jfrog/SKILL.md index 71b3bc6..67e0391 100644 --- a/skills/jfrog/SKILL.md +++ b/skills/jfrog/SKILL.md @@ -1,8 +1,7 @@ --- name: jfrog -version: "0.3.0" description: >- - Interact with the JFrog Platform via the JFrog CLI and REST/GraphQL APIs. + Interact with the JFrog Platform via the JFrog CLI, JFrog MCP server and REST/GraphQL APIs. Use this skill when the user wants to manage Artifactory repositories, upload or download artifacts, manage builds, configure permissions, manage users and groups, work with access tokens, configure JFrog CLI @@ -15,93 +14,122 @@ description: >- evidence, apptrust, onemodel, graphql, workers, mission control, curation, advanced security, exposures, or any JFrog product name. compatibility: >- - Requires curl and jq on PATH. + Requires jq on PATH. metadata: role: base + version: "0.11.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. -Interact with the JFrog Platform through the JFrog CLI (`jf`) and, where the -CLI falls short, through REST APIs and GraphQL. In code examples below, +Interact with the JFrog Platform through three tool tiers — see +[Tool selection strategy](#tool-selection-strategy). 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. +## Tool selection strategy + +Try the tiers in order; move to the next only when the current does not +cover the operation or fails: + +1. **JFrog MCP tools** (preferred): `CallMcpTool` against the JFrog MCP + server. Discover available tools from the server's tool list; never + guess tool names. +2. **`jf` CLI subcommands** (fallback): dedicated commands such as + `jf rt upload`, `jf rt dl`, `jf build-publish`. +3. **`jf api`** (last resort): REST/GraphQL endpoints with no dedicated + 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. + ## Prerequisites The following tools must be available on `PATH`: | Tool | Purpose | |------|---------| -| `curl` | HTTP requests to JFrog REST and GraphQL APIs | | `jq` | JSON parsing of CLI and API output | -## Environment check +All JFrog HTTP traffic from Tiers 2 and 3 goes through the `jf` CLI itself +(`jf api`, see [Invoking platform APIs with `jf api`](#invoking-platform-apis-with-jf-api) below) — +no standalone `curl` is required for any JFrog interaction. -Before your first JFrog operation in a session, run the environment check. -It verifies the CLI is installed, checks for updates, and exports -`JFROG_CLI_USER_AGENT` so every outbound request is identifiable: +**Runtime permission for JFrog calls.** All `jf` calls that touch the network +need an outbound-HTTPS escalation from the agent runtime. The `~/.jfrog/` +credential save (`jf config add` during login) additionally needs a +filesystem-write escalation. -```bash -eval "$(JFROG_SKILL_MODEL="" bash /scripts/check-environment.sh)" -``` +| Runtime | Network | Network + `~/.jfrog/` write | +| ----------- | --------------------------------------------- | ------------------------------- | +| Cursor | `required_permissions: ["full_network"]` | `required_permissions: ["all"]` | +| Claude Code | `allowed-tools: Bash(jf:*)` + host allowlist | same + filesystem allowlist | +| Other | Configure at the runtime/sandbox layer | same | -Set `JFROG_SKILL_MODEL` to the model slug you are running as (e.g. -`opus-4.6`, `sonnet-4`). The script appends it to the user agent string. +If `jf` exits 1 with empty output, the runtime's network gate is the first +thing to check — re-run with the appropriate escalation above. -The `eval` is required — the script outputs -`export JFROG_CLI_USER_AGENT='model/ jfrog-skills/ jfrog-cli-go/'` -on stdout. The JFrog CLI picks this up natively and injects it as the -`User-Agent` header on every HTTP request. JSON state is printed to stderr -for informational purposes (also written to the cache file). +## Environment check -The script uses a 24-hour cache at `/local-cache/jfrog-skill-state.json`. If the -cache is fresh, it returns immediately. If stale or missing, it checks whether -`jf` is installed, its version, and whether a newer version is available. +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 `` for the rest of the +session: -- Exit 0: cache is fresh, CLI is ready -- Exit 1: cache was stale and has been refreshed, CLI is ready -- Exit 2: `jf` is not installed +```bash +bash /scripts/check-environment.sh +# stdout (one line): jfrog-skills/ [(tool=; model=)] jfrog-cli-go/ +# stderr: JSON state (cached 24h at ${JFROG_CLI_HOME_DIR:-$HOME/.jfrog}/skills-cache/jfrog-skill-state.json) +``` -Bypass the cache only when the user explicitly asks to install, upgrade, or -reconfigure the CLI. +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`. -If the CLI is missing (exit 2) or an upgrade is needed, read -`references/jfrog-cli-install-upgrade.md` for install and upgrade instructions. +### Export `JFROG_CLI_USER_AGENT` once per bash invocation -### JSON parsing (`jq`) +At the top of every bash invocation that runs `jf`, export `` once; +all `jf` calls in that invocation pick it up: -Use **`jq`** for all JSON parsing of CLI and API output (pipes, `-r`, filters). -Examples: `base64 -d | jq -r '.url'`, `jf rt curl ... | jq '.[] | .key'`. +```bash +export JFROG_CLI_USER_AGENT='' +jf config show +jf api /artifactory/api/system/version +``` -## Network permissions +Do **not** repeat the assignment per `jf` call (`JFROG_CLI_USER_AGENT='' 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 `` in its prompt; subagents do not re-run the script. -JFrog servers are not on the default sandbox network allowlist. Every Shell -call that contacts a JFrog server — whether via `jf` CLI, `jf rt curl`, -`jf xr curl`, or plain `curl` — requires `required_permissions: ["full_network"]`. +| Exit | Meaning | +|------|---------| +| 0 | Cache fresh — CLI ready (Tiers 2 and 3 available), proceed | +| 1 | Cache refreshed — CLI ready (Tiers 2 and 3 available), proceed | +| 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 | -Without this permission, commands fail silently: `jf` exits with code 1 and -empty output, `curl` returns an empty response, and downstream JSON parsing -crashes. All JFrog operations that touch the network need this permission. +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, +restricted environment), proceed with MCP (Tier 1) only. Both `jf` CLI commands +(Tier 2) and `jf api` (Tier 3) require a working `jf` installation. -### Agent execution environments +### JSON parsing (`jq`) -`check-environment.sh` does **not** call your JFrog server, but it may make an -outbound request to `releases.jfrog.io` for version checking and may **write** -`/local-cache/jfrog-skill-state.json` when the cache is stale. In a **sandboxed** -agent environment, **`full_network` alone may not suffice**: if the workspace -cannot be written, the check can fail before any JFrog call. Request -permissions that allow writing `/local-cache` (or run -outside a restrictive sandbox) when you see filesystem errors from the -environment check. +Use **`jq`** for all JSON parsing of CLI and API output (pipes, `-r`, filters). -### `local-cache/` — allowed files only +## `~/.jfrog/skills-cache/` — allowed files only -`/local-cache/` is **not** a general scratch or temp directory. Use -it **only** for these two artifacts: +`${JFROG_CLI_HOME_DIR:-$HOME/.jfrog}/skills-cache/` is **not** a general scratch +or temp directory. Use it **only** for these two artifacts: 1. **`jfrog-skill-state.json`** — written by `scripts/check-environment.sh` (24-hour CLI check cache). @@ -109,51 +137,157 @@ it **only** for these two artifacts: schema (see `references/onemodel-graphql.md`). **Do not** save HTTP response bodies, GraphQL query results, ad-hoc JSON, reports, -or any other temporary files under `local-cache/`. Write those to a host temp +or any other temporary files under `skills-cache/`. Write those to a host temp path instead (for example `/tmp/-$$.json` or `mktemp -d`), echo the path when a follow-up Shell step must read the file — same pattern as *Preserving command output* below. -Apply `full_network` on the **first** Shell call that hits JFrog. Once -granted for a session, the agent environment typically retains it for -subsequent calls, but always include it explicitly to avoid silent failures. - -## Server management - -Server configuration is always read live from `jf config` (never cached). - -- **List servers**: `jf config show` (local operation, no network needed) -- **Use a specific server**: pass `--server-id ` to any command -- **Switch default**: `jf config use ` -- **Add a new server**: read `references/jfrog-login-flow.md` for the full - login procedure (web login or manual token setup) - -### Server selection rules (mandatory) - -Exactly one server (or an explicit set of servers) must be resolved before any -operation. The rules are strict and apply to every CLI command, API call, and -subagent prompt: - -1. **User named specific server(s)** — use those and only those. Pass - `--server-id ` (CLI) or the matching server-id to - `get-platform-credentials.sh` (REST). Do not touch any other configured - server. -2. **User did not name a server** — use the current default server and only - it. Determine the default via `jf config show` (the entry marked as - default). If no default is set, stop and ask the user which server to use. -3. **Verify before executing** — after resolving the server, confirm it - exists in `jf config show` output before running any command against it. - If the server-id is not listed, stop and tell the user. - -Do not fall back to a different server. Silently switching servers is -dangerous because different servers hold different data, permissions, and -configurations — an operation that succeeds on the wrong server can corrupt -state, leak data across environments, or produce results the user cannot -reproduce. If the resolved server produces any error — does not exist in -`jf config`, authentication failure (401/403), network error, connection -refused, or any other failure — stop immediately and report the error to the -user. Do not try other configured servers, do not iterate through the server -list, and do not silently switch servers. Ask the user how to proceed. +## Cautious execution + +Do not run commands speculatively. Before executing 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. + "builds" could mean Artifactory build-info or CI/CD pipeline runs), + **ask the user for clarification** instead of guessing. Never fetch data + from the wrong system — a wrong answer is worse than asking a question. +2. Resolve the target server using the **Server selection rules** below — + there must be no ambiguity about which server is used +3. For mutating operations (create, update, delete, upload), confirm with the + user unless the intent is clearly implied. This applies to all tiers + (MCP tools, CLI commands, and `jf api` with POST/PUT/DELETE). +4. Prefer read operations first to understand current state before making changes +5. **Never invent preparatory mutations.** If the requested operation fails + because a precondition is not met (artifact missing from the specified repo, + 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 + can have cascading effects the user has not considered — virtual repository + resolution changes, storage quota consumption, replication triggers, Xray + re-indexing, or permission propagation. +6. **Never guess tool names or API paths.** For MCP tools, confirm the tool + exists in the server's tool list. For `jf api` paths, validate against + `/references/` (or + [JFrog OpenAPI specifications](https://docs.jfrog.com/integrations/docs/openapi-specifications) + if you have web access). On a 404, stop and report — never retry with a guessed + alternative path. + +## Server selection rules (mandatory) + +**Single-server invariant.** Every `jf` call MUST pass `--server-id ` +(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. + +**JFrog MCP and CLI use independent auth.** MCP tools authenticate through +the MCP server session (not `jf config`); CLI commands authenticate through +`jf config`. If you switch the CLI target server via `jf config use`, the +MCP connection still points to its original server. Do not mix MCP and CLI +calls targeting different servers in the same session. If the user asks to +switch servers, warn that MCP tools will continue to target the original +server until the MCP connection is re-established. + +**MUST NOT** retry on a second configured server after 401/403/404, empty, or +partial results; **MUST NOT** infer multi-server intent from "my"/"our" or +from seeing extra entries in `jf config show`. **Override:** only when the user +**explicitly** names another id ("on ``, …", "use ``", "compare `` +and ``") — inferred intent is not an override. + +### Resolve the default once per session + +Before your first `jf` call, resolve the default server-id and **remember it** +as `` for the rest of the session, same pattern as ``: + +```bash +jf config show 2>/dev/null \ + | awk '/^Server ID:/{id=$NF} /^Default:[[:space:]]*true/{print id; exit}' +# stdout: the default server-id; if empty, stop and ask which to use +``` + +Pass `--server-id ` to every subsequent `jf` call. The flag goes +**after** the subcommand name, not after `jf` itself: + +- ✅ `jf api --server-id /artifactory/api/system/version` +- ✅ `jf rt ping --server-id ` +- ❌ `jf --server-id api /…` — fails with `flag provided but not defined` + +When launching a subagent, pass `` in its prompt — subagents do not +re-resolve. Examples elsewhere in this skill and in `references/*.md` omit +`--server-id` for readability; the rule is global, same as +`JFROG_CLI_USER_AGENT`. To add a new server, read +`references/jfrog-login-flow.md`. + +### On any error, stop — never switch + +If a `jf` call returns 401/403, 404, network error, timeout, or any other +failure, **stop with no further `jf` calls** and respond: + +> `` returned `` for ``: ``. Other +> configured server(s): `` — 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` + +### Artifactory + +- **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//?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) + +### Xray & security + +- **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 + +### Release lifecycle & distribution + +- **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 + +- **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` ## Command discovery @@ -194,80 +328,67 @@ Top-level security commands: `audit`, `scan`, `build-scan`, `curation-audit`, Top-level other: `access-token-create` (`atc`), `login`, `how`, `stats`, `generate-summary-markdown`, `exchange-oidc-token`, `completion`. -## Artifactory operations - -Artifactory resources are managed through the `jf rt` namespace — repos, files, -builds, permissions, users/groups, and replication. Read -`references/artifactory-operations.md` when performing any of these operations. +## Invoking platform APIs with `jf api` -## Platform administration +`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`. -Access tokens, login, stats, projects, and system health. Read -`references/platform-admin-operations.md` when performing any of these -operations. +### Product-prefix table -## Falling back to REST APIs +`jf api` requires the **full** path including the product prefix; omitting it +returns 404. -When the CLI does not support an operation, use REST APIs. All commands in this -section require `required_permissions: ["full_network"]` (see Network -permissions above). Read `references/jfrog-credential-patterns.md` for detailed -patterns. +| 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) | -### Tier 1: Artifactory (`jf rt curl`) +### Examples -Handles authentication automatically: ```bash -jf rt curl -XGET /api/repositories -jf rt curl -XGET "/api/storage//?properties" -jf rt curl -XPOST /api/search/aql -H "Content-Type: text/plain" -d '' -``` - -### Tier 2: Xray (`jf xr curl`) +jf api /artifactory/api/repositories +jf api --server-id /artifactory/api/system/version -Handles authentication automatically: -```bash -jf xr curl -XGET /api/v2/watches -jf xr curl -XGET /api/v2/policies +# AQL (POST with text/plain body) +jf api /artifactory/api/search/aql \ + -X POST -H "Content-Type: text/plain" -d '' ``` -### Tier 3: Other products (plain curl) - -Extract credentials using the helper script: -```bash -eval "$(bash /scripts/get-platform-credentials.sh [server-id])" -curl -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" "$JFROG_URL/access/api/v2/users/" -``` +Common flags: `-X/--method`, `-H/--header`, `-d/--data`, `--input `, +`--server-id`, `--timeout`. Body on stdout, status on stderr — see +[Gotchas](#gotchas). ### GraphQL (OneModel) -OneModel is the unified GraphQL API on the platform base URL. **Do not** embed -the query string inside a JSON literal (`-d '{"query":"..."}'`) — GraphQL uses -many double quotes and manual escaping breaks requests. Use **`jq -n --arg`** -to build the payload, and **save the HTTP response to a file** with `curl -o` -before running `jq` (same principle as *Preserving command output* below). +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 -eval "$(bash /scripts/get-platform-credentials.sh [server-id])" -QUERY='{ evidence { searchEvidence(first: 5, where: { hasSubjectWith: { repositoryKey: "my-repo-local" } } }) { totalCount } } }' -PAYLOAD=$(jq -n --arg q "$QUERY" '{"query": $q}') -RESPONSE_FILE="/tmp/onemodel-$$.json" -curl -s -X POST "$JFROG_URL/onemodel/api/v1/graphql" \ - -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - -H "Content-Type: application/json" \ - -d "$PAYLOAD" \ - -o "$RESPONSE_FILE" -jq . "$RESPONSE_FILE" -echo "$RESPONSE_FILE" +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: `GET $JFROG_URL/onemodel/api/v1/supergraph/schema` (schema file -only under `/local-cache/` per `references/onemodel-graphql.md` and -**`local-cache/` — allowed files only** above — not for query responses). - -Read **`references/onemodel-graphql.md`** for the full workflow (mandatory schema -fetch, validation, pagination, errors, playground). Read -**`references/onemodel-query-examples.md`** for domain-specific query shapes and -**`references/onemodel-common-patterns.md`** for pagination, variables, and dates. +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. ## Structured inputs @@ -277,7 +398,7 @@ which agents cannot use. Instead, retrieve an existing config via REST API as a starting point and modify it: ```bash -jf rt curl -XGET /api/repositories/ +jf api /artifactory/api/repositories/ ``` For other Artifactory or platform REST patterns, or when you need more than @@ -285,24 +406,37 @@ this repo GET, see **Any API gap** under [When to read reference files](#when-to ## Gotchas -- JFrog network calls require `required_permissions: ["full_network"]` in the - Shell tool. Without it, commands fail silently with empty output. The - environment check does **not** call your JFrog server (it may contact - `releases.jfrog.io` for version checking), but it may need **workspace - write** access for its cache file (see [Agent execution environments](#agent-execution-environments)). -- `jf rt curl` only works for Artifactory. `jf xr curl` only works for Xray. - For other products, use plain curl with extracted credentials. +### 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: returned NNN`. Pipe stdout to + `jq` directly; **never `2>&1 | jq`** — stderr corrupts the JSON. To keep + diagnostics: `jf api 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/` 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 rt curl -XPOST /api/search/aql`. See `references/artifactory-aql-syntax.md`. + `jf api /artifactory/api/search/aql -X POST -H "Content-Type: text/plain" -d ''`. + 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 curl`, `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`. + 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). @@ -314,18 +448,16 @@ this repo GET, see **Any API gap** under [When to read reference files](#when-to 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. -- Always use `jf rt curl -s` (silent flag) when piping output to `jq` or - redirecting to a file. Without `-s`, curl's progress meter is mixed into - stdout and breaks JSON parsing. - 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 REST API call returns 401, the access token may have expired — - re-extract credentials with `get-platform-credentials.sh` for the **same** - server. If 403, the token lacks required permissions. If 404, verify the - endpoint path 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. +- 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 @@ -347,29 +479,6 @@ this repo GET, see **Any API gap** under [When to read reference files](#when-to — read when debugging odd failures; **append** a short entry when you confirm a new, reusable gotcha. -## Cautious execution - -Do not run commands speculatively. Before executing any JFrog CLI command or -API call: - -1. Confirm the operation is needed to fulfill the user's request -2. Resolve the target server using the **Server selection rules** above — - there must be no ambiguity about which server is used -3. For mutating operations (create, update, delete, upload), confirm with the - user unless the intent is clearly implied -4. Prefer read operations first to understand current state before making changes -5. If any command fails with a server-level error (not found, auth, network), - stop and ask the user — never retry against a different server -6. **Never invent preparatory mutations.** If the requested operation fails - because a precondition is not met (artifact missing from the specified repo, - 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 - can have cascading effects the user has not considered — virtual repository - resolution changes, storage quota consumption, replication triggers, Xray - re-indexing, or permission propagation. - ## Batch and parallel execution When a task requires multiple independent operations, use the lightest @@ -385,7 +494,7 @@ file so you can re-read it without re-executing the call: ```bash OUT=/tmp/jf-repos-$$.json -jf rt curl -XGET /api/repositories > "$OUT" +jf api /artifactory/api/repositories > "$OUT" echo "$OUT" ``` @@ -413,72 +522,8 @@ 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 '' /tmp/jf-*-$$.json` (or redirect stdin from the file) instead -of re-running the same `jf rt curl`, `jf xr curl`, Tier 3 `curl`, or other -identical network-backed command. +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. - -## 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` - -### Artifactory - -- **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**: read `references/artifactory-operations.md` (for **listing builds** with a known project key: REST `GET /api/build?project=`, then `GET /api/build/?project=` — see § *Listing builds when the project key is known*) -- **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) - -### Xray & security - -- **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 - -### Release lifecycle & distribution - -- **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)**: read `references/catalog-entities.md` (~190 lines) -- **CVE details, vulnerability lookup by CVE ID, or severity/affected-packages/fix-versions for a specific CVE**: go directly to `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) -- **Credential extraction for products beyond Artifactory and Xray**: read `references/jfrog-credential-patterns.md` (~155 lines; includes **Response handling** for any network-backed response body: fetch once to a temp file, then `jq` the file — plain `curl` or `jf rt curl` / `jf xr curl` alike) - -### 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 - -- **Batching, parallel Shell calls, or launching subagents**: read `references/general-parallel-execution.md` (~135 lines) -- **Large or parallel data gathering, list-vs-detail APIs, sandbox/cache issues**: 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/artifactory-api-gaps.md b/skills/jfrog/references/artifactory-api-gaps.md index 02e4c12..1533d8a 100644 --- a/skills/jfrog/references/artifactory-api-gaps.md +++ b/skills/jfrog/references/artifactory-api-gaps.md @@ -1,37 +1,50 @@ # Artifactory API Gaps Operations available through REST API but not through CLI commands. -Use `jf rt curl` for all of these (handles authentication automatically). +Invoke them via `jf api [flags]` (authentication is handled +automatically against the active `jf config` server; see the base skill's +*Invoking platform APIs with `jf api`* section). ## Repository management ### Get repository configuration ```bash -jf rt curl -XGET /api/repositories/ +jf api /artifactory/api/repositories/ ``` Returns the full JSON configuration of a repository. Useful as a template for creating similar repos. ### List all repositories ```bash -jf rt curl -XGET /api/repositories -# Filter by type, package type, and/or project (all combinable) -jf rt curl -XGET "/api/repositories?type=local" -jf rt curl -XGET "/api/repositories?type=remote" -jf rt curl -XGET "/api/repositories?type=virtual" -jf rt curl -XGET "/api/repositories?packageType=docker" -jf rt curl -XGET "/api/repositories?project=myproj" -jf rt curl -XGET "/api/repositories?project=myproj&type=local&packageType=docker" +jf api /artifactory/api/repositories +``` +Optional query params (combinable): `type` (one of `local`, `remote`, +`virtual`, `federated`), `packageType` (e.g. `docker`, `maven`, `npm`, +`pypi`, `generic`), `project`. Examples: +```bash +jf api "/artifactory/api/repositories?type=local" +jf api "/artifactory/api/repositories?packageType=docker" +jf api "/artifactory/api/repositories?type=remote&packageType=maven&project=my-project" ``` ### Get repositories (v2) ```bash -jf rt curl -XGET "/api/repositories/configurations?repo_type=LOCAL&package_type=maven" +jf api /artifactory/api/repositories/configurations +``` +Optional query params (combinable, comma-separated values allowed): +`repoType` (case-insensitive; one of `local`, `remote`, `virtual`, +`federated`) and `packageType` (e.g. `maven`, `docker`, `npm`). Note: +`repo_type` is silently ignored — the correct name is `repoType`. +Examples: +```bash +jf api "/artifactory/api/repositories/configurations?repoType=local" +jf api "/artifactory/api/repositories/configurations?packageType=maven" +jf api "/artifactory/api/repositories/configurations?repoType=local,remote&packageType=docker" ``` ### Check if repository exists ```bash -jf rt curl -XHEAD /api/repositories/ +jf api /artifactory/api/repositories/ -X HEAD # 200 = exists, 400 = does not exist ``` @@ -39,178 +52,155 @@ jf rt curl -XHEAD /api/repositories/ ### Get storage summary ```bash -jf rt curl -XGET /api/storageinfo +jf api /artifactory/api/storageinfo ``` ### Refresh storage summary ```bash -jf rt curl -XPOST /api/storageinfo/calculate +jf api /artifactory/api/storageinfo/calculate -X POST ``` ### Get storage item info ```bash -jf rt curl -XGET "/api/storage//" +jf api "/artifactory/api/storage//" ``` ### System ping ```bash -jf rt curl -XGET /api/system/ping +jf api /artifactory/api/system/ping ``` ### System version ```bash -jf rt curl -XGET /api/system/version +jf api /artifactory/api/system/version ``` ### System configuration ```bash -jf rt curl -XGET /api/system/configuration +jf api /artifactory/api/system/configuration ``` ## Search (beyond CLI) ### AQL queries ```bash -jf rt curl -XPOST /api/search/aql \ - -H "Content-Type: text/plain" \ +jf api /artifactory/api/search/aql \ + -X POST -H "Content-Type: text/plain" \ -d 'items.find({"repo":"my-repo","name":{"$match":"*.jar"}})' ``` For remote repository content, query the `-cache` suffixed repo: ```bash -jf rt curl -XPOST /api/search/aql \ - -H "Content-Type: text/plain" \ +jf api /artifactory/api/search/aql \ + -X POST -H "Content-Type: text/plain" \ -d 'items.find({"repo":"my-remote-cache"})' ``` ### Property search ```bash -jf rt curl -XGET "/api/search/prop?key=value&repos=my-repo" +jf api "/artifactory/api/search/prop?key=value&repos=my-repo" ``` ### Checksum search ```bash -jf rt curl -XGET "/api/search/checksum?sha256=" +jf api "/artifactory/api/search/checksum?sha256=" ``` ### GAVC search (Maven) ```bash -jf rt curl -XGET "/api/search/gavc?g=com.example&a=mylib&v=1.0" -``` - -## User management (beyond CLI) - -These Access API endpoints are routed through Artifactory's auth proxy via -`jf rt curl`. The same endpoints can also be reached with plain `curl` and -extracted credentials — see `platform-admin-api-gaps.md` (Users section). - -### Get user details -```bash -jf rt curl -XGET /access/api/v2/users/ -``` - -### Update user -```bash -jf rt curl -XPATCH /access/api/v2/users/ \ - -H "Content-Type: application/json" \ - -d '{"email": "new@example.com"}' +jf api "/artifactory/api/search/gavc?g=com.example&a=mylib&v=1.0" ``` -### List all users -```bash -jf rt curl -XGET /access/api/v2/users/ -``` +## User and group management -### Get group details -```bash -jf rt curl -XGET /access/api/v2/groups/ -``` +User and group operations are handled by the Access service. See +`platform-admin-api-gaps.md` (Users / Groups sections) for the full set. ## Metadata calculation Trigger metadata recalculation for various package types: ```bash # Maven -jf rt curl -XPOST /api/maven/calculateMetaData/ +jf api /artifactory/api/maven/calculateMetaData/ -X POST # npm -jf rt curl -XPOST /api/npm//reindex +jf api /artifactory/api/npm//reindex -X POST # Docker # (automatic, no manual trigger) # PyPI -jf rt curl -XPOST /api/pypi//reindex +jf api /artifactory/api/pypi//reindex -X POST # Helm -jf rt curl -XPOST /api/helm//reindex +jf api /artifactory/api/helm//reindex -X POST # Debian -jf rt curl -XPOST /api/deb/reindex/ +jf api /artifactory/api/deb/reindex/ -X POST ``` ## Trash can and garbage collection ### Empty trash ```bash -jf rt curl -XPOST /api/trash/empty +jf api /artifactory/api/trash/empty -X POST ``` ### Restore from trash ```bash -jf rt curl -XPOST "/api/trash/restore//" +jf api "/artifactory/api/trash/restore//" -X POST ``` ### Run garbage collection ```bash -jf rt curl -XPOST /api/system/storage/gc +jf api /artifactory/api/system/storage/gc -X POST ``` ## Federated repositories (beyond basic CRUD) ### Get federation status ```bash -jf rt curl -XGET /api/federation/status/ +jf api /artifactory/api/federation/status/ ``` ### Trigger full sync ```bash -jf rt curl -XPOST "/api/federation/fullSyncAll/" +jf api "/artifactory/api/federation/fullSyncAll/" -X POST ``` ## Build info (beyond CLI) ### List builds (prefer scoped queries) -**Unscoped** `GET /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` (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*. ```bash # Project scope — build names (latest per name) -jf rt curl -XGET "/api/build?project=" +jf api "/artifactory/api/build?project=" # Project scope — all run numbers for one build name (response: buildsNumbers) -jf rt curl -XGET "/api/build/?project=" +jf api "/artifactory/api/build/?project=" # Build-info repo scope — alternative when you know the repo key -jf rt curl -XGET "/api/build?buildRepo=" +jf api "/artifactory/api/build?buildRepo=" ``` ### Get build info ```bash # Default build-info repo only (no project / non-default repo) -jf rt curl -XGET "/api/build//" +jf api "/artifactory/api/build//" # Project or custom build-info repo -jf rt curl -XGET "/api/build//?project=" -jf rt curl -XGET "/api/build//?buildRepo=" +jf api "/artifactory/api/build//?project=" +jf api "/artifactory/api/build//?buildRepo=" ``` ### Delete builds ```bash -jf rt curl -XPOST /api/build/delete \ - -H "Content-Type: application/json" \ +jf api /artifactory/api/build/delete \ + -X POST -H "Content-Type: application/json" \ -d '{"buildName":"my-build","buildNumbers":["1","2"]}' ``` diff --git a/skills/jfrog/references/artifactory-aql-syntax.md b/skills/jfrog/references/artifactory-aql-syntax.md index 937f83c..b120275 100644 --- a/skills/jfrog/references/artifactory-aql-syntax.md +++ b/skills/jfrog/references/artifactory-aql-syntax.md @@ -3,7 +3,8 @@ AQL queries are sent as POST requests with `Content-Type: text/plain`: ```bash -jf rt curl -XPOST /api/search/aql -H "Content-Type: text/plain" -d '' +jf api /artifactory/api/search/aql \ + -X POST -H "Content-Type: text/plain" -d '' ``` ## Query structure @@ -518,7 +519,7 @@ items.find({"repo":"docker-local","path":{"$match":"my-image/*"},"name":"manifes under `//`. Use the V2 manifest API (returns `layers[].size`): ```bash -jf rt curl -s -XGET "/api/docker//v2//manifests/" \ +jf api "/artifactory/api/docker//v2//manifests/" \ -H "Accept: application/vnd.docker.distribution.manifest.v2+json" ``` @@ -537,7 +538,8 @@ Zero-download items lack a stats row — filter client-side instead (see [Gotchas](#gotchas)): ```bash -jf rt curl -s -XPOST /api/search/aql -H "Content-Type: text/plain" -d ' +jf api /artifactory/api/search/aql \ + -X POST -H "Content-Type: text/plain" -d ' items.find({"repo":"my-repo","type":"file"}) .include("repo","path","name","size","stat.downloads") ' | jq '[.results[] | select((.stats[0].downloads // 0) == 0) | {repo, path, name, size}]' diff --git a/skills/jfrog/references/artifactory-operations.md b/skills/jfrog/references/artifactory-operations.md index 84a39e6..bab2b65 100644 --- a/skills/jfrog/references/artifactory-operations.md +++ b/skills/jfrog/references/artifactory-operations.md @@ -7,15 +7,16 @@ namespace. Run `jf rt --help` to discover subcommands not listed here. Repositories are created from JSON templates. The workflow is: -1. Get a template: retrieve an existing repo config - via `jf rt curl -XGET /api/repositories/` and modify it, or - craft JSON manually. +1. Get a template: retrieve an existing repo config via + `jf api /artifactory/api/repositories/` + and modify it, or craft JSON manually. Note: `jf rt repo-template` is interactive and cannot be used by agents. 2. Create: `jf rt repo-create ` 3. Update: `jf rt repo-update ` 4. Delete: `jf rt repo-delete --quiet` -To list repositories, use: `jf rt curl -XGET /api/repositories` +To list repositories, use: +`jf api /artifactory/api/repositories` ## File operations @@ -38,8 +39,8 @@ Use a direct AQL query with `name` and `path` criteria instead — omitting the `repo` field searches all accessible repos via indexed columns: ```bash -jf rt curl -s -XPOST /api/search/aql \ - -H "Content-Type: text/plain" \ +jf api /artifactory/api/search/aql \ + -X POST -H "Content-Type: text/plain" \ -d 'items.find({ "name":"", "path":"" @@ -51,6 +52,17 @@ narrow the search further. ## Build info +**Project scoping rule:** Append `?project=` 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":"-build-info"` (or `"repo":"artifactory-build-info"` for +the default project). + +**Server rule:** A 404 from a `?project=` 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*. + ### Publishing builds - Collect env: `jf rt build-collect-env ` @@ -59,84 +71,80 @@ narrow the search further. - Promote: `jf rt build-promote ` - Discard: `jf rt build-discard ` -### Retrieving build info +### Listing build names -The build detail API (`GET /api/build/{name}/{number}`) returns 404 when the -build is stored in a non-default build-info repo or belongs to a JFrog -Project. **Always resolve the scope before calling the build API:** +**Do not use `GET /api/build`** — it has no pagination and times out on large +instances. Always use AQL with `limit` and `offset`. -1. If the user provided a project key or build-info repo, use it directly. -2. If you need to **list** build names or run numbers and you have a **project - key**, follow [Listing builds when the project key is known](#listing-builds-when-the-project-key-is-known) (REST first — do not jump to AQL). -3. If the project key and build-info repo are still unknown, discover scope - via AQL (see [Discovering build scope without a project key](#discovering-build-scope-without-a-project-key) below). -4. For **detail**, use a scoped detail GET — never call `GET /api/build//` without `?project=` or `?buildRepo=` when the build requires it. +**All builds** (no project scope): ```bash -jf rt curl -s -XGET "/api/build//?buildRepo=" -jf rt curl -s -XGET "/api/build//?project=" +jf api /artifactory/api/search/aql \ + -X POST -H "Content-Type: text/plain" \ + -d 'builds.find().include("name","number","repo","created").sort({"$desc":["created"]}).offset(0).limit(100)' ``` -Scope parameters: - -- `?buildRepo=` — when the build info is stored in a - non-default build-info repository (anything other than - `artifactory-build-info`) -- `?project=` — when the build belongs to a JFrog Project - -### Listing builds when the project key is known +**Project-scoped** — filter by the project's build-info repository +(`-build-info`, or `artifactory-build-info` for the default +project): -When you have a **project key**, use this REST sequence before AQL. It scopes -the server’s work and avoids **unscoped** listing pitfalls (see below). +```bash +jf api /artifactory/api/search/aql \ + -X POST -H "Content-Type: text/plain" \ + -d 'builds.find({"repo":"-build-info"}).include("name","number","repo","created").sort({"$desc":["created"]}).offset(0).limit(100)' +``` -1. **Build names** (one row per logical build): - `GET /api/build?project=` - Response includes `builds[]` with `uri` (path suffix per name) and - `lastStarted` (latest run for that name). +**Pagination:** The response includes a `range` object with `total` (total +matching records). If `total` exceeds the `limit`, tell the user: *"Showing +first 100 of N results (paginated). Ask for the next batch if needed."* +For subsequent pages, increment `offset` by 100. -2. **Run numbers for one name**: - `GET /api/build/?project=` - Response uses the field **`buildsNumbers`** (exact spelling from the API); - each entry has `uri` (e.g. `/33`) and `started`. The same number may appear - more than once with different `started` values — do not assume uniqueness - by number alone. +**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. -3. **Full build info** (unchanged): - `GET /api/build//?project=` +### Listing runs of a specific build ```bash -jf rt curl -s -XGET "/api/build?project=" -jf rt curl -s -XGET "/api/build/?project=" -jf rt curl -s -XGET "/api/build//?project=" +jf api /artifactory/api/search/aql \ + -X POST -H "Content-Type: text/plain" \ + -d 'builds.find({"name":""}).include("name","number","repo","created").sort({"$desc":["created"]}).offset(0).limit(100)' ``` -### Discovering build scope without a project key +Add `"repo":"-build-info"` to the criteria when a project key +is known. Apply the same pagination rules as above. -When the user has not provided the project key or build-info repo, discover -it via AQL. **Do not** use **unscoped** `GET /api/build` (no `?project=` or -`?buildRepo=`) to list all builds — it can time out on large instances with -thousands of builds. +### Retrieving full build info -Use AQL `builds.find()` instead. The builds domain **requires** `name`, -`number`, and `repo` in `.include()` for permission reasons — omitting `repo` -produces an error. +Use the REST detail endpoint for a **single** build run. Always include +`?project=` (or `?project=default` when no key is provided): ```bash -jf rt curl -s -XPOST /api/search/aql \ - -H "Content-Type: text/plain" \ - -d 'builds.find({"name":""}).include("name","number","repo").sort({"$desc":["number"]}).limit(10)' +jf api "/artifactory/api/build//?project=" ``` -The `build.repo` field in the response tells you which build-info repository -the build resides in. Use that value as the `buildRepo` parameter in the -detail GET. +This is the only `/api/build` endpoint that should be used — it returns a +single record and does not need 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. ### Repository listing vs build-info -`GET /api/repositories?project=&type=buildinfo` may return an empty list -even when project-scoped build info exists (for example under a `*-build-info` -repository). Prefer the **build** endpoints above or AQL to discover builds; -do not treat an empty repository list as proof that no builds exist. +`GET /artifactory/api/repositories?project=&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. ## Permissions @@ -156,9 +164,9 @@ Note: `jf rt permission-target-template` is interactive. - Delete group: `jf rt group-delete ` - Add users to group: `jf rt group-add-users ` -To get user details or update users, use `jf rt curl`: +To get user details or update users, use `jf api`: ``` -jf rt curl -XGET /access/api/v2/users/ +jf api /access/api/v2/users/ ``` ## Replication diff --git a/skills/jfrog/references/catalog-entities.md b/skills/jfrog/references/catalog-entities.md index f4f1ee2..085bea0 100644 --- a/skills/jfrog/references/catalog-entities.md +++ b/skills/jfrog/references/catalog-entities.md @@ -215,5 +215,5 @@ These three domains provide different views of package and security data: | **Scope** | Global knowledge base + org overlay | Instance-scoped scanning | Instance-scoped storage | | **Security** | CVE advisories, EPSS, CVSS v2/v3/v4, known exploits | Watches, policies, violations | Vulnerability summary (deprecated) | | **Packages** | Public metadata (description, homepage, OpenSSF) | Components identified during scanning | Packages/versions stored in Artifactory | -| **Access** | GraphQL only | REST + CLI (`jf xr curl`) | GraphQL only | +| **Access** | GraphQL only | REST + CLI (`jf api /xray/...`) | GraphQL only | | **Use case** | Research, compliance reporting, package evaluation | Runtime enforcement, CI/CD gating | Inventory, location queries | 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 aeac970..2b44979 100644 --- a/skills/jfrog/references/general-bulk-operations-and-agent-patterns.md +++ b/skills/jfrog/references/general-bulk-operations-and-agent-patterns.md @@ -44,19 +44,9 @@ line and break parsers (e.g. JSON "Extra data" errors). - One temp file per worker or chunk, then concatenate; or - Use advisory locking (`flock`) if one file must be shared. -## Agent sandboxes and the environment check - -`scripts/check-environment.sh` does **not** call your JFrog server, but it may -make an outbound request to `releases.jfrog.io` for version checking and may -**write** -`/local-cache/jfrog-skill-state.json` when the cache is stale or missing. In a -restricted agent sandbox, **workspace write** access can fail even when -`full_network` is granted. Request permissions that allow writing `/local-cache` -when the check fails with a filesystem error. - For bulk API or CLI output files, use `/tmp` or `mktemp`; do not use -`local-cache/` except for `jfrog-skill-state.json` and the OneModel schema file -(see main SKILL.md). +`~/.jfrog/skills-cache/` except for `jfrog-skill-state.json` and the OneModel +schema file (see main SKILL.md). ## Shell hygiene @@ -79,7 +69,7 @@ When looping over items (repos, builds, users) and fetching detail for each: ```bash : >results.ndjson while read -r key; do - body=$(jf rt curl -sS -XGET "/api/repositories/$key" || true) + body=$(jf api "/artifactory/api/repositories/$key" || true) if echo "$body" | jq -e . >/dev/null 2>&1; then echo "$body" | jq -c . >>results.ndjson else @@ -89,7 +79,7 @@ done < <(jq -r '.[].key' list.json) jq -s '.' results.ndjson > details.json ``` -Never pipe a loop of `jf rt curl` calls directly into `jq -s` without +Never pipe a loop of `jf api` calls directly into `jq -s` without per-body validation. ## Where to find product specifics @@ -99,4 +89,5 @@ per-body validation. - JFrog Projects (endpoints): `references/projects-api.md` - Joining Artifactory repos to Projects (`projectKey`, roles, environments): `references/platform-access-entities.md` -- Credential tiers: `references/jfrog-credential-patterns.md` +- Platform API invocation (all products through `jf api`): see + `SKILL.md` § *Invoking platform APIs with `jf api`* diff --git a/skills/jfrog/references/general-parallel-execution.md b/skills/jfrog/references/general-parallel-execution.md index 340dd2e..a0bb9ba 100644 --- a/skills/jfrog/references/general-parallel-execution.md +++ b/skills/jfrog/references/general-parallel-execution.md @@ -6,45 +6,43 @@ heaviest: | Tier | Mechanism | Best for | |------|-----------|----------| -| 1 | Single Shell call with `&&` | Few commands, same tier, same credentials | -| 2 | Parallel Shell tool calls | Commands across different tiers or credential scopes | +| 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 | ## Tier 1: Batch within a single Shell call -Combine independent commands with `&&` when they share the same credential -scope. This reduces approval prompts. +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: ```bash -jf rt curl -XGET /api/repositories > /tmp/jf-repos-$$.json && \ -jf rt curl -XGET /api/system/ping > /tmp/jf-ping-$$.json && \ -jf rt curl -XGET /api/storageinfo > /tmp/jf-storage-$$.json +jf api /artifactory/api/repositories > /tmp/jf-repos-$$.json && \ +jf api /artifactory/api/system/ping > /tmp/jf-ping-$$.json && \ +jf api /artifactory/api/storageinfo > /tmp/jf-storage-$$.json ``` -For REST calls that share extracted credentials: +Cross-product reads batch the same way: ```bash -eval "$(bash /scripts/get-platform-credentials.sh)" && \ -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" "$JFROG_URL/access/api/v2/users/" > /tmp/jf-users-$$.json && \ -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" "$JFROG_URL/access/api/v2/groups/" > /tmp/jf-groups-$$.json && \ -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" "$JFROG_URL/access/api/v2/permissions/" > /tmp/jf-perms-$$.json +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 ``` ## Tier 2: Parallel Shell tool calls -Use multiple Shell tool calls in the same message when commands target -different tiers or do not share state (e.g. one `jf rt curl` call alongside -one plain `curl` call): +Use multiple Shell tool calls in the same message when the commands are +independent and the total runtime benefits from concurrency: ```bash # Shell call 1 — echo the expanded path so the agent can reference it later OUT=/tmp/jf-repos-$$.json -jf rt curl -XGET /api/repositories > "$OUT" && echo "$OUT" +jf api /artifactory/api/repositories > "$OUT" && echo "$OUT" # Shell call 2 (parallel) — same pattern, different PID OUT=/tmp/jf-users-$$.json -eval "$(bash /scripts/get-platform-credentials.sh)" && \ -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" "$JFROG_URL/access/api/v2/users/" > "$OUT" && echo "$OUT" +jf api /access/api/v2/users/ > "$OUT" && echo "$OUT" ``` Each parallel Shell call gets a different PID, so `$$` expands to different @@ -66,17 +64,19 @@ a structured result. The parent agent assembles the final answer. ``` Subagent 1 (shell): "Collect repository data" - → jf rt curl -XGET /api/repositories - → jf rt curl -XGET /api/storageinfo + → jf api /artifactory/api/repositories + → jf api /artifactory/api/storageinfo → Return repo count, types, total size Subagent 2 (shell): "Collect security configuration" - → jf xr curl -XGET /api/v2/policies - → jf xr curl -XGET /api/v2/watches + → jf api /xray/api/v2/policies + → jf api /xray/api/v2/watches → Return policy count, watch count, coverage gaps Subagent 3 (shell): "Collect user and permission data" - → eval credentials, then curl Access API for users, groups, permissions + → jf api /access/api/v2/users/ + → jf api /access/api/v2/groups/ + → jf api /access/api/v2/permissions/ → Return user count, group count, admin users ``` @@ -92,8 +92,6 @@ merges their results into a unified report. 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. -5. Remind the subagent to use `required_permissions: ["full_network"]` on - every Shell call that contacts the JFrog server. ### Subagent type selection @@ -107,8 +105,8 @@ merges their results into a unified report. | Scenario | Tier | |----------|------| -| 2–5 independent reads, same credential scope | 1 (single Shell) | -| Reads across Artifactory + Access APIs simultaneously | 2 (parallel Shell) | +| 2–5 independent reads, same server | 1 (single Shell) | +| Many independent reads where concurrency cuts total runtime | 2 (parallel Shell) | | Full platform audit, multi-section report, cross-server comparison | 3 (subagents) | | Task branches need different reference files or reasoning | 3 (subagents) | | Simple one-shot data fetch | 1 (single Shell) | diff --git a/skills/jfrog/references/general-use-case-hints.md b/skills/jfrog/references/general-use-case-hints.md index 1cc9bfe..416f1cd 100644 --- a/skills/jfrog/references/general-use-case-hints.md +++ b/skills/jfrog/references/general-use-case-hints.md @@ -6,14 +6,13 @@ workflow). Keep entries short and actionable. | Area | Symptom | Cause | Mitigation | Notes | |------|---------|-------|------------|-------| -| Agent sandbox | `check-environment.sh` fails with permission denied on `local-cache/` | Cache refresh tries to write `jfrog-skill-state.json` under `/local-cache`; sandbox blocks workspace writes | Run with permissions that allow writing the skill's `local-cache` directory, not only `full_network` | The script does not call your JFrog server but may contact `releases.jfrog.io` for version checking | | Parallel I/O | JSONL or ndjson parse errors ("Extra data", merged objects on one line) | Concurrent unsynchronized `>>` to the same file from parallel jobs | Sequential writes, one file per worker then `cat`, or `flock` | Generic pattern for any bulk CLI output | | APIs (general) | Report or script missing fields that appear in the UI or docs | List endpoint omitted fields; detail GET has the full record | List then GET by id/key when needed; see `general-bulk-operations-and-agent-patterns.md` | Applies across products | | Access / Projects | Project groups list looks empty in code | Response may use `members` for the group entries, not `groups` | Accept both keys when parsing | See `projects-api.md` | | Bulk detail fetches | `jq` parse error ("Extra data" or "Invalid ...") on slurped detail array | One or more detail GETs returned empty/HTML/error body; `jq -s` chokes on non-JSON mixed in | Validate each response with `jq -e .` before appending; write error placeholder for failures; see `general-bulk-operations-and-agent-patterns.md` | Applies to any per-item loop (repos, builds, users) | | Agent timeouts | Shell job silently moves to background; no captured output | `block_until_ms` too low for N sequential API calls (~1-2s each) | Estimate `N * 1.5s + 30s` buffer; set `block_until_ms` accordingly; or use parallel execution | Default 30s insufficient for >20 items | -| Sandbox + workspace writes | Generated report JSON or HTML not written; script exits 0 but file missing | Sandbox blocks some paths; agents sometimes wrongly target `local-cache/` for scratch files (disallowed — only state + schema belong there) | Write reports and API responses under `/tmp` or the user workspace; use `required_permissions: ["all"]` only when you must write inside the skill tree for the two allowed cache files | `local-cache/` is not a temp directory; see SKILL.md **`local-cache/` — allowed files only** | -| `jf rt curl` redirects | Responses are empty or contain redirect HTML instead of expected content | `jf rt curl` does not follow HTTP 302 redirects by default | **Always** pass `-L` when using `jf rt curl` so redirects (common with remote repos) are followed automatically | Applies to all `jf rt curl` invocations, not just downloads | +| 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-.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//`), 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 | diff --git a/skills/jfrog/references/jfrog-credential-patterns.md b/skills/jfrog/references/jfrog-credential-patterns.md deleted file mode 100644 index b2972d3..0000000 --- a/skills/jfrog/references/jfrog-credential-patterns.md +++ /dev/null @@ -1,168 +0,0 @@ -# Credential Extraction Patterns - -How to obtain JFrog Platform credentials for API calls outside the CLI's -built-in authentication. - -## Prerequisites - -- `jf` CLI installed and configured with at least one server -- `base64` command available (standard on macOS and Linux) -- `jq` available (standard jq JSON processor) - -## Response handling (network-backed commands) - -Use this whenever the JSON (or text) body comes from a **JFrog network -request**, including: - -- **`jf rt curl`** and **`jf xr curl`** (Tier 1 and Tier 2 in `SKILL.md`) -- **Tier 3** `curl` with `Authorization: Bearer` after - `get-platform-credentials.sh` -- Other **`jf`** read operations that contact the platform over the network - (same idea: one round-trip per logical fetch) - -**Fetch once**, redirect the body to a temp file (for example -`> /tmp/jf-api-$$.json`), then run `jq` on that file. If a filter fails or -you need stderr from jq, adjust and re-run `jq` against the same file—do -**not** repeat the identical network-fetching command in a one-liner with `||` -only to re-parse the same response; that doubles round-trips for no new data. -Local-only commands (for example `jf config show`, `jf --help`) do not need -this anti-pattern to avoid duplicate **HTTP** traffic, though you may still -save output to a file to retry parsing without re-running the command. See -`SKILL.md` (Preserving command output and Gotchas). - -## Extracting credentials from `jf config export` - -The `jf config export` command produces a base64-encoded JSON token containing -the server URL and access token. - -```bash -# For the default server (omit server-id) -JFROG_CONFIG=$(jf config export | base64 -d) - -# For a specific server -JFROG_CONFIG=$(jf config export | base64 -d) - -# Extract individual fields -JFROG_URL=$(echo "$JFROG_CONFIG" | jq -r .url) -JFROG_URL=${JFROG_URL%/} # Remove trailing slash -JFROG_ACCESS_TOKEN=$(echo "$JFROG_CONFIG" | jq -r .accessToken) -``` - -The exported JSON contains fields including: `url`, `artifactoryUrl`, -`distributionUrl`, `xrayUrl`, `missionControlUrl`, -`accessToken`, `user`, `serverId`. - -## Using the helper script - -The skill provides a convenience script that outputs shell variable assignments: - -```bash -# Source the credentials into your shell -eval "$(bash /scripts/get-platform-credentials.sh)" - -# With a specific server -eval "$(bash /scripts/get-platform-credentials.sh myserver)" -``` - -After sourcing, these variables are available: -- `JFROG_URL` — platform base URL (no trailing slash) -- `JFROG_ACCESS_TOKEN` — access token for Bearer auth -- `JFROG_RT_URL` — Artifactory URL -- `JFROG_XR_URL` — Xray URL -- `JFROG_DS_URL` — Distribution URL -- `JFROG_MC_URL` — Mission Control URL - -## API call patterns by product - -### Artifactory (use `jf rt curl`) - -No manual credential management needed: -```bash -jf rt curl -XGET /api/repositories -jf rt curl -XGET /api/system/ping -jf rt curl -XPOST /api/search/aql -H "Content-Type: text/plain" -d 'items.find({"repo":"my-repo"})' -``` - -With a specific server: `jf rt curl --server-id myserver -XGET /api/repositories` - -### Xray (use `jf xr curl`) - -No manual credential management needed: -```bash -jf xr curl -XGET /api/v2/watches -jf xr curl -XGET /api/v2/policies -jf xr curl -XGET /api/v1/system/ping -``` - -### Access / Platform Administration - -```bash -eval "$(bash /scripts/get-platform-credentials.sh)" -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v2/users/" -``` - -### Distribution - -```bash -eval "$(bash /scripts/get-platform-credentials.sh)" -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/distribution/api/v1/release_bundle" -``` - -### Evidence - -```bash -eval "$(bash /scripts/get-platform-credentials.sh)" -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/evidence/api/v1/evidence" -``` - -### AppTrust - -```bash -eval "$(bash /scripts/get-platform-credentials.sh)" -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/apptrust/api/v1/activity/log" -``` - -### GraphQL (OneModel) - -Full workflow (mandatory schema fetch per server, validation, pagination) is -in `references/onemodel-graphql.md` and **GraphQL (OneModel)** in `SKILL.md`. -Below matches those **credential, schema cache path, and payload** patterns. -`get-platform-credentials.sh` exports `JFROG_SERVER_ID` — use it for the schema -filename so each configured server has its own cache (same as -`onemodel-graphql.md`). - -```bash -eval "$(bash /scripts/get-platform-credentials.sh)" - -# Schema SDL only in local-cache (not query responses — those go to /tmp) -mkdir -p "/local-cache" -SCHEMA_FILE="/local-cache/onemodel-schema-${JFROG_SERVER_ID}.graphql" -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/onemodel/api/v1/supergraph/schema" \ - -o "$SCHEMA_FILE" - -# Execute query — do not use raw -d '{"query":"..."}' (escaping breaks); use jq -n --arg -QUERY='{ evidence { searchEvidence(first: 5, where: { hasSubjectWith: { repositoryKey: "my-repo-local" } } }) { totalCount } } }' -PAYLOAD=$(jq -n --arg q "$QUERY" '{"query": $q}') -RESPONSE_FILE="/tmp/onemodel-response-$$.json" -curl -s -X POST "$JFROG_URL/onemodel/api/v1/graphql" \ - -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - -H "Content-Type: application/json" \ - -d "$PAYLOAD" \ - -o "$RESPONSE_FILE" -jq . "$RESPONSE_FILE" -``` - -Authentication requires an access token with wildcard audience (`*@*`). - -### Projects - -```bash -eval "$(bash /scripts/get-platform-credentials.sh)" -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v1/projects" -``` diff --git a/skills/jfrog/references/jfrog-login-flow.md b/skills/jfrog/references/jfrog-login-flow.md index 3b04c69..4bfe5c3 100644 --- a/skills/jfrog/references/jfrog-login-flow.md +++ b/skills/jfrog/references/jfrog-login-flow.md @@ -9,9 +9,9 @@ Requires Artifactory 7.64.0+ and the JFrog CLI (`jf`). - 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. +the token. - `jf config` is the sole credential store. Never store tokens in files, - env var profiles, or project directories. +env var profiles, or project directories. - Validate URLs with the ping endpoint before using them in shell commands. ## Resolve the active environment @@ -21,19 +21,17 @@ jf config show 2>/dev/null ``` - **0 servers** — ask the user for their JFrog Platform URL, then go to - Web Login. +Web Login. - **1 server** — use it: `jf config use `, 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**. +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**. ## Web login (preferred) ### 1. Verify server and register session -Run with `required_permissions: ["full_network"]`: - ```bash bash /scripts/jfrog-login-register-session.sh "https://mycompany.jfrog.io" ``` @@ -51,6 +49,7 @@ Exit codes: 0 = success, 2 = server unreachable, 3 = registration failed. ### 2. Show the user the verification code and login link Build the login URL: + ``` ${JFROG_PLATFORM_URL}/ui/login?jfClientSession=${SESSION_UUID}&jfClientName=JFrog-Skills&jfClientCode=1 ``` @@ -59,8 +58,7 @@ Show the verification code prominently, then the clickable link: > ## Verification code: `` > -> Open this link to log in, then enter the code when prompted: -> [Log in to mycompany.jfrog.io]() +> Open the login link from above, then enter the code. > > Let me know when you're done. @@ -68,9 +66,6 @@ Wait for the user to confirm. Do not poll automatically. ### 3. Retrieve token, save credentials, verify -Run with `required_permissions: ["all"]` (the script writes to `~/.jfrog/` -via `jf config add`, which is outside the workspace): - ```bash bash /scripts/jfrog-login-save-credentials.sh \ "https://mycompany.jfrog.io" \ @@ -80,8 +75,10 @@ bash /scripts/jfrog-login-save-credentials.sh \ Substitute the 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`, sets the server as default, and -verifies with an Artifactory version check. On success it outputs: +saves credentials via `jf config add`, and verifies with an Artifactory +version check. It leaves the current default `jf` server unchanged — pass +`--server-id=` explicitly on subsequent calls (the SKILL.md "Server +selection rules" require this anyway). On success it outputs: ``` SERVER_ID= @@ -96,12 +93,23 @@ 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. +## Post-login handoff (mandatory gate) + +Before any other JFrog operation against the new server, ask the user: + +> Logged in to ``. Do you want to make it the default `jf` +> server? (If you say no, I'll keep using `--server-id=` +> explicitly for follow-up calls.) + +- Confirm → `jf config use `, then resume the original task. +- Decline or no answer → keep `--server-id=` on every `jf` call. + ## Fallback: manual token setup If web login fails (server too old, network restrictions): 1. Ask the user to generate a token in the JFrog UI: - **Administration > Identity and Access > Access Tokens > Generate Token** + **Administration > Identity and Access > Access Tokens > Generate Token** 2. Save it non-interactively: ```bash @@ -114,14 +122,11 @@ jf config add \ ## 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. -- Step 3 requires `required_permissions: ["all"]` because `jf config add` - writes to `~/.jfrog/` which is outside the workspace. Using only - `full_network` silently blocks the write. +(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`. -- Both scripts require `curl` on PATH. The save-credentials script also - requires `jf` and `jq`. +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. + diff --git a/skills/jfrog/references/onemodel-common-patterns.md b/skills/jfrog/references/onemodel-common-patterns.md index 18f7047..ccc193e 100644 --- a/skills/jfrog/references/onemodel-common-patterns.md +++ b/skills/jfrog/references/onemodel-common-patterns.md @@ -203,33 +203,36 @@ query GetEvidence($repoKey: String!, $path: String!, $name: String!) { } ``` -### curl with variables +### `jf api` with variables -Build the JSON body with `jq -n` — do not hand-escape quotes inside the query: +Build the JSON body with `jq -n` into a **file**, then pass the file to +`jf api` with `--input` — do not hand-escape quotes inside the query: ```bash -eval "$(bash /scripts/get-platform-credentials.sh [server-id])" - QUERY='query GetEvidence($repoKey: String!, $path: String!, $name: String!) { evidence { getEvidence(repositoryKey: $repoKey, path: $path, name: $name) { evidenceId verified } } }' -PAYLOAD=$(jq -n \ +PAYLOAD_FILE="/tmp/onemodel-payload-$$.json" +RESPONSE_FILE="/tmp/onemodel-response-$$.json" + +jq -n \ --arg q "$QUERY" \ --arg repoKey "example-repo-local" \ --arg path "path/to" \ --arg name "file.ext" \ - '{"query": $q, "variables": {"repoKey": $repoKey, "path": $path, "name": $name}}') + '{"query": $q, "variables": {"repoKey": $repoKey, "path": $path, "name": $name}}' \ + > "$PAYLOAD_FILE" -RESPONSE_FILE="/tmp/onemodel-response-$$.json" -curl -s -X POST \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/onemodel/api/v1/graphql" \ - -d "$PAYLOAD" \ - -o "$RESPONSE_FILE" +jf api /onemodel/api/v1/graphql \ + -X POST -H "Content-Type: application/json" \ + --input "$PAYLOAD_FILE" \ + > "$RESPONSE_FILE" jq . "$RESPONSE_FILE" ``` +Pass `--server-id ` to `jf api` when targeting a non-default server +(see `onemodel-graphql.md` step 1). + Echo `$RESPONSE_FILE` if a follow-up Shell call must read it (see SKILL.md *Preserving command output*). diff --git a/skills/jfrog/references/onemodel-graphql.md b/skills/jfrog/references/onemodel-graphql.md index f932258..41e03e5 100644 --- a/skills/jfrog/references/onemodel-graphql.md +++ b/skills/jfrog/references/onemodel-graphql.md @@ -12,9 +12,11 @@ pagination, variables, and date formatting, read `onemodel-common-patterns.md`. In examples below, `` is this skill's directory (parent of `references/`). -## `local-cache/` policy +## `~/.jfrog/skills-cache/` policy -The skill’s `local-cache/` directory holds **only**: +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**: 1. **`onemodel-schema-${JFROG_SERVER_ID}.graphql`** — this workflow (supergraph SDL cache). **Always** use the path in [Fetch the schema](#2-fetch-the-schema); @@ -23,19 +25,17 @@ The skill’s `local-cache/` directory holds **only**: manage it; do not delete or replace it casually. **Never** store GraphQL **query responses**, REST bodies, reports, or other -scratch files under `local-cache/`. For responses, use `/tmp` with a unique name +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 `local-cache/`. +example `RESPONSE_FILE` paths 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. -- **`curl` and `jq`** on `PATH` (same as the base skill). - -All network calls require `required_permissions: ["full_network"]` in agent -Shell invocations. +- **`jq`** on `PATH` (same as the base skill). HTTP calls go through + `jf api`; no standalone `curl` is needed. ## Workflow @@ -43,7 +43,7 @@ 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. -1. **Resolve credentials** — `get-platform-credentials.sh` (same server as CLI) +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 @@ -51,17 +51,24 @@ will fail on servers whose schema differs from what you expect. 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 credentials +### 1. Resolve the target server -Use the same helper as Tier 3 REST calls. Pass the **same** `server-id` you -resolved per SKILL.md server selection rules (omit for default 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`: ```bash -eval "$(bash /scripts/get-platform-credentials.sh [server-id])" +# User-specified server: +JFROG_SERVER_ID="" + +# Or the current default: +JFROG_SERVER_ID=$(jf config show --server-id 2>/dev/null \ + || jf config export | base64 -d | jq -r '.servers[] | select(.isDefault==true).serverId') ``` -This exports `JFROG_URL`, `JFROG_ACCESS_TOKEN`, `JFROG_SERVER_ID`, and related -variables. The script normalizes `JFROG_URL` (no trailing slash). +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*). ### 2. Fetch the schema @@ -81,32 +88,34 @@ 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. -The schema is large. Cache it under the skill's local cache (gitignored) keyed -by the concrete `JFROG_SERVER_ID` from step 1 (the CLI `serverId`, never a -placeholder like `default`). +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`). **Always use this exact path** — do not save the schema to `/tmp/` or any other location. The cache path is: -`/local-cache/onemodel-schema-${JFROG_SERVER_ID}.graphql` +`${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: ```bash -SCHEMA_FILE="/local-cache/onemodel-schema-${JFROG_SERVER_ID}.graphql" +SCHEMA_FILE="${JFROG_CLI_HOME_DIR:-$HOME/.jfrog}/skills-cache/onemodel-schema-${JFROG_SERVER_ID}.graphql" if [ -s "$SCHEMA_FILE" ]; then echo "Schema cache hit: $SCHEMA_FILE ($(wc -l < "$SCHEMA_FILE") lines)" else - mkdir -p "/local-cache" - curl -s -X GET \ - -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/onemodel/api/v1/supergraph/schema" \ - -o "$SCHEMA_FILE" + mkdir -p "$(dirname "$SCHEMA_FILE")" + jf api /onemodel/api/v1/supergraph/schema \ + --server-id "$JFROG_SERVER_ID" \ + > "$SCHEMA_FILE" echo "Schema fetched: $SCHEMA_FILE ($(wc -l < "$SCHEMA_FILE") lines)" fi ``` +(Omit `--server-id "$JFROG_SERVER_ID"` to target 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. @@ -235,17 +244,24 @@ Before executing, verify: ### 6. Execute the query -POST to: +POST to `/onemodel/api/v1/graphql`: -`$JFROG_URL/onemodel/api/v1/graphql` +```bash +jf api /onemodel/api/v1/graphql \ + -X POST -H "Content-Type: application/json" \ + --input "$PAYLOAD_FILE" \ + --server-id "$JFROG_SERVER_ID" \ + > "$RESPONSE_FILE" +``` #### Always save the response to a file -Use `curl ... -o "$RESPONSE_FILE"` so you can re-`jq` without re-querying. -**Do not pipe `curl` directly to `jq`** — a wrong filter loses the response. -**Do not** set `RESPONSE_FILE` under `/local-cache/` — that -directory is only for the schema cache and `jfrog-skill-state.json` (see -[`local-cache/` policy](#local-cache-policy) above). +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). For multiple queries in one shell session, use a temp directory under `/tmp` and sequential names: @@ -265,7 +281,9 @@ 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. +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. ##### Avoid `PARSING_ERROR` (broken GraphQL documents) @@ -280,21 +298,21 @@ typo near the end surfaces as an error at a **high column number**. | Query size | How to build the payload | |------------|-------------------------| -| Tiny (few fields, one level) | `QUERY='...'` plus `jq -n --arg q "$QUERY"` is OK. | +| Tiny (few fields, one level) | `QUERY='...'` plus `jq -n --arg q "$QUERY"` into a file is OK. | | Anything nested (connections, `where: { ... }`, multiple roots) | Put the document in a **`.graphql` file** (or a **quoted heredoc**) and use **`jq --rawfile`**. Never maintain a 400+ character one-liner in bash. | -Example — **small** query with `jq --arg` (closing braces match: `searchEvidence { ... }`, `evidence { ... }`, outer `{ ... }`): +Example — **small** query with `jq --arg`: ```bash -QUERY='{ evidence { searchEvidence(first: 5, where: { hasSubjectWith: { repositoryKey: "my-repo-local" } } }) { totalCount } } }' -PAYLOAD=$(jq -n --arg q "$QUERY" '{"query": $q}') +QUERY='{ evidence { searchEvidence(first: 5, where: { hasSubjectWith: { repositoryKey: "my-repo-local" } }) { totalCount } } }' +PAYLOAD_FILE="$ONEMODEL_TMPDIR/payload-$ONEMODEL_QUERY_NUM.json" +jq -n --arg q "$QUERY" '{"query": $q}' > "$PAYLOAD_FILE" -curl -s -X POST \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/onemodel/api/v1/graphql" \ - -d "$PAYLOAD" \ - -o "$RESPONSE_FILE" +jf api /onemodel/api/v1/graphql \ + -X POST -H "Content-Type: application/json" \ + --input "$PAYLOAD_FILE" \ + --server-id "$JFROG_SERVER_ID" \ + > "$RESPONSE_FILE" jq . "$RESPONSE_FILE" ``` @@ -303,14 +321,16 @@ Example — **nested** query from a file (preferred for real OneModel calls): ```bash # my-query.graphql contains a normal multi-line GraphQL document -PAYLOAD=$(jq -n --rawfile q my-query.graphql '{"query": ($q | gsub("#.*"; "") | gsub("\\s+"; " ") | sub("^ +"; "") | sub(" +$"; ""))}') - -curl -s -X POST \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/onemodel/api/v1/graphql" \ - -d "$PAYLOAD" \ - -o "$RESPONSE_FILE" +PAYLOAD_FILE="$ONEMODEL_TMPDIR/payload-$ONEMODEL_QUERY_NUM.json" +jq -n --rawfile q my-query.graphql \ + '{"query": ($q | gsub("#.*"; "") | gsub("\\s+"; " ") | sub("^ +"; "") | sub(" +$"; ""))}' \ + > "$PAYLOAD_FILE" + +jf api /onemodel/api/v1/graphql \ + -X POST -H "Content-Type: application/json" \ + --input "$PAYLOAD_FILE" \ + --server-id "$JFROG_SERVER_ID" \ + > "$RESPONSE_FILE" ``` Strip `#` comments and collapse whitespace only if you need a single-line @@ -341,7 +361,7 @@ Errors appear in an `errors` array. Partial data may coexist with errors. | Symptom | Likely cause | Action | |--------|---------------|--------| -| 401 | Invalid or expired token | Re-run `get-platform-credentials.sh` for the same server | +| 401 | Invalid or expired token | Re-run the login flow (`references/jfrog-login-flow.md`) for the same server | | 403 | Insufficient permissions | User/token lacks access to the resource | | `GRAPHQL_VALIDATION_FAILED` | Bad field or argument | Re-check schema | | `PARSING_ERROR` / syntax at **line 1, column N** | Invalid document (often extra/missing `}`); common with long `QUERY='...'` one-liners | Reformat in a `.graphql` file or heredoc; verify brace balance; use `jq --rawfile` | @@ -401,12 +421,15 @@ Include the resolved base URL so they can open it immediately. provided." Using an enum value where a string is expected (or vice versa) causes `GRAPHQL_VALIDATION_FAILED` errors, so always verify which field you are targeting before choosing the literal form. -- **OneModel endpoint only:** `POST $JFROG_URL/onemodel/api/v1/graphql`. Do not - use legacy `/metadata/api/v1/query` or its `packages` root for OneModel. +- **OneModel endpoint only:** `POST /onemodel/api/v1/graphql` (full path + passed to `jf api`). Do not use legacy + `/metadata/api/v1/query` or its `packages` root for OneModel. - **Token audience** — wildcard `*@*` is required for typical OneModel use; narrow tokens may fail with auth errors. -- **`jf rt curl` is not for OneModel** — OneModel lives on the platform base URL; - use plain `curl` with `JFROG_URL` and bearer token. +- **`jf api` handles auth and URL** — it authenticates against the active + (or `--server-id`-named) server and prepends the configured platform base + URL. Do not construct OneModel URLs manually or attach bearer tokens + yourself. - **Content-Type** — `application/json` on POST. - **Pagination** — do not mix `first/after` with `last/before` in the same field. - **Dates** — fields ending in `...At` default to ISO-8601 UTC; `@dateFormat` diff --git a/skills/jfrog/references/platform-admin-api-gaps.md b/skills/jfrog/references/platform-admin-api-gaps.md index 22c901b..f04216b 100644 --- a/skills/jfrog/references/platform-admin-api-gaps.md +++ b/skills/jfrog/references/platform-admin-api-gaps.md @@ -1,9 +1,11 @@ # Platform Administration API Gaps -Operations available through REST API but not (or only partially) through CLI. -For Artifactory-scoped endpoints, use `jf rt curl`. -For platform-wide endpoints, use plain `curl` with credentials from -`scripts/get-platform-credentials.sh`. +Operations available through REST API but not (or only partially) through the +CLI. Invoke all of them via `jf api` (see the base skill's *Invoking platform +APIs with `jf api`* section). Authentication is handled automatically from +the configured `jf` server — no token extraction needed. Every path includes +the product prefix (`/access/...`, `/artifactory/...`, `/xray/...`, +`/worker/...`). ## Users (full CRUD) @@ -11,29 +13,25 @@ The CLI has `users-create` and `users-delete` but lacks GET and UPDATE. ### Get user details ```bash -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v2/users/" +jf api /access/api/v2/users/ ``` ### List users ```bash -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v2/users/" +jf api /access/api/v2/users/ ``` ### Update user (partial) ```bash -curl -s -XPATCH -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - -H "Content-Type: application/json" \ - "$JFROG_URL/access/api/v2/users/" \ +jf api /access/api/v2/users/ \ + -X PATCH -H "Content-Type: application/json" \ -d '{"email": "newemail@example.com"}' ``` ### Create user ```bash -curl -s -XPOST -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - -H "Content-Type: application/json" \ - "$JFROG_URL/access/api/v2/users/" \ +jf api /access/api/v2/users/ \ + -X POST -H "Content-Type: application/json" \ -d '{"username": "newuser", "email": "user@example.com", "password": "...", "admin": false}' ``` @@ -41,28 +39,24 @@ curl -s -XPOST -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ ### Get group details ```bash -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v2/groups/" +jf api /access/api/v2/groups/ ``` ### List groups ```bash -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v2/groups/" +jf api /access/api/v2/groups/ ``` ## Permissions (full CRUD) ### List permissions ```bash -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v2/permissions/" +jf api /access/api/v2/permissions/ ``` ### Get permission details ```bash -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v2/permissions/" +jf api /access/api/v2/permissions/ ``` ## Access tokens (beyond CLI) @@ -71,29 +65,25 @@ The CLI has `access-token-create` but not list or revoke. ### List tokens ```bash -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v1/tokens" +jf api /access/api/v1/tokens ``` ### Revoke token by ID ```bash -curl -s -XDELETE -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v1/tokens/" +jf api /access/api/v1/tokens/ -X DELETE ``` ## Environments ### List global environments ```bash -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v1/environments" +jf api /access/api/v1/environments ``` ### Create global environment ```bash -curl -s -XPOST -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - -H "Content-Type: application/json" \ - "$JFROG_URL/access/api/v1/environments" \ +jf api /access/api/v1/environments \ + -X POST -H "Content-Type: application/json" \ -d '{"name": "STAGING"}' ``` @@ -106,15 +96,13 @@ environments. ### List webhooks ```bash -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v1/webhooks" +jf api /access/api/v1/webhooks ``` ### Create webhook ```bash -curl -s -XPOST -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - -H "Content-Type: application/json" \ - "$JFROG_URL/access/api/v1/webhooks" \ +jf api /access/api/v1/webhooks \ + -X POST -H "Content-Type: application/json" \ -d '{"key": "my-webhook", "url": "https://example.com/hook", "event_types": ["uploaded"]}' ``` @@ -122,37 +110,35 @@ curl -s -XPOST -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ ### Platform ping ```bash -jf rt curl -XGET /api/system/ping +jf api /artifactory/api/system/ping ``` ### Artifactory version ```bash -jf rt curl -XGET /api/system/version +jf api /artifactory/api/system/version ``` ### Xray ping ```bash -jf xr curl -XGET /api/v1/system/ping +jf api /xray/api/v1/system/ping ``` ### Xray version ```bash -jf xr curl -XGET /api/v1/system/version +jf api /xray/api/v1/system/version ``` ## OIDC configuration ### List OIDC providers ```bash -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v1/oidc" +jf api /access/api/v1/oidc ``` ### Create OIDC configuration ```bash -curl -s -XPOST -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - -H "Content-Type: application/json" \ - "$JFROG_URL/access/api/v1/oidc" \ +jf api /access/api/v1/oidc \ + -X POST -H "Content-Type: application/json" \ -d '{"name": "my-oidc", "issuer_url": "https://...", "provider_type": "generic"}' ``` @@ -160,8 +146,7 @@ curl -s -XPOST -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ ### Get SCIM users ```bash -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v1/scim/v2/Users" +jf api /access/api/v1/scim/v2/Users ``` ## Workers (beyond CLI) @@ -170,12 +155,10 @@ The CLI covers most worker operations. These are API-only: ### Get available actions ```bash -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/worker/api/v1/actions" +jf api /worker/api/v1/actions ``` ### Get actions metadata ```bash -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/worker/api/v1/actions/metadata" +jf api /worker/api/v1/actions/metadata ``` diff --git a/skills/jfrog/references/platform-admin-operations.md b/skills/jfrog/references/platform-admin-operations.md index 001a92b..d033d93 100644 --- a/skills/jfrog/references/platform-admin-operations.md +++ b/skills/jfrog/references/platform-admin-operations.md @@ -23,12 +23,12 @@ jf stats rt [--server-id ] [--format json|table] ## Projects -Projects are managed via the Access API (no CLI support). Use Tier 3 -credentials (plain curl with extracted token): +Projects are managed via the Access API (no CLI subcommand). Invoke the +endpoints through `jf api` (see the base skill's *Invoking platform APIs +with `jf api`* section). Authentication is handled automatically: ```bash -eval "$(bash /scripts/get-platform-credentials.sh)" && \ -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" "$JFROG_URL/access/api/v1/projects" +jf api /access/api/v1/projects ``` - **List projects**: `GET /access/api/v1/projects` @@ -42,10 +42,8 @@ When querying multiple projects, batch the calls in a single Shell invocation to avoid per-project round-trips: ```bash -eval "$(bash /scripts/get-platform-credentials.sh)" && \ for proj in proj1 proj2 proj3; do - curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v1/projects/$proj/users" + jf api "/access/api/v1/projects/$proj/users" done ``` @@ -54,7 +52,7 @@ creating/updating projects, managing members, and assigning repositories. ## System health -Not available in CLI. Use: +Not available as a dedicated CLI subcommand. Use: ```bash -jf rt curl -XGET /api/system/ping +jf api /artifactory/api/system/ping ``` diff --git a/skills/jfrog/references/projects-api.md b/skills/jfrog/references/projects-api.md index a8909a8..06f9f18 100644 --- a/skills/jfrog/references/projects-api.md +++ b/skills/jfrog/references/projects-api.md @@ -3,30 +3,25 @@ **See also:** `references/platform-access-entities.md` for how Projects relate to repositories, members, roles, and environments. -Projects are managed through the Access API. There is no CLI support — use -Tier 3 credentials (plain curl with extracted token via -`get-platform-credentials.sh`). +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 are relative to the JFrog Platform URL. +All endpoints below use full product-prefixed paths (`/access/api/...`, +`/artifactory/api/...`). ## Authentication -All calls in this file require `required_permissions: ["full_network"]` in the -Shell tool (see the Network permissions section in SKILL.md). - -```bash -eval "$(bash /scripts/get-platform-credentials.sh [server-id])" -``` - -Then use `$JFROG_URL` and `$JFROG_ACCESS_TOKEN` in all requests. +Credentials are resolved automatically by `jf api` from the active `jf config` +server — no token extraction or `curl` wiring is needed. ## Projects ### List all projects ```bash -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v1/projects" +jf api /access/api/v1/projects ``` Returns an array of project objects with `project_key`, `display_name`, @@ -35,16 +30,14 @@ Returns an array of project objects with `project_key`, `display_name`, ### Get a single project ```bash -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v1/projects/" +jf api /access/api/v1/projects/ ``` ### Create a project ```bash -curl -s -XPOST -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - -H "Content-Type: application/json" \ - "$JFROG_URL/access/api/v1/projects" \ +jf api /access/api/v1/projects \ + -X POST -H "Content-Type: application/json" \ -d '{ "display_name": "My Project", "description": "Project description", @@ -63,17 +56,15 @@ allowed, no leading/trailing hyphen). ### Update a project ```bash -curl -s -XPUT -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - -H "Content-Type: application/json" \ - "$JFROG_URL/access/api/v1/projects/" \ +jf api /access/api/v1/projects/ \ + -X PUT -H "Content-Type: application/json" \ -d '{"display_name": "Updated Name", "description": "Updated description"}' ``` ### Delete a project ```bash -curl -s -XDELETE -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v1/projects/" +jf api /access/api/v1/projects/ -X DELETE ``` ## Members @@ -81,8 +72,7 @@ curl -s -XDELETE -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ ### List project members (users) ```bash -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v1/projects//users" +jf api /access/api/v1/projects//users ``` Returns `{"members": [{"name": "", "roles": [""]}]}`. @@ -90,24 +80,21 @@ Returns `{"members": [{"name": "", "roles": [""]}]}`. ### Add a member ```bash -curl -s -XPUT -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - -H "Content-Type: application/json" \ - "$JFROG_URL/access/api/v1/projects//users/" \ +jf api /access/api/v1/projects//users/ \ + -X PUT -H "Content-Type: application/json" \ -d '{"name": "", "roles": ["Developer"]}' ``` ### Remove a member ```bash -curl -s -XDELETE -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v1/projects//users/" +jf api /access/api/v1/projects//users/ -X DELETE ``` ### List project groups ```bash -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v1/projects//groups" +jf api /access/api/v1/projects//groups ``` The response may list group entries under **`members`**, **`groups`**, or both, @@ -117,9 +104,8 @@ depending on platform version (same general shape as users: `name` and ### Add a group ```bash -curl -s -XPUT -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - -H "Content-Type: application/json" \ - "$JFROG_URL/access/api/v1/projects//groups/" \ +jf api /access/api/v1/projects//groups/ \ + -X PUT -H "Content-Type: application/json" \ -d '{"name": "", "roles": ["Contributor"]}' ``` @@ -128,8 +114,7 @@ curl -s -XPUT -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ ### List project roles ```bash -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v1/projects//roles" +jf api /access/api/v1/projects//roles ``` Returns an array of role objects. Each has `name`, `description`, `type` @@ -146,9 +131,8 @@ list matches another. See `references/platform-access-entities.md`. ### Create a custom role ```bash -curl -s -XPOST -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - -H "Content-Type: application/json" \ - "$JFROG_URL/access/api/v1/projects//roles" \ +jf api /access/api/v1/projects//roles \ + -X POST -H "Content-Type: application/json" \ -d '{ "name": "QA Engineer", "description": "Read and annotate repos in DEV", @@ -168,8 +152,7 @@ and `references/platform-access-entities.md`. ### List environments (platform API) ```bash -curl -s -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - "$JFROG_URL/access/api/v1/environments" +jf api /access/api/v1/environments ``` Returns `[{"name": "DEV"}, {"name": "PROD"}, ...]` -- the platform environment @@ -178,9 +161,8 @@ list available through this Access API path. ### Create an environment ```bash -curl -s -XPOST -H "Authorization: Bearer $JFROG_ACCESS_TOKEN" \ - -H "Content-Type: application/json" \ - "$JFROG_URL/access/api/v1/environments" \ +jf api /access/api/v1/environments \ + -X POST -H "Content-Type: application/json" \ -d '{"name": "STAGING"}' ``` @@ -193,14 +175,15 @@ Environment names are uppercase by convention. Assign a repository to a project by updating its configuration: ```bash -jf rt curl -XPOST "/api/repositories/" \ - -H "Content-Type: application/json" \ +jf api /artifactory/api/repositories/ \ + -X POST -H "Content-Type: application/json" \ -d '{"projectKey": ""}' ``` ### List repositories for a project -`GET /api/repositories` supports optional query parameters that can be combined: +`GET /artifactory/api/repositories` supports optional query parameters that can +be combined: | Parameter | Values | Example | |-----------|--------|---------| @@ -210,13 +193,13 @@ jf rt curl -XPOST "/api/repositories/" \ ```bash # All repos in a project -jf rt curl -XGET "/api/repositories?project=" +jf api "/artifactory/api/repositories?project=" # Only local Docker repos in a project -jf rt curl -XGET "/api/repositories?project=&type=local&packageType=docker" +jf api "/artifactory/api/repositories?project=&type=local&packageType=docker" # All remote repos (no project filter) -jf rt curl -XGET "/api/repositories?type=remote" +jf api "/artifactory/api/repositories?type=remote" ``` Returns a lite list with `key`, `type`, `packageType`, and `url` per repo. @@ -229,7 +212,7 @@ like `projectKey`, `description`, storage settings, etc. that are absent from the lite list), use the detail endpoint: ```bash -jf rt curl -XGET "/api/repositories/" +jf api "/artifactory/api/repositories/" ``` Use this when you have a specific repo or a short list of repos to inspect -- @@ -253,5 +236,6 @@ versions). - **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**: returns 403 if the token lacks project admin - or platform admin privileges. +- **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. diff --git a/skills/jfrog/references/xray-entities.md b/skills/jfrog/references/xray-entities.md index 10645bf..fdb5b7a 100644 --- a/skills/jfrog/references/xray-entities.md +++ b/skills/jfrog/references/xray-entities.md @@ -9,7 +9,7 @@ When to read this file: - Searching for **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 xr curl -XGET /api/v2/...`. +For REST fallback: `jf api /xray/api/v2/...` (see the base skill's *Invoking platform APIs with `jf api`* section). ## Entity relationship overview @@ -136,8 +136,8 @@ For Docker images, the path format is `default////manifest.json`: ```bash -jf xr curl -s -XPOST /api/v2/summary/artifact \ - -H "Content-Type: application/json" \ +jf api /xray/api/v2/summary/artifact \ + -X POST -H "Content-Type: application/json" \ -d '{"paths": ["default/my-docker-repo/my-image/my-tag/manifest.json"]}' ``` @@ -225,8 +225,8 @@ component type (e.g. npm-only) must be done client-side on `infected_components` or by querying watches that cover specific repository types. ```bash -jf xr curl -s -XPOST /api/v1/violations \ - -H "Content-Type: application/json" \ +jf api /xray/api/v1/violations \ + -X POST -H "Content-Type: application/json" \ -d '{ "filters": { "violation_type": "Security", @@ -340,8 +340,8 @@ The request body uses `component_details` (an array of objects with `component_id`), **not** `component_ids`. ```bash -jf xr curl -s -XPOST /api/v1/summary/component \ - -H "Content-Type: application/json" \ +jf api /xray/api/v1/summary/component \ + -X POST -H "Content-Type: application/json" \ -d '{"component_details": [{"component_id": "npm://lodash:4.17.19"}]}' ``` @@ -376,17 +376,17 @@ entire repository, query individual artifact paths (discovered via AQL or ```bash # v1 — by path -jf xr curl -XPOST /api/v1/summary/artifact \ - -H "Content-Type: application/json" \ +jf api /xray/api/v1/summary/artifact \ + -X POST -H "Content-Type: application/json" \ -d '{"paths": ["default/npm-local/moment-2.29.3.tar.gz"]}' # v2 — by checksum -jf xr curl -XPOST /api/v2/summary/artifact \ - -H "Content-Type: application/json" \ +jf api /xray/api/v2/summary/artifact \ + -X POST -H "Content-Type: application/json" \ -d '{"checksums": ["8240b88c..."]}' ``` -See `SKILL.md` (Tier 2: Xray) for the full response schema. +See `SKILL.md` § *Invoking platform APIs with `jf api`* for the full response schema. ## Impacted resources search @@ -467,13 +467,13 @@ Key response fields: ```bash # Mode 1: all artifacts affected by a CVE -jf xr curl -s -XGET "/api/v2/search/impactedResources?vulnerability=CVE-2021-23337&limit=100" +jf api "/xray/api/v2/search/impactedResources?vulnerability=CVE-2021-23337&limit=100" # Mode 2: artifacts containing a specific package version -jf xr curl -s -XGET "/api/v2/search/impactedResources?name=log4j-core&type=maven&version=2.14.1&namespace=org.apache.logging.log4j" +jf api "/xray/api/v2/search/impactedResources?name=log4j-core&type=maven&version=2.14.1&namespace=org.apache.logging.log4j" # Mode 3: artifacts containing any version of a package -jf xr curl -s -XGET "/api/v2/search/impactedResources?name=lodash&type=npm" +jf api "/xray/api/v2/search/impactedResources?name=lodash&type=npm" ``` ### Pagination @@ -482,11 +482,11 @@ Page through results using `last_key`: ```bash # First page -RESP=$(jf xr curl -s -XGET "/api/v2/search/impactedResources?vulnerability=CVE-2021-23337&limit=1000") +RESP=$(jf api "/xray/api/v2/search/impactedResources?vulnerability=CVE-2021-23337&limit=1000") LAST_KEY=$(echo "$RESP" | jq -r '.last_key') # Subsequent pages (loop until last_key is empty) -jf xr curl -s -XGET "/api/v2/search/impactedResources?vulnerability=CVE-2021-23337&limit=1000&last_key=$LAST_KEY" +jf api "/xray/api/v2/search/impactedResources?vulnerability=CVE-2021-23337&limit=1000&last_key=$LAST_KEY" ``` ## Exposures (Advanced Security) @@ -565,16 +565,16 @@ Response: ```bash # Secrets exposures for an artifact -jf xr curl -s -XGET "/api/v1/secrets/results?repo=my-docker-local&path=my-image/latest/manifest.json&num_of_rows=50" +jf api "/xray/api/v1/secrets/results?repo=my-docker-local&path=my-image/latest/manifest.json&num_of_rows=50" # IaC exposures, sorted by severity descending -jf xr curl -s -XGET "/api/v1/iac/results?repo=my-repo&path=terraform/main.tf&order_by=jfrog_severity&direction=desc" +jf api "/xray/api/v1/iac/results?repo=my-repo&path=terraform/main.tf&order_by=jfrog_severity&direction=desc" # Application exposures with search filter -jf xr curl -s -XGET "/api/v1/applications/results?repo=npm-local&path=app-1.0.0.tgz&search=injection" +jf api "/xray/api/v1/applications/results?repo=npm-local&path=app-1.0.0.tgz&search=injection" # Service misconfigurations -jf xr curl -s -XGET "/api/v1/services/results?repo=docker-local&path=my-service/1.0/manifest.json" +jf api "/xray/api/v1/services/results?repo=docker-local&path=my-service/1.0/manifest.json" ``` ### Paginating exposure results @@ -582,7 +582,7 @@ jf xr curl -s -XGET "/api/v1/services/results?repo=docker-local&path=my-service/ ```bash PAGE=1 while true; do - RESP=$(jf xr curl -s -XGET "/api/v1/secrets/results?repo=my-repo&path=my-artifact&page_num=$PAGE&num_of_rows=100") + RESP=$(jf api "/xray/api/v1/secrets/results?repo=my-repo&path=my-artifact&page_num=$PAGE&num_of_rows=100") echo "$RESP" | jq '.data[]' TOTAL=$(echo "$RESP" | jq '.total_count') COUNT=$(echo "$RESP" | jq '.data | length') @@ -605,8 +605,8 @@ and then fan out to the exposures endpoint. ```bash OUT=/tmp/manifests-$$.json -jf rt curl -s -XPOST /api/search/aql \ - -H "Content-Type: text/plain" \ +jf api /artifactory/api/search/aql \ + -X POST -H "Content-Type: text/plain" \ -d 'items.find({"repo":"my-docker-local","name":"manifest.json","path":{"$nmatch":"*_uploads*"}}).include("repo","path","name")' \ > "$OUT" echo "$OUT" @@ -620,8 +620,8 @@ exposures API's `path` parameter. **Non-Docker repos** — find scannable artifacts: ```bash -jf rt curl -s -XPOST /api/search/aql \ - -H "Content-Type: text/plain" \ +jf api /artifactory/api/search/aql \ + -X POST -H "Content-Type: text/plain" \ -d 'items.find({"repo":"npm-local","type":"file"}).include("repo","path","name").sort({"$desc":["size"]}).limit(20)' ``` @@ -661,7 +661,7 @@ Use 6-day windows to avoid edge-case overflows from hour-level rounding. Example request: ```bash -jf xr curl -XGET "/api/v1/curation/audit/packages?order_by=id&direction=desc&num_of_rows=100&created_at_start=2023-07-20T22:00:00.000Z&created_at_end=2023-07-26T22:00:00.000Z&include_total=true&offset=0" +jf api "/xray/api/v1/curation/audit/packages?order_by=id&direction=desc&num_of_rows=100&created_at_start=2023-07-20T22:00:00.000Z&created_at_end=2023-07-26T22:00:00.000Z&include_total=true&offset=0" ``` Response shape (key fields): diff --git a/skills/jfrog/scripts/check-environment.sh b/skills/jfrog/scripts/check-environment.sh index 6343f54..d58ad7e 100755 --- a/skills/jfrog/scripts/check-environment.sh +++ b/skills/jfrog/scripts/check-environment.sh @@ -2,32 +2,47 @@ # check-environment.sh — Cached JFrog CLI environment check # # Checks if jf is installed and its version, using a 24h-TTL cache -# at /local-cache/jfrog-skill-state.json to avoid redundant checks. -# local-cache/ is only for this file and the OneModel schema cache — not temp API output. +# at ${JFROG_CLI_HOME_DIR:-$HOME/.jfrog}/skills-cache/jfrog-skill-state.json +# to avoid redundant checks. The skills-cache/ dir holds only this file and +# the OneModel schema cache — not temp API output. # -# stdout: eval-able shell exports (JFROG_CLI_USER_AGENT) +# Usage: +# bash check-environment.sh [] [--force] +# +# stdout: bare JFROG_CLI_USER_AGENT value (one line) — agent captures it +# and runs `export JFROG_CLI_USER_AGENT=''` once at the top of +# every bash invocation that calls jf # stderr: JSON state (informational, also written to cache file) # # Exit codes: -# 0 — Cache is fresh, CLI is ready -# 1 — Cache was stale/missing and has been refreshed -# 2 — jf is not installed -# -# Usage: -# eval "$(bash check-environment.sh [--force])" +# 0 — cache fresh, CLI ready +# 1 — cache refreshed, CLI ready +# 2 — jf not installed +# 3 — jf below MIN_CLI_VERSION (required for `jf api`) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SKILL_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -CACHE_DIR="$SKILL_ROOT/local-cache" +JFROG_HOME="${JFROG_CLI_HOME_DIR:-$HOME/.jfrog}" +CACHE_DIR="$JFROG_HOME/skills-cache" CACHE_FILE="$CACHE_DIR/jfrog-skill-state.json" DEFAULT_TTL_HOURS=24 FORCE=false -if [[ "${1:-}" == "--force" ]]; then - FORCE=true -fi +# Minimum jf CLI version required by this skill. `jf api` (the generic +# authenticated REST pass-through used by nearly every reference in this +# skill) landed in 2.100.0; older CLIs fail with "unknown command: api". +MIN_CLI_VERSION="2.100.0" + +MODEL_SLUG="" +for arg in "$@"; do + if [[ "$arg" == "--force" ]]; then + FORCE=true + elif [[ -z "$MODEL_SLUG" ]]; then + MODEL_SLUG="$arg" + fi +done now_epoch() { date -u +%s @@ -37,6 +52,19 @@ iso_now() { date -u '+%Y-%m-%dT%H:%M:%SZ' } +# Returns 0 if $1 is strictly less than $2 (semver via sort -V). +version_lt() { + [[ "$1" == "$2" ]] && return 1 + [[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -1)" == "$1" ]] +} + +emit_min_version_error() { + local v="$1" + cat >&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") - ua="" - if [[ -n "${JFROG_SKILL_MODEL:-}" ]]; then - ua="model/${JFROG_SKILL_MODEL} " + harness=$(detect_harness) + # Build the parens block: semicolon-separated key=value pairs. + local meta="" + if [[ -n "$harness" ]]; then + meta="tool=${harness}" fi - ua="${ua}jfrog-skills/${skill_version} jfrog-cli-go/${cli_version}" - echo "export JFROG_CLI_USER_AGENT='${ua}'" + if [[ -n "$MODEL_SLUG" ]]; then + if [[ -n "$meta" ]]; then + meta="${meta}; model=${MODEL_SLUG}" + else + meta="model=${MODEL_SLUG}" + fi + fi + ua="jfrog-skills/${skill_version}" + if [[ -n "$meta" ]]; then + ua="${ua} (${meta})" + fi + ua="${ua} jfrog-cli-go/${cli_version}" + printf '%s\n' "$ua" } # Main if [[ "$FORCE" == "false" ]] && is_cache_fresh; then cat "$CACHE_FILE" >&2 + # Re-evaluate the minimum on every run so a bumped MIN_CLI_VERSION + # is enforced without waiting for the 24h cache to expire. + cached_version=$(jq -r '.cli_version // "unknown"' "$CACHE_FILE" 2>/dev/null) + if [[ "$cached_version" != "unknown" ]] && version_lt "$cached_version" "$MIN_CLI_VERSION"; then + emit_min_version_error "$cached_version" + exit 3 + fi emit_skill_env exit 0 fi @@ -135,5 +214,11 @@ exit_code=${exit_code:-0} if (( exit_code == 2 )); then exit 2 fi + +refreshed_version=$(jq -r '.cli_version // "unknown"' "$CACHE_FILE" 2>/dev/null) +if [[ "$refreshed_version" != "unknown" ]] && version_lt "$refreshed_version" "$MIN_CLI_VERSION"; then + emit_min_version_error "$refreshed_version" + exit 3 +fi emit_skill_env exit 1 diff --git a/skills/jfrog/scripts/get-platform-credentials.sh b/skills/jfrog/scripts/get-platform-credentials.sh deleted file mode 100755 index de6278a..0000000 --- a/skills/jfrog/scripts/get-platform-credentials.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash -# get-platform-credentials.sh — Extract JFrog Platform credentials -# -# Extracts URL and access token from jf config export for use with -# plain curl calls to JFrog Platform APIs. -# -# Usage: -# eval "$(bash get-platform-credentials.sh [server-id])" -# -# After eval, these variables are available: -# JFROG_URL — Platform base URL (no trailing slash) -# JFROG_ACCESS_TOKEN — Access token for Bearer auth -# JFROG_RT_URL — Artifactory URL -# JFROG_XR_URL — Xray URL -# JFROG_DS_URL — Distribution URL -# JFROG_MC_URL — Mission Control URL -# JFROG_SERVER_ID — Server ID used - -set -euo pipefail - -SERVER_ID="${1:-}" - -if ! command -v jf &>/dev/null; then - echo "echo 'ERROR: jf CLI is not installed'" >&2 - exit 1 -fi - -if ! command -v jq &>/dev/null; then - echo "echo 'ERROR: jq is not installed'" >&2 - exit 1 -fi - -# Export config as base64-encoded JSON -if [[ -n "$SERVER_ID" ]]; then - CONFIG_JSON=$(jf config export "$SERVER_ID" 2>/dev/null | base64 -d) -else - CONFIG_JSON=$(jf config export 2>/dev/null | base64 -d) -fi - -if [[ -z "$CONFIG_JSON" ]]; then - echo "echo 'ERROR: Failed to export jf config'" >&2 - exit 1 -fi - -# Extract fields -JFROG_URL=$(echo "$CONFIG_JSON" | jq -r '.url // empty') -JFROG_URL="${JFROG_URL%/}" -JFROG_ACCESS_TOKEN=$(echo "$CONFIG_JSON" | jq -r '.accessToken // empty') -JFROG_RT_URL=$(echo "$CONFIG_JSON" | jq -r '.artifactoryUrl // empty') -JFROG_RT_URL="${JFROG_RT_URL%/}" -JFROG_XR_URL=$(echo "$CONFIG_JSON" | jq -r '.xrayUrl // empty') -JFROG_XR_URL="${JFROG_XR_URL%/}" -JFROG_DS_URL=$(echo "$CONFIG_JSON" | jq -r '.distributionUrl // empty') -JFROG_DS_URL="${JFROG_DS_URL%/}" -JFROG_MC_URL=$(echo "$CONFIG_JSON" | jq -r '.missionControlUrl // empty') -JFROG_MC_URL="${JFROG_MC_URL%/}" -JFROG_SERVER_ID=$(echo "$CONFIG_JSON" | jq -r '.serverId // empty') - -if [[ -z "$JFROG_URL" ]]; then - echo "echo 'ERROR: No URL found in jf config export'" >&2 - exit 1 -fi - -if [[ -z "$JFROG_ACCESS_TOKEN" ]]; then - echo "echo 'WARNING: No access token found in jf config export'" >&2 -fi - -# Output as shell variable assignments for eval -cat < @@ -22,6 +23,18 @@ 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 +} + JFROG_PLATFORM_URL="${1:-}" if [[ -z "$JFROG_PLATFORM_URL" ]]; then @@ -31,16 +44,22 @@ fi JFROG_PLATFORM_URL="${JFROG_PLATFORM_URL%/}" -if ! command -v curl &>/dev/null; then - echo "ERROR: curl is not installed" >&2 +if ! command -v jf &>/dev/null; then + echo "ERROR: jf is not installed" >&2 exit 1 fi -# Verify server is reachable -PING_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ - "${JFROG_PLATFORM_URL}/artifactory/api/system/ping") +if ! command -v uuidgen &>/dev/null; then + echo "ERROR: uuidgen is not installed" >&2 + exit 1 +fi -if [[ "$PING_CODE" != "200" ]]; then +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 + PING_CODE=$(jf_api_http_status "$TMPERR") echo "ERROR: Server not reachable at ${JFROG_PLATFORM_URL} (HTTP ${PING_CODE})" >&2 exit 2 fi @@ -50,12 +69,13 @@ SESSION_UUID=$(uuidgen | tr '[:upper:]' '[:lower:]') VERIFY_CODE=${SESSION_UUID: -4} # Register the session with the Access API -REG_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ - "${JFROG_PLATFORM_URL}/access/api/v2/authentication/jfrog_client_login/request" \ +: >"$TMPERR" +if ! jf api /access/api/v2/authentication/jfrog_client_login/request \ + --url "$JFROG_PLATFORM_URL" \ + -X POST \ -H "Content-Type: application/json" \ - -d "{\"session\":\"${SESSION_UUID}\"}") - -if [[ "$REG_CODE" != "200" && "$REG_CODE" != "201" ]]; then + -d "{\"session\":\"${SESSION_UUID}\"}" >/dev/null 2>"$TMPERR"; then + REG_CODE=$(jf_api_http_status "$TMPERR") echo "ERROR: Session registration failed (HTTP ${REG_CODE})" >&2 exit 3 fi diff --git a/skills/jfrog/scripts/jfrog-login-save-credentials.sh b/skills/jfrog/scripts/jfrog-login-save-credentials.sh index 295d584..f6ba42b 100755 --- a/skills/jfrog/scripts/jfrog-login-save-credentials.sh +++ b/skills/jfrog/scripts/jfrog-login-save-credentials.sh @@ -3,6 +3,12 @@ # # 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 (the SKILL.md "Server selection rules" +# require this anyway). # # 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 @@ -28,6 +34,17 @@ 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 +} + JFROG_PLATFORM_URL="${1:-}" SESSION_UUID="${2:-}" @@ -38,7 +55,7 @@ fi JFROG_PLATFORM_URL="${JFROG_PLATFORM_URL%/}" -for cmd in curl jq jf; do +for cmd in jq jf; do if ! command -v "$cmd" &>/dev/null; then echo "ERROR: ${cmd} is not installed" >&2 exit 1 @@ -51,22 +68,33 @@ done JFROG_HOST=$(echo "$JFROG_PLATFORM_URL" | \ sed 's|^[a-z]*://||' | sed 's|\.jfrog\.io.*||' | sed 's|[./]|-|g') -# Retrieve the one-time token +# Retrieve the one-time token (stdout = JSON body; stderr = jf status lines) RESP_FILE="/tmp/jf-login-resp-$$.json" -trap 'rm -f "$RESP_FILE"' EXIT - -HTTP_CODE=$(curl -s -o "$RESP_FILE" -w "%{http_code}" \ - "${JFROG_PLATFORM_URL}/access/api/v2/authentication/jfrog_client_login/token/${SESSION_UUID}") +STDERR_FILE="/tmp/jf-login-err-$$.txt" +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" \ + >"$RESP_FILE" 2>"$STDERR_FILE" +api_exit=$? +set -e + +HTTP_CODE=$(jf_api_http_status "$STDERR_FILE") +if [[ "$HTTP_CODE" == "0" ]]; then + HTTP_CODE=$(jf_api_http_status "$RESP_FILE") +fi -if [[ "$HTTP_CODE" != "200" ]]; then - echo "ERROR: Token retrieval failed (HTTP ${HTTP_CODE})." >&2 +if (( api_exit != 0 )); then + echo "ERROR: Token retrieval failed (HTTP ${HTTP_CODE}, exit ${api_exit})." >&2 if [[ "$HTTP_CODE" == "400" ]]; then echo "The user may not have completed the browser login yet." >&2 fi exit 2 fi -ACCESS_TOKEN=$(jq -r '.access_token // empty' "$RESP_FILE") +ACCESS_TOKEN=$(grep -v '\[Info\]' "$RESP_FILE" | jq -r '.access_token // empty') if [[ -z "$ACCESS_TOKEN" ]]; then echo "ERROR: Response contained no access token. Login must restart from step 1." >&2 @@ -85,13 +113,16 @@ if ! jf config add "$JFROG_HOST" \ exit 4 fi -jf config use "$JFROG_HOST" - echo "SERVER_ID=${JFROG_HOST}" -# Verify authentication echo "--- Verifying authentication ---" -if ! jf rt curl -s "/api/system/version" --server-id="$JFROG_HOST"; then +if ! jf api "/artifactory/api/system/version" --server-id="$JFROG_HOST"; then echo "ERROR: Authentication verification failed. Token may not have saved correctly." >&2 exit 4 fi + +echo +echo "NEXT (mandatory): ask the user whether to make '${JFROG_HOST}' the default jf server." +echo " - If yes: run 'jf config use ${JFROG_HOST}'" +echo " - If no: pass '--server-id=${JFROG_HOST}' on every subsequent jf call" +echo "Do not start any other JFrog operation against this server until this question is asked and answered."