@gajae-code/coding-agent 0.12.4 → 0.12.5
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 +4 -0
- package/dist/types/async/job-manager.d.ts +9 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -2
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +5 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +3 -22
- package/dist/types/session/session-manager.d.ts +1 -1
- package/dist/types/task/executor.d.ts +1 -1
- package/dist/types/task/index.d.ts +2 -1
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/types.d.ts +16 -0
- package/dist/types/task/worktree.d.ts +13 -6
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/utils/git.d.ts +1 -0
- package/package.json +8 -8
- package/src/async/job-manager.ts +13 -5
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/sdk/broker/lifecycle.ts +13 -18
- package/src/sdk/bus/chat-daemon-control.ts +6 -4
- package/src/sdk/bus/telegram-daemon-contract.ts +5 -3
- package/src/sdk/session.ts +1 -0
- package/src/session/internal/managed-session-scope.ts +107 -158
- package/src/session/internal/managed-session-storage.ts +57 -445
- package/src/session/session-manager.ts +12 -7
- package/src/session/session-storage.ts +44 -29
- package/src/task/executor.ts +5 -2
- package/src/task/index.ts +232 -64
- package/src/task/receipt.ts +22 -2
- package/src/task/render.ts +12 -1
- package/src/task/types.ts +18 -0
- package/src/task/worktree.ts +122 -43
- package/src/tools/subagent-render.ts +5 -2
- package/src/tools/subagent.ts +8 -0
- package/src/utils/git.ts +2 -0
|
@@ -6,10 +6,8 @@ import {
|
|
|
6
6
|
applyOwnerOnlyFdSecurity,
|
|
7
7
|
applyOwnerOnlyPathSecurity,
|
|
8
8
|
exactRemoveDirectoryTree,
|
|
9
|
-
exactReplacePath,
|
|
10
9
|
exactUnlink,
|
|
11
10
|
type NativeDirectoryTreeSnapshot,
|
|
12
|
-
type NativeExactUnlinkResult,
|
|
13
11
|
type NativeOwnerOnlySecurityResult,
|
|
14
12
|
openRecoveryFsRoot,
|
|
15
13
|
type RecoveryFsRoot,
|
|
@@ -36,6 +34,7 @@ const LOCK_LEASE_MS = 60_000;
|
|
|
36
34
|
const LOCK_HEARTBEAT_MS = 10_000;
|
|
37
35
|
const LOCK_WAIT_MS = 5_000;
|
|
38
36
|
|
|
37
|
+
const LOCK_STALE_RECHECK_MS = 100;
|
|
39
38
|
export class ManagedPublishError extends Error {
|
|
40
39
|
readonly classification:
|
|
41
40
|
| "destination_conflict"
|
|
@@ -45,14 +44,12 @@ export class ManagedPublishError extends Error {
|
|
|
45
44
|
| "identity_mismatch"
|
|
46
45
|
| "io_error"
|
|
47
46
|
| "durability_failed";
|
|
48
|
-
readonly code: ManagedPublishError["classification"];
|
|
49
47
|
readonly diagnostic: string;
|
|
50
48
|
|
|
51
49
|
constructor(classification: ManagedPublishError["classification"], outcome: NativePublishOutcome) {
|
|
52
50
|
super(classification);
|
|
53
51
|
this.name = "ManagedPublishError";
|
|
54
52
|
this.classification = classification;
|
|
55
|
-
this.code = classification;
|
|
56
53
|
this.diagnostic = formatNativePublishDiagnostic(outcome);
|
|
57
54
|
}
|
|
58
55
|
}
|
|
@@ -137,24 +134,15 @@ export interface ManagedStorageLock {
|
|
|
137
134
|
assertOwned(): void;
|
|
138
135
|
release(): Promise<void>;
|
|
139
136
|
}
|
|
140
|
-
export interface ManagedStorageLockSync {
|
|
141
|
-
path: string;
|
|
142
|
-
attemptId: string;
|
|
143
|
-
assertOwned(): void;
|
|
144
|
-
release(): void;
|
|
145
|
-
}
|
|
146
137
|
|
|
147
138
|
export interface ManagedFileSnapshot {
|
|
148
139
|
bytes: Buffer;
|
|
149
140
|
identity: { dev: bigint; ino: bigint; size: number; mtimeNs: bigint; ctimeNs: bigint; sha256: string };
|
|
150
141
|
}
|
|
151
142
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
"acl_present",
|
|
156
|
-
"acl_malformed",
|
|
157
|
-
"acl_unknown",
|
|
143
|
+
const ACL_FAILURE_CODES = new Set(["acl_denied", "acl_io_error", "acl_present", "acl_malformed", "acl_unknown"]);
|
|
144
|
+
const ACL_CLEAR_EVIDENCE = new Set(["cleared", "already_absent", "unsupported", "not_run"]);
|
|
145
|
+
const GENERAL_FAILURE_CODES = new Set([
|
|
158
146
|
"acl_unavailable",
|
|
159
147
|
"acl_apply_failed",
|
|
160
148
|
"acl_verify_failed",
|
|
@@ -167,52 +155,7 @@ export const MANAGED_SESSION_SECURITY_CODES = [
|
|
|
167
155
|
"owner_mismatch",
|
|
168
156
|
"mode_mismatch",
|
|
169
157
|
"io_error",
|
|
170
|
-
] as const;
|
|
171
|
-
export type ManagedSessionSecurityCode = (typeof MANAGED_SESSION_SECURITY_CODES)[number];
|
|
172
|
-
const ACL_FAILURE_CODES = new Set<ManagedSessionSecurityCode>([
|
|
173
|
-
"acl_denied",
|
|
174
|
-
"acl_io_error",
|
|
175
|
-
"acl_present",
|
|
176
|
-
"acl_malformed",
|
|
177
|
-
"acl_unknown",
|
|
178
158
|
]);
|
|
179
|
-
const ACL_CLEAR_EVIDENCE = new Set(["cleared", "already_absent", "unsupported", "not_run"]);
|
|
180
|
-
const GENERAL_FAILURE_CODES = new Set<ManagedSessionSecurityCode>(MANAGED_SESSION_SECURITY_CODES);
|
|
181
|
-
const LOCK_OWNERSHIP_LOSS_CODES = new Set<ManagedSessionSecurityCode>([
|
|
182
|
-
"not_found",
|
|
183
|
-
"not_directory",
|
|
184
|
-
"reparse_point",
|
|
185
|
-
"identity_mismatch",
|
|
186
|
-
]);
|
|
187
|
-
|
|
188
|
-
function lockDescriptorOwnershipLost(error: unknown): boolean {
|
|
189
|
-
const code = managedSecurityFailureClassification(error);
|
|
190
|
-
return code !== undefined && LOCK_OWNERSHIP_LOSS_CODES.has(code as ManagedSessionSecurityCode);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export class ManagedSessionSecurityError extends Error {
|
|
194
|
-
readonly code: ManagedSessionSecurityCode;
|
|
195
|
-
|
|
196
|
-
constructor(code: ManagedSessionSecurityCode) {
|
|
197
|
-
super(code);
|
|
198
|
-
this.name = "ManagedSessionSecurityError";
|
|
199
|
-
this.code = code;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
export function managedSessionSecurityCode(error: unknown): ManagedSessionSecurityCode | undefined {
|
|
204
|
-
if (!(error instanceof Error) || typeof (error as unknown as { code?: unknown }).code !== "string") return undefined;
|
|
205
|
-
const code = (error as unknown as { code: string }).code;
|
|
206
|
-
return GENERAL_FAILURE_CODES.has(code as ManagedSessionSecurityCode)
|
|
207
|
-
? (code as ManagedSessionSecurityCode)
|
|
208
|
-
: undefined;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function managedSessionSecurityErrorForCode(code: string | undefined): ManagedSessionSecurityError {
|
|
212
|
-
return new ManagedSessionSecurityError(
|
|
213
|
-
GENERAL_FAILURE_CODES.has(code as ManagedSessionSecurityCode) ? (code as ManagedSessionSecurityCode) : "io_error",
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
159
|
|
|
217
160
|
function hasOnlyKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
|
|
218
161
|
return Object.keys(value).every(key => allowed.includes(key));
|
|
@@ -230,11 +173,10 @@ export function validateNativeSecurityResult(
|
|
|
230
173
|
const result = value as Record<string, unknown>;
|
|
231
174
|
if (result.ok === false) {
|
|
232
175
|
if (typeof result.code !== "string") throw new Error("Malformed owner-only security failure");
|
|
233
|
-
|
|
234
|
-
if (!ACL_FAILURE_CODES.has(code) && !GENERAL_FAILURE_CODES.has(code)) {
|
|
176
|
+
if (!ACL_FAILURE_CODES.has(result.code) && !GENERAL_FAILURE_CODES.has(result.code)) {
|
|
235
177
|
throw new Error("Unknown owner-only security failure code");
|
|
236
178
|
}
|
|
237
|
-
if (ACL_FAILURE_CODES.has(code)) {
|
|
179
|
+
if (ACL_FAILURE_CODES.has(result.code)) {
|
|
238
180
|
if (
|
|
239
181
|
(result.operation !== "clear" && result.operation !== "query") ||
|
|
240
182
|
(result.attribute !== "access" && result.attribute !== "default")
|
|
@@ -455,7 +397,7 @@ class ManagedSecurityError extends Error {
|
|
|
455
397
|
}
|
|
456
398
|
|
|
457
399
|
export function managedSecurityFailureClassification(error: unknown): string | undefined {
|
|
458
|
-
return error instanceof ManagedSecurityError ? error.getClassification() :
|
|
400
|
+
return error instanceof ManagedSecurityError ? error.getClassification() : undefined;
|
|
459
401
|
}
|
|
460
402
|
|
|
461
403
|
function securityError(pathname: string, result: NativeSecurity): Error {
|
|
@@ -475,11 +417,10 @@ function windowsExistingVerifyFirst(policy: ManagedSessionSecurityPolicy): boole
|
|
|
475
417
|
|
|
476
418
|
function assertManagedPathIdentity(pathname: string, kind: "directory" | "file", expected: fs.BigIntStats): void {
|
|
477
419
|
const current = fs.lstatSync(pathname, { bigint: true });
|
|
478
|
-
if (current.isSymbolicLink()) throw new
|
|
420
|
+
if (current.isSymbolicLink()) throw new Error("reparse_point");
|
|
479
421
|
const expectedKind = kind === "directory" ? current.isDirectory() : current.isFile();
|
|
480
|
-
if (!expectedKind) throw new
|
|
481
|
-
if (current.dev !== expected.dev || current.ino !== expected.ino)
|
|
482
|
-
throw new ManagedSessionSecurityError("identity_mismatch");
|
|
422
|
+
if (!expectedKind) throw new Error(kind === "directory" ? "not_directory" : "not_file");
|
|
423
|
+
if (current.dev !== expected.dev || current.ino !== expected.ino) throw new Error("identity_mismatch");
|
|
483
424
|
}
|
|
484
425
|
|
|
485
426
|
function verifyExistingManagedPathSecurity(
|
|
@@ -499,7 +440,7 @@ function verifyExistingManagedPathSecurity(
|
|
|
499
440
|
function secureExistingManagedDirectory(pathname: string, kind: "directory" | "file"): void {
|
|
500
441
|
const named = fs.lstatSync(pathname, { bigint: true });
|
|
501
442
|
const safeKind = kind === "directory" ? named.isDirectory() : named.isFile();
|
|
502
|
-
if (!safeKind || named.isSymbolicLink()) throw new
|
|
443
|
+
if (!safeKind || named.isSymbolicLink()) throw new Error(`Unsafe managed ${kind}: ${pathname}`);
|
|
503
444
|
const verified = validateNativeSecurityResult(
|
|
504
445
|
verifyOwnerOnlyPathSecurityExpected(pathname, kind, named.dev, named.ino),
|
|
505
446
|
"verify",
|
|
@@ -659,14 +600,15 @@ export class ManagedSessionDescendantStore {
|
|
|
659
600
|
if (this.#authority) {
|
|
660
601
|
if (this.#authorityBaseDir === this.#baseDir) {
|
|
661
602
|
const verified = this.#authority.verifyOwnerOnlyDirectory();
|
|
662
|
-
if (!verified.ok) throw
|
|
603
|
+
if (!verified.ok) throw new Error(verified.code ?? "acl_verify_failed");
|
|
663
604
|
} else {
|
|
664
605
|
this.#assertBound();
|
|
665
606
|
return;
|
|
666
607
|
}
|
|
667
608
|
} else {
|
|
668
609
|
const named = fs.lstatSync(this.#baseDir, { bigint: true });
|
|
669
|
-
if (!named.isDirectory() || named.isSymbolicLink())
|
|
610
|
+
if (!named.isDirectory() || named.isSymbolicLink())
|
|
611
|
+
throw new Error(`Unsafe managed directory: ${this.#baseDir}`);
|
|
670
612
|
if (windowsExistingVerifyFirst(this.#policy))
|
|
671
613
|
verifyExistingManagedPathSecurity(this.#baseDir, "directory", named);
|
|
672
614
|
else {
|
|
@@ -753,13 +695,7 @@ export class ManagedSessionDescendantStore {
|
|
|
753
695
|
this.#assertBound();
|
|
754
696
|
const resolved = this.#resolve(relativePath);
|
|
755
697
|
if (!this.#authority) {
|
|
756
|
-
|
|
757
|
-
const existing = this.readExpected(relativePath);
|
|
758
|
-
if (existing) replaceManagedFileExactSync(resolved, bytes, existing, this.#subtreeRoot);
|
|
759
|
-
else await publishManagedFileNoReplace(resolved, bytes, undefined, this.#subtreeRoot, this.#policy);
|
|
760
|
-
} else {
|
|
761
|
-
await replaceManagedFile(resolved, bytes, this.#subtreeRoot, this.#policy);
|
|
762
|
-
}
|
|
698
|
+
await replaceManagedFile(resolved, bytes, this.#subtreeRoot, this.#policy);
|
|
763
699
|
this.#assertBound();
|
|
764
700
|
return;
|
|
765
701
|
}
|
|
@@ -772,13 +708,7 @@ export class ManagedSessionDescendantStore {
|
|
|
772
708
|
this.#assertBound();
|
|
773
709
|
const resolved = this.#resolve(relativePath);
|
|
774
710
|
if (!this.#authority) {
|
|
775
|
-
|
|
776
|
-
const existing = this.readExpected(relativePath);
|
|
777
|
-
if (existing) replaceManagedFileExactSync(resolved, bytes, existing, this.#subtreeRoot);
|
|
778
|
-
else publishManagedFileNoReplaceSync(resolved, bytes, this.#subtreeRoot, this.#policy);
|
|
779
|
-
} else {
|
|
780
|
-
replaceManagedFileSync(resolved, bytes, this.#subtreeRoot, this.#policy);
|
|
781
|
-
}
|
|
711
|
+
replaceManagedFileSync(resolved, bytes, this.#subtreeRoot, this.#policy);
|
|
782
712
|
this.#assertBound();
|
|
783
713
|
return;
|
|
784
714
|
}
|
|
@@ -1225,7 +1155,7 @@ export class ManagedSessionDescendantStore {
|
|
|
1225
1155
|
}
|
|
1226
1156
|
|
|
1227
1157
|
function secureFileDescriptor(pathname: string, fd: number, operation: "apply" | "verify"): void {
|
|
1228
|
-
if (process.platform !== "linux"
|
|
1158
|
+
if (process.platform !== "linux") {
|
|
1229
1159
|
if (operation === "apply") secure(pathname, "file");
|
|
1230
1160
|
else {
|
|
1231
1161
|
const verified = validateNativeSecurityResult(verifyOwnerOnlyPathSecurity(pathname, "file"), "verify", "file");
|
|
@@ -1309,9 +1239,11 @@ function isCtimeOnlyIdentityMismatch(
|
|
|
1309
1239
|
return sameStableIdentityIgnoringCtime(observed, expected) && observed.ctimeNs !== expected.ctimeNs;
|
|
1310
1240
|
}
|
|
1311
1241
|
|
|
1312
|
-
function
|
|
1242
|
+
function parseLock(pathname: string): LockRecord | undefined {
|
|
1313
1243
|
try {
|
|
1314
|
-
const
|
|
1244
|
+
const stat = fs.lstatSync(pathname);
|
|
1245
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return undefined;
|
|
1246
|
+
const value: unknown = JSON.parse(fs.readFileSync(pathname, "utf8"));
|
|
1315
1247
|
if (!value || typeof value !== "object") return undefined;
|
|
1316
1248
|
const record = value as Partial<LockRecord>;
|
|
1317
1249
|
return typeof record.attemptId === "string" &&
|
|
@@ -1327,16 +1259,6 @@ function parseLockBytes(bytes: Uint8Array): LockRecord | undefined {
|
|
|
1327
1259
|
}
|
|
1328
1260
|
}
|
|
1329
1261
|
|
|
1330
|
-
function parseLock(pathname: string): LockRecord | undefined {
|
|
1331
|
-
try {
|
|
1332
|
-
const stat = fs.lstatSync(pathname);
|
|
1333
|
-
if (!stat.isFile() || stat.isSymbolicLink()) return undefined;
|
|
1334
|
-
return parseLockBytes(fs.readFileSync(pathname));
|
|
1335
|
-
} catch {
|
|
1336
|
-
return undefined;
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
1262
|
function ownerDefinitelyGone(record: LockRecord): boolean {
|
|
1341
1263
|
if (record.bootId && bootId() && record.bootId !== bootId()) return true;
|
|
1342
1264
|
try {
|
|
@@ -1357,82 +1279,6 @@ function writeLockDescriptor(fd: number, record: LockRecord): void {
|
|
|
1357
1279
|
function sameFileIdentity(left: fs.BigIntStats, right: fs.BigIntStats): boolean {
|
|
1358
1280
|
return left.dev === right.dev && left.ino === right.ino;
|
|
1359
1281
|
}
|
|
1360
|
-
function exactLockRetirementAccepted(lockPath: string, removed: NativeExactUnlinkResult): boolean {
|
|
1361
|
-
if (removed.ok) return true;
|
|
1362
|
-
if (removed.code !== "cleanup_pending") return false;
|
|
1363
|
-
const lockDirectory = path.dirname(path.resolve(lockPath));
|
|
1364
|
-
const authority = [
|
|
1365
|
-
removed.detachedPath,
|
|
1366
|
-
removed.retainedSuccessorPath,
|
|
1367
|
-
removed.retainedPlaceholderPath,
|
|
1368
|
-
removed.retainedUnknownPath,
|
|
1369
|
-
].find(pathname => {
|
|
1370
|
-
if (typeof pathname !== "string" || pathname.length === 0 || !path.isAbsolute(pathname)) return false;
|
|
1371
|
-
const resolved = path.resolve(pathname);
|
|
1372
|
-
if (resolved === path.resolve(lockPath) || path.dirname(resolved) !== lockDirectory) return false;
|
|
1373
|
-
if (!path.basename(resolved).startsWith(".gjc-")) return false;
|
|
1374
|
-
try {
|
|
1375
|
-
return !fs.lstatSync(resolved).isSymbolicLink();
|
|
1376
|
-
} catch {
|
|
1377
|
-
return false;
|
|
1378
|
-
}
|
|
1379
|
-
});
|
|
1380
|
-
try {
|
|
1381
|
-
fs.lstatSync(lockPath);
|
|
1382
|
-
return false;
|
|
1383
|
-
} catch (error) {
|
|
1384
|
-
return !!authority && (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
function unlinkManagedLockSnapshotExact(lockPath: string, snapshot: ManagedFileSnapshot): void {
|
|
1389
|
-
const removed = exactUnlink(lockPath, {
|
|
1390
|
-
dev: snapshot.identity.dev,
|
|
1391
|
-
ino: snapshot.identity.ino,
|
|
1392
|
-
size: BigInt(snapshot.identity.size),
|
|
1393
|
-
mtimeNs: snapshot.identity.mtimeNs,
|
|
1394
|
-
sha256: snapshot.identity.sha256,
|
|
1395
|
-
quarantineName: `.gjc-lock-retire-${process.pid}-${randomUUID()}`,
|
|
1396
|
-
});
|
|
1397
|
-
if (!exactLockRetirementAccepted(lockPath, removed)) throw new Error("migration_busy");
|
|
1398
|
-
fsyncDirectory(path.dirname(lockPath));
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
function retireCreatedManagedLockExact(lockPath: string, fd: number): void {
|
|
1402
|
-
const opened = fs.fstatSync(fd, { bigint: true });
|
|
1403
|
-
const snapshot = captureManagedFileNoFollow(lockPath);
|
|
1404
|
-
if (snapshot.identity.dev !== opened.dev || snapshot.identity.ino !== opened.ino) throw new Error("migration_busy");
|
|
1405
|
-
unlinkManagedLockSnapshotExact(lockPath, snapshot);
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
/**
|
|
1409
|
-
* Retire a lock only after exact identity and attempt fencing. A typed
|
|
1410
|
-
* `cleanup_pending` result is terminal lock-authority retirement, not a claim
|
|
1411
|
-
* that retained bytes were deleted: accept it only when native exact-unlink
|
|
1412
|
-
* supplies concrete internal authority in this lock directory and the canonical
|
|
1413
|
-
* lock name is absent, so a successor may acquire while quarantine evidence stays
|
|
1414
|
-
* retained for recovery.
|
|
1415
|
-
*/
|
|
1416
|
-
function retireManagedLockExact(
|
|
1417
|
-
lockPath: string,
|
|
1418
|
-
expected: { lockIdentity?: fs.BigIntStats; attemptId: string },
|
|
1419
|
-
): void {
|
|
1420
|
-
let snapshot: ManagedFileSnapshot;
|
|
1421
|
-
try {
|
|
1422
|
-
snapshot = captureManagedFileNoFollow(lockPath);
|
|
1423
|
-
} catch {
|
|
1424
|
-
throw new Error("migration_busy");
|
|
1425
|
-
}
|
|
1426
|
-
const record = parseLockBytes(snapshot.bytes);
|
|
1427
|
-
if (
|
|
1428
|
-
!record ||
|
|
1429
|
-
record.attemptId !== expected.attemptId ||
|
|
1430
|
-
(expected.lockIdentity &&
|
|
1431
|
-
(snapshot.identity.dev !== expected.lockIdentity.dev || snapshot.identity.ino !== expected.lockIdentity.ino))
|
|
1432
|
-
)
|
|
1433
|
-
throw new Error("migration_busy");
|
|
1434
|
-
unlinkManagedLockSnapshotExact(lockPath, snapshot);
|
|
1435
|
-
}
|
|
1436
1282
|
|
|
1437
1283
|
/** Create a managed directory and fail closed unless its owner-only mode/ACL verifies. */
|
|
1438
1284
|
export function ensureManagedDirectory(
|
|
@@ -1640,126 +1486,8 @@ export function publishManagedFileNoReplaceSync(
|
|
|
1640
1486
|
if (failure !== undefined) throw failure;
|
|
1641
1487
|
}
|
|
1642
1488
|
|
|
1643
|
-
/**
|
|
1644
|
-
* Atomically replace a managed file only when the destination is still the
|
|
1645
|
-
* exact captured object. Unlike `replaceManagedFileSync`, this never performs
|
|
1646
|
-
* an unconditional pathname rename.
|
|
1647
|
-
*/
|
|
1648
|
-
export function replaceManagedFileExactSync(
|
|
1649
|
-
destination: string,
|
|
1650
|
-
bytes: Uint8Array,
|
|
1651
|
-
expected: Pick<ManagedFileSnapshot, "identity">,
|
|
1652
|
-
root: ManagedDirectoryRoot,
|
|
1653
|
-
): void {
|
|
1654
|
-
assertManagedDirectoryRoot(root);
|
|
1655
|
-
managedRelativePath(root, destination);
|
|
1656
|
-
if (process.platform === "win32") {
|
|
1657
|
-
const parent = path.dirname(destination);
|
|
1658
|
-
const lock = acquireManagedLockSync(
|
|
1659
|
-
parent,
|
|
1660
|
-
`.${path.basename(destination)}.replace`,
|
|
1661
|
-
root,
|
|
1662
|
-
"windows-existing-verify-first",
|
|
1663
|
-
);
|
|
1664
|
-
const staging = path.join(parent, `.${path.basename(destination)}.${randomUUID()}.replacement`);
|
|
1665
|
-
let fd: number | undefined;
|
|
1666
|
-
let stagedIdentity: { dev: bigint; ino: bigint } | undefined;
|
|
1667
|
-
let publicationUncertain = false;
|
|
1668
|
-
try {
|
|
1669
|
-
lock.assertOwned();
|
|
1670
|
-
const current = captureManagedFileNoFollow(destination);
|
|
1671
|
-
if (!sameIdentity(current.identity, expected.identity) || current.identity.sha256 !== expected.identity.sha256)
|
|
1672
|
-
throw new Error("managed_replace_identity_mismatch");
|
|
1673
|
-
fd = fs.openSync(
|
|
1674
|
-
staging,
|
|
1675
|
-
fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW,
|
|
1676
|
-
0o600,
|
|
1677
|
-
);
|
|
1678
|
-
secureFileDescriptor(staging, fd, "apply");
|
|
1679
|
-
let offset = 0;
|
|
1680
|
-
while (offset < bytes.byteLength) offset += fs.writeSync(fd, bytes, offset, bytes.byteLength - offset);
|
|
1681
|
-
fs.fsyncSync(fd);
|
|
1682
|
-
secureFileDescriptor(staging, fd, "verify");
|
|
1683
|
-
const staged = fs.fstatSync(fd, { bigint: true });
|
|
1684
|
-
stagedIdentity = { dev: staged.dev, ino: staged.ino };
|
|
1685
|
-
fs.closeSync(fd);
|
|
1686
|
-
fd = undefined;
|
|
1687
|
-
lock.assertOwned();
|
|
1688
|
-
const outcome = exactReplacePath(staging, destination, {
|
|
1689
|
-
dev: expected.identity.dev,
|
|
1690
|
-
ino: expected.identity.ino,
|
|
1691
|
-
size: BigInt(expected.identity.size),
|
|
1692
|
-
mtimeNs: expected.identity.mtimeNs,
|
|
1693
|
-
sha256: expected.identity.sha256,
|
|
1694
|
-
});
|
|
1695
|
-
if (!outcome.ok) {
|
|
1696
|
-
publicationUncertain = true;
|
|
1697
|
-
throw new Error(
|
|
1698
|
-
outcome.detachedPath === staging
|
|
1699
|
-
? "managed_replace_publication_pending"
|
|
1700
|
-
: (outcome.code ?? "managed_replace_identity_mismatch"),
|
|
1701
|
-
);
|
|
1702
|
-
}
|
|
1703
|
-
lock.assertOwned();
|
|
1704
|
-
const published = fs.lstatSync(destination, { bigint: true });
|
|
1705
|
-
if (
|
|
1706
|
-
!published.isFile() ||
|
|
1707
|
-
published.isSymbolicLink() ||
|
|
1708
|
-
published.dev !== staged.dev ||
|
|
1709
|
-
published.ino !== staged.ino
|
|
1710
|
-
) {
|
|
1711
|
-
publicationUncertain = true;
|
|
1712
|
-
throw new Error("destination_identity_changed");
|
|
1713
|
-
}
|
|
1714
|
-
secureExistingManagedDirectory(destination, "file");
|
|
1715
|
-
} finally {
|
|
1716
|
-
if (fd !== undefined) fs.closeSync(fd);
|
|
1717
|
-
if (stagedIdentity && !publicationUncertain) {
|
|
1718
|
-
try {
|
|
1719
|
-
const named = fs.lstatSync(staging, { bigint: true });
|
|
1720
|
-
if (named.dev === stagedIdentity.dev && named.ino === stagedIdentity.ino) fs.unlinkSync(staging);
|
|
1721
|
-
} catch (error) {
|
|
1722
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") publicationUncertain = true;
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
lock.release();
|
|
1726
|
-
}
|
|
1727
|
-
return;
|
|
1728
|
-
}
|
|
1729
|
-
const parent = path.dirname(destination);
|
|
1730
|
-
const parentStat = fs.lstatSync(parent, { bigint: true });
|
|
1731
|
-
if (!parentStat.isDirectory() || parentStat.isSymbolicLink()) throw new Error("managed_replace_parent_unavailable");
|
|
1732
|
-
const authority = openRecoveryFsRoot(parent);
|
|
1733
|
-
try {
|
|
1734
|
-
const retained = authority.identity();
|
|
1735
|
-
if (
|
|
1736
|
-
!retained.ok ||
|
|
1737
|
-
!retained.identity ||
|
|
1738
|
-
retained.identity.dev !== parentStat.dev.toString() ||
|
|
1739
|
-
retained.identity.ino !== parentStat.ino.toString()
|
|
1740
|
-
)
|
|
1741
|
-
throw new Error("managed_replace_parent_identity_changed");
|
|
1742
|
-
const verified = authority.verifyOwnerOnlyDirectory();
|
|
1743
|
-
if (!verified.ok) throw new Error(verified.code ?? "managed_replace_parent_unavailable");
|
|
1744
|
-
const replaced = authority.replaceManaged(
|
|
1745
|
-
path.basename(destination),
|
|
1746
|
-
bytes,
|
|
1747
|
-
expected.identity.dev.toString(),
|
|
1748
|
-
expected.identity.ino.toString(),
|
|
1749
|
-
expected.identity.size.toString(),
|
|
1750
|
-
expected.identity.mtimeNs.toString(),
|
|
1751
|
-
expected.identity.ctimeNs.toString(),
|
|
1752
|
-
expected.identity.sha256,
|
|
1753
|
-
);
|
|
1754
|
-
if (!replaced.ok) throw new Error(replaced.code ?? "managed_replace_identity_mismatch");
|
|
1755
|
-
const synced = authority.fsync();
|
|
1756
|
-
if (!synced.ok) throw new Error(synced.code ?? "managed_replace_fsync_failed");
|
|
1757
|
-
} finally {
|
|
1758
|
-
authority.close();
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
1489
|
/** Replace one managed regular file while retaining the secured staging fd through publication. */
|
|
1762
|
-
function replaceManagedFileSync(
|
|
1490
|
+
export function replaceManagedFileSync(
|
|
1763
1491
|
destination: string,
|
|
1764
1492
|
bytes: Uint8Array,
|
|
1765
1493
|
root: ManagedDirectoryRoot,
|
|
@@ -1807,7 +1535,7 @@ function replaceManagedFileSync(
|
|
|
1807
1535
|
}
|
|
1808
1536
|
}
|
|
1809
1537
|
|
|
1810
|
-
async function replaceManagedFile(
|
|
1538
|
+
export async function replaceManagedFile(
|
|
1811
1539
|
destination: string,
|
|
1812
1540
|
bytes: Uint8Array,
|
|
1813
1541
|
root: ManagedDirectoryRoot,
|
|
@@ -1842,6 +1570,7 @@ export async function acquireManagedLock(
|
|
|
1842
1570
|
ensureManagedDirectory(locksDirectory, root, policy);
|
|
1843
1571
|
const lockPath = path.join(locksDirectory, `${name}.lock`);
|
|
1844
1572
|
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
1573
|
+
let staleObservedAt: number | undefined;
|
|
1845
1574
|
while (true) {
|
|
1846
1575
|
const attemptId = randomUUID();
|
|
1847
1576
|
const now = Date.now();
|
|
@@ -1866,11 +1595,7 @@ export async function acquireManagedLock(
|
|
|
1866
1595
|
fs.fsyncSync(fd);
|
|
1867
1596
|
fsyncDirectory(locksDirectory);
|
|
1868
1597
|
} catch (error) {
|
|
1869
|
-
|
|
1870
|
-
retireCreatedManagedLockExact(lockPath, fd);
|
|
1871
|
-
} finally {
|
|
1872
|
-
fs.closeSync(fd);
|
|
1873
|
-
}
|
|
1598
|
+
fs.closeSync(fd);
|
|
1874
1599
|
throw error;
|
|
1875
1600
|
}
|
|
1876
1601
|
const lockIdentity = fs.fstatSync(fd, { bigint: true });
|
|
@@ -1878,8 +1603,12 @@ export async function acquireManagedLock(
|
|
|
1878
1603
|
let descriptorClosed = false;
|
|
1879
1604
|
const closeDescriptor = (): void => {
|
|
1880
1605
|
if (descriptorClosed) return;
|
|
1881
|
-
|
|
1882
|
-
|
|
1606
|
+
try {
|
|
1607
|
+
secureFileDescriptor(lockPath, fd, "verify");
|
|
1608
|
+
} finally {
|
|
1609
|
+
fs.closeSync(fd);
|
|
1610
|
+
descriptorClosed = true;
|
|
1611
|
+
}
|
|
1883
1612
|
};
|
|
1884
1613
|
const assertOwned = (): void => {
|
|
1885
1614
|
const current = parseLock(lockPath);
|
|
@@ -1894,7 +1623,8 @@ export async function acquireManagedLock(
|
|
|
1894
1623
|
descriptorClosed ||
|
|
1895
1624
|
!current ||
|
|
1896
1625
|
!sameFileIdentity(lockIdentity, named) ||
|
|
1897
|
-
current.attemptId !== attemptId
|
|
1626
|
+
current.attemptId !== attemptId ||
|
|
1627
|
+
current.leaseExpiresAt < Date.now()
|
|
1898
1628
|
)
|
|
1899
1629
|
throw new Error("migration_busy");
|
|
1900
1630
|
};
|
|
@@ -1912,160 +1642,42 @@ export async function acquireManagedLock(
|
|
|
1912
1642
|
attemptId,
|
|
1913
1643
|
assertOwned,
|
|
1914
1644
|
async release(): Promise<void> {
|
|
1915
|
-
|
|
1916
|
-
clearInterval(heartbeat);
|
|
1917
|
-
released = true;
|
|
1918
|
-
closeDescriptor();
|
|
1919
|
-
};
|
|
1645
|
+
clearInterval(heartbeat);
|
|
1920
1646
|
try {
|
|
1921
1647
|
assertOwned();
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
throw error;
|
|
1931
|
-
}
|
|
1932
|
-
try {
|
|
1933
|
-
retireManagedLockExact(lockPath, { lockIdentity, attemptId });
|
|
1934
|
-
} catch (error) {
|
|
1935
|
-
try {
|
|
1936
|
-
assertOwned();
|
|
1937
|
-
} catch {
|
|
1938
|
-
finalize();
|
|
1939
|
-
}
|
|
1940
|
-
throw error;
|
|
1648
|
+
const now = Date.now();
|
|
1649
|
+
// Do not unlink by pathname: a stale owner could otherwise remove a successor.
|
|
1650
|
+
// The lease is retired through the verified inode-bound descriptor instead.
|
|
1651
|
+
writeLockDescriptor(fd, { ...record, heartbeatAt: now, leaseExpiresAt: now });
|
|
1652
|
+
fsyncDirectory(locksDirectory);
|
|
1653
|
+
} finally {
|
|
1654
|
+
released = true;
|
|
1655
|
+
closeDescriptor();
|
|
1941
1656
|
}
|
|
1942
|
-
finalize();
|
|
1943
1657
|
},
|
|
1944
1658
|
};
|
|
1945
1659
|
} catch (error) {
|
|
1946
1660
|
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
1947
1661
|
const owner = parseLock(lockPath);
|
|
1948
|
-
if (owner && owner.leaseExpiresAt < Date.now()
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
if (Date.now() >= deadline) throw new Error("migration_busy");
|
|
1956
|
-
await new Promise<void>(resolve => setTimeout(resolve, 50));
|
|
1957
|
-
}
|
|
1958
|
-
}
|
|
1959
|
-
}
|
|
1960
|
-
/** Acquire a synchronous lease lock for a short cross-process critical section. */
|
|
1961
|
-
export function acquireManagedLockSync(
|
|
1962
|
-
locksDirectory: string,
|
|
1963
|
-
name: string,
|
|
1964
|
-
root?: ManagedDirectoryRoot,
|
|
1965
|
-
policy: ManagedSessionSecurityPolicy = "default",
|
|
1966
|
-
): ManagedStorageLockSync {
|
|
1967
|
-
ensureManagedDirectory(locksDirectory, root, policy);
|
|
1968
|
-
const lockPath = path.join(locksDirectory, `${name}.lock`);
|
|
1969
|
-
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
1970
|
-
while (true) {
|
|
1971
|
-
const attemptId = randomUUID();
|
|
1972
|
-
const now = Date.now();
|
|
1973
|
-
const record: LockRecord = {
|
|
1974
|
-
attemptId,
|
|
1975
|
-
pid: process.pid,
|
|
1976
|
-
bootId: bootId(),
|
|
1977
|
-
processStartId: PROCESS_START_ID,
|
|
1978
|
-
createdAt: now,
|
|
1979
|
-
heartbeatAt: now,
|
|
1980
|
-
leaseExpiresAt: now + LOCK_LEASE_MS,
|
|
1981
|
-
};
|
|
1982
|
-
try {
|
|
1983
|
-
const fd = fs.openSync(
|
|
1984
|
-
lockPath,
|
|
1985
|
-
fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW,
|
|
1986
|
-
0o600,
|
|
1987
|
-
);
|
|
1988
|
-
try {
|
|
1989
|
-
secureFileDescriptor(lockPath, fd, "apply");
|
|
1990
|
-
writeLockDescriptor(fd, record);
|
|
1991
|
-
fsyncDirectory(locksDirectory);
|
|
1992
|
-
} catch (error) {
|
|
1993
|
-
try {
|
|
1994
|
-
retireCreatedManagedLockExact(lockPath, fd);
|
|
1995
|
-
} finally {
|
|
1996
|
-
fs.closeSync(fd);
|
|
1997
|
-
}
|
|
1998
|
-
throw error;
|
|
1999
|
-
}
|
|
2000
|
-
const lockIdentity = fs.fstatSync(fd, { bigint: true });
|
|
2001
|
-
let released = false;
|
|
2002
|
-
let descriptorClosed = false;
|
|
2003
|
-
const assertOwned = (): void => {
|
|
2004
|
-
const current = parseLock(lockPath);
|
|
2005
|
-
let named: fs.BigIntStats;
|
|
2006
|
-
try {
|
|
2007
|
-
named = fs.lstatSync(lockPath, { bigint: true });
|
|
2008
|
-
} catch {
|
|
2009
|
-
throw new Error("migration_busy");
|
|
2010
|
-
}
|
|
2011
|
-
if (
|
|
2012
|
-
released ||
|
|
2013
|
-
descriptorClosed ||
|
|
2014
|
-
!current ||
|
|
2015
|
-
!sameFileIdentity(lockIdentity, named) ||
|
|
2016
|
-
current.attemptId !== attemptId
|
|
2017
|
-
)
|
|
2018
|
-
throw new Error("migration_busy");
|
|
2019
|
-
};
|
|
2020
|
-
return {
|
|
2021
|
-
path: lockPath,
|
|
2022
|
-
attemptId,
|
|
2023
|
-
assertOwned,
|
|
2024
|
-
release(): void {
|
|
2025
|
-
const finalize = (): void => {
|
|
2026
|
-
released = true;
|
|
2027
|
-
if (!descriptorClosed) {
|
|
2028
|
-
fs.closeSync(fd);
|
|
2029
|
-
descriptorClosed = true;
|
|
2030
|
-
}
|
|
2031
|
-
};
|
|
1662
|
+
if (owner && owner.leaseExpiresAt < Date.now()) {
|
|
1663
|
+
const ownerGone = ownerDefinitelyGone(owner);
|
|
1664
|
+
if (staleObservedAt === undefined) staleObservedAt = Date.now();
|
|
1665
|
+
if (ownerGone || Date.now() - staleObservedAt >= LOCK_STALE_RECHECK_MS) {
|
|
1666
|
+
const quarantine = `${lockPath}.${randomUUID()}.stale`;
|
|
2032
1667
|
try {
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
1668
|
+
fs.renameSync(lockPath, quarantine);
|
|
1669
|
+
const quarantined = parseLock(quarantine);
|
|
1670
|
+
if (!quarantined || quarantined.attemptId !== owner.attemptId) throw new Error("migration_busy");
|
|
1671
|
+
fs.unlinkSync(quarantine);
|
|
1672
|
+
fsyncDirectory(locksDirectory);
|
|
1673
|
+
} catch {
|
|
1674
|
+
/* retry owner observation */
|
|
2037
1675
|
}
|
|
2038
|
-
|
|
2039
|
-
secureFileDescriptor(lockPath, fd, "verify");
|
|
2040
|
-
} catch (error) {
|
|
2041
|
-
if (lockDescriptorOwnershipLost(error)) finalize();
|
|
2042
|
-
throw error;
|
|
2043
|
-
}
|
|
2044
|
-
try {
|
|
2045
|
-
retireManagedLockExact(lockPath, { lockIdentity, attemptId });
|
|
2046
|
-
} catch (error) {
|
|
2047
|
-
try {
|
|
2048
|
-
assertOwned();
|
|
2049
|
-
} catch {
|
|
2050
|
-
finalize();
|
|
2051
|
-
}
|
|
2052
|
-
throw error;
|
|
2053
|
-
}
|
|
2054
|
-
finalize();
|
|
2055
|
-
},
|
|
2056
|
-
};
|
|
2057
|
-
} catch (error) {
|
|
2058
|
-
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
2059
|
-
const owner = parseLock(lockPath);
|
|
2060
|
-
if (owner && owner.leaseExpiresAt < Date.now() && ownerDefinitelyGone(owner)) {
|
|
2061
|
-
try {
|
|
2062
|
-
retireManagedLockExact(lockPath, { attemptId: owner.attemptId });
|
|
2063
|
-
} catch {
|
|
2064
|
-
/* Retry owner observation. */
|
|
1676
|
+
staleObservedAt = undefined;
|
|
2065
1677
|
}
|
|
2066
|
-
}
|
|
1678
|
+
} else staleObservedAt = undefined;
|
|
2067
1679
|
if (Date.now() >= deadline) throw new Error("migration_busy");
|
|
2068
|
-
|
|
1680
|
+
await new Promise<void>(resolve => setTimeout(resolve, 50));
|
|
2069
1681
|
}
|
|
2070
1682
|
}
|
|
2071
1683
|
}
|