@cueai/omni-reader-mcp 1.5.4 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -39
- package/dist/artifact-store.d.ts +2 -0
- package/dist/artifact-store.js +45 -10
- package/dist/cli/agent-config.js +2 -2
- package/dist/cli/arguments.d.ts +8 -4
- package/dist/cli/arguments.js +62 -7
- package/dist/cli/config-inspection.d.ts +59 -0
- package/dist/cli/config-inspection.js +307 -0
- package/dist/cli/doctor.d.ts +7 -0
- package/dist/cli/doctor.js +37 -2
- package/dist/cli/setup.js +13 -2
- package/dist/constants.d.ts +2 -1
- package/dist/constants.js +4 -3
- package/dist/cube-client.js +1 -1
- package/dist/cursor.d.ts +4 -0
- package/dist/cursor.js +11 -13
- package/dist/index.js +10 -1
- package/dist/operation-journal.d.ts +4 -1
- package/dist/operation-journal.js +85 -15
- package/dist/operation-manager.d.ts +6 -2
- package/dist/operation-manager.js +246 -63
- package/dist/path-normalization.d.ts +5 -0
- package/dist/path-normalization.js +25 -0
- package/dist/path-security.d.ts +2 -1
- package/dist/path-security.js +49 -28
- package/dist/protocol.d.ts +10 -2
- package/dist/protocol.js +19 -7
- package/dist/result-contract.d.ts +46 -6
- package/dist/result-contract.js +118 -3
- package/dist/task-runtime.d.ts +3 -2
- package/dist/task-runtime.js +2 -2
- package/dist/tools.d.ts +4 -0
- package/dist/tools.js +37 -16
- package/package.json +14 -1
|
@@ -4,8 +4,8 @@ import { OmniBridgeError } from "./errors.js";
|
|
|
4
4
|
import { LEGACY_RECOVERY_FAILURE_CODE } from "./operation-journal.js";
|
|
5
5
|
import { openAllowedFile, } from "./path-security.js";
|
|
6
6
|
import { NOOP_PROGRESS } from "./progress.js";
|
|
7
|
-
import { normalizeRepresentation } from "./protocol.js";
|
|
8
|
-
import { localResultToResultField } from "./result-contract.js";
|
|
7
|
+
import { normalizeRepresentation, } from "./protocol.js";
|
|
8
|
+
import { LOCAL_ARTIFACT_RETENTION_WARNING, createFlatLocalResultCache, localResultToResultField, presentRetainedResult, } from "./result-contract.js";
|
|
9
9
|
const TERMINAL_STATES = new Set([
|
|
10
10
|
"COMPLETED",
|
|
11
11
|
"FAILED",
|
|
@@ -217,30 +217,52 @@ function resultFromRecord(record) {
|
|
|
217
217
|
export class OperationManager {
|
|
218
218
|
#journal;
|
|
219
219
|
#driver;
|
|
220
|
+
#presentResult;
|
|
220
221
|
#now;
|
|
221
222
|
#sleep;
|
|
222
223
|
#submissions = new Map();
|
|
224
|
+
#submissionDelivery = new Map();
|
|
223
225
|
#executions = new Map();
|
|
224
226
|
#results = new Map();
|
|
225
227
|
constructor(options) {
|
|
226
228
|
this.#journal = options.journal;
|
|
227
229
|
this.#driver = options.driver;
|
|
230
|
+
this.#presentResult = options.presentResult ?? (async (_record, result) => result);
|
|
228
231
|
this.#now = options.now ?? Date.now;
|
|
229
232
|
this.#sleep = options.sleep ?? defaultSleep;
|
|
230
233
|
}
|
|
234
|
+
async #strengthenResultDelivery(record, requested) {
|
|
235
|
+
if (requested === "auto" || record.resultDeliveryEffective === "artifact") {
|
|
236
|
+
return record;
|
|
237
|
+
}
|
|
238
|
+
if (record.requestIdentityHmac === null
|
|
239
|
+
&& record.errorCode === LEGACY_RECOVERY_FAILURE_CODE) {
|
|
240
|
+
return record;
|
|
241
|
+
}
|
|
242
|
+
return this.#journal.strengthenResultDelivery(record.clientRequestId, requested);
|
|
243
|
+
}
|
|
231
244
|
async submit(input) {
|
|
232
245
|
const requestHash = operationRequestHash(input);
|
|
246
|
+
const requestedDelivery = input.resultDelivery ?? "auto";
|
|
247
|
+
if (requestedDelivery === "artifact"
|
|
248
|
+
|| this.#submissionDelivery.get(requestHash) === undefined) {
|
|
249
|
+
this.#submissionDelivery.set(requestHash, requestedDelivery);
|
|
250
|
+
}
|
|
233
251
|
const active = this.#submissions.get(requestHash);
|
|
234
|
-
if (active !== undefined)
|
|
235
|
-
|
|
252
|
+
if (active !== undefined) {
|
|
253
|
+
const record = await active;
|
|
254
|
+
return this.#strengthenResultDelivery(record, requestedDelivery);
|
|
255
|
+
}
|
|
236
256
|
const pending = this.#submit(input);
|
|
237
257
|
this.#submissions.set(requestHash, pending);
|
|
238
258
|
try {
|
|
239
|
-
|
|
259
|
+
const record = await pending;
|
|
260
|
+
return await this.#strengthenResultDelivery(record, this.#submissionDelivery.get(requestHash) ?? requestedDelivery);
|
|
240
261
|
}
|
|
241
262
|
finally {
|
|
242
263
|
if (this.#submissions.get(requestHash) === pending) {
|
|
243
264
|
this.#submissions.delete(requestHash);
|
|
265
|
+
this.#submissionDelivery.delete(requestHash);
|
|
244
266
|
}
|
|
245
267
|
}
|
|
246
268
|
}
|
|
@@ -282,7 +304,7 @@ export class OperationManager {
|
|
|
282
304
|
effectiveInput = { ...input, clientRequestId: matching.clientRequestId };
|
|
283
305
|
}
|
|
284
306
|
else {
|
|
285
|
-
record = await this.#journal.beginIntent(input.clientRequestId, identityJson, input.sourceKind, sourceLocator, representation);
|
|
307
|
+
record = await this.#journal.beginIntent(input.clientRequestId, identityJson, input.sourceKind, sourceLocator, representation, input.resultDelivery ?? "auto");
|
|
286
308
|
}
|
|
287
309
|
}
|
|
288
310
|
else if (record.requestIdentityHmac === null) {
|
|
@@ -291,18 +313,26 @@ export class OperationManager {
|
|
|
291
313
|
// recovery failure. It can never be resumed as grounded/layout.
|
|
292
314
|
const migrated = await this.#journal.migrateLegacyRecord(input.clientRequestId, identityJson, sourceLocator);
|
|
293
315
|
record = migrated === null
|
|
294
|
-
? await this.#journal.beginIntent(input.clientRequestId, identityJson, input.sourceKind, sourceLocator, representation)
|
|
316
|
+
? await this.#journal.beginIntent(input.clientRequestId, identityJson, input.sourceKind, sourceLocator, representation, input.resultDelivery ?? "auto")
|
|
295
317
|
: migrated;
|
|
296
318
|
if (TERMINAL_STATES.has(record.state))
|
|
297
319
|
return record;
|
|
298
320
|
}
|
|
299
321
|
else {
|
|
300
|
-
record = await this.#journal.beginIntent(input.clientRequestId, identityJson, input.sourceKind, sourceLocator, representation);
|
|
322
|
+
record = await this.#journal.beginIntent(input.clientRequestId, identityJson, input.sourceKind, sourceLocator, representation, input.resultDelivery ?? "auto");
|
|
301
323
|
}
|
|
324
|
+
record = await this.#strengthenResultDelivery(record, input.resultDelivery ?? "auto");
|
|
302
325
|
const signal = effectiveInput.signal ?? new AbortController().signal;
|
|
303
326
|
if (TERMINAL_STATES.has(record.state))
|
|
304
327
|
return record;
|
|
305
328
|
if (record.state !== "CREATED") {
|
|
329
|
+
if (record.sourceKind === "url"
|
|
330
|
+
&& record.operationId !== null) {
|
|
331
|
+
// The remote server already owns this operation. A repeated parse call
|
|
332
|
+
// would create or attach to a second remote submission; callers must
|
|
333
|
+
// reuse the journaled operation and continue through status instead.
|
|
334
|
+
return record;
|
|
335
|
+
}
|
|
306
336
|
if (record.operationId !== null && this.#executions.has(record.operationId)) {
|
|
307
337
|
return record;
|
|
308
338
|
}
|
|
@@ -334,6 +364,14 @@ export class OperationManager {
|
|
|
334
364
|
throw structured;
|
|
335
365
|
}
|
|
336
366
|
}
|
|
367
|
+
async #present(record, canonicalResult) {
|
|
368
|
+
if (canonicalResult.status === "processing")
|
|
369
|
+
return canonicalResult;
|
|
370
|
+
const latest = record.operationId === null
|
|
371
|
+
? await this.#journal.loadByRequestId(record.clientRequestId)
|
|
372
|
+
: await this.#journal.loadByOperationId(record.operationId);
|
|
373
|
+
return this.#presentResult(latest ?? record, canonicalResult);
|
|
374
|
+
}
|
|
337
375
|
async submitResult(input, foregroundBudgetMs = FOREGROUND_BUDGET_MS) {
|
|
338
376
|
const startedAt = this.#now();
|
|
339
377
|
let record = await this.submit(input);
|
|
@@ -341,19 +379,20 @@ export class OperationManager {
|
|
|
341
379
|
? undefined
|
|
342
380
|
: this.#results.get(record.operationId);
|
|
343
381
|
if (existingResult !== undefined)
|
|
344
|
-
return existingResult;
|
|
382
|
+
return this.#present(record, existingResult);
|
|
345
383
|
const execution = record.operationId === null
|
|
346
384
|
? undefined
|
|
347
385
|
: this.#executions.get(record.operationId);
|
|
348
386
|
if (execution === undefined) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
387
|
+
if (TERMINAL_STATES.has(record.state) && record.operationId !== null) {
|
|
388
|
+
return this.statusResult(record.operationId, 0, input.signal);
|
|
389
|
+
}
|
|
390
|
+
return this.#present(record, resultFromRecord(record));
|
|
352
391
|
}
|
|
353
392
|
const remaining = Math.max(0, foregroundBudgetMs - (this.#now() - startedAt));
|
|
354
393
|
if (remaining === 0) {
|
|
355
394
|
record = await this.#applyUpdate(record, execution.handle.snapshot());
|
|
356
|
-
return resultFromRecord(record);
|
|
395
|
+
return this.#present(record, resultFromRecord(record));
|
|
357
396
|
}
|
|
358
397
|
const winner = await Promise.race([
|
|
359
398
|
execution.settled.then((result) => ({ kind: "completed", result })),
|
|
@@ -361,76 +400,80 @@ export class OperationManager {
|
|
|
361
400
|
]);
|
|
362
401
|
if (winner.kind === "completed") {
|
|
363
402
|
return this.#now() - startedAt < foregroundBudgetMs
|
|
364
|
-
? winner.result
|
|
365
|
-
: resultFromRecord(record);
|
|
403
|
+
? this.#present(record, winner.result)
|
|
404
|
+
: this.#present(record, resultFromRecord(record));
|
|
366
405
|
}
|
|
367
406
|
const current = await this.#journal.loadByOperationId(record.operationId);
|
|
368
407
|
if (current !== null && !TERMINAL_STATES.has(current.state)) {
|
|
369
408
|
record = await this.#applyUpdate(current, execution.handle.snapshot());
|
|
370
409
|
}
|
|
371
|
-
|
|
372
|
-
record = current;
|
|
373
|
-
}
|
|
374
|
-
return resultFromRecord(record);
|
|
410
|
+
return this.#present(record, resultFromRecord(record));
|
|
375
411
|
}
|
|
376
412
|
async statusResult(operationId, waitMs = 0, signal = new AbortController().signal) {
|
|
377
413
|
const boundedWait = Math.min(STATUS_LONG_POLL_MAX_MS, Math.max(0, waitMs));
|
|
378
414
|
let record = await this.#existingOperation(operationId);
|
|
379
415
|
const cached = this.#results.get(operationId);
|
|
380
|
-
if (cached !== undefined && TERMINAL_STATES.has(record.state))
|
|
381
|
-
return cached;
|
|
416
|
+
if (cached !== undefined && TERMINAL_STATES.has(record.state)) {
|
|
417
|
+
return this.#present(record, cached);
|
|
418
|
+
}
|
|
382
419
|
if (TERMINAL_STATES.has(record.state)) {
|
|
383
420
|
const recovered = await this.#driver.result?.(record, signal);
|
|
384
421
|
if (recovered !== undefined) {
|
|
385
422
|
this.#results.set(operationId, recovered);
|
|
386
|
-
return recovered;
|
|
423
|
+
return this.#present(record, recovered);
|
|
387
424
|
}
|
|
388
|
-
return resultFromRecord(record);
|
|
425
|
+
return this.#present(record, resultFromRecord(record));
|
|
389
426
|
}
|
|
390
427
|
if (record.resultExpiresAt !== null &&
|
|
391
428
|
Date.parse(record.resultExpiresAt) <= this.#now()) {
|
|
392
429
|
record = await this.#journal.transition(record.clientRequestId, record.state, "EXPIRED", { errorCode: "RESULT_EXPIRED" });
|
|
393
|
-
return resultFromRecord(record);
|
|
430
|
+
return this.#present(record, resultFromRecord(record));
|
|
394
431
|
}
|
|
395
432
|
let execution = this.#executions.get(operationId);
|
|
396
433
|
if (execution === undefined) {
|
|
397
434
|
record = await this.status(operationId, boundedWait, signal);
|
|
398
435
|
const statusResult = this.#results.get(operationId);
|
|
399
436
|
if (statusResult !== undefined && TERMINAL_STATES.has(record.state)) {
|
|
400
|
-
return statusResult;
|
|
437
|
+
return this.#present(record, statusResult);
|
|
401
438
|
}
|
|
402
439
|
execution = this.#executions.get(operationId);
|
|
403
|
-
if (execution === undefined)
|
|
404
|
-
return statusResult ?? resultFromRecord(record);
|
|
440
|
+
if (execution === undefined) {
|
|
441
|
+
return this.#present(record, statusResult ?? resultFromRecord(record));
|
|
442
|
+
}
|
|
405
443
|
}
|
|
406
444
|
if (!TERMINAL_STATES.has(record.state)) {
|
|
407
445
|
record = await this.#applyUpdate(record, execution.handle.snapshot());
|
|
408
446
|
}
|
|
409
|
-
if (boundedWait === 0)
|
|
410
|
-
return this.#results.get(operationId) ?? resultFromRecord(record);
|
|
447
|
+
if (boundedWait === 0) {
|
|
448
|
+
return this.#present(record, this.#results.get(operationId) ?? resultFromRecord(record));
|
|
449
|
+
}
|
|
411
450
|
const winner = await Promise.race([
|
|
412
451
|
execution.settled.then((value) => ({ kind: "completed", value })),
|
|
413
452
|
this.#sleep(boundedWait).then(() => ({ kind: "timeout" })),
|
|
414
453
|
]);
|
|
415
454
|
if (winner.kind === "completed")
|
|
416
|
-
return winner.value;
|
|
455
|
+
return this.#present(record, winner.value);
|
|
417
456
|
const current = await this.#existingOperation(operationId);
|
|
418
457
|
record = TERMINAL_STATES.has(current.state)
|
|
419
458
|
? current
|
|
420
459
|
: await this.#applyUpdate(current, execution.handle.snapshot());
|
|
421
|
-
return this.#results.get(operationId) ?? resultFromRecord(record);
|
|
460
|
+
return this.#present(record, this.#results.get(operationId) ?? resultFromRecord(record));
|
|
422
461
|
}
|
|
423
462
|
async cancelResult(operationId, signal = new AbortController().signal) {
|
|
463
|
+
let record = await this.#existingOperation(operationId);
|
|
424
464
|
const existing = this.#results.get(operationId);
|
|
425
|
-
if (existing !== undefined && existing.status !== "processing")
|
|
426
|
-
return existing;
|
|
465
|
+
if (existing !== undefined && existing.status !== "processing") {
|
|
466
|
+
return this.#present(record, existing);
|
|
467
|
+
}
|
|
427
468
|
const execution = this.#executions.get(operationId);
|
|
428
469
|
if (execution !== undefined) {
|
|
429
470
|
await execution.handle.cancel();
|
|
430
|
-
|
|
471
|
+
const settled = await execution.settled;
|
|
472
|
+
record = await this.#existingOperation(operationId);
|
|
473
|
+
return this.#present(record, settled);
|
|
431
474
|
}
|
|
432
|
-
|
|
433
|
-
return this.#results.get(operationId) ?? resultFromRecord(record);
|
|
475
|
+
record = await this.cancel(operationId, signal);
|
|
476
|
+
return this.#present(record, this.#results.get(operationId) ?? resultFromRecord(record));
|
|
434
477
|
}
|
|
435
478
|
async status(operationId, waitMs, signal = new AbortController().signal) {
|
|
436
479
|
const record = await this.#existingOperation(operationId);
|
|
@@ -537,18 +580,10 @@ export class OperationManager {
|
|
|
537
580
|
}
|
|
538
581
|
#shouldReuseByRequestHash(record) {
|
|
539
582
|
if (!TERMINAL_STATES.has(record.state)) {
|
|
540
|
-
// Local uploads may resume mid-flight
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
// server owns it and a fresh parse creates a NEW operation id — reusing
|
|
545
|
-
// the stale record pairs old op_id with the server's new one →
|
|
546
|
-
// "changed the operation identifier" → REMOTE_PROTOCOL_ERROR
|
|
547
|
-
// (dogfood 2026-08-06). Reuse URL non-terminal records only when no
|
|
548
|
-
// operation id has been issued yet.
|
|
549
|
-
if (record.sourceKind === "url") {
|
|
550
|
-
return record.operationId === null;
|
|
551
|
-
}
|
|
583
|
+
// Local uploads may resume mid-flight. URL request identities must also
|
|
584
|
+
// reuse the existing record: before an operation id exists, the retry
|
|
585
|
+
// keeps the same server idempotency key; after it exists, #submit returns
|
|
586
|
+
// the record without calling parse again and callers continue by status.
|
|
552
587
|
return true;
|
|
553
588
|
}
|
|
554
589
|
if (record.state !== "COMPLETED" || record.operationId === null)
|
|
@@ -663,18 +698,34 @@ export async function hydrateInlineUrlResult(result, artifactStore) {
|
|
|
663
698
|
...start,
|
|
664
699
|
resultDigest: `sha256:${createHash("sha256").update(bytes).digest("hex")}`,
|
|
665
700
|
});
|
|
666
|
-
|
|
701
|
+
const local = retention.result();
|
|
702
|
+
return {
|
|
703
|
+
...result,
|
|
704
|
+
result: localResultToResultField(local),
|
|
705
|
+
local_result_cache: createFlatLocalResultCache({
|
|
706
|
+
resultId: local.resultId,
|
|
707
|
+
resultBytes: local.resultBytes,
|
|
708
|
+
expiresAt: local.expiresAt,
|
|
709
|
+
}),
|
|
710
|
+
};
|
|
667
711
|
}
|
|
668
712
|
// hydrateInlineUrlResult is a pure local-storage optimization on top of content the caller
|
|
669
713
|
// already has in full -- a local-disk failure here (e.g. disk full, permission error) must never
|
|
670
|
-
// turn an otherwise-successful URL parse into a failure.
|
|
671
|
-
//
|
|
672
|
-
// they just lose local read_result/read_outline re-read capability for that particular result.
|
|
714
|
+
// turn an otherwise-successful URL parse into a failure. Preserve the delivered inline content
|
|
715
|
+
// and expose one exact warning instead of silently advertising unavailable local actions.
|
|
673
716
|
export async function hydrateInlineUrlResultSafely(result, artifactStore) {
|
|
674
717
|
try {
|
|
675
718
|
return await hydrateInlineUrlResult(result, artifactStore);
|
|
676
719
|
}
|
|
677
720
|
catch {
|
|
721
|
+
if ((result.status === "completed" || result.status === "cleanup_pending")
|
|
722
|
+
&& result.result?.kind === "inline") {
|
|
723
|
+
const { local_result_cache: _unavailableCache, ...delivered } = result;
|
|
724
|
+
return {
|
|
725
|
+
...delivered,
|
|
726
|
+
delivery_warning: LOCAL_ARTIFACT_RETENTION_WARNING,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
678
729
|
return result;
|
|
679
730
|
}
|
|
680
731
|
}
|
|
@@ -723,6 +774,13 @@ function remoteFailure(result) {
|
|
|
723
774
|
...(error.constraints === undefined ? {} : { constraints: error.constraints }),
|
|
724
775
|
});
|
|
725
776
|
}
|
|
777
|
+
function retainedFlatResultId(result) {
|
|
778
|
+
if (result.status !== "completed" && result.status !== "cleanup_pending") {
|
|
779
|
+
return undefined;
|
|
780
|
+
}
|
|
781
|
+
const cache = result.local_result_cache;
|
|
782
|
+
return cache !== undefined && "result_id" in cache ? cache.result_id : undefined;
|
|
783
|
+
}
|
|
726
784
|
function remoteResultUpdate(result, expectedOperationId) {
|
|
727
785
|
if (result.status === "failed")
|
|
728
786
|
throw remoteFailure(result);
|
|
@@ -770,6 +828,9 @@ function remoteResultUpdate(result, expectedOperationId) {
|
|
|
770
828
|
processingCopy: "deleted",
|
|
771
829
|
temporaryData: "deleted",
|
|
772
830
|
deliveryResult: "deleted_after_ack",
|
|
831
|
+
...(retainedFlatResultId(result) === undefined
|
|
832
|
+
? {}
|
|
833
|
+
: { resultId: retainedFlatResultId(result) }),
|
|
773
834
|
},
|
|
774
835
|
result,
|
|
775
836
|
};
|
|
@@ -793,6 +854,9 @@ function remoteResultUpdate(result, expectedOperationId) {
|
|
|
793
854
|
temporaryData: result.data_handling.temporary_data,
|
|
794
855
|
deliveryResult: "pending",
|
|
795
856
|
resultExpiresAt: result.cleanup_deadline,
|
|
857
|
+
...(retainedFlatResultId(result) === undefined
|
|
858
|
+
? {}
|
|
859
|
+
: { resultId: retainedFlatResultId(result) }),
|
|
796
860
|
},
|
|
797
861
|
result,
|
|
798
862
|
};
|
|
@@ -890,6 +954,19 @@ function localResultValue(local) {
|
|
|
890
954
|
next_cursor: local.nextCursor,
|
|
891
955
|
};
|
|
892
956
|
}
|
|
957
|
+
function localResultCache(local) {
|
|
958
|
+
if (local.kind === "bundle") {
|
|
959
|
+
return {
|
|
960
|
+
expires_at: local.expiresAt,
|
|
961
|
+
discard_action: "discard_result",
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
return createFlatLocalResultCache({
|
|
965
|
+
resultId: local.resultId,
|
|
966
|
+
resultBytes: local.resultBytes,
|
|
967
|
+
expiresAt: local.expiresAt,
|
|
968
|
+
});
|
|
969
|
+
}
|
|
893
970
|
function completedLocalParse(local) {
|
|
894
971
|
return {
|
|
895
972
|
status: "completed",
|
|
@@ -902,12 +979,7 @@ function completedLocalParse(local) {
|
|
|
902
979
|
original_source: "unchanged",
|
|
903
980
|
remote_content_retained: false,
|
|
904
981
|
},
|
|
905
|
-
|
|
906
|
-
local_result_cache: {
|
|
907
|
-
expires_at: local.expiresAt,
|
|
908
|
-
discard_action: "discard_result",
|
|
909
|
-
},
|
|
910
|
-
} : {}),
|
|
982
|
+
local_result_cache: localResultCache(local),
|
|
911
983
|
};
|
|
912
984
|
}
|
|
913
985
|
function cleanupPendingParse(local, cleanupDeadline) {
|
|
@@ -922,6 +994,39 @@ function cleanupPendingParse(local, cleanupDeadline) {
|
|
|
922
994
|
delivery_result: "pending",
|
|
923
995
|
original_source: "unchanged",
|
|
924
996
|
},
|
|
997
|
+
local_result_cache: localResultCache(local),
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
function withRecoveredRemoteResult(result, local) {
|
|
1001
|
+
if (result.status !== "completed" && result.status !== "cleanup_pending") {
|
|
1002
|
+
return result;
|
|
1003
|
+
}
|
|
1004
|
+
return {
|
|
1005
|
+
...result,
|
|
1006
|
+
result: localResultValue(local),
|
|
1007
|
+
local_result_cache: localResultCache(local),
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
function recoveredRemoteParse(record, local) {
|
|
1011
|
+
if (record.operationId === null) {
|
|
1012
|
+
throw managerError("OPERATION_ID_UNAVAILABLE", "The remote parse operation identifier is unavailable.");
|
|
1013
|
+
}
|
|
1014
|
+
if (record.state === "CLEANUP_PENDING") {
|
|
1015
|
+
return {
|
|
1016
|
+
status: "cleanup_pending",
|
|
1017
|
+
operation_id: record.operationId,
|
|
1018
|
+
result: localResultValue(local),
|
|
1019
|
+
cleanup_deadline: record.resultExpiresAt ?? record.expiresAt ?? new Date(0).toISOString(),
|
|
1020
|
+
data_handling: recordDataHandling(record),
|
|
1021
|
+
local_result_cache: localResultCache(local),
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
return {
|
|
1025
|
+
status: "completed",
|
|
1026
|
+
operation_id: record.operationId,
|
|
1027
|
+
result: localResultValue(local),
|
|
1028
|
+
data_handling: recordDataHandling(record),
|
|
1029
|
+
local_result_cache: localResultCache(local),
|
|
925
1030
|
};
|
|
926
1031
|
}
|
|
927
1032
|
function canceledParse(operationId, cleanupDeadline) {
|
|
@@ -1033,6 +1138,30 @@ export function createLocalParseOperationManager(options) {
|
|
|
1033
1138
|
}
|
|
1034
1139
|
throw managerError("JOURNAL_STATE_CONFLICT", "The operation changed repeatedly while saving a delivery checkpoint.", { operationCreated: true, retryable: true });
|
|
1035
1140
|
}
|
|
1141
|
+
async function recoverArtifactAccess(record, action) {
|
|
1142
|
+
try {
|
|
1143
|
+
return await action();
|
|
1144
|
+
}
|
|
1145
|
+
catch (error) {
|
|
1146
|
+
const local = localFailure(error);
|
|
1147
|
+
throw new OmniBridgeError({
|
|
1148
|
+
code: local.code,
|
|
1149
|
+
message: local.message,
|
|
1150
|
+
failureScope: local.failureScope,
|
|
1151
|
+
sourceKind: local.sourceKind,
|
|
1152
|
+
userAction: local.userAction,
|
|
1153
|
+
requestId: local.requestId,
|
|
1154
|
+
operationCreated: local.operationCreated || record.operationId !== null,
|
|
1155
|
+
fileUploaded: local.fileUploaded || record.fileUploaded,
|
|
1156
|
+
parserStarted: local.parserStarted || record.parserStarted,
|
|
1157
|
+
billed: local.billed || record.billed,
|
|
1158
|
+
contentReleased: local.contentReleased || record.contentReleased,
|
|
1159
|
+
retryable: local.retryable,
|
|
1160
|
+
retryAfter: local.retryAfter,
|
|
1161
|
+
constraints: local.constraints,
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1036
1165
|
// A retained logical bundle is reconstructed through its closed descriptor
|
|
1037
1166
|
// and keeps the stored representation tuple; without the descriptor the
|
|
1038
1167
|
// recovery fails safe instead of degrading the bundle to text.
|
|
@@ -1056,7 +1185,7 @@ export function createLocalParseOperationManager(options) {
|
|
|
1056
1185
|
contentReleased: record.contentReleased,
|
|
1057
1186
|
});
|
|
1058
1187
|
}
|
|
1059
|
-
const descriptor = await options.artifactStore.readBundleDescriptor(record.resultId);
|
|
1188
|
+
const descriptor = await recoverArtifactAccess(record, () => options.artifactStore.readBundleDescriptor(record.resultId));
|
|
1060
1189
|
if (descriptor === null || descriptor.kind !== "bundle") {
|
|
1061
1190
|
throw managerError("RESULT_NOT_AVAILABLE", "The retained local result bundle is unavailable.", {
|
|
1062
1191
|
operationCreated: true,
|
|
@@ -1068,11 +1197,11 @@ export function createLocalParseOperationManager(options) {
|
|
|
1068
1197
|
}
|
|
1069
1198
|
return descriptor;
|
|
1070
1199
|
}
|
|
1071
|
-
const preview = await options.artifactStore.read(record.resultId, undefined, 2_048);
|
|
1200
|
+
const preview = await recoverArtifactAccess(record, () => options.artifactStore.read(record.resultId, undefined, 2_048));
|
|
1072
1201
|
if (preview.resultBytes <= INLINE_RESULT_MAX_BYTES) {
|
|
1073
1202
|
const recovered = preview.nextCursor === undefined
|
|
1074
1203
|
? preview
|
|
1075
|
-
: await options.artifactStore.read(record.resultId, undefined, INLINE_RESULT_MAX_BYTES);
|
|
1204
|
+
: await recoverArtifactAccess(record, () => options.artifactStore.read(record.resultId, undefined, INLINE_RESULT_MAX_BYTES));
|
|
1076
1205
|
if (recovered.nextCursor !== undefined) {
|
|
1077
1206
|
throw managerError("LOCAL_RESULT_INTEGRITY_FAILED", "The retained inline result was not fully readable.", {
|
|
1078
1207
|
operationCreated: true,
|
|
@@ -1101,6 +1230,16 @@ export function createLocalParseOperationManager(options) {
|
|
|
1101
1230
|
...(preview.nextCursor === undefined ? {} : { nextCursor: preview.nextCursor }),
|
|
1102
1231
|
};
|
|
1103
1232
|
}
|
|
1233
|
+
async function preflightArtifactDelivery(input, recovery) {
|
|
1234
|
+
const delivery = recovery?.resultDeliveryEffective ?? input.resultDelivery ?? "auto";
|
|
1235
|
+
if (delivery !== "artifact")
|
|
1236
|
+
return;
|
|
1237
|
+
if (options.artifactStore.preflightWrite === undefined
|
|
1238
|
+
|| options.artifactStore.mintReadCursor === undefined) {
|
|
1239
|
+
throw managerError("ARTIFACT_PREFLIGHT_FAILED", "The local result store cannot provide artifact delivery.");
|
|
1240
|
+
}
|
|
1241
|
+
await options.artifactStore.preflightWrite();
|
|
1242
|
+
}
|
|
1104
1243
|
async function startExecution(input, recovery) {
|
|
1105
1244
|
// Recovery keeps the stored representation tuple exactly: status, download
|
|
1106
1245
|
// (recoverLocalArtifact), and ACK all operate on the representation the
|
|
@@ -1136,6 +1275,7 @@ export function createLocalParseOperationManager(options) {
|
|
|
1136
1275
|
throw managerError("SOURCE_CHANGED_DURING_SUBMISSION", "The local source changed before the upload operation was created.", { retryable: true });
|
|
1137
1276
|
}
|
|
1138
1277
|
try {
|
|
1278
|
+
await preflightArtifactDelivery(input, recovery);
|
|
1139
1279
|
// D2-D Task 14: the normalized detail reaches the grant boundary so
|
|
1140
1280
|
// CubeGrantClient selects the exact direct profile BEFORE constructing
|
|
1141
1281
|
// the v3 request and requires the v3 response tuple before any upload.
|
|
@@ -1492,6 +1632,7 @@ export function createLocalParseOperationManager(options) {
|
|
|
1492
1632
|
assertSameRepresentation(input, recovery);
|
|
1493
1633
|
}
|
|
1494
1634
|
const context = remoteContext(input.context);
|
|
1635
|
+
await preflightArtifactDelivery(input, recovery);
|
|
1495
1636
|
const result = await options.remoteClient.parse(context.source, input.clientRequestId, input.signal ?? new AbortController().signal, representation.detail);
|
|
1496
1637
|
const hydrated = await hydrateInlineUrlResultSafely(result, options.artifactStore);
|
|
1497
1638
|
return remoteResultUpdate(hydrated, recovery?.operationId ?? undefined);
|
|
@@ -1507,9 +1648,30 @@ export function createLocalParseOperationManager(options) {
|
|
|
1507
1648
|
if (record.operationId === null)
|
|
1508
1649
|
return undefined;
|
|
1509
1650
|
if (record.sourceKind === "url") {
|
|
1510
|
-
if (options.remoteClient === undefined)
|
|
1511
|
-
|
|
1651
|
+
if (options.remoteClient === undefined) {
|
|
1652
|
+
if (record.resultId === null || record.state !== "CLEANUP_PENDING") {
|
|
1653
|
+
return undefined;
|
|
1654
|
+
}
|
|
1655
|
+
return {
|
|
1656
|
+
state: record.state,
|
|
1657
|
+
result: recoveredRemoteParse(record, await recoverLocalArtifact(record)),
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1512
1660
|
const result = await options.remoteClient.status(record.operationId, waitMs, signal);
|
|
1661
|
+
if (record.resultId !== null && record.state === "CLEANUP_PENDING") {
|
|
1662
|
+
let local;
|
|
1663
|
+
try {
|
|
1664
|
+
local = await recoverLocalArtifact(record);
|
|
1665
|
+
}
|
|
1666
|
+
catch {
|
|
1667
|
+
// A missing or corrupt retained copy must not block authoritative
|
|
1668
|
+
// remote status recovery. Rehydrate the returned body when it is
|
|
1669
|
+
// available, preserving the existing safe warning behavior.
|
|
1670
|
+
}
|
|
1671
|
+
if (local !== undefined) {
|
|
1672
|
+
return remoteResultUpdate(withRecoveredRemoteResult(result, local), record.operationId);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1513
1675
|
const hydrated = await hydrateInlineUrlResultSafely(result, options.artifactStore);
|
|
1514
1676
|
return remoteResultUpdate(hydrated, record.operationId);
|
|
1515
1677
|
}
|
|
@@ -1613,6 +1775,15 @@ export function createLocalParseOperationManager(options) {
|
|
|
1613
1775
|
if (record.sourceKind === "local" && record.resultId !== null) {
|
|
1614
1776
|
return completedLocalParse(await recoverLocalArtifact(record));
|
|
1615
1777
|
}
|
|
1778
|
+
if (record.sourceKind === "url" && record.resultId !== null) {
|
|
1779
|
+
try {
|
|
1780
|
+
return recoveredRemoteParse(record, await recoverLocalArtifact(record));
|
|
1781
|
+
}
|
|
1782
|
+
catch (error) {
|
|
1783
|
+
if (options.remoteClient === undefined)
|
|
1784
|
+
throw error;
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1616
1787
|
if (record.sourceKind === "url" && options.remoteClient !== undefined) {
|
|
1617
1788
|
const result = await options.remoteClient.status(record.operationId, 0, signal);
|
|
1618
1789
|
const hydrated = await hydrateInlineUrlResultSafely(result, options.artifactStore);
|
|
@@ -1624,6 +1795,18 @@ export function createLocalParseOperationManager(options) {
|
|
|
1624
1795
|
return new OperationManager({
|
|
1625
1796
|
journal: options.journal,
|
|
1626
1797
|
driver,
|
|
1798
|
+
presentResult: (record, canonicalResult) => presentRetainedResult(canonicalResult, record.resultDeliveryEffective, async (resultId, byteOffset) => {
|
|
1799
|
+
if (options.artifactStore.mintReadCursor === undefined) {
|
|
1800
|
+
throw managerError("LOCAL_RESULT_INTEGRITY_FAILED", "The retained result cursor capability is unavailable.", {
|
|
1801
|
+
operationCreated: record.operationId !== null,
|
|
1802
|
+
fileUploaded: record.fileUploaded,
|
|
1803
|
+
parserStarted: record.parserStarted,
|
|
1804
|
+
billed: record.billed,
|
|
1805
|
+
contentReleased: record.contentReleased,
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
return options.artifactStore.mintReadCursor(resultId, byteOffset);
|
|
1809
|
+
}),
|
|
1627
1810
|
now: () => now().getTime(),
|
|
1628
1811
|
sleep: options.sleep,
|
|
1629
1812
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
const MSYS_DRIVE_PATH = /^\/([A-Za-z])(?:\/|$)/u;
|
|
3
|
+
export class PathNormalizationError extends Error {
|
|
4
|
+
code = "INVALID_WINDOWS_PATH";
|
|
5
|
+
constructor() {
|
|
6
|
+
super("Use an absolute Windows drive path such as C:\\Reports or exact MSYS syntax such as /c/Reports.");
|
|
7
|
+
this.name = "PathNormalizationError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function normalizePlatformPath(value, platform = process.platform) {
|
|
11
|
+
if (platform !== "win32")
|
|
12
|
+
return value;
|
|
13
|
+
if (value.startsWith("//") || value.startsWith("\\\\"))
|
|
14
|
+
return value;
|
|
15
|
+
const match = MSYS_DRIVE_PATH.exec(value);
|
|
16
|
+
if (match !== null) {
|
|
17
|
+
const drive = match[1].toUpperCase();
|
|
18
|
+
const remainder = value.slice(match[0].length).replaceAll("/", "\\");
|
|
19
|
+
return path.win32.normalize(`${drive}:\\${remainder}`);
|
|
20
|
+
}
|
|
21
|
+
if (value.startsWith("/") || value.startsWith("\\")) {
|
|
22
|
+
throw new PathNormalizationError();
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
package/dist/path-security.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface OpenAllowedFileOptions {
|
|
|
9
9
|
workspace: string;
|
|
10
10
|
extraRoots?: readonly string[];
|
|
11
11
|
homeDirectory?: string;
|
|
12
|
+
platform?: NodeJS.Platform;
|
|
12
13
|
fileSystem?: PathSecurityFileSystem;
|
|
13
14
|
}
|
|
14
15
|
export interface OpenedAllowedFile {
|
|
@@ -19,5 +20,5 @@ export interface OpenedAllowedFile {
|
|
|
19
20
|
createReadStream(): ReadStream;
|
|
20
21
|
close(): Promise<void>;
|
|
21
22
|
}
|
|
22
|
-
export declare function splitAllowedRoots(value: string | undefined): string[];
|
|
23
|
+
export declare function splitAllowedRoots(value: string | undefined, platform?: NodeJS.Platform): string[];
|
|
23
24
|
export declare function openAllowedFile(input: string, options: OpenAllowedFileOptions): Promise<OpenedAllowedFile>;
|