@mikeargento/bitgraph-mcp 0.5.1 → 0.6.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/LICENSE +1 -1
- package/README.md +1 -1
- package/dist/__tests__/carrier.test.d.ts +2 -0
- package/dist/__tests__/carrier.test.d.ts.map +1 -0
- package/dist/__tests__/carrier.test.js +243 -0
- package/dist/__tests__/carrier.test.js.map +1 -0
- package/dist/__tests__/encoding.test.js +1 -1
- package/dist/__tests__/encoding.test.js.map +1 -1
- package/dist/__tests__/format.test.js +6 -6
- package/dist/__tests__/format.test.js.map +1 -1
- package/dist/__tests__/scan.test.js +1 -1
- package/dist/__tests__/scan.test.js.map +1 -1
- package/dist/__tests__/tools.test.js +8 -7
- package/dist/__tests__/tools.test.js.map +1 -1
- package/dist/api.js +1 -1
- package/dist/api.js.map +1 -1
- package/dist/carrier-io.d.ts +52 -0
- package/dist/carrier-io.d.ts.map +1 -0
- package/dist/carrier-io.js +91 -0
- package/dist/carrier-io.js.map +1 -0
- package/dist/encoding.js +1 -1
- package/dist/encoding.js.map +1 -1
- package/dist/format.d.ts +17 -3
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +36 -15
- package/dist/format.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/scan.d.ts +4 -0
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +44 -1
- package/dist/scan.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +151 -29
- package/dist/server.js.map +1 -1
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +2 -2
- package/dist/task.js.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +13 -5
- package/src/__tests__/carrier.test.ts +271 -0
- package/src/__tests__/encoding.test.ts +1 -1
- package/src/__tests__/format.test.ts +6 -6
- package/src/__tests__/scan.test.ts +1 -1
- package/src/__tests__/tools.test.ts +8 -7
- package/src/api.ts +1 -1
- package/src/carrier-io.ts +120 -0
- package/src/encoding.ts +1 -1
- package/src/format.ts +56 -19
- package/src/index.ts +1 -1
- package/src/scan.ts +48 -1
- package/src/server.ts +169 -34
- package/src/task.ts +2 -2
- package/src/types.ts +1 -1
package/src/server.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c) 2024-2026
|
|
1
|
+
// Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @mikeargento/bitgraph-mcp: tool definitions.
|
|
@@ -48,11 +48,13 @@ import {
|
|
|
48
48
|
type RecordOutcome,
|
|
49
49
|
type SetOutcome,
|
|
50
50
|
} from "./format.js";
|
|
51
|
-
import { expandPaths, fusedDigestFor, scanFile, type ScannedFile } from "./scan.js";
|
|
51
|
+
import { expandPaths, fusedDigestFor, scanFile, sniffC2paBytes, type ScannedFile } from "./scan.js";
|
|
52
|
+
import { readCarrierFile, sniffCarrierTail, type CarrierWindowView } from "./carrier-io.js";
|
|
53
|
+
import { stat } from "node:fs/promises";
|
|
52
54
|
import { SLOT_TTL_SECONDS, TASK_INSTRUCTIONS, beginTask, decodeTaskToken, sealTask, writeProofBeside } from "./task.js";
|
|
53
|
-
import type { BitGraphProof } from "./types.js";
|
|
55
|
+
import type { BitGraphProof, ProofDetailResponse } from "./types.js";
|
|
54
56
|
|
|
55
|
-
export const SERVER_VERSION = "0.
|
|
57
|
+
export const SERVER_VERSION = "0.6.0";
|
|
56
58
|
|
|
57
59
|
const SCAN_CONCURRENCY = 4;
|
|
58
60
|
/** Paths per call; a directory counts once and expands to its files. */
|
|
@@ -184,6 +186,51 @@ async function fuseSetDefault(
|
|
|
184
186
|
};
|
|
185
187
|
}
|
|
186
188
|
|
|
189
|
+
/**
|
|
190
|
+
* A path is either plain bytes to scan or a BitGraphed file (bitgraph-carrier/1),
|
|
191
|
+
* decided by one 8-byte tail read. A BitGraphed file is judged OFFLINE from the
|
|
192
|
+
* proof it carries, its lookups use the digest of the committed bytes inside,
|
|
193
|
+
* and it is never minted: the envelope is not the recorded thing, the bytes
|
|
194
|
+
* inside are. That is the same structural guarantee the site's drop box gives.
|
|
195
|
+
*/
|
|
196
|
+
interface CarrierRow {
|
|
197
|
+
kind: "carrier";
|
|
198
|
+
path: string;
|
|
199
|
+
status: "ok" | "corrupt" | "too-large";
|
|
200
|
+
/** Standard base64 SHA-256 of the committed bytes inside (status "ok"). */
|
|
201
|
+
innerDigestB64: string | null;
|
|
202
|
+
view: CarrierWindowView | null;
|
|
203
|
+
/** The carried bitgraph/1 proof, for offline answers when the ledger has no row. */
|
|
204
|
+
proof: unknown | null;
|
|
205
|
+
c2pa: boolean;
|
|
206
|
+
reason: string | null;
|
|
207
|
+
}
|
|
208
|
+
type ClassifiedPath = { kind: "plain"; file: ScannedFile } | CarrierRow;
|
|
209
|
+
|
|
210
|
+
async function classifyPath(p: string): Promise<ClassifiedPath> {
|
|
211
|
+
const info = await stat(p);
|
|
212
|
+
if (info.isFile() && (await sniffCarrierTail(p, info.size))) {
|
|
213
|
+
if (info.size > MAX_LOADED_BYTES) {
|
|
214
|
+
return {
|
|
215
|
+
kind: "carrier", path: p, status: "too-large", innerDigestB64: null, view: null, proof: null, c2pa: false,
|
|
216
|
+
reason: `a BitGraphed file larger than this tool loads (${Math.round(MAX_LOADED_BYTES / (1024 * 1024))} MiB); judge it offline with: npx @mikeargento/bitgraph-audit "${p}"`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const r = await readCarrierFile(p);
|
|
220
|
+
if (r.status === "corrupt" || r.innerDigestB64 === null) {
|
|
221
|
+
return {
|
|
222
|
+
kind: "carrier", path: p, status: "corrupt", innerDigestB64: null, view: null, proof: null, c2pa: false,
|
|
223
|
+
reason: `a carrier block was found at the end of the file but is unreadable: ${r.corruptReason ?? "unspecified"}. A corrupted block, not a forgery; nothing was minted for it.`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
kind: "carrier", path: p, status: "ok", innerDigestB64: r.innerDigestB64, view: r.view,
|
|
228
|
+
proof: r.result?.payload?.proof ?? null, c2pa: r.inner !== null && sniffC2paBytes(r.inner), reason: null,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
return { kind: "plain", file: await scanFile(p) };
|
|
232
|
+
}
|
|
233
|
+
|
|
187
234
|
/** Hash the given paths (bounded concurrency). Throws before any network call. */
|
|
188
235
|
async function hashPaths(paths: readonly string[]): Promise<string[]> {
|
|
189
236
|
const failures: string[] = [];
|
|
@@ -332,8 +379,9 @@ export function buildServer(deps: ServerDeps = {}): McpServer {
|
|
|
332
379
|
},
|
|
333
380
|
{
|
|
334
381
|
instructions:
|
|
335
|
-
"BitGraph gives a file's bytes a causal position in a public
|
|
382
|
+
"BitGraph gives a file's bytes a causal position in a public sequence bracketed by Ethereum anchors. bitgraph_record makes ONE BitGraph of everything in a call, files and folders alike: a single file is fused on its own; two or more become one set under one slot, one position, every file's new fused bytes listed by digest in the committed artifact. " +
|
|
336
383
|
"Files are read on this machine and never uploaded or modified; the new bytes are virtual and never written. Recordings are permanent: only make BitGraphs of files the user asked for, and never generate content just to record it. bitgraph_check and bitgraph_get_proof are read-only. " +
|
|
384
|
+
"A BitGraphed file (one that carries its own proof, bitgraph-carrier/1) is recognized by its structure: bitgraph_check judges it offline from the proof inside and states the window, and bitgraph_record never re-mints it, because the envelope is not the recorded thing, the bytes inside are. " +
|
|
337
385
|
"To do work INSIDE a BitGraph, call bitgraph_open BEFORE starting: it returns a position and its commitment; put the commitment string into the task, seal the task with bitgraph_commit within 120 seconds, then record the outputs with bitgraph_record. The task then could not have existed before the position's floor block, and the outputs sit after it.",
|
|
338
386
|
}
|
|
339
387
|
);
|
|
@@ -343,11 +391,12 @@ export function buildServer(deps: ServerDeps = {}): McpServer {
|
|
|
343
391
|
{
|
|
344
392
|
title: "Make a BitGraph",
|
|
345
393
|
description:
|
|
346
|
-
"Make a BitGraph of files or folders. Everything in one call becomes ONE BitGraph, the way a drop on the site works: a single file is fused on its own slot; two or more files become a set under a single slot
|
|
394
|
+
"Make a BitGraph of files or folders. Everything in one call becomes ONE BitGraph, the way a drop on the site works: a single file is fused on its own slot; two or more files become a set under a single slot on BitGraph (bitgraph.ing), one position for all of them. " +
|
|
347
395
|
"On this machine each file is read once for its SHA-256 (the origin) and a hasher state; an unused slot is allocated before any new file exists; every file's new fused bytes (the original plus a registered placement carrying the slot's commitment: a 48-byte trailer for JPEG, PNG, GIF, TIFF and TIFF-based raws, BMP, WebP, WAV and AVI, a small tar container with the original first for everything else) are hashed from that state without being written or held; and for a set the canonical list of those digests (above 2,000 files, a Merkle root over it) is committed under the same slot. " +
|
|
348
396
|
"Files are never modified and never uploaded: only digests, the committed artifact and slot records leave the machine. " +
|
|
349
397
|
"Give file paths, directory paths, or both (absolute paths preferred): a directory is every regular file under it, recursively, with hidden entries and symbolic links left out. " +
|
|
350
|
-
"Files
|
|
398
|
+
"Files already on record are NOT made again by default; they come back as 'on record' with their earliest position. A file can also hold a BitGraph its holder keeps, which no lookup sees. Pass again=true to make a new BitGraph regardless. " +
|
|
399
|
+
"A BitGraphed file (bitgraph-carrier/1, a file that carries its own proof) is never minted, with or without again: its carried proof is judged offline and reported, because the envelope is not the recorded thing, the bytes inside are. " +
|
|
351
400
|
"Positions are permanent and the proof comes back to you to keep, so only BitGraph files the user asked to, and never generate content just to record it. " +
|
|
352
401
|
"Returns one outcome per file: 'fused' (for a set, its row, one of N, and the set's position and proof page; for a single file, its own position and Frame), 'on record', or 'not fused' (with the reason). Keep the proof beside the files; BitGraph does not index it. " +
|
|
353
402
|
"Use bitgraph_check instead when the user only wants to know whether files are on record.",
|
|
@@ -361,7 +410,7 @@ export function buildServer(deps: ServerDeps = {}): McpServer {
|
|
|
361
410
|
.boolean()
|
|
362
411
|
.default(false)
|
|
363
412
|
.describe(
|
|
364
|
-
"false (default): files
|
|
413
|
+
"false (default): files already on record are returned as-is, nothing made. true: put every file in the set regardless. Outcomes are per unique file content: two paths with identical bytes are one member."
|
|
365
414
|
),
|
|
366
415
|
response_format: responseFormatSchema,
|
|
367
416
|
},
|
|
@@ -393,15 +442,18 @@ export function buildServer(deps: ServerDeps = {}): McpServer {
|
|
|
393
442
|
}
|
|
394
443
|
}
|
|
395
444
|
|
|
396
|
-
// 2. The scan: one pass per file.
|
|
445
|
+
// 2. The scan: one pass per file. A BitGraphed file is set aside here,
|
|
446
|
+
// judged from the proof it carries, and never enters the mint below.
|
|
397
447
|
let scanned = 0;
|
|
398
448
|
report(0, files.length, `hashing ${files.length} files`);
|
|
399
|
-
const
|
|
400
|
-
const
|
|
449
|
+
const classified = await mapConcurrent(files, SCAN_CONCURRENCY, async (p) => {
|
|
450
|
+
const c = await classifyPath(p);
|
|
401
451
|
scanned += 1;
|
|
402
452
|
report(scanned, files.length, `hashed ${scanned} of ${files.length}`);
|
|
403
|
-
return
|
|
453
|
+
return c;
|
|
404
454
|
});
|
|
455
|
+
const scans = classified.filter((c): c is { kind: "plain"; file: ScannedFile } => c.kind === "plain").map((c) => c.file);
|
|
456
|
+
const carriers = classified.filter((c): c is CarrierRow => c.kind === "carrier");
|
|
405
457
|
|
|
406
458
|
// 3. Unique by content; the first path names the member, every path is reported.
|
|
407
459
|
const byDigest = new Map<string, { file: ScannedFile; paths: string[] }>();
|
|
@@ -412,14 +464,22 @@ export function buildServer(deps: ServerDeps = {}): McpServer {
|
|
|
412
464
|
}
|
|
413
465
|
const unique = [...byDigest.keys()];
|
|
414
466
|
|
|
415
|
-
// 4. What is on record already.
|
|
416
|
-
|
|
417
|
-
|
|
467
|
+
// 4. What is on record already. A BitGraphed file is looked up by the
|
|
468
|
+
// digest of its committed bytes, never by the envelope's.
|
|
469
|
+
report(0, 1, "checking BitGraph's copy");
|
|
470
|
+
const carrierInner = [...new Set(carriers.filter((c) => c.innerDigestB64 !== null).map((c) => c.innerDigestB64 as string))];
|
|
471
|
+
const lookups = [...new Set([...unique, ...carrierInner])];
|
|
472
|
+
const checked = lookups.length > 0 ? await batchCheck(config, lookups.map(toUrlSafeB64)) : { results: {} as Record<string, { proofs: Array<{ proof: BitGraphProof }> }> };
|
|
418
473
|
const existing = new Map<string, Array<{ proof: BitGraphProof }>>();
|
|
419
474
|
for (const d of unique) {
|
|
420
475
|
const entry = checked.results[toUrlSafeB64(d)];
|
|
421
476
|
if (entry && entry.proofs.length > 0) existing.set(d, entry.proofs);
|
|
422
477
|
}
|
|
478
|
+
const carrierLedger = new Map<string, Array<{ proof: BitGraphProof }>>();
|
|
479
|
+
for (const d of carrierInner) {
|
|
480
|
+
const entry = checked.results[toUrlSafeB64(d)];
|
|
481
|
+
if (entry && entry.proofs.length > 0) carrierLedger.set(d, entry.proofs);
|
|
482
|
+
}
|
|
423
483
|
|
|
424
484
|
// 5. The set: every fresh file (every file, with again), one call.
|
|
425
485
|
const excluded = new Map<string, string>();
|
|
@@ -494,7 +554,7 @@ export function buildServer(deps: ServerDeps = {}): McpServer {
|
|
|
494
554
|
const m = memberOf.get(digest);
|
|
495
555
|
const prior = existing.get(digest);
|
|
496
556
|
for (const path of entry.paths) {
|
|
497
|
-
const base = { path, digest: toUrlSafeB64(digest) };
|
|
557
|
+
const base = { path, digest: toUrlSafeB64(digest), ...(entry.file.c2pa ? { c2pa: true as const } : {}) };
|
|
498
558
|
if (solo !== null && solo.file.digestB64 === digest) {
|
|
499
559
|
const { counter, epoch } = positionOf(solo.proof);
|
|
500
560
|
outcomes.push({
|
|
@@ -555,8 +615,36 @@ export function buildServer(deps: ServerDeps = {}): McpServer {
|
|
|
555
615
|
}
|
|
556
616
|
}
|
|
557
617
|
}
|
|
618
|
+
// 7b. BitGraphed files: judged from the proof inside, never minted.
|
|
619
|
+
for (const c of carriers) {
|
|
620
|
+
if (c.status !== "ok" || c.innerDigestB64 === null) {
|
|
621
|
+
outcomes.push({
|
|
622
|
+
path: c.path, digest: "", outcome: "not fused", artifact_digest: null, placement: null,
|
|
623
|
+
counter: null, epoch: null, member: null, member_count: null, total_positions: 0, proof_url: null,
|
|
624
|
+
error: c.reason ?? "unreadable BitGraphed file",
|
|
625
|
+
});
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const inner = c.innerDigestB64;
|
|
629
|
+
const rows = carrierLedger.get(inner);
|
|
630
|
+
const extras = { ...(c.view ? { carrier: c.view } : {}), ...(c.c2pa ? { c2pa: true as const } : {}) };
|
|
631
|
+
if (rows !== undefined && rows.length > 0) {
|
|
632
|
+
const first = rows[0] as { proof: BitGraphProof; member?: { index: number; count: number } };
|
|
633
|
+
const { counter, epoch } = positionOf(first.proof);
|
|
634
|
+
outcomes.push({
|
|
635
|
+
path: c.path, digest: toUrlSafeB64(inner), outcome: "on record", artifact_digest: null, placement: null,
|
|
636
|
+
counter, epoch, member: first.member ? first.member.index + 1 : null, member_count: first.member ? first.member.count : null,
|
|
637
|
+
total_positions: rows.length, proof_url: proofUrl(config.baseUrl, inner), ...extras,
|
|
638
|
+
});
|
|
639
|
+
} else {
|
|
640
|
+
outcomes.push({
|
|
641
|
+
path: c.path, digest: toUrlSafeB64(inner), outcome: "carried", artifact_digest: null, placement: null,
|
|
642
|
+
counter: null, epoch: null, member: null, member_count: null, total_positions: 0, proof_url: null, ...extras,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
558
646
|
// Rows that need reading come first, so a cap drops fused rows, which all share one position.
|
|
559
|
-
const order = { "not fused": 0, "on record": 1,
|
|
647
|
+
const order = { "not fused": 0, "on record": 1, carried: 2, fused: 3 } as const;
|
|
560
648
|
outcomes.sort((a, b) => order[a.outcome] - order[b.outcome]);
|
|
561
649
|
const listed = outcomes.slice(0, ROW_CAP);
|
|
562
650
|
const omitted = outcomes.length - listed.length;
|
|
@@ -565,6 +653,7 @@ export function buildServer(deps: ServerDeps = {}): McpServer {
|
|
|
565
653
|
directories: expanded.directories,
|
|
566
654
|
fused: outcomes.filter((o) => o.outcome === "fused").length,
|
|
567
655
|
on_record: outcomes.filter((o) => o.outcome === "on record").length,
|
|
656
|
+
carried: outcomes.filter((o) => o.outcome === "carried").length,
|
|
568
657
|
not_fused: outcomes.filter((o) => o.outcome === "not fused").length,
|
|
569
658
|
};
|
|
570
659
|
const structured = {
|
|
@@ -664,7 +753,7 @@ export function buildServer(deps: ServerDeps = {}): McpServer {
|
|
|
664
753
|
proofNote = `The proof could not be written beside the file (${errorText(err)}); save the JSON below whole and unedited, every field. `;
|
|
665
754
|
}
|
|
666
755
|
} else {
|
|
667
|
-
proofNote = "Save the JSON below as a file beside the task bytes, whole and unedited, every field: a proof missing slotAllocation or
|
|
756
|
+
proofNote = "Save the JSON below as a file beside the task bytes, whole and unedited, every field, including environment.attestation.reportB64, the long base64 string, copied exactly: a proof missing slotAllocation, environment, or the attestation cannot be verified. ";
|
|
668
757
|
}
|
|
669
758
|
const structured = { outcome: "sealed", slot_counter: state.slot.counter, counter, epoch: epoch ? toUrlSafeB64(epoch) : null, floor_block: floor, artifact_digest: toUrlSafeB64(sealed.artifactDigestB64), commitment_offsets: sealed.offsets, proof_path: proofPath, instructions: proofNote.trim(), proof: sealed.proof };
|
|
670
759
|
if (response_format === "json") return ok(JSON.stringify(structured, null, 2), structured);
|
|
@@ -687,9 +776,10 @@ export function buildServer(deps: ServerDeps = {}): McpServer {
|
|
|
687
776
|
{
|
|
688
777
|
title: "Check for BitGraphs",
|
|
689
778
|
description:
|
|
690
|
-
"Check whether files or digests are on record in
|
|
779
|
+
"Check whether files or digests are on record in BitGraph's copy, without recording anything. " +
|
|
691
780
|
`Accepts file paths and directory paths (every regular file under them, up to ${MAX_CHECK_FILES} in all; hashed locally, only digests are sent) and/or raw SHA-256 digests in standard or URL-safe base64. ` +
|
|
692
781
|
"Returns, per item: on_record (the bytes are on record, as an exact recording, as the original a new file was made from, or as a member of a set), every position by counter, and the proof page URL. " +
|
|
782
|
+
"A BitGraphed file (bitgraph-carrier/1) is recognized by structure: the proof it carries is verified OFFLINE here (signatures, floor identity, block-header witnesses) and its verdict and window are reported, and the lookup uses the committed bytes inside, never the envelope. " +
|
|
693
783
|
"Read-only. Use bitgraph_record to BitGraph files that turn out not to be on record.",
|
|
694
784
|
inputSchema: {
|
|
695
785
|
paths: z
|
|
@@ -714,11 +804,20 @@ export function buildServer(deps: ServerDeps = {}): McpServer {
|
|
|
714
804
|
async ({ paths, digests, response_format }) => {
|
|
715
805
|
const config = configFromEnv();
|
|
716
806
|
try {
|
|
717
|
-
const inputs: Array<{ label: string; standardDigest: string }> = [];
|
|
807
|
+
const inputs: Array<{ label: string; standardDigest: string; carrier?: CarrierWindowView; c2pa?: boolean; note?: string }> = [];
|
|
718
808
|
if (paths && paths.length > 0) {
|
|
719
809
|
const { files } = await expandPaths(paths, MAX_CHECK_FILES);
|
|
720
|
-
const
|
|
721
|
-
|
|
810
|
+
const classified = await mapConcurrent(files, SCAN_CONCURRENCY, classifyPath);
|
|
811
|
+
classified.forEach((c, i) => {
|
|
812
|
+
const label = files[i] as string;
|
|
813
|
+
if (c.kind === "plain") {
|
|
814
|
+
inputs.push({ label, standardDigest: c.file.digestB64, ...(c.file.c2pa ? { c2pa: true } : {}) });
|
|
815
|
+
} else if (c.status === "ok" && c.innerDigestB64 !== null) {
|
|
816
|
+
inputs.push({ label, standardDigest: c.innerDigestB64, ...(c.view ? { carrier: c.view } : {}), ...(c.c2pa ? { c2pa: true } : {}) });
|
|
817
|
+
} else {
|
|
818
|
+
inputs.push({ label, standardDigest: "", note: c.reason ?? "unreadable BitGraphed file" });
|
|
819
|
+
}
|
|
820
|
+
});
|
|
722
821
|
}
|
|
723
822
|
for (const d of digests ?? []) {
|
|
724
823
|
const trimmed = d.trim();
|
|
@@ -736,23 +835,24 @@ export function buildServer(deps: ServerDeps = {}): McpServer {
|
|
|
736
835
|
return fail(`Error: ${inputs.length} items; check at most ${MAX_CHECK_FILES} at a time.`);
|
|
737
836
|
}
|
|
738
837
|
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
inputs.map((i) => toUrlSafeB64(i.standardDigest))
|
|
742
|
-
);
|
|
838
|
+
const lookable = [...new Set(inputs.filter((i) => i.standardDigest !== "").map((i) => toUrlSafeB64(i.standardDigest)))];
|
|
839
|
+
const checked = lookable.length > 0 ? await batchCheck(config, lookable) : { results: {} };
|
|
743
840
|
|
|
744
841
|
const outcomes: CheckOutcome[] = inputs.map((input) => {
|
|
745
|
-
const entry = checked.results[toUrlSafeB64(input.standardDigest)];
|
|
842
|
+
const entry = input.standardDigest === "" ? undefined : checked.results[toUrlSafeB64(input.standardDigest)];
|
|
746
843
|
const proofs = entry?.proofs ?? [];
|
|
747
844
|
const positions = proofs.map((p) => ({ ...positionOf(p.proof), ...(p.member ? { member: p.member } : {}) }));
|
|
748
845
|
return {
|
|
749
846
|
input: input.label,
|
|
750
|
-
digest: toUrlSafeB64(input.standardDigest),
|
|
847
|
+
digest: input.standardDigest === "" ? "" : toUrlSafeB64(input.standardDigest),
|
|
751
848
|
// A fused descendant that names these bytes as origin is not a recording of them.
|
|
752
849
|
// The original and the new file made from it find the same proof.
|
|
753
850
|
on_record: proofs.length > 0,
|
|
754
851
|
positions,
|
|
755
852
|
proof_url: proofs.length > 0 ? proofUrl(config.baseUrl, input.standardDigest) : null,
|
|
853
|
+
...(input.carrier ? { carrier: input.carrier } : {}),
|
|
854
|
+
...(input.c2pa ? { c2pa: true } : {}),
|
|
855
|
+
...(input.note !== undefined ? { note: input.note } : {}),
|
|
756
856
|
};
|
|
757
857
|
});
|
|
758
858
|
|
|
@@ -778,7 +878,8 @@ export function buildServer(deps: ServerDeps = {}): McpServer {
|
|
|
778
878
|
title: "Get a BitGraph proof",
|
|
779
879
|
description:
|
|
780
880
|
"Fetch a BitGraph proof and its context: causal position, every position the same bytes occupy, the row a set member holds (one of N), and the two-sided Ethereum anchor window " +
|
|
781
|
-
"('
|
|
881
|
+
"(after the floor block's time; before the ANCHORING of the later block, a position bound, never that block's mine time). Look up by digest (base64, either form), by BitGraph number (e.g. '4523' or '#4,523', current epoch), or by file path (hashed locally). " +
|
|
882
|
+
"A path to a BitGraphed file (bitgraph-carrier/1) looks up the committed bytes inside it, and when the ledger has no row the proof the file carries answers offline. " +
|
|
782
883
|
"Exactly one of digest, number, or path is required. Read-only. " +
|
|
783
884
|
"markdown returns a summary; json returns the full proof object with positions and anchor window.",
|
|
784
885
|
inputSchema: {
|
|
@@ -822,6 +923,7 @@ export function buildServer(deps: ServerDeps = {}): McpServer {
|
|
|
822
923
|
|
|
823
924
|
let urlSafeDigest: string;
|
|
824
925
|
let selCounter = counter;
|
|
926
|
+
let carrierRow: CarrierRow | null = null;
|
|
825
927
|
if (number !== undefined) {
|
|
826
928
|
const result = await search(config, number);
|
|
827
929
|
if (!result.found || result.digest === undefined) {
|
|
@@ -832,8 +934,16 @@ export function buildServer(deps: ServerDeps = {}): McpServer {
|
|
|
832
934
|
urlSafeDigest = result.digest;
|
|
833
935
|
if (selCounter === undefined && result.counter != null) selCounter = result.counter;
|
|
834
936
|
} else if (path !== undefined) {
|
|
835
|
-
const
|
|
836
|
-
|
|
937
|
+
const c = await classifyPath(path);
|
|
938
|
+
if (c.kind === "carrier") {
|
|
939
|
+
if (c.status !== "ok" || c.innerDigestB64 === null) {
|
|
940
|
+
return fail(`Error: ${c.reason ?? "unreadable BitGraphed file"}`);
|
|
941
|
+
}
|
|
942
|
+
carrierRow = c;
|
|
943
|
+
urlSafeDigest = toUrlSafeB64(c.innerDigestB64);
|
|
944
|
+
} else {
|
|
945
|
+
urlSafeDigest = toUrlSafeB64(c.file.digestB64);
|
|
946
|
+
}
|
|
837
947
|
} else {
|
|
838
948
|
const trimmed = (digest as string).trim();
|
|
839
949
|
if (!looksLikeDigest(trimmed)) {
|
|
@@ -848,16 +958,41 @@ export function buildServer(deps: ServerDeps = {}): McpServer {
|
|
|
848
958
|
const selEpoch = epoch !== undefined ? toUrlSafeB64(fromUrlSafeB64(epoch)) : undefined;
|
|
849
959
|
const detail = await getProofDetail(config, urlSafeDigest, selCounter, selEpoch);
|
|
850
960
|
if (detail.proofs.length === 0) {
|
|
961
|
+
if (carrierRow !== null && carrierRow.view !== null && carrierRow.proof !== null) {
|
|
962
|
+
// The ledger has no row, and the file answers for itself.
|
|
963
|
+
const v = carrierRow.view;
|
|
964
|
+
const synth: ProofDetailResponse = {
|
|
965
|
+
proofs: [{ proof: carrierRow.proof as BitGraphProof }],
|
|
966
|
+
positions: [],
|
|
967
|
+
causalWindow: {
|
|
968
|
+
anchorBefore: v.not_before
|
|
969
|
+
? { blockNumber: v.not_before.block, blockHash: v.not_before.hash, etherscanUrl: `https://etherscan.io/block/${v.not_before.block}`, blockTime: v.not_before.time }
|
|
970
|
+
: null,
|
|
971
|
+
anchorAfter: v.not_after
|
|
972
|
+
? { blockNumber: v.not_after.block, blockHash: v.not_after.hash, etherscanUrl: `https://etherscan.io/block/${v.not_after.block}`, blockTime: v.not_after.time }
|
|
973
|
+
: null,
|
|
974
|
+
},
|
|
975
|
+
};
|
|
976
|
+
const offlineNote = `Judged offline from the proof this BitGraphed file carries: carried proof ${v.verdict}${v.not_after === null ? ", closing anchor NOT FETCHED" : ""}. Not found in this ledger.`;
|
|
977
|
+
const structured = { ...synth, carrier: v } as unknown as Record<string, unknown>;
|
|
978
|
+
if (response_format === "json") return ok(capJson(structured).text, structured);
|
|
979
|
+
return ok(`${renderProofMarkdown(synth, config.baseUrl)}\n\n${offlineNote}`, structured);
|
|
980
|
+
}
|
|
851
981
|
return fail(
|
|
852
982
|
`Not on record: no proof exists for digest ${urlSafeDigest}. Use bitgraph_record to make a BitGraph of the file.`
|
|
853
983
|
);
|
|
854
984
|
}
|
|
855
985
|
|
|
856
986
|
if (response_format === "json") {
|
|
857
|
-
const
|
|
858
|
-
|
|
987
|
+
const withCarrier = carrierRow?.view ? ({ ...detail, carrier: carrierRow.view } as unknown) : detail;
|
|
988
|
+
const capped = capJson(withCarrier);
|
|
989
|
+
return ok(capped.text, withCarrier as Record<string, unknown>);
|
|
859
990
|
}
|
|
860
|
-
|
|
991
|
+
const md = renderProofMarkdown(detail, config.baseUrl);
|
|
992
|
+
const carrierNote = carrierRow?.view
|
|
993
|
+
? `\n\nThis file carries its own proof (carried proof ${carrierRow.view.verdict}, judged offline).`
|
|
994
|
+
: "";
|
|
995
|
+
return ok(`${md}${carrierNote}`, detail as unknown as Record<string, unknown>);
|
|
861
996
|
} catch (err) {
|
|
862
997
|
return fail(errorText(err));
|
|
863
998
|
}
|
package/src/task.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c) 2024-2026
|
|
1
|
+
// Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* A position BEFORE the work: the task form of the stdio server.
|
|
@@ -161,7 +161,7 @@ export const TASK_INSTRUCTIONS =
|
|
|
161
161
|
`When the output exists, call bitgraph_commit with this fuse_token and the output file's path, within ${SLOT_TTL_SECONDS} seconds of opening: the output itself is sealed under the position, and the proof is written beside it. ` +
|
|
162
162
|
"If the output cannot hold text (a PNG, audio, video) or will take longer than that to make, put the commitment into a task file instead (the exact prompt or request you send), seal that file the same way, and when the output exists record it with bitgraph_record: the task is sealed before the output existed, and the output is recorded after. " +
|
|
163
163
|
"Keep the exact bytes that were sealed unchanged: a verifier recomputes the commitment from the proof and looks for the string inside them. " +
|
|
164
|
-
"Leave the proof file as written, whole, every field: a proof missing slotAllocation or
|
|
164
|
+
"Leave the proof file as written, whole, every field, environment.attestation.reportB64 included: a proof missing slotAllocation, environment, or the attestation cannot be verified.";
|
|
165
165
|
|
|
166
166
|
/**
|
|
167
167
|
* Write a sealed task's proof beside the file it is about, whole, and say
|
package/src/types.ts
CHANGED