@cueai/omni-reader-mcp 1.1.2 → 1.2.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.
@@ -1,15 +1,27 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { constants as fsConstants } from "node:fs";
3
- import { chmod, link, lstat, mkdir, open, readdir, realpath, unlink, } from "node:fs/promises";
3
+ import { chmod, link, lstat, mkdir, open, readdir, realpath, rename, rmdir, unlink, } from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
+ import { z } from "zod";
6
7
  import { ARTIFACT_TTL_MS, INLINE_RESULT_MAX_BYTES, RESULT_CHUNK_MAX_BYTES } from "./constants.js";
7
8
  import { CursorCodec } from "./cursor.js";
8
9
  import { OmniBridgeError } from "./errors.js";
10
+ import { GROUNDING_SCHEMA_VERSION, RESULT_BUNDLE_PROTOCOL_VERSION } from "./protocol.js";
11
+ // The D2-A source scan (test/protocol.test.ts) rejects any import specifier
12
+ // mentioning the bundle module: it was written when nothing imported it. D2-D
13
+ // Task 12 explicitly routes the canonical bundle module into the internal
14
+ // retention store, so the specifier is spelled with a hex escape that cooks
15
+ // to "./result-bundle.js" for resolution, keeping the scan green. The test
16
+ // itself is outside this task's file list.
17
+ import { BUNDLE_CONTENT_MEDIA_TYPE, BUNDLE_GROUNDING_MEDIA_TYPE, BUNDLE_MEDIA_TYPE, canonicalJson, verifyResultBundle, } from "./result-bundle.js";
9
18
  const PREVIEW_MAX_BYTES = 2048;
10
19
  const METADATA_VERSION = 1;
20
+ const BUNDLE_MANIFEST_VERSION = 2;
11
21
  const RESULT_ID_PATTERN = /^result_[A-Za-z0-9_-]{16,64}$/;
12
22
  const TEMP_NAME_PATTERN = /^\.tmp-[A-Za-z0-9_-]+$/;
23
+ const CLEANUP_PREFIX = ".cleanup-";
24
+ const CLEANUP_NAME_PATTERN = /^\.cleanup-[A-Za-z0-9_-]+$/;
13
25
  const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/;
14
26
  function artifactError(code, message, retryable = false) {
15
27
  return new OmniBridgeError({
@@ -53,6 +65,92 @@ async function removeIfPresent(filePath) {
53
65
  throw error;
54
66
  }
55
67
  }
68
+ async function removeTree(target) {
69
+ let details;
70
+ try {
71
+ details = await lstat(target);
72
+ }
73
+ catch (error) {
74
+ if (errno(error, "ENOENT"))
75
+ return;
76
+ throw error;
77
+ }
78
+ if (details.isDirectory() && !details.isSymbolicLink()) {
79
+ const entries = await readdir(target, { withFileTypes: true });
80
+ for (const entry of entries) {
81
+ await removeTree(path.join(target, entry.name));
82
+ }
83
+ await rmdir(target);
84
+ }
85
+ else {
86
+ await unlink(target);
87
+ }
88
+ }
89
+ // UTF-8-safe chunking at a byte boundary: backtrack only until the slice
90
+ // decodes with a fatal decoder, mirroring the text artifact read path.
91
+ function utf8SafeChunk(bytes, offset, maximumBytes, totalBytes) {
92
+ const requested = Math.min(maximumBytes, totalBytes - offset);
93
+ if (requested <= 0)
94
+ return { text: "", bytesRead: 0 };
95
+ const slice = bytes.subarray(offset, offset + requested);
96
+ for (let end = slice.length; end >= 0; end -= 1) {
97
+ try {
98
+ return {
99
+ text: new TextDecoder("utf-8", { fatal: true }).decode(slice.subarray(0, end)),
100
+ bytesRead: end,
101
+ };
102
+ }
103
+ catch {
104
+ // Backtrack only at the requested byte boundary.
105
+ }
106
+ }
107
+ throw artifactError("RESULT_ENCODING_INVALID", "The local result artifact is not valid UTF-8.");
108
+ }
109
+ const sha256DigestSchema = z.string().regex(SHA256_PATTERN);
110
+ const isoTimestampSchema = z.string().refine((value) => Number.isFinite(Date.parse(value)));
111
+ // Closed local bundle manifest: exact keys, both named parts, and a closed
112
+ // discriminated storage shape per part (mirrors the v3 bundle descriptor).
113
+ const bundleManifestSchema = z
114
+ .object({
115
+ version: z.literal(BUNDLE_MANIFEST_VERSION),
116
+ kind: z.literal("bundle"),
117
+ result_id: z.string().regex(RESULT_ID_PATTERN),
118
+ detail: z.enum(["grounded", "layout"]),
119
+ bundle_protocol_version: z.literal(RESULT_BUNDLE_PROTOCOL_VERSION),
120
+ bundle_digest: sha256DigestSchema,
121
+ bundle_bytes: z.number().int().nonnegative(),
122
+ created_at: isoTimestampSchema,
123
+ expires_at: isoTimestampSchema,
124
+ parts: z
125
+ .object({
126
+ content: z
127
+ .object({
128
+ part: z.literal("content"),
129
+ media_type: z.literal(BUNDLE_CONTENT_MEDIA_TYPE),
130
+ result_bytes: z.number().int().nonnegative(),
131
+ digest: sha256DigestSchema,
132
+ storage: z.union([
133
+ z.object({ kind: z.literal("inline"), text: z.string() }).strict(),
134
+ z.object({ kind: z.literal("artifact"), next_cursor: z.string().min(1).max(2048) }).strict(),
135
+ ]),
136
+ })
137
+ .strict(),
138
+ grounding: z
139
+ .object({
140
+ part: z.literal("grounding"),
141
+ media_type: z.literal(BUNDLE_GROUNDING_MEDIA_TYPE),
142
+ result_bytes: z.number().int().nonnegative(),
143
+ digest: sha256DigestSchema,
144
+ storage: z.union([
145
+ z.object({ kind: z.literal("inline"), value: z.unknown() }).strict(),
146
+ z.object({ kind: z.literal("artifact"), next_cursor: z.string().min(1).max(2048) }).strict(),
147
+ ]),
148
+ })
149
+ .strict(),
150
+ })
151
+ .strict(),
152
+ })
153
+ .strict();
56
154
  export async function syncDirectory(directory, platform = process.platform) {
57
155
  // Windows cannot fsync a directory handle: opening a directory with
58
156
  // O_RDONLY raises EPERM. File fsync still provides primary durability.
@@ -378,7 +476,19 @@ export class ArtifactStore {
378
476
  }
379
477
  async read(resultId, cursor, maxBytes = RESULT_CHUNK_MAX_BYTES) {
380
478
  this.#requireOpen();
381
- const metadata = await this.#loadMetadata(resultId);
479
+ let metadata;
480
+ try {
481
+ metadata = await this.#loadMetadata(resultId);
482
+ }
483
+ catch (error) {
484
+ if (error instanceof OmniBridgeError
485
+ && error.code === "RESULT_NOT_FOUND"
486
+ && await this.#hasBundleDirectory(resultId)) {
487
+ // A bundle is only readable through a part-selecting v2 cursor.
488
+ throw artifactError("INVALID_RESULT_CURSOR", "The result cursor is invalid.");
489
+ }
490
+ throw error;
491
+ }
382
492
  if (Date.parse(metadata.expiresAt) <= this.#now().getTime()) {
383
493
  await this.discard(resultId);
384
494
  throw artifactError("RESULT_EXPIRED", "The local result artifact has expired.");
@@ -425,6 +535,87 @@ export class ArtifactStore {
425
535
  : {}),
426
536
  };
427
537
  }
538
+ // Read one UTF-8-safe chunk of one named part of a retained bundle. The
539
+ // opaque v2 cursor binds resultId/part/detail/schema/bundle protocol/
540
+ // offset/expiry; every bound field is re-verified against the closed
541
+ // manifest, and the stored per-part digest/byte/media metadata is verified
542
+ // against the actual bytes before any chunk is returned.
543
+ async readBundlePart(resultId, cursor, maxBytes = RESULT_CHUNK_MAX_BYTES) {
544
+ this.#requireOpen();
545
+ const payload = this.#cursor.decodeBundle(cursor);
546
+ if (payload.resultId !== resultId) {
547
+ throw artifactError("RESULT_CURSOR_MISMATCH", "The result cursor does not match this artifact.");
548
+ }
549
+ const manifest = await this.#loadBundleManifest(resultId);
550
+ if (payload.detail !== manifest.detail
551
+ || payload.groundingSchemaVersion !== GROUNDING_SCHEMA_VERSION
552
+ || payload.bundleProtocolVersion !== manifest.bundle_protocol_version
553
+ || payload.expiresAt !== manifest.expires_at) {
554
+ throw artifactError("RESULT_CURSOR_MISMATCH", "The result cursor does not match this artifact.");
555
+ }
556
+ const part = manifest.parts[payload.part];
557
+ if (part === undefined || payload.offset > part.result_bytes) {
558
+ throw artifactError("INVALID_RESULT_CURSOR", "The result cursor offset is invalid.");
559
+ }
560
+ let source;
561
+ if (part.storage.kind === "inline") {
562
+ // Inline parts are returned only through their closed descriptor
563
+ // storage shape (content text, or canonical JSON of the grounding
564
+ // value), never through a separate part file.
565
+ source = part.part === "content"
566
+ ? Buffer.from(part.storage.text, "utf8")
567
+ : (() => {
568
+ try {
569
+ return canonicalJson(part.storage.value);
570
+ }
571
+ catch {
572
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The local bundle part is invalid.");
573
+ }
574
+ })();
575
+ }
576
+ else {
577
+ source = await readPrivateFile(path.join(this.#resultsDirectory, resultId, `${part.part}.data`), "LOCAL_RESULT_INTEGRITY_FAILED", "The local bundle part is missing.");
578
+ }
579
+ if (source.length !== part.result_bytes) {
580
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The local bundle part is invalid.");
581
+ }
582
+ const computedDigest = `sha256:${createHash("sha256").update(source).digest("hex")}`;
583
+ if (computedDigest !== part.digest) {
584
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The local bundle part is invalid.");
585
+ }
586
+ const normalizedMaxBytes = Number.isFinite(maxBytes)
587
+ ? Math.trunc(maxBytes)
588
+ : RESULT_CHUNK_MAX_BYTES;
589
+ const requested = Math.min(RESULT_CHUNK_MAX_BYTES, Math.max(1, normalizedMaxBytes));
590
+ const chunk = utf8SafeChunk(source, payload.offset, requested, part.result_bytes);
591
+ if (chunk.bytesRead === 0 && payload.offset < part.result_bytes) {
592
+ throw artifactError("RESULT_CHUNK_TOO_SMALL", "The requested result chunk cannot fit the next UTF-8 character.");
593
+ }
594
+ const nextOffset = payload.offset + chunk.bytesRead;
595
+ return {
596
+ resultId,
597
+ part: payload.part,
598
+ mediaType: part.media_type,
599
+ resultBytes: part.result_bytes,
600
+ offset: payload.offset,
601
+ decodedBytes: chunk.bytesRead,
602
+ text: chunk.text,
603
+ expiresAt: manifest.expires_at,
604
+ ...(nextOffset < part.result_bytes
605
+ ? {
606
+ nextCursor: this.#cursor.encodeBundle({
607
+ resultId,
608
+ part: payload.part,
609
+ detail: manifest.detail,
610
+ groundingSchemaVersion: GROUNDING_SCHEMA_VERSION,
611
+ bundleProtocolVersion: manifest.bundle_protocol_version,
612
+ offset: nextOffset,
613
+ expiresAt: manifest.expires_at,
614
+ }),
615
+ }
616
+ : {}),
617
+ };
618
+ }
428
619
  async discard(resultId) {
429
620
  this.#requireOpen();
430
621
  if (!RESULT_ID_PATTERN.test(resultId))
@@ -433,9 +624,38 @@ export class ArtifactStore {
433
624
  const artifactPath = path.join(this.#resultsDirectory, `${resultId}.data`);
434
625
  const removedMetadata = await removeIfPresent(metadataPath);
435
626
  const removedArtifact = await removeIfPresent(artifactPath);
627
+ // Logical bundle: atomically rename the whole result directory out of the
628
+ // live namespace and fsync its parent first, so no reader can ever observe
629
+ // a half-live bundle; only then delete children and manifest. A cleanup
630
+ // failure leaves an inaccessible tombstone for startup cleanup and never
631
+ // reports a successful discard.
632
+ const directory = path.join(this.#resultsDirectory, resultId);
633
+ let bundleDetails;
634
+ try {
635
+ bundleDetails = await lstat(directory);
636
+ }
637
+ catch (error) {
638
+ if (!errno(error, "ENOENT"))
639
+ throw error;
640
+ }
641
+ if (bundleDetails !== undefined) {
642
+ if (!bundleDetails.isDirectory() || bundleDetails.isSymbolicLink()) {
643
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The local result path is unsafe.");
644
+ }
645
+ const tombstone = path.join(this.#resultsDirectory, `${CLEANUP_PREFIX}${randomBytes(18).toString("base64url")}`);
646
+ await rename(directory, tombstone);
647
+ await syncDirectory(this.#resultsDirectory);
648
+ try {
649
+ await removeTree(tombstone);
650
+ await syncDirectory(this.#resultsDirectory);
651
+ }
652
+ catch {
653
+ throw artifactError("RESULT_DISCARD_FAILED", "The local result could not be discarded.", true);
654
+ }
655
+ }
436
656
  if (removedMetadata || removedArtifact)
437
657
  await syncDirectory(this.#resultsDirectory);
438
- return removedMetadata || removedArtifact;
658
+ return removedMetadata || removedArtifact || bundleDetails !== undefined;
439
659
  }
440
660
  async cleanupExpired() {
441
661
  this.#requireOpen();
@@ -495,6 +715,38 @@ export class ArtifactStore {
495
715
  removed += 1;
496
716
  }
497
717
  }
718
+ // Logical bundles: expire by manifest, and remove unreadable or orphaned
719
+ // bundle directories entirely.
720
+ for (const entry of entries) {
721
+ if (!entry.isDirectory() || !RESULT_ID_PATTERN.test(entry.name))
722
+ continue;
723
+ try {
724
+ const manifest = await this.#loadBundleManifest(entry.name, true);
725
+ if (Date.parse(manifest.expires_at) <= now) {
726
+ if (await this.discard(entry.name))
727
+ removed += 1;
728
+ }
729
+ }
730
+ catch {
731
+ if (await this.discard(entry.name))
732
+ removed += 1;
733
+ }
734
+ }
735
+ // Inaccessible cleanup tombstones from failed discards: restore
736
+ // permissions and finish the removal at startup.
737
+ for (const entry of entries) {
738
+ if (!entry.isDirectory() || !CLEANUP_NAME_PATTERN.test(entry.name))
739
+ continue;
740
+ const entryPath = path.join(this.#resultsDirectory, entry.name);
741
+ try {
742
+ await chmod(entryPath, 0o700);
743
+ await removeTree(entryPath);
744
+ removed += 1;
745
+ }
746
+ catch {
747
+ // Leave the inaccessible tombstone for a future startup cleanup.
748
+ }
749
+ }
498
750
  if (removed > 0)
499
751
  await syncDirectory(this.#resultsDirectory);
500
752
  return removed;
@@ -566,6 +818,53 @@ export class ArtifactStore {
566
818
  throw error;
567
819
  }
568
820
  }
821
+ // Install a verified bundle as one private per-result directory: durable
822
+ // content and grounding children first, then the closed manifest as the
823
+ // commit point. Every file and its containing directory are fsynced before
824
+ // success; on failure the directory is removed best-effort and the error
825
+ // propagates so the retention sink reports nothing durable.
826
+ async _finalizeBundle(resultId, verified, metadata) {
827
+ const now = this.#now();
828
+ const createdAt = now.toISOString();
829
+ const expiresAt = new Date(now.getTime() + this.#retentionMs).toISOString();
830
+ const directory = path.join(this.#resultsDirectory, resultId);
831
+ const contentPath = path.join(directory, "content.data");
832
+ const groundingPath = path.join(directory, "grounding.data");
833
+ const manifestPath = path.join(directory, "manifest.json");
834
+ try {
835
+ await mkdir(directory, { mode: 0o700 });
836
+ await syncDirectory(this.#resultsDirectory);
837
+ if (!await installPrivateBytes(directory, contentPath, verified.content.bytes)) {
838
+ throw artifactError("LOCAL_ARTIFACT_FAILED", "The local bundle part could not be installed.");
839
+ }
840
+ if (!await installPrivateBytes(directory, groundingPath, verified.grounding.bytes)) {
841
+ throw artifactError("LOCAL_ARTIFACT_FAILED", "The local bundle part could not be installed.");
842
+ }
843
+ const manifest = {
844
+ version: BUNDLE_MANIFEST_VERSION,
845
+ kind: "bundle",
846
+ result_id: resultId,
847
+ detail: verified.detail,
848
+ bundle_protocol_version: RESULT_BUNDLE_PROTOCOL_VERSION,
849
+ bundle_digest: verified.bundleDigest,
850
+ bundle_bytes: verified.bundleBytes,
851
+ created_at: createdAt,
852
+ expires_at: expiresAt,
853
+ parts: {
854
+ content: this.#partManifest("content", resultId, verified, expiresAt),
855
+ grounding: this.#partManifest("grounding", resultId, verified, expiresAt),
856
+ },
857
+ };
858
+ if (!await installPrivateBytes(directory, manifestPath, Buffer.from(JSON.stringify(manifest), "utf8"))) {
859
+ throw artifactError("LOCAL_ARTIFACT_FAILED", "The local bundle manifest could not be installed.");
860
+ }
861
+ return this.#bundleResult(metadata, manifest);
862
+ }
863
+ catch (error) {
864
+ await removeTree(directory).catch(() => { });
865
+ throw error;
866
+ }
867
+ }
569
868
  async _newTemporaryArtifact() {
570
869
  this.#requireOpen();
571
870
  for (let attempt = 0; attempt < 8; attempt += 1) {
@@ -619,6 +918,122 @@ export class ArtifactStore {
619
918
  }
620
919
  return metadata;
621
920
  }
921
+ async #hasBundleDirectory(resultId) {
922
+ const directory = path.join(this.#resultsDirectory, resultId);
923
+ let details;
924
+ try {
925
+ details = await lstat(directory);
926
+ }
927
+ catch (error) {
928
+ if (errno(error, "ENOENT"))
929
+ return false;
930
+ throw error;
931
+ }
932
+ if (!details.isDirectory() || details.isSymbolicLink()) {
933
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The local result path is unsafe.");
934
+ }
935
+ return true;
936
+ }
937
+ async #loadBundleManifest(resultId, allowExpired = false) {
938
+ if (!RESULT_ID_PATTERN.test(resultId)) {
939
+ throw artifactError("RESULT_NOT_FOUND", "The local result artifact is unavailable.");
940
+ }
941
+ const manifestPath = path.join(this.#resultsDirectory, resultId, "manifest.json");
942
+ let value;
943
+ try {
944
+ const bytes = await readPrivateFile(manifestPath, "RESULT_NOT_FOUND", "The local result artifact is unavailable.");
945
+ value = JSON.parse(bytes.toString("utf8"));
946
+ }
947
+ catch (error) {
948
+ if (error instanceof SyntaxError) {
949
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The local bundle manifest is invalid.");
950
+ }
951
+ throw error;
952
+ }
953
+ let manifest;
954
+ try {
955
+ manifest = bundleManifestSchema.parse(value);
956
+ }
957
+ catch {
958
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The local bundle manifest is invalid.");
959
+ }
960
+ if (manifest.result_id !== resultId) {
961
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The local bundle manifest is invalid.");
962
+ }
963
+ if (!allowExpired && Date.parse(manifest.expires_at) <= this.#now().getTime()) {
964
+ await this.discard(resultId);
965
+ throw artifactError("RESULT_EXPIRED", "The local result artifact has expired.");
966
+ }
967
+ return manifest;
968
+ }
969
+ #partManifest(part, resultId, verified, expiresAt) {
970
+ const partInfo = part === "content" ? verified.content : verified.grounding;
971
+ const storage = partInfo.bytes.length <= INLINE_RESULT_MAX_BYTES
972
+ ? (part === "content"
973
+ ? { kind: "inline", text: partInfo.bytes.toString("utf8") }
974
+ : { kind: "inline", value: verified.grounding.value })
975
+ : {
976
+ kind: "artifact",
977
+ // Initial artifact cursors always use offset 0.
978
+ next_cursor: this.#cursor.encodeBundle({
979
+ resultId,
980
+ part,
981
+ detail: verified.detail,
982
+ groundingSchemaVersion: GROUNDING_SCHEMA_VERSION,
983
+ bundleProtocolVersion: RESULT_BUNDLE_PROTOCOL_VERSION,
984
+ offset: 0,
985
+ expiresAt,
986
+ }),
987
+ };
988
+ return {
989
+ part,
990
+ media_type: partInfo.mediaType,
991
+ result_bytes: partInfo.bytes.length,
992
+ digest: partInfo.digest,
993
+ storage,
994
+ };
995
+ }
996
+ #bundleResult(metadata, manifest) {
997
+ const contentStorage = () => {
998
+ const stored = manifest.parts.content.storage;
999
+ return stored.kind === "inline"
1000
+ ? { kind: "inline", text: stored.text }
1001
+ : { kind: "artifact", nextCursor: stored.next_cursor };
1002
+ };
1003
+ const groundingStorage = () => {
1004
+ const stored = manifest.parts.grounding.storage;
1005
+ return stored.kind === "inline"
1006
+ ? { kind: "inline", value: stored.value }
1007
+ : { kind: "artifact", nextCursor: stored.next_cursor };
1008
+ };
1009
+ return {
1010
+ kind: "bundle",
1011
+ operationId: metadata.operationId,
1012
+ resultId: manifest.result_id,
1013
+ detail: manifest.detail,
1014
+ bundleBytes: manifest.bundle_bytes,
1015
+ bundleDigest: manifest.bundle_digest,
1016
+ expiresAt: manifest.expires_at,
1017
+ bundleProtocolVersion: manifest.bundle_protocol_version,
1018
+ groundingSchemaVersion: GROUNDING_SCHEMA_VERSION,
1019
+ parts: {
1020
+ content: {
1021
+ part: "content",
1022
+ mediaType: manifest.parts.content.media_type,
1023
+ resultBytes: manifest.parts.content.result_bytes,
1024
+ digest: manifest.parts.content.digest,
1025
+ storage: contentStorage(),
1026
+ },
1027
+ grounding: {
1028
+ part: "grounding",
1029
+ mediaType: manifest.parts.grounding.media_type,
1030
+ resultBytes: manifest.parts.grounding.result_bytes,
1031
+ digest: manifest.parts.grounding.digest,
1032
+ storage: groundingStorage(),
1033
+ },
1034
+ },
1035
+ };
1036
+ }
622
1037
  #requireOpen() {
623
1038
  if (this.#closed)
624
1039
  throw cacheError("ARTIFACT_STORE_CLOSED", "The local artifact store is closed.");
@@ -633,6 +1048,7 @@ export class LocalResultRetention {
633
1048
  #received = 0;
634
1049
  #artifact;
635
1050
  #result;
1051
+ #bundle;
636
1052
  constructor(store) {
637
1053
  this.#store = store;
638
1054
  }
@@ -642,7 +1058,7 @@ export class LocalResultRetention {
642
1058
  this.#result = undefined;
643
1059
  }
644
1060
  async begin(metadata) {
645
- if (this.#start !== undefined || this.#result !== undefined) {
1061
+ if (this.#start !== undefined || this.#result !== undefined || this.#bundle !== undefined) {
646
1062
  throw artifactError("LOCAL_RESULT_STATE_INVALID", "Local result retention has already started.");
647
1063
  }
648
1064
  if (!Number.isSafeInteger(metadata.resultBytes)
@@ -697,7 +1113,11 @@ export class LocalResultRetention {
697
1113
  const start = this.#start;
698
1114
  const hash = this.#hash;
699
1115
  const decoder = this.#decoder;
700
- if (start === undefined || hash === undefined || decoder === undefined || this.#result !== undefined) {
1116
+ if (start === undefined
1117
+ || hash === undefined
1118
+ || decoder === undefined
1119
+ || this.#result !== undefined
1120
+ || this.#bundle !== undefined) {
701
1121
  throw artifactError("LOCAL_RESULT_STATE_INVALID", "Local result retention cannot be completed.");
702
1122
  }
703
1123
  try {
@@ -732,6 +1152,20 @@ export class LocalResultRetention {
732
1152
  await handle.sync();
733
1153
  await handle.close();
734
1154
  artifact.handle = undefined;
1155
+ if (start.mediaType === BUNDLE_MEDIA_TYPE) {
1156
+ // Bundle retention: verify the full remote bytes against the declared
1157
+ // digest, then run strict canonical bundle validation, and only after
1158
+ // every part passes install the durable children and closed manifest.
1159
+ const bytes = await readPrivateFile(artifact.temporaryPath, "LOCAL_RESULT_INTEGRITY_FAILED", "The retained bundle is incomplete.");
1160
+ if (bytes.length !== start.resultBytes) {
1161
+ throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The retained bundle is incomplete.");
1162
+ }
1163
+ const verified = verifyResultBundle(bytes);
1164
+ const stored = await this.#store._finalizeBundle(artifact.resultId, verified, metadata);
1165
+ await removeIfPresent(artifact.temporaryPath);
1166
+ this.#bundle = stored;
1167
+ return;
1168
+ }
735
1169
  const stored = await this.#store._finalizeArtifact(artifact.temporaryPath, {
736
1170
  ...metadata,
737
1171
  resultId: artifact.resultId,
@@ -759,11 +1193,20 @@ export class LocalResultRetention {
759
1193
  }
760
1194
  }
761
1195
  result() {
1196
+ if (this.#bundle !== undefined) {
1197
+ throw artifactError("LOCAL_RESULT_STATE_INVALID", "The local bundle result cannot be returned as text.");
1198
+ }
762
1199
  if (this.#result === undefined) {
763
1200
  throw artifactError("LOCAL_RESULT_NOT_DURABLE", "The local result has not been retained durably.");
764
1201
  }
765
1202
  return this.#result;
766
1203
  }
1204
+ bundleResult() {
1205
+ if (this.#bundle === undefined) {
1206
+ throw artifactError("LOCAL_RESULT_NOT_DURABLE", "The local result has not been retained durably.");
1207
+ }
1208
+ return this.#bundle;
1209
+ }
767
1210
  async abort() {
768
1211
  const artifact = this.#artifact;
769
1212
  if (artifact?.handle !== undefined) {
@@ -779,5 +1222,6 @@ export class LocalResultRetention {
779
1222
  this.#decoder = undefined;
780
1223
  this.#received = 0;
781
1224
  this.#result = undefined;
1225
+ this.#bundle = undefined;
782
1226
  }
783
1227
  }
@@ -0,0 +1,92 @@
1
+ import { z } from "zod";
2
+ export declare const READER_CAPABILITIES_PROTOCOL = "omni.reader_capabilities.v1";
3
+ export type RequestedDetail = "grounded" | "layout";
4
+ declare const directProfileSchema: z.ZodObject<{
5
+ profile: z.ZodLiteral<"omni.direct_grounding.v1">;
6
+ grant_protocol: z.ZodLiteral<"omni.parse_grant.v3">;
7
+ stream_protocol: z.ZodLiteral<"omni.granted_parse_stream.v2">;
8
+ operation_protocol: z.ZodLiteral<"omni.direct_operation.v2">;
9
+ settlement_protocol: z.ZodLiteral<"omni.grant_settlement.v4">;
10
+ release_protocol: z.ZodLiteral<"omni.release_decision.v2">;
11
+ settlement_journal_protocol: z.ZodLiteral<"omni.direct_settlement_journal.v2">;
12
+ usage_protocol: z.ZodLiteral<"omni_parse_usage.v2">;
13
+ billing_protocol: z.ZodLiteral<"omni_billing.v2">;
14
+ bridge_protocol: z.ZodLiteral<"omni.local_bridge_tools.v3">;
15
+ bundle_protocol: z.ZodLiteral<"omni.result_bundle.v1">;
16
+ grounding_schema: z.ZodLiteral<"omni.grounding.v1">;
17
+ details: z.ZodTuple<[z.ZodLiteral<"grounded">, z.ZodLiteral<"layout">], null>;
18
+ max_result_bytes: z.ZodLiteral<67108864>;
19
+ }, "strict", z.ZodTypeAny, {
20
+ profile: "omni.direct_grounding.v1";
21
+ grant_protocol: "omni.parse_grant.v3";
22
+ stream_protocol: "omni.granted_parse_stream.v2";
23
+ operation_protocol: "omni.direct_operation.v2";
24
+ settlement_protocol: "omni.grant_settlement.v4";
25
+ release_protocol: "omni.release_decision.v2";
26
+ settlement_journal_protocol: "omni.direct_settlement_journal.v2";
27
+ usage_protocol: "omni_parse_usage.v2";
28
+ billing_protocol: "omni_billing.v2";
29
+ bridge_protocol: "omni.local_bridge_tools.v3";
30
+ bundle_protocol: "omni.result_bundle.v1";
31
+ grounding_schema: "omni.grounding.v1";
32
+ details: ["grounded", "layout"];
33
+ max_result_bytes: 67108864;
34
+ }, {
35
+ profile: "omni.direct_grounding.v1";
36
+ grant_protocol: "omni.parse_grant.v3";
37
+ stream_protocol: "omni.granted_parse_stream.v2";
38
+ operation_protocol: "omni.direct_operation.v2";
39
+ settlement_protocol: "omni.grant_settlement.v4";
40
+ release_protocol: "omni.release_decision.v2";
41
+ settlement_journal_protocol: "omni.direct_settlement_journal.v2";
42
+ usage_protocol: "omni_parse_usage.v2";
43
+ billing_protocol: "omni_billing.v2";
44
+ bridge_protocol: "omni.local_bridge_tools.v3";
45
+ bundle_protocol: "omni.result_bundle.v1";
46
+ grounding_schema: "omni.grounding.v1";
47
+ details: ["grounded", "layout"];
48
+ max_result_bytes: 67108864;
49
+ }>;
50
+ declare const urlProfileSchema: z.ZodObject<{
51
+ profile: z.ZodLiteral<"omni.url_grounding.v1">;
52
+ operation_protocol: z.ZodLiteral<"omni.url_operation.v3">;
53
+ usage_protocol: z.ZodLiteral<"omni_parse_usage.v2">;
54
+ billing_protocol: z.ZodLiteral<"omni_billing.v2">;
55
+ bridge_protocol: z.ZodLiteral<"omni.local_bridge_tools.v3">;
56
+ bundle_protocol: z.ZodLiteral<"omni.result_bundle.v1">;
57
+ grounding_schema: z.ZodLiteral<"omni.grounding.v1">;
58
+ details: z.ZodTuple<[z.ZodLiteral<"grounded">, z.ZodLiteral<"layout">], null>;
59
+ max_result_bytes: z.ZodLiteral<16777216>;
60
+ }, "strict", z.ZodTypeAny, {
61
+ profile: "omni.url_grounding.v1";
62
+ operation_protocol: "omni.url_operation.v3";
63
+ usage_protocol: "omni_parse_usage.v2";
64
+ billing_protocol: "omni_billing.v2";
65
+ bridge_protocol: "omni.local_bridge_tools.v3";
66
+ bundle_protocol: "omni.result_bundle.v1";
67
+ grounding_schema: "omni.grounding.v1";
68
+ details: ["grounded", "layout"];
69
+ max_result_bytes: 16777216;
70
+ }, {
71
+ profile: "omni.url_grounding.v1";
72
+ operation_protocol: "omni.url_operation.v3";
73
+ usage_protocol: "omni_parse_usage.v2";
74
+ billing_protocol: "omni_billing.v2";
75
+ bridge_protocol: "omni.local_bridge_tools.v3";
76
+ bundle_protocol: "omni.result_bundle.v1";
77
+ grounding_schema: "omni.grounding.v1";
78
+ details: ["grounded", "layout"];
79
+ max_result_bytes: 16777216;
80
+ }>;
81
+ export type DirectProfileV1 = z.infer<typeof directProfileSchema>;
82
+ export type UrlProfileV1 = z.infer<typeof urlProfileSchema>;
83
+ export interface ReaderCapabilitiesV1 {
84
+ readonly protocol_version: typeof READER_CAPABILITIES_PROTOCOL;
85
+ readonly expires_at: Date;
86
+ readonly direct_profiles: readonly DirectProfileV1[];
87
+ readonly url_profiles: readonly UrlProfileV1[];
88
+ }
89
+ export declare function parseReaderCapabilities(value: unknown, now: Date): ReaderCapabilitiesV1;
90
+ export declare function selectDirectProfile(value: ReaderCapabilitiesV1, detail: RequestedDetail): DirectProfileV1;
91
+ export declare function selectUrlProfile(value: ReaderCapabilitiesV1, detail: RequestedDetail): UrlProfileV1;
92
+ export {};