pluribus-context 0.3.38 → 0.3.39

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.39 - 2026-06-07
8
+
9
+ - Added `pluribus demo mcp-telemetry-import`, a tiny npm-runnable converter from MCP `rpc-messages.jsonl`-style JSON-RPC traces into privacy-safe audit receipts that preserve attribution, redacted shapes, status, and timing gaps without storing raw tool payloads.
10
+
7
11
  ## 0.3.38 - 2026-06-06
8
12
 
9
13
  - 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.
package/bin/pluribus.js CHANGED
@@ -68,6 +68,7 @@ OPTIONS (watch)
68
68
 
69
69
  OPTIONS (demo)
70
70
  --receipt Validate a custom demo receipt JSON file
71
+ --input Import a custom demo input file, such as rpc-messages.jsonl
71
72
  --json Print machine-readable demo results
72
73
 
73
74
  EXAMPLES
@@ -91,6 +92,8 @@ EXAMPLES
91
92
  pluribus demo skill-use-rate --json
92
93
  pluribus demo mcp-audit-receipt
93
94
  pluribus demo mcp-audit-receipt --json
95
+ pluribus demo mcp-telemetry-import
96
+ pluribus demo mcp-telemetry-import --json
94
97
 
95
98
  DOCS
96
99
  https://github.com/caioribeiroclw-pixel/pluribus
@@ -102,7 +105,7 @@ const COMMAND_FLAGS = {
102
105
  validate: new Set(['source', 'update-imports']),
103
106
  audit: new Set(['source', 'tools', 'update-imports', 'strict', 'ci', 'json', 'output', 'github-annotations', 'fidelity-report']),
104
107
  watch: new Set(['source', 'tools', 'update-imports', 'dry-run', 'once', 'debounce']),
105
- demo: new Set(['receipt', 'json']),
108
+ demo: new Set(['receipt', 'input', 'json']),
106
109
  }
107
110
 
108
111
  function getFlagNames(argv) {
@@ -0,0 +1,27 @@
1
+ # MCP telemetry import demo
2
+
3
+ This example converts a tiny MCP `rpc-messages.jsonl`-style trace into the same privacy-safe audit receipt shape used by `pluribus demo mcp-audit-receipt`.
4
+
5
+ Run from any directory after `pluribus-context@latest` includes this demo:
6
+
7
+ ```bash
8
+ npx --yes pluribus-context@latest demo mcp-telemetry-import
9
+ npx --yes pluribus-context@latest demo mcp-telemetry-import --json
10
+ ```
11
+
12
+ Or convert your own log:
13
+
14
+ ```bash
15
+ npx --yes pluribus-context@latest demo mcp-telemetry-import --input ./rpc-messages.jsonl --json
16
+ ```
17
+
18
+ The point is not to store raw MCP payloads forever. The import keeps only:
19
+
20
+ - request/session IDs;
21
+ - hashed user/token subjects;
22
+ - token scopes;
23
+ - tool name;
24
+ - redacted argument/result shape;
25
+ - status, duration if timestamps exist, and error class.
26
+
27
+ If only fallback `rpc-messages.jsonl` exists, the receipt can still prove tool-call attribution. If gateway telemetry is absent, latency/status coverage should be marked as a gap instead of silently implied.
@@ -0,0 +1,4 @@
1
+ {"timestamp":"2026-06-07T13:00:00.000Z","direction":"client_to_server","session_id":"sess_demo","user_id":"user-123","token_subject":"oauth-subject-456","token_scopes":["repo:read"],"message":{"jsonrpc":"2.0","id":"1","method":"tools/call","params":{"name":"github.search_issues","arguments":{"query":"repo:org/app label:bug MCP audit","limit":5}}}}
2
+ {"timestamp":"2026-06-07T13:00:00.142Z","direction":"server_to_client","session_id":"sess_demo","message":{"jsonrpc":"2.0","id":"1","result":{"content":[{"type":"text","text":"2 issues found"}],"isError":false}}}
3
+ {"timestamp":"2026-06-07T13:00:02.000Z","direction":"client_to_server","session_id":"sess_demo","user_id":"user-123","token_subject":"oauth-subject-456","token_scopes":["repo:read"],"message":{"jsonrpc":"2.0","id":"2","method":"tools/call","params":{"name":"github.create_issue","arguments":{"repo":"org/app","title":"Add audit log","body":"redacted before receipt export"}}}}
4
+ {"timestamp":"2026-06-07T13:00:02.019Z","direction":"server_to_client","session_id":"sess_demo","message":{"jsonrpc":"2.0","id":"2","error":{"code":"insufficient_scope","message":"write scope required"}}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pluribus-context",
3
- "version": "0.3.38",
3
+ "version": "0.3.39",
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",
@@ -4,12 +4,14 @@
4
4
 
5
5
  import * as fs from 'fs'
6
6
  import * as path from 'path'
7
+ import { createHash } from 'crypto'
7
8
  import { fileURLToPath } from 'url'
8
9
 
9
10
  const DEFAULT_DEMO = 'skill-use-rate'
10
11
  const SKILL_USE_RATE_DEMO = 'skill-use-rate'
11
12
  const MCP_AUDIT_RECEIPT_DEMO = 'mcp-audit-receipt'
12
- const AVAILABLE_DEMOS = [SKILL_USE_RATE_DEMO, MCP_AUDIT_RECEIPT_DEMO]
13
+ const MCP_TELEMETRY_IMPORT_DEMO = 'mcp-telemetry-import'
14
+ const AVAILABLE_DEMOS = [SKILL_USE_RATE_DEMO, MCP_AUDIT_RECEIPT_DEMO, MCP_TELEMETRY_IMPORT_DEMO]
13
15
  const SKILL_USE_RATE_SCHEMA = 'pluribus.skill_use_rate_receipt.v1'
14
16
  const MCP_AUDIT_RECEIPT_SCHEMA = 'pluribus.mcp_tool_call_audit_receipt.v1'
15
17
 
@@ -25,6 +27,8 @@ export async function runDemo(args, positional = []) {
25
27
  return runSkillUseRateDemo(args)
26
28
  case MCP_AUDIT_RECEIPT_DEMO:
27
29
  return runMcpAuditReceiptDemo(args)
30
+ case MCP_TELEMETRY_IMPORT_DEMO:
31
+ return runMcpTelemetryImportDemo(args)
28
32
  default:
29
33
  console.error(`❌ Unknown demo: ${demoName}`)
30
34
  console.error(` Available demos: ${AVAILABLE_DEMOS.join(', ')}`)
@@ -82,6 +86,12 @@ function runSkillUseRateDemo(args) {
82
86
  if (result.errors.length > 0) process.exit(1)
83
87
  }
84
88
 
89
+ function selectedInputPath(args, defaultPath) {
90
+ return typeof args.input === 'string' && args.input.trim()
91
+ ? path.resolve(process.cwd(), args.input)
92
+ : defaultPath
93
+ }
94
+
85
95
  function runMcpAuditReceiptDemo(args) {
86
96
  const receiptPath = selectedReceiptPath(args, bundledMcpAuditReceiptPath())
87
97
  const receipt = readReceipt(receiptPath, 'MCP audit')
@@ -116,6 +126,56 @@ function runMcpAuditReceiptDemo(args) {
116
126
  if (result.errors.length > 0) process.exit(1)
117
127
  }
118
128
 
129
+
130
+ function runMcpTelemetryImportDemo(args) {
131
+ const inputPath = selectedInputPath(args, bundledMcpTelemetryJsonlPath())
132
+ let logText
133
+ try {
134
+ logText = fs.readFileSync(inputPath, 'utf8')
135
+ } catch (err) {
136
+ console.error(`❌ Could not read MCP telemetry JSONL at ${inputPath}: ${err.message}`)
137
+ process.exit(1)
138
+ }
139
+
140
+ const imported = importMcpTelemetryJsonl(logText)
141
+ const result = validateMcpAuditReceipt(imported.receipt)
142
+ const warnings = [...imported.warnings, ...result.warnings]
143
+
144
+ if (Boolean(args.json)) {
145
+ console.log(JSON.stringify({
146
+ ok: result.errors.length === 0,
147
+ demo: MCP_TELEMETRY_IMPORT_DEMO,
148
+ input: path.relative(process.cwd(), inputPath) || inputPath,
149
+ summary: {
150
+ ...result.summary,
151
+ parsedEntryCount: imported.summary.parsedEntryCount,
152
+ matchedResponseCount: imported.summary.matchedResponseCount,
153
+ missingGatewayLatency: imported.summary.missingGatewayLatency,
154
+ },
155
+ receipt: imported.receipt,
156
+ warnings,
157
+ errors: result.errors,
158
+ }, null, 2))
159
+ } else {
160
+ console.log('🧪 Pluribus demo: MCP telemetry import')
161
+ console.log(` Input: ${path.relative(process.cwd(), inputPath) || inputPath}`)
162
+ console.log('')
163
+
164
+ if (result.errors.length === 0) {
165
+ console.log(`✅ MCP telemetry imported: ${imported.summary.parsedEntryCount} JSONL entries → ${result.summary.toolCallCount} audit receipt tool calls`)
166
+ if (warnings.length > 0) for (const warning of warnings) console.log(` • ${warning}`)
167
+ console.log('')
168
+ console.log('Why this matters: rpc-messages.jsonl is a useful fallback, but it usually proves tool-call attribution before it proves gateway latency. Convert raw JSON-RPC traces into privacy-safe receipts, then mark missing gateway evidence explicitly.')
169
+ console.log('Try your own log: pluribus demo mcp-telemetry-import --input path/to/rpc-messages.jsonl --json')
170
+ } else {
171
+ console.error('❌ MCP telemetry import produced an invalid receipt:')
172
+ for (const error of result.errors) console.error(` • ${error}`)
173
+ }
174
+ }
175
+
176
+ if (result.errors.length > 0) process.exit(1)
177
+ }
178
+
119
179
  function bundledSkillUseRateReceiptPath() {
120
180
  return fileURLToPath(new URL('../../examples/skill-use-rate-receipts/skill-use-rate-receipt.json', import.meta.url))
121
181
  }
@@ -124,6 +184,10 @@ function bundledMcpAuditReceiptPath() {
124
184
  return fileURLToPath(new URL('../../examples/mcp-audit-receipts/mcp-audit-receipt.json', import.meta.url))
125
185
  }
126
186
 
187
+ function bundledMcpTelemetryJsonlPath() {
188
+ return fileURLToPath(new URL('../../examples/mcp-telemetry-import/sample-rpc-messages.jsonl', import.meta.url))
189
+ }
190
+
127
191
  export function validateSkillUseRateReceipt(receipt) {
128
192
  const errors = []
129
193
  const warnings = []
@@ -209,6 +273,172 @@ export function validateSkillUseRateReceipt(receipt) {
209
273
  }
210
274
  }
211
275
 
276
+
277
+ export function importMcpTelemetryJsonl(logText) {
278
+ const warnings = []
279
+ const entries = []
280
+ const pending = new Map()
281
+ const toolCalls = []
282
+ let matchedResponseCount = 0
283
+ let missingGatewayLatency = true
284
+
285
+ for (const [lineIndex, rawLine] of logText.split(/\r?\n/).entries()) {
286
+ const line = rawLine.trim()
287
+ if (!line) continue
288
+ try {
289
+ const entry = JSON.parse(line)
290
+ entries.push(entry)
291
+ const message = unwrapMcpMessage(entry)
292
+ const timestamp = entry.timestamp || entry.time || message.timestamp || null
293
+
294
+ if (isToolCallRequest(message)) {
295
+ pending.set(String(message.id), { entry, message, timestamp, lineIndex })
296
+ } else if (message.id != null && pending.has(String(message.id))) {
297
+ const request = pending.get(String(message.id))
298
+ pending.delete(String(message.id))
299
+ matchedResponseCount++
300
+ const durationMs = durationBetween(request.timestamp, timestamp)
301
+ if (durationMs > 0) missingGatewayLatency = false
302
+ toolCalls.push(toolCallFromRequestResponse(request, message, durationMs))
303
+ }
304
+ } catch (err) {
305
+ warnings.push(`line ${lineIndex + 1} was skipped: invalid JSON (${err.message})`)
306
+ }
307
+ }
308
+
309
+ for (const request of pending.values()) {
310
+ toolCalls.push(toolCallFromRequestResponse(request, null, 0))
311
+ }
312
+
313
+ if (toolCalls.length === 0) warnings.push('no tools/call request/response pairs were found')
314
+ if (missingGatewayLatency) warnings.push('gateway.jsonl-style latency/status evidence is missing; fallback rpc-messages.jsonl can still prove tool-call attribution')
315
+
316
+ const receipt = {
317
+ schema: MCP_AUDIT_RECEIPT_SCHEMA,
318
+ run_id: 'mcp-telemetry-import-demo',
319
+ generated_at: '2026-06-07T13:00:00Z',
320
+ server: {
321
+ name: 'mcp-gateway-or-fallback-log',
322
+ transport: 'jsonrpc-jsonl',
323
+ version: 'unknown',
324
+ },
325
+ client: {
326
+ name: 'unknown-mcp-client',
327
+ workspace: 'redacted',
328
+ },
329
+ audit_policy: {
330
+ raw_arguments: 'redacted_shape_only',
331
+ raw_results: 'redacted_shape_only',
332
+ privacy_boundary: 'source JSONL may contain raw protocol data; receipt keeps only shapes, hashes, status, and timing evidence',
333
+ },
334
+ telemetry_source: {
335
+ kind: missingGatewayLatency ? 'rpc-messages.jsonl-fallback' : 'gateway-or-timestamped-jsonl',
336
+ parsed_entries: entries.length,
337
+ matched_responses: matchedResponseCount,
338
+ },
339
+ tool_calls: toolCalls,
340
+ usage_metrics: buildMcpUsageMetrics(toolCalls),
341
+ }
342
+
343
+ return {
344
+ receipt,
345
+ warnings,
346
+ summary: {
347
+ parsedEntryCount: entries.length,
348
+ matchedResponseCount,
349
+ missingGatewayLatency,
350
+ },
351
+ }
352
+ }
353
+
354
+ function unwrapMcpMessage(entry) {
355
+ return entry.message || entry.msg || entry.rpc || entry.jsonrpc_message || entry
356
+ }
357
+
358
+ function isToolCallRequest(message) {
359
+ return message && message.id != null && ['tools/call', 'tools.call', 'mcp.tools.call'].includes(message.method)
360
+ }
361
+
362
+ function toolCallFromRequestResponse(request, response, durationMs) {
363
+ const params = request.message.params || {}
364
+ const toolName = params.name || params.tool_name || params.tool || 'unknown_tool'
365
+ const status = response == null ? 'empty' : response.error ? 'error' : 'ok'
366
+ const resultShape = response == null ? 'missing_response' : response.error ? `error:${response.error.code || 'unknown'}` : shapeLabel(response.result)
367
+ const userSource = request.entry.user_id || request.entry.actor || request.entry.principal || request.entry.session_id || 'unknown-user'
368
+ const tokenSource = request.entry.token_subject || request.entry.token_id || request.entry.principal || 'unknown-token'
369
+
370
+ return {
371
+ event: 'mcp.tool_call',
372
+ request_id: String(request.message.id),
373
+ session_id: String(request.entry.session_id || request.entry.run_id || 'unknown-session'),
374
+ user_id_hash: privacyHash(userSource),
375
+ token_subject_hash: privacyHash(tokenSource),
376
+ token_scopes: Array.isArray(request.entry.token_scopes) && request.entry.token_scopes.length > 0 ? request.entry.token_scopes : ['unknown'],
377
+ tool_name: String(toolName),
378
+ args_shape: shapeObject(params.arguments || params.args || {}),
379
+ status,
380
+ duration_ms: Math.max(0, durationMs),
381
+ result_shape: resultShape,
382
+ error_class: response?.error ? String(response.error.code || response.error.message || 'mcp_error') : null,
383
+ }
384
+ }
385
+
386
+ function buildMcpUsageMetrics(toolCalls) {
387
+ const callsByStatus = new Map()
388
+ for (const call of toolCalls) {
389
+ const key = `${call.tool_name}:${call.status}:${call.token_scopes[0] || 'unknown'}`
390
+ callsByStatus.set(key, (callsByStatus.get(key) || 0) + 1)
391
+ }
392
+ const metrics = [...callsByStatus.entries()].map(([key, value]) => ({
393
+ name: 'mcp_tool_calls_total',
394
+ type: 'counter',
395
+ value: String(value),
396
+ labels: ['tool_name', 'status', 'token_scope'],
397
+ dimensions: key,
398
+ }))
399
+ const durations = toolCalls.filter((call) => call.duration_ms > 0)
400
+ if (durations.length > 0) {
401
+ metrics.push({
402
+ name: 'mcp_tool_call_duration_ms',
403
+ type: 'histogram',
404
+ value: String(Math.round(durations.reduce((sum, call) => sum + call.duration_ms, 0) / durations.length)),
405
+ labels: ['tool_name', 'status'],
406
+ })
407
+ }
408
+ return metrics.length > 0 ? metrics : [{ name: 'mcp_tool_calls_total', type: 'counter', value: '0', labels: ['tool_name', 'status'] }]
409
+ }
410
+
411
+ function durationBetween(start, end) {
412
+ if (!start || !end) return 0
413
+ const started = Date.parse(start)
414
+ const ended = Date.parse(end)
415
+ if (Number.isNaN(started) || Number.isNaN(ended) || ended < started) return 0
416
+ return ended - started
417
+ }
418
+
419
+ function privacyHash(value) {
420
+ return `sha256:${createHash('sha256').update(String(value)).digest('hex')}`
421
+ }
422
+
423
+ function shapeObject(value) {
424
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
425
+ return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, shapeLabel(nested)]))
426
+ }
427
+
428
+ function shapeLabel(value) {
429
+ if (value === null) return 'null'
430
+ if (Array.isArray(value)) return `array:${value.length}`
431
+ if (typeof value === 'object') return `object:${Object.keys(value).length}`
432
+ if (typeof value === 'string') return looksSensitive(value) ? 'redacted_string' : 'string'
433
+ if (typeof value === 'number') return 'number'
434
+ if (typeof value === 'boolean') return 'boolean'
435
+ return typeof value
436
+ }
437
+
438
+ function looksSensitive(value) {
439
+ return /select\s|insert\s|update\s|delete\s|token|secret|password|bearer/i.test(value)
440
+ }
441
+
212
442
  export function validateMcpAuditReceipt(receipt) {
213
443
  const errors = []
214
444
  const warnings = []
@@ -1 +1 @@
1
- export const VERSION = '0.3.38'
1
+ export const VERSION = '0.3.39'