diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index b8d56ee..719191d 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "jfrog", - "version": "0.1.5", + "version": "0.1.6", "description": "JFrog skills and the JFrog MCP server for Codex \u2014 interact with the JFrog Platform.", "author": { "name": "JFrog", diff --git a/package.json b/package.json index 7835809..fa4584c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codex-plugin", - "version": "0.1.5", + "version": "0.1.6", "private": true, "type": "module", "description": "JFrog skills and MCP server plugin for OpenAI Codex.", diff --git a/scripts/sync-skills-vendor.json b/scripts/sync-skills-vendor.json index 7495664..f1891cf 100644 --- a/scripts/sync-skills-vendor.json +++ b/scripts/sync-skills-vendor.json @@ -1,6 +1,6 @@ { "repo": "jfrog/jfrog-skills", - "pin": "v0.34.0", + "pin": "v0.35.0", "paths": [ "skills" ] diff --git a/skills/jfrog-init/references/flow-diagram.md b/skills/jfrog-init/references/flow-diagram.md index 6f77852..8ed406d 100644 --- a/skills/jfrog-init/references/flow-diagram.md +++ b/skills/jfrog-init/references/flow-diagram.md @@ -72,20 +72,11 @@ flowchart TD S6["6. Project resolved?"]:::stepBox S6 -->|state file has current project| ASKREUSE["AskUserQuestion: reuse CURRENT or pick different"]:::fixBox - ASKREUSE -->|reuse| VALPROJ["Validate via authenticated GET /access/api/v1/projects/KEY"]:::stepBox - ASKREUSE -->|different| ASKPROJ["AskUserQuestion: first 2 projects, or Other to type one"]:::fixBox + ASKREUSE -->|reuse| S7 + ASKREUSE -->|different| ASKPROJ["AskUserQuestion: first 2 projects (from enumeration), or Other to type one"]:::fixBox S6 -->|no state file| ASKPROJ - ASKPROJ --> RESOLVE["Resolve name-or-key (case-insensitive) against project list from authenticated GET /access/api/v1/projects"]:::stepBox - RESOLVE -->|no match, 1st attempt| ASKPROJ - RESOLVE -->|no match again, 2nd attempt: give up| F6 - RESOLVE -->|matched| VALPROJ - VALPROJ -->|404 or 403, 1st attempt| ASKPROJ - VALPROJ -->|404 or 403 again, 2nd attempt: give up| F6 - VALPROJ -->|401, credentials rejected| STOPCREDS["STOP: show raw error (re-auth via the Step 3/4 picker)"]:::stopBox - VALPROJ -->|2xx| S7 - - F6["Note: no project resolved after 1 retry — continue without one (non-blocking)"]:::fixBox - F6 --> S7 + ASKPROJ -->|picked or typed — accepted verbatim, no probe, no matching| S7 + ASKPROJ -->|enumeration failed: jf missing / credentials rejected| STOPCREDS["STOP: show raw error (re-auth via the Step 3/4 picker)"]:::stopBox S7["7. AI Catalog reachable and user entitled?"]:::stepBox S7 -->|anon 404 / connection failure / 5xx, exit 1| F7U["Note: catalogReason=unreachable — JPD may not host AI Catalog, or it's down right now (non-blocking)"]:::fixBox diff --git a/skills/jfrog-init/references/project-resolution-branches.md b/skills/jfrog-init/references/project-resolution-branches.md index 501c7c9..51e4e6a 100644 --- a/skills/jfrog-init/references/project-resolution-branches.md +++ b/skills/jfrog-init/references/project-resolution-branches.md @@ -1,4 +1,4 @@ -# Step 6 — resolve/validate branches +# Step 6 — resolve branches **Required behavior for Step 6, not optional background.** Read this in full whenever `jfrog-detect-project.mjs` returns anything other than a @@ -15,49 +15,32 @@ where one is shown, or the Final Summary. — the server-id is ambiguous. Follow "Resolving `` for Steps 4-7" in `SKILL.md` (prompt for a server from `candidates`), then re-invoke Step 6 with the picked server-id as arg 1. -- **No input passed (`ask`, exit 2, no `unresolved`)** → use the - picker/free-form ask from `references/project-picker.md`, then - re-invoke with the picked value as arg 2. -- **Input passed (resolve + validate)** → - - **Exit 0 (green)** → project exists and is accessible; the - canonical key is in the JSON `resolvedKey` field. Proceed to - Step 7 (`jfrog-detect-catalog-runtime.mjs [server-id]`), which - takes no project argument — the input string only needs to be - kept around as arg 2 to `jfrog-detect-all.mjs` itself, so a - re-run re-resolves Step 6 the same way. - - **Exit 1 (red)** → ambiguous input, 404, or 403 — **cap re-asks at - one retry within a single walk.** The first time any of these - three happens, re-run the picker/free-form ask (`project-picker.md`); - if the user's second attempt *also* comes back ambiguous/404/403, - stop asking — proceed to Step 7 without a resolved project - (non-blocking, same pattern as Step 5), and note it in the Final - Summary instead of asking a third time. Never loop indefinitely on - a silently-automatic retry the user didn't explicitly choose to - continue (unlike Step 3's config picker, which loops on an - explicit "did you finish?" the user opts into each time). - - Ambiguous input → `candidates` lists the tied keys; re-run the - picker/free-form ask (using the full `candidatesWithNames`, not - just the tied subset). - - HTTP 404 → project does not exist on this JPD; re-run the - picker/free-form ask. - - HTTP 403 → project exists but the user isn't entitled to this - **specific** one — JFrog project ACLs are per-project, so this - says nothing about any other project. Show the raw error, then - re-run the picker/free-form ask (using `candidatesWithNames`, - same as the 404 case) so the user can pick a different project - instead of dead-ending; mention they can also ask their JFrog - admin for access to the one they tried. - - HTTP 5xx, or the probe could not connect at all → the JPD is - erroring or unreachable right now. Re-picking won't help, so show - the raw error and move on rather than re-running the picker; the - Final Summary reports it via `projectResolved: false`. (Grouped - with the retryable reds rather than with Exit 3 because it is a - transient backend/network condition, not a broken setup — the - same reason `jfrog-detect-catalog-runtime.mjs` calls its own - "can't connect" red.) - - **Exit 3 (error)** → `jf` missing, credentials unavailable/rejected - (including HTTP 401 — this says nothing about whether the project - exists), a 2xx response that wasn't shaped like the real GetProject - endpoint, or an unexpected HTTP code. Show the raw detector error — - this one is a genuine stop, not subject to the retry cap above (no - re-pick can fix bad credentials). +- **No input passed (`ask`/`error`, no `unresolved`)** → enumeration + (`GET /access/api/v1/projects`) runs to drive the picker, and this is + the only remaining way Step 6 can fail to resolve a project: + - **Exit 2 (`ask`)** → use the picker/free-form ask from + `references/project-picker.md`, then re-invoke with the picked + value as arg 2. Whatever the user picks or types is then accepted + verbatim by the Exit 0 branch below — no second round-trip through + matching. + - **Exit 3 (error)** → `jf` missing or credentials unavailable/ + rejected (needed to authenticate the enumeration call). Show the + raw detector error — this is a genuine stop (no re-pick can fix bad + credentials). +- **Input passed (resolve — no matching, no validation)** → accepted + exactly as given, with no network call at all: no per-project + existence/access probe (GetProject requires Platform/Project Admin, + same as the enumeration call — a non-admin caller would just get 403 + from both and never learn whether the project is real), and no + name-or-key matching against the enumerated list either (that also + depended on enumeration succeeding, and could only ever flag two + enumerated projects as ambiguous — never useful to the same + non-admin accounts this exists for). There is no ambiguous (red) + outcome any more; a typed input is always green. + - **Exit 0 (green)** → the input is accepted verbatim; `resolvedKey` + is exactly what was passed, and the detail line says + existence/access was not verified. Proceed to Step 7 + (`jfrog-detect-catalog-runtime.mjs [server-id]`), which takes no + project argument — the input string only needs to be kept around + as arg 2 to `jfrog-detect-all.mjs` itself, so a re-run re-resolves + Step 6 the same way. diff --git a/skills/jfrog-init/scripts/jfrog-detect-project.mjs b/skills/jfrog-init/scripts/jfrog-detect-project.mjs index 55469d9..5084318 100755 --- a/skills/jfrog-init/scripts/jfrog-detect-project.mjs +++ b/skills/jfrog-init/scripts/jfrog-detect-project.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -// Resolves and validates the project key for the walk. The picked value is +// Resolves the project key for the walk. The picked value is // NEVER persisted (no env var, no shell profile) — the caller passes it as // a positional argument on the re-invocation and threads it forward. // @@ -12,42 +12,33 @@ // /access/, off the JPD root, so credentials are resolved via `jf config // export` and a direct fetch is issued instead. // -// Resolution — matches BOTH project_key and display_name (case- -// insensitive; see lib/projects.mjs for the exact tier order). Never -// guesses, never assumes "default", never invents a value. -// -// The enumeration call is cached per server for a few minutes (see -// lib/project-cache.mjs) — the caller re-invokes this script once per -// user attempt within a single walk, and matching is offline anyway, -// so only the first attempt actually hits the network. +// Enumeration only ever runs to populate the interactive picker (no input +// passed) — see "Why no matching either" below for why a passed input skips +// it entirely. Cached per server for a few minutes (see lib/project-cache.mjs) +// since the picker can re-invoke this script more than once per walk. // // Usage: node jfrog-detect-project.mjs [server-id] [project-input] // -// Every non-green result also carries `candidatesWithNames` (up to the -// full enumerated project list, `{key, displayName}`, sorted) whenever +// A no-input `ask` result carries `candidatesWithNames` (up to the full +// enumerated project list, `{key, displayName}`, sorted) whenever // enumeration succeeded, so the caller can offer the first two as an // interactive pick-one-or-type-your-own prompt instead of demanding the -// user type a key or name from memory. A confirmed-not-found input (404) -// additionally carries `similarProjects` — up to 2 "did you mean...?" -// suggestions (see lib/projects.mjs) — when the typed input looks like a -// near-miss of an existing project (e.g. "widgets20" when "widgets2" exists). +// user type a key from memory. // -// Exit 0 -> green (project exists and is accessible; resolved canonical key in detail) -// Exit 1 -> red (project does not exist, no access, ambiguous match, or a -// 5xx from the existence probe — the backend is erroring -// — or the probe could not connect at all. The last two -// are indistinguishable from here, so they share a -// classification, the same way -// jfrog-detect-catalog-runtime.mjs treats its own "000".) +// Exit 0 -> green (input accepted exactly as passed, in `resolvedKey` — +// no matching, no existence/access check; see below) // Exit 2 -> ask (no input passed, or multiple jf servers configured with none // resolvable — caller must prompt the user and re-invoke) -// Exit 3 -> error (jf missing, credentials unavailable/rejected, an -// unexpected non-5xx HTTP code, or a 2xx response that -// wasn't shaped like the real GetProject endpoint) +// Exit 3 -> error (jf missing, or credentials unavailable/rejected) +// +// Matching against the enumerated list is also skipped: GetProjectsList +// requires Platform/Project Admin (same as the removed per-key probe), so +// non-admin callers got 403 either way. A typed input is accepted verbatim; +// enumeration only runs for the interactive picker (ask path) below. -import { emit, isMainModule, resolveCreds, authedFetch, NETWORK_UNREACHABLE_HINT } from "./lib/jf.mjs"; +import { emit, isMainModule, resolveCreds, authedFetch } from "./lib/jf.mjs"; import { resolveServerOrEmit } from "./jfrog-resolve-jf-server.mjs"; -import { resolveProject, projectsWithNames, findSimilarProjects, capCandidatesForDisplay } from "./lib/projects.mjs"; +import { projectsWithNames, capCandidatesForDisplay } from "./lib/projects.mjs"; import { readCachedProjectList, writeCachedProjectList } from "./lib/project-cache.mjs"; // Exported so jfrog-detect-all.mjs can call this in-process instead of @@ -67,7 +58,20 @@ export async function detectProject(serverIdArg, projectInputArg) { return { exitCode: resolved.exitCode }; } const { serverId } = resolved; - const projectInput = projectInputArg || ""; + const projectInput = (projectInputArg || "").trim(); + + // ---------- Resolve branch: an input was passed ---------- + // No matching, no existence/access probe — see "Why no matching either" + // above. Accepted exactly as given; always green. + if (projectInput) { + emit({ + check: "project", + status: "green", + detail: `project ${projectInput} accepted — existence/access on the JPD was not verified`, + resolvedKey: projectInput, + }); + return { exitCode: 0, resolvedKey: projectInput }; + } const creds = resolveCreds(serverId); if (!creds) { @@ -79,132 +83,22 @@ export async function detectProject(serverIdArg, projectInputArg) { return { exitCode: 3 }; } - const rawGet = (path) => authedFetch(creds, path); - - // ---------- Fetch project list first (needed for every branch) ---------- + // ---------- Ask branch: no input passed; return candidates ---------- // Cached per server for a short TTL (lib/project-cache.mjs): the // interactive picker re-invokes this script once per user attempt within - // the same walk, and re-enumerating on every typed guess is wasted - // network traffic — matching/similarity search below already runs - // offline against whatever list we have in memory. + // the same walk, and re-enumerating on every attempt is wasted network + // traffic. let list; const cachedBody = readCachedProjectList(serverId, creds); if (cachedBody !== null) { list = { code: 200, body: cachedBody }; } else { - list = await rawGet("/access/api/v1/projects"); + list = await authedFetch(creds, "/access/api/v1/projects"); if (list.code >= 200 && list.code < 300) writeCachedProjectList(serverId, creds, list.body); } const enumOk = list.code >= 200 && list.code < 300; const candidatesWithNames = enumOk ? projectsWithNames(list.body) : []; - // ---------- Resolve+validate branch: an input was passed ---------- - if (projectInput) { - let resolvedKey = projectInput; - if (enumOk) { - const resolved = resolveProject(candidatesWithNames, projectInput); - if (resolved?.tied) { - emit({ - check: "project", - status: "red", - detail: `input "${projectInput}" matches multiple projects; be more specific`, - candidates: resolved.tied, - ...capCandidatesForDisplay(candidatesWithNames), - }); - return { exitCode: 1 }; - } - if (resolved?.key) resolvedKey = resolved.key; - // No match against the enumeration — could still be a valid key the - // enumeration missed (paging, ACL), so let the existence probe below - // be the ultimate arbiter. - } - - // Encode as a single path segment (not just URI-escape) so an input like - // "../projects" can't change which endpoint gets hit. - const projectPath = `/access/api/v1/projects/${encodeURIComponent(resolvedKey)}`; - const probe = await rawGet(projectPath); - // A 2xx status alone isn't proof this is really the GetProject response — - // a captive portal or misrouted network can also answer 200. Require the - // expected shape (an object carrying the project's own key) too, the - // same guard jfrog-detect-catalog-runtime.mjs applies to its endpoint. - const looksLikeProject = - probe.body && - typeof probe.body === "object" && - (typeof probe.body.project_key === "string" || typeof probe.body.projectKey === "string"); - if (probe.code >= 200 && probe.code < 300 && looksLikeProject) { - emit({ check: "project", status: "green", detail: `project ${resolvedKey} exists and is accessible (HTTP ${probe.code})`, resolvedKey }); - return { exitCode: 0, resolvedKey }; - } - if (probe.code >= 200 && probe.code < 300 && !looksLikeProject) { - emit({ - check: "project", - status: "error", - detail: `got HTTP ${probe.code} from ${creds.baseUrl}${projectPath} but the response wasn't the expected project shape — this may not be the JPD's real endpoint (captive portal / proxy?)`, - }); - return { exitCode: 3 }; - } - if (probe.code >= 500 && probe.code < 600) { - // A 5xx means the backend itself is erroring, same as a connection - // failure from the caller's perspective — treat it as "red", not - // "error", matching jfrog-detect-catalog-runtime.mjs and - // jfrog-detect-server-ping.mjs's classification of the same code class. - emit({ check: "project", status: "red", detail: `${creds.baseUrl}${projectPath} returned HTTP ${probe.code} — the backend is erroring` }); - return { exitCode: 1 }; - } - if (probe.code === 404) { - const similarProjects = enumOk ? findSimilarProjects(candidatesWithNames, projectInput) : []; - emit({ - check: "project", - status: "red", - detail: `no project matches "${projectInput}" on this JPD — pick a different one`, - resolvedKey, - ...capCandidatesForDisplay(candidatesWithNames), - ...(similarProjects.length > 0 ? { similarProjects } : {}), - }); - return { exitCode: 1 }; - } - if (probe.code === 401) { - // Unlike 403, a 401 means the credentials themselves were rejected — - // this says nothing about whether the project exists. - emit({ - check: "project", - status: "error", - detail: `cannot verify project ${resolvedKey}: /access rejected the credentials in jf config (HTTP 401). Re-run \`jf config add --interactive\`.`, - }); - return { exitCode: 3 }; - } - if (probe.code === 403) { - // ACLs are per-project — not entitled to this one says nothing about - // any other, so carry candidatesWithNames the same as the 404 branch - // to let the caller re-offer the picker instead of dead-ending. - emit({ - check: "project", - status: "red", - detail: `project ${resolvedKey} exists but your user is not entitled to see it (HTTP 403) — pick a project you have access to, or contact your JFrog admin`, - ...capCandidatesForDisplay(candidatesWithNames), - }); - return { exitCode: 1 }; - } - if (probe.code === 0) { - // Red, not error — the 5xx branch above treats "the backend is - // erroring" as red precisely because it's indistinguishable from a - // connection failure from here, and - // jfrog-detect-catalog-runtime.mjs maps its own "000" to red too. - // Classifying the real thing as an error would make the same - // condition blocking in Step 6 and non-blocking in Step 7. - emit({ - check: "project", - status: "red", - detail: `could not reach ${creds.baseUrl}${projectPath} (connection failed)${NETWORK_UNREACHABLE_HINT}`, - ...capCandidatesForDisplay(candidatesWithNames), - }); - return { exitCode: 1 }; - } - emit({ check: "project", status: "error", detail: `project validation returned unexpected HTTP ${probe.code} for ${creds.baseUrl}${projectPath}` }); - return { exitCode: 3 }; - } - - // ---------- Ask branch: no input passed; return candidates ---------- // Branches on `enumOk`, not `candidatesWithNames.length`, so a JPD with // zero accessible projects (enumeration succeeded, list is empty) isn't // misreported as "enumeration was not available". @@ -220,6 +114,9 @@ export async function detectProject(serverIdArg, projectInputArg) { : "no project chosen — ask the user which project to use (no projects are accessible on this JPD)", ...(candidatesWithNames.length > 0 ? { candidates: shown.candidatesWithNames.map((p) => p.key), ...shown } : {}), }); + } else if (list.code === 401 || list.code === 403) { + emit({ check: "project", status: "error", detail: `project enumeration failed: /access rejected credentials (HTTP ${list.code}). Re-run \`jf config add --interactive\`.` }); + return { exitCode: 3 }; } else { emit({ check: "project", status: "ask", detail: "no project chosen — ask the user which project to use (project enumeration was not available)" }); } diff --git a/skills/jfrog/SKILL.md b/skills/jfrog/SKILL.md index c47dffa..7ca1520 100644 --- a/skills/jfrog/SKILL.md +++ b/skills/jfrog/SKILL.md @@ -19,7 +19,7 @@ compatibility: >- (CLI) and Tier 3 (jf api) operations; without it, only MCP (Tier 1) is available. metadata: role: base - version: "0.34.0" + version: "0.35.0" --- # JFrog Skill