sandoichi 0.4.1 → 0.4.2
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/README.md +2 -2
- package/index.mjs +70 -0
- package/package.json +1 -1
- package/src/artifact-cli.mjs +67 -0
- package/src/artifact-recovery.mjs +133 -0
- package/src/artifact-store.mjs +43 -0
- package/src/context-audit-cli.mjs +104 -0
- package/src/context-capture.mjs +200 -0
- package/src/context-classifier.mjs +142 -0
- package/src/context-footprint.mjs +299 -0
- package/src/context-transform.mjs +18 -1
- package/src/core.mjs +18 -2
- package/src/f1-telemetry.mjs +80 -0
- package/src/f4-telemetry.mjs +183 -0
- package/src/gateway-gate-cli.mjs +88 -0
- package/src/gateway-gate.mjs +412 -0
- package/src/history-disclosure.mjs +70 -0
- package/src/lazy-mcp-gateway-stdio.mjs +59 -0
- package/src/lazy-mcp-gateway.mjs +291 -0
- package/src/mcp-server.mjs +31 -5
- package/src/proxy.mjs +424 -15
- package/src/result-disclosure.mjs +109 -0
- package/src/telemetry.mjs +61 -17
package/src/telemetry.mjs
CHANGED
|
@@ -5,16 +5,30 @@ import path from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
|
|
7
7
|
import { atomicWrite, ensureDirectory, withLock } from './provider-usage.mjs';
|
|
8
|
+
import { PLUGIN_VERSION } from './version.mjs';
|
|
8
9
|
|
|
9
|
-
const SCHEMA_VERSION =
|
|
10
|
+
export const SCHEMA_VERSION = 2;
|
|
11
|
+
const LEGACY_SCHEMA_VERSION = 1;
|
|
12
|
+
const SUPPORTED_QUEUE_SCHEMA_VERSIONS = new Set([LEGACY_SCHEMA_VERSION, SCHEMA_VERSION]);
|
|
10
13
|
const MAX_STRING_LENGTH = 32;
|
|
11
14
|
const MAX_EVENT_BYTES = 2048;
|
|
12
15
|
|
|
13
|
-
const COUNT_BUCKETS = ['zero', 'one', '2_to_5', '6_to_20', '
|
|
14
|
-
const BYTE_BUCKETS = ['lt_4k', '4_to_16k', '16_to_64k', '
|
|
16
|
+
const COUNT_BUCKETS = ['zero', 'one', '2_to_5', '6_to_20', '21_to_100', 'gt_100'];
|
|
17
|
+
const BYTE_BUCKETS = ['lt_4k', '4_to_16k', '16_to_64k', '64_to_256k', '256k_to_1m', 'gte_1m'];
|
|
18
|
+
const LOCAL_ONLY_EVENTS = new Set(['f1_footprint', 'f4_gateway']);
|
|
15
19
|
const HOSTS = ['claude', 'codex'];
|
|
16
20
|
const PROVIDERS = ['anthropic', 'openai', 'unknown'];
|
|
17
21
|
const MODES = ['enforce', 'observe', 'dry_run'];
|
|
22
|
+
const F4_HOSTS = ['claude', 'codex', 'unknown'];
|
|
23
|
+
const F4_OPERATIONS = ['catalog', 'call'];
|
|
24
|
+
const F4_OUTCOMES = ['success', 'rejected', 'timeout', 'cancelled', 'error'];
|
|
25
|
+
const F4_LATENCY_BUCKETS = ['lt_10ms', '10_to_100ms', '100_to_1000ms', 'gte_1000ms'];
|
|
26
|
+
const F4_RESULT_BUCKETS = ['zero', 'one', '2_to_5', '6_to_20', 'gt_20', 'unknown'];
|
|
27
|
+
const F1_HOSTS = ['claude', 'codex'];
|
|
28
|
+
const F1_STATUSES = ['complete', 'partial', 'unavailable'];
|
|
29
|
+
const F1_RATIO_BUCKETS = ['zero', 'lt_1pct', '1_to_10pct', 'gt_10pct', 'unavailable'];
|
|
30
|
+
const F1_SIZE_BUCKETS = [...BYTE_BUCKETS, 'unavailable'];
|
|
31
|
+
const F1_INPUT_BUCKETS = [...COUNT_BUCKETS, 'unavailable'];
|
|
18
32
|
export const FAILURE_STAGES = [
|
|
19
33
|
'policy', 'input', 'redaction', 'optimization', 'artifact', 'output', 'upstream', 'response',
|
|
20
34
|
];
|
|
@@ -23,6 +37,7 @@ const SHARED_FIELDS = {
|
|
|
23
37
|
schema_version: (value) => value === SCHEMA_VERSION,
|
|
24
38
|
event: (value) => [
|
|
25
39
|
'hook_summary', 'proxy_summary', 'active_day', 'hook_failure_summary', 'proxy_failure_summary',
|
|
40
|
+
'f1_footprint', 'f4_gateway',
|
|
26
41
|
].includes(value),
|
|
27
42
|
day_utc: (value) => typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value),
|
|
28
43
|
plugin_version: (value) => typeof value === 'string' && /^\d+\.\d+(?:\.\d+)?$/.test(value) && value.length <= MAX_STRING_LENGTH,
|
|
@@ -54,12 +69,28 @@ const PROXY_FAILURE_FIELDS = {
|
|
|
54
69
|
provider: (value) => PROVIDERS.includes(value),
|
|
55
70
|
failure_stage: (value) => FAILURE_STAGES.includes(value),
|
|
56
71
|
};
|
|
72
|
+
const F4_FIELDS = {
|
|
73
|
+
f4_host: (value) => F4_HOSTS.includes(value),
|
|
74
|
+
f4_operation: (value) => F4_OPERATIONS.includes(value),
|
|
75
|
+
f4_outcome: (value) => F4_OUTCOMES.includes(value),
|
|
76
|
+
f4_latency_bucket: (value) => F4_LATENCY_BUCKETS.includes(value),
|
|
77
|
+
f4_result_bucket: (value) => F4_RESULT_BUCKETS.includes(value),
|
|
78
|
+
};
|
|
79
|
+
const F1_FIELDS = {
|
|
80
|
+
f1_host: (value) => F1_HOSTS.includes(value),
|
|
81
|
+
f1_status: (value) => F1_STATUSES.includes(value),
|
|
82
|
+
f1_unknown_ratio_bucket: (value) => F1_RATIO_BUCKETS.includes(value),
|
|
83
|
+
f1_body_size_bucket: (value) => F1_SIZE_BUCKETS.includes(value),
|
|
84
|
+
f1_input_tokens_bucket: (value) => F1_INPUT_BUCKETS.includes(value),
|
|
85
|
+
};
|
|
57
86
|
|
|
58
87
|
function fieldsForEvent(eventType) {
|
|
88
|
+
if (eventType === 'f1_footprint') return F1_FIELDS;
|
|
59
89
|
if (eventType === 'hook_summary') return HOOK_FIELDS;
|
|
60
90
|
if (eventType === 'proxy_summary') return PROXY_FIELDS;
|
|
61
91
|
if (eventType === 'active_day') return ACTIVE_DAY_FIELDS;
|
|
62
92
|
if (eventType === 'hook_failure_summary') return HOOK_FAILURE_FIELDS;
|
|
93
|
+
if (eventType === 'f4_gateway') return F4_FIELDS;
|
|
63
94
|
return PROXY_FAILURE_FIELDS;
|
|
64
95
|
}
|
|
65
96
|
|
|
@@ -69,7 +100,8 @@ export function countBucket(count) {
|
|
|
69
100
|
if (count === 1) return 'one';
|
|
70
101
|
if (count <= 5) return '2_to_5';
|
|
71
102
|
if (count <= 20) return '6_to_20';
|
|
72
|
-
return '
|
|
103
|
+
if (count <= 100) return '21_to_100';
|
|
104
|
+
return 'gt_100';
|
|
73
105
|
}
|
|
74
106
|
|
|
75
107
|
export function byteBucket(bytes) {
|
|
@@ -77,7 +109,9 @@ export function byteBucket(bytes) {
|
|
|
77
109
|
if (bytes < 4096) return 'lt_4k';
|
|
78
110
|
if (bytes < 16384) return '4_to_16k';
|
|
79
111
|
if (bytes < 65536) return '16_to_64k';
|
|
80
|
-
return '
|
|
112
|
+
if (bytes < 262144) return '64_to_256k';
|
|
113
|
+
if (bytes < 1048576) return '256k_to_1m';
|
|
114
|
+
return 'gte_1m';
|
|
81
115
|
}
|
|
82
116
|
|
|
83
117
|
export function validateEvent(payload) {
|
|
@@ -106,11 +140,6 @@ export const TELEMETRY_CONFIG_VERSION = 1;
|
|
|
106
140
|
export const CONSENT_VERSION = 1;
|
|
107
141
|
export const TELEMETRY_DETAILS_URL = 'https://github.com/yuzushi-dev/Sando/blob/main/TELEMETRY.md';
|
|
108
142
|
export const CONSENT_STATES = ['unasked', 'asked', 'enabled', 'declined'];
|
|
109
|
-
// Canary phase: shared backend, fronted by a Cloudflare Tunnel so it's
|
|
110
|
-
// reachable from any of the owner's machines (see
|
|
111
|
-
// session-handoff/deploy/telemetry/). Rate-limited at nginx (30 req/min/IP).
|
|
112
|
-
// Release/broader publication is still gated on the open items in
|
|
113
|
-
// session-handoff/docs/telemetry-canary-report.md.
|
|
114
143
|
export const TELEMETRY_ENDPOINT = 'https://telemetry.yuzushi.party/v1/logs';
|
|
115
144
|
|
|
116
145
|
export function isDoNotTrack(env = process.env) {
|
|
@@ -239,7 +268,17 @@ function readCounters(countersPath) {
|
|
|
239
268
|
|
|
240
269
|
function readQueueRows(queuePath) {
|
|
241
270
|
if (!fs.existsSync(queuePath)) return [];
|
|
242
|
-
return fs.readFileSync(queuePath, 'utf8').split('\n').filter(Boolean).map((line) =>
|
|
271
|
+
return fs.readFileSync(queuePath, 'utf8').split('\n').filter(Boolean).map((line) => {
|
|
272
|
+
const row = JSON.parse(line);
|
|
273
|
+
assertQueueRow(row);
|
|
274
|
+
return row;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function assertQueueRow(row) {
|
|
279
|
+
if (!record(row) || !SUPPORTED_QUEUE_SCHEMA_VERSIONS.has(row.schema_version)) {
|
|
280
|
+
throw new Error('telemetry queue event schema is unsupported');
|
|
281
|
+
}
|
|
243
282
|
}
|
|
244
283
|
|
|
245
284
|
function writeQueueRows(queuePath, rows) {
|
|
@@ -253,7 +292,11 @@ function writeQueueRows(queuePath, rows) {
|
|
|
253
292
|
|
|
254
293
|
/** Enforces the bounded queue (4096 rows / 4 MiB), dropping the oldest rows first —
|
|
255
294
|
* a telemetry backlog must never grow without bound or block product behavior. */
|
|
256
|
-
function appendQueueRows(queuePath, newRows) {
|
|
295
|
+
export function appendQueueRows(queuePath, newRows) {
|
|
296
|
+
for (const row of newRows) {
|
|
297
|
+
assertQueueRow(row);
|
|
298
|
+
if (LOCAL_ONLY_EVENTS.has(row.event)) throw new Error(`${row.event}: local-only event must never reach the upload queue`);
|
|
299
|
+
}
|
|
257
300
|
ensureDirectory(path.dirname(queuePath));
|
|
258
301
|
withLock(`${queuePath}.lock`, () => {
|
|
259
302
|
let rows = readQueueRows(queuePath);
|
|
@@ -279,7 +322,7 @@ function publicRow(row) {
|
|
|
279
322
|
function bucketEntry(entry, pluginVersion) {
|
|
280
323
|
if (entry.event === 'hook_summary') {
|
|
281
324
|
return {
|
|
282
|
-
schema_version:
|
|
325
|
+
schema_version: SCHEMA_VERSION, event: 'hook_summary', day_utc: entry.day, plugin_version: PLUGIN_VERSION,
|
|
283
326
|
host: entry.host, mode: entry.mode,
|
|
284
327
|
tool_calls_bucket: countBucket(entry.toolCalls ?? 0),
|
|
285
328
|
capped_outputs_bucket: countBucket(entry.cappedOutputs ?? 0),
|
|
@@ -288,18 +331,18 @@ function bucketEntry(entry, pluginVersion) {
|
|
|
288
331
|
};
|
|
289
332
|
}
|
|
290
333
|
if (entry.event === 'proxy_summary') return {
|
|
291
|
-
schema_version:
|
|
334
|
+
schema_version: SCHEMA_VERSION, event: 'proxy_summary', day_utc: entry.day, plugin_version: PLUGIN_VERSION,
|
|
292
335
|
provider: entry.provider ?? 'unknown', mode: entry.mode ?? 'enforce',
|
|
293
336
|
rewrites_applied_bucket: countBucket(entry.rewritesApplied ?? 0),
|
|
294
337
|
rewrites_skipped_cache_bucket: countBucket(entry.rewritesSkippedCache ?? 0),
|
|
295
338
|
input_tokens_saved_bucket: byteBucket(entry.inputTokensSaved ?? 0),
|
|
296
339
|
};
|
|
297
340
|
if (entry.event === 'hook_failure_summary') return {
|
|
298
|
-
schema_version:
|
|
341
|
+
schema_version: SCHEMA_VERSION, event: 'hook_failure_summary', day_utc: entry.day, plugin_version: PLUGIN_VERSION,
|
|
299
342
|
host: entry.host, failure_stage: entry.failureStage,
|
|
300
343
|
};
|
|
301
344
|
return {
|
|
302
|
-
schema_version:
|
|
345
|
+
schema_version: SCHEMA_VERSION, event: 'proxy_failure_summary', day_utc: entry.day, plugin_version: PLUGIN_VERSION,
|
|
303
346
|
provider: entry.provider, failure_stage: entry.failureStage,
|
|
304
347
|
};
|
|
305
348
|
}
|
|
@@ -341,7 +384,7 @@ export function recordFailure({ statePaths, day, event, host, provider, failureS
|
|
|
341
384
|
/** Queues a single non-aggregate activity marker for this UTC day and host. */
|
|
342
385
|
export function recordActiveDay({ statePaths, day, pluginVersion, host }) {
|
|
343
386
|
const marker = {
|
|
344
|
-
schema_version: SCHEMA_VERSION, event: 'active_day', day_utc: day, plugin_version:
|
|
387
|
+
schema_version: SCHEMA_VERSION, event: 'active_day', day_utc: day, plugin_version: PLUGIN_VERSION, host,
|
|
345
388
|
};
|
|
346
389
|
const validatedMarker = validateEvent(marker);
|
|
347
390
|
const activeDayKey = `${day}|${host}`;
|
|
@@ -459,6 +502,7 @@ export function toOtlpLogs(rows) {
|
|
|
459
502
|
resource: { attributes: [{ key: 'service.name', value: { stringValue: 'sando' } }] },
|
|
460
503
|
scopeLogs: [{
|
|
461
504
|
logRecords: rows.map((row) => ({
|
|
505
|
+
...(typeof row._timeUnixNano === 'string' ? { timeUnixNano: row._timeUnixNano } : {}),
|
|
462
506
|
body: { stringValue: 'sando.daily_aggregate' },
|
|
463
507
|
attributes: Object.entries(publicRow(row)).map(([key, value]) => ({ key, value: { stringValue: String(value) } })),
|
|
464
508
|
})),
|