@gajae-code/coding-agent 0.12.21 → 0.13.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/CHANGELOG.md +67 -2
- package/dist/types/cli/args.d.ts +5 -2
- package/dist/types/cli/root-flags.d.ts +246 -0
- package/dist/types/cli.d.ts +11 -19
- package/dist/types/commands/daemon.d.ts +0 -30
- package/dist/types/commands/launch.d.ts +11 -19
- package/dist/types/commands/quick-lane.d.ts +32 -0
- package/dist/types/commands/sdk.d.ts +21 -15
- package/dist/types/config/file-lock-gc.d.ts +2 -0
- package/dist/types/config/model-equivalence.d.ts +20 -0
- package/dist/types/config/model-profile-activation.d.ts +62 -17
- package/dist/types/config/model-profiles.d.ts +10 -0
- package/dist/types/config/model-registry.d.ts +68 -1
- package/dist/types/config/model-resolver.d.ts +15 -4
- package/dist/types/config/models-config-schema.d.ts +7 -2
- package/dist/types/config/provider-selection-policy.d.ts +62 -0
- package/dist/types/config/settings-schema.d.ts +36 -78
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/edit/renderer.d.ts +5 -2
- package/dist/types/edit/streaming.d.ts +13 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
- package/dist/types/extensibility/extensions/compact-handler.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +19 -5
- package/dist/types/internal-urls/local-protocol.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +0 -2
- package/dist/types/modes/components/provider-order-context.d.ts +45 -0
- package/dist/types/modes/components/provider-order-editor.d.ts +24 -0
- package/dist/types/modes/components/settings-defs.d.ts +5 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/quick-lane/classify.d.ts +40 -0
- package/dist/types/runtime/stt-settings-setup.d.ts +18 -0
- package/dist/types/sdk/broker/broker.d.ts +56 -2
- package/dist/types/sdk/broker/operation-receipt-ledger.d.ts +25 -0
- package/dist/types/sdk/broker/session-index.d.ts +83 -2
- package/dist/types/sdk/broker/state-version.d.ts +1 -1
- package/dist/types/sdk/bus/notification-service.d.ts +6 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +8 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +26 -0
- package/dist/types/sdk/cli/rows.d.ts +98 -0
- package/dist/types/sdk/cli/session-cli.d.ts +35 -3
- package/dist/types/sdk/elevation/allowlist.d.ts +20 -0
- package/dist/types/sdk/elevation/audit.d.ts +93 -0
- package/dist/types/sdk/elevation/capability.d.ts +3 -0
- package/dist/types/sdk/elevation/digest.d.ts +72 -0
- package/dist/types/sdk/elevation/dispatch-receipt.d.ts +85 -0
- package/dist/types/sdk/elevation/grant-ledger.d.ts +163 -0
- package/dist/types/sdk/elevation/owner.d.ts +48 -0
- package/dist/types/sdk/guides/cache.d.ts +98 -0
- package/dist/types/sdk/guides/catalog.d.ts +156 -0
- package/dist/types/sdk/guides/cli.d.ts +39 -0
- package/dist/types/sdk/guides/manifest.d.ts +66 -0
- package/dist/types/sdk/guides/verify.d.ts +66 -0
- package/dist/types/sdk/host/control/dispatch.d.ts +8 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +3 -2
- package/dist/types/sdk/host/query/cursor.d.ts +10 -0
- package/dist/types/sdk/host/query/handlers.d.ts +21 -0
- package/dist/types/sdk/host/session-runtime.d.ts +9 -0
- package/dist/types/sdk/mcp/server.d.ts +2 -2
- package/dist/types/sdk/prompt-status.d.ts +11 -0
- package/dist/types/sdk/session.d.ts +4 -0
- package/dist/types/sdk/transport/index.d.ts +1 -0
- package/dist/types/sdk/transport/relay.d.ts +1 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +20 -1
- package/dist/types/session/agent-session.d.ts +32 -18
- package/dist/types/session/blob-store.d.ts +13 -7
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +25 -1
- package/dist/types/session/internal/session-memory-sidecar.d.ts +5 -5
- package/dist/types/session/session-manager.d.ts +139 -10
- package/dist/types/session/session-storage.d.ts +48 -0
- package/dist/types/setup/provider-onboarding.d.ts +7 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +14 -0
- package/dist/types/stt/setup.d.ts +2 -0
- package/dist/types/task/executor.d.ts +10 -2
- package/dist/types/tools/browser/runtime-diagnostics.d.ts +65 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -0
- package/dist/types/tools/browser.d.ts +2 -0
- package/dist/types/tools/image-gen.d.ts +14 -18
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resource-gc.d.ts +2 -0
- package/dist/types/utils/pasted-image-path.d.ts +6 -0
- package/package.json +9 -9
- package/scripts/generate-sdk-operation-inventory.ts +8 -0
- package/scripts/run-test-manifest.ts +61 -21
- package/scripts/verify-gjc-sdk-canonicalization.ts +9 -4
- package/src/cli/args.ts +62 -9
- package/src/cli/completion-cli.ts +1 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/root-flags.ts +100 -0
- package/src/cli/setup-cli.ts +6 -2
- package/src/cli.ts +42 -65
- package/src/commands/acp.ts +1 -1
- package/src/commands/daemon.ts +2 -31
- package/src/commands/gc.ts +1 -1
- package/src/commands/launch.ts +8 -129
- package/src/commands/quick-lane.ts +51 -0
- package/src/commands/sdk.ts +200 -26
- package/src/commands/setup.ts +4 -2
- package/src/config/file-lock-gc.ts +4 -1
- package/src/config/model-equivalence.ts +76 -7
- package/src/config/model-profile-activation.ts +1115 -265
- package/src/config/model-profiles.ts +134 -2
- package/src/config/model-registry.ts +352 -50
- package/src/config/model-resolver.ts +289 -38
- package/src/config/models-config-schema.ts +11 -4
- package/src/config/provider-selection-policy.ts +146 -0
- package/src/config/settings-schema.ts +50 -77
- package/src/config/settings.ts +40 -0
- package/src/coordinator-mcp/server.ts +32 -2
- package/src/edit/renderer.ts +175 -223
- package/src/edit/streaming.ts +108 -1
- package/src/export/html/index.ts +57 -7
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/extensions/compact-handler.ts +3 -2
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/gjc-runtime/gc-runtime.ts +379 -20
- package/src/gjc-runtime/launch-worktree.ts +5 -1
- package/src/gjc-runtime/team-runtime.ts +59 -2
- package/src/gjc-runtime/tmux-owner-isolation.ts +19 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +16 -1
- package/src/internal-urls/docs-index.generated.ts +17 -13
- package/src/internal-urls/local-protocol.ts +1 -0
- package/src/main.ts +40 -3
- package/src/memories/index.ts +4 -2
- package/src/modes/acp/acp-agent.ts +235 -91
- package/src/modes/components/model-selector.ts +206 -42
- package/src/modes/components/provider-order-context.ts +197 -0
- package/src/modes/components/provider-order-editor.ts +441 -0
- package/src/modes/components/settings-defs.ts +21 -1
- package/src/modes/components/settings-selector.ts +45 -2
- package/src/modes/components/tool-execution.ts +47 -15
- package/src/modes/controllers/command-controller.ts +6 -3
- package/src/modes/controllers/extension-ui-controller.ts +2 -0
- package/src/modes/controllers/input-controller.ts +73 -2
- package/src/modes/controllers/selector-controller.ts +260 -153
- package/src/modes/prompt-suggestion-controller.ts +1 -1
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +18 -0
- package/src/prompts/tools/browser.md +1 -0
- package/src/quick-lane/classify.ts +257 -0
- package/src/rlm/index.ts +1 -1
- package/src/runtime/stt-settings-setup.ts +57 -0
- package/src/sdk/acp/adapter.ts +2 -1
- package/src/sdk/broker/broker.ts +780 -12
- package/src/sdk/broker/lifecycle.ts +70 -29
- package/src/sdk/broker/operation-receipt-ledger.ts +144 -0
- package/src/sdk/broker/session-index.ts +463 -48
- package/src/sdk/broker/state-version.ts +13 -9
- package/src/sdk/broker/transport.ts +14 -1
- package/src/sdk/bus/index.ts +34 -13
- package/src/sdk/bus/kind-aware-reconciliation.ts +21 -2
- package/src/sdk/bus/notification-service.ts +47 -1
- package/src/sdk/bus/prompt-reconciliation.ts +22 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
- package/src/sdk/bus/telegram-daemon.ts +54 -3
- package/src/sdk/cli/rows.ts +247 -0
- package/src/sdk/cli/session-cli.ts +930 -79
- package/src/sdk/elevation/allowlist.ts +54 -0
- package/src/sdk/elevation/audit.ts +208 -0
- package/src/sdk/elevation/capability.ts +41 -0
- package/src/sdk/elevation/digest.ts +129 -0
- package/src/sdk/elevation/dispatch-receipt.ts +184 -0
- package/src/sdk/elevation/grant-ledger.ts +1142 -0
- package/src/sdk/elevation/owner.ts +109 -0
- package/src/sdk/guides/cache.ts +363 -0
- package/src/sdk/guides/catalog.ts +554 -0
- package/src/sdk/guides/cli.ts +235 -0
- package/src/sdk/guides/manifest.ts +152 -0
- package/src/sdk/guides/verify.ts +178 -0
- package/src/sdk/host/control/dispatch.ts +49 -1
- package/src/sdk/host/control/index.ts +1 -0
- package/src/sdk/host/control/operations.ts +8 -1
- package/src/sdk/host/query/cursor.ts +47 -3
- package/src/sdk/host/query/handlers.ts +155 -2
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +391 -1
- package/src/sdk/host/session-runtime.ts +308 -73
- package/src/sdk/host/surface-policy.ts +3 -0
- package/src/sdk/host/websocket-transport.ts +7 -1
- package/src/sdk/mcp/server.ts +88 -15
- package/src/sdk/prompt-status.ts +8 -0
- package/src/sdk/protocol/operation-inventory.generated.json +62 -0
- package/src/sdk/protocol/operation-registry.ts +3 -2
- package/src/sdk/session.ts +87 -39
- package/src/sdk/transport/index.ts +1 -0
- package/src/sdk/transport/relay.ts +3 -0
- package/src/sdk/transport/serve-cli.ts +111 -24
- package/src/secrets/obfuscator.ts +92 -12
- package/src/session/agent-session.ts +322 -93
- package/src/session/blob-store.ts +60 -8
- package/src/session/fallback-chain-controller.ts +35 -0
- package/src/session/internal/managed-session-scope.ts +25 -3
- package/src/session/internal/managed-session-storage.ts +574 -73
- package/src/session/internal/session-memory-sidecar.ts +19 -29
- package/src/session/session-manager.ts +2166 -743
- package/src/session/session-storage.ts +715 -48
- package/src/setup/provider-onboarding.ts +19 -3
- package/src/setup/provider-presets.json +3 -1
- package/src/slash-commands/builtin-registry.ts +85 -3
- package/src/stt/downloader.ts +2 -3
- package/src/stt/setup.ts +29 -4
- package/src/task/executor.ts +22 -18
- package/src/task/index.ts +24 -1
- package/src/tools/archive-reader.ts +11 -2
- package/src/tools/browser/runtime-diagnostics.ts +249 -0
- package/src/tools/browser/tab-protocol.ts +4 -0
- package/src/tools/browser/tab-supervisor.ts +5 -0
- package/src/tools/browser/tab-worker.ts +36 -1
- package/src/tools/browser.ts +7 -0
- package/src/tools/image-gen.ts +110 -166
- package/src/tools/index.ts +2 -0
- package/src/tools/read.ts +1 -1
- package/src/tools/resource-gc.ts +6 -1
- package/src/tools/tool-catalog.generated.ts +5 -1
- package/src/utils/pasted-image-path.ts +93 -0
- package/src/web/search/index.ts +3 -3
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
RecoveryFsIdentity,
|
|
10
10
|
RecoveryFsRoot,
|
|
11
11
|
} from "@gajae-code/natives";
|
|
12
|
-
import type { SessionStorageStat } from "../session-storage";
|
|
12
|
+
import type { SessionStorageRangeSnapshot, SessionStorageStat } from "../session-storage";
|
|
13
13
|
import {
|
|
14
14
|
classifyNativePublishOutcome,
|
|
15
15
|
formatNativePublishDiagnostic,
|
|
@@ -42,6 +42,8 @@ export const MANAGED_ARTIFACT_MAX_DEPTH = 32;
|
|
|
42
42
|
export const MANAGED_ARTIFACT_MAX_FILES = 50_000;
|
|
43
43
|
export const MANAGED_ARTIFACT_MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
44
44
|
export const MANAGED_ARTIFACT_MAX_TOTAL_BYTES = 512 * 1024 * 1024;
|
|
45
|
+
const REPLACEMENT_CLEANUP_RECEIPT_MAX_BYTES = 64 * 1024;
|
|
46
|
+
const REPLACEMENT_CLEANUP_RECEIPT_SCAN_LIMIT = MANAGED_ARTIFACT_MAX_FILES;
|
|
45
47
|
export const MANAGED_ARTIFACT_COPY_BATCH_SIZE = 256;
|
|
46
48
|
const LOCK_LEASE_MS = 60_000;
|
|
47
49
|
const LOCK_HEARTBEAT_MS = 10_000;
|
|
@@ -88,6 +90,23 @@ export class ManagedReplaceError extends Error {
|
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
export class ManagedCommittedMutationError extends Error {
|
|
94
|
+
constructor(
|
|
95
|
+
readonly operation: "replace" | "append",
|
|
96
|
+
cause: unknown,
|
|
97
|
+
) {
|
|
98
|
+
super(`managed_${operation}_committed_outcome_uncertain`, { cause });
|
|
99
|
+
this.name = "ManagedCommittedMutationError";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function managedAppendFailure(code: string | undefined): Error {
|
|
104
|
+
const error = new Error(code ?? "managed_append_failed");
|
|
105
|
+
return code === "content_too_large" || code === "too_large" || code === "header_patch_write_failed"
|
|
106
|
+
? error
|
|
107
|
+
: new ManagedCommittedMutationError("append", error);
|
|
108
|
+
}
|
|
109
|
+
|
|
91
110
|
function publishFailure(outcome: NativePublishOutcome): ManagedPublishError {
|
|
92
111
|
const classification =
|
|
93
112
|
outcome.reason === "destination_exists"
|
|
@@ -117,6 +136,10 @@ function exactUnlinkCompleted(result: NativeExactUnlinkResult): boolean {
|
|
|
117
136
|
);
|
|
118
137
|
}
|
|
119
138
|
|
|
139
|
+
function isRetryableReplacementReceiptCleanupError(error: unknown): boolean {
|
|
140
|
+
return error instanceof Error && error.message === "managed_replace_receipt_cleanup_pending:io_error";
|
|
141
|
+
}
|
|
142
|
+
|
|
120
143
|
/** A same-filesystem rename updates the moved root's ctime but no other tree identity. */
|
|
121
144
|
function sameDirectoryTreeSnapshotAfterRename(
|
|
122
145
|
left: NativeDirectoryTreeSnapshot,
|
|
@@ -382,6 +405,39 @@ function replacementCleanupReceiptBinding(
|
|
|
382
405
|
: undefined;
|
|
383
406
|
}
|
|
384
407
|
|
|
408
|
+
function parseReplacementIdentity(value: unknown): ManagedFileSnapshot["identity"] | undefined {
|
|
409
|
+
const parsed = parseLegacyReplacementCleanupIdentity(value);
|
|
410
|
+
if (!parsed || parsed.size > BigInt(Number.MAX_SAFE_INTEGER)) return undefined;
|
|
411
|
+
return {
|
|
412
|
+
dev: parsed.dev,
|
|
413
|
+
ino: parsed.ino,
|
|
414
|
+
nlink: parsed.nlink,
|
|
415
|
+
size: Number(parsed.size),
|
|
416
|
+
mtimeNs: parsed.mtimeNs,
|
|
417
|
+
ctimeNs: parsed.ctimeNs,
|
|
418
|
+
sha256: parsed.sha256,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function parseReplacementCleanupReceipt(bytes: Uint8Array): ReplacementCleanupReceipt | undefined {
|
|
423
|
+
let value: unknown;
|
|
424
|
+
try {
|
|
425
|
+
value = JSON.parse(Buffer.from(bytes).toString("utf8")) as unknown;
|
|
426
|
+
} catch {
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
const receipt = asRecord(value);
|
|
430
|
+
if (
|
|
431
|
+
receipt?.version !== 3 ||
|
|
432
|
+
typeof receipt.staging !== "string" ||
|
|
433
|
+
typeof receipt.destination !== "string" ||
|
|
434
|
+
!parseReplacementIdentity(receipt.successor) ||
|
|
435
|
+
!parseReplacementIdentity(receipt.predecessor)
|
|
436
|
+
)
|
|
437
|
+
return undefined;
|
|
438
|
+
return receipt as ReplacementCleanupReceipt;
|
|
439
|
+
}
|
|
440
|
+
|
|
385
441
|
function serializeReplacementIdentity(identity: ManagedFileSnapshot["identity"]): SerializedReplacementIdentity {
|
|
386
442
|
return {
|
|
387
443
|
dev: identity.dev.toString(),
|
|
@@ -420,6 +476,65 @@ function replacementReceiptPlaceholderRetirementName(
|
|
|
420
476
|
return `.gjc-receipt-placeholder-remove-${placeholder.dev.toString(16)}-${placeholder.ino.toString(16)}-${predecessor.dev.toString(16)}-${predecessor.ino.toString(16)}-${receipt.dev.toString(16)}-${receipt.ino.toString(16)}`;
|
|
421
477
|
}
|
|
422
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Terminal write-protocol remnant names. The POSIX exact-unlink fallback cannot
|
|
481
|
+
* descriptor-unlink, so it detaches, scrubs, and retains zero-length quarantine
|
|
482
|
+
* entries and exchange placeholders instead of removing them (`cleanup_pending`
|
|
483
|
+
* with a durable payload). Linux completes deletion through the retained
|
|
484
|
+
* directory authority, but macOS has no retained authority, so every managed
|
|
485
|
+
* replacement leaks its scrubbed remnants and scope directories grow without
|
|
486
|
+
* bound (tens of thousands of dirents per workspace), degrading every
|
|
487
|
+
* per-mutation receipt scan and widening quarantine-collision windows.
|
|
488
|
+
*/
|
|
489
|
+
const SCRUBBED_REMNANT_PREFIXES = [
|
|
490
|
+
".gjc-exact-unlink-placeholder-",
|
|
491
|
+
".gjc-exact-replace-destination-",
|
|
492
|
+
".gjc-receipt-remove-",
|
|
493
|
+
".gjc-receipt-placeholder-remove-",
|
|
494
|
+
".gjc-replace-retry-",
|
|
495
|
+
] as const;
|
|
496
|
+
|
|
497
|
+
/** In-flight protocol steps complete in milliseconds; anything older is abandoned. */
|
|
498
|
+
const SCRUBBED_REMNANT_MIN_AGE_MS = 15 * 60 * 1000;
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Best-effort removal of scrubbed write-protocol remnants from one managed
|
|
502
|
+
* directory. Only zero-length, single-link, non-symlink regular files whose
|
|
503
|
+
* names carry a terminal remnant prefix and whose timestamps are older than
|
|
504
|
+
* the age gate are removed: a zero-length single-link entry is exactly what
|
|
505
|
+
* the scrub proof leaves behind, and non-zero entries (displaced predecessors
|
|
506
|
+
* or detached receipts retained as evidence) are never touched. Failures are
|
|
507
|
+
* swallowed — a concurrent same-owner writer may legitimately own or already
|
|
508
|
+
* have reconciled any candidate.
|
|
509
|
+
*/
|
|
510
|
+
export function reapScrubbedProtocolRemnantsSync(
|
|
511
|
+
directory: string,
|
|
512
|
+
minAgeMs: number = SCRUBBED_REMNANT_MIN_AGE_MS,
|
|
513
|
+
): number {
|
|
514
|
+
let names: string[];
|
|
515
|
+
try {
|
|
516
|
+
names = fs.readdirSync(directory);
|
|
517
|
+
} catch {
|
|
518
|
+
return 0;
|
|
519
|
+
}
|
|
520
|
+
const cutoff = Date.now() - minAgeMs;
|
|
521
|
+
let reaped = 0;
|
|
522
|
+
for (const name of names) {
|
|
523
|
+
if (!SCRUBBED_REMNANT_PREFIXES.some(prefix => name.startsWith(prefix))) continue;
|
|
524
|
+
const pathname = path.join(directory, name);
|
|
525
|
+
try {
|
|
526
|
+
const named = fs.lstatSync(pathname);
|
|
527
|
+
if (!named.isFile() || named.isSymbolicLink() || named.nlink !== 1 || named.size !== 0) continue;
|
|
528
|
+
if (named.mtimeMs > cutoff) continue;
|
|
529
|
+
fs.unlinkSync(pathname);
|
|
530
|
+
reaped += 1;
|
|
531
|
+
} catch {
|
|
532
|
+
// Best effort: concurrently reconciled or removed remnants are not errors.
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return reaped;
|
|
536
|
+
}
|
|
537
|
+
|
|
423
538
|
const ACL_FAILURE_CODES = new Set(["acl_denied", "acl_io_error", "acl_present", "acl_malformed", "acl_unknown"]);
|
|
424
539
|
const ACL_CLEAR_EVIDENCE = new Set(["cleared", "already_absent", "unsupported", "not_run"]);
|
|
425
540
|
const GENERAL_FAILURE_CODES = new Set([
|
|
@@ -948,7 +1063,12 @@ export class ManagedSessionDescendantStore {
|
|
|
948
1063
|
this.#assertBound();
|
|
949
1064
|
}
|
|
950
1065
|
#assertBound(): void {
|
|
951
|
-
if (!this.#authority)
|
|
1066
|
+
if (!this.#authority) {
|
|
1067
|
+
const named = fs.statSync(this.#baseDir, { bigint: true });
|
|
1068
|
+
if (!named.isDirectory() || named.dev !== this.#subtreeRoot.dev || named.ino !== this.#subtreeRoot.ino)
|
|
1069
|
+
throw new Error("Managed descendant root binding changed");
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
952
1072
|
const named = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
953
1073
|
const retained =
|
|
954
1074
|
this.#authorityBaseDir === this.#baseDir
|
|
@@ -1025,7 +1145,7 @@ export class ManagedSessionDescendantStore {
|
|
|
1025
1145
|
if (!binding) throw new Error("managed_replace_cleanup_receipt_invalid");
|
|
1026
1146
|
let receipt: ManagedFileSnapshot;
|
|
1027
1147
|
try {
|
|
1028
|
-
receipt =
|
|
1148
|
+
receipt = captureManagedFilePrefixNoFollow(receiptPath, REPLACEMENT_CLEANUP_RECEIPT_MAX_BYTES);
|
|
1029
1149
|
} catch (error) {
|
|
1030
1150
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
1031
1151
|
throw error;
|
|
@@ -1070,7 +1190,7 @@ export class ManagedSessionDescendantStore {
|
|
|
1070
1190
|
if (!binding) throw new Error("managed_replace_cleanup_receipt_invalid");
|
|
1071
1191
|
let receipt: ManagedFileSnapshot;
|
|
1072
1192
|
try {
|
|
1073
|
-
receipt =
|
|
1193
|
+
receipt = captureManagedFilePrefixNoFollow(receiptPath, REPLACEMENT_CLEANUP_RECEIPT_MAX_BYTES);
|
|
1074
1194
|
} catch (error) {
|
|
1075
1195
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
1076
1196
|
throw error;
|
|
@@ -1105,7 +1225,7 @@ export class ManagedSessionDescendantStore {
|
|
|
1105
1225
|
|
|
1106
1226
|
let currentReceipt: ManagedFileSnapshot;
|
|
1107
1227
|
try {
|
|
1108
|
-
currentReceipt =
|
|
1228
|
+
currentReceipt = captureManagedFilePrefixNoFollow(receiptPath, REPLACEMENT_CLEANUP_RECEIPT_MAX_BYTES);
|
|
1109
1229
|
} catch (error) {
|
|
1110
1230
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
1111
1231
|
fsyncDirectory(this.#baseDir);
|
|
@@ -1134,21 +1254,82 @@ export class ManagedSessionDescendantStore {
|
|
|
1134
1254
|
fsyncDirectory(this.#baseDir);
|
|
1135
1255
|
}
|
|
1136
1256
|
|
|
1257
|
+
#reconcilePendingReplacementReceipt(receiptPath: string): void {
|
|
1258
|
+
const pending = captureManagedFilePrefixNoFollow(receiptPath, REPLACEMENT_CLEANUP_RECEIPT_MAX_BYTES);
|
|
1259
|
+
const parsed = parseReplacementCleanupReceipt(pending.bytes);
|
|
1260
|
+
const predecessor = parsed ? parseReplacementIdentity(parsed.predecessor) : undefined;
|
|
1261
|
+
if (
|
|
1262
|
+
!parsed ||
|
|
1263
|
+
!predecessor ||
|
|
1264
|
+
path.dirname(path.resolve(parsed.staging)) !== this.#baseDir ||
|
|
1265
|
+
path.dirname(path.resolve(parsed.destination)) !== this.#baseDir
|
|
1266
|
+
)
|
|
1267
|
+
throw new Error("managed_replace_cleanup_receipt_invalid");
|
|
1268
|
+
const destination = replacementReceiptPath(this.#baseDir, predecessor, pending.identity);
|
|
1269
|
+
const outcome = classifyNativePublishOutcome(
|
|
1270
|
+
nativeSessionStorage().renameNoReplacePath(receiptPath, destination),
|
|
1271
|
+
);
|
|
1272
|
+
if (outcome.ok) {
|
|
1273
|
+
fsyncDirectory(this.#baseDir);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
if (outcome.reason === "destination_exists") {
|
|
1277
|
+
const existing = captureManagedFilePrefixNoFollow(destination, REPLACEMENT_CLEANUP_RECEIPT_MAX_BYTES);
|
|
1278
|
+
if (existing.identity.sha256 !== pending.identity.sha256)
|
|
1279
|
+
throw new Error("managed_replace_cleanup_receipt_invalid");
|
|
1280
|
+
const removed = nativeSessionStorage().exactUnlink(receiptPath, {
|
|
1281
|
+
dev: pending.identity.dev,
|
|
1282
|
+
ino: pending.identity.ino,
|
|
1283
|
+
nlink: pending.identity.nlink,
|
|
1284
|
+
parentDev: this.#subtreeRoot.dev,
|
|
1285
|
+
parentIno: this.#subtreeRoot.ino,
|
|
1286
|
+
size: BigInt(pending.identity.size),
|
|
1287
|
+
mtimeNs: pending.identity.mtimeNs,
|
|
1288
|
+
sha256: pending.identity.sha256,
|
|
1289
|
+
quarantineName: `.gjc-receipt-pending-remove-${pending.identity.dev.toString(16)}-${pending.identity.ino.toString(16)}`,
|
|
1290
|
+
});
|
|
1291
|
+
if (!exactUnlinkCompleted(removed) && removed.code !== "not_found")
|
|
1292
|
+
throw new Error(`managed_replace_receipt_cleanup_pending:${removed.code ?? "unknown"}`);
|
|
1293
|
+
fsyncDirectory(this.#baseDir);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
throw publishFailure(outcome);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1137
1299
|
#reconcileReplacementCleanupReceipts(): void {
|
|
1138
1300
|
if (this.#reconcilingReplacementCleanup) return;
|
|
1139
1301
|
this.#reconcilingReplacementCleanup = true;
|
|
1140
1302
|
try {
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1303
|
+
const directory = fs.opendirSync(this.#baseDir);
|
|
1304
|
+
try {
|
|
1305
|
+
let examined = 0;
|
|
1306
|
+
for (let entry = directory.readSync(); entry; entry = directory.readSync()) {
|
|
1307
|
+
examined++;
|
|
1308
|
+
if (examined > REPLACEMENT_CLEANUP_RECEIPT_SCAN_LIMIT)
|
|
1309
|
+
throw new Error("managed_replace_cleanup_receipt_limit_exceeded");
|
|
1310
|
+
const name = entry.name;
|
|
1311
|
+
if (!name.startsWith(".gjc-replace-receipt-pending-") && !name.startsWith(".gjc-replace-cleanup-"))
|
|
1312
|
+
continue;
|
|
1313
|
+
if (name.startsWith(".gjc-replace-receipt-pending-")) {
|
|
1314
|
+
this.#reconcilePendingReplacementReceipt(path.join(this.#baseDir, name));
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
if (replacementCleanupReceiptBinding(name)) {
|
|
1318
|
+
try {
|
|
1319
|
+
this.#detachReplacementCleanupReceipt(path.join(this.#baseDir, name));
|
|
1320
|
+
} catch (error) {
|
|
1321
|
+
if (!isRetryableReplacementReceiptCleanupError(error)) throw error;
|
|
1322
|
+
}
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
if (legacyReplacementCleanupReceiptBinding(name)) {
|
|
1326
|
+
this.#reconcileLegacyReplacementCleanupReceipt(path.join(this.#baseDir, name), name);
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
throw new Error("managed_replace_cleanup_receipt_invalid");
|
|
1150
1330
|
}
|
|
1151
|
-
|
|
1331
|
+
} finally {
|
|
1332
|
+
directory.closeSync();
|
|
1152
1333
|
}
|
|
1153
1334
|
} finally {
|
|
1154
1335
|
this.#reconcilingReplacementCleanup = false;
|
|
@@ -1271,18 +1452,20 @@ export class ManagedSessionDescendantStore {
|
|
|
1271
1452
|
|
|
1272
1453
|
captureBoundedAppendExpectation(relativePath: string): ManagedBoundedAppendExpectation | undefined {
|
|
1273
1454
|
this.#assertBound();
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1455
|
+
let identity: ManagedFileSnapshot["identity"];
|
|
1456
|
+
try {
|
|
1457
|
+
identity = captureManagedFileIdentityStreamingNoFollow(this.#resolve(relativePath));
|
|
1458
|
+
} catch (error) {
|
|
1459
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
1460
|
+
throw error;
|
|
1461
|
+
}
|
|
1279
1462
|
return {
|
|
1280
|
-
dev:
|
|
1281
|
-
ino:
|
|
1282
|
-
size:
|
|
1283
|
-
mtimeNs:
|
|
1284
|
-
ctimeNs:
|
|
1285
|
-
sha256: sha256
|
|
1463
|
+
dev: identity.dev.toString(),
|
|
1464
|
+
ino: identity.ino.toString(),
|
|
1465
|
+
size: identity.size.toString(),
|
|
1466
|
+
mtimeNs: identity.mtimeNs.toString(),
|
|
1467
|
+
ctimeNs: identity.ctimeNs.toString(),
|
|
1468
|
+
sha256: identity.sha256,
|
|
1286
1469
|
};
|
|
1287
1470
|
}
|
|
1288
1471
|
|
|
@@ -1293,7 +1476,26 @@ export class ManagedSessionDescendantStore {
|
|
|
1293
1476
|
): ManagedAppendReceipt {
|
|
1294
1477
|
this.#beforeMutation();
|
|
1295
1478
|
this.#assertBound();
|
|
1296
|
-
if (!this.#authority)
|
|
1479
|
+
if (!this.#authority) {
|
|
1480
|
+
const current = captureManagedFileIdentityStreamingNoFollow(this.#resolve(relativePath));
|
|
1481
|
+
if (
|
|
1482
|
+
current.dev.toString() !== expected.dev ||
|
|
1483
|
+
current.ino.toString() !== expected.ino ||
|
|
1484
|
+
current.size.toString() !== expected.size ||
|
|
1485
|
+
current.mtimeNs.toString() !== expected.mtimeNs ||
|
|
1486
|
+
current.ctimeNs.toString() !== expected.ctimeNs ||
|
|
1487
|
+
current.sha256 !== expected.sha256
|
|
1488
|
+
)
|
|
1489
|
+
throw new Error("managed_append_identity_mismatch");
|
|
1490
|
+
const appended = appendManagedFileStreamingSync(
|
|
1491
|
+
this.#resolve(relativePath),
|
|
1492
|
+
bytes,
|
|
1493
|
+
this.#subtreeRoot,
|
|
1494
|
+
this.#policy,
|
|
1495
|
+
);
|
|
1496
|
+
this.#assertBound();
|
|
1497
|
+
return managedAppendReceiptFromIdentity(appended);
|
|
1498
|
+
}
|
|
1297
1499
|
const relative = this.#relative(this.#resolve(relativePath));
|
|
1298
1500
|
const appended = this.#authority.appendManaged(
|
|
1299
1501
|
relative,
|
|
@@ -1305,11 +1507,11 @@ export class ManagedSessionDescendantStore {
|
|
|
1305
1507
|
expected.ctimeNs,
|
|
1306
1508
|
expected.sha256,
|
|
1307
1509
|
);
|
|
1308
|
-
if (!appended.ok) throw
|
|
1309
|
-
|
|
1310
|
-
|
|
1510
|
+
if (!appended.ok) throw managedAppendFailure(appended.code);
|
|
1511
|
+
if (!appended.identity)
|
|
1512
|
+
throw new ManagedCommittedMutationError("append", new Error("managed_append_identity_unavailable"));
|
|
1311
1513
|
this.#assertBound();
|
|
1312
|
-
return managedAppendReceiptFromIdentity(managedFileIdentityFromNative(
|
|
1514
|
+
return managedAppendReceiptFromIdentity(managedFileIdentityFromNative(appended.identity));
|
|
1313
1515
|
}
|
|
1314
1516
|
|
|
1315
1517
|
appendSync(relativePath: string, bytes: Uint8Array): ManagedAppendReceipt {
|
|
@@ -1336,12 +1538,10 @@ export class ManagedSessionDescendantStore {
|
|
|
1336
1538
|
observed.identity.ctimeNs,
|
|
1337
1539
|
expectedSha256,
|
|
1338
1540
|
);
|
|
1339
|
-
if (!appended.ok) throw
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
const
|
|
1343
|
-
if (!stat.ok || !stat.identity) throw new Error(stat.code ?? "managed_append_identity_unavailable");
|
|
1344
|
-
const receipt = managedAppendReceiptFromIdentity(managedFileIdentityFromNative(stat.identity));
|
|
1541
|
+
if (!appended.ok) throw managedAppendFailure(appended.code);
|
|
1542
|
+
if (!appended.identity)
|
|
1543
|
+
throw new ManagedCommittedMutationError("append", new Error("managed_append_identity_unavailable"));
|
|
1544
|
+
const receipt = managedAppendReceiptFromIdentity(managedFileIdentityFromNative(appended.identity));
|
|
1345
1545
|
this.#assertBound();
|
|
1346
1546
|
return receipt;
|
|
1347
1547
|
}
|
|
@@ -1355,6 +1555,30 @@ export class ManagedSessionDescendantStore {
|
|
|
1355
1555
|
return receipt;
|
|
1356
1556
|
}
|
|
1357
1557
|
|
|
1558
|
+
#assertPathBackedReadRelative(relativePath: string): void {
|
|
1559
|
+
if (!this.#authority && relativePath.split(/[\\/]/).length > 1)
|
|
1560
|
+
throw new Error("managed_nested_path_unsupported");
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/**
|
|
1564
|
+
* Without retained root authority a nested read resolves through intermediate
|
|
1565
|
+
* directories that `O_NOFOLLOW` does not cover, so each component between the
|
|
1566
|
+
* bound base directory and the target is verified as a real same-device
|
|
1567
|
+
* directory before the capture opens the leaf.
|
|
1568
|
+
*/
|
|
1569
|
+
#assertPathBackedDirectoryChain(resolved: string): void {
|
|
1570
|
+
if (this.#authority) return;
|
|
1571
|
+
const relative = path.relative(this.#baseDir, resolved);
|
|
1572
|
+
const components = relative.split(path.sep);
|
|
1573
|
+
let current = this.#baseDir;
|
|
1574
|
+
for (const component of components.slice(0, -1)) {
|
|
1575
|
+
current = path.join(current, component);
|
|
1576
|
+
const stat = fs.lstatSync(current, { bigint: true });
|
|
1577
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.dev !== this.#subtreeRoot.dev)
|
|
1578
|
+
throw new Error("Managed descendant path escapes retained store");
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1358
1582
|
#relative(resolved: string): string {
|
|
1359
1583
|
return path.relative(this.#authorityBaseDir, resolved).split(path.sep).join("/");
|
|
1360
1584
|
}
|
|
@@ -1463,11 +1687,59 @@ export class ManagedSessionDescendantStore {
|
|
|
1463
1687
|
|
|
1464
1688
|
/** Capture descriptor identity without copying file bytes when retained authority is available. */
|
|
1465
1689
|
descriptorExpected(relativePath: string): SessionStorageStat | null {
|
|
1690
|
+
this.#assertPathBackedReadRelative(relativePath);
|
|
1466
1691
|
this.#assertBound();
|
|
1467
1692
|
const resolved = this.#resolve(relativePath);
|
|
1468
1693
|
if (!this.#authority) {
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1694
|
+
const rootBefore = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
1695
|
+
if (
|
|
1696
|
+
!rootBefore.isDirectory() ||
|
|
1697
|
+
rootBefore.isSymbolicLink() ||
|
|
1698
|
+
rootBefore.dev !== this.#subtreeRoot.dev ||
|
|
1699
|
+
rootBefore.ino !== this.#subtreeRoot.ino
|
|
1700
|
+
)
|
|
1701
|
+
throw new Error("Managed descendant root binding changed");
|
|
1702
|
+
let fd: number | undefined;
|
|
1703
|
+
try {
|
|
1704
|
+
fd = fs.openSync(
|
|
1705
|
+
resolved,
|
|
1706
|
+
fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | (fs.constants.O_NOFOLLOW ?? 0),
|
|
1707
|
+
);
|
|
1708
|
+
const opened = fs.fstatSync(fd, { bigint: true });
|
|
1709
|
+
const named = fs.lstatSync(resolved, { bigint: true });
|
|
1710
|
+
if (
|
|
1711
|
+
!opened.isFile() ||
|
|
1712
|
+
opened.nlink > 1n ||
|
|
1713
|
+
!named.isFile() ||
|
|
1714
|
+
named.isSymbolicLink() ||
|
|
1715
|
+
named.dev !== opened.dev ||
|
|
1716
|
+
named.ino !== opened.ino ||
|
|
1717
|
+
named.nlink !== opened.nlink
|
|
1718
|
+
)
|
|
1719
|
+
throw new Error("source_changed");
|
|
1720
|
+
const rootAfter = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
1721
|
+
if (
|
|
1722
|
+
!rootAfter.isDirectory() ||
|
|
1723
|
+
rootAfter.isSymbolicLink() ||
|
|
1724
|
+
rootAfter.dev !== rootBefore.dev ||
|
|
1725
|
+
rootAfter.ino !== rootBefore.ino
|
|
1726
|
+
)
|
|
1727
|
+
throw new Error("Managed descendant root binding changed");
|
|
1728
|
+
this.#assertBound();
|
|
1729
|
+
return managedAppendReceiptFromIdentity({
|
|
1730
|
+
dev: opened.dev,
|
|
1731
|
+
ino: opened.ino,
|
|
1732
|
+
nlink: opened.nlink,
|
|
1733
|
+
size: Number(opened.size),
|
|
1734
|
+
mtimeNs: opened.mtimeNs,
|
|
1735
|
+
ctimeNs: opened.ctimeNs,
|
|
1736
|
+
}).descriptor;
|
|
1737
|
+
} catch (error) {
|
|
1738
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
1739
|
+
throw error;
|
|
1740
|
+
} finally {
|
|
1741
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
1742
|
+
}
|
|
1471
1743
|
}
|
|
1472
1744
|
const stat = this.#authority.stat(this.#relative(resolved));
|
|
1473
1745
|
if (!stat.ok) {
|
|
@@ -1478,13 +1750,132 @@ export class ManagedSessionDescendantStore {
|
|
|
1478
1750
|
return managedAppendReceiptFromIdentity(managedFileIdentityFromNative(stat.identity)).descriptor;
|
|
1479
1751
|
}
|
|
1480
1752
|
|
|
1753
|
+
/**
|
|
1754
|
+
* Read one bounded range through a no-follow descriptor while binding the
|
|
1755
|
+
* opened file to retained authority (when available) and the managed root
|
|
1756
|
+
* before and after the read. This avoids whole-transcript capture for cold
|
|
1757
|
+
* managed-session indexing without weakening pathname authority.
|
|
1758
|
+
*/
|
|
1759
|
+
readRangeExpectedSync(
|
|
1760
|
+
relativePath: string,
|
|
1761
|
+
start: number,
|
|
1762
|
+
length: number,
|
|
1763
|
+
expectedDescriptor?: Pick<SessionStorageStat, "dev" | "ino" | "nlink" | "size" | "mtimeNs" | "ctimeNs">,
|
|
1764
|
+
): SessionStorageRangeSnapshot {
|
|
1765
|
+
if (!Number.isSafeInteger(start) || start < 0 || !Number.isSafeInteger(length) || length < 0)
|
|
1766
|
+
throw new RangeError("Invalid managed range read");
|
|
1767
|
+
if (start > Number.MAX_SAFE_INTEGER - length) throw new RangeError("Managed range read start overflows");
|
|
1768
|
+
if (length > 64 * 1024 * 1024) throw new RangeError("Managed range read exceeds the bounded maximum");
|
|
1769
|
+
this.#assertPathBackedReadRelative(relativePath);
|
|
1770
|
+
this.#assertBound();
|
|
1771
|
+
const rootBefore = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
1772
|
+
if (
|
|
1773
|
+
!rootBefore.isDirectory() ||
|
|
1774
|
+
rootBefore.isSymbolicLink() ||
|
|
1775
|
+
rootBefore.dev !== this.#subtreeRoot.dev ||
|
|
1776
|
+
rootBefore.ino !== this.#subtreeRoot.ino
|
|
1777
|
+
)
|
|
1778
|
+
throw new Error("Managed descendant root binding changed");
|
|
1779
|
+
const resolved = this.#resolve(relativePath);
|
|
1780
|
+
const relative = this.#relative(resolved);
|
|
1781
|
+
const retained = this.#authority?.stat(relative);
|
|
1782
|
+
if (retained && !retained.ok) {
|
|
1783
|
+
if (retained.code === "not_found")
|
|
1784
|
+
throw Object.assign(new Error("Managed file not found"), { code: "ENOENT" });
|
|
1785
|
+
throw new Error(retained.code ?? "managed_stat_failed");
|
|
1786
|
+
}
|
|
1787
|
+
if (retained && !retained.identity) throw new Error("managed_stat_identity_unavailable");
|
|
1788
|
+
const fd = fs.openSync(
|
|
1789
|
+
resolved,
|
|
1790
|
+
fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | (fs.constants.O_NOFOLLOW ?? 0),
|
|
1791
|
+
);
|
|
1792
|
+
try {
|
|
1793
|
+
const before = fs.fstatSync(fd, { bigint: true });
|
|
1794
|
+
if (!before.isFile() || before.nlink > 1n) throw new Error("source_changed");
|
|
1795
|
+
if (retained?.identity) {
|
|
1796
|
+
const expected = retained.identity;
|
|
1797
|
+
if (
|
|
1798
|
+
before.dev.toString() !== expected.dev ||
|
|
1799
|
+
before.ino.toString() !== expected.ino ||
|
|
1800
|
+
before.nlink.toString() !== expected.nlink ||
|
|
1801
|
+
before.size.toString() !== expected.size ||
|
|
1802
|
+
before.mtimeNs.toString() !== expected.mtimeNs ||
|
|
1803
|
+
before.ctimeNs.toString() !== expected.ctimeNs
|
|
1804
|
+
)
|
|
1805
|
+
throw new Error("source_changed");
|
|
1806
|
+
}
|
|
1807
|
+
if (
|
|
1808
|
+
expectedDescriptor &&
|
|
1809
|
+
(before.dev !== expectedDescriptor.dev ||
|
|
1810
|
+
before.ino !== expectedDescriptor.ino ||
|
|
1811
|
+
before.nlink !== (expectedDescriptor.nlink ?? before.nlink) ||
|
|
1812
|
+
Number(before.size) !== expectedDescriptor.size ||
|
|
1813
|
+
before.mtimeNs !== expectedDescriptor.mtimeNs ||
|
|
1814
|
+
before.ctimeNs !== expectedDescriptor.ctimeNs)
|
|
1815
|
+
)
|
|
1816
|
+
throw new Error("managed_range_generation_mismatch");
|
|
1817
|
+
if (Number(before.size) < start + length) throw new Error("range_not_present");
|
|
1818
|
+
const bytes = Buffer.alloc(length);
|
|
1819
|
+
let offset = 0;
|
|
1820
|
+
while (offset < length) {
|
|
1821
|
+
const count = fs.readSync(fd, bytes, offset, length - offset, start + offset);
|
|
1822
|
+
if (count === 0) throw new Error("range_not_present");
|
|
1823
|
+
offset += count;
|
|
1824
|
+
}
|
|
1825
|
+
const after = fs.fstatSync(fd, { bigint: true });
|
|
1826
|
+
const named = fs.lstatSync(resolved, { bigint: true });
|
|
1827
|
+
if (
|
|
1828
|
+
after.dev !== before.dev ||
|
|
1829
|
+
after.ino !== before.ino ||
|
|
1830
|
+
after.nlink !== before.nlink ||
|
|
1831
|
+
after.size !== before.size ||
|
|
1832
|
+
after.mtimeNs !== before.mtimeNs ||
|
|
1833
|
+
after.ctimeNs !== before.ctimeNs ||
|
|
1834
|
+
!named.isFile() ||
|
|
1835
|
+
named.isSymbolicLink() ||
|
|
1836
|
+
named.dev !== before.dev ||
|
|
1837
|
+
named.ino !== before.ino ||
|
|
1838
|
+
named.nlink !== before.nlink
|
|
1839
|
+
)
|
|
1840
|
+
throw new Error("source_changed");
|
|
1841
|
+
const rootAfter = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
1842
|
+
if (
|
|
1843
|
+
!rootAfter.isDirectory() ||
|
|
1844
|
+
rootAfter.isSymbolicLink() ||
|
|
1845
|
+
rootAfter.dev !== rootBefore.dev ||
|
|
1846
|
+
rootAfter.ino !== rootBefore.ino
|
|
1847
|
+
)
|
|
1848
|
+
throw new Error("Managed descendant root binding changed");
|
|
1849
|
+
this.#assertBound();
|
|
1850
|
+
return {
|
|
1851
|
+
bytes,
|
|
1852
|
+
stat: {
|
|
1853
|
+
dev: after.dev,
|
|
1854
|
+
ino: after.ino,
|
|
1855
|
+
nlink: after.nlink,
|
|
1856
|
+
size: Number(after.size),
|
|
1857
|
+
mtimeNs: after.mtimeNs,
|
|
1858
|
+
ctimeNs: after.ctimeNs,
|
|
1859
|
+
mtimeMs: Number(after.mtimeNs) / 1_000_000,
|
|
1860
|
+
mtime: new Date(Number(after.mtimeNs) / 1_000_000),
|
|
1861
|
+
isFile: true,
|
|
1862
|
+
},
|
|
1863
|
+
};
|
|
1864
|
+
} finally {
|
|
1865
|
+
fs.closeSync(fd);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1481
1868
|
/** Read an exact managed file without exposing its pathname as authority. */
|
|
1482
1869
|
readExpected(relativePath: string): ManagedFileSnapshot | null {
|
|
1483
1870
|
this.#assertBound();
|
|
1484
|
-
const
|
|
1871
|
+
const resolved = this.#resolve(relativePath);
|
|
1872
|
+
const relative = this.#relative(resolved);
|
|
1485
1873
|
if (!this.#authority) {
|
|
1486
1874
|
try {
|
|
1487
|
-
|
|
1875
|
+
this.#assertPathBackedDirectoryChain(resolved);
|
|
1876
|
+
const captured = captureManagedFileNoFollow(resolved);
|
|
1877
|
+
this.#assertBound();
|
|
1878
|
+
return captured;
|
|
1488
1879
|
} catch (error) {
|
|
1489
1880
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
1490
1881
|
throw error;
|
|
@@ -1515,12 +1906,15 @@ export class ManagedSessionDescendantStore {
|
|
|
1515
1906
|
this.#beforeMutation();
|
|
1516
1907
|
this.#assertBound();
|
|
1517
1908
|
if (!this.#authority) {
|
|
1909
|
+
const parent = fs.statSync(path.dirname(this.#resolve(relativePath)), { bigint: true });
|
|
1518
1910
|
const removed = nativeSessionStorage().exactUnlink(this.#resolve(relativePath), {
|
|
1519
1911
|
dev: expected.identity.dev,
|
|
1520
1912
|
ino: expected.identity.ino,
|
|
1521
1913
|
size: BigInt(expected.identity.size),
|
|
1522
1914
|
mtimeNs: expected.identity.mtimeNs,
|
|
1523
1915
|
sha256: expected.identity.sha256,
|
|
1916
|
+
parentDev: parent.dev,
|
|
1917
|
+
parentIno: parent.ino,
|
|
1524
1918
|
quarantineName: `.gjc-remove-${process.pid}-${randomUUID()}`,
|
|
1525
1919
|
});
|
|
1526
1920
|
if (
|
|
@@ -1550,6 +1944,31 @@ export class ManagedSessionDescendantStore {
|
|
|
1550
1944
|
throw new Error(removed.code ?? "managed_remove_failed");
|
|
1551
1945
|
this.#assertBound();
|
|
1552
1946
|
}
|
|
1947
|
+
|
|
1948
|
+
/** Remove one exact file without allocating its contents. */
|
|
1949
|
+
removeIfExistsDescriptor(relativePath: string): boolean {
|
|
1950
|
+
this.#beforeMutation();
|
|
1951
|
+
this.#assertBound();
|
|
1952
|
+
const resolved = this.#resolve(relativePath);
|
|
1953
|
+
let named: fs.BigIntStats;
|
|
1954
|
+
try {
|
|
1955
|
+
named = fs.lstatSync(resolved, { bigint: true });
|
|
1956
|
+
} catch (error) {
|
|
1957
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
1958
|
+
throw error;
|
|
1959
|
+
}
|
|
1960
|
+
if (!named.isFile() || named.isSymbolicLink()) throw new Error("managed_remove_failed");
|
|
1961
|
+
let identity: ManagedFileSnapshot["identity"];
|
|
1962
|
+
try {
|
|
1963
|
+
identity = captureManagedFileIdentityStreamingNoFollow(resolved);
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
1966
|
+
throw error;
|
|
1967
|
+
}
|
|
1968
|
+
this.#assertBound();
|
|
1969
|
+
this.removeExpected(relativePath, { bytes: Buffer.alloc(0), identity });
|
|
1970
|
+
return true;
|
|
1971
|
+
}
|
|
1553
1972
|
/** Read and remove one managed descendant through retained authority. */
|
|
1554
1973
|
async consume(relativePath: string): Promise<Uint8Array | null> {
|
|
1555
1974
|
this.#beforeMutation();
|
|
@@ -1598,14 +2017,28 @@ export class ManagedSessionDescendantStore {
|
|
|
1598
2017
|
/** Capture a complete descendant tree through this retained root. */
|
|
1599
2018
|
captureTree(relativePath: string): NativeDirectoryTreeSnapshot {
|
|
1600
2019
|
this.#assertBound();
|
|
2020
|
+
const resolved = this.#resolve(relativePath);
|
|
2021
|
+
try {
|
|
2022
|
+
fs.lstatSync(resolved);
|
|
2023
|
+
} catch (error) {
|
|
2024
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") throw new Error("not_found");
|
|
2025
|
+
throw error;
|
|
2026
|
+
}
|
|
2027
|
+
validateManagedArtifactTree(resolved);
|
|
1601
2028
|
const relative = this.#relative(this.#resolve(relativePath));
|
|
1602
2029
|
if (this.#authority) {
|
|
1603
2030
|
const captured = this.#authority.snapshotManagedTree(relative);
|
|
1604
|
-
if (!captured.ok || !captured.snapshot)
|
|
2031
|
+
if (!captured.ok || !captured.snapshot)
|
|
2032
|
+
throw new Error(
|
|
2033
|
+
captured.code === "not_found" ? "artifact_source_changed" : (captured.code ?? "unsafe_artifacts"),
|
|
2034
|
+
);
|
|
1605
2035
|
return captured.snapshot;
|
|
1606
2036
|
}
|
|
1607
2037
|
const captured = nativeSessionStorage().snapshotDirectoryTree(this.#resolve(relativePath));
|
|
1608
|
-
if (!captured.ok || !captured.snapshot)
|
|
2038
|
+
if (!captured.ok || !captured.snapshot)
|
|
2039
|
+
throw new Error(
|
|
2040
|
+
captured.code === "not_found" ? "artifact_source_changed" : (captured.code ?? "unsafe_artifacts"),
|
|
2041
|
+
);
|
|
1609
2042
|
return captured.snapshot;
|
|
1610
2043
|
}
|
|
1611
2044
|
|
|
@@ -1861,7 +2294,8 @@ function sameReplacementIdentity(
|
|
|
1861
2294
|
left.ino === right.ino &&
|
|
1862
2295
|
left.nlink === right.nlink &&
|
|
1863
2296
|
left.size === right.size &&
|
|
1864
|
-
left.mtimeNs === right.mtimeNs
|
|
2297
|
+
left.mtimeNs === right.mtimeNs &&
|
|
2298
|
+
left.sha256 === right.sha256
|
|
1865
2299
|
);
|
|
1866
2300
|
}
|
|
1867
2301
|
|
|
@@ -2011,7 +2445,7 @@ export function captureManagedFileNoFollow(pathname: string): ManagedFileSnapsho
|
|
|
2011
2445
|
}
|
|
2012
2446
|
|
|
2013
2447
|
function captureManagedFileNoFollowLimit(pathname: string, maxBytes?: number): ManagedFileSnapshot {
|
|
2014
|
-
const fd = fs.openSync(pathname, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
2448
|
+
const fd = fs.openSync(pathname, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | (fs.constants.O_NOFOLLOW ?? 0));
|
|
2015
2449
|
try {
|
|
2016
2450
|
const before = fs.fstatSync(fd, { bigint: true });
|
|
2017
2451
|
if (!before.isFile() || before.nlink > 1) throw new Error("source_changed");
|
|
@@ -2037,7 +2471,7 @@ function captureManagedFileNoFollowLimit(pathname: string, maxBytes?: number): M
|
|
|
2037
2471
|
/** Streams a managed file once while retaining only a bounded header prefix and the full descriptor-bound digest. */
|
|
2038
2472
|
export function inspectManagedFileNoFollow(pathname: string, prefixLimit: number): ManagedFileSnapshot {
|
|
2039
2473
|
if (!Number.isSafeInteger(prefixLimit) || prefixLimit < 0) throw new Error("invalid_capture_limit");
|
|
2040
|
-
const fd = fs.openSync(pathname, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
2474
|
+
const fd = fs.openSync(pathname, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | (fs.constants.O_NOFOLLOW ?? 0));
|
|
2041
2475
|
try {
|
|
2042
2476
|
const before = fs.fstatSync(fd, { bigint: true });
|
|
2043
2477
|
if (!before.isFile() || before.nlink > 1) throw new Error("source_changed");
|
|
@@ -2068,7 +2502,7 @@ export function inspectManagedFileNoFollow(pathname: string, prefixLimit: number
|
|
|
2068
2502
|
}
|
|
2069
2503
|
|
|
2070
2504
|
function captureManagedFileIdentityStreamingNoFollow(pathname: string): ManagedFileSnapshot["identity"] {
|
|
2071
|
-
const fd = fs.openSync(pathname, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
2505
|
+
const fd = fs.openSync(pathname, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | (fs.constants.O_NOFOLLOW ?? 0));
|
|
2072
2506
|
try {
|
|
2073
2507
|
const before = fs.fstatSync(fd, { bigint: true });
|
|
2074
2508
|
if (!before.isFile() || before.nlink > 1) throw new Error("source_changed");
|
|
@@ -2098,6 +2532,7 @@ export async function publishManagedFileNoReplace(
|
|
|
2098
2532
|
root?: ManagedDirectoryRoot,
|
|
2099
2533
|
policy: ManagedSessionSecurityPolicy = "default",
|
|
2100
2534
|
): Promise<void> {
|
|
2535
|
+
if (bytes.byteLength > MANAGED_ARTIFACT_MAX_FILE_BYTES) throw new Error("content_too_large");
|
|
2101
2536
|
const parent = path.dirname(destination);
|
|
2102
2537
|
ensureManagedDirectory(parent, root, policy);
|
|
2103
2538
|
const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.staging`);
|
|
@@ -2179,6 +2614,7 @@ export function publishManagedFileNoReplaceSync(
|
|
|
2179
2614
|
root?: ManagedDirectoryRoot,
|
|
2180
2615
|
policy: ManagedSessionSecurityPolicy = "default",
|
|
2181
2616
|
): ManagedFileSnapshot["identity"] {
|
|
2617
|
+
if (bytes.byteLength > MANAGED_ARTIFACT_MAX_FILE_BYTES) throw new Error("content_too_large");
|
|
2182
2618
|
const parent = path.dirname(destination);
|
|
2183
2619
|
ensureManagedDirectory(parent, root, policy);
|
|
2184
2620
|
const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.staging`);
|
|
@@ -2250,7 +2686,9 @@ function replaceManagedFileGeneratedSync(
|
|
|
2250
2686
|
policy: ManagedSessionSecurityPolicy = "default",
|
|
2251
2687
|
assertFence?: () => void,
|
|
2252
2688
|
expectedDestination?: ManagedFileSnapshot["identity"],
|
|
2253
|
-
|
|
2689
|
+
acceptCommittedCleanupFailure = false,
|
|
2690
|
+
operation: "replace" | "append" = "replace",
|
|
2691
|
+
): ManagedFileSnapshot["identity"] {
|
|
2254
2692
|
const parent = path.dirname(destination);
|
|
2255
2693
|
ensureManagedDirectory(parent, root, policy);
|
|
2256
2694
|
const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.replacement`);
|
|
@@ -2267,6 +2705,10 @@ function replaceManagedFileGeneratedSync(
|
|
|
2267
2705
|
}
|
|
2268
2706
|
| undefined;
|
|
2269
2707
|
let failure: unknown;
|
|
2708
|
+
let publishedIdentity: ManagedFileSnapshot["identity"] | undefined;
|
|
2709
|
+
let publicationDurable = false;
|
|
2710
|
+
let publicationCommitted = false;
|
|
2711
|
+
let expectedSuccessor: ManagedFileSnapshot["identity"] | undefined;
|
|
2270
2712
|
try {
|
|
2271
2713
|
fd = fs.openSync(
|
|
2272
2714
|
staging,
|
|
@@ -2274,11 +2716,14 @@ function replaceManagedFileGeneratedSync(
|
|
|
2274
2716
|
0o600,
|
|
2275
2717
|
);
|
|
2276
2718
|
secureFileDescriptor(staging, fd, "apply");
|
|
2719
|
+
const initiallyStaged = fs.fstatSync(fd, { bigint: true });
|
|
2720
|
+
stagedIdentity = { dev: initiallyStaged.dev, ino: initiallyStaged.ino };
|
|
2277
2721
|
const generated = writeContent(fd);
|
|
2278
2722
|
fs.fsyncSync(fd);
|
|
2279
2723
|
secureFileDescriptor(staging, fd, "verify");
|
|
2280
2724
|
const staged = fs.fstatSync(fd, { bigint: true });
|
|
2281
2725
|
stagedIdentity = { dev: staged.dev, ino: staged.ino };
|
|
2726
|
+
expectedSuccessor = identity(staged, generated.sha256);
|
|
2282
2727
|
if (process.platform === "win32" && expectedDestination) {
|
|
2283
2728
|
fs.closeSync(fd);
|
|
2284
2729
|
fd = undefined;
|
|
@@ -2288,7 +2733,7 @@ function replaceManagedFileGeneratedSync(
|
|
|
2288
2733
|
if (expectedDestination) {
|
|
2289
2734
|
const parentIdentity = fs.lstatSync(parent, { bigint: true });
|
|
2290
2735
|
if (Number(staged.size) !== generated.size) throw new Error("managed_replace_generated_size_changed");
|
|
2291
|
-
const successor =
|
|
2736
|
+
const successor = expectedSuccessor;
|
|
2292
2737
|
const receiptStagingPath = path.join(parent, `.gjc-replace-receipt-pending-${randomUUID()}.json`);
|
|
2293
2738
|
preserveStaging = true;
|
|
2294
2739
|
const publishedReceiptIdentity = publishManagedFileNoReplaceSync(
|
|
@@ -2310,7 +2755,7 @@ function replaceManagedFileGeneratedSync(
|
|
|
2310
2755
|
nativeSessionStorage().renameNoReplacePath(receiptStagingPath, receiptPath),
|
|
2311
2756
|
);
|
|
2312
2757
|
if (!receiptPublish.ok) throw publishFailure(receiptPublish);
|
|
2313
|
-
const namedReceipt =
|
|
2758
|
+
const namedReceipt = captureManagedFilePrefixNoFollow(receiptPath, REPLACEMENT_CLEANUP_RECEIPT_MAX_BYTES);
|
|
2314
2759
|
if (
|
|
2315
2760
|
namedReceipt.identity.dev !== publishedReceiptIdentity.dev ||
|
|
2316
2761
|
namedReceipt.identity.ino !== publishedReceiptIdentity.ino
|
|
@@ -2348,23 +2793,41 @@ function replaceManagedFileGeneratedSync(
|
|
|
2348
2793
|
sha256: expectedDestination.sha256,
|
|
2349
2794
|
},
|
|
2350
2795
|
);
|
|
2351
|
-
if (!replaced.ok)
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2796
|
+
if (!replaced.ok) {
|
|
2797
|
+
publicationCommitted =
|
|
2798
|
+
replaced.code === "cleanup_pending" ||
|
|
2799
|
+
replaced.code === "durability_failed" ||
|
|
2800
|
+
Boolean(
|
|
2801
|
+
replaced.detachedPath ??
|
|
2802
|
+
replaced.retainedSuccessorPath ??
|
|
2803
|
+
replaced.retainedPlaceholderPath ??
|
|
2804
|
+
replaced.retainedUnknownPath,
|
|
2805
|
+
);
|
|
2806
|
+
throw new ManagedReplaceError(replaced, receiptCleanup.path);
|
|
2807
|
+
}
|
|
2808
|
+
publicationCommitted = true;
|
|
2355
2809
|
fsyncDirectory(parent);
|
|
2356
|
-
} else
|
|
2810
|
+
} else {
|
|
2811
|
+
fs.renameSync(staging, destination);
|
|
2812
|
+
publicationCommitted = true;
|
|
2813
|
+
}
|
|
2357
2814
|
assertManagedDirectoryRoot(root);
|
|
2358
2815
|
const named = fs.lstatSync(destination, { bigint: true });
|
|
2359
2816
|
if (!named.isFile() || named.isSymbolicLink() || named.dev !== staged.dev || named.ino !== staged.ino) {
|
|
2360
2817
|
throw new Error("destination_identity_changed");
|
|
2361
2818
|
}
|
|
2819
|
+
if (!expectedSuccessor) throw new Error("managed_replace_identity_unavailable");
|
|
2362
2820
|
if (fd !== undefined) {
|
|
2363
2821
|
secureFileDescriptor(destination, fd, "verify");
|
|
2364
2822
|
fs.closeSync(fd);
|
|
2365
2823
|
fd = undefined;
|
|
2366
2824
|
}
|
|
2367
2825
|
fsyncDirectory(parent);
|
|
2826
|
+
const verifiedIdentity = captureManagedFileIdentityStreamingNoFollow(destination);
|
|
2827
|
+
if (!sameReplacementIdentity(verifiedIdentity, expectedSuccessor))
|
|
2828
|
+
throw new Error("destination_identity_changed");
|
|
2829
|
+
publishedIdentity = verifiedIdentity;
|
|
2830
|
+
publicationDurable = true;
|
|
2368
2831
|
if (receiptCleanup) {
|
|
2369
2832
|
try {
|
|
2370
2833
|
const removed = nativeSessionStorage().exactUnlink(receiptCleanup.path, {
|
|
@@ -2394,9 +2857,27 @@ function replaceManagedFileGeneratedSync(
|
|
|
2394
2857
|
}
|
|
2395
2858
|
}
|
|
2396
2859
|
} catch (error) {
|
|
2397
|
-
failure =
|
|
2860
|
+
failure =
|
|
2861
|
+
publicationCommitted && !publishedIdentity ? new ManagedCommittedMutationError(operation, error) : error;
|
|
2398
2862
|
} finally {
|
|
2399
|
-
if (fd !== undefined)
|
|
2863
|
+
if (fd !== undefined) {
|
|
2864
|
+
try {
|
|
2865
|
+
fs.closeSync(fd);
|
|
2866
|
+
} catch (error) {
|
|
2867
|
+
if (publicationCommitted && !publishedIdentity) {
|
|
2868
|
+
const cause =
|
|
2869
|
+
failure === undefined
|
|
2870
|
+
? error
|
|
2871
|
+
: new AggregateError([failure, error], "Managed replacement and descriptor close both failed.");
|
|
2872
|
+
failure = new ManagedCommittedMutationError(operation, cause);
|
|
2873
|
+
} else {
|
|
2874
|
+
failure =
|
|
2875
|
+
failure === undefined
|
|
2876
|
+
? error
|
|
2877
|
+
: new AggregateError([failure, error], "Managed replacement and descriptor close both failed.");
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2400
2881
|
if (stagedIdentity && !preserveStaging) {
|
|
2401
2882
|
try {
|
|
2402
2883
|
const named = fs.lstatSync(staging, { bigint: true });
|
|
@@ -2411,7 +2892,10 @@ function replaceManagedFileGeneratedSync(
|
|
|
2411
2892
|
}
|
|
2412
2893
|
}
|
|
2413
2894
|
}
|
|
2414
|
-
if (failure !== undefined
|
|
2895
|
+
if (failure !== undefined && !(acceptCommittedCleanupFailure && publicationDurable && publishedIdentity))
|
|
2896
|
+
throw failure;
|
|
2897
|
+
if (!publishedIdentity) throw new Error("managed_replace_identity_unavailable");
|
|
2898
|
+
return publishedIdentity;
|
|
2415
2899
|
}
|
|
2416
2900
|
|
|
2417
2901
|
export function replaceManagedFileSync(
|
|
@@ -2422,6 +2906,7 @@ export function replaceManagedFileSync(
|
|
|
2422
2906
|
assertFence?: () => void,
|
|
2423
2907
|
expectedDestination?: ManagedFileSnapshot["identity"],
|
|
2424
2908
|
): void {
|
|
2909
|
+
if (bytes.byteLength > MANAGED_ARTIFACT_MAX_FILE_BYTES) throw new Error("content_too_large");
|
|
2425
2910
|
replaceManagedFileGeneratedSync(
|
|
2426
2911
|
destination,
|
|
2427
2912
|
fd => {
|
|
@@ -2449,10 +2934,15 @@ function appendManagedFileStreamingSync(
|
|
|
2449
2934
|
policy: ManagedSessionSecurityPolicy,
|
|
2450
2935
|
): ManagedFileSnapshot["identity"] {
|
|
2451
2936
|
const predecessor = captureManagedFileIdentityStreamingNoFollow(destination);
|
|
2452
|
-
|
|
2937
|
+
if (predecessor.size > MANAGED_ARTIFACT_MAX_FILE_BYTES - appendedBytes.byteLength)
|
|
2938
|
+
throw new Error("content_too_large");
|
|
2939
|
+
return replaceManagedFileGeneratedSync(
|
|
2453
2940
|
destination,
|
|
2454
2941
|
stagingFd => {
|
|
2455
|
-
const sourceFd = fs.openSync(
|
|
2942
|
+
const sourceFd = fs.openSync(
|
|
2943
|
+
destination,
|
|
2944
|
+
fs.constants.O_RDONLY | fs.constants.O_NONBLOCK | (fs.constants.O_NOFOLLOW ?? 0),
|
|
2945
|
+
);
|
|
2456
2946
|
try {
|
|
2457
2947
|
const before = fs.fstatSync(sourceFd, { bigint: true });
|
|
2458
2948
|
if (!sameReplacementIdentity(identity(before, predecessor.sha256), predecessor))
|
|
@@ -2480,6 +2970,8 @@ function appendManagedFileStreamingSync(
|
|
|
2480
2970
|
)
|
|
2481
2971
|
throw new Error("managed_replace_identity_mismatch");
|
|
2482
2972
|
if (hash.copy().digest("hex") !== predecessor.sha256) throw new Error("managed_replace_identity_mismatch");
|
|
2973
|
+
if (total > MANAGED_ARTIFACT_MAX_FILE_BYTES - appendedBytes.byteLength)
|
|
2974
|
+
throw new Error("content_too_large");
|
|
2483
2975
|
let appended = 0;
|
|
2484
2976
|
while (appended < appendedBytes.byteLength) {
|
|
2485
2977
|
const amount = fs.writeSync(stagingFd, appendedBytes, appended, appendedBytes.byteLength - appended);
|
|
@@ -2496,8 +2988,9 @@ function appendManagedFileStreamingSync(
|
|
|
2496
2988
|
policy,
|
|
2497
2989
|
undefined,
|
|
2498
2990
|
predecessor,
|
|
2991
|
+
true,
|
|
2992
|
+
"append",
|
|
2499
2993
|
);
|
|
2500
|
-
return captureManagedFileIdentityStreamingNoFollow(destination);
|
|
2501
2994
|
}
|
|
2502
2995
|
|
|
2503
2996
|
export async function replaceManagedFile(
|
|
@@ -2697,23 +3190,31 @@ export function validateManagedArtifactTree(root: string, limits: ManagedArtifac
|
|
|
2697
3190
|
};
|
|
2698
3191
|
const maxFiles = clampLimit(limits.maxFiles, MANAGED_ARTIFACT_MAX_FILES);
|
|
2699
3192
|
const maxTotalBytes = clampLimit(limits.maxTotalBytes, MANAGED_ARTIFACT_MAX_TOTAL_BYTES);
|
|
3193
|
+
let entries = 0;
|
|
2700
3194
|
let files = 0;
|
|
2701
3195
|
let bytes = 0;
|
|
2702
3196
|
const visit = (directory: string, depth: number): void => {
|
|
2703
3197
|
if (depth > MANAGED_ARTIFACT_MAX_DEPTH) throw new Error("unsafe_artifacts");
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
3198
|
+
const handle = fs.opendirSync(directory);
|
|
3199
|
+
try {
|
|
3200
|
+
for (let entry = handle.readSync(); entry; entry = handle.readSync()) {
|
|
3201
|
+
entries++;
|
|
3202
|
+
if (entries > maxFiles) throw new Error("artifact_capacity_exceeded");
|
|
3203
|
+
const entryPath = path.join(directory, entry.name);
|
|
3204
|
+
const stat = fs.lstatSync(entryPath);
|
|
3205
|
+
if (stat.isSymbolicLink()) throw new Error("unsafe_artifacts");
|
|
3206
|
+
if (stat.isDirectory()) {
|
|
3207
|
+
visit(entryPath, depth + 1);
|
|
3208
|
+
continue;
|
|
3209
|
+
}
|
|
3210
|
+
if (stat.nlink > 1) throw new Error("unsafe_artifacts");
|
|
3211
|
+
if (!stat.isFile() || stat.size > MANAGED_ARTIFACT_MAX_FILE_BYTES) throw new Error("unsafe_artifacts");
|
|
3212
|
+
files++;
|
|
3213
|
+
bytes += stat.size;
|
|
3214
|
+
if (files > maxFiles || bytes > maxTotalBytes) throw new Error("artifact_capacity_exceeded");
|
|
2711
3215
|
}
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
files++;
|
|
2715
|
-
bytes += stat.size;
|
|
2716
|
-
if (files > maxFiles || bytes > maxTotalBytes) throw new Error("artifact_capacity_exceeded");
|
|
3216
|
+
} finally {
|
|
3217
|
+
handle.closeSync();
|
|
2717
3218
|
}
|
|
2718
3219
|
};
|
|
2719
3220
|
const rootStat = fs.lstatSync(root);
|