Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5d9c909
JFSK-19 - Add package resolution hook assembly
omerzi Jul 27, 2026
09a3141
JFSK-19 - Document VS Code hooks setting
omerzi Jul 27, 2026
c677ec2
JFSK-19 - Vendor and validate VS Code hooks
omerzi Jul 27, 2026
7ac4a73
JFSK-19 - Keep plugin validation focused
omerzi Jul 27, 2026
eafdc9b
JFSK-19 - Align package hook with Agent Guard skill
omerzi Jul 29, 2026
2bd079e
JFSK-19 - Sync harness detection review fix
omerzi Jul 29, 2026
7b8a514
JFSK-19 - Refresh vendored modules after upstream rebase
omerzi Aug 2, 2026
36ef43f
JFSK-19 - Refresh vendored modules for harness fingerprint detection
omerzi Aug 2, 2026
1509c43
JFSK-19 - Cover the routing path and restore the manual sync entry point
omerzi Aug 3, 2026
95dc527
JFSK-19 - Bump plugin version above the skills sync on main
omerzi Aug 3, 2026
b56aa03
JFSK-19 - Rename vendored adapter to copilot-session-start.mjs
omerzi Aug 3, 2026
a34693c
JFSK-19 - Re-pin vendor source after upstream rebase
omerzi Aug 3, 2026
b3fc5e8
JFSK-19 - Re-pin vendor source to merged master SHA now that agent-ho…
omerzi Aug 5, 2026
a8141e8
JFSK-19 - Allow verified repository resolution time
omerzi Aug 6, 2026
e66cb32
JFSK-19 - Prepare VS Code plugin for hardened hook release
omerzi Aug 9, 2026
e8449d1
JFSK-19 - Pin VS Code plugin to agent-hooks v0.9.0
omerzi Aug 9, 2026
17d08c3
JFSK-19 - Cover verified package-resolution paths
omerzi Aug 9, 2026
eca3db4
JFSK-19 - Keep plugin CI independent of upstream host
omerzi Aug 9, 2026
d357336
JFSK-19 - Cover vendored setup support modules
omerzi Aug 10, 2026
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
67 changes: 67 additions & 0 deletions .github/scripts/check-vendored-modules.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env node

import { createHash } from "node:crypto";
import { readFile, readdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

const root = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"../..",
);
const modulesRoot = path.join(root, "plugin", "modules");
const manifestFile = path.join(
root,
".github",
"scripts",
"sync-modules-integrity.json",
);

async function filesUnder(dir, prefix = "") {
const entries = await readdir(dir, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const relative = path.join(prefix, entry.name);
if (entry.isDirectory())
files.push(...(await filesUnder(path.join(dir, entry.name), relative)));
else files.push(relative);
}
return files.sort();
}

async function digest(relative) {
const bytes = await readFile(path.join(modulesRoot, relative));
return createHash("sha256").update(bytes).digest("hex");
}

async function snapshot(pin) {
const files = {};
for (const relative of await filesUnder(modulesRoot))
files[relative] = await digest(relative);
return { schemaVersion: 1, pin, files };
}

const vendor = JSON.parse(
await readFile(
path.join(root, ".github", "scripts", "sync-modules-vendor.json"),
"utf8",
),
);
const actual = await snapshot(vendor.pin);

if (process.argv.includes("--write")) {
await writeFile(manifestFile, `${JSON.stringify(actual, null, 2)}\n`);
console.log(`wrote ${path.relative(root, manifestFile)}`);
process.exit(0);
}

const expected = JSON.parse(await readFile(manifestFile, "utf8"));
if (expected.pin !== vendor.pin)
throw new Error(
`integrity pin mismatch: manifest=${expected.pin} vendor=${vendor.pin}`,
);
if (JSON.stringify(expected.files) !== JSON.stringify(actual.files))
throw new Error(
"vendored modules differ from sync-modules-integrity.json; re-vendor and update the manifest",
);
console.log(`vendored modules match pin ${vendor.pin}`);
28 changes: 28 additions & 0 deletions .github/scripts/sync-modules-integrity.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"schemaVersion": 1,
"pin": "jfrog-agent-hooks/v0.9.0",
"files": {
"assets/agents-default-conf.json": "04aae9b1dcfc75271c3ed786adceea1635b0a1ae0be64fadd7b1111229f11f01",
"claude-session-start.mjs": "2ca1edc6b939cdff6c5faa6ac4b69636e6e92bc7b079316e1fdee7c53c6e837b",
"copilot-session-start.mjs": "8811e0829c90ff0987bed158f5ef571195ee5eb54dfc8021b4396fe76ad8a499",
"core/agents-config.mjs": "3ade16fd6e08b8ac6cb8570edfbed1e9677b26d9c31513720680dd17513480af",
"core/io.mjs": "63ea75df635a4e15cf36f2158fe78ae42e3ca886abe267ee5ed2577042eb153d",
"core/jf-identity.mjs": "9d0301d4a60b9c9297cde24e0bab0c2660c56f831617f2b69db17c844276b19b",
"core/logger.mjs": "1ebdffcdf4af14b19e3ee8e82cfeb377fb9961a09d4e9d6ecdc922d07b0848c2",
"core/run-capability.mjs": "9fac890b7fd4866f9d3322469b2a7301e28cebfa3faebd77857f9f79d2d1c532",
"cursor-session-start.mjs": "37dd25ffee18e9f357e3cbb8453552766fd89295e85aa09bf93bc208df74aa20",
"package-resolution/scripts/eager-setup-receipt.mjs": "69213084bc1976ec63b346ca26e8a63eb713b0da7ad6ab4fc018934e70fef091",
"package-resolution/scripts/eager-setup.mjs": "d78fc422d15a271e9ae68b754a28fa72ea25a9e86b505b1a0e5e053add864c0d",
"package-resolution/scripts/feature-flag.mjs": "18b258e4d1999de31bad54a1f7f3c3f9cf65c598bbb83f328b45f68a739821f3",
"package-resolution/scripts/index.mjs": "f3ea8f71ecd156515a4a5eb14e33de5c61287f4f2e0e7ca90f9b7d010c4d6567",
"package-resolution/scripts/package-manager-family.mjs": "50d066910638e37c2375f696094fde1252181398be56c4218ca14342a76a34e5",
"package-resolution/scripts/print-policy.mjs": "02593c6e401006d226b908221d007ba9e1951a61c4cb060789877c1fe919faa2",
"package-resolution/scripts/render-instruction.mjs": "9e4205b5715e2c79515de19d473a6487d61971368103f2851388530ed0a180c4",
"package-resolution/scripts/repo-types.mjs": "b432bcdd6e77f80ca2c9dddfbf4d9e1299019758fd58b04b29fc21b18a86f788",
"package-resolution/scripts/resolver.mjs": "3485575a65fd5579420d69a51f723047d44f3cfa246511565c6e89ca32b02b4d",
"package-resolution/scripts/setup-conflict.mjs": "fdc589561813a9c5e708f50a20a87bacdad3f490158c973b12b217cb984e2845",
"package-resolution/scripts/workspace-config.mjs": "f8f8eaaf0fb8a0c3691938e99afebbc87d8779e508db0ef821fa23f0a55b786a",
"package-resolution/templates/package-resolution-unconfigured.md": "e7645b89d1c4d618fb45692de084d627ca24b5e2e975416176115d3c233e9c00",
"package-resolution/templates/package-resolution.md": "c305751d24fe352b6334a208f7831eb9db58704f1baa693c6921264f6a4456d1"
}
}
6 changes: 6 additions & 0 deletions .github/scripts/sync-modules-vendor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"repo": "JFROG/jfrog-agent-hooks",
"pin": "jfrog-agent-hooks/v0.9.0",
"paths": ["modules"],
"dest_prefix": "plugin"
}
66 changes: 66 additions & 0 deletions .github/scripts/sync-modules.mjs
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();
39 changes: 39 additions & 0 deletions .github/workflows/validate-package-resolution-hook.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 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"
Comment thread
omerzi marked this conversation as resolved.
- ".github/scripts/sync-modules-vendor.json"
- ".github/scripts/sync-modules.mjs"
Comment thread
omerzi marked this conversation as resolved.
- ".github/scripts/sync-modules-integrity.json"
- ".github/scripts/check-vendored-modules.mjs"
- ".github/workflows/validate-package-resolution-hook.yml"

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

- name: Verify vendored module integrity
run: node .github/scripts/check-vendored-modules.mjs
65 changes: 42 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# JFrog Plugin for VS Code

The official JFrog plugin for [Visual Studio Code](https://code.visualstudio.com/) and **GitHub Copilot Chat**. The plugin connects your Copilot agent to the JFrog Platform with policy-governed MCP access, auto-installed governance instructions, and Agent Guard.

Paste this into your browser:
Expand All @@ -13,10 +14,11 @@ vscode://chat-plugin/install?source=jfrog/vscode-plugin

The JFrog plugin provides the following capabilities, grouped by component:

| Component | Feature | Description |
| --- | --- | --- |
| **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. |
| Component | Feature | Description |
| --------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **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. |

---

Expand All @@ -27,10 +29,12 @@ 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.

---

## Installation
Expand Down Expand Up @@ -67,9 +71,7 @@ VS Code opens, prompts you to install the plugin, and asks you to **Trust** the
2. Add the following entry inside the top-level `{ ... }` object (don't forget a trailing comma if it isn't the last entry):
```json
{
"chat.plugins.marketplaces": [
"https://github.com/jfrog/vscode-plugin/"
]
"chat.plugins.marketplaces": ["https://github.com/jfrog/vscode-plugin/"]
}
```
3. Open the Extensions panel (`Cmd+Shift+X`) and search for `@agentPlugins jfrog/vscode-plugin`.
Expand All @@ -81,10 +83,10 @@ VS Code opens, prompts you to install the plugin, and asks you to **Trust** the

### 1. Set persistent environment variables

| Variable | Description |
| --- | --- |
| `JFROG_URL` | Your JFrog platform URL, e.g. `https://mycompany.jfrog.io` (no trailing `/`) |
| `JFROG_ACCESS_TOKEN` | Your JFrog access token |
| Variable | Description |
| -------------------- | ---------------------------------------------------------------------------- |
| `JFROG_URL` | Your JFrog platform URL, e.g. `https://mycompany.jfrog.io` (no trailing `/`) |
| `JFROG_ACCESS_TOKEN` | Your JFrog access token |

### 2. Configure the JFrog CLI

Expand All @@ -96,25 +98,42 @@ If you have never configured the JFrog CLI on this machine:
jf config add
```
3. Follow the interactive prompts to enter the same JFrog platform URL and access token.
---

---

## Usage

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
Comment thread
omerzi marked this conversation as resolved.

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.

See the [user guide](docs/package-resolution-user-guide.md) for setup and the
[administrator guide](docs/package-resolution-admin-guide.md) for rollout and
governance configuration.

### Discover, inspect, and install MCPs

| Ask the agent… | What happens |
| --- | --- |
| "Which MCP servers can I install?" | Returns all MCP servers approved for your current project that you can install. |
| "What MCP servers do I already have?" | Returns only the MCP servers already installed on your machine. |
| "Show me the details for the filesystem MCP server." | Returns detailed metadata, required configuration (environment variables, runtime arguments), and active tool policies for a given server. |
| "Add the GitHub MCP server." | Installs an approved MCP server and syncs its tool policies locally. Secrets are requested via a CLI command — never in chat. |
| "Update the environment variables for the Slack MCP." | Replaces the configuration for an already-installed server without removing and reinstalling it. |
| "Remove the Slack MCP server." | Removes the server and its stored credentials from your local setup. Changes apply immediately. |
| "Log in to the remote Jira MCP server using OAuth." | Authenticates with a remote HTTP-based MCP server (OAuth, API key, or bearer token). |
| "Log out of the Jira MCP server." | Removes stored authentication credentials for a server. |
| Ask the agent… | What happens |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| "Which MCP servers can I install?" | Returns all MCP servers approved for your current project that you can install. |
| "What MCP servers do I already have?" | Returns only the MCP servers already installed on your machine. |
| "Show me the details for the filesystem MCP server." | Returns detailed metadata, required configuration (environment variables, runtime arguments), and active tool policies for a given server. |
| "Add the GitHub MCP server." | Installs an approved MCP server and syncs its tool policies locally. Secrets are requested via a CLI command — never in chat. |
| "Update the environment variables for the Slack MCP." | Replaces the configuration for an already-installed server without removing and reinstalling it. |
| "Remove the Slack MCP server." | Removes the server and its stored credentials from your local setup. Changes apply immediately. |
| "Log in to the remote Jira MCP server using OAuth." | Authenticates with a remote HTTP-based MCP server (OAuth, API key, or bearer token). |
| "Log out of the Jira MCP server." | Removes stored authentication credentials for a server. |

### How secrets are handled

Expand Down
42 changes: 42 additions & 0 deletions docs/package-resolution-admin-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Agent Package Resolution — VS Code administrator guide

Agent Package Resolution is opt-in. Deploy
`~/.jfrog/agents-conf.json` through the organization’s normal device-management
system; the plugin never overwrites an existing file.

## Recommended configuration

Declare only repositories approved for the organization:

```json
{
"logLevel": "info",
"packageResolution": {
"enabled": true,
"verifyRepos": true,
"cacheTtlDays": 7,
"defaultGlobalRepos": {
"npm": "npm-virtual",
"pypi": "pypi-virtual"
},
"autoSetup": []
}
}
```

Repository keys are verified against Artifactory before routing. Invalid or
unreachable repositories remain unresolved rather than falling back to public
registries. Workspace files may replace a repository for an already-governed
package type, but cannot expand the governed scope.

`cacheTtlDays: 0` re-checks all cached state on every session, including
eligible zero-touch `jf setup` receipts. Use it temporarily when troubleshooting,
not as a normal operating setting.

## Rollout checklist

1. Configure and test `jf` on a non-production server.
2. Deploy the configuration to a pilot group.
3. Enable `"chat.plugins.enabled": true` and `"chat.useHooks": true` in VS Code.
4. Start a new Copilot chat and verify the resolved repository table.
5. Monitor `~/.jfrog/logs/agent-hooks.log` before expanding rollout.
Loading
Loading