pkgxray 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -96,11 +96,10 @@ Use the stdio server from any MCP-capable agent:
96
96
  }
97
97
  ```
98
98
 
99
- The server exposes three tools:
99
+ The server exposes two tools:
100
100
 
101
- - `audit_agent_extension_supply_chain` — zero-dep static heuristics
101
+ - `audit_agent_extension_supply_chain` — static heuristics on supplied evidence
102
102
  - `guard_agent_extension_install` — stage, vuln-check, audit a real package
103
- - `reason_about_extension_supply_chain` — Claude-powered authoritative verdict (requires `ANTHROPIC_API_KEY`)
104
103
 
105
104
  Tool arguments:
106
105
 
@@ -114,50 +113,22 @@ Tool arguments:
114
113
  `guard_agent_extension_install` accepts `reference`, optional `quarantineRoot`,
115
114
  optional `promoteTo`, `policy`, `force`, and `outputFormat`.
116
115
 
117
- `reason_about_extension_supply_chain` accepts the same evidence shape as
118
- `audit_agent_extension_supply_chain`, plus optional `model` (default
119
- `claude-opus-4-7`) and `maxFiles` (default 200). It returns a JSON verdict per
120
- the prompt's schema (`verdict`, `summary`, `findings`, `evidenceGaps`,
121
- `promotable`) plus `usage` and `latencyMs`.
116
+ ## Static heuristics calibration
122
117
 
123
- ## Reasoning mode (`--reason`)
118
+ The heuristics are calibrated to keep legitimate packages out of `block`. Real
119
+ malicious patterns that gate the verdict:
124
120
 
125
- Layer an LLM-powered authoritative verdict on top of the static heuristics.
126
- Supports Anthropic (Claude), OpenAI (GPT), and Google (Gemini).
121
+ - **block** (HIGH) prompt-injection text in README/docs, credential reads in
122
+ proximity to a filesystem-read primitive, persistence writes to shell rc /
123
+ cron / launchagents, dynamic exec + hardcoded IP/shortener/webhook target,
124
+ bulk `process.env` harvest in the same file as outbound network.
125
+ - **review** (MEDIUM) — install / postinstall / prepare lifecycle scripts,
126
+ dynamic eval / new Function / vm, clipboard read/write, missing
127
+ package.json, missing entrypoint source.
128
+ - **info** — child_process / fetch / network in isolation. Common in build
129
+ tools and CLIs; recorded but does not gate the verdict.
127
130
 
128
- ```bash
129
- # Anthropic (default)
130
- export ANTHROPIC_API_KEY=sk-ant-...
131
- npm install -g @anthropic-ai/sdk
132
- pkgxray --reason --file evidence.json
133
- pkgxray guard npm:some-pkg --reason --format json
134
-
135
- # OpenAI
136
- export OPENAI_API_KEY=sk-...
137
- npm install -g openai
138
- pkgxray guard npm:some-pkg --reason --reason-provider openai
139
-
140
- # Gemini
141
- export GEMINI_API_KEY=...
142
- npm install -g @google/generative-ai
143
- pkgxray guard npm:some-pkg --reason --reason-model gemini-2.5-pro
144
- ```
145
-
146
- Provider is selected by `--reason-provider <anthropic|openai|gemini>` or
147
- auto-detected from the model prefix (`claude-*` → anthropic, `gpt-*`/`o*` →
148
- openai, `gemini-*` → gemini). Defaults: `claude-opus-4-7`, `gpt-5`,
149
- `gemini-2.5-pro` — overridable with `--reason-model`.
150
-
151
- The Anthropic path uses adaptive thinking, `effort: "high"`, and caches the
152
- system prompt for 5-minute TTL (~90% cheaper on prompt tokens for repeated
153
- calls in the window). OpenAI uses strict structured outputs against the same
154
- JSON Schema. Gemini uses JSON-mode responses.
155
-
156
- Source files are capped at 200 files / 32 KB each / 500 KB total before
157
- sending — override with `--reason-max-files`.
158
-
159
- The reasoning verdict supersedes the static decision when `--reason` is used.
160
- Exit codes: `0` = safe, `2` = block, `3` = review.
131
+ `.d.ts`, `.map`, `.min.js`, and `.lock` files are skipped entirely.
161
132
 
162
133
  ## Browser Extension
163
134
 
package/bin/audit.js CHANGED
@@ -4,8 +4,6 @@
4
4
  const fs = require("node:fs");
5
5
  const { auditEvidence, renderMarkdown } = require("../src/auditor");
6
6
  const { guardExtension } = require("../src/quarantine");
7
- const { reasonAbout } = require("../src/reasoner");
8
- const { detectAvailableProvider, reasoningSetupHint } = require("../src/providers");
9
7
 
10
8
  function printUsage() {
11
9
  process.stderr.write(
@@ -14,17 +12,10 @@ function printUsage() {
14
12
  " pkgxray < evidence.json",
15
13
  " pkgxray --format json < evidence.json",
16
14
  " pkgxray --file evidence.json --format markdown",
17
- " pkgxray --reason --file evidence.json",
18
- " pkgxray guard <npm-package|npm:name@version|./path> [--reason] [--promote-to dir] [--no-source-scan]",
15
+ " pkgxray guard <npm-package|npm:name@version|./path> [--promote-to dir] [--no-source-scan]",
19
16
  "",
20
17
  "Evidence JSON fields:",
21
18
  " packageName, npmMetadata, githubMetadata, webPresence, sourceFiles",
22
- "",
23
- "LLM reasoning runs automatically when any of ANTHROPIC_API_KEY,",
24
- "OPENAI_API_KEY, or GEMINI_API_KEY is set AND the matching SDK is",
25
- "installed (@anthropic-ai/sdk, openai, or @google/generative-ai).",
26
- "Use --no-reason to force static-only, --reason-provider <name> to pin a",
27
- "provider, --reason-model <id> to pin a model.",
28
19
  ""
29
20
  ].join("\n")
30
21
  );
@@ -58,17 +49,6 @@ function parseArgs(argv) {
58
49
  options.sourceScan = false;
59
50
  } else if (arg === "--no-vulnerability-check") {
60
51
  options.vulnerabilityCheck = false;
61
- } else if (arg === "--reason") {
62
- options.reason = true;
63
- } else if (arg === "--no-reason") {
64
- options.reason = false;
65
- options.reasonExplicitOff = true;
66
- } else if (arg === "--reason-model") {
67
- options.reasonModel = argv[++i];
68
- } else if (arg === "--reason-provider") {
69
- options.reasonProvider = argv[++i];
70
- } else if (arg === "--reason-max-files") {
71
- options.reasonMaxFiles = Number(argv[++i]);
72
52
  } else {
73
53
  throw new Error(`Unknown argument: ${arg}`);
74
54
  }
@@ -86,36 +66,6 @@ function readInput(file) {
86
66
  return fs.readFileSync(0, "utf8");
87
67
  }
88
68
 
89
- function resolveReasonMode(options) {
90
- if (options.reasonExplicitOff) return { enabled: false, autoDetected: false };
91
- if (options.reason) return { enabled: true, autoDetected: false };
92
- const detected = detectAvailableProvider();
93
- if (detected) {
94
- return { enabled: true, autoDetected: true, detectedProvider: detected.name };
95
- }
96
- return { enabled: false, autoDetected: false };
97
- }
98
-
99
- async function maybeReason(evidence, options, mode) {
100
- if (!mode || !mode.enabled) return null;
101
- try {
102
- return await reasonAbout(evidence, {
103
- provider: options.reasonProvider || mode.detectedProvider,
104
- model: options.reasonModel,
105
- maxFiles: options.reasonMaxFiles
106
- });
107
- } catch (error) {
108
- return { error: { code: error.code || "REASONER_ERROR", message: error.message } };
109
- }
110
- }
111
-
112
- function reasoningExitCode(reasoning) {
113
- if (!reasoning || reasoning.error) return null;
114
- if (reasoning.verdict === "block") return 2;
115
- if (reasoning.verdict === "review") return 3;
116
- return 0;
117
- }
118
-
119
69
  async function main() {
120
70
  const options = parseArgs(process.argv.slice(2));
121
71
  if (options.help) {
@@ -128,27 +78,6 @@ async function main() {
128
78
  throw new Error("guard requires an extension reference");
129
79
  }
130
80
  const result = await guardExtension(options.reference, options);
131
- const reasonMode = resolveReasonMode(options);
132
- if (reasonMode.enabled) {
133
- const evidenceForReason = {
134
- packageName: result.resolved && result.resolved.packageName,
135
- npmMetadata: result.resolved && result.resolved.npmMetadata,
136
- githubMetadata: null,
137
- webPresence: null,
138
- sourceFiles: result.sourceFiles || {}
139
- };
140
- const reasoning = await maybeReason(evidenceForReason, options, reasonMode);
141
- result.reasoning = reasoning;
142
- if (reasoning && !reasoning.error && reasoning.verdict) {
143
- result.decision = reasoning.verdict === "block"
144
- ? "block"
145
- : reasoning.verdict === "safe"
146
- ? "allow"
147
- : "review";
148
- }
149
- } else {
150
- result.reasoningHint = reasoningSetupHint();
151
- }
152
81
  if (options.format === "json") {
153
82
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
154
83
  } else {
@@ -165,30 +94,14 @@ async function main() {
165
94
 
166
95
  const evidence = JSON.parse(raw);
167
96
  const report = auditEvidence(evidence);
168
- const reasonMode = resolveReasonMode(options);
169
- const reasoning = await maybeReason(evidence, options, reasonMode);
170
- const hint = reasonMode.enabled ? null : reasoningSetupHint();
171
-
172
- const payload = reasoning
173
- ? { report, reasoning }
174
- : hint
175
- ? { report, reasoningHint: hint }
176
- : report;
177
97
 
178
98
  if (options.format === "json") {
179
- process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
180
- } else if (reasoning) {
181
- process.stdout.write(`${renderMarkdown(report)}\n\n---\n\n${renderReasoningMarkdown(reasoning)}\n`);
182
- } else if (hint) {
183
- process.stdout.write(`${renderMarkdown(report)}\n\n💡 ${hint}\n`);
99
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
184
100
  } else {
185
101
  process.stdout.write(`${renderMarkdown(report)}\n`);
186
102
  }
187
103
 
188
- const exitFromReason = reasoningExitCode(reasoning);
189
- if (exitFromReason !== null) {
190
- process.exitCode = exitFromReason;
191
- }
104
+ process.exitCode = report.verdict === "block" ? 2 : report.verdict === "review" ? 3 : 0;
192
105
  }
193
106
 
194
107
  function renderGuardMarkdown(result) {
@@ -204,55 +117,6 @@ function renderGuardMarkdown(result) {
204
117
  }
205
118
 
206
119
  lines.push(renderMarkdown(result.report));
207
-
208
- if (result.reasoning) {
209
- lines.push("", "---", "", renderReasoningMarkdown(result.reasoning));
210
- } else if (result.reasoningHint) {
211
- lines.push("", `💡 ${result.reasoningHint}`);
212
- }
213
-
214
- return lines.join("\n");
215
- }
216
-
217
- function renderReasoningMarkdown(reasoning) {
218
- if (reasoning.error) {
219
- return `Reasoning: **unavailable** (${reasoning.error.code}: ${reasoning.error.message})`;
220
- }
221
- const lines = [
222
- `Reasoning verdict: **${(reasoning.verdict || "?").toUpperCase()}**`,
223
- `Provider: \`${reasoning.provider || "?"}\` · Model: \`${reasoning.model}\` · latency: ${reasoning.latencyMs} ms`,
224
- "",
225
- reasoning.summary || "",
226
- ""
227
- ];
228
- if (reasoning.usage) {
229
- const u = reasoning.usage;
230
- const parts = [
231
- `in=${u.input_tokens ?? "?"}`,
232
- `out=${u.output_tokens ?? "?"}`,
233
- `cache_read=${u.cache_read_input_tokens ?? 0}`,
234
- `cache_write=${u.cache_creation_input_tokens ?? 0}`
235
- ];
236
- lines.push(`Tokens: ${parts.join(" · ")}`, "");
237
- }
238
- if (reasoning.findings && reasoning.findings.length > 0) {
239
- lines.push("Findings:");
240
- for (const finding of reasoning.findings) {
241
- lines.push(
242
- `- **${finding.severity.toUpperCase()} - ${finding.category}**: ${finding.reasoning}`,
243
- ` Evidence: \`${finding.evidence}\``
244
- );
245
- }
246
- lines.push("");
247
- } else {
248
- lines.push("Findings: none reported.", "");
249
- }
250
- if (reasoning.evidenceGaps && reasoning.evidenceGaps.length > 0) {
251
- lines.push("Evidence gaps:");
252
- for (const gap of reasoning.evidenceGaps) {
253
- lines.push(`- ${gap}`);
254
- }
255
- }
256
120
  return lines.join("\n");
257
121
  }
258
122
 
package/bin/mcp-server.js CHANGED
@@ -3,11 +3,9 @@
3
3
 
4
4
  const { auditEvidence, renderMarkdown } = require("../src/auditor");
5
5
  const { guardExtension } = require("../src/quarantine");
6
- const { reasonAbout } = require("../src/reasoner");
7
6
 
8
7
  const TOOL_NAME = "audit_agent_extension_supply_chain";
9
8
  const GUARD_TOOL_NAME = "guard_agent_extension_install";
10
- const REASON_TOOL_NAME = "reason_about_extension_supply_chain";
11
9
  let buffer = "";
12
10
 
13
11
  function send(message) {
@@ -115,59 +113,6 @@ function guardToolDefinition() {
115
113
  };
116
114
  }
117
115
 
118
- function reasonToolDefinition() {
119
- return {
120
- name: REASON_TOOL_NAME,
121
- description:
122
- "Consult an LLM as an authoritative reasoning layer over supplied evidence. Supports Anthropic, OpenAI, and Gemini providers; defaults to Anthropic + claude-opus-4-7. Returns a structured JSON verdict (verdict, summary, findings, evidenceGaps, promotable). Requires the matching env key (ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY) and SDK installed.",
123
- inputSchema: {
124
- type: "object",
125
- additionalProperties: false,
126
- properties: {
127
- packageName: { type: "string" },
128
- npmMetadata: {},
129
- githubMetadata: {},
130
- webPresence: {},
131
- sourceFiles: {
132
- description:
133
- "Map of file path to source text, or an array of objects with path/name and content/text.",
134
- anyOf: [
135
- { type: "object", additionalProperties: { type: "string" } },
136
- {
137
- type: "array",
138
- items: {
139
- type: "object",
140
- additionalProperties: true,
141
- properties: {
142
- path: { type: "string" },
143
- name: { type: "string" },
144
- content: { type: "string" },
145
- text: { type: "string" }
146
- }
147
- }
148
- }
149
- ]
150
- },
151
- provider: {
152
- type: "string",
153
- enum: ["anthropic", "openai", "gemini"],
154
- description: "LLM provider. Defaults to anthropic, or auto-detected from model."
155
- },
156
- model: {
157
- type: "string",
158
- description:
159
- "Model ID. Defaults per provider: anthropic=claude-opus-4-7, openai=gpt-5, gemini=gemini-2.5-pro."
160
- },
161
- maxFiles: {
162
- type: "integer",
163
- description: "Cap on source files sent to the model. Default 200."
164
- }
165
- },
166
- required: ["sourceFiles"]
167
- }
168
- };
169
- }
170
-
171
116
  function handleRequest(request) {
172
117
  const { id, method, params } = request;
173
118
 
@@ -195,14 +140,14 @@ function handleRequest(request) {
195
140
  jsonrpc: "2.0",
196
141
  id,
197
142
  result: {
198
- tools: [toolDefinition(), guardToolDefinition(), reasonToolDefinition()]
143
+ tools: [toolDefinition(), guardToolDefinition()]
199
144
  }
200
145
  };
201
146
  }
202
147
 
203
148
  if (method === "tools/call") {
204
149
  const name = params && params.name;
205
- if (name !== TOOL_NAME && name !== GUARD_TOOL_NAME && name !== REASON_TOOL_NAME) {
150
+ if (name !== TOOL_NAME && name !== GUARD_TOOL_NAME) {
206
151
  return {
207
152
  jsonrpc: "2.0",
208
153
  id,
@@ -215,27 +160,6 @@ function handleRequest(request) {
215
160
 
216
161
  const args = (params && params.arguments) || {};
217
162
 
218
- if (name === REASON_TOOL_NAME) {
219
- return reasonAbout(args, {
220
- provider: args.provider,
221
- model: args.model,
222
- maxFiles: args.maxFiles
223
- })
224
- .then((reasoning) => ({
225
- jsonrpc: "2.0",
226
- id,
227
- result: {
228
- content: textContent(JSON.stringify(reasoning, null, 2)),
229
- structuredContent: reasoning
230
- }
231
- }))
232
- .catch((error) => ({
233
- jsonrpc: "2.0",
234
- id,
235
- error: { code: -32603, message: `${error.code || "REASONER_ERROR"}: ${error.message}` }
236
- }));
237
- }
238
-
239
163
  if (name === GUARD_TOOL_NAME) {
240
164
  return guardExtension(args.reference, args).then((guardResult) => {
241
165
  const text =
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pkgxray",
3
- "version": "0.2.0",
4
- "description": "Local CLI and MCP server that audits AI-agent extensions and npm packages for supply-chain risk. Zero-dep static heuristics + sandboxed quarantine + optional multi-provider (Claude / GPT / Gemini) reasoning layer.",
3
+ "version": "0.4.0",
4
+ "description": "Zero-dep local CLI and MCP server that scans npm packages and AI-agent extensions for supply-chain risk. OSV vuln pre-check, sandboxed quarantine, tarball-integrity verification, calibrated static heuristics.",
5
5
  "license": "MIT",
6
6
  "author": "Jack Adams-Lovell",
7
7
  "type": "commonjs",
@@ -34,26 +34,9 @@
34
34
  "security",
35
35
  "supply-chain",
36
36
  "npm-audit",
37
- "agent-extension",
38
- "ai-safety",
39
- "claude",
40
- "openai",
41
- "gemini"
37
+ "agent-extension"
42
38
  ],
43
39
  "engines": {
44
40
  "node": ">=18"
45
- },
46
- "peerDependencies": {
47
- "@anthropic-ai/sdk": ">=0.40.0",
48
- "openai": ">=4.0.0",
49
- "@google/generative-ai": ">=0.20.0"
50
- },
51
- "peerDependenciesMeta": {
52
- "@anthropic-ai/sdk": { "optional": true },
53
- "openai": { "optional": true },
54
- "@google/generative-ai": { "optional": true }
55
- },
56
- "devDependencies": {
57
- "@anthropic-ai/sdk": "^0.105.0"
58
41
  }
59
42
  }