-
Notifications
You must be signed in to change notification settings - Fork 0
AX-1694 — Align VS Code copilot-instructions with Claude/Cursor agent-guard docs #21
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
cba08b4
5f4db9a
e722a81
a033dca
0606423
6a626ac
3585b81
e344389
5cc4d1d
2c199e5
01652bf
e247621
2fc87e1
af832bb
ba067fa
dace1a6
5036f0d
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 |
|---|---|---|
| @@ -1,11 +1,14 @@ | ||
| { | ||
| "hooks": { | ||
| "sessionStart": [ | ||
| "SessionStart": [ | ||
| { | ||
| "type": "command", | ||
| "command": "${CLAUDE_PLUGIN_ROOT}/scripts/ensure-instructions.sh", | ||
| "windows": "powershell -ExecutionPolicy Bypass -File ${CLAUDE_PLUGIN_ROOT}/scripts/ensure-instructions.ps1" | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/inject-instructions.mjs\"" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } |
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| #!/usr/bin/env node | ||
| // Copyright (c) JFrog Ltd. 2026 | ||
| // Licensed under the Apache License, Version 2.0 | ||
| // https://www.apache.org/licenses/LICENSE-2.0 | ||
|
|
||
| import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"; | ||
| import os from "node:os"; | ||
| import path from "node:path"; | ||
| import process from "node:process"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
||
| // Logs go to stderr; stdout is reserved for the hook JSON payload. | ||
| const debugEnabled = process.env.JF_AGENT_GUARD_DEBUG === "true"; | ||
| const log = (message) => console.error(`[jfrog-agent-guard] ${message}`); | ||
| const debug = (message) => { | ||
| if (debugEnabled) log(message); | ||
| }; | ||
|
|
||
| // New JFROG_* env vars take precedence over the legacy JF_* names. | ||
| const env = (newName, oldName) => | ||
| process.env[newName] ?? process.env[oldName]; | ||
|
|
||
| const forceDisabled = | ||
| env("_JF_AGENT_GUARD_FORCE_DISABLE") === "true"; | ||
| const forceEnabled = | ||
| env("JF_AGENT_GUARD_FORCE_ENABLE") === "true"; | ||
|
|
||
| // Resolve {baseUrl, token}, preferring env vars and falling back to the JF CLI | ||
| // config (~/.jfrog/jfrog-cli.conf.v6): the profile marked isDefault, or the | ||
| // only profile when exactly one is defined. Returns null when nothing resolves. | ||
| function resolveCredentials() { | ||
| const baseUrl = env("JFROG_URL", "JF_URL"); | ||
| const token = env("JFROG_ACCESS_TOKEN", "JF_ACCESS_TOKEN"); | ||
| if (baseUrl && token) { | ||
| debug("Resolved credentials from environment variables"); | ||
| return { baseUrl, token }; | ||
| } | ||
|
|
||
| const confPath = path.join(os.homedir(), ".jfrog", "jfrog-cli.conf.v6"); | ||
| let conf; | ||
| try { | ||
| conf = JSON.parse(readFileSync(confPath, "utf8")); | ||
| } catch (error) { | ||
| debug(`Could not read or parse JF CLI config at ${confPath}: ${error.message}`); | ||
| return null; | ||
| } | ||
|
|
||
| // Only profiles that actually carry a URL and access token are usable. | ||
| const servers = Array.isArray(conf?.servers) | ||
| ? conf.servers.filter((s) => s.url && s.accessToken) | ||
| : []; | ||
| if (servers.length === 0) { | ||
| debug("No usable server profiles found in JF CLI config"); | ||
| return null; | ||
| } | ||
|
|
||
| const defaultProfile = servers.find((s) => s.isDefault); | ||
| if (defaultProfile) { | ||
| debug(`Resolved credentials using default profile: ${defaultProfile.serverId}`); | ||
| return { baseUrl: defaultProfile.url, token: defaultProfile.accessToken }; | ||
| } | ||
|
|
||
| if (servers.length === 1) { | ||
| debug(`Resolved credentials using the single available profile: ${servers[0].serverId}`); | ||
| return { baseUrl: servers[0].url, token: servers[0].accessToken }; | ||
| } | ||
|
|
||
| debug("Multiple JF CLI profiles exist but none is marked default; cannot resolve credentials"); | ||
| return null; | ||
| } | ||
|
|
||
| async function isAgentGuardEnabledViaSettings() { | ||
| const credentials = resolveCredentials(); | ||
| if (!credentials) { | ||
| debug("No JFrog credentials resolved; skipping settings check"); | ||
| return false; | ||
| } | ||
| const { baseUrl, token } = credentials; | ||
|
|
||
| const url = | ||
| baseUrl.replace(/\/+$/, "") + | ||
| "/ml/core/api/v1/administration/account-settings/mcp_gateway_plugin_enabled"; | ||
|
|
||
| debug(`Fetching agent guard setting from ${url}`); | ||
|
|
||
| const controller = new AbortController(); | ||
| const timeout = setTimeout(() => controller.abort(), 5000); | ||
| try { | ||
| const response = await fetch(url, { | ||
| method: "GET", | ||
| headers: { | ||
| Accept: "application/json", | ||
| Authorization: `Bearer ${token}`, | ||
| }, | ||
| signal: controller.signal, | ||
| }); | ||
| if (!response.ok) { | ||
| const body = await response.text().catch(() => ""); | ||
| debug(`Settings request returned HTTP ${response.status}; body: ${body || "<empty>"}`); | ||
| return false; | ||
| } | ||
| const data = await response.json(); | ||
| const enabled = data?.settings?.mcpGatewayPluginEnabled?.value === true; | ||
| debug(`Settings response indicates agent guard enabled=${enabled}`); | ||
| return enabled; | ||
| } catch (error) { | ||
| const reason = error?.name === "AbortError" ? "timeout" : error?.message ?? "unknown error"; | ||
| debug(`Settings request failed: ${reason}`); | ||
| return false; | ||
| } finally { | ||
| clearTimeout(timeout); | ||
| } | ||
| } | ||
|
|
||
| if (forceDisabled) { | ||
| debug("Force-disable flag is set."); | ||
| process.stdout.write("{}"); | ||
| process.exit(0); | ||
| } else if (forceEnabled) { | ||
| debug("Force-enable flag is set."); | ||
| } else if (!(await isAgentGuardEnabledViaSettings())) { | ||
| debug("Agent Guard not enabled; exiting without injecting instructions"); | ||
| process.stdout.write("{}"); | ||
| process.exit(0); | ||
| } | ||
| debug("Injecting instructions"); | ||
|
|
||
| const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); | ||
|
|
||
| let template; | ||
| try { | ||
| template = readFileSync( | ||
| path.join(root, "templates", "copilot-instructions.md"), | ||
| "utf8", | ||
| ); | ||
| } catch (error) { | ||
| debug(`Could not read instructions template: ${error.message}`); | ||
| process.exit(0); | ||
| } | ||
|
Comment on lines
+130
to
+139
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. Blocking — wrong template filename (regression). This reads |
||
|
|
||
| // Materialize the template into the workspace at .github/copilot-instructions.md, | ||
| // which is the file VS Code / GitHub Copilot actually reads. This mirrors the | ||
| // legacy ensure-instructions scripts and is the primary delivery path for | ||
| // Copilot; the additionalContext payload below additionally covers Claude Code | ||
| // sessions. Only write when absent so we never clobber a user-edited file. | ||
| try { | ||
| const targetDir = path.join(process.cwd(), ".github"); | ||
| const targetFile = path.join(targetDir, "copilot-instructions.md"); | ||
| if (!existsSync(targetFile)) { | ||
| mkdirSync(targetDir, { recursive: true }); | ||
| writeFileSync(targetFile, template, "utf8"); | ||
| debug(`Wrote instructions to ${targetFile}`); | ||
| } else { | ||
| debug(`Instructions already present at ${targetFile}; leaving as-is`); | ||
| } | ||
| } catch (error) { | ||
| debug(`Failed to write .github/copilot-instructions.md: ${error.message}`); | ||
| } | ||
|
|
||
| process.stdout.write( | ||
| JSON.stringify({ | ||
| hookSpecificOutput: { | ||
| hookEventName: "SessionStart", | ||
| additionalContext: template, | ||
| }, | ||
| }), | ||
| ); | ||
|
Comment on lines
+160
to
+167
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. Major — this no longer writes
Comment on lines
+160
to
+167
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. Blocking — two regressions here.
process.stdout.write(JSON.stringify({
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: template,
},
}));
|
||
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.
Critical — wrong template filename; the feature silently injects nothing. This reads
templates/jfrog-mcp-management.md, but the only template in this repo istemplates/copilot-instructions.md(jfrog-mcp-management.mdis the Claude/Cursor filename — this looks copy-pasted from the Claude plugin). Because the read is wrapped incatch { process.exit(0) }, the failure is silent: the read throws, the script exits 0, and no instructions are ever injected. Change line 170 to"copilot-instructions.md".