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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "jfrog",
"version": "0.1.10",
"version": "0.1.11",
"description": "JFrog skills and the JFrog MCP server for Codex \u2014 interact with the JFrog Platform.",
"author": {
"name": "JFrog",
Expand Down
2 changes: 1 addition & 1 deletion .github/scripts/sync-modules-vendor.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"repo": "JFROG/jfrog-agent-hooks",
"pin": "jfrog-agent-hooks/v0.12.1",
"pin": "jfrog-agent-hooks/v0.12.2",
"paths": [
"modules"
]
Expand Down
30 changes: 12 additions & 18 deletions docs/package-resolution-admin-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,21 @@ Route AI-assisted package installs through your JFrog Artifactory repositories w

Agent Package Resolution runs at the start of each agent session. When enabled, it injects routing policy and resolved Artifactory URLs into the session so the agent prefers your repositories over public registries. Durable enforcement still comes from **package manager configuration** (`jf setup`) and **JFrog Curation** on the server.

This guide is for **platform administrators** and **developers** onboarding the JFrog coding-agent plugins. For installing the plugin itself, see the JFrog documentation for your IDE ([Cursor](https://docs.jfrog.com/ai-ml/docs/cursor), [Claude Code](https://docs.jfrog.com/ai-ml/docs/claude-code/), [VS Code](https://docs.jfrog.com/ai-ml/docs/vs-code)) or this repository's [README](../README.md) for **Codex**.
This guide is for **platform administrators** and **developers** onboarding the JFrog coding-agent plugins. For installing the plugin itself, see the JFrog documentation for your IDE ([Cursor](https://docs.jfrog.com/ai-ml/docs/cursor), [Claude Code](https://docs.jfrog.com/ai-ml/docs/claude-code/), [VS Code](https://docs.jfrog.com/ai-ml/docs/vs-code)) or the [Codex plugin README](https://github.com/jfrog/codex-plugin#installation) for Codex.

> **Related:** [Use the MCP Registry with Agent Guard](https://docs.jfrog.com/ai-ml/docs/configure-coding-agents) covers MCP governance. Agent Package Resolution is a separate capability in the same JFrog plugin family and uses the same local configuration file for admin settings.

---

## Setup summary

| Step | Action |
| ---- | ---------------------------------------------------------------------------------------------------------- |
| 1 | Install the JFrog plugin in your coding assistant |
| 2 | Install and configure the JFrog CLI (`jf config add`) — required for **routing** mode |
| 3 | Confirm `~/.jfrog/agents-conf.json` (shipped template enables APR with empty bindings; or deploy your own) |
| 4 | **Codex:** restart, then `/hooks` and trust the SessionStart command (plugin install does not skip this) |
| 5 | Start a **new agent session** — policy and URLs are injected once per session |

| Step | Action |
| ---- | ------------------------------------------------------------------------------------------------------------------------- |
| 1 | Install the JFrog plugin in your coding assistant |
| 2 | Install and configure the JFrog CLI (`jf config add`) — required for **routing** mode |
| 3 | If an administrator pre-deployed `.jfrog/agents-conf.json`, confirm it; otherwise continue—the first session creates it |
| 4 | **Codex:** after install, restart, then `/hooks` and trust the SessionStart command (plugin install does not skip this — see the [Codex plugin README](https://github.com/jfrog/codex-plugin#installation)) |
| 5 | Start a **new agent session** — missing config is created, then policy and URLs are injected once per session |

### Codex install and hook trust

Expand All @@ -35,16 +34,14 @@ After `codex plugin add jfrog@codex-plugin`:

The hook command uses `${PLUGIN_ROOT}` (Codex also sets `CLAUDE_PLUGIN_ROOT` for compatibility). Local `make install-codex` from `jfrog-agent-hooks` writes a different command into `~/.codex/hooks.json`; that trust hash does **not** cover the plugin definition. Uninstall the local hook before smoke-testing the plugin so you are not approving two SessionStart entries.

The shipped template turns Agent Package Resolution **on** (`enabled: true`) with empty `defaultGlobalRepos`. Nothing is routed until Consent Enable or an administrator adds bindings. Set `enabled: false` or `JF_AGENT_PACKAGE_RESOLUTION_DISABLE=1` to keep it off.

The shipped template turns Agent Package Resolution **on** (`enabled: true`) with empty `defaultGlobalRepos`. Nothing is routed until Consent Enable or an administrator adds bindings. `JF_AGENT_PACKAGE_RESOLUTION_DISABLE=1` always keeps it off; `enabled: false` alone only keeps it off if you deploy your own `agents-conf.json` — on the plugin's auto-created default file it is not durable (see below).

**At a glance:**

- **Default:** on, but routes nothing until you add repositories.
- **To route installs:** add repository keys under `defaultGlobalRepos` (org config or Consent Enable).
- **To turn it off org-wide:** deploy your own `agents-conf.json` with `"enabled": false` (see [Turning Agent Package Resolution off](#turning-agent-package-resolution-off-admins)). Setting `"enabled": false` on the plugin's **default file without also deploying your own** is not durable — the plugin re-enables it on the next session.


---

## Prerequisites
Expand Down Expand Up @@ -151,9 +148,6 @@ the `jfrog` skill + `verify-repo`, or manually — see
With default `verifyRepos: true`, Consent Enable / `configure.mjs enable` accepts
keys Artifactory confirms as virtual repositories of the requested package type.

---


### Turning Agent Package Resolution off (admins)

> **Why `enabled: false` alone may not stick.** Because the feature now ships **on**, the plugin re-enables its **own default file** if it finds it still turned off. "Default file" means the `agents-conf.json` the plugin auto-created and that no one has changed except (at most) the `enabled` flag. As soon as you deploy your **own** config, or add any other setting (like `onboardingPrompt`), the plugin treats it as yours and never re-enables it.
Expand All @@ -179,6 +173,8 @@ keys Artifactory confirms as virtual repositories of the requested package type.

Setting **only** `"onboardingPrompt": "off"` stops the Consent Enable prompts but does **not** turn the feature off — leave `enabled: false` in place for that. See also [emergency disable](#environment-variable-emergency-disable) for the environment variable.

---

## Admin control: deploy `agents-conf.json` across your organization

Use standard endpoint management to place a consistent `agents-conf.json` on every developer machine.
Expand All @@ -200,8 +196,6 @@ Use standard endpoint management to place a consistent `agents-conf.json` on eve
| Auto-configure package managers at first session | Add types to `autoSetup` ([Zero-touch setup](#zero-touch-setup-autosetup)) |
| Force all cached state to refresh | Set `"cacheTtlDays": 0` (this also re-runs eligible zero-touch `jf setup` each session), or edit `agents-conf.json` |
| Support troubleshooting | Set `"logLevel": "debug"` temporarily; logs go to `~/.jfrog/logs/agent-hooks.log` |
| Keep APR **off** (durable) | Deploy your own file with `"enabled": false`, **or** set `"enabled": false` **and** `"onboardingPrompt": "off"` on the plugin's default file — see [Turning off](#turning-agent-package-resolution-off-admins) |
| Silence Consent Enable offers only | Set `"onboardingPrompt": "off"` (does not disable APR while `enabled` is `true`) |

---

Expand Down Expand Up @@ -529,5 +523,5 @@ Workspace values win over `agents-conf.json` for matching types during that sess
- [Install JFrog Plugin for Cursor](https://docs.jfrog.com/ai-ml/docs/install-jfrog-plugin-for-cursor)
- [Install JFrog Plugin for Claude Code](https://docs.jfrog.com/ai-ml/docs/install-jfrog-plugin-for-claude-code)
- [Install JFrog Plugin for VS Code](https://docs.jfrog.com/ai-ml/docs/install-jfrog-plugin-for-vs-code)
- [Install the JFrog plugin for Codex](../README.md#installation)
- [Install JFrog Plugin for Codex](https://github.com/jfrog/codex-plugin#installation)
- [Use the MCP Registry with Agent Guard](https://docs.jfrog.com/ai-ml/docs/configure-coding-agents)
11 changes: 10 additions & 1 deletion modules/core/agent-guard-check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { execFileSync } from "node:child_process";
import process from "node:process";

import { isMainEntry } from "./entry.mjs";
import { jfInvocation } from "./jf-process.mjs";
import { skillsProductUserAgent } from "./jf-user-agent.mjs";

export const SETTINGS_PATH =
Expand Down Expand Up @@ -88,6 +89,7 @@ export function resolveAgentGuardCredentials(opts = {}) {
if (explicitServerId) {
const fromCli = resolveFromCliConfig({
serverId: explicitServerId,
env,
execFileSyncFn: execFn,
debug,
});
Expand All @@ -114,6 +116,7 @@ export function resolveAgentGuardCredentials(opts = {}) {
if (explicitServerId) return null;
return resolveFromCliConfig({
serverId: undefined,
env,
execFileSyncFn: execFn,
debug,
});
Expand All @@ -122,22 +125,28 @@ export function resolveAgentGuardCredentials(opts = {}) {
/**
* @param {{
* serverId?: string,
* env?: NodeJS.ProcessEnv,
* execFileSyncFn?: typeof execFileSync,
* debug?: (message: string) => void,
* }} opts
*/
function resolveFromCliConfig(opts) {
const debug = opts.debug ?? (() => {});
const env = opts.env ?? process.env;
const execFn = opts.execFileSyncFn ?? execFileSync;
const exportArgs = opts.serverId
? ["config", "export", opts.serverId]
: ["config", "export"];
const invocation = jfInvocation(exportArgs, { env });
let exported;
try {
exported = execFn("jf", exportArgs, {
exported = execFn(invocation.command, invocation.args, {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
timeout: 2000,
windowsHide: true,
shell: false,
env,
}).trim();
} catch (error) {
debug(
Expand Down
4 changes: 2 additions & 2 deletions modules/core/jf-identity.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
// async.

import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import process from "node:process";

import { createLogger } from "./logger.mjs";
import { spawnJfSync } from "./jf-process.mjs";
import { skillsProductUserAgent } from "./jf-user-agent.mjs";

const log = createLogger("jf-identity");
Expand Down Expand Up @@ -82,7 +82,7 @@ function jfConfigIdentity(serverId) {

let result;
try {
result = spawnSync("jf", args, {
result = spawnJfSync(args, {
encoding: "utf8",
timeout: 2000,
stdio: ["ignore", "pipe", "pipe"],
Expand Down
50 changes: 50 additions & 0 deletions modules/core/jf-process.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Shell-free JFrog CLI process boundary shared by coding-agent hooks.
//
// Production invokes the native CLI directly (`jf.exe` on Windows, `jf`
// elsewhere). Tests may substitute the Node fake only when the explicit test
// harness gate is active; normal production environments ignore the driver.

import { spawnSync } from "node:child_process";
import process from "node:process";

/**
* Resolve the shell-free executable and argv for a JFrog CLI invocation.
* @param {string[]} args
* @param {{ env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform }} [options]
* @returns {{ command: string, args: string[] }}
*/
export function jfInvocation(
args,
{ env = process.env, platform = process.platform } = {},
) {
const testDriver =
env.JFROG_TEST_HARNESS === "1" ? env.JFROG_TEST_JF_DRIVER : undefined;
return testDriver
? { command: process.execPath, args: [testDriver, ...args] }
: { command: platform === "win32" ? "jf.exe" : "jf", args };
}

/**
* Synchronously invoke the JFrog CLI without a shell.
*
* Test-only injection requires both:
* JFROG_TEST_HARNESS=1
* JFROG_TEST_JF_DRIVER=/absolute/path/to/fake-jf.mjs
*
* @param {string[]} args
* @param {import("node:child_process").SpawnSyncOptionsWithStringEncoding} [options]
* @returns {import("node:child_process").SpawnSyncReturns<string>}
*/
export function spawnJfSync(args, options = {}) {
const env = options.env ?? process.env;
const spawnSyncFn = options.spawnSyncFn ?? spawnSync;
const rest = { ...options };
delete rest.spawnSyncFn;
const invocation = jfInvocation(args, { env });
return spawnSyncFn(invocation.command, invocation.args, {
windowsHide: true,
shell: false,
...rest,
env,
});
}
6 changes: 3 additions & 3 deletions modules/core/jf-user-agent.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
// Spawn env is inherited so CLI DetectExecutionContext can append
// ai-agent/ / ai-client/ / ai-model/ when those signals exist at jf start.

import { spawnSync } from "node:child_process";
import { spawnJfSync } from "./jf-process.mjs";

// Plugin sync stamps this literal with the release semver (jfrog-sync-modules.py
// stamp). Only `modules/` is vendored, so nothing outside this tree is readable
// at runtime. Unstamped trees (this repo, local dev) report 0.0.0.
const PKG_VERSION = "0.12.1";
const PKG_VERSION = "0.12.2";

/** Product UA for plugin `fetch()` (no trigger, no jfrog-cli-go). */
export function skillsProductUserAgent() {
Expand Down Expand Up @@ -55,7 +55,7 @@ function resolveCliVersion(env = process.env) {
try {
// Keep process PATH/HOME even when callers pass a sparse env object
// (unit tests often pass only UA-related keys).
const res = spawnSync("jf", ["--version"], {
const res = spawnJfSync(["--version"], {
encoding: "utf8",
timeout: 3000,
env: { ...process.env, ...env },
Expand Down
6 changes: 5 additions & 1 deletion modules/core/rewrite-mcp-json.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import path from "node:path";
import process from "node:process";

import { EXIT_ENABLED, runAgentGuardCheck } from "./agent-guard-check.mjs";
import { jfInvocation } from "./jf-process.mjs";
import { createLogger } from "./logger.mjs";

const log = createLogger("rewrite-mcp-json");
Expand Down Expand Up @@ -285,13 +286,16 @@ export function pickDefaultJfCliServer(servers) {
export function listJfCliServers(opts = {}) {
const env = opts.env ?? process.env;
const spawnSyncFn = opts.spawnSyncFn ?? spawnSync;
const invocation = jfInvocation(["config", "show", "--format=json"], { env });
let res;
try {
res = spawnSyncFn("jf", ["config", "show", "--format=json"], {
res = spawnSyncFn(invocation.command, invocation.args, {
encoding: "utf8",
timeout: 5_000,
env,
stdio: ["ignore", "pipe", "pipe"],
windowsHide: true,
shell: false,
});
} catch {
return [];
Expand Down
10 changes: 8 additions & 2 deletions modules/package-resolution/scripts/apr-heartbeat.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import path from "node:path";
import { createLogger } from "../../core/logger.mjs";
import { getPlatformIdentity } from "../../core/jf-identity.mjs";
import { envWithHookUserAgent } from "../../core/jf-user-agent.mjs";
import { jfInvocation } from "../../core/jf-process.mjs";

const log = createLogger("apr-heartbeat");

Expand Down Expand Up @@ -183,12 +184,16 @@ export function tryAcquireHeartbeatLock(serverId, opts = {}) {
* Detached `jf rt ping --server-id <id>` with hook User-Agent.
* Waits for spawn success vs async error before unref.
* @param {string} serverId
* @param {{ spawn?: typeof spawn, env?: NodeJS.ProcessEnv }} [opts]
* @param {{ spawn?: typeof spawn, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform }} [opts]
* @returns {Promise<boolean>} true if the process started
*/
export function spawnHeartbeatPing(serverId, opts = {}) {
const spawnImpl = opts.spawn ?? spawn;
const env = opts.env ?? process.env;
const invocation = jfInvocation(["rt", "ping", "--server-id", serverId], {
env,
platform: opts.platform,
});
return new Promise((resolve) => {
let settled = false;
const finish = (ok) => {
Expand All @@ -198,8 +203,9 @@ export function spawnHeartbeatPing(serverId, opts = {}) {
};
let child;
try {
child = spawnImpl("jf", ["rt", "ping", "--server-id", serverId], {
child = spawnImpl(invocation.command, invocation.args, {
detached: true,
windowsHide: true,
stdio: "ignore",
env: envWithHookUserAgent(env),
});
Expand Down
11 changes: 7 additions & 4 deletions modules/package-resolution/scripts/eager-setup.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ import {
packageManagerBinaryOnPath,
} from "./package-manager-family.mjs";
import { detectSetupConflict } from "./setup-conflict.mjs";
import { isMainEntry } from "../../core/entry.mjs";
import { spawnJfSync } from "../../core/jf-process.mjs";
import { envWithHookUserAgent } from "../../core/jf-user-agent.mjs";

const log = createLogger("eager-setup");
Expand Down Expand Up @@ -247,6 +249,7 @@ function spawnWorker(payloadB64, jobCount = 1) {
if (process.env.JFROG_EAGER_SETUP_SYNC === "1") {
spawnSync(process.execPath, [workerPath(), "--run", payloadB64], {
stdio: "ignore",
windowsHide: true,
env: process.env,
timeout: syncWorkerTimeoutMs(jobCount),
});
Expand All @@ -255,6 +258,7 @@ function spawnWorker(payloadB64, jobCount = 1) {
try {
const child = spawn(process.execPath, [workerPath(), "--run", payloadB64], {
detached: true,
windowsHide: true,
stdio: "ignore",
env: process.env,
});
Expand Down Expand Up @@ -643,7 +647,7 @@ export function releaseLock() {
function supportedPackageManagers() {
try {
// --help is local (no Artifactory traffic); no UA needed for telemetry.
const res = spawnSync("jf", ["setup", "--help"], {
const res = spawnJfSync(["setup", "--help"], {
encoding: "utf8",
timeout: 5000,
env: process.env,
Expand Down Expand Up @@ -701,7 +705,7 @@ function runJfSetup(packageManager, serverId, repoKey) {
"--repo",
repoKey,
];
const res = spawnSync("jf", args, {
const res = spawnJfSync(args, {
encoding: "utf8",
timeout: PER_PACKAGE_MANAGER_TIMEOUT_MS,
env: envWithHookUserAgent(process.env),
Expand Down Expand Up @@ -838,8 +842,7 @@ export async function runWorker(payload) {
// CLI entry (worker mode)
// ---------------------------------------------------------------------------

const isMain = import.meta.url === `file://${process.argv[1]}`;
if (isMain && process.argv[2] === "--run") {
if (isMainEntry(import.meta.url) && process.argv[2] === "--run") {
const b64 = process.argv[3];
try {
const payload = JSON.parse(Buffer.from(b64, "base64").toString("utf8"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ Do not delegate governed installs to `agent -p` or another child while routing i
## After setup — load the routing policy

Once `jf` is configured and `jfrog-setup-package-managers` has bound package
managers, load the current routing policy:
managers, load the current routing policy. Run this command:

```bash
```text
{{REFRESH_COMMAND}}
```

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codex-plugin",
"version": "0.1.10",
"version": "0.1.11",
"private": true,
"type": "module",
"description": "JFrog skills and MCP server plugin for OpenAI Codex.",
Expand Down
Loading