@interlok/cli 0.0.0-stage → 1.3.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.
@@ -0,0 +1,513 @@
1
+ /**
2
+ * Interlok proof bundle verifier (.ilproof, format "interlok.proof/1").
3
+ *
4
+ * Runs unchanged in a browser (interlok.io/verify) and in Node (`interlok verify`): WebCrypto for
5
+ * ECDSA P-256 and SHA-256, fflate for the zip. Imports nothing else, so the CLI vendors this file
6
+ * byte-for-byte and a test fails the build if the two copies drift. Nothing is uploaded anywhere.
7
+ *
8
+ * Seven checks, each pass / fail / skipped with record-level detail:
9
+ * 1 manifest DSSE signature over manifest.json by an org key valid at generated_at
10
+ * 2 files every file's SHA-256 matches the manifest
11
+ * 3 records every record's payload hash, entry hash and signature; statements match records
12
+ * 4 continuity seq is contiguous and prev_hash links are unbroken from chain_start_prev to chain_head
13
+ * 5 anchors every record has a Merkle inclusion proof to a signed anchor
14
+ * 6 public the bundle's anchor roots match the publicly published anchors (online, optional)
15
+ * 7 keys the org keys used are listed in the customer's own published key file (optional)
16
+ */
17
+ import { unzipSync, strFromU8 } from "fflate";
18
+ export const PROOF_FORMAT = "interlok.proof/1";
19
+ export const DSSE_PAYLOAD_TYPE = "application/vnd.interlok.proof-manifest+json";
20
+ export const STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
21
+ export const PREDICATE_TYPE = "https://interlok.io/attestation/determination/v1";
22
+ /* ---------------- primitives (identical semantics to src/lib/ledger) ---------------- */
23
+ const enc = new TextEncoder();
24
+ const subtle = () => globalThis.crypto.subtle;
25
+ const buf = (u) => u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
26
+ /** Canonical JSON: sorted keys, no whitespace, undefined dropped. Same as the ledger's. */
27
+ export function canonicalize(value) {
28
+ if (value === null || typeof value === "boolean")
29
+ return JSON.stringify(value);
30
+ if (typeof value === "number") {
31
+ if (!Number.isFinite(value))
32
+ throw new Error("non-finite number");
33
+ return JSON.stringify(value);
34
+ }
35
+ if (typeof value === "string")
36
+ return JSON.stringify(value);
37
+ if (typeof value === "undefined")
38
+ throw new Error("undefined not allowed in canonical JSON");
39
+ if (Array.isArray(value))
40
+ return "[" + value.map(canonicalize).join(",") + "]";
41
+ if (typeof value === "object") {
42
+ const obj = value;
43
+ const keys = Object.keys(obj)
44
+ .filter((k) => obj[k] !== undefined)
45
+ .sort();
46
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k])).join(",") + "}";
47
+ }
48
+ throw new Error(`unsupported type ${typeof value}`);
49
+ }
50
+ export async function sha256Hex(data) {
51
+ const bytes = typeof data === "string" ? enc.encode(data) : data;
52
+ const d = new Uint8Array(await subtle().digest("SHA-256", buf(bytes)));
53
+ return Array.from(d, (b) => b.toString(16).padStart(2, "0")).join("");
54
+ }
55
+ export function fromB64url(s) {
56
+ const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - (s.length % 4));
57
+ const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + pad);
58
+ const out = new Uint8Array(bin.length);
59
+ for (let i = 0; i < bin.length; i++)
60
+ out[i] = bin.charCodeAt(i);
61
+ return out;
62
+ }
63
+ export function toB64(u) {
64
+ let bin = "";
65
+ for (const x of u)
66
+ bin += String.fromCharCode(x);
67
+ return btoa(bin);
68
+ }
69
+ export function fromB64(s) {
70
+ const bin = atob(s);
71
+ const out = new Uint8Array(bin.length);
72
+ for (let i = 0; i < bin.length; i++)
73
+ out[i] = bin.charCodeAt(i);
74
+ return out;
75
+ }
76
+ const keyCache = new Map();
77
+ function importPub(jwk) {
78
+ const k = `${jwk.x}.${jwk.y}`;
79
+ let p = keyCache.get(k);
80
+ if (!p) {
81
+ p = subtle().importKey("jwk", { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y, ext: true }, { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"]);
82
+ keyCache.set(k, p);
83
+ }
84
+ return p;
85
+ }
86
+ export async function verifySig(jwk, sigB64url, data) {
87
+ try {
88
+ const bytes = typeof data === "string" ? enc.encode(data) : data;
89
+ return await subtle().verify({ name: "ECDSA", hash: "SHA-256" }, await importPub(jwk), buf(fromB64url(sigB64url)), buf(bytes));
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ }
95
+ /** DSSE pre-authentication encoding: "DSSEv1 <len(type)> <type> <len(body)> <body>". */
96
+ export function pae(payloadType, body) {
97
+ const head = enc.encode(`DSSEv1 ${enc.encode(payloadType).length} ${payloadType} ${body.length} `);
98
+ const out = new Uint8Array(head.length + body.length);
99
+ out.set(head, 0);
100
+ out.set(body, head.length);
101
+ return out;
102
+ }
103
+ export async function verifyMerkle(leaf, proof, root) {
104
+ let h = leaf;
105
+ for (const s of proof)
106
+ h = await sha256Hex(s.position === "right" ? h + s.hash : s.hash + h);
107
+ return h === root;
108
+ }
109
+ /* ---------------- ledger record v2: salted field commitments ---------------- */
110
+ /**
111
+ * A v2 record's payload_hash is a Merkle root over one commitment per leaf field,
112
+ * sha256(salt | path | JCS(value)), with paths as RFC 6901 JSON pointers sorted by code point.
113
+ * Revealing a field means giving its value and salt; hiding it means giving only its commitment.
114
+ * Every signature still verifies either way. Arrays and empty objects are leaves.
115
+ */
116
+ const esc6901 = (k) => k.replace(/~/g, "~0").replace(/\//g, "~1");
117
+ const unesc6901 = (k) => k.replace(/~1/g, "/").replace(/~0/g, "~");
118
+ export function flattenPayload(value, path = "") {
119
+ if (value !== null &&
120
+ typeof value === "object" &&
121
+ !Array.isArray(value) &&
122
+ Object.keys(value).length) {
123
+ const out = [];
124
+ for (const k of Object.keys(value).sort()) {
125
+ const v = value[k];
126
+ if (v === undefined)
127
+ continue;
128
+ out.push(...flattenPayload(v, `${path}/${esc6901(k)}`));
129
+ }
130
+ return out;
131
+ }
132
+ return [{ path: path || "/", value }];
133
+ }
134
+ export const leafHash = (salt, path, value) => sha256Hex(`${salt}|${path}|${canonicalize(value)}`);
135
+ export async function merkleRootHex(leaves) {
136
+ if (!leaves.length)
137
+ return sha256Hex("");
138
+ let level = leaves.slice();
139
+ while (level.length > 1) {
140
+ if (level.length % 2)
141
+ level.push(level[level.length - 1]);
142
+ const next = [];
143
+ for (let i = 0; i < level.length; i += 2)
144
+ next.push(await sha256Hex(level[i] + level[i + 1]));
145
+ level = next;
146
+ }
147
+ return level[0];
148
+ }
149
+ /** The root over a record's fields, revealed or committed. */
150
+ export async function rootFromFields(fields) {
151
+ const paths = Object.keys(fields).sort();
152
+ const leaves = [];
153
+ for (const p of paths) {
154
+ const f = fields[p];
155
+ leaves.push("c" in f ? f.c : await leafHash(f.s, p, f.v));
156
+ }
157
+ return merkleRootHex(leaves);
158
+ }
159
+ /** The root for a full payload and its salts (one salt per leaf path). */
160
+ export async function payloadRootV2(payload, salts) {
161
+ const fields = {};
162
+ for (const { path, value } of flattenPayload(payload)) {
163
+ const s = salts[path];
164
+ if (typeof s !== "string")
165
+ throw new Error(`missing salt for ${path}`);
166
+ fields[path] = { v: value, s };
167
+ }
168
+ return rootFromFields(fields);
169
+ }
170
+ /** What an auditor sees: revealed values in place, hidden ones as "[redacted]". */
171
+ export function viewFromFields(fields) {
172
+ const root = {};
173
+ for (const p of Object.keys(fields).sort()) {
174
+ const f = fields[p];
175
+ const segs = p.split("/").slice(1).map(unesc6901);
176
+ let cur = root;
177
+ for (let i = 0; i < segs.length - 1; i++)
178
+ cur = (cur[segs[i]] ??= {});
179
+ cur[segs[segs.length - 1]] = "c" in f ? "[redacted]" : f.v;
180
+ }
181
+ return root;
182
+ }
183
+ /** In-scope record → in-toto Statement v1. Used by the builder and re-derived by the verifier. */
184
+ export function statementFor(org, r) {
185
+ const p = (r.payload ?? {});
186
+ return {
187
+ _type: STATEMENT_TYPE,
188
+ subject: [{ name: `interlok:ledger:${org}:${r.seq}`, digest: { sha256: r.entry_hash } }],
189
+ predicateType: PREDICATE_TYPE,
190
+ predicate: {
191
+ kind: p.kind ?? null,
192
+ ts: p.ts ?? null,
193
+ actor: p.actor ?? null,
194
+ subject: p.subject ?? null,
195
+ data: p.data ?? null,
196
+ ledger: { seq: r.seq, entry_hash: r.entry_hash, signer_kid: r.signer_kid },
197
+ },
198
+ };
199
+ }
200
+ /* ---------------- the verifier ---------------- */
201
+ const CHECK_NAMES = [
202
+ "",
203
+ "Manifest signature",
204
+ "File digests",
205
+ "Record signatures",
206
+ "Chain continuity",
207
+ "Anchor inclusion",
208
+ "Public anchors",
209
+ "Customer key file",
210
+ ];
211
+ const MAX_LISTED = 50;
212
+ const MAX_BUNDLE_BYTES = 256 * 1024 * 1024;
213
+ const MAX_UNPACKED_BYTES = 1024 * 1024 * 1024;
214
+ function check(id) {
215
+ return { id, name: CHECK_NAMES[id], status: "pass", detail: "", records: [] };
216
+ }
217
+ function fail(c, seq, problem) {
218
+ c.status = "fail";
219
+ if (c.records.length < MAX_LISTED)
220
+ c.records.push({ seq, problem });
221
+ }
222
+ function keyValidAt(k, iso, graceMs) {
223
+ if (!iso)
224
+ return true;
225
+ const t = Date.parse(iso);
226
+ if (Number.isNaN(t))
227
+ return true;
228
+ if (k.valid_from && t < Date.parse(k.valid_from) - graceMs)
229
+ return false;
230
+ if (k.valid_to && t > Date.parse(k.valid_to) + graceMs)
231
+ return false;
232
+ return true;
233
+ }
234
+ const parseJsonl = (s) => s
235
+ .split("\n")
236
+ .filter((l) => l.trim())
237
+ .map((l) => JSON.parse(l));
238
+ export async function verifyProofBundle(zip, opts = {}) {
239
+ const grace = opts.keyGraceMs ?? 5 * 60_000;
240
+ const checks = [1, 2, 3, 4, 5, 6, 7].map(check);
241
+ const [c1, c2, c3, c4, c5, c6, c7] = checks;
242
+ const result = {
243
+ ok: false,
244
+ format: null,
245
+ bundle_id: null,
246
+ org: null,
247
+ record_count: 0,
248
+ checks,
249
+ };
250
+ const abort = (msg) => {
251
+ for (const c of checks) {
252
+ c.status = c.id <= 5 ? "fail" : "skipped";
253
+ c.detail = c.id <= 5 ? msg : "not checked";
254
+ }
255
+ return result;
256
+ };
257
+ let files;
258
+ if (zip.length > MAX_BUNDLE_BYTES)
259
+ return abort("bundle is larger than the 256 MB this verifier accepts");
260
+ try {
261
+ // A zip bomb would expand to gigabytes in memory: cap what we are willing to unpack.
262
+ let total = 0;
263
+ files = unzipSync(zip, {
264
+ filter: (f) => {
265
+ total += f.originalSize;
266
+ if (total > MAX_UNPACKED_BYTES)
267
+ throw new Error("too large");
268
+ return true;
269
+ },
270
+ });
271
+ }
272
+ catch {
273
+ return abort("not a readable .ilproof (zip) file, or it unpacks to more than 1 GB");
274
+ }
275
+ const text = (n) => (files[n] ? strFromU8(files[n]) : null);
276
+ let manifest;
277
+ let envelope;
278
+ let keys;
279
+ let chain;
280
+ let statements;
281
+ let proofs;
282
+ let anchors;
283
+ try {
284
+ manifest = JSON.parse(text("manifest.json") ?? "null");
285
+ envelope = JSON.parse(text("manifest.dsse.json") ?? "null");
286
+ keys = JSON.parse(text("keys.json") ?? "null");
287
+ chain = parseJsonl(text("chain.jsonl") ?? "");
288
+ statements = parseJsonl(text("statements.jsonl") ?? "");
289
+ proofs = JSON.parse(text("proofs/merkle.json") ?? "{}");
290
+ anchors = JSON.parse(text("anchors.json") ?? "[]");
291
+ }
292
+ catch {
293
+ return abort("a bundle file is not valid JSON");
294
+ }
295
+ if (!manifest || !envelope || !keys)
296
+ return abort("manifest, signature or key file is missing");
297
+ result.format = manifest.format ?? null;
298
+ result.bundle_id = manifest.bundle_id ?? null;
299
+ result.org = manifest.org ?? null;
300
+ result.record_count = chain.length;
301
+ if (manifest.format !== PROOF_FORMAT)
302
+ return abort(`unsupported format ${manifest.format}`);
303
+ const orgKeys = keys.org ?? {};
304
+ const orgId = manifest.org?.id ?? "";
305
+ // 1 · manifest signature: the DSSE payload must BE manifest.json, and an org key valid at
306
+ // generated_at must have signed it.
307
+ {
308
+ const body = files["manifest.json"];
309
+ let payloadOk = false;
310
+ try {
311
+ payloadOk = envelope.payloadType === DSSE_PAYLOAD_TYPE && toB64(body) === envelope.payload;
312
+ }
313
+ catch {
314
+ payloadOk = false;
315
+ }
316
+ if (!payloadOk)
317
+ fail(c1, null, "the signed payload is not this manifest.json");
318
+ const signed = [];
319
+ for (const s of envelope.signatures ?? []) {
320
+ const k = orgKeys[s.keyid];
321
+ if (!k)
322
+ continue;
323
+ if (!keyValidAt(k, manifest.generated_at, grace)) {
324
+ fail(c1, null, `key ${s.keyid} was not valid at ${manifest.generated_at}`);
325
+ continue;
326
+ }
327
+ if (await verifySig(k.jwk, s.sig, pae(DSSE_PAYLOAD_TYPE, body)))
328
+ signed.push(s.keyid);
329
+ }
330
+ if (!signed.length)
331
+ fail(c1, null, "no valid signature by an org key");
332
+ c1.detail =
333
+ c1.status === "pass"
334
+ ? `signed by ${signed.join(", ")}; generated ${manifest.generated_at} by ${manifest.generated_by?.display ?? manifest.generated_by?.id ?? "unknown"}`
335
+ : "the manifest cannot be trusted";
336
+ }
337
+ // 2 · file digests: every listed file matches, and nothing unlisted hides in the zip.
338
+ {
339
+ const listed = manifest.files ?? {};
340
+ for (const [name, meta] of Object.entries(listed)) {
341
+ if (!files[name])
342
+ fail(c2, null, `${name} is listed but missing`);
343
+ else if ((await sha256Hex(files[name])) !== meta.sha256)
344
+ fail(c2, null, `${name} was modified`);
345
+ }
346
+ for (const name of Object.keys(files)) {
347
+ if (name.endsWith("/"))
348
+ continue;
349
+ if (name === "manifest.json" || name === "manifest.dsse.json")
350
+ continue;
351
+ if (!listed[name])
352
+ fail(c2, null, `${name} is not in the manifest`);
353
+ }
354
+ c2.detail =
355
+ c2.status === "pass"
356
+ ? `${Object.keys(listed).length} files match`
357
+ : "files differ from what was signed";
358
+ }
359
+ // 3 · records: payload hash (when revealed), entry hash, signature by a key valid at the time.
360
+ const bySeq = new Map();
361
+ {
362
+ let revealed = 0;
363
+ for (const r of chain) {
364
+ if (r.fields) {
365
+ // v2: the field reveals and commitments must hash to the signed payload_hash.
366
+ revealed++;
367
+ let root = "";
368
+ try {
369
+ root = await rootFromFields(r.fields);
370
+ }
371
+ catch {
372
+ root = "";
373
+ }
374
+ if (root !== r.payload_hash) {
375
+ fail(c3, r.seq, "fields do not match the signed commitment");
376
+ continue;
377
+ }
378
+ r.payload = viewFromFields(r.fields);
379
+ }
380
+ bySeq.set(r.seq, r);
381
+ if (r.payload !== undefined && !r.fields) {
382
+ revealed++;
383
+ if ((await sha256Hex(canonicalize(r.payload))) !== r.payload_hash) {
384
+ fail(c3, r.seq, "content does not match its signed hash");
385
+ continue;
386
+ }
387
+ }
388
+ if ((await sha256Hex(`${r.seq}|${r.prev_hash}|${r.payload_hash}`)) !== r.entry_hash) {
389
+ fail(c3, r.seq, "entry hash mismatch");
390
+ continue;
391
+ }
392
+ const k = orgKeys[r.signer_kid];
393
+ if (!k) {
394
+ fail(c3, r.seq, `unknown signing key ${r.signer_kid}`);
395
+ continue;
396
+ }
397
+ if (!keyValidAt(k, r.payload?.ts, grace))
398
+ fail(c3, r.seq, "key was not valid at the record's time");
399
+ else if (!(await verifySig(k.jwk, r.signature, r.entry_hash)))
400
+ fail(c3, r.seq, "bad signature");
401
+ }
402
+ // Statements must be exactly the in-toto form of the revealed in-scope records.
403
+ for (const s of statements) {
404
+ const seq = Number(String(s?.subject?.[0]?.name ?? "")
405
+ .split(":")
406
+ .pop());
407
+ const r = bySeq.get(seq);
408
+ if (!r || r.payload === undefined) {
409
+ fail(c3, Number.isFinite(seq) ? seq : null, "statement has no revealed record");
410
+ continue;
411
+ }
412
+ if (canonicalize(statementFor(orgId, r)) !== canonicalize(s))
413
+ fail(c3, seq, "statement does not match its record");
414
+ }
415
+ c3.detail =
416
+ c3.status === "pass"
417
+ ? `${chain.length} records verified (${revealed} revealed, ${chain.length - revealed} hash-only); ${statements.length} statements`
418
+ : "one or more records fail";
419
+ }
420
+ // 4 · continuity: contiguous seq, unbroken prev_hash links, the head the manifest names.
421
+ {
422
+ const sorted = [...chain].sort((a, b) => a.seq - b.seq);
423
+ if (sorted.length !== chain.length || chain.some((r, i) => r.seq !== sorted[i].seq))
424
+ fail(c4, null, "records are out of order");
425
+ if (chain.length !== manifest.record_count)
426
+ fail(c4, null, `manifest says ${manifest.record_count} records, bundle has ${chain.length}`);
427
+ let prev = manifest.chain_start_prev;
428
+ let expect = manifest.first_seq;
429
+ for (const r of sorted) {
430
+ if (r.seq !== expect)
431
+ fail(c4, r.seq, `expected #${expect}: a record is missing`);
432
+ if (r.prev_hash !== prev)
433
+ fail(c4, r.seq, "link to the previous record is broken");
434
+ prev = r.entry_hash;
435
+ expect = r.seq + 1;
436
+ }
437
+ if (sorted.length && sorted[sorted.length - 1].seq !== manifest.last_seq)
438
+ fail(c4, null, "the last record is not the one the manifest names");
439
+ if (prev !== manifest.chain_head)
440
+ fail(c4, null, "chain head does not match the manifest");
441
+ c4.detail =
442
+ c4.status === "pass"
443
+ ? `#${manifest.first_seq}–#${manifest.last_seq}, no gaps, head ${String(manifest.chain_head).slice(0, 12)}`
444
+ : "the chain is incomplete or altered";
445
+ }
446
+ // 5 · anchors: each anchor is signed by the anchor key; each record proves into one.
447
+ {
448
+ const byId = new Map(anchors.map((a) => [a.id, a]));
449
+ for (const a of anchors) {
450
+ const { anchor_hash, signature, signer_kid, ...body } = a.anchor;
451
+ const pub = keys.anchor?.[signer_kid];
452
+ if (!pub)
453
+ fail(c5, null, `anchor ${a.id}: unknown anchor key`);
454
+ else if ((await sha256Hex(canonicalize(body))) !== anchor_hash)
455
+ fail(c5, null, `anchor ${a.id}: altered`);
456
+ else if (!(await verifySig(pub, signature, anchor_hash)))
457
+ fail(c5, null, `anchor ${a.id}: bad signature`);
458
+ else if (a.anchor.merkle_root !== a.merkle_root)
459
+ fail(c5, null, `anchor ${a.id}: root mismatch`);
460
+ }
461
+ for (const r of chain) {
462
+ const p = proofs[String(r.seq)];
463
+ const a = p ? byId.get(p.anchor_id) : undefined;
464
+ if (!p || !a) {
465
+ fail(c5, r.seq, "not covered by an anchor");
466
+ continue;
467
+ }
468
+ if (r.seq < a.from_seq || r.seq > a.to_seq)
469
+ fail(c5, r.seq, "proof names the wrong anchor");
470
+ else if (!(await verifyMerkle(r.entry_hash, p.proof, a.merkle_root)))
471
+ fail(c5, r.seq, "inclusion proof does not reach the anchor root");
472
+ }
473
+ c5.detail =
474
+ c5.status === "pass"
475
+ ? `${chain.length} records proven into ${anchors.length} signed anchors`
476
+ : "anchoring fails";
477
+ }
478
+ // 6 · public anchors (online)
479
+ if (!opts.publicAnchors) {
480
+ c6.status = "skipped";
481
+ c6.detail = "not checked (offline)";
482
+ }
483
+ else {
484
+ const pub = new Set(opts.publicAnchors.map((a) => a.merkle_root));
485
+ for (const a of anchors)
486
+ if (!pub.has(a.merkle_root))
487
+ fail(c6, null, `anchor ${a.id} is not in the public feed`);
488
+ c6.detail =
489
+ c6.status === "pass"
490
+ ? `${anchors.length} anchors match the public feed`
491
+ : "an anchor was not published";
492
+ }
493
+ // 7 · customer key file (optional)
494
+ if (!opts.publishedKids) {
495
+ c7.status = "skipped";
496
+ c7.detail = "no customer key file supplied";
497
+ }
498
+ else {
499
+ const listed = new Set(opts.publishedKids);
500
+ const used = new Set(chain.map((r) => r.signer_kid));
501
+ for (const s of envelope.signatures ?? [])
502
+ used.add(s.keyid);
503
+ for (const kid of used)
504
+ if (!listed.has(kid))
505
+ fail(c7, null, `key ${kid} is not in the customer's file`);
506
+ c7.detail =
507
+ c7.status === "pass"
508
+ ? `${used.size} keys listed by the customer`
509
+ : "a key is not the customer's";
510
+ }
511
+ result.ok = checks.every((c) => c.status !== "fail");
512
+ return result;
513
+ }
@@ -0,0 +1,69 @@
1
+ import { type IncomingMessage, type ServerResponse } from "node:http";
2
+ /**
3
+ * `interlok proxy`: sits between any MCP client and any MCP server. Every `tools/call` is either
4
+ * recorded into a Shadow run (and always forwarded) or asked of the Gate (and forwarded only when
5
+ * allowed or released by a person). Everything else passes through untouched.
6
+ *
7
+ * stdio: interlok proxy --name github -- npx -y @modelcontextprotocol/server-github
8
+ * http: interlok proxy --name crm --upstream https://mcp.example.com/mcp --port 8787
9
+ *
10
+ * Mode: Shadow when INTERLOK_SHADOW_RUN (or --shadow <run>) is set, Gate otherwise. The proxy never
11
+ * reads or forwards Interlok credentials to the MCP server, and writes only protocol messages to
12
+ * stdout; everything it has to say goes to stderr.
13
+ */
14
+ export type JsonRpc = {
15
+ jsonrpc: "2.0";
16
+ id?: string | number | null;
17
+ method?: string;
18
+ params?: any;
19
+ result?: any;
20
+ error?: any;
21
+ };
22
+ export interface Decision {
23
+ /** forward the call to the server, or answer the client with `message` instead */
24
+ forward: boolean;
25
+ message?: string;
26
+ }
27
+ export interface ProxyConfig {
28
+ api: string;
29
+ key: string;
30
+ agent: string;
31
+ connection: string;
32
+ shadowRun?: string;
33
+ environment?: "production" | "staging" | "dev";
34
+ /** How long to wait for a person on a held call before answering "still held". */
35
+ waitSeconds: number;
36
+ /** When Interlok cannot be reached: refuse (default) or let the call through. */
37
+ failOpen: boolean;
38
+ fetch?: typeof fetch;
39
+ log?: (line: string) => void;
40
+ sleep?: (ms: number) => Promise<void>;
41
+ }
42
+ export declare const isToolCall: (m: JsonRpc) => boolean;
43
+ /** A tools/call answer the model can read: MCP reports a refused call as a result with isError. */
44
+ export declare function refusal(id: JsonRpc["id"], text: string): JsonRpc;
45
+ /** MCP `_meta` key an agent may use to say what it is trying to do (intent, alternatives, confidence). */
46
+ export declare const DECISION_META_KEY = "interlok/decision";
47
+ /**
48
+ * Take the agent's decision record off the call. It is for Interlok, so it never reaches the MCP
49
+ * server: the key is deleted from the message in place, before the message can be forwarded.
50
+ */
51
+ export declare function takeDecisionRecord(msg: JsonRpc): unknown;
52
+ /** One line for the model: why, and what to do next. */
53
+ export declare function guidanceText(g: any): string;
54
+ /** Decide one tools/call. Shadow never blocks; Gate forwards only on allow or a human release. */
55
+ export declare function decider(cfg: ProxyConfig): (msg: JsonRpc) => Promise<Decision>;
56
+ /** Register the server's tools once its tools/list answer comes back: pins descriptions for drift. */
57
+ export declare function registerTools(cfg: ProxyConfig, tools: any[], transport: string, url?: string): Promise<void>;
58
+ /**
59
+ * The message pump shared by both transports. `toServer` / `toClient` write one message each way.
60
+ * Tool calls are decided concurrently so a held call never blocks pings, cancellations or other calls.
61
+ */
62
+ export declare function interceptor(cfg: ProxyConfig, toServer: (m: JsonRpc) => void, toClient: (m: JsonRpc) => void, transport: string): {
63
+ fromClient(m: JsonRpc): void;
64
+ fromServer(m: JsonRpc): void;
65
+ };
66
+ export declare function proxyStdio(cfg: ProxyConfig, command: string, args: string[]): Promise<number>;
67
+ /** Streamable HTTP: the client talks to http://127.0.0.1:<port>/mcp; the proxy talks to --upstream. */
68
+ export declare function proxyHttp(cfg: ProxyConfig, upstream: string, port: number, host?: string): Promise<import("http").Server<typeof IncomingMessage, typeof ServerResponse>>;
69
+ export declare function proxyCommand(args: string[], value: (n: string) => string | undefined, has: (n: string) => boolean): Promise<undefined>;