Skip to content
Draft
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
14 changes: 12 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,18 @@ jobs:
fi
echo "Version ${VERSION} is newer than ${LATEST} — proceeding with release"

# NOTE: no plugin-layout validator exists in this repo to gate the release on. If one
# lands, run it as a step here. See CONTRIBUTING.md#releasing.
- name: Set up Node.js
if: steps.detect.outputs.triggered == 'true'
uses: actions/setup-node@v4
with:
node-version: "20"

# validate-skill-governance.yml only runs on pull requests, so nothing checks the merge
# commit the release is actually cut from. Re-running its script here is what gates the
# release on it. See CONTRIBUTING.md#releasing.
- name: Validate skill governance before releasing
if: steps.detect.outputs.triggered == 'true'
run: node scripts/validate-skill-governance.mjs

# Tracked files at HEAD only, so nothing left on the runner can end up in the zip.
- name: Package release artifact
Expand Down
42 changes: 42 additions & 0 deletions .github/workflows/validate-skill-governance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright (c) JFrog Ltd. 2026
# Licensed under the Apache License, Version 2.0
# https://www.apache.org/licenses/LICENSE-2.0

name: Validate skill governance

on:
pull_request:
branches: [main]
paths:
# Governance lives entirely in agent-guard, so the wiring IS the hooks file. The
# plugin/scripts/** glob stays so that adding any plugin-side governance code re-triggers
# the validator that asserts it must not exist.
- "plugin/hooks/hooks.json"
- "plugin/scripts/**"
- "plugin/modules/**"
- "plugin/.claude-plugin/plugin.json"
- "marketplace.json"
- "scripts/validate-skill-governance.mjs"
- ".github/workflows/validate-skill-governance.yml"
workflow_dispatch:

permissions:
contents: read

jobs:
validate:
name: Validate skill governance
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"

# Hermetic: no network and no JFrog credentials. Every governed call runs against a stub
# npx, so the fail-open and fail-closed paths are exercised without reaching a registry.
- name: Run skill-governance validation
run: node scripts/validate-skill-governance.mjs
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The JFrog plugin provides the following capabilities, grouped by component:
| **Hook** | MCP server alignment | Secures installed plugins' `mcp.json` and `.mcp.json` server commands with JFrog Agent Guard at Copilot SessionStart. |
| **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. |
| **Hook** | Skills governance | Check the skills you invoke against your JFrog governance policy and block the ones it disallows. Covers the two entry points that carry a skill's identity: `/<skill-name>` and Copilot's `skill` tool. Coverage differs by surface — see [Skills governance](#skills-governance). |

---

Expand Down Expand Up @@ -217,6 +218,55 @@ rewritten MCP servers.

When an MCP server requires a sensitive configuration, the agent cannot set the value directly. Instead, it returns a CLI command for you to copy and run in your terminal. Secrets such as API keys, tokens, and connection strings are never exposed in the agent chat history.

### Skills governance

When a skill is about to run, a hook checks it against your JFrog governance policy and blocks it if policy disallows it. Enforcement runs entirely in the JFrog Agent Guard; the hook only carries the event to it.

Scope is **GitHub Copilot Chat**. VS Code can also host the Claude Code extension, which loads plugins through its own mechanism and governs skills with its own hook.

The two entry points that carry a skill's identity are covered:

- you running a skill with `/<skill-name>`,
- and Copilot invoking one through its `skill` tool.

For each, the hook computes the skill's content **fingerprint** and asks the JFrog governance service for a verdict:

| Verdict | What happens |
| --- | --- |
| **Allowed** | The skill runs. |
| **Blocked** | The skill is prevented from running, and each violated policy is named along with the reason it failed. |
| **Not yet scanned** | The skill is submitted for an on-the-fly scan and blocked with a "scan started — retry shortly" message. |
| **Not entitled** | If your account isn't entitled to AI Catalog skills governance, enforcement is skipped and skills run normally. |

Skills are looked for under `<project>/{.github,.claude,.agents}/skills` and `~/{.copilot,.claude,.agents}/skills`, and at each ancestor of the workspace root. The plugin's own bundled skills are exempt — that exemption is by location, not by name, and it runs before any credential or network work so the plugin keeps working when governance is unconfigured.

#### Requesting a waiver

When a policy block carries a waiver scope, the block message shows the command that requests one, against the blocking policy's application, stage, and gate, with your justification attached. The Agent Guard files it — `agent-guard --request-waiver` — so the plugin holds no credentials and no waiver logic of its own. The request goes to your project admin for review; it does not unblock the skill on its own, and nothing is submitted unless you ask for it and give a reason.

**Requirements & behavior**

> [!IMPORTANT]
> **A verdict reaches VS Code as JSON on the hook's stdout.** Three outcomes, and they are distinct:
>
> - **Your JFrog policies deny the skill** — **blocked**, naming the policies it violated and the
> command to request a waiver.
> - **The Agent Guard reaches the check but cannot finish it in time** — **blocked**. It writes a
> refusal explaining that it could not answer. It got as far as the check, so it does not guess.
> - **The Agent Guard cannot be *run at all*** — `npx` missing, the registry unreachable, no JFrog
> server configured, or it fails internally — **allowed**. A machine that cannot get a verdict is
> not governed by it, and blocking there would stop work without enforcing anything.
>
> A user who is entitled to nothing is unaffected either way: the Agent Guard answers "allow" for an
> unconfigured or unentitled user, so no setup is needed to opt out of the feature.

- Set `JFROG_PLATFORM_URL` and `JFROG_ACCESS_TOKEN` (or configure the JFrog CLI — see [Authentication](#authentication)) and `JF_PROJECT` (the JFrog project the skill runs in). For an entitled account with credentials but no project, skills are **blocked** with a message telling you what to set; with no credentials at all they are **allowed**, per the table above.
- **Node.js (≥ 20) with `npx` on your `PATH`** — the hook resolves the Agent Guard through `npx`. Without it, governed actions are allowed unchecked.
- **Unlike the MCP alignment hook, this one does not pin the Agent Guard version.** It always resolves the latest published release, so a governance fix reaches you without waiting for a plugin release. `JFROG_AGENT_GUARD_REPO` still redirects the registry.
- **Coverage limits.** Model-initiated invocation is governed only while `github.copilot.chat.skillTool.enabled` is on; with it off, a skill's body is injected into context with no tool call to intercept. Skills contributed by other extensions (`contributes.chatSkills`) are not searched, and neither are folders added through `chat.agentSkillsLocations`. Hooks are a VS Code Preview feature and can be disabled organization-side with no signal, so an absence of blocks never by itself means a skill was allowed.
- **Cost per call.** VS Code parses `matcher` for Claude Code compatibility but ignores its value, so the `PreToolUse` hook runs on *every* tool call, and `UserPromptSubmit` on every prompt. The prompt hook revalidates the Agent Guard against the registry; the tool hook reads that from cache, which is what keeps the per-call cost down.
- To turn enforcement off, remove the `UserPromptSubmit` and `PreToolUse` entries from `plugin/hooks/hooks.json`.

---

## Troubleshooting
Expand Down
2 changes: 1 addition & 1 deletion marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{
"name": "jfrog",
"description": "JFrog Platform integration with MCP, security skills, and supply-chain best practices",
"version": "1.0.26",
"version": "1.1.0",
"license": "Apache-2.0",
"source": "plugin",
"categories": [
Expand Down
2 changes: 1 addition & 1 deletion plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "jfrog",
"description": "JFrog Platform integration with MCP, security skills, and supply-chain best practices",
"version": "1.0.26",
"version": "1.1.0",
"license": "Apache-2.0",
"author": {
"name": "JFrog",
Expand Down
24 changes: 24 additions & 0 deletions plugin/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,30 @@
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "npm_config_fetch_retries=0 npm_config_fetch_timeout=10000 JF_AGENT_GUARD_ENFORCE_DEADLINE=\"$(_JFAG_NOW=$(date +%s 2>/dev/null); echo ${_JFAG_NOW:+$((_JFAG_NOW + 25))})\" npx --yes --registry \"${JFROG_AGENT_GUARD_REPO:-https://releases.jfrog.io/artifactory/api/npm/coding-agents-npm/}\" @jfrog/agent-guard --enforce-skill --client vscode",
"timeout": 30,
"statusMessage": "Checking this skill against your JFrog governance policy…"
}
]
}
],
"PreToolUse": [
{
"hooks": [
{
"type": "command",
"command": "npm_config_fetch_retries=0 npm_config_fetch_timeout=10000 JF_AGENT_GUARD_ENFORCE_DEADLINE=\"$(_JFAG_NOW=$(date +%s 2>/dev/null); echo ${_JFAG_NOW:+$((_JFAG_NOW + 25))})\" npx --yes --prefer-offline --registry \"${JFROG_AGENT_GUARD_REPO:-https://releases.jfrog.io/artifactory/api/npm/coding-agents-npm/}\" @jfrog/agent-guard --enforce-skill --client vscode",
"timeout": 30,
"statusMessage": "Checking this skill against your JFrog governance policy…"
}
]
}
]
}
}
21 changes: 11 additions & 10 deletions scripts/validate-package-resolution-hook.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -329,28 +329,29 @@ function main() {
const hooks = (config?.hooks?.SessionStart ?? []).flatMap(
(entry) => entry.hooks ?? [],
);
// Located by CONTENT, never by index or list length: SessionStart is a shared list that
// other features append to, so an index pins this check to whatever happens to sit there
// and a length check fails the moment anything else registers.
const commands = hooks.map((hook) => hook.command);
if (
commands.length !== 2 ||
commands[0] !== expectedCommand ||
commands[1] !== expectedAlignCommand
) {
const resolution = hooks.find((hook) => hook.command === expectedCommand);
const align = hooks.find((hook) => hook.command === expectedAlignCommand);
if (!resolution || !align) {
throw new Error(
`unexpected SessionStart commands: ${JSON.stringify(commands)}`,
);
}
if (hooks[0]?.timeout !== 15) {
if (resolution.timeout !== 15) {
throw new Error(
`expected a 15-second hook timeout, got ${hooks[0]?.timeout}`,
`expected a 15-second hook timeout, got ${resolution.timeout}`,
);
}
if (
hooks[1]?.timeout !== 60 ||
hooks[1]?.statusMessage !==
align.timeout !== 60 ||
align.statusMessage !==
"Securing plugin MCP servers with JFrog Agent Guard…"
) {
throw new Error(
`unexpected MCP alignment hook: ${JSON.stringify(hooks[1])}`,
`unexpected MCP alignment hook: ${JSON.stringify(align)}`,
);
}
});
Expand Down
Loading
Loading