pluribus-context 0.3.38 → 0.3.40
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 +9 -0
- package/README.md +1 -1
- package/bin/pluribus.js +6 -1
- package/docs/context-budget-receipts.md +43 -0
- package/examples/mcp-telemetry-import/README.md +27 -0
- package/examples/mcp-telemetry-import/sample-rpc-messages.jsonl +4 -0
- package/examples/tool-surface-diff-receipts/tool-surface-diff-receipt.json +61 -0
- package/package.json +6 -1
- package/src/commands/demo.js +350 -1
- package/src/utils/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
All notable changes to Pluribus are documented here.
|
|
6
6
|
|
|
7
|
+
## 0.3.40 - 2026-06-09
|
|
8
|
+
|
|
9
|
+
- Added `pluribus demo tool-surface-diff`, a tiny npm-runnable MCP dynamic-discovery receipt demo for proving discovered, activated, withheld, and blocked runtime tool-surface changes without logging raw schemas, prompts, or results.
|
|
10
|
+
- Expanded npm discovery keywords around MCP audit, gateways, security, tool discovery, and audit trails so the package is easier to find from the market lane now forming around MCP governance.
|
|
11
|
+
|
|
12
|
+
## 0.3.39 - 2026-06-07
|
|
13
|
+
|
|
14
|
+
- 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.
|
|
15
|
+
|
|
7
16
|
## 0.3.38 - 2026-06-06
|
|
8
17
|
|
|
9
18
|
- 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/README.md
CHANGED
|
@@ -14,7 +14,7 @@ The original sync workflow is still useful: Pluribus can keep project instructio
|
|
|
14
14
|
|
|
15
15
|
It is **not** a persistent memory layer, retrieval system, agent orchestrator, enterprise ContextOps platform, or agent-merging framework. Think evidence for context boundaries: `CLAUDE.md`, `.cursorrules`, `copilot-instructions.md`, `AGENTS.md`, MCP Tool Search, Agent Skills, RAG/code-search, pruning, and compaction — with privacy-safe receipts instead of raw content dumps.
|
|
16
16
|
|
|
17
|
-
**Reviewer shortcut:** evaluating Pluribus for a list, newsletter, package roundup, or tool directory? Use the [Community Review Packet](docs/community-review-packet.md) for copy-paste directory submission fields, safety/removability notes, feedback links, and disposable 60-second smoke tests. If you only run one command for the cross-tool audit, try `npx --yes pluribus-context@latest audit --json --fidelity-report` to see native discovery surfaces, generic fallbacks, load evidence, duplicate-load selection evidence, manual activation requirements, effective context scope, and semantic differences. For the agent-observability wedge, start with [context-budget receipts](docs/context-budget-receipts.md): privacy-safe evidence for what MCP schemas, skills, memory, subagents, CLI help, retrieval chunks, pruning runs, or compaction summaries crossed an agent boundary. If you want the same idea as a copyable skill, use the [context-receipts Agent Skill recipe](skills/context-receipts/). npm `latest` is currently aligned with the GitHub release; the review packet also documents a GitHub-release smoke fallback for future release-lag windows.
|
|
17
|
+
**Reviewer shortcut:** evaluating Pluribus for a list, newsletter, package roundup, or tool directory? Use the [Community Review Packet](docs/community-review-packet.md) for copy-paste directory submission fields, safety/removability notes, feedback links, and disposable 60-second smoke tests. If you only run one command for the cross-tool audit, try `npx --yes pluribus-context@latest audit --json --fidelity-report` to see native discovery surfaces, generic fallbacks, load evidence, duplicate-load selection evidence, manual activation requirements, effective context scope, and semantic differences. For the agent-observability wedge, start with [context-budget receipts](docs/context-budget-receipts.md): privacy-safe evidence for what MCP schemas, skills, memory, subagents, CLI help, retrieval chunks, pruning runs, or compaction summaries crossed an agent boundary. It now explicitly covers the "Tool Search fixed MCP bloat" objection: the receipt proves which lane stayed deferred, which tool was expanded, and whether schemas leaked through `messages`/bootstrap anyway. For a 60-second runtime-discovery proof, run `npx --yes pluribus-context@latest demo tool-surface-diff --json`; it validates a receipt for discovered → activated → withheld/blocked MCP tools without raw schemas/prompts/results. If you want the same idea as a copyable skill, use the [context-receipts Agent Skill recipe](skills/context-receipts/). npm `latest` is currently aligned with the GitHub release; the review packet also documents a GitHub-release smoke fallback for future release-lag windows.
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
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,10 @@ 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
|
|
97
|
+
pluribus demo tool-surface-diff
|
|
98
|
+
pluribus demo tool-surface-diff --json
|
|
94
99
|
|
|
95
100
|
DOCS
|
|
96
101
|
https://github.com/caioribeiroclw-pixel/pluribus
|
|
@@ -102,7 +107,7 @@ const COMMAND_FLAGS = {
|
|
|
102
107
|
validate: new Set(['source', 'update-imports']),
|
|
103
108
|
audit: new Set(['source', 'tools', 'update-imports', 'strict', 'ci', 'json', 'output', 'github-annotations', 'fidelity-report']),
|
|
104
109
|
watch: new Set(['source', 'tools', 'update-imports', 'dry-run', 'once', 'debounce']),
|
|
105
|
-
demo: new Set(['receipt', 'json']),
|
|
110
|
+
demo: new Set(['receipt', 'input', 'json']),
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
function getFlagNames(argv) {
|
|
@@ -8,6 +8,49 @@ This is different from generic token accounting. A context-budget receipt should
|
|
|
8
8
|
|
|
9
9
|
If you want a copyable Agent Skill recipe instead of a spec-style guide, see [`skills/context-receipts/`](../skills/context-receipts/). It turns the receipt pattern into a 60-second smoke checklist for Tool Search, skills, and subagent boundaries.
|
|
10
10
|
|
|
11
|
+
## If Tool Search already fixed the bloat
|
|
12
|
+
|
|
13
|
+
Modern hosts can defer large MCP catalogs behind Tool Search or similar lazy discovery. That changes the receipt question; it does not remove it.
|
|
14
|
+
|
|
15
|
+
Do not use a context-budget receipt to re-prove that every schema was smaller than before. Use it to prove the boundary that lazy loading promised:
|
|
16
|
+
|
|
17
|
+
- the catalog/index was loaded instead of full definitions;
|
|
18
|
+
- the selected query loaded only the matching tool definitions;
|
|
19
|
+
- unselected tool groups stayed deferred or withheld;
|
|
20
|
+
- a schema did not enter a side lane such as `messages`, subagent bootstrap, skill preamble, or memory hydration; and
|
|
21
|
+
- the receipt records what evidence is missing when only fallback client telemetry exists.
|
|
22
|
+
|
|
23
|
+
This makes the receipt useful in the common objection case: "MCP context bloat is solved by Tool Search." A good receipt should answer: **solved where, for this turn, through which lane, and with what proof?**
|
|
24
|
+
|
|
25
|
+
Runnable fixture for the normal happy path:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
node examples/context-input-evidence/convert-mcp-tool-search-log.mjs
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Public trace:
|
|
32
|
+
|
|
33
|
+
- `examples/context-input-evidence/mcp-tool-search-otel-trace.json`
|
|
34
|
+
|
|
35
|
+
Minimum hidden-bypass fields for managed seats or gateways:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"event.name": "mcp.deferral.evaluated",
|
|
40
|
+
"mcp.defer_loading.enabled": true,
|
|
41
|
+
"mcp.catalog.deferred": true,
|
|
42
|
+
"mcp.tool_search.selected_tool_count": 0,
|
|
43
|
+
"context.messages.remote_mcp_schema_count_bucket": "over_25",
|
|
44
|
+
"context.messages.delta_token_bucket": "under_100k",
|
|
45
|
+
"context.attribution": "remote_mcp_schema_in_messages",
|
|
46
|
+
"expected_behavior": "deferred_until_tool_search_match",
|
|
47
|
+
"verdict": "deferral_bypassed",
|
|
48
|
+
"privacy.raw_schema_included": false
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
That shape deliberately avoids raw schema bodies, connector names, private URLs, and prompt text. It proves attribution, not whether the selected tool was semantically optimal.
|
|
53
|
+
|
|
11
54
|
## When to use this receipt
|
|
12
55
|
|
|
13
56
|
Use a context-budget receipt when a coding agent looks lazy, fails with `prompt is too long`, or returns a tiny summary after a subagent/tool-heavy step and you need to distinguish:
|
|
@@ -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"}}}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "pluribus.mcp_tool_surface_diff_receipt.v1",
|
|
3
|
+
"run_id": "tool-surface-diff-demo",
|
|
4
|
+
"generated_at": "2026-06-09T13:00:00Z",
|
|
5
|
+
"platform": {
|
|
6
|
+
"name": "enterprise-mcp-dynamic-discovery",
|
|
7
|
+
"audit_sink": "admin-center-or-siem"
|
|
8
|
+
},
|
|
9
|
+
"catalog": {
|
|
10
|
+
"server_id": "mcp://sales-ops-gateway",
|
|
11
|
+
"previous_hash": "sha256:previous-catalog-redacted",
|
|
12
|
+
"current_hash": "sha256:current-catalog-redacted"
|
|
13
|
+
},
|
|
14
|
+
"runtime_discovery": {
|
|
15
|
+
"enabled": true,
|
|
16
|
+
"trigger": "runtime_tool_catalog_diff"
|
|
17
|
+
},
|
|
18
|
+
"privacy_boundary": {
|
|
19
|
+
"raw_schemas": "omitted_hash_only",
|
|
20
|
+
"raw_prompts": "omitted",
|
|
21
|
+
"raw_results": "omitted"
|
|
22
|
+
},
|
|
23
|
+
"tools": [
|
|
24
|
+
{
|
|
25
|
+
"tool_id": "tool:crm.search_accounts",
|
|
26
|
+
"name_hash": "sha256:0cc2efb4a26f4c5eb4f7d8c99e78d37adbdba07d50ee7873452c0216d02b1f48",
|
|
27
|
+
"schema_hash": "sha256:6f4fbe0a8be41b6e29c0c1c113aac38dfefdd12b89c6e9d4a996df2537acdb71",
|
|
28
|
+
"status": "activated",
|
|
29
|
+
"validation_outcome": "accepted",
|
|
30
|
+
"diff_summary": {
|
|
31
|
+
"added_fields": 1,
|
|
32
|
+
"removed_fields": 0,
|
|
33
|
+
"changed_fields": 0
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"tool_id": "tool:crm.export_contacts",
|
|
38
|
+
"name_hash": "sha256:f38f53f9ba3c348e67332a24a7d15f5e7ab1c9253cf01f3451e15d9e15435e13",
|
|
39
|
+
"schema_hash": "sha256:95ddc9b86a0c2d8bc6f3ca0bcce93dca2469ced51b0d38c8f8c5aa88554e0032",
|
|
40
|
+
"status": "blocked",
|
|
41
|
+
"validation_outcome": "blocked_by_rai",
|
|
42
|
+
"diff_summary": {
|
|
43
|
+
"added_fields": 4,
|
|
44
|
+
"removed_fields": 0,
|
|
45
|
+
"changed_fields": 2
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"tool_id": "tool:billing.refund_invoice",
|
|
50
|
+
"name_hash": "sha256:abfdaf6f0a3f33342aca6d8b0d63c303e99978481d26827843a901cb6237617d",
|
|
51
|
+
"schema_hash": "sha256:3b4fdc3ec15d2fa1cc589c1c7de280a19772d5745b4ef54d27806714be12bb8c",
|
|
52
|
+
"status": "withheld",
|
|
53
|
+
"validation_outcome": "entitlement_filtered",
|
|
54
|
+
"diff_summary": {
|
|
55
|
+
"added_fields": 0,
|
|
56
|
+
"removed_fields": 0,
|
|
57
|
+
"changed_fields": 0
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pluribus-context",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.40",
|
|
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",
|
|
@@ -68,6 +68,11 @@
|
|
|
68
68
|
"ai-agent-observability",
|
|
69
69
|
"opentelemetry",
|
|
70
70
|
"mcp",
|
|
71
|
+
"mcp-audit",
|
|
72
|
+
"mcp-gateway",
|
|
73
|
+
"mcp-security",
|
|
74
|
+
"tool-discovery",
|
|
75
|
+
"audit-trail",
|
|
71
76
|
"drift-detection",
|
|
72
77
|
"openclaw",
|
|
73
78
|
"rules",
|
package/src/commands/demo.js
CHANGED
|
@@ -4,14 +4,18 @@
|
|
|
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 TOOL_SURFACE_DIFF_DEMO = 'tool-surface-diff'
|
|
15
|
+
const AVAILABLE_DEMOS = [SKILL_USE_RATE_DEMO, MCP_AUDIT_RECEIPT_DEMO, MCP_TELEMETRY_IMPORT_DEMO, TOOL_SURFACE_DIFF_DEMO]
|
|
13
16
|
const SKILL_USE_RATE_SCHEMA = 'pluribus.skill_use_rate_receipt.v1'
|
|
14
17
|
const MCP_AUDIT_RECEIPT_SCHEMA = 'pluribus.mcp_tool_call_audit_receipt.v1'
|
|
18
|
+
const TOOL_SURFACE_DIFF_SCHEMA = 'pluribus.mcp_tool_surface_diff_receipt.v1'
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
21
|
* @param {Record<string, string | boolean>} args
|
|
@@ -25,6 +29,10 @@ export async function runDemo(args, positional = []) {
|
|
|
25
29
|
return runSkillUseRateDemo(args)
|
|
26
30
|
case MCP_AUDIT_RECEIPT_DEMO:
|
|
27
31
|
return runMcpAuditReceiptDemo(args)
|
|
32
|
+
case MCP_TELEMETRY_IMPORT_DEMO:
|
|
33
|
+
return runMcpTelemetryImportDemo(args)
|
|
34
|
+
case TOOL_SURFACE_DIFF_DEMO:
|
|
35
|
+
return runToolSurfaceDiffDemo(args)
|
|
28
36
|
default:
|
|
29
37
|
console.error(`❌ Unknown demo: ${demoName}`)
|
|
30
38
|
console.error(` Available demos: ${AVAILABLE_DEMOS.join(', ')}`)
|
|
@@ -82,6 +90,12 @@ function runSkillUseRateDemo(args) {
|
|
|
82
90
|
if (result.errors.length > 0) process.exit(1)
|
|
83
91
|
}
|
|
84
92
|
|
|
93
|
+
function selectedInputPath(args, defaultPath) {
|
|
94
|
+
return typeof args.input === 'string' && args.input.trim()
|
|
95
|
+
? path.resolve(process.cwd(), args.input)
|
|
96
|
+
: defaultPath
|
|
97
|
+
}
|
|
98
|
+
|
|
85
99
|
function runMcpAuditReceiptDemo(args) {
|
|
86
100
|
const receiptPath = selectedReceiptPath(args, bundledMcpAuditReceiptPath())
|
|
87
101
|
const receipt = readReceipt(receiptPath, 'MCP audit')
|
|
@@ -116,6 +130,56 @@ function runMcpAuditReceiptDemo(args) {
|
|
|
116
130
|
if (result.errors.length > 0) process.exit(1)
|
|
117
131
|
}
|
|
118
132
|
|
|
133
|
+
|
|
134
|
+
function runMcpTelemetryImportDemo(args) {
|
|
135
|
+
const inputPath = selectedInputPath(args, bundledMcpTelemetryJsonlPath())
|
|
136
|
+
let logText
|
|
137
|
+
try {
|
|
138
|
+
logText = fs.readFileSync(inputPath, 'utf8')
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.error(`❌ Could not read MCP telemetry JSONL at ${inputPath}: ${err.message}`)
|
|
141
|
+
process.exit(1)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const imported = importMcpTelemetryJsonl(logText)
|
|
145
|
+
const result = validateMcpAuditReceipt(imported.receipt)
|
|
146
|
+
const warnings = [...imported.warnings, ...result.warnings]
|
|
147
|
+
|
|
148
|
+
if (Boolean(args.json)) {
|
|
149
|
+
console.log(JSON.stringify({
|
|
150
|
+
ok: result.errors.length === 0,
|
|
151
|
+
demo: MCP_TELEMETRY_IMPORT_DEMO,
|
|
152
|
+
input: path.relative(process.cwd(), inputPath) || inputPath,
|
|
153
|
+
summary: {
|
|
154
|
+
...result.summary,
|
|
155
|
+
parsedEntryCount: imported.summary.parsedEntryCount,
|
|
156
|
+
matchedResponseCount: imported.summary.matchedResponseCount,
|
|
157
|
+
missingGatewayLatency: imported.summary.missingGatewayLatency,
|
|
158
|
+
},
|
|
159
|
+
receipt: imported.receipt,
|
|
160
|
+
warnings,
|
|
161
|
+
errors: result.errors,
|
|
162
|
+
}, null, 2))
|
|
163
|
+
} else {
|
|
164
|
+
console.log('🧪 Pluribus demo: MCP telemetry import')
|
|
165
|
+
console.log(` Input: ${path.relative(process.cwd(), inputPath) || inputPath}`)
|
|
166
|
+
console.log('')
|
|
167
|
+
|
|
168
|
+
if (result.errors.length === 0) {
|
|
169
|
+
console.log(`✅ MCP telemetry imported: ${imported.summary.parsedEntryCount} JSONL entries → ${result.summary.toolCallCount} audit receipt tool calls`)
|
|
170
|
+
if (warnings.length > 0) for (const warning of warnings) console.log(` • ${warning}`)
|
|
171
|
+
console.log('')
|
|
172
|
+
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.')
|
|
173
|
+
console.log('Try your own log: pluribus demo mcp-telemetry-import --input path/to/rpc-messages.jsonl --json')
|
|
174
|
+
} else {
|
|
175
|
+
console.error('❌ MCP telemetry import produced an invalid receipt:')
|
|
176
|
+
for (const error of result.errors) console.error(` • ${error}`)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (result.errors.length > 0) process.exit(1)
|
|
181
|
+
}
|
|
182
|
+
|
|
119
183
|
function bundledSkillUseRateReceiptPath() {
|
|
120
184
|
return fileURLToPath(new URL('../../examples/skill-use-rate-receipts/skill-use-rate-receipt.json', import.meta.url))
|
|
121
185
|
}
|
|
@@ -124,6 +188,48 @@ function bundledMcpAuditReceiptPath() {
|
|
|
124
188
|
return fileURLToPath(new URL('../../examples/mcp-audit-receipts/mcp-audit-receipt.json', import.meta.url))
|
|
125
189
|
}
|
|
126
190
|
|
|
191
|
+
function bundledMcpTelemetryJsonlPath() {
|
|
192
|
+
return fileURLToPath(new URL('../../examples/mcp-telemetry-import/sample-rpc-messages.jsonl', import.meta.url))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function bundledToolSurfaceDiffReceiptPath() {
|
|
196
|
+
return fileURLToPath(new URL('../../examples/tool-surface-diff-receipts/tool-surface-diff-receipt.json', import.meta.url))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function runToolSurfaceDiffDemo(args) {
|
|
200
|
+
const receiptPath = selectedReceiptPath(args, bundledToolSurfaceDiffReceiptPath())
|
|
201
|
+
const receipt = readReceipt(receiptPath, 'tool-surface diff')
|
|
202
|
+
const result = validateToolSurfaceDiffReceipt(receipt)
|
|
203
|
+
|
|
204
|
+
if (Boolean(args.json)) {
|
|
205
|
+
console.log(JSON.stringify({
|
|
206
|
+
ok: result.errors.length === 0,
|
|
207
|
+
demo: TOOL_SURFACE_DIFF_DEMO,
|
|
208
|
+
receipt: path.relative(process.cwd(), receiptPath) || receiptPath,
|
|
209
|
+
summary: result.summary,
|
|
210
|
+
warnings: result.warnings,
|
|
211
|
+
errors: result.errors,
|
|
212
|
+
}, null, 2))
|
|
213
|
+
} else {
|
|
214
|
+
console.log('🧪 Pluribus demo: MCP tool-surface diff receipt')
|
|
215
|
+
console.log(` Receipt: ${path.relative(process.cwd(), receiptPath) || receiptPath}`)
|
|
216
|
+
console.log('')
|
|
217
|
+
|
|
218
|
+
if (result.errors.length === 0) {
|
|
219
|
+
console.log(`✅ tool-surface diff receipt ok: ${result.summary.discoveredCount} discovered, ${result.summary.activatedCount} activated, ${result.summary.withheldCount} withheld/blocked`)
|
|
220
|
+
for (const warning of result.warnings) console.log(` • ${warning}`)
|
|
221
|
+
console.log('')
|
|
222
|
+
console.log('Why this matters: runtime MCP discovery changes the active tool surface. Persist a low-cardinality receipt of discovered → activated → withheld/blocked tools without logging raw schemas, prompts, or results.')
|
|
223
|
+
console.log('Try your own receipt: pluribus demo tool-surface-diff --receipt path/to/tool-surface-diff-receipt.json --json')
|
|
224
|
+
} else {
|
|
225
|
+
console.error('❌ tool-surface diff receipt invalid:')
|
|
226
|
+
for (const error of result.errors) console.error(` • ${error}`)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (result.errors.length > 0) process.exit(1)
|
|
231
|
+
}
|
|
232
|
+
|
|
127
233
|
export function validateSkillUseRateReceipt(receipt) {
|
|
128
234
|
const errors = []
|
|
129
235
|
const warnings = []
|
|
@@ -209,6 +315,172 @@ export function validateSkillUseRateReceipt(receipt) {
|
|
|
209
315
|
}
|
|
210
316
|
}
|
|
211
317
|
|
|
318
|
+
|
|
319
|
+
export function importMcpTelemetryJsonl(logText) {
|
|
320
|
+
const warnings = []
|
|
321
|
+
const entries = []
|
|
322
|
+
const pending = new Map()
|
|
323
|
+
const toolCalls = []
|
|
324
|
+
let matchedResponseCount = 0
|
|
325
|
+
let missingGatewayLatency = true
|
|
326
|
+
|
|
327
|
+
for (const [lineIndex, rawLine] of logText.split(/\r?\n/).entries()) {
|
|
328
|
+
const line = rawLine.trim()
|
|
329
|
+
if (!line) continue
|
|
330
|
+
try {
|
|
331
|
+
const entry = JSON.parse(line)
|
|
332
|
+
entries.push(entry)
|
|
333
|
+
const message = unwrapMcpMessage(entry)
|
|
334
|
+
const timestamp = entry.timestamp || entry.time || message.timestamp || null
|
|
335
|
+
|
|
336
|
+
if (isToolCallRequest(message)) {
|
|
337
|
+
pending.set(String(message.id), { entry, message, timestamp, lineIndex })
|
|
338
|
+
} else if (message.id != null && pending.has(String(message.id))) {
|
|
339
|
+
const request = pending.get(String(message.id))
|
|
340
|
+
pending.delete(String(message.id))
|
|
341
|
+
matchedResponseCount++
|
|
342
|
+
const durationMs = durationBetween(request.timestamp, timestamp)
|
|
343
|
+
if (durationMs > 0) missingGatewayLatency = false
|
|
344
|
+
toolCalls.push(toolCallFromRequestResponse(request, message, durationMs))
|
|
345
|
+
}
|
|
346
|
+
} catch (err) {
|
|
347
|
+
warnings.push(`line ${lineIndex + 1} was skipped: invalid JSON (${err.message})`)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
for (const request of pending.values()) {
|
|
352
|
+
toolCalls.push(toolCallFromRequestResponse(request, null, 0))
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (toolCalls.length === 0) warnings.push('no tools/call request/response pairs were found')
|
|
356
|
+
if (missingGatewayLatency) warnings.push('gateway.jsonl-style latency/status evidence is missing; fallback rpc-messages.jsonl can still prove tool-call attribution')
|
|
357
|
+
|
|
358
|
+
const receipt = {
|
|
359
|
+
schema: MCP_AUDIT_RECEIPT_SCHEMA,
|
|
360
|
+
run_id: 'mcp-telemetry-import-demo',
|
|
361
|
+
generated_at: '2026-06-07T13:00:00Z',
|
|
362
|
+
server: {
|
|
363
|
+
name: 'mcp-gateway-or-fallback-log',
|
|
364
|
+
transport: 'jsonrpc-jsonl',
|
|
365
|
+
version: 'unknown',
|
|
366
|
+
},
|
|
367
|
+
client: {
|
|
368
|
+
name: 'unknown-mcp-client',
|
|
369
|
+
workspace: 'redacted',
|
|
370
|
+
},
|
|
371
|
+
audit_policy: {
|
|
372
|
+
raw_arguments: 'redacted_shape_only',
|
|
373
|
+
raw_results: 'redacted_shape_only',
|
|
374
|
+
privacy_boundary: 'source JSONL may contain raw protocol data; receipt keeps only shapes, hashes, status, and timing evidence',
|
|
375
|
+
},
|
|
376
|
+
telemetry_source: {
|
|
377
|
+
kind: missingGatewayLatency ? 'rpc-messages.jsonl-fallback' : 'gateway-or-timestamped-jsonl',
|
|
378
|
+
parsed_entries: entries.length,
|
|
379
|
+
matched_responses: matchedResponseCount,
|
|
380
|
+
},
|
|
381
|
+
tool_calls: toolCalls,
|
|
382
|
+
usage_metrics: buildMcpUsageMetrics(toolCalls),
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
receipt,
|
|
387
|
+
warnings,
|
|
388
|
+
summary: {
|
|
389
|
+
parsedEntryCount: entries.length,
|
|
390
|
+
matchedResponseCount,
|
|
391
|
+
missingGatewayLatency,
|
|
392
|
+
},
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function unwrapMcpMessage(entry) {
|
|
397
|
+
return entry.message || entry.msg || entry.rpc || entry.jsonrpc_message || entry
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function isToolCallRequest(message) {
|
|
401
|
+
return message && message.id != null && ['tools/call', 'tools.call', 'mcp.tools.call'].includes(message.method)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function toolCallFromRequestResponse(request, response, durationMs) {
|
|
405
|
+
const params = request.message.params || {}
|
|
406
|
+
const toolName = params.name || params.tool_name || params.tool || 'unknown_tool'
|
|
407
|
+
const status = response == null ? 'empty' : response.error ? 'error' : 'ok'
|
|
408
|
+
const resultShape = response == null ? 'missing_response' : response.error ? `error:${response.error.code || 'unknown'}` : shapeLabel(response.result)
|
|
409
|
+
const userSource = request.entry.user_id || request.entry.actor || request.entry.principal || request.entry.session_id || 'unknown-user'
|
|
410
|
+
const tokenSource = request.entry.token_subject || request.entry.token_id || request.entry.principal || 'unknown-token'
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
event: 'mcp.tool_call',
|
|
414
|
+
request_id: String(request.message.id),
|
|
415
|
+
session_id: String(request.entry.session_id || request.entry.run_id || 'unknown-session'),
|
|
416
|
+
user_id_hash: privacyHash(userSource),
|
|
417
|
+
token_subject_hash: privacyHash(tokenSource),
|
|
418
|
+
token_scopes: Array.isArray(request.entry.token_scopes) && request.entry.token_scopes.length > 0 ? request.entry.token_scopes : ['unknown'],
|
|
419
|
+
tool_name: String(toolName),
|
|
420
|
+
args_shape: shapeObject(params.arguments || params.args || {}),
|
|
421
|
+
status,
|
|
422
|
+
duration_ms: Math.max(0, durationMs),
|
|
423
|
+
result_shape: resultShape,
|
|
424
|
+
error_class: response?.error ? String(response.error.code || response.error.message || 'mcp_error') : null,
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function buildMcpUsageMetrics(toolCalls) {
|
|
429
|
+
const callsByStatus = new Map()
|
|
430
|
+
for (const call of toolCalls) {
|
|
431
|
+
const key = `${call.tool_name}:${call.status}:${call.token_scopes[0] || 'unknown'}`
|
|
432
|
+
callsByStatus.set(key, (callsByStatus.get(key) || 0) + 1)
|
|
433
|
+
}
|
|
434
|
+
const metrics = [...callsByStatus.entries()].map(([key, value]) => ({
|
|
435
|
+
name: 'mcp_tool_calls_total',
|
|
436
|
+
type: 'counter',
|
|
437
|
+
value: String(value),
|
|
438
|
+
labels: ['tool_name', 'status', 'token_scope'],
|
|
439
|
+
dimensions: key,
|
|
440
|
+
}))
|
|
441
|
+
const durations = toolCalls.filter((call) => call.duration_ms > 0)
|
|
442
|
+
if (durations.length > 0) {
|
|
443
|
+
metrics.push({
|
|
444
|
+
name: 'mcp_tool_call_duration_ms',
|
|
445
|
+
type: 'histogram',
|
|
446
|
+
value: String(Math.round(durations.reduce((sum, call) => sum + call.duration_ms, 0) / durations.length)),
|
|
447
|
+
labels: ['tool_name', 'status'],
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
return metrics.length > 0 ? metrics : [{ name: 'mcp_tool_calls_total', type: 'counter', value: '0', labels: ['tool_name', 'status'] }]
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function durationBetween(start, end) {
|
|
454
|
+
if (!start || !end) return 0
|
|
455
|
+
const started = Date.parse(start)
|
|
456
|
+
const ended = Date.parse(end)
|
|
457
|
+
if (Number.isNaN(started) || Number.isNaN(ended) || ended < started) return 0
|
|
458
|
+
return ended - started
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function privacyHash(value) {
|
|
462
|
+
return `sha256:${createHash('sha256').update(String(value)).digest('hex')}`
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function shapeObject(value) {
|
|
466
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
|
|
467
|
+
return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, shapeLabel(nested)]))
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function shapeLabel(value) {
|
|
471
|
+
if (value === null) return 'null'
|
|
472
|
+
if (Array.isArray(value)) return `array:${value.length}`
|
|
473
|
+
if (typeof value === 'object') return `object:${Object.keys(value).length}`
|
|
474
|
+
if (typeof value === 'string') return looksSensitive(value) ? 'redacted_string' : 'string'
|
|
475
|
+
if (typeof value === 'number') return 'number'
|
|
476
|
+
if (typeof value === 'boolean') return 'boolean'
|
|
477
|
+
return typeof value
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function looksSensitive(value) {
|
|
481
|
+
return /select\s|insert\s|update\s|delete\s|token|secret|password|bearer/i.test(value)
|
|
482
|
+
}
|
|
483
|
+
|
|
212
484
|
export function validateMcpAuditReceipt(receipt) {
|
|
213
485
|
const errors = []
|
|
214
486
|
const warnings = []
|
|
@@ -307,3 +579,80 @@ export function validateMcpAuditReceipt(receipt) {
|
|
|
307
579
|
},
|
|
308
580
|
}
|
|
309
581
|
}
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
export function validateToolSurfaceDiffReceipt(receipt) {
|
|
585
|
+
const errors = []
|
|
586
|
+
const warnings = []
|
|
587
|
+
|
|
588
|
+
function requireString(value, field) {
|
|
589
|
+
if (typeof value !== 'string' || value.trim() === '') errors.push(`${field} must be a non-empty string`)
|
|
590
|
+
}
|
|
591
|
+
function requireBoolean(value, field) {
|
|
592
|
+
if (typeof value !== 'boolean') errors.push(`${field} must be boolean`)
|
|
593
|
+
}
|
|
594
|
+
function requireNonNegativeInteger(value, field) {
|
|
595
|
+
if (!Number.isInteger(value) || value < 0) errors.push(`${field} must be a non-negative integer`)
|
|
596
|
+
}
|
|
597
|
+
function requireArray(value, field) {
|
|
598
|
+
if (!Array.isArray(value) || value.length === 0) errors.push(`${field} must be a non-empty array`)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (receipt.schema !== TOOL_SURFACE_DIFF_SCHEMA) errors.push(`schema must be ${TOOL_SURFACE_DIFF_SCHEMA}`)
|
|
602
|
+
requireString(receipt.run_id, 'run_id')
|
|
603
|
+
requireString(receipt.generated_at, 'generated_at')
|
|
604
|
+
requireString(receipt.platform?.name, 'platform.name')
|
|
605
|
+
requireString(receipt.platform?.audit_sink, 'platform.audit_sink')
|
|
606
|
+
requireString(receipt.catalog?.server_id, 'catalog.server_id')
|
|
607
|
+
requireString(receipt.catalog?.previous_hash, 'catalog.previous_hash')
|
|
608
|
+
requireString(receipt.catalog?.current_hash, 'catalog.current_hash')
|
|
609
|
+
requireBoolean(receipt.runtime_discovery?.enabled, 'runtime_discovery.enabled')
|
|
610
|
+
requireString(receipt.runtime_discovery?.trigger, 'runtime_discovery.trigger')
|
|
611
|
+
requireArray(receipt.tools, 'tools')
|
|
612
|
+
requireString(receipt.privacy_boundary?.raw_schemas, 'privacy_boundary.raw_schemas')
|
|
613
|
+
requireString(receipt.privacy_boundary?.raw_prompts, 'privacy_boundary.raw_prompts')
|
|
614
|
+
requireString(receipt.privacy_boundary?.raw_results, 'privacy_boundary.raw_results')
|
|
615
|
+
|
|
616
|
+
if (receipt.privacy_boundary?.raw_schemas !== 'omitted_hash_only') errors.push('privacy_boundary.raw_schemas must be omitted_hash_only')
|
|
617
|
+
if (receipt.privacy_boundary?.raw_prompts !== 'omitted') errors.push('privacy_boundary.raw_prompts must be omitted')
|
|
618
|
+
if (receipt.privacy_boundary?.raw_results !== 'omitted') errors.push('privacy_boundary.raw_results must be omitted')
|
|
619
|
+
|
|
620
|
+
const statuses = new Set(['discovered', 'activated', 'withheld', 'blocked', 'removed'])
|
|
621
|
+
const outcomes = new Set(['accepted', 'blocked_by_rai', 'blocked_by_xpia', 'schema_invalid', 'entitlement_filtered', 'not_selected', 'removed'])
|
|
622
|
+
let discoveredCount = 0
|
|
623
|
+
let activatedCount = 0
|
|
624
|
+
let withheldCount = 0
|
|
625
|
+
let rawLeakCount = 0
|
|
626
|
+
|
|
627
|
+
for (const [index, tool] of (receipt.tools || []).entries()) {
|
|
628
|
+
const prefix = `tools[${index}]`
|
|
629
|
+
requireString(tool.tool_id, `${prefix}.tool_id`)
|
|
630
|
+
requireString(tool.name_hash, `${prefix}.name_hash`)
|
|
631
|
+
requireString(tool.schema_hash, `${prefix}.schema_hash`)
|
|
632
|
+
requireString(tool.status, `${prefix}.status`)
|
|
633
|
+
requireString(tool.validation_outcome, `${prefix}.validation_outcome`)
|
|
634
|
+
requireNonNegativeInteger(tool.diff_summary?.added_fields, `${prefix}.diff_summary.added_fields`)
|
|
635
|
+
requireNonNegativeInteger(tool.diff_summary?.removed_fields, `${prefix}.diff_summary.removed_fields`)
|
|
636
|
+
requireNonNegativeInteger(tool.diff_summary?.changed_fields, `${prefix}.diff_summary.changed_fields`)
|
|
637
|
+
|
|
638
|
+
if (!statuses.has(tool.status)) errors.push(`${prefix}.status must be one of ${[...statuses].join('|')}`)
|
|
639
|
+
if (!outcomes.has(tool.validation_outcome)) errors.push(`${prefix}.validation_outcome must be one of ${[...outcomes].join('|')}`)
|
|
640
|
+
if (!String(tool.name_hash || '').startsWith('sha256:')) errors.push(`${prefix}.name_hash must be a sha256: hash, not a raw tool name`)
|
|
641
|
+
if (!String(tool.schema_hash || '').startsWith('sha256:')) errors.push(`${prefix}.schema_hash must be a sha256: hash, not a raw schema`)
|
|
642
|
+
if (typeof tool.raw_schema === 'string' || typeof tool.description === 'string') rawLeakCount++
|
|
643
|
+
|
|
644
|
+
if (['discovered', 'activated', 'withheld', 'blocked'].includes(tool.status)) discoveredCount++
|
|
645
|
+
if (tool.status === 'activated') activatedCount++
|
|
646
|
+
if (['withheld', 'blocked'].includes(tool.status)) withheldCount++
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (rawLeakCount > 0) errors.push(`tools must not include raw_schema or description (${rawLeakCount} raw fields found)`)
|
|
650
|
+
if (activatedCount === 0) warnings.push('no activated tools recorded; receipt may only prove discovery/withholding')
|
|
651
|
+
if (withheldCount === 0) warnings.push('no withheld/blocked tools recorded; receipt does not prove negative space')
|
|
652
|
+
|
|
653
|
+
return {
|
|
654
|
+
errors,
|
|
655
|
+
warnings,
|
|
656
|
+
summary: { discoveredCount, activatedCount, withheldCount },
|
|
657
|
+
}
|
|
658
|
+
}
|
package/src/utils/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.3.
|
|
1
|
+
export const VERSION = '0.3.40'
|