@gajae-code/coding-agent 0.12.10 → 0.12.11
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 +9 -2
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +12 -2
- package/dist/types/session/session-manager.d.ts +17 -0
- package/dist/types/session/session-storage.d.ts +1 -0
- package/dist/types/tools/computer.d.ts +35 -2
- package/dist/types/tools/todo-write.d.ts +2 -0
- package/package.json +8 -8
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/prompts/tools/computer.md +6 -1
- package/src/sdk/bus/chat-daemon-control.ts +5 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -2
- package/src/session/agent-session.ts +111 -21
- package/src/session/internal/managed-session-storage.ts +244 -246
- package/src/session/session-manager.ts +140 -6
- package/src/session/session-storage.ts +3 -0
- package/src/tools/computer.ts +322 -45
- package/src/tools/todo-write.ts +29 -11
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
applyOwnerOnlyPathSecurity,
|
|
8
8
|
exactRemoveDirectoryTree,
|
|
9
9
|
exactReplacePath,
|
|
10
|
-
exactRestore,
|
|
11
10
|
exactUnlink,
|
|
12
11
|
linkNoReplacePath,
|
|
13
12
|
type NativeDirectoryTreeSnapshot,
|
|
13
|
+
type NativeExactUnlinkResult,
|
|
14
14
|
type NativeOwnerOnlySecurityResult,
|
|
15
15
|
openRecoveryFsRoot,
|
|
16
16
|
type RecoveryFsRoot,
|
|
@@ -55,6 +55,28 @@ export class ManagedPublishError extends Error {
|
|
|
55
55
|
this.diagnostic = formatNativePublishDiagnostic(outcome);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
export class ManagedReplaceError extends Error {
|
|
59
|
+
readonly code: string;
|
|
60
|
+
readonly cleanupReceiptPath: string;
|
|
61
|
+
readonly detachedPath: string | undefined;
|
|
62
|
+
readonly retainedSuccessorPath: string | undefined;
|
|
63
|
+
readonly retainedPlaceholderPath: string | undefined;
|
|
64
|
+
readonly retainedUnknownPath: string | undefined;
|
|
65
|
+
readonly replacementCause: unknown;
|
|
66
|
+
|
|
67
|
+
constructor(result: NativeExactUnlinkResult, cleanupReceiptPath: string, replacementCause?: unknown) {
|
|
68
|
+
const code = result.code ?? "unknown";
|
|
69
|
+
super(`managed_replace_failed:${code}`, replacementCause === undefined ? undefined : { cause: replacementCause });
|
|
70
|
+
this.name = "ManagedReplaceError";
|
|
71
|
+
this.code = code;
|
|
72
|
+
this.cleanupReceiptPath = cleanupReceiptPath;
|
|
73
|
+
this.detachedPath = result.detachedPath;
|
|
74
|
+
this.retainedSuccessorPath = result.retainedSuccessorPath;
|
|
75
|
+
this.retainedPlaceholderPath = result.retainedPlaceholderPath;
|
|
76
|
+
this.retainedUnknownPath = result.retainedUnknownPath;
|
|
77
|
+
this.replacementCause = replacementCause;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
58
80
|
|
|
59
81
|
function publishFailure(outcome: NativePublishOutcome): ManagedPublishError {
|
|
60
82
|
const classification =
|
|
@@ -74,6 +96,17 @@ function publishFailure(outcome: NativePublishOutcome): ManagedPublishError {
|
|
|
74
96
|
return new ManagedPublishError(classification, outcome);
|
|
75
97
|
}
|
|
76
98
|
|
|
99
|
+
function exactUnlinkCompleted(result: NativeExactUnlinkResult): boolean {
|
|
100
|
+
return (
|
|
101
|
+
result.ok ||
|
|
102
|
+
(result.code === "cleanup_pending" &&
|
|
103
|
+
result.payloadDurable === true &&
|
|
104
|
+
result.detachedPath !== undefined &&
|
|
105
|
+
result.retainedSuccessorPath === undefined &&
|
|
106
|
+
result.retainedUnknownPath === undefined)
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
77
110
|
/** A same-filesystem rename updates the moved root's ctime but no other tree identity. */
|
|
78
111
|
function sameDirectoryTreeSnapshotAfterRename(
|
|
79
112
|
left: NativeDirectoryTreeSnapshot,
|
|
@@ -166,27 +199,69 @@ export interface ManagedFileSnapshot {
|
|
|
166
199
|
};
|
|
167
200
|
}
|
|
168
201
|
|
|
202
|
+
type SerializedReplacementIdentity = {
|
|
203
|
+
dev: string;
|
|
204
|
+
ino: string;
|
|
205
|
+
nlink: string;
|
|
206
|
+
size: string;
|
|
207
|
+
mtimeNs: string;
|
|
208
|
+
ctimeNs: string;
|
|
209
|
+
sha256: string;
|
|
210
|
+
};
|
|
211
|
+
|
|
169
212
|
type ReplacementCleanupReceipt = {
|
|
170
|
-
version:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
ino: string;
|
|
176
|
-
nlink: string;
|
|
177
|
-
size: string;
|
|
178
|
-
mtimeNs: string;
|
|
179
|
-
ctimeNs: string;
|
|
180
|
-
sha256: string;
|
|
181
|
-
};
|
|
213
|
+
version: 3;
|
|
214
|
+
staging: string;
|
|
215
|
+
destination: string;
|
|
216
|
+
successor: SerializedReplacementIdentity;
|
|
217
|
+
predecessor: SerializedReplacementIdentity;
|
|
182
218
|
};
|
|
183
219
|
|
|
184
220
|
const U64_MAX = 18_446_744_073_709_551_615n;
|
|
185
221
|
|
|
186
|
-
function
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
222
|
+
function replacementCleanupReceiptBinding(
|
|
223
|
+
name: string,
|
|
224
|
+
): { predecessor: { dev: bigint; ino: bigint }; receipt: { dev: bigint; ino: bigint } } | undefined {
|
|
225
|
+
const match =
|
|
226
|
+
/^\.gjc-replace-cleanup-(0|[1-9a-f][0-9a-f]*)-(0|[1-9a-f][0-9a-f]*)-receipt-(0|[1-9a-f][0-9a-f]*)-(0|[1-9a-f][0-9a-f]*)\.json$/.exec(
|
|
227
|
+
name,
|
|
228
|
+
);
|
|
229
|
+
if (!match?.[1] || !match[2] || !match[3] || !match[4]) return undefined;
|
|
230
|
+
const predecessor = { dev: BigInt(`0x${match[1]}`), ino: BigInt(`0x${match[2]}`) };
|
|
231
|
+
const receipt = { dev: BigInt(`0x${match[3]}`), ino: BigInt(`0x${match[4]}`) };
|
|
232
|
+
return predecessor.dev <= U64_MAX && predecessor.ino <= U64_MAX && receipt.dev <= U64_MAX && receipt.ino <= U64_MAX
|
|
233
|
+
? { predecessor, receipt }
|
|
234
|
+
: undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function serializeReplacementIdentity(identity: ManagedFileSnapshot["identity"]): SerializedReplacementIdentity {
|
|
238
|
+
return {
|
|
239
|
+
dev: identity.dev.toString(),
|
|
240
|
+
ino: identity.ino.toString(),
|
|
241
|
+
nlink: identity.nlink.toString(),
|
|
242
|
+
size: String(identity.size),
|
|
243
|
+
mtimeNs: identity.mtimeNs.toString(),
|
|
244
|
+
ctimeNs: identity.ctimeNs.toString(),
|
|
245
|
+
sha256: identity.sha256,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function replacementReceiptPath(
|
|
250
|
+
baseDir: string,
|
|
251
|
+
predecessor: ManagedFileSnapshot["identity"],
|
|
252
|
+
receipt: ManagedFileSnapshot["identity"],
|
|
253
|
+
): string {
|
|
254
|
+
return path.join(
|
|
255
|
+
baseDir,
|
|
256
|
+
`.gjc-replace-cleanup-${predecessor.dev.toString(16)}-${predecessor.ino.toString(16)}-receipt-${receipt.dev.toString(16)}-${receipt.ino.toString(16)}.json`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function replacementReceiptRetirementName(
|
|
261
|
+
receipt: ManagedFileSnapshot["identity"],
|
|
262
|
+
predecessor: ManagedFileSnapshot["identity"],
|
|
263
|
+
): string {
|
|
264
|
+
return `.gjc-receipt-remove-${receipt.dev.toString(16)}-${receipt.ino.toString(16)}-${predecessor.dev.toString(16)}-${predecessor.ino.toString(16)}`;
|
|
190
265
|
}
|
|
191
266
|
|
|
192
267
|
const ACL_FAILURE_CODES = new Set(["acl_denied", "acl_io_error", "acl_present", "acl_malformed", "acl_unknown"]);
|
|
@@ -710,72 +785,44 @@ export class ManagedSessionDescendantStore {
|
|
|
710
785
|
}
|
|
711
786
|
}
|
|
712
787
|
|
|
788
|
+
#detachReplacementCleanupReceipt(receiptPath: string): void {
|
|
789
|
+
const binding = replacementCleanupReceiptBinding(path.basename(receiptPath));
|
|
790
|
+
if (!binding) throw new Error("managed_replace_cleanup_receipt_invalid");
|
|
791
|
+
const receipt = captureManagedFileNoFollow(receiptPath);
|
|
792
|
+
if (receipt.identity.dev !== binding.receipt.dev || receipt.identity.ino !== binding.receipt.ino)
|
|
793
|
+
throw new Error("managed_replace_cleanup_receipt_invalid");
|
|
794
|
+
const predecessor = binding.predecessor;
|
|
795
|
+
const quarantineName = `.gjc-receipt-remove-${receipt.identity.dev.toString(16)}-${receipt.identity.ino.toString(16)}-${predecessor.dev.toString(16)}-${predecessor.ino.toString(16)}`;
|
|
796
|
+
const detached = exactUnlink(receiptPath, {
|
|
797
|
+
dev: receipt.identity.dev,
|
|
798
|
+
ino: receipt.identity.ino,
|
|
799
|
+
nlink: receipt.identity.nlink,
|
|
800
|
+
parentDev: this.#subtreeRoot.dev,
|
|
801
|
+
parentIno: this.#subtreeRoot.ino,
|
|
802
|
+
size: BigInt(receipt.identity.size),
|
|
803
|
+
mtimeNs: receipt.identity.mtimeNs,
|
|
804
|
+
sha256: receipt.identity.sha256,
|
|
805
|
+
detachOnly: true,
|
|
806
|
+
quarantineName,
|
|
807
|
+
});
|
|
808
|
+
if (
|
|
809
|
+
(!detached.ok && detached.code !== "cleanup_pending") ||
|
|
810
|
+
detached.detachedPath !== path.join(this.#baseDir, quarantineName) ||
|
|
811
|
+
detached.retainedSuccessorPath ||
|
|
812
|
+
detached.retainedUnknownPath
|
|
813
|
+
)
|
|
814
|
+
throw new Error(`managed_replace_receipt_cleanup_pending:${detached.code ?? "unknown"}`);
|
|
815
|
+
fsyncDirectory(this.#baseDir);
|
|
816
|
+
}
|
|
817
|
+
|
|
713
818
|
#reconcileReplacementCleanupReceipts(): void {
|
|
714
819
|
if (this.#reconcilingReplacementCleanup) return;
|
|
715
820
|
this.#reconcilingReplacementCleanup = true;
|
|
716
821
|
try {
|
|
717
822
|
for (const name of fs.readdirSync(this.#baseDir)) {
|
|
718
|
-
|
|
719
|
-
if (!
|
|
720
|
-
|
|
721
|
-
const parsed = JSON.parse(
|
|
722
|
-
captureManagedFileNoFollow(receiptPath).bytes.toString("utf8"),
|
|
723
|
-
) as Partial<ReplacementCleanupReceipt>;
|
|
724
|
-
const identity = parsed.identity;
|
|
725
|
-
const dev = parseCanonicalU64(identity?.dev);
|
|
726
|
-
const ino = parseCanonicalU64(identity?.ino);
|
|
727
|
-
const nlink = parseCanonicalU64(identity?.nlink);
|
|
728
|
-
const size = parseCanonicalU64(identity?.size);
|
|
729
|
-
const mtimeNs = parseCanonicalU64(identity?.mtimeNs);
|
|
730
|
-
const ctimeNs = parseCanonicalU64(identity?.ctimeNs);
|
|
731
|
-
const expectedPredecessor =
|
|
732
|
-
dev !== undefined && ino !== undefined
|
|
733
|
-
? path.join(this.#baseDir, `.gjc-exact-replace-destination-${dev.toString(16)}-${ino.toString(16)}`)
|
|
734
|
-
: undefined;
|
|
735
|
-
if (
|
|
736
|
-
parsed.version !== 1 ||
|
|
737
|
-
dev === undefined ||
|
|
738
|
-
ino === undefined ||
|
|
739
|
-
nlink === undefined ||
|
|
740
|
-
size === undefined ||
|
|
741
|
-
mtimeNs === undefined ||
|
|
742
|
-
ctimeNs === undefined ||
|
|
743
|
-
typeof identity?.sha256 !== "string" ||
|
|
744
|
-
!match[1] ||
|
|
745
|
-
!match[2] ||
|
|
746
|
-
BigInt(`0x${match[1]}`) !== dev ||
|
|
747
|
-
BigInt(`0x${match[2]}`) !== ino ||
|
|
748
|
-
typeof parsed.predecessor !== "string" ||
|
|
749
|
-
path.resolve(parsed.predecessor) !== expectedPredecessor ||
|
|
750
|
-
typeof parsed.successor !== "string" ||
|
|
751
|
-
path.dirname(path.resolve(parsed.successor)) !== this.#baseDir
|
|
752
|
-
)
|
|
753
|
-
throw new Error("managed_replace_cleanup_receipt_invalid");
|
|
754
|
-
const retired = exactUnlink(expectedPredecessor, {
|
|
755
|
-
dev,
|
|
756
|
-
ino,
|
|
757
|
-
nlink,
|
|
758
|
-
parentDev: this.#subtreeRoot.dev,
|
|
759
|
-
parentIno: this.#subtreeRoot.ino,
|
|
760
|
-
size,
|
|
761
|
-
mtimeNs,
|
|
762
|
-
sha256: identity.sha256,
|
|
763
|
-
quarantineName: `.gjc-replace-retry-${dev.toString(16)}-${ino.toString(16)}`,
|
|
764
|
-
});
|
|
765
|
-
if (!retired.ok) throw new Error(`managed_replace_cleanup_pending:${retired.code ?? "unknown"}`);
|
|
766
|
-
const receipt = captureManagedFileNoFollow(receiptPath);
|
|
767
|
-
const removed = exactUnlink(receiptPath, {
|
|
768
|
-
dev: receipt.identity.dev,
|
|
769
|
-
ino: receipt.identity.ino,
|
|
770
|
-
nlink: receipt.identity.nlink,
|
|
771
|
-
parentDev: this.#subtreeRoot.dev,
|
|
772
|
-
parentIno: this.#subtreeRoot.ino,
|
|
773
|
-
size: BigInt(receipt.identity.size),
|
|
774
|
-
mtimeNs: receipt.identity.mtimeNs,
|
|
775
|
-
sha256: receipt.identity.sha256,
|
|
776
|
-
quarantineName: `.gjc-receipt-remove-${receipt.identity.dev.toString(16)}-${receipt.identity.ino.toString(16)}`,
|
|
777
|
-
});
|
|
778
|
-
if (!removed.ok) throw new Error(`managed_replace_receipt_cleanup_pending:${removed.code ?? "unknown"}`);
|
|
823
|
+
if (!name.startsWith(".gjc-replace-cleanup-")) continue;
|
|
824
|
+
if (!replacementCleanupReceiptBinding(name)) throw new Error("managed_replace_cleanup_receipt_invalid");
|
|
825
|
+
this.#detachReplacementCleanupReceipt(path.join(this.#baseDir, name));
|
|
779
826
|
}
|
|
780
827
|
} finally {
|
|
781
828
|
this.#reconcilingReplacementCleanup = false;
|
|
@@ -861,21 +908,7 @@ export class ManagedSessionDescendantStore {
|
|
|
861
908
|
const current = captureManagedFileNoFollow(resolved);
|
|
862
909
|
if (!sameIdentity(current.identity, expected.identity) || current.identity.sha256 !== expected.identity.sha256)
|
|
863
910
|
throw new Error("managed_replace_identity_mismatch");
|
|
864
|
-
replaceManagedFileSync(
|
|
865
|
-
resolved,
|
|
866
|
-
bytes,
|
|
867
|
-
this.#subtreeRoot,
|
|
868
|
-
this.#policy,
|
|
869
|
-
() => {
|
|
870
|
-
const currentAtCommit = captureManagedFileNoFollow(resolved);
|
|
871
|
-
if (
|
|
872
|
-
!sameIdentity(currentAtCommit.identity, expected.identity) ||
|
|
873
|
-
currentAtCommit.identity.sha256 !== expected.identity.sha256
|
|
874
|
-
)
|
|
875
|
-
throw new Error("managed_replace_identity_mismatch");
|
|
876
|
-
},
|
|
877
|
-
expected.identity,
|
|
878
|
-
);
|
|
911
|
+
replaceManagedFileSync(resolved, bytes, this.#subtreeRoot, this.#policy, undefined, expected.identity);
|
|
879
912
|
return;
|
|
880
913
|
}
|
|
881
914
|
const replaced = (this.#authority as RecoveryFsRoot & RetainedManagedReplacer).replaceManaged(
|
|
@@ -914,7 +947,7 @@ export class ManagedSessionDescendantStore {
|
|
|
914
947
|
this.#beforeMutation();
|
|
915
948
|
this.#assertBound();
|
|
916
949
|
const resolved = this.#resolve(relativePath);
|
|
917
|
-
|
|
950
|
+
const existing = this.readExpected(relativePath);
|
|
918
951
|
if (!existing) throw new Error("managed_append_missing");
|
|
919
952
|
if (this.#authority) {
|
|
920
953
|
const appended = this.#authority.appendManaged(
|
|
@@ -931,52 +964,18 @@ export class ManagedSessionDescendantStore {
|
|
|
931
964
|
this.#assertBound();
|
|
932
965
|
return;
|
|
933
966
|
}
|
|
934
|
-
//
|
|
935
|
-
//
|
|
936
|
-
//
|
|
937
|
-
//
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
process.platform === "darwin" &&
|
|
947
|
-
!darwinCtimeRefreshConsumed &&
|
|
948
|
-
isCtimeOnlyIdentityMismatch(before, existing.identity);
|
|
949
|
-
if (!allowDarwinCtimeRefresh) throw new Error("identity_mismatch");
|
|
950
|
-
fs.closeSync(fd);
|
|
951
|
-
fd = undefined;
|
|
952
|
-
darwinCtimeRefreshConsumed = true;
|
|
953
|
-
const original = existing;
|
|
954
|
-
const refreshed = this.readExpected(relativePath);
|
|
955
|
-
if (
|
|
956
|
-
!refreshed ||
|
|
957
|
-
!sameStableIdentityIgnoringCtime(refreshed.identity, original.identity) ||
|
|
958
|
-
refreshed.identity.sha256 !== original.identity.sha256 ||
|
|
959
|
-
!refreshed.bytes.equals(original.bytes)
|
|
960
|
-
) {
|
|
961
|
-
throw new Error("identity_mismatch");
|
|
962
|
-
}
|
|
963
|
-
existing = refreshed;
|
|
964
|
-
continue;
|
|
965
|
-
}
|
|
966
|
-
secureFileDescriptor(resolved, fd, "verify");
|
|
967
|
-
let offset = 0;
|
|
968
|
-
while (offset < bytes.byteLength) offset += fs.writeSync(fd, bytes, offset, bytes.byteLength - offset);
|
|
969
|
-
fs.fsyncSync(fd);
|
|
970
|
-
secureFileDescriptor(resolved, fd, "verify");
|
|
971
|
-
const after = identity(fs.fstatSync(fd, { bigint: true }));
|
|
972
|
-
const named = identity(fs.lstatSync(resolved, { bigint: true }));
|
|
973
|
-
if (!sameIdentity(after, named) || after.dev !== before.dev || after.ino !== before.ino)
|
|
974
|
-
throw new Error("identity_mismatch");
|
|
975
|
-
break;
|
|
976
|
-
}
|
|
977
|
-
} finally {
|
|
978
|
-
if (fd !== undefined) fs.closeSync(fd);
|
|
979
|
-
}
|
|
967
|
+
// Darwin has no retained native root authority. Do not append in place:
|
|
968
|
+
// a short write can leave a malformed JSONL tail with no safe recovery
|
|
969
|
+
// boundary. Replace the exact captured file instead so the old transcript
|
|
970
|
+
// or the complete successor remains recoverable after every reported error.
|
|
971
|
+
replaceManagedFileSync(
|
|
972
|
+
resolved,
|
|
973
|
+
Buffer.concat([existing.bytes, Buffer.from(bytes)]),
|
|
974
|
+
this.#subtreeRoot,
|
|
975
|
+
this.#policy,
|
|
976
|
+
undefined,
|
|
977
|
+
existing.identity,
|
|
978
|
+
);
|
|
980
979
|
this.#assertBound();
|
|
981
980
|
}
|
|
982
981
|
|
|
@@ -1425,25 +1424,19 @@ function sameIdentity(left: ManagedFileSnapshot["identity"], right: ManagedFileS
|
|
|
1425
1424
|
left.ctimeNs === right.ctimeNs
|
|
1426
1425
|
);
|
|
1427
1426
|
}
|
|
1428
|
-
|
|
1429
|
-
/** Content/path identity that remains meaningful across a Darwin write-open ctime-only transition. */
|
|
1430
|
-
function sameStableIdentityIgnoringCtime(
|
|
1427
|
+
function sameReplacementIdentity(
|
|
1431
1428
|
left: ManagedFileSnapshot["identity"],
|
|
1432
1429
|
right: ManagedFileSnapshot["identity"],
|
|
1433
1430
|
): boolean {
|
|
1434
1431
|
return (
|
|
1435
|
-
left.dev === right.dev &&
|
|
1432
|
+
left.dev === right.dev &&
|
|
1433
|
+
left.ino === right.ino &&
|
|
1434
|
+
left.nlink === right.nlink &&
|
|
1435
|
+
left.size === right.size &&
|
|
1436
|
+
left.mtimeNs === right.mtimeNs
|
|
1436
1437
|
);
|
|
1437
1438
|
}
|
|
1438
1439
|
|
|
1439
|
-
/** True when observed identity matches expected on every field except ctimeNs. */
|
|
1440
|
-
function isCtimeOnlyIdentityMismatch(
|
|
1441
|
-
observed: ManagedFileSnapshot["identity"],
|
|
1442
|
-
expected: ManagedFileSnapshot["identity"],
|
|
1443
|
-
): boolean {
|
|
1444
|
-
return sameStableIdentityIgnoringCtime(observed, expected) && observed.ctimeNs !== expected.ctimeNs;
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
1440
|
function parseLockBytes(bytes: Uint8Array): LockRecord | undefined {
|
|
1448
1441
|
try {
|
|
1449
1442
|
const value: unknown = JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
@@ -1668,12 +1661,13 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1668
1661
|
bytes: Uint8Array,
|
|
1669
1662
|
root?: ManagedDirectoryRoot,
|
|
1670
1663
|
policy: ManagedSessionSecurityPolicy = "default",
|
|
1671
|
-
):
|
|
1664
|
+
): ManagedFileSnapshot["identity"] {
|
|
1672
1665
|
const parent = path.dirname(destination);
|
|
1673
1666
|
ensureManagedDirectory(parent, root, policy);
|
|
1674
1667
|
const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.staging`);
|
|
1675
1668
|
let fd: number | undefined;
|
|
1676
1669
|
let stagingIdentity: { dev: bigint; ino: bigint } | undefined;
|
|
1670
|
+
let publishedIdentity: ManagedFileSnapshot["identity"] | undefined;
|
|
1677
1671
|
let failure: unknown;
|
|
1678
1672
|
let outcome: NativePublishOutcome | undefined;
|
|
1679
1673
|
let linkPublished = false;
|
|
@@ -1691,6 +1685,7 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1691
1685
|
secureFileDescriptor(staging, fd, "verify");
|
|
1692
1686
|
const staged = fs.fstatSync(fd, { bigint: true });
|
|
1693
1687
|
stagingIdentity = { dev: staged.dev, ino: staged.ino };
|
|
1688
|
+
publishedIdentity = identity(staged, createHash("sha256").update(bytes).digest("hex"));
|
|
1694
1689
|
|
|
1695
1690
|
outcome = classifyNativePublishOutcome(renameNoReplacePath(staging, destination));
|
|
1696
1691
|
if (renameFlagsUnsupported(outcome)) {
|
|
@@ -1726,6 +1721,8 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1726
1721
|
}
|
|
1727
1722
|
}
|
|
1728
1723
|
if (failure !== undefined) throw failure;
|
|
1724
|
+
if (!publishedIdentity) throw new Error("managed_publish_identity_unavailable");
|
|
1725
|
+
return publishedIdentity;
|
|
1729
1726
|
}
|
|
1730
1727
|
|
|
1731
1728
|
/** Replace one managed regular file while retaining the secured staging fd through publication. */
|
|
@@ -1743,6 +1740,16 @@ export function replaceManagedFileSync(
|
|
|
1743
1740
|
let fd: number | undefined;
|
|
1744
1741
|
let stagedIdentity: { dev: bigint; ino: bigint } | undefined;
|
|
1745
1742
|
let preserveStaging = false;
|
|
1743
|
+
let receiptCleanup:
|
|
1744
|
+
| {
|
|
1745
|
+
path: string;
|
|
1746
|
+
parentDev: bigint;
|
|
1747
|
+
parentIno: bigint;
|
|
1748
|
+
identity: ManagedFileSnapshot["identity"];
|
|
1749
|
+
predecessor: ManagedFileSnapshot["identity"];
|
|
1750
|
+
}
|
|
1751
|
+
| undefined;
|
|
1752
|
+
let failure: unknown;
|
|
1746
1753
|
try {
|
|
1747
1754
|
fd = fs.openSync(
|
|
1748
1755
|
staging,
|
|
@@ -1762,23 +1769,54 @@ export function replaceManagedFileSync(
|
|
|
1762
1769
|
}
|
|
1763
1770
|
assertManagedDirectoryRoot(root);
|
|
1764
1771
|
assertFence?.();
|
|
1765
|
-
if (
|
|
1766
|
-
throw new Error("managed_replace_exact_unavailable");
|
|
1767
|
-
}
|
|
1768
|
-
if (process.platform === "win32" && expectedDestination) {
|
|
1772
|
+
if (expectedDestination) {
|
|
1769
1773
|
const parentIdentity = fs.lstatSync(parent, { bigint: true });
|
|
1774
|
+
const successor = identity(staged, createHash("sha256").update(bytes).digest("hex"));
|
|
1775
|
+
const receiptStagingPath = path.join(parent, `.gjc-replace-receipt-pending-${randomUUID()}.json`);
|
|
1776
|
+
preserveStaging = true;
|
|
1777
|
+
const publishedReceiptIdentity = publishManagedFileNoReplaceSync(
|
|
1778
|
+
receiptStagingPath,
|
|
1779
|
+
Buffer.from(
|
|
1780
|
+
JSON.stringify({
|
|
1781
|
+
version: 3,
|
|
1782
|
+
staging,
|
|
1783
|
+
destination,
|
|
1784
|
+
successor: serializeReplacementIdentity(successor),
|
|
1785
|
+
predecessor: serializeReplacementIdentity(expectedDestination),
|
|
1786
|
+
} satisfies ReplacementCleanupReceipt),
|
|
1787
|
+
),
|
|
1788
|
+
root,
|
|
1789
|
+
policy,
|
|
1790
|
+
);
|
|
1791
|
+
const receiptPath = replacementReceiptPath(parent, expectedDestination, publishedReceiptIdentity);
|
|
1792
|
+
const receiptPublish = classifyNativePublishOutcome(renameNoReplacePath(receiptStagingPath, receiptPath));
|
|
1793
|
+
if (!receiptPublish.ok) throw publishFailure(receiptPublish);
|
|
1794
|
+
const namedReceipt = captureManagedFileNoFollow(receiptPath);
|
|
1795
|
+
if (
|
|
1796
|
+
namedReceipt.identity.dev !== publishedReceiptIdentity.dev ||
|
|
1797
|
+
namedReceipt.identity.ino !== publishedReceiptIdentity.ino
|
|
1798
|
+
)
|
|
1799
|
+
throw new Error("managed_replace_cleanup_receipt_identity_changed");
|
|
1800
|
+
fsyncDirectory(parent);
|
|
1801
|
+
receiptCleanup = {
|
|
1802
|
+
path: receiptPath,
|
|
1803
|
+
parentDev: parentIdentity.dev,
|
|
1804
|
+
parentIno: parentIdentity.ino,
|
|
1805
|
+
identity: publishedReceiptIdentity,
|
|
1806
|
+
predecessor: expectedDestination,
|
|
1807
|
+
};
|
|
1770
1808
|
const replaced = exactReplacePath(
|
|
1771
1809
|
staging,
|
|
1772
1810
|
destination,
|
|
1773
1811
|
{
|
|
1774
|
-
dev:
|
|
1775
|
-
ino:
|
|
1776
|
-
nlink:
|
|
1812
|
+
dev: successor.dev,
|
|
1813
|
+
ino: successor.ino,
|
|
1814
|
+
nlink: successor.nlink,
|
|
1777
1815
|
parentDev: parentIdentity.dev,
|
|
1778
1816
|
parentIno: parentIdentity.ino,
|
|
1779
|
-
size:
|
|
1780
|
-
mtimeNs:
|
|
1781
|
-
sha256:
|
|
1817
|
+
size: BigInt(successor.size),
|
|
1818
|
+
mtimeNs: successor.mtimeNs,
|
|
1819
|
+
sha256: successor.sha256,
|
|
1782
1820
|
},
|
|
1783
1821
|
{
|
|
1784
1822
|
dev: expectedDestination.dev,
|
|
@@ -1791,88 +1829,12 @@ export function replaceManagedFileSync(
|
|
|
1791
1829
|
sha256: expectedDestination.sha256,
|
|
1792
1830
|
},
|
|
1793
1831
|
);
|
|
1794
|
-
if (!replaced.ok)
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
const retired = exactUnlink(replaced.retainedPlaceholderPath!, {
|
|
1800
|
-
dev: expectedDestination.dev,
|
|
1801
|
-
ino: expectedDestination.ino,
|
|
1802
|
-
nlink: expectedDestination.nlink,
|
|
1803
|
-
parentDev: parentIdentity.dev,
|
|
1804
|
-
parentIno: parentIdentity.ino,
|
|
1805
|
-
size: BigInt(expectedDestination.size),
|
|
1806
|
-
mtimeNs: expectedDestination.mtimeNs,
|
|
1807
|
-
sha256: expectedDestination.sha256,
|
|
1808
|
-
quarantineName: `.gjc-replace-cleanup-${expectedDestination.dev.toString(16)}-${expectedDestination.ino.toString(16)}`,
|
|
1809
|
-
});
|
|
1810
|
-
if (!retired.ok) {
|
|
1811
|
-
const predecessor =
|
|
1812
|
-
retired.detachedPath ?? retired.retainedPlaceholderPath ?? replaced.retainedPlaceholderPath;
|
|
1813
|
-
const receiptPath = path.join(
|
|
1814
|
-
parent,
|
|
1815
|
-
`.gjc-replace-cleanup-${expectedDestination.dev.toString(16)}-${expectedDestination.ino.toString(16)}.json`,
|
|
1816
|
-
);
|
|
1817
|
-
publishManagedFileNoReplaceSync(
|
|
1818
|
-
receiptPath,
|
|
1819
|
-
Buffer.from(
|
|
1820
|
-
JSON.stringify({
|
|
1821
|
-
version: 1,
|
|
1822
|
-
predecessor,
|
|
1823
|
-
successor: destination,
|
|
1824
|
-
identity: {
|
|
1825
|
-
dev: expectedDestination.dev.toString(),
|
|
1826
|
-
ino: expectedDestination.ino.toString(),
|
|
1827
|
-
nlink: expectedDestination.nlink.toString(),
|
|
1828
|
-
size: String(expectedDestination.size),
|
|
1829
|
-
mtimeNs: expectedDestination.mtimeNs.toString(),
|
|
1830
|
-
ctimeNs: expectedDestination.ctimeNs.toString(),
|
|
1831
|
-
sha256: expectedDestination.sha256,
|
|
1832
|
-
},
|
|
1833
|
-
}),
|
|
1834
|
-
),
|
|
1835
|
-
root,
|
|
1836
|
-
policy,
|
|
1837
|
-
);
|
|
1838
|
-
throw new Error(
|
|
1839
|
-
`managed_replace_cleanup_pending:receipt=${receiptPath}:predecessor=${predecessor}:successor=${destination}:code=${retired.code ?? "unknown"}`,
|
|
1840
|
-
);
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
if (!committedWithPredecessor) {
|
|
1844
|
-
if (replaced.detachedPath) {
|
|
1845
|
-
const restored = exactRestore(replaced.detachedPath, staging, {
|
|
1846
|
-
dev: staged.dev,
|
|
1847
|
-
ino: staged.ino,
|
|
1848
|
-
nlink: staged.nlink,
|
|
1849
|
-
parentDev: parentIdentity.dev,
|
|
1850
|
-
parentIno: parentIdentity.ino,
|
|
1851
|
-
size: staged.size,
|
|
1852
|
-
mtimeNs: staged.mtimeNs,
|
|
1853
|
-
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
1854
|
-
});
|
|
1855
|
-
if (restored.ok) preserveStaging = true;
|
|
1856
|
-
if (!restored.ok)
|
|
1857
|
-
throw new Error(
|
|
1858
|
-
`managed_replace_failed:${replaced.code ?? "unknown"}:retained=${replaced.detachedPath}:restore=${restored.code ?? "unknown"}`,
|
|
1859
|
-
);
|
|
1860
|
-
}
|
|
1861
|
-
const retained = [
|
|
1862
|
-
replaced.detachedPath,
|
|
1863
|
-
replaced.retainedSuccessorPath,
|
|
1864
|
-
replaced.retainedPlaceholderPath,
|
|
1865
|
-
replaced.retainedUnknownPath,
|
|
1866
|
-
]
|
|
1867
|
-
.filter((pathname): pathname is string => typeof pathname === "string")
|
|
1868
|
-
.join(",");
|
|
1869
|
-
throw new Error(
|
|
1870
|
-
`managed_replace_failed:${replaced.code ?? "unknown"}${retained ? `:retained=${retained}` : ""}`,
|
|
1871
|
-
);
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1832
|
+
if (!replaced.ok) throw new ManagedReplaceError(replaced, receiptCleanup.path);
|
|
1833
|
+
const named = captureManagedFileNoFollow(destination);
|
|
1834
|
+
if (!sameReplacementIdentity(named.identity, successor) || named.identity.sha256 !== successor.sha256)
|
|
1835
|
+
throw new Error("destination_identity_changed");
|
|
1836
|
+
fsyncDirectory(parent);
|
|
1874
1837
|
} else fs.renameSync(staging, destination);
|
|
1875
|
-
assertFence?.();
|
|
1876
1838
|
assertManagedDirectoryRoot(root);
|
|
1877
1839
|
const named = fs.lstatSync(destination, { bigint: true });
|
|
1878
1840
|
if (!named.isFile() || named.isSymbolicLink() || named.dev !== staged.dev || named.ino !== staged.ino) {
|
|
@@ -1884,17 +1846,53 @@ export function replaceManagedFileSync(
|
|
|
1884
1846
|
fd = undefined;
|
|
1885
1847
|
}
|
|
1886
1848
|
fsyncDirectory(parent);
|
|
1849
|
+
if (receiptCleanup) {
|
|
1850
|
+
try {
|
|
1851
|
+
const removed = exactUnlink(receiptCleanup.path, {
|
|
1852
|
+
dev: receiptCleanup.identity.dev,
|
|
1853
|
+
ino: receiptCleanup.identity.ino,
|
|
1854
|
+
nlink: receiptCleanup.identity.nlink,
|
|
1855
|
+
parentDev: receiptCleanup.parentDev,
|
|
1856
|
+
parentIno: receiptCleanup.parentIno,
|
|
1857
|
+
size: BigInt(receiptCleanup.identity.size),
|
|
1858
|
+
mtimeNs: receiptCleanup.identity.mtimeNs,
|
|
1859
|
+
sha256: receiptCleanup.identity.sha256,
|
|
1860
|
+
quarantineName: replacementReceiptRetirementName(receiptCleanup.identity, receiptCleanup.predecessor),
|
|
1861
|
+
});
|
|
1862
|
+
if (!exactUnlinkCompleted(removed) && removed.code !== "not_found")
|
|
1863
|
+
throw new ManagedReplaceError(removed, receiptCleanup.path);
|
|
1864
|
+
try {
|
|
1865
|
+
fsyncDirectory(parent);
|
|
1866
|
+
} catch (error) {
|
|
1867
|
+
throw new ManagedReplaceError(
|
|
1868
|
+
{ ...removed, ok: false, code: "durability_failed" },
|
|
1869
|
+
receiptCleanup.path,
|
|
1870
|
+
error,
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
} catch (error) {
|
|
1878
|
+
failure = error;
|
|
1887
1879
|
} finally {
|
|
1888
1880
|
if (fd !== undefined) fs.closeSync(fd);
|
|
1889
1881
|
if (stagedIdentity && !preserveStaging) {
|
|
1890
1882
|
try {
|
|
1891
1883
|
const named = fs.lstatSync(staging, { bigint: true });
|
|
1892
1884
|
if (named.dev === stagedIdentity.dev && named.ino === stagedIdentity.ino) fs.unlinkSync(staging);
|
|
1893
|
-
} catch {
|
|
1894
|
-
|
|
1885
|
+
} catch (error) {
|
|
1886
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
1887
|
+
failure =
|
|
1888
|
+
failure === undefined
|
|
1889
|
+
? error
|
|
1890
|
+
: new AggregateError([failure, error], "Managed replacement and staging cleanup both failed.");
|
|
1891
|
+
}
|
|
1895
1892
|
}
|
|
1896
1893
|
}
|
|
1897
1894
|
}
|
|
1895
|
+
if (failure !== undefined) throw failure;
|
|
1898
1896
|
}
|
|
1899
1897
|
|
|
1900
1898
|
export async function replaceManagedFile(
|