pluribus-context 0.3.37 → 0.3.38

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/CHANGELOG.md CHANGED
@@ -4,6 +4,10 @@
4
4
 
5
5
  All notable changes to Pluribus are documented here.
6
6
 
7
+ ## 0.3.38 - 2026-06-06
8
+
9
+ - Added `pluribus demo mcp-audit-receipt`, a tiny npm-runnable demo that validates privacy-safe MCP tool-call audit events and low-cardinality usage metrics without logging raw prompts, args, results, tokens, or row data.
10
+
7
11
  ## 0.3.37 - 2026-06-06
8
12
 
9
13
  - Published the canonical top-level Agent Skills layout (`skills/*/SKILL.md`) and backwards-compatible legacy mirrors so external skill registries can keep source links verifiable while package users get the current recipes from npm.
package/bin/pluribus.js CHANGED
@@ -67,7 +67,7 @@ OPTIONS (watch)
67
67
  --debounce Debounce delay in ms (minimum 300, default 400)
68
68
 
69
69
  OPTIONS (demo)
70
- --receipt Validate a custom skill use-rate receipt JSON file
70
+ --receipt Validate a custom demo receipt JSON file
71
71
  --json Print machine-readable demo results
72
72
 
73
73
  EXAMPLES
@@ -89,6 +89,8 @@ EXAMPLES
89
89
  pluribus watch --tools claude,cursor
90
90
  pluribus demo skill-use-rate
91
91
  pluribus demo skill-use-rate --json
92
+ pluribus demo mcp-audit-receipt
93
+ pluribus demo mcp-audit-receipt --json
92
94
 
93
95
  DOCS
94
96
  https://github.com/caioribeiroclw-pixel/pluribus
@@ -0,0 +1,24 @@
1
+ # MCP audit receipt demo
2
+
3
+ This example validates a privacy-safe audit receipt for MCP `tools/call` activity.
4
+
5
+ Run from any directory after `pluribus-context@latest` is published:
6
+
7
+ ```bash
8
+ npx --yes pluribus-context@latest demo mcp-audit-receipt
9
+ npx --yes pluribus-context@latest demo mcp-audit-receipt --json
10
+ ```
11
+
12
+ Or validate your own receipt shape:
13
+
14
+ ```bash
15
+ npx --yes pluribus-context@latest demo mcp-audit-receipt --receipt ./mcp-audit-receipt.json
16
+ ```
17
+
18
+ The point is to split:
19
+
20
+ - **audit events**: correlation IDs, hashed user/token subject, token scopes, tool name, redacted argument shape, status, duration, result shape, and error class;
21
+ - **usage metrics**: low-cardinality counters/histograms such as tool name, status, and token scope;
22
+ - **privacy boundary**: no raw prompts, raw SQL, row data, tokens, tool outputs, or private connection strings in the receipt.
23
+
24
+ This is for MCP server/gateway operators who need to answer: “who invoked which tool, under what scope, and did it succeed?” without dumping sensitive content into logs.
@@ -0,0 +1,70 @@
1
+ {
2
+ "schema": "pluribus.mcp_tool_call_audit_receipt.v1",
3
+ "run_id": "mcp-audit-2026-06-06T22:00Z",
4
+ "generated_at": "2026-06-06T22:00:00Z",
5
+ "server": {
6
+ "name": "analytics-mcp",
7
+ "transport": "http",
8
+ "version": "1.4.0"
9
+ },
10
+ "client": {
11
+ "name": "claude-desktop",
12
+ "workspace": "hashed:7f3f0f5f"
13
+ },
14
+ "audit_policy": {
15
+ "raw_arguments": "redacted_shape_only",
16
+ "raw_results": "redacted_shape_only",
17
+ "privacy_boundary": "no prompts, raw SQL, row data, tokens, or private connection strings in receipt"
18
+ },
19
+ "tool_calls": [
20
+ {
21
+ "event": "mcp.tool_call",
22
+ "request_id": "req_01JY6GATEWAY",
23
+ "session_id": "sess_01JY6RUN",
24
+ "user_id_hash": "sha256:0d9b4a1a3f0a6f2fd0dc8aa9d0c6f2b7a35f4f5d0b2a4d3e1e04a8b4b6b2e5d8",
25
+ "token_subject_hash": "sha256:6aaf4a3b4d10c45c8fd2cb4f3c73b8cde42bb62779b7e1c6a2c5e0dd8d78f4a1",
26
+ "token_scopes": ["database:read", "query:run"],
27
+ "tool_name": "query_database",
28
+ "args_shape": {
29
+ "database_id": "number",
30
+ "query": "redacted_sql",
31
+ "limit": "number"
32
+ },
33
+ "status": "ok",
34
+ "duration_ms": 184,
35
+ "result_shape": "rows:12 columns:4",
36
+ "error_class": null
37
+ },
38
+ {
39
+ "event": "mcp.tool_call",
40
+ "request_id": "req_01JY6DENIED",
41
+ "session_id": "sess_01JY6RUN",
42
+ "user_id_hash": "sha256:0d9b4a1a3f0a6f2fd0dc8aa9d0c6f2b7a35f4f5d0b2a4d3e1e04a8b4b6b2e5d8",
43
+ "token_subject_hash": "sha256:6aaf4a3b4d10c45c8fd2cb4f3c73b8cde42bb62779b7e1c6a2c5e0dd8d78f4a1",
44
+ "token_scopes": ["database:read"],
45
+ "tool_name": "update_dashboard",
46
+ "args_shape": {
47
+ "dashboard_id": "number",
48
+ "mutation": "redacted_object"
49
+ },
50
+ "status": "denied",
51
+ "duration_ms": 12,
52
+ "result_shape": "policy_denial",
53
+ "error_class": "insufficient_scope"
54
+ }
55
+ ],
56
+ "usage_metrics": [
57
+ {
58
+ "name": "mcp_tool_calls_total",
59
+ "type": "counter",
60
+ "value": "1",
61
+ "labels": ["tool_name", "status", "token_scope"]
62
+ },
63
+ {
64
+ "name": "mcp_tool_call_duration_ms",
65
+ "type": "histogram",
66
+ "value": "184",
67
+ "labels": ["tool_name", "status"]
68
+ }
69
+ ]
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pluribus-context",
3
- "version": "0.3.37",
3
+ "version": "0.3.38",
4
4
  "description": "AI context and rules sync CLI for Claude.md, Claude Code, Cursor, and Copilot instructions, with privacy-safe context receipts that prove what memory, tools, skills, compactions, and security findings crossed agent boundaries without logging raw content.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/caioribeiroclw-pixel/pluribus#readme",
@@ -7,7 +7,11 @@ import * as path from 'path'
7
7
  import { fileURLToPath } from 'url'
8
8
 
9
9
  const DEFAULT_DEMO = 'skill-use-rate'
10
+ const SKILL_USE_RATE_DEMO = 'skill-use-rate'
11
+ const MCP_AUDIT_RECEIPT_DEMO = 'mcp-audit-receipt'
12
+ const AVAILABLE_DEMOS = [SKILL_USE_RATE_DEMO, MCP_AUDIT_RECEIPT_DEMO]
10
13
  const SKILL_USE_RATE_SCHEMA = 'pluribus.skill_use_rate_receipt.v1'
14
+ const MCP_AUDIT_RECEIPT_SCHEMA = 'pluribus.mcp_tool_call_audit_receipt.v1'
11
15
 
12
16
  /**
13
17
  * @param {Record<string, string | boolean>} args
@@ -16,29 +20,42 @@ const SKILL_USE_RATE_SCHEMA = 'pluribus.skill_use_rate_receipt.v1'
16
20
  export async function runDemo(args, positional = []) {
17
21
  const demoName = positional[0] || DEFAULT_DEMO
18
22
 
19
- if (demoName !== DEFAULT_DEMO) {
20
- console.error(`❌ Unknown demo: ${demoName}`)
21
- console.error(' Available demos: skill-use-rate')
22
- process.exit(1)
23
+ switch (demoName) {
24
+ case SKILL_USE_RATE_DEMO:
25
+ return runSkillUseRateDemo(args)
26
+ case MCP_AUDIT_RECEIPT_DEMO:
27
+ return runMcpAuditReceiptDemo(args)
28
+ default:
29
+ console.error(`❌ Unknown demo: ${demoName}`)
30
+ console.error(` Available demos: ${AVAILABLE_DEMOS.join(', ')}`)
31
+ process.exit(1)
23
32
  }
33
+ }
24
34
 
25
- const receiptPath = typeof args.receipt === 'string' && args.receipt.trim()
26
- ? path.resolve(process.cwd(), args.receipt)
27
- : bundledSkillUseRateReceiptPath()
28
-
29
- let receipt
35
+ function readReceipt(receiptPath, label) {
30
36
  try {
31
- receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf8'))
37
+ return JSON.parse(fs.readFileSync(receiptPath, 'utf8'))
32
38
  } catch (err) {
33
- console.error(`❌ Could not read skill use-rate receipt at ${receiptPath}: ${err.message}`)
39
+ console.error(`❌ Could not read ${label} receipt at ${receiptPath}: ${err.message}`)
34
40
  process.exit(1)
35
41
  }
42
+ }
36
43
 
44
+ function selectedReceiptPath(args, defaultPath) {
45
+ return typeof args.receipt === 'string' && args.receipt.trim()
46
+ ? path.resolve(process.cwd(), args.receipt)
47
+ : defaultPath
48
+ }
49
+
50
+ function runSkillUseRateDemo(args) {
51
+ const receiptPath = selectedReceiptPath(args, bundledSkillUseRateReceiptPath())
52
+ const receipt = readReceipt(receiptPath, 'skill use-rate')
37
53
  const result = validateSkillUseRateReceipt(receipt)
54
+
38
55
  if (Boolean(args.json)) {
39
56
  console.log(JSON.stringify({
40
57
  ok: result.errors.length === 0,
41
- demo: DEFAULT_DEMO,
58
+ demo: SKILL_USE_RATE_DEMO,
42
59
  receipt: path.relative(process.cwd(), receiptPath) || receiptPath,
43
60
  summary: result.summary,
44
61
  warnings: result.warnings,
@@ -65,10 +82,48 @@ export async function runDemo(args, positional = []) {
65
82
  if (result.errors.length > 0) process.exit(1)
66
83
  }
67
84
 
85
+ function runMcpAuditReceiptDemo(args) {
86
+ const receiptPath = selectedReceiptPath(args, bundledMcpAuditReceiptPath())
87
+ const receipt = readReceipt(receiptPath, 'MCP audit')
88
+ const result = validateMcpAuditReceipt(receipt)
89
+
90
+ if (Boolean(args.json)) {
91
+ console.log(JSON.stringify({
92
+ ok: result.errors.length === 0,
93
+ demo: MCP_AUDIT_RECEIPT_DEMO,
94
+ receipt: path.relative(process.cwd(), receiptPath) || receiptPath,
95
+ summary: result.summary,
96
+ warnings: result.warnings,
97
+ errors: result.errors,
98
+ }, null, 2))
99
+ } else {
100
+ console.log('🧪 Pluribus demo: MCP audit receipt')
101
+ console.log(` Receipt: ${path.relative(process.cwd(), receiptPath) || receiptPath}`)
102
+ console.log('')
103
+
104
+ if (result.errors.length === 0) {
105
+ console.log(`✅ MCP audit receipt ok: ${result.summary.toolCallCount} tool calls, ${result.summary.auditEventCount} audit events, ${result.summary.metricCount} metrics`)
106
+ for (const warning of result.warnings) console.log(` • ${warning}`)
107
+ console.log('')
108
+ console.log('Why this matters: production MCP needs audit events and low-cardinality metrics, not raw prompt/tool dumps. Prove who invoked which tool, under which scope, with redacted argument/result shape.')
109
+ console.log('Try your own receipt: pluribus demo mcp-audit-receipt --receipt path/to/mcp-audit-receipt.json')
110
+ } else {
111
+ console.error('❌ MCP audit receipt invalid:')
112
+ for (const error of result.errors) console.error(` • ${error}`)
113
+ }
114
+ }
115
+
116
+ if (result.errors.length > 0) process.exit(1)
117
+ }
118
+
68
119
  function bundledSkillUseRateReceiptPath() {
69
120
  return fileURLToPath(new URL('../../examples/skill-use-rate-receipts/skill-use-rate-receipt.json', import.meta.url))
70
121
  }
71
122
 
123
+ function bundledMcpAuditReceiptPath() {
124
+ return fileURLToPath(new URL('../../examples/mcp-audit-receipts/mcp-audit-receipt.json', import.meta.url))
125
+ }
126
+
72
127
  export function validateSkillUseRateReceipt(receipt) {
73
128
  const errors = []
74
129
  const warnings = []
@@ -153,3 +208,102 @@ export function validateSkillUseRateReceipt(receipt) {
153
208
  },
154
209
  }
155
210
  }
211
+
212
+ export function validateMcpAuditReceipt(receipt) {
213
+ const errors = []
214
+ const warnings = []
215
+
216
+ function requireString(value, field) {
217
+ if (typeof value !== 'string' || value.trim() === '') {
218
+ errors.push(`${field} must be a non-empty string`)
219
+ }
220
+ }
221
+
222
+ function requireArray(value, field) {
223
+ if (!Array.isArray(value) || value.length === 0) {
224
+ errors.push(`${field} must be a non-empty array`)
225
+ }
226
+ }
227
+
228
+ function requireNonNegativeNumber(value, field) {
229
+ if (typeof value !== 'number' || Number.isNaN(value) || value < 0) {
230
+ errors.push(`${field} must be a non-negative number`)
231
+ }
232
+ }
233
+
234
+ if (receipt.schema !== MCP_AUDIT_RECEIPT_SCHEMA) {
235
+ errors.push(`schema must be ${MCP_AUDIT_RECEIPT_SCHEMA}`)
236
+ }
237
+
238
+ requireString(receipt.run_id, 'run_id')
239
+ requireString(receipt.generated_at, 'generated_at')
240
+ requireString(receipt.server?.name, 'server.name')
241
+ requireString(receipt.server?.transport, 'server.transport')
242
+ requireString(receipt.client?.name, 'client.name')
243
+ requireString(receipt.audit_policy?.raw_arguments, 'audit_policy.raw_arguments')
244
+ requireString(receipt.audit_policy?.raw_results, 'audit_policy.raw_results')
245
+ requireString(receipt.audit_policy?.privacy_boundary, 'audit_policy.privacy_boundary')
246
+ requireArray(receipt.tool_calls, 'tool_calls')
247
+ requireArray(receipt.usage_metrics, 'usage_metrics')
248
+
249
+ if (receipt.audit_policy?.raw_arguments !== 'redacted_shape_only') {
250
+ errors.push('audit_policy.raw_arguments must be redacted_shape_only')
251
+ }
252
+ if (receipt.audit_policy?.raw_results !== 'redacted_shape_only') {
253
+ errors.push('audit_policy.raw_results must be redacted_shape_only')
254
+ }
255
+
256
+ const lowCardinalityMetricLabels = new Set(['tool_name', 'status', 'token_scope', 'user_type'])
257
+
258
+ for (const [index, call] of (receipt.tool_calls || []).entries()) {
259
+ const prefix = `tool_calls[${index}]`
260
+ requireString(call.event, `${prefix}.event`)
261
+ requireString(call.request_id, `${prefix}.request_id`)
262
+ requireString(call.session_id, `${prefix}.session_id`)
263
+ requireString(call.user_id_hash, `${prefix}.user_id_hash`)
264
+ requireString(call.token_subject_hash, `${prefix}.token_subject_hash`)
265
+ requireArray(call.token_scopes, `${prefix}.token_scopes`)
266
+ requireString(call.tool_name, `${prefix}.tool_name`)
267
+ requireString(call.status, `${prefix}.status`)
268
+ requireNonNegativeNumber(call.duration_ms, `${prefix}.duration_ms`)
269
+ requireString(call.result_shape, `${prefix}.result_shape`)
270
+
271
+ if (call.event !== 'mcp.tool_call') errors.push(`${prefix}.event must be mcp.tool_call`)
272
+ if (!['ok', 'empty', 'error', 'timeout', 'denied'].includes(call.status)) {
273
+ errors.push(`${prefix}.status must be one of ok|empty|error|timeout|denied`)
274
+ }
275
+ if (!call.args_shape || typeof call.args_shape !== 'object' || Array.isArray(call.args_shape)) {
276
+ errors.push(`${prefix}.args_shape must be an object with redacted argument types/shapes`)
277
+ }
278
+ if (typeof call.args_preview === 'string' || typeof call.result_preview === 'string') {
279
+ errors.push(`${prefix} must not include raw args/results previews; use args_shape/result_shape instead`)
280
+ }
281
+ if (call.error_class != null && typeof call.error_class !== 'string') {
282
+ errors.push(`${prefix}.error_class must be string or null`)
283
+ }
284
+ }
285
+
286
+ for (const [index, metric] of (receipt.usage_metrics || []).entries()) {
287
+ const prefix = `usage_metrics[${index}]`
288
+ requireString(metric.name, `${prefix}.name`)
289
+ requireString(metric.type, `${prefix}.type`)
290
+ requireString(metric.value, `${prefix}.value`)
291
+ requireArray(metric.labels, `${prefix}.labels`)
292
+
293
+ for (const label of metric.labels || []) {
294
+ if (!lowCardinalityMetricLabels.has(label)) {
295
+ warnings.push(`${prefix}.labels includes high-cardinality label ${label}; prefer ${[...lowCardinalityMetricLabels].join(', ')}`)
296
+ }
297
+ }
298
+ }
299
+
300
+ return {
301
+ errors,
302
+ warnings,
303
+ summary: {
304
+ toolCallCount: Array.isArray(receipt.tool_calls) ? receipt.tool_calls.length : 0,
305
+ auditEventCount: Array.isArray(receipt.tool_calls) ? receipt.tool_calls.length : 0,
306
+ metricCount: Array.isArray(receipt.usage_metrics) ? receipt.usage_metrics.length : 0,
307
+ },
308
+ }
309
+ }
@@ -1 +1 @@
1
- export const VERSION = '0.3.37'
1
+ export const VERSION = '0.3.38'