-
Notifications
You must be signed in to change notification settings - Fork 1
Add Copilot package-resolution hook to the VS Code plugin #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5d9c909
09a3141
c677ec2
7ac4a73
eafdc9b
2bd079e
7b8a514
36ef43f
1509c43
95dc527
b56aa03
a34693c
b3fc5e8
a8141e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| "repo": "JFROG/jfrog-agent-hooks", | ||
| "pin": "17df9b61b5cf78f49bf36d68e1c007f5db71e072", | ||
| "paths": [ | ||
| "modules" | ||
| ], | ||
| "dest_prefix": "plugin" | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| #!/usr/bin/env node | ||
| // Vendors modules bundle from jfrog-agent-hooks into plugin/. | ||
| // | ||
| // Usage: | ||
| // JFROG_AGENT_HOOKS_PATH=/path/to/jfrog-agent-hooks node .github/scripts/sync-modules.mjs | ||
| // | ||
| // Defaults JFROG_AGENT_HOOKS_PATH to ../jfrog-agent-hooks (sibling clone). | ||
| // Reads paths from sync-modules-vendor.json. | ||
|
|
||
| import { promises as fs } from "node:fs"; | ||
| import path from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
||
| const scriptDir = path.dirname(fileURLToPath(import.meta.url)); | ||
| const repoRoot = path.resolve(scriptDir, "..", ".."); | ||
| const vendorPath = path.join(scriptDir, "sync-modules-vendor.json"); | ||
|
|
||
| async function fileExists(p) { | ||
| try { | ||
| await fs.access(p); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| 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: ${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)}`); | ||
| } | ||
|
|
||
| async function main() { | ||
| const vendor = JSON.parse(await fs.readFile(vendorPath, "utf8")); | ||
| const paths = vendor.paths; | ||
| if (!Array.isArray(paths) || paths.length === 0) { | ||
| throw new Error(`${vendorPath} must define a non-empty paths array`); | ||
| } | ||
|
|
||
| const hooksRoot = | ||
| process.env.JFROG_AGENT_HOOKS_PATH?.trim() || | ||
| path.resolve(repoRoot, "..", "jfrog-agent-hooks"); | ||
|
|
||
| if (!(await fileExists(hooksRoot))) { | ||
| throw new Error( | ||
| `jfrog-agent-hooks not found at ${hooksRoot}. Set JFROG_AGENT_HOOKS_PATH.`, | ||
| ); | ||
| } | ||
|
|
||
| const destPrefix = (vendor.dest_prefix ?? "").replace(/^\/+|\/+$/g, ""); | ||
| const destRoot = destPrefix ? path.join(repoRoot, destPrefix) : repoRoot; | ||
|
|
||
| console.log(`--- sync from ${hooksRoot} (pin: ${vendor.pin ?? "local"}) ---`); | ||
| for (const rel of paths) { | ||
| await copyPath(hooksRoot, destRoot, rel); | ||
| } | ||
| console.log("done."); | ||
| } | ||
|
|
||
| await main(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # Copyright (c) JFrog Ltd. 2026 | ||
| # Licensed under the Apache License, Version 2.0 | ||
| # https://www.apache.org/licenses/LICENSE-2.0 | ||
|
|
||
| name: Validate package resolution hook | ||
|
|
||
| on: | ||
| pull_request: | ||
| branches: [main] | ||
| paths: | ||
| - "plugin/hooks/hooks.json" | ||
| - "plugin/modules/**" | ||
| - "plugin/.claude-plugin/plugin.json" | ||
| - "marketplace.json" | ||
| - "scripts/validate-package-resolution-hook.mjs" | ||
|
omerzi marked this conversation as resolved.
|
||
| - ".github/scripts/sync-modules-vendor.json" | ||
| - ".github/scripts/sync-modules.mjs" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
|
|
||
| jobs: | ||
| validate: | ||
| name: Validate package resolution hook | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: "20" | ||
|
|
||
| - name: Validate hook assembly | ||
| run: node scripts/validate-package-resolution-hook.mjs | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ The JFrog plugin provides the following capabilities, grouped by component: | |
| | --- | --- | --- | | ||
| | **MCP** | JFrog MCP server | Remote JFrog MCP server auto-attached to every session via `.mcp.json` at `${JFROG_URL}/mcp` (OAuth, no API keys). | | ||
| | **Skill** | Agent Guard | Copilot manages MCPs through the JFrog Agent Guard. Through it you can discover, install, configure, update, and remove MCP servers from the JFrog AI Catalog approved for your project, and authenticate to remote HTTP MCPs via OAuth, API key, or bearer token. | | ||
| | **Hook** | Agent Package Resolution (Preview) | Inject Artifactory routing instructions at the start of each Copilot session. | | ||
|
|
||
| --- | ||
|
|
||
|
|
@@ -27,7 +28,8 @@ Before installing, make sure you have: | |
| - **JFrog host URL and access token** — Your JFrog platform URL and a valid access token. | ||
| - **VS Code** — With the **GitHub Copilot Chat** extension installed and signed in. | ||
| - **GitHub Copilot editor preview features enabled** (organizations only) — If your Copilot access is managed by a GitHub organization, an admin must navigate to **Settings → Copilot → Policies → Editor preview features** and set it to **Enabled**. Individual (non-org) Copilot users can skip this step. | ||
| - **Node.js** (≥ 18) — with `npx` on your `PATH` | ||
| - **Agent plugins and hooks enabled** — Set both `"chat.plugins.enabled": true` and `"chat.useHooks": true` in VS Code settings. Enabling hooks alone still leaves the plugin unloaded. | ||
| - **Node.js** (≥ 20) — available as `node` on your `PATH` | ||
| - **JFrog CLI** (≥ 2.x, optional) — Recommended for `jf config add` authentication (see [Authentication](#authentication)). | ||
| - **JFrog Platform access** (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. | ||
|
|
@@ -103,6 +105,19 @@ If you have never configured the JFrog CLI on this machine: | |
|
|
||
| After authentication, open a workspace in VS Code. The JFrog skills load on demand, the JFrog Agent Guard becomes active, and any MCP servers approved for your project become available to your Copilot agent. You can manage everything through natural language — no terminal commands required. | ||
|
|
||
| ### Agent Package Resolution | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Review comment by Cursor AI 🟠 Missing APR user/admin docs vs sibling plugins Cursor and Claude ship Shipping Copilot APR without the same user/admin guides leaves setup and governance undocumented for this harness. Fix: Port the two guides (add VS Code prerequisites:
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed there's no |
||
|
|
||
| When Agent Package Resolution is enabled in `~/.jfrog/agents-conf.json`, a | ||
| SessionStart hook adds the resolved Artifactory repositories and package-routing | ||
| rules to every new Copilot chat. Configure the JFrog CLI with `jf config add`, | ||
| then start a new chat after changing the configuration. | ||
|
|
||
| The feature is fail-open for the chat session: disabled or unexpected failure | ||
| returns an empty hook result instead of preventing Copilot from starting. An | ||
| enabled but unconfigured installation injects a `NOT READY` advisory with setup | ||
| instructions. The hook has a 15-second limit to accommodate a cold, verified | ||
| repository lookup without delaying indefinitely. | ||
|
|
||
| ### Discover, inspect, and install MCPs | ||
|
|
||
| | Ask the agent… | What happens | | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| { | ||
| "name": "jfrog", | ||
| "description": "JFrog Platform integration with MCP, security skills, and supply-chain best practices", | ||
| "version": "1.0.11", | ||
| "author": { "name": "JFrog", "url": "https://jfrog.com" } | ||
| "version": "1.0.13", | ||
| "author": { "name": "JFrog", "url": "https://jfrog.com" }, | ||
| "hooks": "hooks/hooks.json" | ||
| } |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Review comment by Cursor AI 🟡 PR description is stale vs HEAD Body still describes:
HEAD is package-resolution-only SessionStart ( Fix: Refresh overview, mermaid flow, pin/version, and test-plan checkboxes to match current tree.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Agent Guard checkbox is checked, but there's nothing left to verify: |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| { | ||
| "hooks": { | ||
| "SessionStart": [ | ||
| { | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "node \"${CLAUDE_PLUGIN_ROOT}/modules/copilot-session-start.mjs\" package-resolution", | ||
| "timeout": 15, | ||
| "statusMessage": "Routing package installs through JFrog Artifactory…" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "logLevel": "info", | ||
| "packageResolution": { | ||
| "enabled": false, | ||
| "verifyRepos": true, | ||
| "cacheTtlDays": 7, | ||
| "defaultGlobalRepos": { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This default config governs all 8 package types with placeholder repo keys ( |
||
| "npm": "npm-virtual", | ||
| "pypi": "pypi-virtual", | ||
| "maven": "maven-virtual", | ||
| "gradle": "gradle-virtual", | ||
| "go": "go-virtual", | ||
| "docker": "docker-virtual", | ||
| "helm": "helm-virtual", | ||
| "nuget": "nuget-virtual" | ||
| }, | ||
| "autoSetup": [] | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| #!/usr/bin/env node | ||
| // Claude Code SessionStart hook runner. | ||
| // | ||
| // Usage: node claude-session-start.mjs <capability> | ||
| // Example: node claude-session-start.mjs package-resolution | ||
| // | ||
| // stdout: JSON with hookSpecificOutput.additionalContext. No stdout 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 = "claude_code"; | ||
| 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) process.stdout.write(payload); | ||
| } | ||
|
|
||
| function writeNoOp() { | ||
| // Claude SessionStart: no stdout on no-op. | ||
| } | ||
|
|
||
| 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: "claude-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); | ||
| }); |
| 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); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤖 Review comment by Cursor AI
🟠 Pin is post-
v0.9.0; Sync Plugins on that tag would remove the Copilot adapterpin=17df9b61…(merged master SHA that addsmodules/copilot-session-start.mjs). Tagjfrog-agent-hooks/v0.9.0does not contain that adapter.If Sync Plugins runs from
v0.9.0after this merges, it will replaceplugin/modules/with a tree that lackscopilot-session-start.mjswhilehooks.jsonstill points at it → SessionStart fails open with no routing.Sibling plugins pin release tags (e.g.
jfrog-agent-hooks/v0.8.1).Fix: Cut an agent-hooks release that includes
17df9b6, re-pin this file to that tag, then allow Sync Plugins. Do not sync fromv0.9.0.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can confirm the parts of this checkable from this repo: the pin does match
copilot-session-start.mjs's current content, andhooks.jsondoes reference that exact file as the command to run, so its absence would break the hook exactly as described. Worth folding in here too, nothing in the validate script or the workflow actually diffsplugin/modulesagainst this pin value.sync-modules.mjsreads it but only logs it, never compares. So a re-vendor from the wrong ref would pass CI as long ascopilot-session-start.mjsstill exists and the version numbers match, even if the resolver logic underneath had drifted.