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.
|
|
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",
|
package/src/commands/demo.js
CHANGED
|
@@ -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
|
|
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 = []
|
package/src/utils/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.3.
|
|
1
|
+
export const VERSION = '0.3.39'
|