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
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export const GATE_EVIDENCE_SCHEMA = 'sando-progressive-gateway-evidence/v1';
|
|
4
|
+
export const GATE_SCHEMA = 'sando-progressive-context-gate/v1';
|
|
5
|
+
export const GATE_VERSION = 1;
|
|
6
|
+
export const GATE_THRESHOLDS = Object.freeze({
|
|
7
|
+
hosts: Object.freeze(['claude', 'codex']),
|
|
8
|
+
minimumPairedSamples: 10,
|
|
9
|
+
minimumDiscoveryIntents: 50,
|
|
10
|
+
minimumResidualMcpTokens: 10_000,
|
|
11
|
+
minimumResidualMcpRatio: 0.10,
|
|
12
|
+
minimumDiscoverySuccessRate: 0.95,
|
|
13
|
+
minimumSafetyCriticalDiscoveryRate: 1,
|
|
14
|
+
maximumWrongMutativeCalls: 0,
|
|
15
|
+
minimumProviderInputReductionRate: 0.10,
|
|
16
|
+
maximumExtraToolCallsMedian: 1,
|
|
17
|
+
maximumP50LatencyOverhead: 0.15,
|
|
18
|
+
maximumStartupOverhead: 0.10,
|
|
19
|
+
minimumSchemaValidationRate: 1,
|
|
20
|
+
minimumCancellationPropagationRate: 1,
|
|
21
|
+
minimumTimeoutPropagationRate: 1,
|
|
22
|
+
minimumErrorPropagationRate: 1,
|
|
23
|
+
minimumProgressPropagationRate: 1,
|
|
24
|
+
minimumListChangedInvalidationRate: 1,
|
|
25
|
+
maximumWeightedCostRatio: 1,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const HOSTS = new Set(GATE_THRESHOLDS.hosts);
|
|
29
|
+
const STATES = new Set(['enabled', 'disabled', 'unavailable', 'indeterminate']);
|
|
30
|
+
const METRIC_FIELDS = Object.freeze([
|
|
31
|
+
'discoverySuccessRate',
|
|
32
|
+
'safetyCriticalDiscoveryRate',
|
|
33
|
+
'wrongMutativeCalls',
|
|
34
|
+
'providerInputReductionRate',
|
|
35
|
+
'extraToolCallsMedian',
|
|
36
|
+
'p50LatencyOverhead',
|
|
37
|
+
'startupOverhead',
|
|
38
|
+
'schemaValidationRate',
|
|
39
|
+
'cancellationPropagationRate',
|
|
40
|
+
'timeoutPropagationRate',
|
|
41
|
+
'errorPropagationRate',
|
|
42
|
+
'progressPropagationRate',
|
|
43
|
+
'listChangedInvalidationRate',
|
|
44
|
+
'authPropagationRate',
|
|
45
|
+
'approvalPropagationRate',
|
|
46
|
+
'elicitationPropagationRate',
|
|
47
|
+
'weightedCostRatio',
|
|
48
|
+
'acceptanceControlRate',
|
|
49
|
+
'acceptanceTreatmentRate',
|
|
50
|
+
]);
|
|
51
|
+
const BOUNDED_RATE_FIELDS = new Set([
|
|
52
|
+
'discoverySuccessRate',
|
|
53
|
+
'safetyCriticalDiscoveryRate',
|
|
54
|
+
'schemaValidationRate',
|
|
55
|
+
'cancellationPropagationRate',
|
|
56
|
+
'timeoutPropagationRate',
|
|
57
|
+
'errorPropagationRate',
|
|
58
|
+
'progressPropagationRate',
|
|
59
|
+
'listChangedInvalidationRate',
|
|
60
|
+
'authPropagationRate',
|
|
61
|
+
'approvalPropagationRate',
|
|
62
|
+
'elicitationPropagationRate',
|
|
63
|
+
'acceptanceControlRate',
|
|
64
|
+
'acceptanceTreatmentRate',
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
function object(value) {
|
|
68
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function digest(value, name) {
|
|
72
|
+
if (typeof value !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(value)) throw new TypeError(`${name} is invalid`);
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function integer(value, name) {
|
|
77
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError(`${name} is invalid`);
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function rate(value, name, { minimum = 0, maximum = Infinity } = {}) {
|
|
82
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < minimum || value > maximum) throw new TypeError(`${name} is invalid`);
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function optionalInteger(value, name) {
|
|
87
|
+
if (value === undefined || value === null) return null;
|
|
88
|
+
return integer(value, name);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function optionalRate(value, name, options) {
|
|
92
|
+
if (value === undefined || value === null) return null;
|
|
93
|
+
return rate(value, name, options);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function stableJson(value, seen = new Set()) {
|
|
97
|
+
if (value === null || typeof value !== 'object') {
|
|
98
|
+
const result = JSON.stringify(value);
|
|
99
|
+
if (result === undefined) throw new TypeError('gateway gate value is not serializable');
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
if (seen.has(value)) throw new TypeError('gateway gate value must not be cyclic');
|
|
103
|
+
seen.add(value);
|
|
104
|
+
const result = Array.isArray(value)
|
|
105
|
+
? `[${value.map((item) => stableJson(item, seen)).join(',')}]`
|
|
106
|
+
: `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key], seen)}`).join(',')}}`;
|
|
107
|
+
seen.delete(value);
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function provenanceDigest(value) {
|
|
112
|
+
return `sha256:${createHash('sha256').update(stableJson(value)).digest('hex')}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function identity(value, name) {
|
|
116
|
+
if (value === undefined || value === null) return null;
|
|
117
|
+
if (!object(value)) throw new TypeError(`${name} is invalid`);
|
|
118
|
+
const result = {};
|
|
119
|
+
for (const field of ['scenarioDigest', 'workloadDigest']) {
|
|
120
|
+
result[field] = value[field] === undefined || value[field] === null
|
|
121
|
+
? null : digest(value[field], `${name}.${field}`);
|
|
122
|
+
}
|
|
123
|
+
for (const field of ['promptDigest', 'modelDigest', 'clientVersionDigest']) {
|
|
124
|
+
if (value[field] !== undefined && value[field] !== null) result[field] = digest(value[field], `${name}.${field}`);
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function sameIdentity(control, treatment) {
|
|
130
|
+
if (!control || !treatment || control.scenarioDigest === null || control.workloadDigest === null
|
|
131
|
+
|| treatment.scenarioDigest === null || treatment.workloadDigest === null) return false;
|
|
132
|
+
const fields = new Set([...Object.keys(control), ...Object.keys(treatment)]);
|
|
133
|
+
return [...fields].every((field) => control[field] === treatment[field]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeMetrics(value, host) {
|
|
137
|
+
if (value === undefined || value === null) {
|
|
138
|
+
return { source: null, ...Object.fromEntries(METRIC_FIELDS.map((field) => [field, null])) };
|
|
139
|
+
}
|
|
140
|
+
if (!object(value) || value.source === 'mechanical-estimate'
|
|
141
|
+
|| (value.source !== undefined && value.source !== null && value.source !== 'provider-reported')) {
|
|
142
|
+
throw new TypeError(`${host}.metrics must use provider-reported evidence`);
|
|
143
|
+
}
|
|
144
|
+
const result = { source: value.source ?? null };
|
|
145
|
+
for (const field of METRIC_FIELDS) {
|
|
146
|
+
const candidate = value[field];
|
|
147
|
+
if (candidate === undefined || candidate === null) result[field] = null;
|
|
148
|
+
else if (field === 'wrongMutativeCalls') result[field] = integer(candidate, `${host}.metrics.${field}`);
|
|
149
|
+
else if (field === 'extraToolCallsMedian') result[field] = rate(candidate, `${host}.metrics.${field}`);
|
|
150
|
+
else if (field === 'weightedCostRatio') result[field] = rate(candidate, `${host}.metrics.${field}`);
|
|
151
|
+
else result[field] = rate(candidate, `${host}.metrics.${field}`, {
|
|
152
|
+
minimum: field === 'providerInputReductionRate' ? -Infinity : 0,
|
|
153
|
+
maximum: BOUNDED_RATE_FIELDS.has(field) || field === 'providerInputReductionRate' ? 1 : Infinity,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function normalizeHost(value) {
|
|
160
|
+
if (!object(value) || !HOSTS.has(value.host)) throw new TypeError('gateway evidence host is invalid');
|
|
161
|
+
const controlArm = value.controlArm ?? null;
|
|
162
|
+
const treatmentArm = value.treatmentArm ?? null;
|
|
163
|
+
if ((controlArm !== null && controlArm !== 'native-tool-search')
|
|
164
|
+
|| (treatmentArm !== null && treatmentArm !== 'sando-gateway')) {
|
|
165
|
+
throw new TypeError(`${value.host} arm provenance is invalid`);
|
|
166
|
+
}
|
|
167
|
+
if ((value.realWorkload !== undefined && typeof value.realWorkload !== 'boolean')
|
|
168
|
+
|| (value.originalMcpDisabled !== undefined && typeof value.originalMcpDisabled !== 'boolean')) {
|
|
169
|
+
throw new TypeError(`${value.host} workload isolation evidence is invalid`);
|
|
170
|
+
}
|
|
171
|
+
if ((value.catalogReadOnly !== undefined && typeof value.catalogReadOnly !== 'boolean')
|
|
172
|
+
|| (value.allowlistEnforced !== undefined && typeof value.allowlistEnforced !== 'boolean')
|
|
173
|
+
|| (value.killSwitchRollbackTested !== undefined && typeof value.killSwitchRollbackTested !== 'boolean')) {
|
|
174
|
+
throw new TypeError(`${value.host} gateway safety evidence is invalid`);
|
|
175
|
+
}
|
|
176
|
+
const nativeToolSearchState = value.nativeToolSearchState ?? null;
|
|
177
|
+
if (nativeToolSearchState !== null && !STATES.has(nativeToolSearchState)) throw new TypeError(`${value.host} Tool Search state is invalid`);
|
|
178
|
+
const identityEvidence = object(value.identity)
|
|
179
|
+
? value.identity
|
|
180
|
+
: value.identity === undefined || value.identity === null ? {} : null;
|
|
181
|
+
if (!identityEvidence) throw new TypeError(`${value.host} identity evidence is invalid`);
|
|
182
|
+
const control = identity(identityEvidence.control, `${value.host}.identity.control`);
|
|
183
|
+
const treatment = identity(identityEvidence.treatment, `${value.host}.identity.treatment`);
|
|
184
|
+
const prerequisite = object(value.prerequisite) ? value.prerequisite : {};
|
|
185
|
+
const metrics = normalizeMetrics(value.metrics, value.host);
|
|
186
|
+
return {
|
|
187
|
+
host: value.host,
|
|
188
|
+
controlArm,
|
|
189
|
+
treatmentArm,
|
|
190
|
+
realWorkload: value.realWorkload ?? null,
|
|
191
|
+
originalMcpDisabled: value.originalMcpDisabled ?? null,
|
|
192
|
+
catalogReadOnly: value.catalogReadOnly ?? null,
|
|
193
|
+
allowlistEnforced: value.allowlistEnforced ?? null,
|
|
194
|
+
killSwitchRollbackTested: value.killSwitchRollbackTested ?? null,
|
|
195
|
+
nativeToolSearchState,
|
|
196
|
+
samplePairs: value.samplePairs === undefined || value.samplePairs === null
|
|
197
|
+
? null : integer(value.samplePairs, `${value.host}.samplePairs`),
|
|
198
|
+
discoveryIntents: value.discoveryIntents === undefined || value.discoveryIntents === null
|
|
199
|
+
? null : integer(value.discoveryIntents, `${value.host}.discoveryIntents`),
|
|
200
|
+
identity: { control, treatment, paired: sameIdentity(control, treatment) },
|
|
201
|
+
prerequisite: {
|
|
202
|
+
residualMcpTokens: optionalInteger(prerequisite.residualMcpTokens, `${value.host}.prerequisite.residualMcpTokens`),
|
|
203
|
+
initialContextTokens: optionalInteger(prerequisite.initialContextTokens, `${value.host}.prerequisite.initialContextTokens`),
|
|
204
|
+
selectionStartupDefectRate: optionalRate(
|
|
205
|
+
prerequisite.selectionStartupDefectRate,
|
|
206
|
+
`${value.host}.prerequisite.selectionStartupDefectRate`,
|
|
207
|
+
{ maximum: 1 },
|
|
208
|
+
),
|
|
209
|
+
},
|
|
210
|
+
metrics,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function check(host, id, observed, required, pass, { missing = observed === null } = {}) {
|
|
215
|
+
return {
|
|
216
|
+
host,
|
|
217
|
+
id,
|
|
218
|
+
status: missing ? 'missing' : pass ? 'pass' : 'fail',
|
|
219
|
+
observed,
|
|
220
|
+
required,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function prerequisiteValue(value) {
|
|
225
|
+
const { residualMcpTokens, initialContextTokens, selectionStartupDefectRate } = value.prerequisite;
|
|
226
|
+
const residualTokens = residualMcpTokens !== null && residualMcpTokens >= GATE_THRESHOLDS.minimumResidualMcpTokens;
|
|
227
|
+
const residualRatio = initialContextTokens !== null && initialContextTokens > 0 && residualMcpTokens !== null
|
|
228
|
+
&& residualMcpTokens / initialContextTokens >= GATE_THRESHOLDS.minimumResidualMcpRatio;
|
|
229
|
+
const defect = selectionStartupDefectRate !== null && selectionStartupDefectRate > 0;
|
|
230
|
+
const available = residualMcpTokens !== null || initialContextTokens !== null || selectionStartupDefectRate !== null;
|
|
231
|
+
return { residualTokens, residualRatio, defect, available, qualified: residualTokens || residualRatio || defect };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function hostChecks(value) {
|
|
235
|
+
const prerequisite = prerequisiteValue(value);
|
|
236
|
+
const metrics = value.metrics;
|
|
237
|
+
const checks = [
|
|
238
|
+
check(value.host, 'native-tool-search-control-arm', value.controlArm, 'native-tool-search', value.controlArm === 'native-tool-search', {
|
|
239
|
+
missing: value.controlArm === null,
|
|
240
|
+
}),
|
|
241
|
+
check(value.host, 'sando-gateway-treatment-arm', value.treatmentArm, 'sando-gateway', value.treatmentArm === 'sando-gateway', {
|
|
242
|
+
missing: value.treatmentArm === null,
|
|
243
|
+
}),
|
|
244
|
+
check(value.host, 'paired-identity', value.identity.paired, true, value.identity.paired, { missing: !value.identity.paired }),
|
|
245
|
+
check(value.host, 'real-workload', value.realWorkload, true, value.realWorkload === true, { missing: value.realWorkload === null }),
|
|
246
|
+
check(value.host, 'native-tool-search-control', value.nativeToolSearchState, 'enabled', value.nativeToolSearchState === 'enabled', {
|
|
247
|
+
missing: !['enabled', 'disabled'].includes(value.nativeToolSearchState),
|
|
248
|
+
}),
|
|
249
|
+
check(value.host, 'isolated-original-mcp', value.originalMcpDisabled, true, value.originalMcpDisabled === true, { missing: value.originalMcpDisabled === null }),
|
|
250
|
+
check(value.host, 'paired-sample-count', value.samplePairs, GATE_THRESHOLDS.minimumPairedSamples,
|
|
251
|
+
value.samplePairs !== null && value.samplePairs >= GATE_THRESHOLDS.minimumPairedSamples),
|
|
252
|
+
check(value.host, 'discovery-intent-count', value.discoveryIntents, GATE_THRESHOLDS.minimumDiscoveryIntents,
|
|
253
|
+
value.discoveryIntents !== null && value.discoveryIntents >= GATE_THRESHOLDS.minimumDiscoveryIntents),
|
|
254
|
+
check(value.host, 'catalog-read-only', value.catalogReadOnly, true, value.catalogReadOnly === true, { missing: value.catalogReadOnly === null }),
|
|
255
|
+
check(value.host, 'allowlist', value.allowlistEnforced, true, value.allowlistEnforced === true, { missing: value.allowlistEnforced === null }),
|
|
256
|
+
check(value.host, 'kill-switch-rollback', value.killSwitchRollbackTested, true, value.killSwitchRollbackTested === true, { missing: value.killSwitchRollbackTested === null }),
|
|
257
|
+
check(value.host, 'gateway-prerequisite', {
|
|
258
|
+
residualMcpTokens: value.prerequisite.residualMcpTokens,
|
|
259
|
+
initialContextTokens: value.prerequisite.initialContextTokens,
|
|
260
|
+
selectionStartupDefectRate: value.prerequisite.selectionStartupDefectRate,
|
|
261
|
+
qualified: prerequisite.qualified,
|
|
262
|
+
}, 'residual threshold or measured selection/startup defect', prerequisite.qualified, {
|
|
263
|
+
missing: !prerequisite.available,
|
|
264
|
+
}),
|
|
265
|
+
check(value.host, 'provider-reported-metrics', metrics.source, 'provider-reported', metrics.source === 'provider-reported', {
|
|
266
|
+
missing: metrics.source === null,
|
|
267
|
+
}),
|
|
268
|
+
check(value.host, 'discovery-success', metrics.discoverySuccessRate, GATE_THRESHOLDS.minimumDiscoverySuccessRate,
|
|
269
|
+
metrics.discoverySuccessRate !== null && metrics.discoverySuccessRate >= GATE_THRESHOLDS.minimumDiscoverySuccessRate),
|
|
270
|
+
check(value.host, 'safety-critical-discovery', metrics.safetyCriticalDiscoveryRate, GATE_THRESHOLDS.minimumSafetyCriticalDiscoveryRate,
|
|
271
|
+
metrics.safetyCriticalDiscoveryRate !== null && metrics.safetyCriticalDiscoveryRate >= GATE_THRESHOLDS.minimumSafetyCriticalDiscoveryRate),
|
|
272
|
+
check(value.host, 'wrong-mutative-calls', metrics.wrongMutativeCalls, GATE_THRESHOLDS.maximumWrongMutativeCalls,
|
|
273
|
+
metrics.wrongMutativeCalls !== null && metrics.wrongMutativeCalls <= GATE_THRESHOLDS.maximumWrongMutativeCalls),
|
|
274
|
+
check(value.host, 'provider-input-reduction', metrics.providerInputReductionRate, GATE_THRESHOLDS.minimumProviderInputReductionRate,
|
|
275
|
+
metrics.providerInputReductionRate !== null && metrics.providerInputReductionRate >= GATE_THRESHOLDS.minimumProviderInputReductionRate),
|
|
276
|
+
check(value.host, 'extra-tool-calls', metrics.extraToolCallsMedian, GATE_THRESHOLDS.maximumExtraToolCallsMedian,
|
|
277
|
+
metrics.extraToolCallsMedian !== null && metrics.extraToolCallsMedian <= GATE_THRESHOLDS.maximumExtraToolCallsMedian),
|
|
278
|
+
check(value.host, 'p50-latency-overhead', metrics.p50LatencyOverhead, GATE_THRESHOLDS.maximumP50LatencyOverhead,
|
|
279
|
+
metrics.p50LatencyOverhead !== null && metrics.p50LatencyOverhead <= GATE_THRESHOLDS.maximumP50LatencyOverhead),
|
|
280
|
+
check(value.host, 'startup-overhead', metrics.startupOverhead, GATE_THRESHOLDS.maximumStartupOverhead,
|
|
281
|
+
metrics.startupOverhead !== null && metrics.startupOverhead <= GATE_THRESHOLDS.maximumStartupOverhead),
|
|
282
|
+
check(value.host, 'schema-validation', metrics.schemaValidationRate, GATE_THRESHOLDS.minimumSchemaValidationRate,
|
|
283
|
+
metrics.schemaValidationRate !== null && metrics.schemaValidationRate >= GATE_THRESHOLDS.minimumSchemaValidationRate),
|
|
284
|
+
check(value.host, 'cancellation-propagation', metrics.cancellationPropagationRate, GATE_THRESHOLDS.minimumCancellationPropagationRate,
|
|
285
|
+
metrics.cancellationPropagationRate !== null && metrics.cancellationPropagationRate >= GATE_THRESHOLDS.minimumCancellationPropagationRate),
|
|
286
|
+
check(value.host, 'timeout-propagation', metrics.timeoutPropagationRate, GATE_THRESHOLDS.minimumTimeoutPropagationRate,
|
|
287
|
+
metrics.timeoutPropagationRate !== null && metrics.timeoutPropagationRate >= GATE_THRESHOLDS.minimumTimeoutPropagationRate),
|
|
288
|
+
check(value.host, 'error-propagation', metrics.errorPropagationRate, GATE_THRESHOLDS.minimumErrorPropagationRate,
|
|
289
|
+
metrics.errorPropagationRate !== null && metrics.errorPropagationRate >= GATE_THRESHOLDS.minimumErrorPropagationRate),
|
|
290
|
+
check(value.host, 'progress-propagation', metrics.progressPropagationRate, GATE_THRESHOLDS.minimumProgressPropagationRate,
|
|
291
|
+
metrics.progressPropagationRate !== null && metrics.progressPropagationRate >= GATE_THRESHOLDS.minimumProgressPropagationRate),
|
|
292
|
+
check(value.host, 'list-changed-invalidation', metrics.listChangedInvalidationRate, GATE_THRESHOLDS.minimumListChangedInvalidationRate,
|
|
293
|
+
metrics.listChangedInvalidationRate !== null && metrics.listChangedInvalidationRate >= GATE_THRESHOLDS.minimumListChangedInvalidationRate),
|
|
294
|
+
check(value.host, 'auth-propagation', metrics.authPropagationRate, 1,
|
|
295
|
+
metrics.authPropagationRate !== null && metrics.authPropagationRate >= 1),
|
|
296
|
+
check(value.host, 'approval-propagation', metrics.approvalPropagationRate, 1,
|
|
297
|
+
metrics.approvalPropagationRate !== null && metrics.approvalPropagationRate >= 1),
|
|
298
|
+
check(value.host, 'elicitation-propagation', metrics.elicitationPropagationRate, 1,
|
|
299
|
+
metrics.elicitationPropagationRate !== null && metrics.elicitationPropagationRate >= 1),
|
|
300
|
+
check(value.host, 'weighted-cost', metrics.weightedCostRatio, GATE_THRESHOLDS.maximumWeightedCostRatio,
|
|
301
|
+
metrics.weightedCostRatio !== null && metrics.weightedCostRatio <= GATE_THRESHOLDS.maximumWeightedCostRatio),
|
|
302
|
+
check(value.host, 'acceptance-no-regression', {
|
|
303
|
+
control: metrics.acceptanceControlRate,
|
|
304
|
+
treatment: metrics.acceptanceTreatmentRate,
|
|
305
|
+
}, 'treatment >= control',
|
|
306
|
+
metrics.acceptanceControlRate !== null && metrics.acceptanceTreatmentRate !== null
|
|
307
|
+
&& metrics.acceptanceTreatmentRate >= metrics.acceptanceControlRate,
|
|
308
|
+
{ missing: metrics.acceptanceControlRate === null || metrics.acceptanceTreatmentRate === null }),
|
|
309
|
+
];
|
|
310
|
+
return checks;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function reasonFor(checkValue) {
|
|
314
|
+
const names = {
|
|
315
|
+
'paired-identity': 'paired-identity-required',
|
|
316
|
+
'native-tool-search-control-arm': 'native-tool-search-control-arm-required',
|
|
317
|
+
'sando-gateway-treatment-arm': 'sando-gateway-treatment-arm-required',
|
|
318
|
+
'provider-reported-metrics': 'provider-reported-metrics-required',
|
|
319
|
+
'both-hosts': 'both-hosts-required',
|
|
320
|
+
'real-workload': 'real-workload-required',
|
|
321
|
+
'native-tool-search-control': 'native-tool-search-control-required',
|
|
322
|
+
'isolated-original-mcp': 'isolated-original-mcp-required',
|
|
323
|
+
'paired-sample-count': 'minimum-paired-samples-required',
|
|
324
|
+
'discovery-intent-count': 'minimum-discovery-intents-required',
|
|
325
|
+
'catalog-read-only': 'catalog-read-only-required',
|
|
326
|
+
allowlist: 'allowlist-required',
|
|
327
|
+
'kill-switch-rollback': 'kill-switch-rollback-required',
|
|
328
|
+
'gateway-prerequisite': 'gateway-prerequisite-not-established',
|
|
329
|
+
'discovery-success': 'discovery-success-threshold',
|
|
330
|
+
'safety-critical-discovery': 'safety-critical-discovery-threshold',
|
|
331
|
+
'wrong-mutative-calls': 'wrong-mutative-call',
|
|
332
|
+
'provider-input-reduction': 'provider-input-reduction-threshold',
|
|
333
|
+
'extra-tool-calls': 'extra-tool-call-threshold',
|
|
334
|
+
'p50-latency-overhead': 'latency-overhead-threshold',
|
|
335
|
+
'startup-overhead': 'startup-overhead-threshold',
|
|
336
|
+
'schema-validation': 'schema-validation-threshold',
|
|
337
|
+
'cancellation-propagation': 'cancellation-propagation-threshold',
|
|
338
|
+
'timeout-propagation': 'timeout-propagation-threshold',
|
|
339
|
+
'error-propagation': 'error-propagation-threshold',
|
|
340
|
+
'progress-propagation': 'progress-propagation-threshold',
|
|
341
|
+
'list-changed-invalidation': 'list-changed-invalidation-threshold',
|
|
342
|
+
'auth-propagation': 'auth-propagation-threshold',
|
|
343
|
+
'approval-propagation': 'approval-propagation-threshold',
|
|
344
|
+
'elicitation-propagation': 'elicitation-propagation-threshold',
|
|
345
|
+
'weighted-cost': 'weighted-cost-regression',
|
|
346
|
+
'acceptance-no-regression': 'acceptance-regression',
|
|
347
|
+
};
|
|
348
|
+
return names[checkValue.id] ?? `${checkValue.id}-check`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function normalizeEvidence(value) {
|
|
352
|
+
if (!object(value) || value.schema !== GATE_EVIDENCE_SCHEMA || value.version !== GATE_VERSION) {
|
|
353
|
+
throw new TypeError('gateway evidence schema is invalid');
|
|
354
|
+
}
|
|
355
|
+
if (!Array.isArray(value.hosts)) throw new TypeError('gateway evidence hosts are invalid');
|
|
356
|
+
const hosts = value.hosts.map(normalizeHost).sort((left, right) => left.host.localeCompare(right.host));
|
|
357
|
+
if (new Set(hosts.map((item) => item.host)).size !== hosts.length) throw new TypeError('gateway evidence contains duplicate hosts');
|
|
358
|
+
return hosts;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function safeHost(value) {
|
|
362
|
+
return {
|
|
363
|
+
host: value.host,
|
|
364
|
+
controlArm: value.controlArm,
|
|
365
|
+
treatmentArm: value.treatmentArm,
|
|
366
|
+
samplePairs: value.samplePairs,
|
|
367
|
+
discoveryIntents: value.discoveryIntents,
|
|
368
|
+
realWorkload: value.realWorkload,
|
|
369
|
+
originalMcpDisabled: value.originalMcpDisabled,
|
|
370
|
+
catalogReadOnly: value.catalogReadOnly,
|
|
371
|
+
allowlistEnforced: value.allowlistEnforced,
|
|
372
|
+
killSwitchRollbackTested: value.killSwitchRollbackTested,
|
|
373
|
+
nativeToolSearchState: value.nativeToolSearchState,
|
|
374
|
+
identity: value.identity,
|
|
375
|
+
prerequisite: value.prerequisite,
|
|
376
|
+
metrics: value.metrics,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function evaluateGatewayGate({ evidence } = {}) {
|
|
381
|
+
const hosts = normalizeEvidence(evidence ?? { schema: GATE_EVIDENCE_SCHEMA, version: GATE_VERSION, hosts: [] });
|
|
382
|
+
const checks = hosts.flatMap(hostChecks);
|
|
383
|
+
if (hosts.length !== GATE_THRESHOLDS.hosts.length || hosts.some((item) => !HOSTS.has(item.host))) {
|
|
384
|
+
checks.unshift({
|
|
385
|
+
host: 'all', id: 'both-hosts', status: 'missing', observed: hosts.map((item) => item.host), required: GATE_THRESHOLDS.hosts,
|
|
386
|
+
});
|
|
387
|
+
} else {
|
|
388
|
+
checks.unshift({ host: 'all', id: 'both-hosts', status: 'pass', observed: hosts.map((item) => item.host), required: GATE_THRESHOLDS.hosts });
|
|
389
|
+
}
|
|
390
|
+
const missing = checks.filter((item) => item.status === 'missing');
|
|
391
|
+
const failed = checks.filter((item) => item.status === 'fail');
|
|
392
|
+
const status = missing.length > 0 ? 'insufficient-evidence' : failed.length > 0 ? 'no-go' : 'go';
|
|
393
|
+
const reasons = [...new Set(checks.filter((item) => item.status !== 'pass').map(reasonFor))].sort();
|
|
394
|
+
const report = {
|
|
395
|
+
schema: GATE_SCHEMA,
|
|
396
|
+
version: GATE_VERSION,
|
|
397
|
+
feature: 'lazy-mcp-gateway',
|
|
398
|
+
status,
|
|
399
|
+
thresholds: GATE_THRESHOLDS,
|
|
400
|
+
hosts: hosts.map(safeHost),
|
|
401
|
+
checks,
|
|
402
|
+
reasons,
|
|
403
|
+
};
|
|
404
|
+
return { ...report, provenanceDigest: provenanceDigest(report) };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function serializeGatewayGate(report) {
|
|
408
|
+
if (!object(report) || report.schema !== GATE_SCHEMA || report.version !== GATE_VERSION) {
|
|
409
|
+
throw new TypeError('gateway gate report is invalid');
|
|
410
|
+
}
|
|
411
|
+
return stableJson(report);
|
|
412
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { createRedactionProfile } from './redaction-profile.mjs';
|
|
4
|
+
|
|
5
|
+
export const HISTORY_DISCLOSURE_SCHEMA = 'sando-history-disclosure/v1';
|
|
6
|
+
export const HISTORY_DISCLOSURE_VERSION = 1;
|
|
7
|
+
const DEFAULT_PROFILE = createRedactionProfile();
|
|
8
|
+
|
|
9
|
+
function digest(text) {
|
|
10
|
+
return `sha256:${createHash('sha256').update(text).digest('hex')}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function typeOf(toolName) {
|
|
14
|
+
const name = toolName.toLowerCase();
|
|
15
|
+
if (name === 'read') return 'read';
|
|
16
|
+
if (name === 'grep') return 'grep';
|
|
17
|
+
if (name === 'bash' || name === 'exec') return 'bash';
|
|
18
|
+
if (name === 'log') return 'log';
|
|
19
|
+
return 'mcp';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function bytes(value, name) {
|
|
23
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError(`${name} is invalid`);
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildHistoryDisclosure({ toolName, reason, originalText, visibleText, recovery = 'rerun-tool', redactionProfile } = {}) {
|
|
28
|
+
if (typeof toolName !== 'string' || !toolName.trim() || typeof reason !== 'string' || !reason
|
|
29
|
+
|| typeof originalText !== 'string' || typeof visibleText !== 'string') {
|
|
30
|
+
throw new TypeError('history disclosure input is invalid');
|
|
31
|
+
}
|
|
32
|
+
if (!['rerun-tool', 'newer-result'].includes(recovery)) throw new TypeError('history disclosure recovery is invalid');
|
|
33
|
+
const original = Buffer.byteLength(originalText, 'utf8');
|
|
34
|
+
const profile = redactionProfile ?? DEFAULT_PROFILE;
|
|
35
|
+
if (!profile || typeof profile.redact !== 'function') throw new TypeError('history disclosure redaction profile is invalid');
|
|
36
|
+
const redactedText = profile.redact(originalText).text;
|
|
37
|
+
const redacted = Buffer.byteLength(redactedText, 'utf8');
|
|
38
|
+
const visible = Buffer.byteLength(visibleText, 'utf8');
|
|
39
|
+
return {
|
|
40
|
+
schema: HISTORY_DISCLOSURE_SCHEMA,
|
|
41
|
+
version: HISTORY_DISCLOSURE_VERSION,
|
|
42
|
+
type: typeOf(toolName),
|
|
43
|
+
reason,
|
|
44
|
+
provenanceDigest: digest(redactedText),
|
|
45
|
+
bytes: {
|
|
46
|
+
original: bytes(original, 'original bytes'),
|
|
47
|
+
redacted: bytes(redacted, 'redacted bytes'),
|
|
48
|
+
visible: bytes(visible, 'visible bytes'),
|
|
49
|
+
},
|
|
50
|
+
recovery: { mode: recovery, bounded: true },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function stableJson(value, seen = new Set()) {
|
|
55
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
56
|
+
if (seen.has(value)) throw new TypeError('history disclosure must not be cyclic');
|
|
57
|
+
seen.add(value);
|
|
58
|
+
const result = Array.isArray(value)
|
|
59
|
+
? `[${value.map((item) => stableJson(item, seen)).join(',')}]`
|
|
60
|
+
: `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key], seen)}`).join(',')}}`;
|
|
61
|
+
seen.delete(value);
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function serializeHistoryDisclosure(report) {
|
|
66
|
+
if (!report || report.schema !== HISTORY_DISCLOSURE_SCHEMA || report.version !== HISTORY_DISCLOSURE_VERSION) {
|
|
67
|
+
throw new TypeError('history disclosure is invalid');
|
|
68
|
+
}
|
|
69
|
+
return stableJson(report);
|
|
70
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import readline from 'node:readline';
|
|
3
|
+
|
|
4
|
+
export function spawnMcpTransport({ command, args = [], cwd, env, onMessage }) {
|
|
5
|
+
if (typeof command !== 'string' || !command || !Array.isArray(args) || args.some((arg) => typeof arg !== 'string')) throw new TypeError('gateway command configuration is invalid');
|
|
6
|
+
const child = spawn(command, args, { cwd, env: env ? { ...process.env, ...env } : process.env, stdio: ['pipe', 'pipe', 'ignore'] });
|
|
7
|
+
const pending = new Map();
|
|
8
|
+
let sequence = 0;
|
|
9
|
+
let closed = false;
|
|
10
|
+
const lines = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
11
|
+
lines.on('line', (line) => {
|
|
12
|
+
let message;
|
|
13
|
+
try { message = JSON.parse(line); } catch { return; }
|
|
14
|
+
if (message.id !== undefined && pending.has(message.id)) { const { resolve } = pending.get(message.id); pending.delete(message.id); resolve(message); }
|
|
15
|
+
else { const reply = onMessage?.(message); if (reply) child.stdin.write(`${JSON.stringify(reply)}\n`); }
|
|
16
|
+
});
|
|
17
|
+
const fail = (error) => { closed = true; for (const { reject } of pending.values()) reject(error); pending.clear(); };
|
|
18
|
+
child.on('error', fail);
|
|
19
|
+
child.on('close', (code) => fail(new Error(`downstream MCP exited with code ${code}`)));
|
|
20
|
+
return {
|
|
21
|
+
request(message, { signal, notify } = {}) {
|
|
22
|
+
if (closed) return Promise.reject(new Error('downstream MCP transport is closed'));
|
|
23
|
+
const id = `sando:${++sequence}`;
|
|
24
|
+
const request = { ...message, id };
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const abort = () => { pending.delete(id); this.notify({ jsonrpc: '2.0', method: 'notifications/cancelled', params: { requestId: id, reason: 'cancelled' } }); reject(Object.assign(new Error('downstream request cancelled'), { code: 'CANCELLED' })); };
|
|
27
|
+
if (signal?.aborted) return abort();
|
|
28
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
29
|
+
pending.set(id, { resolve: (value) => { signal?.removeEventListener('abort', abort); resolve(value); }, reject });
|
|
30
|
+
child.stdin.write(`${JSON.stringify(request)}\n`, (error) => { if (error) reject(error); });
|
|
31
|
+
void notify;
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
notify(message) { if (!closed) child.stdin.write(`${JSON.stringify({ ...message, id: undefined })}\n`); },
|
|
35
|
+
close() { if (!closed) { closed = true; child.kill(); lines.close(); } },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createConfiguredMcpServers(config) {
|
|
40
|
+
if (!Array.isArray(config?.servers)) throw new TypeError('gateway servers must be an array');
|
|
41
|
+
return config.servers.map((server) => ({ ...server, connect: ({ onMessage }) => spawnMcpTransport({ ...server, onMessage }) }));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function startLazyMcpGatewayStdio({ gateway, input = process.stdin, output = process.stdout } = {}) {
|
|
45
|
+
if (!gateway) throw new TypeError('gateway is required');
|
|
46
|
+
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
47
|
+
let queue = Promise.resolve();
|
|
48
|
+
input.resume();
|
|
49
|
+
lines.on('line', (line) => {
|
|
50
|
+
queue = queue.then(() => processLine(line));
|
|
51
|
+
});
|
|
52
|
+
async function processLine(line) {
|
|
53
|
+
let message;
|
|
54
|
+
try { message = JSON.parse(line); } catch { output.write(`${JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } })}\n`); return; }
|
|
55
|
+
try { const result = await gateway.handle(message); if (result) output.write(`${JSON.stringify(result)}\n`); }
|
|
56
|
+
catch (error) { output.write(`${JSON.stringify({ jsonrpc: '2.0', id: message?.id ?? null, error: { code: error.code ?? -32000, message: error.message || 'Gateway failure' } })}\n`); }
|
|
57
|
+
}
|
|
58
|
+
return lines;
|
|
59
|
+
}
|