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 .cursor-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
},
"metadata": {
"description": "JFrog Platform plugins for Cursor",
"version": "0.5.13",
"version": "0.5.14",
"pluginRoot": "plugins"
},
"plugins": [
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.8.1",
"pin": "jfrog-agent-hooks/v0.10.0",
"paths": [
"modules"
]
Expand Down
6 changes: 4 additions & 2 deletions .github/scripts/sync-skills-vendor.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"repo": "jfrog/jfrog-skills",
"pin": "v0.22.0",
"paths": ["skills"]
"pin": "v0.23.0",
"paths": [
"skills"
]
}
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,28 @@ Agent Package Resolution is in preview and opt-in. To get started:
- **Users:** see the [User Guide](docs/package-resolution-user-guide.md).
- **Admins:** see the [Admin Guide](docs/package-resolution-admin-guide.md).

### Confirming it is installed

Agent Package Resolution runs from a `sessionStart` hook, so there are two things worth knowing before you go looking for it:

- **Restart Cursor after installing the plugin.** Hooks are registered when Cursor starts, so a session that was already open when you installed will not run it.
- **The hook is not written into your own configuration.** It stays inside the plugin (`plugins/jfrog/hooks/hooks.json`) and is merged at runtime, so it does not appear in `~/.cursor/hooks.json` — finding nothing there does not mean the install failed.

To confirm it is running, start a new session and check the hook log:

```bash
tail ~/.jfrog/logs/agent-hooks.log
```

You should see one entry per session naming the mode it resolved to:

| Mode | Meaning |
| --- | --- |
| `off` | Not enabled yet — expected until an admin or you turn it on (see the guides above) |
| `pending` | Enabled, but JFrog identity is missing, unusable, or rejected — nothing is routed yet |
| `routing` | Enabled and configured — packages resolve through Artifactory |

For more detail, set `"logLevel": "debug"` in `~/.jfrog/agents-conf.json` and start another session.
---

## Usage
Expand Down
2 changes: 1 addition & 1 deletion plugins/jfrog/.cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "jfrog",
"displayName": "JFrog Platform",
"version": "0.5.13",
"version": "0.5.14",
"description": "JFrog Platform integration with MCP, security skills, Agent Package Resolution, supply-chain best practices, and JFrog Agent Guard governance for adding, removing, and listing MCP servers.",
"author": {
"name": "JFrog",
Expand Down
11 changes: 1 addition & 10 deletions plugins/jfrog/modules/assets/agents-default-conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,7 @@
"enabled": false,
"verifyRepos": true,
"cacheTtlDays": 7,
"defaultGlobalRepos": {
"npm": "npm-virtual",
"pypi": "pypi-virtual",
"maven": "maven-virtual",
"gradle": "gradle-virtual",
"go": "go-virtual",
"docker": "docker-virtual",
"helm": "helm-virtual",
"nuget": "nuget-virtual"
},
"defaultGlobalRepos": {},
"autoSetup": []
}
}
12 changes: 10 additions & 2 deletions plugins/jfrog/modules/claude-session-start.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,16 @@
import process from "node:process";

import { runCapability } from "./core/run-capability.mjs";
import { ensureAgentsConfigScaffold, agentsConfigLoadWarnings } from "./core/agents-config.mjs";
import { readStdin, parseSessionId, detectHarness, parseWorkspaceRoots } from "./core/io.mjs";
import {
ensureAgentsConfigScaffold,
agentsConfigLoadWarnings,
} from "./core/agents-config.mjs";
import {
readStdin,
parseSessionId,
detectHarness,
parseWorkspaceRoots,
} from "./core/io.mjs";
import { setLogContext, createLogger } from "./core/logger.mjs";

const HARNESS_ID = "claude_code";
Expand Down
90 changes: 90 additions & 0 deletions plugins/jfrog/modules/copilot-session-start.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env node
// GitHub Copilot Chat SessionStart hook runner (installed via the VS Code
// Copilot plugin — see jfrog/vscode-plugin).
//
// Usage: node copilot-session-start.mjs <capability>
// Example: node copilot-session-start.mjs package-resolution
//
// stdout: JSON with hookSpecificOutput.additionalContext. "{}" is a no-op.

import process from "node:process";

import { runCapability } from "./core/run-capability.mjs";
import {
ensureAgentsConfigScaffold,
agentsConfigLoadWarnings,
} from "./core/agents-config.mjs";
import {
readStdin,
parseSessionId,
detectHarness,
parseWorkspaceRoots,
} from "./core/io.mjs";
import { setLogContext, createLogger } from "./core/logger.mjs";

const HARNESS_ID = "copilot";
const log = createLogger("session-start");

/** @returns {string | null} JSON stdout payload, or null when there is nothing to inject. */
function formatSessionStartStdout(text) {
if (!text?.trim()) return null;
return JSON.stringify({
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: text,
},
});
}

function writeStdout(payload) {
if (payload === null) {
writeNoOp();
return;
}
process.stdout.write(payload);
}

function writeNoOp() {
process.stdout.write("{}");
}

async function main() {
const capability = process.argv[2];
if (!capability) {
writeNoOp();
return;
}

const startedAtMs = Date.now();
const stdinRaw = await readStdin();
const harness = detectHarness(stdinRaw);
if (harness && harness !== HARNESS_ID) {
setLogContext({ ide: HARNESS_ID, sessionId: parseSessionId(stdinRaw) });
log.warn("harness mismatch; wrong adapter invoked", {
expected: HARNESS_ID,
detected: harness,
adapter: "copilot-session-start",
});
writeNoOp();
return;
}
const sessionId = parseSessionId(stdinRaw);
const workspaceRoots = parseWorkspaceRoots(stdinRaw);
setLogContext({ ide: HARNESS_ID, sessionId });
ensureAgentsConfigScaffold();
for (const w of agentsConfigLoadWarnings()) {
log.warn(w.message, { path: w.path });
}
const text = await runCapability(capability, {
ide: HARNESS_ID,
sessionId,
workspaceRoots,
startedAtMs,
});
writeStdout(formatSessionStartStdout(text));
}

main().catch(() => {
writeNoOp();
process.exit(0);
});
9 changes: 5 additions & 4 deletions plugins/jfrog/modules/core/agents-config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { homedir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { isSafeRepoKey } from "../package-resolution/scripts/repo-types.mjs";

/** modules bundle root (parent of core/ and assets/). */
const PLUGIN_ROOT = path.resolve(
Expand Down Expand Up @@ -174,9 +175,9 @@ export function getGlobalLogLevel() {
}

/**
* Package types the admin declares globally (governance source). Governance is
* the UNION of these and any workspace `.jfrog/local` repositories; the workspace
* side is added by the resolver (workspace-dependent, per-session).
* Package types the admin declares globally (the governance boundary).
* Workspace files may override repository keys for these types but cannot add
* new governed types.
* @returns {string[]} defaultGlobalRepos keys (unordered)
*/
export function globalDeclaredTypes() {
Expand Down Expand Up @@ -233,7 +234,7 @@ export function normalizeRepoMap(raw) {
if (!raw || typeof raw !== "object") return {};
const out = {};
for (const [type, key] of Object.entries(raw)) {
if (typeof key === "string" && key.trim()) out[type] = key.trim();
if (isSafeRepoKey(key?.trim())) out[type] = key.trim();
}
return out;
}
37 changes: 28 additions & 9 deletions plugins/jfrog/modules/core/io.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Shared stdin helpers for the subprocess-style adapters (Claude, Cursor).
// Shared stdin helpers for subprocess-style adapters (Claude, Cursor, VS Code).
//
// Hooks deliver their JSON payload on stdin immediately; in non-hook contexts
// (CI, npm scripts, terminal smoke tests) nothing arrives, so we bail out after
Expand Down Expand Up @@ -71,29 +71,47 @@ export function parseSessionId(stdinRaw) {
}
}

// Claude's documented SessionStart sources. VS Code Copilot documents only
// "new", so the two sets stay disjoint and neither can claim the other's
// sessions.
const CLAUDE_SESSION_SOURCES = new Set([
"startup",
"resume",
"clear",
"compact",
]);

// Positively identify the harness that invoked this hook from its stdin
// payload. Returns "cursor", "claude_code", or null when it can't tell
// (no stdin — e.g. terminal smoke tests — or an unrecognized shape).
// payload. Returns "cursor", "copilot", "claude_code", or null when no harness
// left a fingerprint (no stdin — e.g. terminal smoke tests — or a shape none of
// them own).
//
// Why this matters: Cursor reads sessionStart hooks from BOTH
// ~/.cursor/hooks.json AND ~/.claude/settings.json. Without this, a Cursor
// session fires the Claude adapter too, double-injecting the policy. Each
// adapter uses this to no-op when a different harness invoked it.
//
// Cursor: cursor_version / agent_type. Claude: transcript_path / hook_event_name /
// session_id. Cursor also reads ~/.claude/settings.json, so each adapter no-ops
// when a different harness invoked it.
// Every branch below is a signal exactly one harness documents, and null means
// "can't tell". An adapter is only ever registered by the harness it serves, so
// a payload no harness claims is left to whichever adapter was invoked.
export function detectHarness(stdinRaw) {
if (!stdinRaw) return null;
try {
const p = JSON.parse(stdinRaw);
if (!p) return null;
// Cursor stamps its own version/agent on every hook payload.
if (p.cursor_version || p.agent_type === "cursor") {
return "cursor";
}
if (p.transcript_path || p.hook_event_name || p.session_id) {
return "claude_code";
if (p.hook_event_name === "SessionStart") {
// Copilot's documented `new` source is decisive. Current VS Code payloads
// also include a transcript_path, so path presence cannot classify Claude
// before the source is checked.
if (p.source === "new") return "copilot";
if (CLAUDE_SESSION_SOURCES.has(p.source)) return "claude_code";
}
// Claude writes a transcript for non-SessionStart hooks too.
if (p.transcript_path) return "claude_code";
} catch {
// stdin wasn't JSON — can't tell.
}
Expand All @@ -102,7 +120,8 @@ export function detectHarness(stdinRaw) {

/**
* Workspace roots for this hook invocation.
* Cursor: workspace_roots[]. Claude: payload cwd. Fallback: process.cwd().
* Cursor: workspace_roots[]. Claude and VS Code Copilot: payload cwd.
* Fallback: process.cwd().
*
* @param {string} [stdinRaw]
* @returns {string[]}
Expand Down
Loading
Loading