@gajae-code/coding-agent 0.12.10 → 0.12.12
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 +47 -2
- package/dist/types/cli/notify-cli.d.ts +42 -1
- package/dist/types/cli/update-cli.d.ts +9 -3
- package/dist/types/commands/notify.d.ts +6 -0
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +3 -1
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/ouroboros-ooo-bridge.d.ts +13 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -1
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/main.d.ts +6 -2
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/utils/host-status.d.ts +23 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +12 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -0
- package/dist/types/sdk/bus/chat-daemon-command-channel.d.ts +244 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +13 -1
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +7 -0
- package/dist/types/sdk/bus/control-plane-frames.d.ts +43 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +18 -0
- package/dist/types/sdk/bus/existing-thread-readiness.d.ts +71 -0
- package/dist/types/sdk/bus/notification-service.d.ts +10 -0
- package/dist/types/sdk/bus/slack-daemon.d.ts +27 -0
- package/dist/types/sdk/bus/slack-live-provider.d.ts +11 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +14 -0
- package/dist/types/sdk/bus/slack-thread-binding.d.ts +140 -0
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +25 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +104 -16
- package/dist/types/sdk/bus/telegram-reference.d.ts +10 -1
- package/dist/types/sdk/bus/topic-registry.d.ts +130 -85
- package/dist/types/sdk/host/host.d.ts +52 -0
- package/dist/types/sdk/lifecycle-session.d.ts +6 -0
- package/dist/types/sdk/session-activation.d.ts +41 -0
- package/dist/types/sdk/session.d.ts +18 -0
- package/dist/types/sdk/startup-capability.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +7 -0
- package/dist/types/session/artifacts.d.ts +2 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +12 -2
- package/dist/types/session/session-manager.d.ts +40 -5
- package/dist/types/session/session-storage.d.ts +1 -0
- package/dist/types/tools/computer.d.ts +35 -2
- package/dist/types/tools/index.d.ts +8 -0
- package/dist/types/tools/todo-write.d.ts +2 -0
- package/dist/types/utils/npm-registry.d.ts +71 -0
- package/examples/extensions/README.md +38 -4
- package/examples/extensions/ooo-bridge.ts +15 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/src/async/job-manager.ts +9 -2
- package/src/cli/notify-cli.ts +226 -2
- package/src/cli/update-cli.ts +90 -23
- package/src/commands/notify.ts +27 -3
- package/src/commands/sdk.ts +1 -0
- package/src/config/file-lock-gc.ts +18 -0
- package/src/config/file-lock.ts +61 -16
- package/src/config/model-profiles.ts +31 -15
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +2 -0
- package/src/coordinator-mcp/server.ts +358 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +235 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +4 -9
- package/src/extensibility/extensions/runner.ts +15 -2
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/gjc-runtime/launch-tmux.ts +53 -1
- package/src/internal-urls/docs-index.generated.ts +13 -12
- package/src/main.ts +203 -67
- package/src/modes/DESIGN.md +8 -5
- package/src/modes/components/footer.ts +10 -18
- package/src/modes/components/session-selector.ts +26 -3
- package/src/modes/components/status-line/gh.ts +23 -2
- package/src/modes/controllers/event-controller.ts +3 -0
- package/src/modes/controllers/extension-ui-controller.ts +2 -0
- package/src/modes/controllers/input-controller.ts +11 -0
- package/src/modes/controllers/selector-controller.ts +8 -2
- package/src/modes/interactive-mode.ts +7 -1
- package/src/modes/utils/host-status.ts +38 -0
- package/src/prompts/tools/computer.md +6 -1
- package/src/runtime-mcp/types.ts +2 -0
- package/src/sdk/broker/lifecycle.ts +60 -3
- package/src/sdk/bus/chat-daemon-cli.ts +42 -0
- package/src/sdk/bus/chat-daemon-command-channel.ts +921 -0
- package/src/sdk/bus/chat-daemon-control.ts +18 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +257 -21
- package/src/sdk/bus/control-plane-frames.ts +51 -0
- package/src/sdk/bus/conversation-store.ts +29 -1
- package/src/sdk/bus/existing-thread-readiness.ts +155 -0
- package/src/sdk/bus/index.ts +70 -0
- package/src/sdk/bus/notification-service.ts +28 -1
- package/src/sdk/bus/rich-render.ts +3 -2
- package/src/sdk/bus/slack-daemon.ts +99 -4
- package/src/sdk/bus/slack-live-provider.ts +32 -2
- package/src/sdk/bus/slack-provider.ts +11 -0
- package/src/sdk/bus/slack-thread-binding.ts +641 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +11 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +25 -7
- package/src/sdk/bus/telegram-daemon.ts +1961 -497
- package/src/sdk/bus/telegram-reference.ts +42 -6
- package/src/sdk/bus/topic-registry.ts +674 -181
- package/src/sdk/host/host.ts +180 -1
- package/src/sdk/lifecycle-session.ts +8 -2
- package/src/sdk/protocol/operation-inventory.generated.json +22 -0
- package/src/sdk/session-activation.ts +127 -0
- package/src/sdk/session.ts +139 -28
- package/src/sdk/startup-capability.ts +10 -1
- package/src/session/agent-session.ts +331 -30
- package/src/session/artifacts.ts +56 -13
- package/src/session/internal/managed-session-storage.ts +440 -243
- package/src/session/session-manager.ts +288 -44
- package/src/session/session-storage.ts +3 -0
- package/src/setup/provider-presets.json +1 -1
- package/src/task/index.ts +92 -44
- package/src/tools/computer.ts +322 -45
- package/src/tools/index.ts +8 -0
- package/src/tools/todo-write.ts +29 -11
- package/src/utils/npm-registry.ts +660 -0
|
@@ -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,29 +199,155 @@ 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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
213
|
+
version: 3;
|
|
214
|
+
staging: string;
|
|
215
|
+
destination: string;
|
|
216
|
+
successor: SerializedReplacementIdentity;
|
|
217
|
+
predecessor: SerializedReplacementIdentity;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
type LegacyReplacementCleanupIdentity = {
|
|
221
|
+
dev: bigint;
|
|
222
|
+
ino: bigint;
|
|
223
|
+
nlink: bigint;
|
|
224
|
+
size: bigint;
|
|
225
|
+
mtimeNs: bigint;
|
|
226
|
+
ctimeNs: bigint;
|
|
227
|
+
sha256: string;
|
|
182
228
|
};
|
|
183
229
|
|
|
184
230
|
const U64_MAX = 18_446_744_073_709_551_615n;
|
|
185
231
|
|
|
186
232
|
function parseCanonicalU64(value: unknown): bigint | undefined {
|
|
187
|
-
if (typeof value !== "string" || !/^(0|[1-9][0-9]*)$/.test(value)) return undefined;
|
|
233
|
+
if (typeof value !== "string" || !/^(0|[1-9][0-9]*)$/.test(value) || value.length > 20) return undefined;
|
|
188
234
|
const parsed = BigInt(value);
|
|
189
235
|
return parsed <= U64_MAX ? parsed : undefined;
|
|
190
236
|
}
|
|
191
237
|
|
|
238
|
+
function parseLegacyHexU64(value: string): bigint | undefined {
|
|
239
|
+
if (!/^[0-9a-f]+$/.test(value) || value.length > 16) return undefined;
|
|
240
|
+
const parsed = BigInt(`0x${value}`);
|
|
241
|
+
return parsed <= U64_MAX ? parsed : undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
245
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
246
|
+
? (value as Record<string, unknown>)
|
|
247
|
+
: undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function legacyReplacementCleanupReceiptBinding(name: string): { dev: bigint; ino: bigint } | undefined {
|
|
251
|
+
const match = /^\.gjc-replace-cleanup-([0-9a-f]+)-([0-9a-f]+)\.json$/.exec(name);
|
|
252
|
+
if (!match?.[1] || !match[2]) return undefined;
|
|
253
|
+
const dev = parseLegacyHexU64(match[1]);
|
|
254
|
+
const ino = parseLegacyHexU64(match[2]);
|
|
255
|
+
return dev !== undefined && ino !== undefined ? { dev, ino } : undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function parseLegacyReplacementCleanupIdentity(value: unknown): LegacyReplacementCleanupIdentity | undefined {
|
|
259
|
+
const identity = asRecord(value);
|
|
260
|
+
if (!identity) return undefined;
|
|
261
|
+
const dev = parseCanonicalU64(identity.dev);
|
|
262
|
+
const ino = parseCanonicalU64(identity.ino);
|
|
263
|
+
const nlink = parseCanonicalU64(identity.nlink);
|
|
264
|
+
const size = parseCanonicalU64(identity.size);
|
|
265
|
+
const mtimeNs = parseCanonicalU64(identity.mtimeNs);
|
|
266
|
+
const ctimeNs = parseCanonicalU64(identity.ctimeNs);
|
|
267
|
+
const sha256 = identity.sha256;
|
|
268
|
+
if (
|
|
269
|
+
dev === undefined ||
|
|
270
|
+
ino === undefined ||
|
|
271
|
+
nlink === undefined ||
|
|
272
|
+
size === undefined ||
|
|
273
|
+
mtimeNs === undefined ||
|
|
274
|
+
ctimeNs === undefined ||
|
|
275
|
+
typeof sha256 !== "string" ||
|
|
276
|
+
!/^[0-9a-f]{64}$/.test(sha256)
|
|
277
|
+
)
|
|
278
|
+
return undefined;
|
|
279
|
+
return { dev, ino, nlink, size, mtimeNs, ctimeNs, sha256 };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function parseLegacyReplacementCleanupReceipt(
|
|
283
|
+
bytes: Uint8Array,
|
|
284
|
+
): { predecessor: string; successor: string; identity: LegacyReplacementCleanupIdentity } | undefined {
|
|
285
|
+
let value: unknown;
|
|
286
|
+
try {
|
|
287
|
+
value = JSON.parse(Buffer.from(bytes).toString("utf8")) as unknown;
|
|
288
|
+
} catch {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
const receipt = asRecord(value);
|
|
292
|
+
if (receipt?.version !== 1 || typeof receipt.predecessor !== "string" || typeof receipt.successor !== "string")
|
|
293
|
+
return undefined;
|
|
294
|
+
const identity = parseLegacyReplacementCleanupIdentity(receipt.identity);
|
|
295
|
+
return identity ? { predecessor: receipt.predecessor, successor: receipt.successor, identity } : undefined;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function replacementCleanupReceiptBinding(
|
|
299
|
+
name: string,
|
|
300
|
+
): { predecessor: { dev: bigint; ino: bigint }; receipt: { dev: bigint; ino: bigint } } | undefined {
|
|
301
|
+
const match =
|
|
302
|
+
/^\.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(
|
|
303
|
+
name,
|
|
304
|
+
);
|
|
305
|
+
if (!match?.[1] || !match[2] || !match[3] || !match[4]) return undefined;
|
|
306
|
+
const predecessor = { dev: BigInt(`0x${match[1]}`), ino: BigInt(`0x${match[2]}`) };
|
|
307
|
+
const receipt = { dev: BigInt(`0x${match[3]}`), ino: BigInt(`0x${match[4]}`) };
|
|
308
|
+
return predecessor.dev <= U64_MAX && predecessor.ino <= U64_MAX && receipt.dev <= U64_MAX && receipt.ino <= U64_MAX
|
|
309
|
+
? { predecessor, receipt }
|
|
310
|
+
: undefined;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function serializeReplacementIdentity(identity: ManagedFileSnapshot["identity"]): SerializedReplacementIdentity {
|
|
314
|
+
return {
|
|
315
|
+
dev: identity.dev.toString(),
|
|
316
|
+
ino: identity.ino.toString(),
|
|
317
|
+
nlink: identity.nlink.toString(),
|
|
318
|
+
size: String(identity.size),
|
|
319
|
+
mtimeNs: identity.mtimeNs.toString(),
|
|
320
|
+
ctimeNs: identity.ctimeNs.toString(),
|
|
321
|
+
sha256: identity.sha256,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function replacementReceiptPath(
|
|
326
|
+
baseDir: string,
|
|
327
|
+
predecessor: ManagedFileSnapshot["identity"],
|
|
328
|
+
receipt: ManagedFileSnapshot["identity"],
|
|
329
|
+
): string {
|
|
330
|
+
return path.join(
|
|
331
|
+
baseDir,
|
|
332
|
+
`.gjc-replace-cleanup-${predecessor.dev.toString(16)}-${predecessor.ino.toString(16)}-receipt-${receipt.dev.toString(16)}-${receipt.ino.toString(16)}.json`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function replacementReceiptRetirementName(
|
|
337
|
+
receipt: { dev: bigint; ino: bigint },
|
|
338
|
+
predecessor: { dev: bigint; ino: bigint },
|
|
339
|
+
): string {
|
|
340
|
+
return `.gjc-receipt-remove-${receipt.dev.toString(16)}-${receipt.ino.toString(16)}-${predecessor.dev.toString(16)}-${predecessor.ino.toString(16)}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function replacementReceiptPlaceholderRetirementName(
|
|
344
|
+
placeholder: { dev: bigint; ino: bigint },
|
|
345
|
+
predecessor: { dev: bigint; ino: bigint },
|
|
346
|
+
receipt: { dev: bigint; ino: bigint },
|
|
347
|
+
): string {
|
|
348
|
+
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)}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
192
351
|
const ACL_FAILURE_CODES = new Set(["acl_denied", "acl_io_error", "acl_present", "acl_malformed", "acl_unknown"]);
|
|
193
352
|
const ACL_CLEAR_EVIDENCE = new Set(["cleared", "already_absent", "unsupported", "not_run"]);
|
|
194
353
|
const GENERAL_FAILURE_CODES = new Set([
|
|
@@ -710,72 +869,159 @@ export class ManagedSessionDescendantStore {
|
|
|
710
869
|
}
|
|
711
870
|
}
|
|
712
871
|
|
|
872
|
+
#recoverReplacementCleanupPlaceholder(
|
|
873
|
+
receiptPath: string,
|
|
874
|
+
binding: { predecessor: { dev: bigint; ino: bigint }; receipt: { dev: bigint; ino: bigint } },
|
|
875
|
+
placeholder: ManagedFileSnapshot,
|
|
876
|
+
): boolean {
|
|
877
|
+
if (placeholder.bytes.byteLength !== 0) return false;
|
|
878
|
+
const detachedReceiptPath = path.join(
|
|
879
|
+
this.#baseDir,
|
|
880
|
+
replacementReceiptRetirementName(binding.receipt, binding.predecessor),
|
|
881
|
+
);
|
|
882
|
+
let detachedReceipt: ManagedFileSnapshot;
|
|
883
|
+
try {
|
|
884
|
+
detachedReceipt = captureManagedFileNoFollow(detachedReceiptPath);
|
|
885
|
+
} catch (error) {
|
|
886
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
887
|
+
throw error;
|
|
888
|
+
}
|
|
889
|
+
if (detachedReceipt.identity.dev !== binding.receipt.dev || detachedReceipt.identity.ino !== binding.receipt.ino)
|
|
890
|
+
return false;
|
|
891
|
+
|
|
892
|
+
const quarantineName = replacementReceiptPlaceholderRetirementName(
|
|
893
|
+
placeholder.identity,
|
|
894
|
+
binding.predecessor,
|
|
895
|
+
binding.receipt,
|
|
896
|
+
);
|
|
897
|
+
const removed = exactUnlink(receiptPath, {
|
|
898
|
+
dev: placeholder.identity.dev,
|
|
899
|
+
ino: placeholder.identity.ino,
|
|
900
|
+
nlink: placeholder.identity.nlink,
|
|
901
|
+
parentDev: this.#subtreeRoot.dev,
|
|
902
|
+
parentIno: this.#subtreeRoot.ino,
|
|
903
|
+
size: BigInt(placeholder.identity.size),
|
|
904
|
+
mtimeNs: placeholder.identity.mtimeNs,
|
|
905
|
+
sha256: placeholder.identity.sha256,
|
|
906
|
+
quarantineName,
|
|
907
|
+
});
|
|
908
|
+
if (
|
|
909
|
+
(!exactUnlinkCompleted(removed) && removed.code !== "not_found") ||
|
|
910
|
+
removed.retainedSuccessorPath !== undefined ||
|
|
911
|
+
removed.retainedUnknownPath !== undefined
|
|
912
|
+
)
|
|
913
|
+
throw new Error(`managed_replace_receipt_cleanup_pending:${removed.code ?? "unknown"}`);
|
|
914
|
+
fsyncDirectory(this.#baseDir);
|
|
915
|
+
return true;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
#detachReplacementCleanupReceipt(receiptPath: string): void {
|
|
919
|
+
const binding = replacementCleanupReceiptBinding(path.basename(receiptPath));
|
|
920
|
+
if (!binding) throw new Error("managed_replace_cleanup_receipt_invalid");
|
|
921
|
+
let receipt: ManagedFileSnapshot;
|
|
922
|
+
try {
|
|
923
|
+
receipt = captureManagedFileNoFollow(receiptPath);
|
|
924
|
+
} catch (error) {
|
|
925
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
926
|
+
throw error;
|
|
927
|
+
}
|
|
928
|
+
if (receipt.identity.dev !== binding.receipt.dev || receipt.identity.ino !== binding.receipt.ino) {
|
|
929
|
+
if (this.#recoverReplacementCleanupPlaceholder(receiptPath, binding, receipt)) return;
|
|
930
|
+
throw new Error("managed_replace_cleanup_receipt_invalid");
|
|
931
|
+
}
|
|
932
|
+
const predecessor = binding.predecessor;
|
|
933
|
+
const quarantineName = replacementReceiptRetirementName(receipt.identity, predecessor);
|
|
934
|
+
const detached = exactUnlink(receiptPath, {
|
|
935
|
+
dev: receipt.identity.dev,
|
|
936
|
+
ino: receipt.identity.ino,
|
|
937
|
+
nlink: receipt.identity.nlink,
|
|
938
|
+
parentDev: this.#subtreeRoot.dev,
|
|
939
|
+
parentIno: this.#subtreeRoot.ino,
|
|
940
|
+
size: BigInt(receipt.identity.size),
|
|
941
|
+
mtimeNs: receipt.identity.mtimeNs,
|
|
942
|
+
sha256: receipt.identity.sha256,
|
|
943
|
+
detachOnly: true,
|
|
944
|
+
quarantineName,
|
|
945
|
+
});
|
|
946
|
+
if (
|
|
947
|
+
(!detached.ok && detached.code !== "cleanup_pending") ||
|
|
948
|
+
detached.detachedPath !== path.join(this.#baseDir, quarantineName) ||
|
|
949
|
+
detached.retainedSuccessorPath ||
|
|
950
|
+
detached.retainedUnknownPath
|
|
951
|
+
)
|
|
952
|
+
throw new Error(`managed_replace_receipt_cleanup_pending:${detached.code ?? "unknown"}`);
|
|
953
|
+
fsyncDirectory(this.#baseDir);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
#reconcileLegacyReplacementCleanupReceipt(receiptPath: string, name: string): void {
|
|
957
|
+
const binding = legacyReplacementCleanupReceiptBinding(name);
|
|
958
|
+
if (!binding) throw new Error("managed_replace_cleanup_receipt_invalid");
|
|
959
|
+
const receipt = captureManagedFileNoFollow(receiptPath);
|
|
960
|
+
const parsed = parseLegacyReplacementCleanupReceipt(receipt.bytes);
|
|
961
|
+
const expectedPredecessor = path.join(
|
|
962
|
+
this.#baseDir,
|
|
963
|
+
`.gjc-exact-replace-destination-${binding.dev.toString(16)}-${binding.ino.toString(16)}`,
|
|
964
|
+
);
|
|
965
|
+
if (
|
|
966
|
+
!parsed ||
|
|
967
|
+
parsed.identity.dev !== binding.dev ||
|
|
968
|
+
parsed.identity.ino !== binding.ino ||
|
|
969
|
+
path.resolve(parsed.predecessor) !== expectedPredecessor ||
|
|
970
|
+
path.dirname(path.resolve(parsed.successor)) !== this.#baseDir
|
|
971
|
+
)
|
|
972
|
+
throw new Error("managed_replace_cleanup_receipt_invalid");
|
|
973
|
+
|
|
974
|
+
const retired = exactUnlink(expectedPredecessor, {
|
|
975
|
+
dev: parsed.identity.dev,
|
|
976
|
+
ino: parsed.identity.ino,
|
|
977
|
+
nlink: parsed.identity.nlink,
|
|
978
|
+
parentDev: this.#subtreeRoot.dev,
|
|
979
|
+
parentIno: this.#subtreeRoot.ino,
|
|
980
|
+
size: parsed.identity.size,
|
|
981
|
+
mtimeNs: parsed.identity.mtimeNs,
|
|
982
|
+
sha256: parsed.identity.sha256,
|
|
983
|
+
quarantineName: `.gjc-replace-retry-${parsed.identity.dev.toString(16)}-${parsed.identity.ino.toString(16)}`,
|
|
984
|
+
});
|
|
985
|
+
if (!exactUnlinkCompleted(retired) && retired.code !== "not_found")
|
|
986
|
+
throw new Error(`managed_replace_cleanup_pending:${retired.code ?? "unknown"}`);
|
|
987
|
+
|
|
988
|
+
const currentReceipt = captureManagedFileNoFollow(receiptPath);
|
|
989
|
+
if (
|
|
990
|
+
!sameIdentity(currentReceipt.identity, receipt.identity) ||
|
|
991
|
+
currentReceipt.identity.sha256 !== receipt.identity.sha256
|
|
992
|
+
)
|
|
993
|
+
throw new Error("managed_replace_cleanup_receipt_invalid");
|
|
994
|
+
const removed = exactUnlink(receiptPath, {
|
|
995
|
+
dev: currentReceipt.identity.dev,
|
|
996
|
+
ino: currentReceipt.identity.ino,
|
|
997
|
+
nlink: currentReceipt.identity.nlink,
|
|
998
|
+
parentDev: this.#subtreeRoot.dev,
|
|
999
|
+
parentIno: this.#subtreeRoot.ino,
|
|
1000
|
+
size: BigInt(currentReceipt.identity.size),
|
|
1001
|
+
mtimeNs: currentReceipt.identity.mtimeNs,
|
|
1002
|
+
sha256: currentReceipt.identity.sha256,
|
|
1003
|
+
quarantineName: `.gjc-receipt-remove-${currentReceipt.identity.dev.toString(16)}-${currentReceipt.identity.ino.toString(16)}`,
|
|
1004
|
+
});
|
|
1005
|
+
if (!exactUnlinkCompleted(removed) && removed.code !== "not_found")
|
|
1006
|
+
throw new Error(`managed_replace_receipt_cleanup_pending:${removed.code ?? "unknown"}`);
|
|
1007
|
+
fsyncDirectory(this.#baseDir);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
713
1010
|
#reconcileReplacementCleanupReceipts(): void {
|
|
714
1011
|
if (this.#reconcilingReplacementCleanup) return;
|
|
715
1012
|
this.#reconcilingReplacementCleanup = true;
|
|
716
1013
|
try {
|
|
717
1014
|
for (const name of fs.readdirSync(this.#baseDir)) {
|
|
718
|
-
|
|
719
|
-
if (
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
)
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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"}`);
|
|
1015
|
+
if (!name.startsWith(".gjc-replace-cleanup-")) continue;
|
|
1016
|
+
if (replacementCleanupReceiptBinding(name)) {
|
|
1017
|
+
this.#detachReplacementCleanupReceipt(path.join(this.#baseDir, name));
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
if (legacyReplacementCleanupReceiptBinding(name)) {
|
|
1021
|
+
this.#reconcileLegacyReplacementCleanupReceipt(path.join(this.#baseDir, name), name);
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
throw new Error("managed_replace_cleanup_receipt_invalid");
|
|
779
1025
|
}
|
|
780
1026
|
} finally {
|
|
781
1027
|
this.#reconcilingReplacementCleanup = false;
|
|
@@ -861,21 +1107,7 @@ export class ManagedSessionDescendantStore {
|
|
|
861
1107
|
const current = captureManagedFileNoFollow(resolved);
|
|
862
1108
|
if (!sameIdentity(current.identity, expected.identity) || current.identity.sha256 !== expected.identity.sha256)
|
|
863
1109
|
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
|
-
);
|
|
1110
|
+
replaceManagedFileSync(resolved, bytes, this.#subtreeRoot, this.#policy, undefined, expected.identity);
|
|
879
1111
|
return;
|
|
880
1112
|
}
|
|
881
1113
|
const replaced = (this.#authority as RecoveryFsRoot & RetainedManagedReplacer).replaceManaged(
|
|
@@ -914,7 +1146,7 @@ export class ManagedSessionDescendantStore {
|
|
|
914
1146
|
this.#beforeMutation();
|
|
915
1147
|
this.#assertBound();
|
|
916
1148
|
const resolved = this.#resolve(relativePath);
|
|
917
|
-
|
|
1149
|
+
const existing = this.readExpected(relativePath);
|
|
918
1150
|
if (!existing) throw new Error("managed_append_missing");
|
|
919
1151
|
if (this.#authority) {
|
|
920
1152
|
const appended = this.#authority.appendManaged(
|
|
@@ -931,52 +1163,18 @@ export class ManagedSessionDescendantStore {
|
|
|
931
1163
|
this.#assertBound();
|
|
932
1164
|
return;
|
|
933
1165
|
}
|
|
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
|
-
}
|
|
1166
|
+
// Darwin has no retained native root authority. Do not append in place:
|
|
1167
|
+
// a short write can leave a malformed JSONL tail with no safe recovery
|
|
1168
|
+
// boundary. Replace the exact captured file instead so the old transcript
|
|
1169
|
+
// or the complete successor remains recoverable after every reported error.
|
|
1170
|
+
replaceManagedFileSync(
|
|
1171
|
+
resolved,
|
|
1172
|
+
Buffer.concat([existing.bytes, Buffer.from(bytes)]),
|
|
1173
|
+
this.#subtreeRoot,
|
|
1174
|
+
this.#policy,
|
|
1175
|
+
undefined,
|
|
1176
|
+
existing.identity,
|
|
1177
|
+
);
|
|
980
1178
|
this.#assertBound();
|
|
981
1179
|
}
|
|
982
1180
|
|
|
@@ -1425,25 +1623,19 @@ function sameIdentity(left: ManagedFileSnapshot["identity"], right: ManagedFileS
|
|
|
1425
1623
|
left.ctimeNs === right.ctimeNs
|
|
1426
1624
|
);
|
|
1427
1625
|
}
|
|
1428
|
-
|
|
1429
|
-
/** Content/path identity that remains meaningful across a Darwin write-open ctime-only transition. */
|
|
1430
|
-
function sameStableIdentityIgnoringCtime(
|
|
1626
|
+
function sameReplacementIdentity(
|
|
1431
1627
|
left: ManagedFileSnapshot["identity"],
|
|
1432
1628
|
right: ManagedFileSnapshot["identity"],
|
|
1433
1629
|
): boolean {
|
|
1434
1630
|
return (
|
|
1435
|
-
left.dev === right.dev &&
|
|
1631
|
+
left.dev === right.dev &&
|
|
1632
|
+
left.ino === right.ino &&
|
|
1633
|
+
left.nlink === right.nlink &&
|
|
1634
|
+
left.size === right.size &&
|
|
1635
|
+
left.mtimeNs === right.mtimeNs
|
|
1436
1636
|
);
|
|
1437
1637
|
}
|
|
1438
1638
|
|
|
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
1639
|
function parseLockBytes(bytes: Uint8Array): LockRecord | undefined {
|
|
1448
1640
|
try {
|
|
1449
1641
|
const value: unknown = JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
@@ -1668,12 +1860,13 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1668
1860
|
bytes: Uint8Array,
|
|
1669
1861
|
root?: ManagedDirectoryRoot,
|
|
1670
1862
|
policy: ManagedSessionSecurityPolicy = "default",
|
|
1671
|
-
):
|
|
1863
|
+
): ManagedFileSnapshot["identity"] {
|
|
1672
1864
|
const parent = path.dirname(destination);
|
|
1673
1865
|
ensureManagedDirectory(parent, root, policy);
|
|
1674
1866
|
const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.staging`);
|
|
1675
1867
|
let fd: number | undefined;
|
|
1676
1868
|
let stagingIdentity: { dev: bigint; ino: bigint } | undefined;
|
|
1869
|
+
let publishedIdentity: ManagedFileSnapshot["identity"] | undefined;
|
|
1677
1870
|
let failure: unknown;
|
|
1678
1871
|
let outcome: NativePublishOutcome | undefined;
|
|
1679
1872
|
let linkPublished = false;
|
|
@@ -1691,6 +1884,7 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1691
1884
|
secureFileDescriptor(staging, fd, "verify");
|
|
1692
1885
|
const staged = fs.fstatSync(fd, { bigint: true });
|
|
1693
1886
|
stagingIdentity = { dev: staged.dev, ino: staged.ino };
|
|
1887
|
+
publishedIdentity = identity(staged, createHash("sha256").update(bytes).digest("hex"));
|
|
1694
1888
|
|
|
1695
1889
|
outcome = classifyNativePublishOutcome(renameNoReplacePath(staging, destination));
|
|
1696
1890
|
if (renameFlagsUnsupported(outcome)) {
|
|
@@ -1726,6 +1920,8 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1726
1920
|
}
|
|
1727
1921
|
}
|
|
1728
1922
|
if (failure !== undefined) throw failure;
|
|
1923
|
+
if (!publishedIdentity) throw new Error("managed_publish_identity_unavailable");
|
|
1924
|
+
return publishedIdentity;
|
|
1729
1925
|
}
|
|
1730
1926
|
|
|
1731
1927
|
/** Replace one managed regular file while retaining the secured staging fd through publication. */
|
|
@@ -1743,6 +1939,16 @@ export function replaceManagedFileSync(
|
|
|
1743
1939
|
let fd: number | undefined;
|
|
1744
1940
|
let stagedIdentity: { dev: bigint; ino: bigint } | undefined;
|
|
1745
1941
|
let preserveStaging = false;
|
|
1942
|
+
let receiptCleanup:
|
|
1943
|
+
| {
|
|
1944
|
+
path: string;
|
|
1945
|
+
parentDev: bigint;
|
|
1946
|
+
parentIno: bigint;
|
|
1947
|
+
identity: ManagedFileSnapshot["identity"];
|
|
1948
|
+
predecessor: ManagedFileSnapshot["identity"];
|
|
1949
|
+
}
|
|
1950
|
+
| undefined;
|
|
1951
|
+
let failure: unknown;
|
|
1746
1952
|
try {
|
|
1747
1953
|
fd = fs.openSync(
|
|
1748
1954
|
staging,
|
|
@@ -1762,23 +1968,54 @@ export function replaceManagedFileSync(
|
|
|
1762
1968
|
}
|
|
1763
1969
|
assertManagedDirectoryRoot(root);
|
|
1764
1970
|
assertFence?.();
|
|
1765
|
-
if (
|
|
1766
|
-
throw new Error("managed_replace_exact_unavailable");
|
|
1767
|
-
}
|
|
1768
|
-
if (process.platform === "win32" && expectedDestination) {
|
|
1971
|
+
if (expectedDestination) {
|
|
1769
1972
|
const parentIdentity = fs.lstatSync(parent, { bigint: true });
|
|
1973
|
+
const successor = identity(staged, createHash("sha256").update(bytes).digest("hex"));
|
|
1974
|
+
const receiptStagingPath = path.join(parent, `.gjc-replace-receipt-pending-${randomUUID()}.json`);
|
|
1975
|
+
preserveStaging = true;
|
|
1976
|
+
const publishedReceiptIdentity = publishManagedFileNoReplaceSync(
|
|
1977
|
+
receiptStagingPath,
|
|
1978
|
+
Buffer.from(
|
|
1979
|
+
JSON.stringify({
|
|
1980
|
+
version: 3,
|
|
1981
|
+
staging,
|
|
1982
|
+
destination,
|
|
1983
|
+
successor: serializeReplacementIdentity(successor),
|
|
1984
|
+
predecessor: serializeReplacementIdentity(expectedDestination),
|
|
1985
|
+
} satisfies ReplacementCleanupReceipt),
|
|
1986
|
+
),
|
|
1987
|
+
root,
|
|
1988
|
+
policy,
|
|
1989
|
+
);
|
|
1990
|
+
const receiptPath = replacementReceiptPath(parent, expectedDestination, publishedReceiptIdentity);
|
|
1991
|
+
const receiptPublish = classifyNativePublishOutcome(renameNoReplacePath(receiptStagingPath, receiptPath));
|
|
1992
|
+
if (!receiptPublish.ok) throw publishFailure(receiptPublish);
|
|
1993
|
+
const namedReceipt = captureManagedFileNoFollow(receiptPath);
|
|
1994
|
+
if (
|
|
1995
|
+
namedReceipt.identity.dev !== publishedReceiptIdentity.dev ||
|
|
1996
|
+
namedReceipt.identity.ino !== publishedReceiptIdentity.ino
|
|
1997
|
+
)
|
|
1998
|
+
throw new Error("managed_replace_cleanup_receipt_identity_changed");
|
|
1999
|
+
fsyncDirectory(parent);
|
|
2000
|
+
receiptCleanup = {
|
|
2001
|
+
path: receiptPath,
|
|
2002
|
+
parentDev: parentIdentity.dev,
|
|
2003
|
+
parentIno: parentIdentity.ino,
|
|
2004
|
+
identity: publishedReceiptIdentity,
|
|
2005
|
+
predecessor: expectedDestination,
|
|
2006
|
+
};
|
|
1770
2007
|
const replaced = exactReplacePath(
|
|
1771
2008
|
staging,
|
|
1772
2009
|
destination,
|
|
1773
2010
|
{
|
|
1774
|
-
dev:
|
|
1775
|
-
ino:
|
|
1776
|
-
nlink:
|
|
2011
|
+
dev: successor.dev,
|
|
2012
|
+
ino: successor.ino,
|
|
2013
|
+
nlink: successor.nlink,
|
|
1777
2014
|
parentDev: parentIdentity.dev,
|
|
1778
2015
|
parentIno: parentIdentity.ino,
|
|
1779
|
-
size:
|
|
1780
|
-
mtimeNs:
|
|
1781
|
-
sha256:
|
|
2016
|
+
size: BigInt(successor.size),
|
|
2017
|
+
mtimeNs: successor.mtimeNs,
|
|
2018
|
+
sha256: successor.sha256,
|
|
1782
2019
|
},
|
|
1783
2020
|
{
|
|
1784
2021
|
dev: expectedDestination.dev,
|
|
@@ -1791,88 +2028,12 @@ export function replaceManagedFileSync(
|
|
|
1791
2028
|
sha256: expectedDestination.sha256,
|
|
1792
2029
|
},
|
|
1793
2030
|
);
|
|
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
|
-
}
|
|
2031
|
+
if (!replaced.ok) throw new ManagedReplaceError(replaced, receiptCleanup.path);
|
|
2032
|
+
const named = captureManagedFileNoFollow(destination);
|
|
2033
|
+
if (!sameReplacementIdentity(named.identity, successor) || named.identity.sha256 !== successor.sha256)
|
|
2034
|
+
throw new Error("destination_identity_changed");
|
|
2035
|
+
fsyncDirectory(parent);
|
|
1874
2036
|
} else fs.renameSync(staging, destination);
|
|
1875
|
-
assertFence?.();
|
|
1876
2037
|
assertManagedDirectoryRoot(root);
|
|
1877
2038
|
const named = fs.lstatSync(destination, { bigint: true });
|
|
1878
2039
|
if (!named.isFile() || named.isSymbolicLink() || named.dev !== staged.dev || named.ino !== staged.ino) {
|
|
@@ -1884,17 +2045,53 @@ export function replaceManagedFileSync(
|
|
|
1884
2045
|
fd = undefined;
|
|
1885
2046
|
}
|
|
1886
2047
|
fsyncDirectory(parent);
|
|
2048
|
+
if (receiptCleanup) {
|
|
2049
|
+
try {
|
|
2050
|
+
const removed = exactUnlink(receiptCleanup.path, {
|
|
2051
|
+
dev: receiptCleanup.identity.dev,
|
|
2052
|
+
ino: receiptCleanup.identity.ino,
|
|
2053
|
+
nlink: receiptCleanup.identity.nlink,
|
|
2054
|
+
parentDev: receiptCleanup.parentDev,
|
|
2055
|
+
parentIno: receiptCleanup.parentIno,
|
|
2056
|
+
size: BigInt(receiptCleanup.identity.size),
|
|
2057
|
+
mtimeNs: receiptCleanup.identity.mtimeNs,
|
|
2058
|
+
sha256: receiptCleanup.identity.sha256,
|
|
2059
|
+
quarantineName: replacementReceiptRetirementName(receiptCleanup.identity, receiptCleanup.predecessor),
|
|
2060
|
+
});
|
|
2061
|
+
if (!exactUnlinkCompleted(removed) && removed.code !== "not_found")
|
|
2062
|
+
throw new ManagedReplaceError(removed, receiptCleanup.path);
|
|
2063
|
+
try {
|
|
2064
|
+
fsyncDirectory(parent);
|
|
2065
|
+
} catch (error) {
|
|
2066
|
+
throw new ManagedReplaceError(
|
|
2067
|
+
{ ...removed, ok: false, code: "durability_failed" },
|
|
2068
|
+
receiptCleanup.path,
|
|
2069
|
+
error,
|
|
2070
|
+
);
|
|
2071
|
+
}
|
|
2072
|
+
} catch (error) {
|
|
2073
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
} catch (error) {
|
|
2077
|
+
failure = error;
|
|
1887
2078
|
} finally {
|
|
1888
2079
|
if (fd !== undefined) fs.closeSync(fd);
|
|
1889
2080
|
if (stagedIdentity && !preserveStaging) {
|
|
1890
2081
|
try {
|
|
1891
2082
|
const named = fs.lstatSync(staging, { bigint: true });
|
|
1892
2083
|
if (named.dev === stagedIdentity.dev && named.ino === stagedIdentity.ino) fs.unlinkSync(staging);
|
|
1893
|
-
} catch {
|
|
1894
|
-
|
|
2084
|
+
} catch (error) {
|
|
2085
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
2086
|
+
failure =
|
|
2087
|
+
failure === undefined
|
|
2088
|
+
? error
|
|
2089
|
+
: new AggregateError([failure, error], "Managed replacement and staging cleanup both failed.");
|
|
2090
|
+
}
|
|
1895
2091
|
}
|
|
1896
2092
|
}
|
|
1897
2093
|
}
|
|
2094
|
+
if (failure !== undefined) throw failure;
|
|
1898
2095
|
}
|
|
1899
2096
|
|
|
1900
2097
|
export async function replaceManagedFile(
|