@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.
- package/README.md +139 -80
- package/dist/artifact-store.d.ts +58 -0
- package/dist/artifact-store.js +449 -5
- package/dist/capabilities.d.ts +92 -0
- package/dist/capabilities.js +123 -0
- package/dist/constants.d.ts +34 -1
- package/dist/constants.js +39 -1
- package/dist/cube-client.d.ts +3 -0
- package/dist/cube-client.js +141 -9
- package/dist/cursor.d.ts +12 -0
- package/dist/cursor.js +89 -9
- package/dist/operation-journal.d.ts +19 -5
- package/dist/operation-journal.js +435 -73
- package/dist/operation-manager.d.ts +7 -2
- package/dist/operation-manager.js +215 -16
- package/dist/protocol.d.ts +16 -3
- package/dist/protocol.js +25 -1
- package/dist/remote-client.d.ts +4 -2
- package/dist/remote-client.js +155 -13
- package/dist/result-bundle.d.ts +21 -0
- package/dist/result-bundle.js +320 -0
- package/dist/result-contract.d.ts +398 -4
- package/dist/result-contract.js +175 -19
- package/dist/server.d.ts +17 -0
- package/dist/server.js +45 -1
- package/dist/tools.d.ts +7 -3
- package/dist/tools.js +77 -1
- package/package.json +1 -1
package/dist/artifact-store.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 {};
|