@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.
Files changed (56) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1 -1
  3. package/dist/__tests__/carrier.test.d.ts +2 -0
  4. package/dist/__tests__/carrier.test.d.ts.map +1 -0
  5. package/dist/__tests__/carrier.test.js +243 -0
  6. package/dist/__tests__/carrier.test.js.map +1 -0
  7. package/dist/__tests__/encoding.test.js +1 -1
  8. package/dist/__tests__/encoding.test.js.map +1 -1
  9. package/dist/__tests__/format.test.js +6 -6
  10. package/dist/__tests__/format.test.js.map +1 -1
  11. package/dist/__tests__/scan.test.js +1 -1
  12. package/dist/__tests__/scan.test.js.map +1 -1
  13. package/dist/__tests__/tools.test.js +8 -7
  14. package/dist/__tests__/tools.test.js.map +1 -1
  15. package/dist/api.js +1 -1
  16. package/dist/api.js.map +1 -1
  17. package/dist/carrier-io.d.ts +52 -0
  18. package/dist/carrier-io.d.ts.map +1 -0
  19. package/dist/carrier-io.js +91 -0
  20. package/dist/carrier-io.js.map +1 -0
  21. package/dist/encoding.js +1 -1
  22. package/dist/encoding.js.map +1 -1
  23. package/dist/format.d.ts +17 -3
  24. package/dist/format.d.ts.map +1 -1
  25. package/dist/format.js +36 -15
  26. package/dist/format.js.map +1 -1
  27. package/dist/index.js +1 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/scan.d.ts +4 -0
  30. package/dist/scan.d.ts.map +1 -1
  31. package/dist/scan.js +44 -1
  32. package/dist/scan.js.map +1 -1
  33. package/dist/server.d.ts +1 -1
  34. package/dist/server.d.ts.map +1 -1
  35. package/dist/server.js +151 -29
  36. package/dist/server.js.map +1 -1
  37. package/dist/task.d.ts.map +1 -1
  38. package/dist/task.js +2 -2
  39. package/dist/task.js.map +1 -1
  40. package/dist/types.js +1 -1
  41. package/dist/types.js.map +1 -1
  42. package/package.json +13 -5
  43. package/src/__tests__/carrier.test.ts +271 -0
  44. package/src/__tests__/encoding.test.ts +1 -1
  45. package/src/__tests__/format.test.ts +6 -6
  46. package/src/__tests__/scan.test.ts +1 -1
  47. package/src/__tests__/tools.test.ts +8 -7
  48. package/src/api.ts +1 -1
  49. package/src/carrier-io.ts +120 -0
  50. package/src/encoding.ts +1 -1
  51. package/src/format.ts +56 -19
  52. package/src/index.ts +1 -1
  53. package/src/scan.ts +48 -1
  54. package/src/server.ts +169 -34
  55. package/src/task.ts +2 -2
  56. package/src/types.ts +1 -1
package/src/server.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
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.5.1";
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 ledger 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. " +
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 in the BitGraph ledger (bitgraph.ing), one position for all of them. " +
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 BitGraph still indexes (recorded before 2026-09-08) are NOT made again by default; they come back as 'on record' with their earliest position. BitGraph does not index new proofs, so a file may already have a BitGraph its holder keeps. Pass again=true to make a new BitGraph regardless. " +
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 BitGraph still indexes 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."
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 scans = await mapConcurrent(files, SCAN_CONCURRENCY, async (p) => {
400
- const s = await scanFile(p);
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 s;
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
- report(0, 1, "checking the ledger");
417
- const checked = await batchCheck(config, unique.map(toUrlSafeB64));
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, fused: 2 } as const;
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 environment cannot be verified. ";
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 the BitGraph ledger, without recording anything. " +
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 hashed = await hashPaths(files);
721
- hashed.forEach((d, i) => inputs.push({ label: files[i] as string, standardDigest: d }));
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 checked = await batchCheck(
740
- config,
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
- "('BitGraphed between X and Y'). Look up by digest (base64, either form), by BitGraph number (e.g. '4523' or '#4,523', current epoch), or by file path (hashed locally). " +
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 hashed = await hashPaths([path]);
836
- urlSafeDigest = toUrlSafeB64(hashed[0] as string);
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 capped = capJson(detail);
858
- return ok(capped.text, detail as unknown as Record<string, unknown>);
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
- return ok(renderProofMarkdown(detail, config.baseUrl), detail as unknown as Record<string, unknown>);
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 Mike Argento. Licensed under the MIT License. See LICENSE.
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 environment cannot be verified.";
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
@@ -1,4 +1,4 @@
1
- // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
1
+ // Copyright (c) 2024-2026 Argento Computing Inc. Licensed under the MIT License. See LICENSE.
2
2
 
3
3
  /**
4
4
  * @mikeargento/bitgraph-mcp: wire types.