@chatpanel/events 0.9.0 → 0.11.0
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/index.js +4 -0
- package/observability.js +119 -0
- package/package.json +4 -2
package/index.js
CHANGED
|
@@ -28,6 +28,10 @@ export { createRegistry, REGISTRY_STATES } from './registry.js';
|
|
|
28
28
|
export { defineSearchEngine, reconcileEngines, attemptOrder, ENGINE_KINDS, SearchEngineError } from './search-engines.js';
|
|
29
29
|
export { defineToolGroup, createToolGroupRegistry, ToolGroupError } from './tool-groups.js';
|
|
30
30
|
export { toolNeedFor } from './tool-need.js';
|
|
31
|
+
export {
|
|
32
|
+
ACCESS_LOG_VERSION, ACCESS_LOG_MAX, redactAccessArgs, makeAccessEvent,
|
|
33
|
+
createAccessLog, makeStorageTier, formatBytes,
|
|
34
|
+
} from './observability.js';
|
|
31
35
|
export { routeGraph, projectChain } from './route-graph.js';
|
|
32
36
|
export { defineAdapter, createAdapterRegistry, AdapterError } from './adapters.js';
|
|
33
37
|
export { linkifyCitations, sourcesFromToolText } from './citations.js';
|
package/observability.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// observability.js — the contract for "who consumed what, when, and how much is stored".
|
|
2
|
+
//
|
|
3
|
+
// ChatPanel's data is reachable by more than one agent now: the side panel, and any CLI
|
|
4
|
+
// (Codex, Claude Code, OpenCode…) wired to the gateway's MCP server. Once several agents
|
|
5
|
+
// read your history and skills, you need to SEE that — which agent touched what, and how
|
|
6
|
+
// much sits in each storage tier. That is one question with one answer shape, so it lives
|
|
7
|
+
// here, not re-derived in every client. The extension renders it; the gateway records it;
|
|
8
|
+
// a desktop/mobile app will do both against this same contract.
|
|
9
|
+
//
|
|
10
|
+
// Pure and dependency-free (the @chatpanel/events rule): identical code in browser ESM,
|
|
11
|
+
// the gateway (Node) and a mobile JS runtime. No clock, no storage, no platform APIs —
|
|
12
|
+
// the caller passes `ts`; the caller owns persistence.
|
|
13
|
+
//
|
|
14
|
+
// PRIVACY IS THE POINT of the redactor below. An access log that stored raw tool arguments
|
|
15
|
+
// would quietly become a second copy of every search query — the exact PII we redact
|
|
16
|
+
// everywhere else. So the note attached to each event is built from a per-tool WHITELIST of
|
|
17
|
+
// non-sensitive fields; a search query's TEXT is never recorded, only that a search ran.
|
|
18
|
+
|
|
19
|
+
export const ACCESS_LOG_VERSION = 1;
|
|
20
|
+
|
|
21
|
+
// Default ring size — enough to see a working session's activity without unbounded growth.
|
|
22
|
+
export const ACCESS_LOG_MAX = 500;
|
|
23
|
+
|
|
24
|
+
// Per-tool whitelist: which argument fields are safe to keep in the human note. Anything not
|
|
25
|
+
// listed here is dropped. Content-bearing fields (a search `query`) are deliberately ABSENT —
|
|
26
|
+
// the tool name already says "a search happened"; the words searched are not logged.
|
|
27
|
+
const SAFE_ARGS = {
|
|
28
|
+
// Metadata filters are safe to keep (they are not content) and useful to see in the log:
|
|
29
|
+
// "type=meeting since=7d". The search QUERY is deliberately absent — never recorded.
|
|
30
|
+
search_history: ['type', 'since', 'before', 'limit', 'offset'],
|
|
31
|
+
list_history: ['limit', 'offset'],
|
|
32
|
+
get_record: ['id', 'maxChars', 'offset'], // opaque record id + paging, not content
|
|
33
|
+
find_related: ['id', 'limit'], // graph navigation from an opaque id
|
|
34
|
+
open_skill: ['skill'], // skill names are catalog identifiers, not PII
|
|
35
|
+
read_skill_file: ['skill', 'path'],
|
|
36
|
+
list_skills: ['limit'],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A short, SAFE descriptor of a call's arguments for display. Never returns content that
|
|
41
|
+
* could carry PII. Unknown tools get an empty note (the tool name is the only signal).
|
|
42
|
+
*/
|
|
43
|
+
export function redactAccessArgs(tool, args) {
|
|
44
|
+
const allow = SAFE_ARGS[tool];
|
|
45
|
+
if (!allow || !args || typeof args !== 'object') return '';
|
|
46
|
+
const parts = [];
|
|
47
|
+
for (const key of allow) {
|
|
48
|
+
const v = args[key];
|
|
49
|
+
if (v === undefined || v === null || v === '') continue;
|
|
50
|
+
// Cap any string field so a long id/path can't smuggle content or blow up the row.
|
|
51
|
+
const s = typeof v === 'string' ? (v.length > 80 ? `${v.slice(0, 77)}…` : v) : String(v);
|
|
52
|
+
parts.push(`${key}=${s}`);
|
|
53
|
+
}
|
|
54
|
+
return parts.join(' ');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Normalize one access into the record everything stores and renders. `client` is the calling
|
|
59
|
+
* agent's self-reported name (MCP clientInfo) — untrusted, so it's coerced to a short string.
|
|
60
|
+
*/
|
|
61
|
+
export function makeAccessEvent({ ts, client, tool, ok = true, ms, args, error } = {}) {
|
|
62
|
+
return {
|
|
63
|
+
v: ACCESS_LOG_VERSION,
|
|
64
|
+
ts: Number(ts) || 0,
|
|
65
|
+
client: shortStr(client, 'unknown', 60),
|
|
66
|
+
tool: shortStr(tool, 'unknown', 60),
|
|
67
|
+
ok: !!ok,
|
|
68
|
+
ms: Number.isFinite(ms) ? Math.max(0, Math.round(ms)) : null,
|
|
69
|
+
note: redactAccessArgs(tool, args),
|
|
70
|
+
error: error ? shortStr(error, '', 200) : '',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function shortStr(v, fallback, max) {
|
|
75
|
+
const s = (v == null ? '' : String(v)).trim() || fallback;
|
|
76
|
+
return s.length > max ? `${s.slice(0, max - 1)}…` : s;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A tiny fixed-capacity ring for access events. Pure and synchronous — the gateway keeps one
|
|
81
|
+
* in memory and snapshots it for the dashboard; the caller decides whether/how to persist.
|
|
82
|
+
*/
|
|
83
|
+
export function createAccessLog(max = ACCESS_LOG_MAX) {
|
|
84
|
+
const cap = Math.max(1, max | 0);
|
|
85
|
+
let buf = [];
|
|
86
|
+
return {
|
|
87
|
+
push(evt) { buf.push(evt); if (buf.length > cap) buf = buf.slice(buf.length - cap); return evt; },
|
|
88
|
+
// Newest first, optionally limited — the order a dashboard wants.
|
|
89
|
+
snapshot(limit) { const out = buf.slice().reverse(); return limit ? out.slice(0, limit) : out; },
|
|
90
|
+
get size() { return buf.length; },
|
|
91
|
+
clear() { buf = []; },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Storage tiers ────────────────────────────────────────────────────────────────────────
|
|
96
|
+
// One descriptor per place data lives: hot (browser), warm (local gateway), cold (cloud,
|
|
97
|
+
// future). The dashboard renders a row per tier; a tier that isn't configured says so.
|
|
98
|
+
|
|
99
|
+
export function makeStorageTier({ tier, label, present = true, records = null, bytes = null, newest = null, note = '' } = {}) {
|
|
100
|
+
return {
|
|
101
|
+
tier: String(tier || ''),
|
|
102
|
+
label: String(label || tier || ''),
|
|
103
|
+
present: !!present,
|
|
104
|
+
records: records == null ? null : Math.max(0, records | 0),
|
|
105
|
+
bytes: bytes == null ? null : Math.max(0, Number(bytes) || 0),
|
|
106
|
+
newest: newest == null ? null : Number(newest) || 0,
|
|
107
|
+
note: String(note || ''),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Human-friendly byte size. Binary units, one decimal above KB. */
|
|
112
|
+
export function formatBytes(n) {
|
|
113
|
+
const b = Number(n);
|
|
114
|
+
if (!Number.isFinite(b) || b <= 0) return '0 B';
|
|
115
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
116
|
+
let i = 0, v = b;
|
|
117
|
+
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
|
|
118
|
+
return `${i === 0 ? Math.round(v) : v.toFixed(1)} ${units[i]}`;
|
|
119
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/events",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "The canonical ChatPanel event-log and capability contracts \u2014 typed durable facts, clock-free deterministic linearization, schema upcasting, and the invariants the replay harness asserts. Pure, dependency-free ESM shared by the ChatPanel extension, gateway and bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"./tool-groups.js": "./tool-groups.js",
|
|
38
38
|
"./tool-need.js": "./tool-need.js",
|
|
39
39
|
"./trajectory.js": "./trajectory.js",
|
|
40
|
-
"./upcast.js": "./upcast.js"
|
|
40
|
+
"./upcast.js": "./upcast.js",
|
|
41
|
+
"./observability.js": "./observability.js"
|
|
41
42
|
},
|
|
42
43
|
"files": [
|
|
43
44
|
"LICENSE",
|
|
@@ -55,6 +56,7 @@
|
|
|
55
56
|
"markdown-authoring.js",
|
|
56
57
|
"mcp-errors.js",
|
|
57
58
|
"meeting-analyzers.js",
|
|
59
|
+
"observability.js",
|
|
58
60
|
"order.js",
|
|
59
61
|
"ref.js",
|
|
60
62
|
"registry.js",
|