pluribus-context 0.3.21 → 0.3.26

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.
Files changed (93) hide show
  1. package/CHANGELOG.md +31 -3
  2. package/README.md +2 -2
  3. package/docs/community-review-packet.md +4 -2
  4. package/docs/context-budget-receipts.md +150 -0
  5. package/docs/context-input-evidence.md +397 -0
  6. package/docs/context-receipts-for-agent-observability.md +177 -0
  7. package/docs/orchestration-search-receipts.md +102 -0
  8. package/docs/portability-fidelity-report.md +10 -6
  9. package/examples/context-input-evidence/AGENTS.md +12 -0
  10. package/examples/context-input-evidence/agent-overlay-log.jsonl +4 -0
  11. package/examples/context-input-evidence/agent-overlay-otel-trace.json +548 -0
  12. package/examples/context-input-evidence/agent-overlay-receipt.ndjson +3 -0
  13. package/examples/context-input-evidence/agentgateway-progressive-disclosure-otel-trace.json +393 -0
  14. package/examples/context-input-evidence/agentgateway-progressive-disclosure-receipt.ndjson +4 -0
  15. package/examples/context-input-evidence/brain-remediation-otel-trace.json +645 -0
  16. package/examples/context-input-evidence/brain-remediation-receipt.ndjson +7 -0
  17. package/examples/context-input-evidence/claudekit-mcp-manager-otel-trace.json +417 -0
  18. package/examples/context-input-evidence/claudekit-mcp-manager-receipt.ndjson +5 -0
  19. package/examples/context-input-evidence/cli-progressive-disclosure-otel-trace.json +399 -0
  20. package/examples/context-input-evidence/cli-progressive-disclosure-receipt.ndjson +4 -0
  21. package/examples/context-input-evidence/compaction-otel-trace.json +711 -0
  22. package/examples/context-input-evidence/compaction-receipt.ndjson +6 -0
  23. package/examples/context-input-evidence/context-selection-otel-trace.json +627 -0
  24. package/examples/context-input-evidence/context-selection-receipt.ndjson +7 -0
  25. package/examples/context-input-evidence/convert-agent-overlay-log.mjs +156 -0
  26. package/examples/context-input-evidence/convert-agentgateway-progressive-disclosure-log.mjs +251 -0
  27. package/examples/context-input-evidence/convert-brain-remediation-log.mjs +241 -0
  28. package/examples/context-input-evidence/convert-claudekit-mcp-manager-log.mjs +253 -0
  29. package/examples/context-input-evidence/convert-cli-progressive-disclosure-log.mjs +251 -0
  30. package/examples/context-input-evidence/convert-compaction-log.mjs +224 -0
  31. package/examples/context-input-evidence/convert-context-selection-log.mjs +247 -0
  32. package/examples/context-input-evidence/convert-mcp-tool-search-log.mjs +242 -0
  33. package/examples/context-input-evidence/convert-memory-consolidation-log.mjs +240 -0
  34. package/examples/context-input-evidence/convert-memory-governance-delete-log.mjs +223 -0
  35. package/examples/context-input-evidence/convert-memory-log.mjs +226 -0
  36. package/examples/context-input-evidence/convert-memory-provenance-log.mjs +263 -0
  37. package/examples/context-input-evidence/convert-secret-scanning-log.mjs +233 -0
  38. package/examples/context-input-evidence/convert-session-log.mjs +186 -0
  39. package/examples/context-input-evidence/convert-skill-log.mjs +161 -0
  40. package/examples/context-input-evidence/convert-skill-registry-log.mjs +246 -0
  41. package/examples/context-input-evidence/convert-skill-routing-log.mjs +253 -0
  42. package/examples/context-input-evidence/convert-subagent-context-budget-log.mjs +267 -0
  43. package/examples/context-input-evidence/convert-subagent-delegation-log.mjs +264 -0
  44. package/examples/context-input-evidence/export-otel-trace.mjs +128 -0
  45. package/examples/context-input-evidence/generate-receipt.mjs +188 -0
  46. package/examples/context-input-evidence/mcp-tool-search-otel-trace.json +477 -0
  47. package/examples/context-input-evidence/mcp-tool-search-receipt.ndjson +5 -0
  48. package/examples/context-input-evidence/memory-consolidation-otel-trace.json +492 -0
  49. package/examples/context-input-evidence/memory-consolidation-receipt.ndjson +4 -0
  50. package/examples/context-input-evidence/memory-governance-delete-otel-trace.json +614 -0
  51. package/examples/context-input-evidence/memory-governance-delete-receipt.ndjson +5 -0
  52. package/examples/context-input-evidence/memory-otel-trace.json +645 -0
  53. package/examples/context-input-evidence/memory-provenance-otel-trace.json +711 -0
  54. package/examples/context-input-evidence/memory-provenance-receipt.ndjson +5 -0
  55. package/examples/context-input-evidence/memory-receipt.ndjson +4 -0
  56. package/examples/context-input-evidence/otel-trace.json +1119 -0
  57. package/examples/context-input-evidence/receipt.ndjson +6 -0
  58. package/examples/context-input-evidence/sample-agentgateway-progressive-disclosure-log.jsonl +5 -0
  59. package/examples/context-input-evidence/sample-brain-remediation-log.jsonl +9 -0
  60. package/examples/context-input-evidence/sample-claudekit-mcp-manager-log.jsonl +6 -0
  61. package/examples/context-input-evidence/sample-cli-progressive-disclosure-log.jsonl +5 -0
  62. package/examples/context-input-evidence/sample-compaction-log.jsonl +7 -0
  63. package/examples/context-input-evidence/sample-context-selection-log.jsonl +7 -0
  64. package/examples/context-input-evidence/sample-mcp-tool-search-log.jsonl +6 -0
  65. package/examples/context-input-evidence/sample-memory-consolidation-log.jsonl +5 -0
  66. package/examples/context-input-evidence/sample-memory-governance-delete-log.jsonl +6 -0
  67. package/examples/context-input-evidence/sample-memory-provenance-log.jsonl +6 -0
  68. package/examples/context-input-evidence/sample-memory-retrieval-log.jsonl +6 -0
  69. package/examples/context-input-evidence/sample-secret-scanning-log.jsonl +7 -0
  70. package/examples/context-input-evidence/sample-session-log.jsonl +6 -0
  71. package/examples/context-input-evidence/sample-skill-registry-log.jsonl +5 -0
  72. package/examples/context-input-evidence/sample-skill-routing-log.jsonl +7 -0
  73. package/examples/context-input-evidence/sample-subagent-context-budget-log.jsonl +6 -0
  74. package/examples/context-input-evidence/sample-subagent-delegation-log.jsonl +5 -0
  75. package/examples/context-input-evidence/secret-scanning-otel-trace.json +794 -0
  76. package/examples/context-input-evidence/secret-scanning-receipt.ndjson +6 -0
  77. package/examples/context-input-evidence/session-otel-trace.json +411 -0
  78. package/examples/context-input-evidence/session-receipt.ndjson +2 -0
  79. package/examples/context-input-evidence/skill-invocation-log.jsonl +4 -0
  80. package/examples/context-input-evidence/skill-otel-trace.json +548 -0
  81. package/examples/context-input-evidence/skill-receipt.ndjson +3 -0
  82. package/examples/context-input-evidence/skill-registry-otel-trace.json +471 -0
  83. package/examples/context-input-evidence/skill-registry-receipt.ndjson +5 -0
  84. package/examples/context-input-evidence/skill-routing-otel-trace.json +567 -0
  85. package/examples/context-input-evidence/skill-routing-receipt.ndjson +6 -0
  86. package/examples/context-input-evidence/subagent-context-budget-otel-trace.json +507 -0
  87. package/examples/context-input-evidence/subagent-context-budget-receipt.ndjson +5 -0
  88. package/examples/context-input-evidence/subagent-delegation-otel-trace.json +388 -0
  89. package/examples/context-input-evidence/subagent-delegation-receipt.ndjson +4 -0
  90. package/package.json +6 -2
  91. package/schemas/audit-result.schema.json +468 -43
  92. package/src/commands/audit.js +105 -5
  93. package/src/utils/version.js +1 -1
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from 'node:crypto';
3
+ import { readFileSync, writeFileSync } from 'node:fs';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const here = dirname(fileURLToPath(import.meta.url));
8
+ const inputPath = process.argv[2] ? resolve(process.argv[2]) : join(here, 'sample-memory-provenance-log.jsonl');
9
+ const receiptPath = process.argv[3] ? resolve(process.argv[3]) : join(here, 'memory-provenance-receipt.ndjson');
10
+ const tracePath = process.argv[4] ? resolve(process.argv[4]) : join(here, 'memory-provenance-otel-trace.json');
11
+
12
+ function sha256(value) {
13
+ return `sha256:${createHash('sha256').update(value ?? '').digest('hex')}`;
14
+ }
15
+
16
+ function hashRef(value) {
17
+ return sha256(value ?? '').slice(0, 19);
18
+ }
19
+
20
+ function readJsonl(path) {
21
+ return readFileSync(path, 'utf8')
22
+ .trim()
23
+ .split('\n')
24
+ .filter(Boolean)
25
+ .map((line, index) => {
26
+ try {
27
+ return JSON.parse(line);
28
+ } catch (error) {
29
+ throw new Error(`Invalid JSONL at ${path}:${index + 1}: ${error.message}`);
30
+ }
31
+ });
32
+ }
33
+
34
+ function unixNano(isoTimestamp) {
35
+ return `${BigInt(Date.parse(isoTimestamp)) * 1_000_000n}`;
36
+ }
37
+
38
+ function otelValue(value) {
39
+ if (typeof value === 'boolean') return { boolValue: value };
40
+ if (typeof value === 'number' && Number.isInteger(value)) return { intValue: String(value) };
41
+ if (typeof value === 'number') return { doubleValue: value };
42
+ if (typeof value === 'string') {
43
+ if (value === 'true' || value === 'false') return { boolValue: value === 'true' };
44
+ if (/^-?\d+$/.test(value)) return { intValue: value };
45
+ return { stringValue: value };
46
+ }
47
+ if (value == null) return { stringValue: '' };
48
+ return { stringValue: JSON.stringify(value) };
49
+ }
50
+
51
+ function attributesToOtel(attributes) {
52
+ return Object.entries(attributes).map(([key, value]) => ({ key, value: otelValue(value) }));
53
+ }
54
+
55
+ const records = readJsonl(inputPath);
56
+ const sessionStart = records.find((record) => record.type === 'session.start') ?? {};
57
+ const memoryWrites = records.filter((record) => record.type === 'memory.entry.promoted' || record.type === 'memory.entry.corrected');
58
+ const hydrations = records.filter((record) => record.type === 'memory.bundle.hydrated');
59
+ const provenanceChecks = records.filter((record) => record.type === 'memory.provenance.evaluated');
60
+
61
+ if (memoryWrites.length === 0) {
62
+ throw new Error(`No memory entry write/correction records found in ${inputPath}`);
63
+ }
64
+
65
+ const sessionId = sessionStart.session_id ?? 'unknown-session';
66
+ const conversationId = sessionStart.conversation_id ?? sessionId;
67
+ const traceId = sha256(`${sessionId}:team-memory-provenance-trace`).replace('sha256:', '').slice(0, 32);
68
+ const spanId = sha256(`${sessionId}:team-memory-provenance-span`).replace('sha256:', '').slice(0, 16);
69
+ const writesByEntry = new Map();
70
+
71
+ function baseAttributes(record) {
72
+ return {
73
+ 'team_memory.team.hash': hashRef(record.team ?? sessionStart.team ?? ''),
74
+ 'team_memory.scope.hash': hashRef(record.scope ?? ''),
75
+ 'team_memory.visibility': record.visibility ?? 'unknown',
76
+ 'session.id': sessionId,
77
+ 'gen_ai.conversation.id': conversationId
78
+ };
79
+ }
80
+
81
+ const writeEvents = memoryWrites.map((record) => {
82
+ if (!Number.isInteger(record.sequence)) {
83
+ throw new Error(`Memory write ${record.entry_id ?? '<unknown>'} is missing integer sequence`);
84
+ }
85
+ const entryVersions = writesByEntry.get(record.entry_id) ?? [];
86
+ entryVersions.push(record);
87
+ writesByEntry.set(record.entry_id, entryVersions);
88
+
89
+ return {
90
+ trace_id: traceId,
91
+ span_id: spanId,
92
+ name: record.type === 'memory.entry.corrected' ? 'team_memory.entry.corrected' : 'team_memory.entry.promoted',
93
+ time: record.time,
94
+ attributes: {
95
+ ...baseAttributes(record),
96
+ 'team_memory.entry.id_hash': hashRef(record.entry_id ?? ''),
97
+ 'team_memory.entry.sequence': record.sequence,
98
+ 'team_memory.entry.operation': record.type.replace('memory.entry.', ''),
99
+ 'team_memory.entry.body_hash': sha256(record.raw_body ?? ''),
100
+ 'team_memory.entry.body_recorded': 'false',
101
+ 'team_memory.entry.previous.id_hash': record.supersedes_entry_id ? hashRef(record.supersedes_entry_id) : '',
102
+ 'team_memory.author.agent.id_hash': hashRef(record.author_agent_id ?? ''),
103
+ 'team_memory.author.human.id_hash': hashRef(record.author_human_id ?? ''),
104
+ 'team_memory.author.role': record.author_role ?? 'unknown',
105
+ 'team_memory.source.session.id_hash': hashRef(record.source_session_id ?? ''),
106
+ 'team_memory.source.compaction_epoch': record.source_compaction_epoch ?? 0,
107
+ 'team_memory.promotion.reason_hash': sha256(record.promotion_reason ?? ''),
108
+ 'team_memory.decision.scope': record.decision_scope ?? 'unknown',
109
+ 'team_memory.privacy.raw_body_recorded': 'false',
110
+ 'team_memory.privacy.raw_rationale_recorded': 'false'
111
+ }
112
+ };
113
+ });
114
+
115
+ const hydrationEvents = hydrations.map((record) => {
116
+ const selectedIds = record.selected_entry_ids ?? [];
117
+ const candidateIds = record.candidate_entry_ids ?? [];
118
+ const suppressedIds = record.suppressed_entry_ids ?? [];
119
+ const unknownAuthorIds = selectedIds.filter((entryId) => !writesByEntry.has(entryId));
120
+ if (unknownAuthorIds.length > 0) {
121
+ throw new Error(`Hydration selected entries without known provenance: ${unknownAuthorIds.join(', ')}`);
122
+ }
123
+ if ((record.loaded_sequence_min ?? 0) > (record.loaded_sequence_max ?? 0)) {
124
+ throw new Error(`Hydration sequence range is not monotonic for ${record.team ?? '<unknown team>'}`);
125
+ }
126
+
127
+ return {
128
+ trace_id: traceId,
129
+ span_id: spanId,
130
+ name: 'team_memory.bundle.hydrated',
131
+ time: record.time,
132
+ attributes: {
133
+ ...baseAttributes(record),
134
+ 'team_memory.consumer.agent.id_hash': hashRef(record.consumer_agent_id ?? ''),
135
+ 'team_memory.ticket.id_hash': hashRef(record.ticket_id ?? ''),
136
+ 'team_memory.selection.policy': record.selection_policy ?? 'unknown',
137
+ 'team_memory.query.hash': sha256(record.query_text ?? ''),
138
+ 'team_memory.candidate.count': candidateIds.length,
139
+ 'team_memory.selected.count': selectedIds.length,
140
+ 'team_memory.suppressed.count': suppressedIds.length,
141
+ 'team_memory.selected.entry_ids_hash': sha256(selectedIds.map(hashRef).join('\n')),
142
+ 'team_memory.suppressed.entry_ids_hash': sha256(suppressedIds.map(hashRef).join('\n')),
143
+ 'team_memory.loaded.sequence_min': record.loaded_sequence_min ?? 0,
144
+ 'team_memory.loaded.sequence_max': record.loaded_sequence_max ?? 0,
145
+ 'team_memory.loaded.ordering': 'monotonic_team_memory_sequence',
146
+ 'team_memory.bundle.body_hash': sha256(record.raw_bundle ?? ''),
147
+ 'team_memory.bundle.body_recorded': 'false',
148
+ 'team_memory.privacy.raw_query_recorded': 'false',
149
+ 'team_memory.privacy.raw_bundle_recorded': 'false'
150
+ }
151
+ };
152
+ });
153
+
154
+ const provenanceEvents = provenanceChecks.map((record) => {
155
+ const selectedCount = record.selected_count ?? 0;
156
+ const knownAuthorCount = record.known_author_count ?? 0;
157
+ const unknownAuthorCount = record.unknown_author_count ?? 0;
158
+ const accountedRelevance = (record.decisive_count ?? 0)
159
+ + (record.supporting_count ?? 0)
160
+ + (record.unused_count ?? 0)
161
+ + (record.unknown_relevance_count ?? 0);
162
+
163
+ if (knownAuthorCount + unknownAuthorCount !== selectedCount) {
164
+ throw new Error(`Author accounting invariant failed: selected_count != known_author_count + unknown_author_count`);
165
+ }
166
+ if (accountedRelevance !== selectedCount) {
167
+ throw new Error(`Relevance accounting invariant failed: selected_count != decisive + supporting + unused + unknown`);
168
+ }
169
+
170
+ return {
171
+ trace_id: traceId,
172
+ span_id: spanId,
173
+ name: 'team_memory.provenance.evaluated',
174
+ time: record.time,
175
+ attributes: {
176
+ ...baseAttributes(record),
177
+ 'team_memory.consumer.agent.id_hash': hashRef(record.consumer_agent_id ?? ''),
178
+ 'team_memory.selected.count': selectedCount,
179
+ 'team_memory.selected.known_author_count': knownAuthorCount,
180
+ 'team_memory.selected.unknown_author_count': unknownAuthorCount,
181
+ 'team_memory.loaded.ordered_sequence_count': record.ordered_sequence_count ?? 0,
182
+ 'team_memory.relevance.decisive_count': record.decisive_count ?? 0,
183
+ 'team_memory.relevance.supporting_count': record.supporting_count ?? 0,
184
+ 'team_memory.relevance.unused_count': record.unused_count ?? 0,
185
+ 'team_memory.relevance.unknown_count': record.unknown_relevance_count ?? 0,
186
+ 'team_memory.accounting.invariant': 'selected_count == known_author_count + unknown_author_count && selected_count == decisive_count + supporting_count + unused_count + unknown_relevance_count',
187
+ 'team_memory.audit_gap': record.audit_gap ?? 'proves provenance and loading boundaries, not factual correctness'
188
+ }
189
+ };
190
+ });
191
+
192
+ const events = [...writeEvents, ...hydrationEvents, ...provenanceEvents]
193
+ .sort((left, right) => Date.parse(left.time) - Date.parse(right.time));
194
+ writeFileSync(receiptPath, `${events.map((event) => JSON.stringify(event)).join('\n')}\n`);
195
+
196
+ const eventTimes = records.map((record) => Date.parse(record.time)).filter(Number.isFinite);
197
+ const startTimeMs = Number.isFinite(Date.parse(sessionStart.time)) ? Date.parse(sessionStart.time) : Math.min(...eventTimes);
198
+ const endTimeMs = Math.max(...eventTimes) + 1;
199
+
200
+ const otlpTrace = {
201
+ resourceSpans: [
202
+ {
203
+ resource: {
204
+ attributes: attributesToOtel({
205
+ 'service.name': 'pluribus-team-memory-provenance-demo',
206
+ 'service.version': '0.0.0-fixture',
207
+ 'deployment.environment.name': 'local-fixture'
208
+ })
209
+ },
210
+ scopeSpans: [
211
+ {
212
+ scope: {
213
+ name: 'pluribus.context_input_evidence.team_memory_provenance_demo',
214
+ version: '0.0.0-fixture'
215
+ },
216
+ spans: [
217
+ {
218
+ traceId,
219
+ spanId,
220
+ parentSpanId: '',
221
+ name: 'agent.session',
222
+ kind: 1,
223
+ startTimeUnixNano: `${BigInt(startTimeMs) * 1_000_000n}`,
224
+ endTimeUnixNano: `${BigInt(endTimeMs) * 1_000_000n}`,
225
+ attributes: attributesToOtel({
226
+ 'session.id': sessionId,
227
+ 'gen_ai.conversation.id': conversationId,
228
+ 'gen_ai.agent.name': sessionStart.agent ?? 'unknown',
229
+ 'gen_ai.operation.name': 'agent_session',
230
+ 'code.repository.name': sessionStart.repo ?? '',
231
+ 'pluribus.team_memory.write.count': writeEvents.length,
232
+ 'pluribus.team_memory.hydration.count': hydrationEvents.length,
233
+ 'pluribus.team_memory.provenance_evaluation.count': provenanceEvents.length
234
+ }),
235
+ events: events.map((event) => ({
236
+ name: event.name,
237
+ timeUnixNano: unixNano(event.time),
238
+ attributes: attributesToOtel(event.attributes)
239
+ }))
240
+ }
241
+ ]
242
+ }
243
+ ]
244
+ }
245
+ ]
246
+ };
247
+
248
+ writeFileSync(tracePath, `${JSON.stringify(otlpTrace, null, 2)}\n`);
249
+
250
+ console.log(JSON.stringify({
251
+ schema: 'pluribus.contextInputEvidence.teamMemoryProvenanceDemo.v0',
252
+ inputPath,
253
+ receiptPath,
254
+ tracePath,
255
+ sessionId,
256
+ conversationId,
257
+ writeEventCount: writeEvents.length,
258
+ hydrationEventCount: hydrationEvents.length,
259
+ provenanceEvaluationEventCount: provenanceEvents.length,
260
+ invariant: 'each hydrated memory has known author provenance, monotonic order, and selected_count accounting for author + relevance buckets',
261
+ privacyDefault: 'outputs hashes, counts, buckets, roles, scope, and sequence numbers; does not copy raw memory bodies, prompts, tickets, private paths, secrets, or customer data',
262
+ lesson: 'Team memory needs provenance receipts: who/which agent wrote or corrected each memory, in what order, and which entries actually hydrated into the receiving agent context.'
263
+ }, null, 2));
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from 'node:crypto';
3
+ import { readFileSync, writeFileSync } from 'node:fs';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const here = dirname(fileURLToPath(import.meta.url));
8
+ const inputPath = process.argv[2] ? resolve(process.argv[2]) : join(here, 'sample-secret-scanning-log.jsonl');
9
+ const receiptPath = process.argv[3] ? resolve(process.argv[3]) : join(here, 'secret-scanning-receipt.ndjson');
10
+ const tracePath = process.argv[4] ? resolve(process.argv[4]) : join(here, 'secret-scanning-otel-trace.json');
11
+
12
+ function sha256(value) {
13
+ return `sha256:${createHash('sha256').update(value ?? '').digest('hex')}`;
14
+ }
15
+
16
+ function hashRef(value) {
17
+ return sha256(value ?? '').slice(0, 19);
18
+ }
19
+
20
+ function hashList(values = []) {
21
+ return sha256(values.join('\n'));
22
+ }
23
+
24
+ function readJsonl(path) {
25
+ return readFileSync(path, 'utf8')
26
+ .trim()
27
+ .split('\n')
28
+ .filter(Boolean)
29
+ .map((line, index) => {
30
+ try {
31
+ return JSON.parse(line);
32
+ } catch (error) {
33
+ throw new Error(`Invalid JSONL at ${path}:${index + 1}: ${error.message}`);
34
+ }
35
+ });
36
+ }
37
+
38
+ function unixNano(isoTimestamp) {
39
+ return `${BigInt(Date.parse(isoTimestamp)) * 1_000_000n}`;
40
+ }
41
+
42
+ function otelValue(value) {
43
+ if (typeof value === 'boolean') return { boolValue: value };
44
+ if (typeof value === 'number' && Number.isInteger(value)) return { intValue: String(value) };
45
+ if (typeof value === 'number') return { doubleValue: value };
46
+ if (value == null) return { stringValue: '' };
47
+ return { stringValue: String(value) };
48
+ }
49
+
50
+ function attributesToOtel(attributes) {
51
+ return Object.entries(attributes).map(([key, value]) => ({ key, value: otelValue(value) }));
52
+ }
53
+
54
+ function countBucket(value) {
55
+ if (value === 0) return 'zero';
56
+ if (value <= 2) return 'under_2';
57
+ if (value <= 5) return 'under_5';
58
+ return 'over_5';
59
+ }
60
+
61
+ function lineBucket(line) {
62
+ if (line <= 20) return '1_20';
63
+ if (line <= 100) return '21_100';
64
+ return 'over_100';
65
+ }
66
+
67
+ function durationBucket(ms) {
68
+ if (ms < 1_000) return 'under_1s';
69
+ if (ms < 10_000) return 'under_10s';
70
+ return 'over_10s';
71
+ }
72
+
73
+ const records = readJsonl(inputPath);
74
+ const session = records.find((record) => record.type === 'session.start');
75
+ const requested = records.find((record) => record.type === 'security.secret_scanning.requested');
76
+ const completed = records.find((record) => record.type === 'security.secret_scanning.completed');
77
+ const findings = records.filter((record) => record.type === 'security.secret_scanning.finding.presented');
78
+ const bypass = records.find((record) => record.type === 'security.secret_scanning.bypass.evaluated');
79
+ const verified = records.find((record) => record.type === 'security.secret_scanning.remediation.verified');
80
+
81
+ if (!session || !requested || !completed || findings.length === 0 || !bypass || !verified) {
82
+ throw new Error(`Expected session, secret scanning request/completion/findings/bypass/remediation records in ${inputPath}`);
83
+ }
84
+
85
+ const traceSeed = `${session.session_id}:${session.conversation_id}:secret-scanning`;
86
+ const traceId = sha256(traceSeed).replace('sha256:', '').slice(0, 32);
87
+ const spanId = sha256(`${traceSeed}:span`).replace('sha256:', '').slice(0, 16);
88
+
89
+ const baseAttrs = {
90
+ 'session.id': session.session_id,
91
+ 'gen_ai.conversation.id': session.conversation_id,
92
+ 'agent.name': session.agent,
93
+ 'mcp.provider': session.provider,
94
+ 'mcp.client': session.client,
95
+ 'repository.hash': hashRef(session.repository),
96
+ 'repository.path_hash': sha256(session.repo),
97
+ };
98
+
99
+ const events = [
100
+ {
101
+ name: 'security.secret_scanning.requested',
102
+ time: requested.time,
103
+ attributes: {
104
+ ...baseAttrs,
105
+ 'security.scan.request.hash': hashRef(requested.request_id),
106
+ 'security.scan.trigger': requested.trigger,
107
+ 'security.scan.toolset': requested.toolset,
108
+ 'security.scan.tool': requested.tool,
109
+ 'security.scan.scope': requested.scan_scope,
110
+ 'security.scan.diff_path_count': requested.diff_paths.length,
111
+ 'security.scan.diff_paths_hash': hashList(requested.diff_paths),
112
+ 'security.scan.prompt_hash': sha256(requested.prompt),
113
+ 'security.secret_scanning.push_protection_customization': requested.push_protection_customization,
114
+ 'security.secret_scanning.persisted_as_github_alert': requested.persisted_as_github_alert,
115
+ 'privacy.raw_prompt_recorded': false,
116
+ 'privacy.raw_diff_paths_recorded': false,
117
+ 'privacy.raw_secret_values_recorded': false,
118
+ },
119
+ },
120
+ {
121
+ name: 'security.secret_scanning.completed',
122
+ time: completed.time,
123
+ attributes: {
124
+ ...baseAttrs,
125
+ 'security.scan.request.hash': hashRef(completed.request_id),
126
+ 'security.scan.status': completed.status,
127
+ 'security.scan.files_scanned': completed.files_scanned,
128
+ 'security.scan.line_count_bucket': countBucket(Math.ceil(completed.line_count / 100)),
129
+ 'security.secret_scanning.finding_count': completed.finding_count,
130
+ 'security.secret_scanning.finding_count_bucket': countBucket(completed.finding_count),
131
+ 'security.secret_scanning.detector_count': completed.detector_count,
132
+ 'security.secret_scanning.engine_snapshot_hash': hashRef(completed.engine_snapshot),
133
+ 'security.scan.latency_bucket': durationBucket(completed.latency_ms),
134
+ 'security.scan.tool_response_hash': sha256(completed.tool_response_excerpt),
135
+ 'privacy.raw_tool_response_recorded': false,
136
+ 'privacy.raw_secret_values_recorded': false,
137
+ },
138
+ },
139
+ ...findings.map((finding) => ({
140
+ name: 'security.secret_scanning.finding.presented',
141
+ time: finding.time,
142
+ attributes: {
143
+ ...baseAttrs,
144
+ 'security.scan.request.hash': hashRef(finding.request_id),
145
+ 'security.finding.hash': hashRef(finding.finding_id),
146
+ 'security.secret_scanning.secret_type': finding.secret_type,
147
+ 'security.finding.severity': finding.severity,
148
+ 'security.finding.location_path_hash': sha256(finding.location_path),
149
+ 'security.finding.line_bucket': lineBucket(finding.line),
150
+ 'security.finding.secret_value_hash': sha256(finding.secret_value),
151
+ 'security.finding.remediation_hash': sha256(finding.remediation),
152
+ 'security.secret_scanning.push_protection_action': finding.push_protection_action,
153
+ 'security.secret_scanning.bypass_allowed': finding.bypass_allowed,
154
+ 'privacy.raw_location_path_recorded': false,
155
+ 'privacy.raw_secret_value_recorded': false,
156
+ 'privacy.raw_remediation_text_recorded': false,
157
+ },
158
+ })),
159
+ {
160
+ name: 'security.secret_scanning.bypass.evaluated',
161
+ time: bypass.time,
162
+ attributes: {
163
+ ...baseAttrs,
164
+ 'security.scan.request.hash': hashRef(bypass.request_id),
165
+ 'security.policy.hash': hashRef(bypass.policy),
166
+ 'security.secret_scanning.bypass_requested': bypass.bypass_requested,
167
+ 'security.secret_scanning.bypass_allowed': bypass.bypass_allowed,
168
+ 'security.secret_scanning.bypass_reason': bypass.bypass_reason,
169
+ 'security.secret_scanning.decision': bypass.decision,
170
+ 'security.operator_note_hash': sha256(bypass.operator_note),
171
+ 'privacy.raw_operator_note_recorded': false,
172
+ },
173
+ },
174
+ {
175
+ name: 'security.secret_scanning.remediation.verified',
176
+ time: verified.time,
177
+ attributes: {
178
+ ...baseAttrs,
179
+ 'security.scan.request.hash': hashRef(verified.request_id),
180
+ 'security.scan.rescan.hash': hashRef(verified.rescan_id),
181
+ 'security.scan.status': verified.status,
182
+ 'security.scan.changed_path_count': verified.changed_paths.length,
183
+ 'security.scan.changed_paths_hash': hashList(verified.changed_paths),
184
+ 'security.secret_scanning.finding_count_after': verified.finding_count_after,
185
+ 'security.remediation.rotation_ticket_hash': sha256(verified.rotation_ticket),
186
+ 'security.scan.latency_bucket': durationBucket(verified.latency_ms),
187
+ 'privacy.raw_changed_paths_recorded': false,
188
+ 'privacy.raw_rotation_ticket_recorded': false,
189
+ 'audit.gap': 'receipt_proves_scan_findings_policy_and_clean_rescan_not_secret_revocation_completion',
190
+ },
191
+ },
192
+ ].map((event) => ({ trace_id: traceId, span_id: spanId, ...event }));
193
+
194
+ const receipt = events.map((event) => JSON.stringify(event)).join('\n') + '\n';
195
+ writeFileSync(receiptPath, receipt);
196
+
197
+ const trace = {
198
+ resourceSpans: [
199
+ {
200
+ resource: {
201
+ attributes: attributesToOtel({
202
+ 'service.name': 'pluribus-context-input-evidence-demo',
203
+ 'service.version': '0.3.23',
204
+ }),
205
+ },
206
+ scopeSpans: [
207
+ {
208
+ scope: { name: 'pluribus.context_input_evidence.secret_scanning', version: '0.1.0' },
209
+ spans: [
210
+ {
211
+ traceId,
212
+ spanId,
213
+ name: 'agent.session',
214
+ kind: 1,
215
+ startTimeUnixNano: unixNano(session.time),
216
+ endTimeUnixNano: unixNano(verified.time),
217
+ attributes: attributesToOtel(baseAttrs),
218
+ events: events.map((event) => ({
219
+ name: event.name,
220
+ timeUnixNano: unixNano(event.time),
221
+ attributes: attributesToOtel(event.attributes),
222
+ })),
223
+ },
224
+ ],
225
+ },
226
+ ],
227
+ },
228
+ ],
229
+ };
230
+
231
+ writeFileSync(tracePath, `${JSON.stringify(trace, null, 2)}\n`);
232
+ console.log(`wrote ${events.length} secret scanning receipt events to ${receiptPath}`);
233
+ console.log(`wrote OpenTelemetry-style trace to ${tracePath}`);
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from 'node:crypto';
3
+ import { readFileSync, writeFileSync } from 'node:fs';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const here = dirname(fileURLToPath(import.meta.url));
8
+ const inputPath = process.argv[2] ? resolve(process.argv[2]) : join(here, 'sample-session-log.jsonl');
9
+ const receiptPath = process.argv[3] ? resolve(process.argv[3]) : join(here, 'session-receipt.ndjson');
10
+ const tracePath = process.argv[4] ? resolve(process.argv[4]) : join(here, 'session-otel-trace.json');
11
+
12
+ function sha256(value) {
13
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
14
+ }
15
+
16
+ function canonicalize(text) {
17
+ return text.normalize('NFC').replace(/\r\n/g, '\n');
18
+ }
19
+
20
+ function readJsonl(path) {
21
+ return readFileSync(path, 'utf8')
22
+ .trim()
23
+ .split('\n')
24
+ .filter(Boolean)
25
+ .map((line, index) => {
26
+ try {
27
+ return JSON.parse(line);
28
+ } catch (error) {
29
+ throw new Error(`Invalid JSONL at ${path}:${index + 1}: ${error.message}`);
30
+ }
31
+ });
32
+ }
33
+
34
+ function unixNano(isoTimestamp) {
35
+ return `${BigInt(Date.parse(isoTimestamp)) * 1_000_000n}`;
36
+ }
37
+
38
+ function otelValue(value) {
39
+ if (typeof value === 'boolean') return { boolValue: value };
40
+ if (typeof value === 'number' && Number.isInteger(value)) return { intValue: String(value) };
41
+ if (typeof value === 'number') return { doubleValue: value };
42
+ if (typeof value === 'string') {
43
+ if (value === 'true' || value === 'false') return { boolValue: value === 'true' };
44
+ if (/^-?\d+$/.test(value)) return { intValue: value };
45
+ return { stringValue: value };
46
+ }
47
+ if (value == null) return { stringValue: '' };
48
+ return { stringValue: JSON.stringify(value) };
49
+ }
50
+
51
+ function attributesToOtel(attributes) {
52
+ return Object.entries(attributes).map(([key, value]) => ({ key, value: otelValue(value) }));
53
+ }
54
+
55
+ const records = readJsonl(inputPath);
56
+ const sessionStart = records.find((record) => record.type === 'session.start') ?? {};
57
+ const sessionEnd = [...records].reverse().find((record) => record.type === 'session.end') ?? {};
58
+ const contextRecords = records.filter((record) => record.type === 'context.input');
59
+
60
+ if (contextRecords.length === 0) {
61
+ throw new Error(`No context.input records found in ${inputPath}`);
62
+ }
63
+
64
+ const sessionId = sessionStart.session_id ?? 'unknown-session';
65
+ const conversationId = sessionStart.conversation_id ?? sessionId;
66
+ const traceId = sha256(`${sessionId}:trace`).replace('sha256:', '').slice(0, 32);
67
+ const spanId = sha256(`${sessionId}:span`).replace('sha256:', '').slice(0, 16);
68
+
69
+ const events = contextRecords.map((record) => {
70
+ const sourceText = record.source_text ?? '';
71
+ const deliveredText = record.delivered_text ?? sourceText;
72
+ const sourceCanonicalText = canonicalize(sourceText);
73
+ const deliveredHash = sha256(deliveredText);
74
+ const sourceIdentity = record.source_path ?? record.source_uri ?? 'unknown';
75
+ const sourceAttributeKey = record.source_uri ? 'context.input.source.uri' : 'context.input.source.path';
76
+
77
+ return {
78
+ trace_id: traceId,
79
+ span_id: spanId,
80
+ name: 'context.input.loaded',
81
+ time: record.time,
82
+ attributes: {
83
+ 'context.input.kind': record.kind ?? 'unknown',
84
+ [sourceAttributeKey]: sourceIdentity,
85
+ 'context.input.source.bytes_hash': sha256(sourceText),
86
+ 'context.input.source.canonical.form': 'otel.context.source.nfc_lf.v1_candidate',
87
+ 'context.input.source.canonical.hash': sha256(sourceCanonicalText),
88
+ 'context.input.source.canonicalization': 'utf8,unicode_nfc,crlf_to_lf',
89
+ 'context.input.delivered.hash': deliveredHash,
90
+ 'context.input.delivered.full_render.hash': deliveredHash,
91
+ 'context.input.delivered.full_render.status': 'available',
92
+ 'context.input.delivered.template_hash': '',
93
+ 'context.input.delivered.transform': record.delivery_transform ?? 'as-recorded-by-session-log',
94
+ 'context.input.delivered.nondeterministic': 'false',
95
+ 'context.input.delivered.truncated': 'false',
96
+ 'context.input.loaded_by': record.loaded_by ?? 'unknown',
97
+ 'context.input.activation': record.activation ?? 'unknown',
98
+ 'context.input.scope': record.scope ?? 'unknown',
99
+ 'context.input.applies_to': record.applies_to ?? sessionStart.agent ?? 'unknown',
100
+ 'context.input.why_loaded': record.why_loaded ?? 'unknown',
101
+ 'context.input.expected_benefit': record.expected_benefit ?? 'unknown',
102
+ 'context.input.duplicate.dedupe_key': `${conversationId}:${deliveredHash}`,
103
+ 'context.input.duplicate.dedupe_scope': 'conversation',
104
+ 'context.input.duplicate.suppression_policy': 'suppress_equal_dedupe_key_within_scope',
105
+ 'context.input.duplicate.role': 'selected',
106
+ 'context.input.duplicate.risk': 'unknown',
107
+ 'session.id': sessionId,
108
+ 'gen_ai.conversation.id': conversationId
109
+ }
110
+ };
111
+ });
112
+
113
+ writeFileSync(receiptPath, `${events.map((event) => JSON.stringify(event)).join('\n')}\n`);
114
+
115
+ const eventTimes = records.map((record) => Date.parse(record.time)).filter(Number.isFinite);
116
+ const startTimeMs = Number.isFinite(Date.parse(sessionStart.time)) ? Date.parse(sessionStart.time) : Math.min(...eventTimes);
117
+ const endTimeMs = Number.isFinite(Date.parse(sessionEnd.time)) ? Date.parse(sessionEnd.time) : Math.max(...eventTimes) + 1;
118
+ const toolCallCount = records.filter((record) => record.type === 'tool.call').length;
119
+
120
+ const otlpTrace = {
121
+ resourceSpans: [
122
+ {
123
+ resource: {
124
+ attributes: attributesToOtel({
125
+ 'service.name': 'pluribus-session-log-context-demo',
126
+ 'service.version': '0.0.0-fixture',
127
+ 'deployment.environment.name': 'local-fixture'
128
+ })
129
+ },
130
+ scopeSpans: [
131
+ {
132
+ scope: {
133
+ name: 'pluribus.context_input_evidence.session_log_demo',
134
+ version: '0.0.0-fixture'
135
+ },
136
+ spans: [
137
+ {
138
+ traceId,
139
+ spanId,
140
+ parentSpanId: '',
141
+ name: 'agent.session',
142
+ kind: 1,
143
+ startTimeUnixNano: `${BigInt(startTimeMs) * 1_000_000n}`,
144
+ endTimeUnixNano: `${BigInt(endTimeMs) * 1_000_000n}`,
145
+ attributes: attributesToOtel({
146
+ 'session.id': sessionId,
147
+ 'gen_ai.conversation.id': conversationId,
148
+ 'gen_ai.agent.name': sessionStart.agent ?? 'unknown',
149
+ 'gen_ai.operation.name': 'agent_session',
150
+ 'code.repository.name': sessionStart.repo ?? '',
151
+ 'pluribus.session_log.tool_call.count': toolCallCount
152
+ }),
153
+ events: events.map((event) => ({
154
+ name: event.name,
155
+ timeUnixNano: unixNano(event.time),
156
+ attributes: attributesToOtel(event.attributes)
157
+ }))
158
+ }
159
+ ]
160
+ }
161
+ ]
162
+ }
163
+ ]
164
+ };
165
+
166
+ writeFileSync(tracePath, `${JSON.stringify(otlpTrace, null, 2)}\n`);
167
+
168
+ const kindCounts = events.reduce((counts, event) => {
169
+ const kind = event.attributes['context.input.kind'];
170
+ counts[kind] = (counts[kind] ?? 0) + 1;
171
+ return counts;
172
+ }, {});
173
+
174
+ console.log(JSON.stringify({
175
+ schema: 'pluribus.contextInputEvidence.sessionLogDemo.v0',
176
+ inputPath,
177
+ receiptPath,
178
+ tracePath,
179
+ sessionId,
180
+ conversationId,
181
+ contextInputEventCount: events.length,
182
+ toolCallCount,
183
+ kindCounts,
184
+ privacyDefault: 'outputs hashes, paths/uris, counts, categorical fields, and session identifiers; does not copy raw context text into receipts or trace events',
185
+ lesson: 'A post-hoc session log can be converted into context.input.loaded SpanEvents without asking retrieval/search tools to inspect the whole transcript.'
186
+ }, null, 2));