@gajae-code/coding-agent 0.12.1 → 0.12.2

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/types/config/settings-schema.d.ts +2 -2
  3. package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
  4. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -3
  5. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
  6. package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
  7. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  8. package/dist/types/modes/controllers/event-controller.d.ts +1 -1
  9. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
  10. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  11. package/dist/types/modes/interactive-mode.d.ts +9 -0
  12. package/dist/types/modes/theme/theme.d.ts +3 -1
  13. package/dist/types/modes/types.d.ts +32 -0
  14. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  15. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -2
  16. package/dist/types/session/agent-session.d.ts +2 -0
  17. package/dist/types/session/internal/managed-session-storage.d.ts +22 -3
  18. package/dist/types/task/index.d.ts +4 -0
  19. package/dist/types/task/output-manager.d.ts +1 -0
  20. package/dist/types/task/receipt.d.ts +2 -0
  21. package/dist/types/tools/index.d.ts +2 -0
  22. package/package.json +8 -8
  23. package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
  24. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  25. package/scripts/run-acp-conformance.ts +5 -0
  26. package/src/async/job-manager.ts +22 -20
  27. package/src/config/settings-schema.ts +2 -2
  28. package/src/debug/index.ts +42 -17
  29. package/src/defaults/gjc/skills/ralplan/SKILL.md +9 -7
  30. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -2
  31. package/src/gjc-runtime/launch-worktree.ts +103 -1
  32. package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
  33. package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
  34. package/src/gjc-runtime/ralplan-runtime.ts +80 -10
  35. package/src/gjc-runtime/ultragoal-evidence.ts +6 -74
  36. package/src/gjc-runtime/ultragoal-runtime.ts +236 -3
  37. package/src/gjc-runtime/workflow-manifest.generated.json +29 -0
  38. package/src/gjc-runtime/workflow-manifest.ts +17 -2
  39. package/src/internal-urls/docs-index.generated.ts +1 -1
  40. package/src/modes/DESIGN.md +53 -0
  41. package/src/modes/components/irc-sidebar.ts +30 -1
  42. package/src/modes/components/tool-status-header.ts +0 -1
  43. package/src/modes/controllers/command-controller.ts +38 -34
  44. package/src/modes/controllers/event-controller.ts +27 -16
  45. package/src/modes/controllers/extension-ui-controller.ts +186 -34
  46. package/src/modes/controllers/selector-controller.ts +115 -22
  47. package/src/modes/interactive-mode.ts +189 -62
  48. package/src/modes/jobs-observer.ts +1 -1
  49. package/src/modes/theme/theme.ts +11 -9
  50. package/src/modes/types.ts +88 -0
  51. package/src/sdk/broker/lifecycle.ts +18 -13
  52. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  53. package/src/sdk/bus/telegram-daemon-contract.ts +3 -2
  54. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  55. package/src/sdk/session.ts +1 -0
  56. package/src/session/agent-session.ts +22 -1
  57. package/src/session/internal/managed-session-scope.ts +158 -107
  58. package/src/session/internal/managed-session-storage.ts +445 -57
  59. package/src/session/session-storage.ts +29 -44
  60. package/src/task/index.ts +257 -31
  61. package/src/task/output-manager.ts +24 -14
  62. package/src/task/receipt.ts +11 -1
  63. package/src/tools/index.ts +2 -0
@@ -6,8 +6,10 @@ import {
6
6
  applyOwnerOnlyFdSecurity,
7
7
  applyOwnerOnlyPathSecurity,
8
8
  exactRemoveDirectoryTree,
9
+ exactReplacePath,
9
10
  exactUnlink,
10
11
  type NativeDirectoryTreeSnapshot,
12
+ type NativeExactUnlinkResult,
11
13
  type NativeOwnerOnlySecurityResult,
12
14
  openRecoveryFsRoot,
13
15
  type RecoveryFsRoot,
@@ -34,7 +36,6 @@ const LOCK_LEASE_MS = 60_000;
34
36
  const LOCK_HEARTBEAT_MS = 10_000;
35
37
  const LOCK_WAIT_MS = 5_000;
36
38
 
37
- const LOCK_STALE_RECHECK_MS = 100;
38
39
  export class ManagedPublishError extends Error {
39
40
  readonly classification:
40
41
  | "destination_conflict"
@@ -44,12 +45,14 @@ export class ManagedPublishError extends Error {
44
45
  | "identity_mismatch"
45
46
  | "io_error"
46
47
  | "durability_failed";
48
+ readonly code: ManagedPublishError["classification"];
47
49
  readonly diagnostic: string;
48
50
 
49
51
  constructor(classification: ManagedPublishError["classification"], outcome: NativePublishOutcome) {
50
52
  super(classification);
51
53
  this.name = "ManagedPublishError";
52
54
  this.classification = classification;
55
+ this.code = classification;
53
56
  this.diagnostic = formatNativePublishDiagnostic(outcome);
54
57
  }
55
58
  }
@@ -134,15 +137,24 @@ export interface ManagedStorageLock {
134
137
  assertOwned(): void;
135
138
  release(): Promise<void>;
136
139
  }
140
+ export interface ManagedStorageLockSync {
141
+ path: string;
142
+ attemptId: string;
143
+ assertOwned(): void;
144
+ release(): void;
145
+ }
137
146
 
138
147
  export interface ManagedFileSnapshot {
139
148
  bytes: Buffer;
140
149
  identity: { dev: bigint; ino: bigint; size: number; mtimeNs: bigint; ctimeNs: bigint; sha256: string };
141
150
  }
142
151
 
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([
152
+ export const MANAGED_SESSION_SECURITY_CODES = [
153
+ "acl_denied",
154
+ "acl_io_error",
155
+ "acl_present",
156
+ "acl_malformed",
157
+ "acl_unknown",
146
158
  "acl_unavailable",
147
159
  "acl_apply_failed",
148
160
  "acl_verify_failed",
@@ -155,7 +167,52 @@ const GENERAL_FAILURE_CODES = new Set([
155
167
  "owner_mismatch",
156
168
  "mode_mismatch",
157
169
  "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",
158
178
  ]);
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
+ }
159
216
 
160
217
  function hasOnlyKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
161
218
  return Object.keys(value).every(key => allowed.includes(key));
@@ -173,10 +230,11 @@ export function validateNativeSecurityResult(
173
230
  const result = value as Record<string, unknown>;
174
231
  if (result.ok === false) {
175
232
  if (typeof result.code !== "string") throw new Error("Malformed owner-only security failure");
176
- if (!ACL_FAILURE_CODES.has(result.code) && !GENERAL_FAILURE_CODES.has(result.code)) {
233
+ const code = result.code as ManagedSessionSecurityCode;
234
+ if (!ACL_FAILURE_CODES.has(code) && !GENERAL_FAILURE_CODES.has(code)) {
177
235
  throw new Error("Unknown owner-only security failure code");
178
236
  }
179
- if (ACL_FAILURE_CODES.has(result.code)) {
237
+ if (ACL_FAILURE_CODES.has(code)) {
180
238
  if (
181
239
  (result.operation !== "clear" && result.operation !== "query") ||
182
240
  (result.attribute !== "access" && result.attribute !== "default")
@@ -397,7 +455,7 @@ class ManagedSecurityError extends Error {
397
455
  }
398
456
 
399
457
  export function managedSecurityFailureClassification(error: unknown): string | undefined {
400
- return error instanceof ManagedSecurityError ? error.getClassification() : undefined;
458
+ return error instanceof ManagedSecurityError ? error.getClassification() : managedSessionSecurityCode(error);
401
459
  }
402
460
 
403
461
  function securityError(pathname: string, result: NativeSecurity): Error {
@@ -417,10 +475,11 @@ function windowsExistingVerifyFirst(policy: ManagedSessionSecurityPolicy): boole
417
475
 
418
476
  function assertManagedPathIdentity(pathname: string, kind: "directory" | "file", expected: fs.BigIntStats): void {
419
477
  const current = fs.lstatSync(pathname, { bigint: true });
420
- if (current.isSymbolicLink()) throw new Error("reparse_point");
478
+ if (current.isSymbolicLink()) throw new ManagedSessionSecurityError("reparse_point");
421
479
  const expectedKind = kind === "directory" ? current.isDirectory() : current.isFile();
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");
480
+ if (!expectedKind) throw new ManagedSessionSecurityError("not_directory");
481
+ if (current.dev !== expected.dev || current.ino !== expected.ino)
482
+ throw new ManagedSessionSecurityError("identity_mismatch");
424
483
  }
425
484
 
426
485
  function verifyExistingManagedPathSecurity(
@@ -440,7 +499,7 @@ function verifyExistingManagedPathSecurity(
440
499
  function secureExistingManagedDirectory(pathname: string, kind: "directory" | "file"): void {
441
500
  const named = fs.lstatSync(pathname, { bigint: true });
442
501
  const safeKind = kind === "directory" ? named.isDirectory() : named.isFile();
443
- if (!safeKind || named.isSymbolicLink()) throw new Error(`Unsafe managed ${kind}: ${pathname}`);
502
+ if (!safeKind || named.isSymbolicLink()) throw new ManagedSessionSecurityError("reparse_point");
444
503
  const verified = validateNativeSecurityResult(
445
504
  verifyOwnerOnlyPathSecurityExpected(pathname, kind, named.dev, named.ino),
446
505
  "verify",
@@ -600,15 +659,14 @@ export class ManagedSessionDescendantStore {
600
659
  if (this.#authority) {
601
660
  if (this.#authorityBaseDir === this.#baseDir) {
602
661
  const verified = this.#authority.verifyOwnerOnlyDirectory();
603
- if (!verified.ok) throw new Error(verified.code ?? "acl_verify_failed");
662
+ if (!verified.ok) throw managedSessionSecurityErrorForCode(verified.code);
604
663
  } else {
605
664
  this.#assertBound();
606
665
  return;
607
666
  }
608
667
  } else {
609
668
  const named = fs.lstatSync(this.#baseDir, { bigint: true });
610
- if (!named.isDirectory() || named.isSymbolicLink())
611
- throw new Error(`Unsafe managed directory: ${this.#baseDir}`);
669
+ if (!named.isDirectory() || named.isSymbolicLink()) throw new ManagedSessionSecurityError("reparse_point");
612
670
  if (windowsExistingVerifyFirst(this.#policy))
613
671
  verifyExistingManagedPathSecurity(this.#baseDir, "directory", named);
614
672
  else {
@@ -695,7 +753,13 @@ export class ManagedSessionDescendantStore {
695
753
  this.#assertBound();
696
754
  const resolved = this.#resolve(relativePath);
697
755
  if (!this.#authority) {
698
- await replaceManagedFile(resolved, bytes, this.#subtreeRoot, this.#policy);
756
+ if (process.platform === "win32") {
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
+ }
699
763
  this.#assertBound();
700
764
  return;
701
765
  }
@@ -708,7 +772,13 @@ export class ManagedSessionDescendantStore {
708
772
  this.#assertBound();
709
773
  const resolved = this.#resolve(relativePath);
710
774
  if (!this.#authority) {
711
- replaceManagedFileSync(resolved, bytes, this.#subtreeRoot, this.#policy);
775
+ if (process.platform === "win32") {
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
+ }
712
782
  this.#assertBound();
713
783
  return;
714
784
  }
@@ -1155,7 +1225,7 @@ export class ManagedSessionDescendantStore {
1155
1225
  }
1156
1226
 
1157
1227
  function secureFileDescriptor(pathname: string, fd: number, operation: "apply" | "verify"): void {
1158
- if (process.platform !== "linux") {
1228
+ if (process.platform !== "linux" && process.platform !== "win32") {
1159
1229
  if (operation === "apply") secure(pathname, "file");
1160
1230
  else {
1161
1231
  const verified = validateNativeSecurityResult(verifyOwnerOnlyPathSecurity(pathname, "file"), "verify", "file");
@@ -1239,11 +1309,9 @@ function isCtimeOnlyIdentityMismatch(
1239
1309
  return sameStableIdentityIgnoringCtime(observed, expected) && observed.ctimeNs !== expected.ctimeNs;
1240
1310
  }
1241
1311
 
1242
- function parseLock(pathname: string): LockRecord | undefined {
1312
+ function parseLockBytes(bytes: Uint8Array): LockRecord | undefined {
1243
1313
  try {
1244
- const stat = fs.lstatSync(pathname);
1245
- if (!stat.isFile() || stat.isSymbolicLink()) return undefined;
1246
- const value: unknown = JSON.parse(fs.readFileSync(pathname, "utf8"));
1314
+ const value: unknown = JSON.parse(Buffer.from(bytes).toString("utf8"));
1247
1315
  if (!value || typeof value !== "object") return undefined;
1248
1316
  const record = value as Partial<LockRecord>;
1249
1317
  return typeof record.attemptId === "string" &&
@@ -1259,6 +1327,16 @@ function parseLock(pathname: string): LockRecord | undefined {
1259
1327
  }
1260
1328
  }
1261
1329
 
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
+
1262
1340
  function ownerDefinitelyGone(record: LockRecord): boolean {
1263
1341
  if (record.bootId && bootId() && record.bootId !== bootId()) return true;
1264
1342
  try {
@@ -1279,6 +1357,82 @@ function writeLockDescriptor(fd: number, record: LockRecord): void {
1279
1357
  function sameFileIdentity(left: fs.BigIntStats, right: fs.BigIntStats): boolean {
1280
1358
  return left.dev === right.dev && left.ino === right.ino;
1281
1359
  }
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
+ }
1282
1436
 
1283
1437
  /** Create a managed directory and fail closed unless its owner-only mode/ACL verifies. */
1284
1438
  export function ensureManagedDirectory(
@@ -1486,8 +1640,126 @@ export function publishManagedFileNoReplaceSync(
1486
1640
  if (failure !== undefined) throw failure;
1487
1641
  }
1488
1642
 
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
+ }
1489
1761
  /** Replace one managed regular file while retaining the secured staging fd through publication. */
1490
- export function replaceManagedFileSync(
1762
+ function replaceManagedFileSync(
1491
1763
  destination: string,
1492
1764
  bytes: Uint8Array,
1493
1765
  root: ManagedDirectoryRoot,
@@ -1535,7 +1807,7 @@ export function replaceManagedFileSync(
1535
1807
  }
1536
1808
  }
1537
1809
 
1538
- export async function replaceManagedFile(
1810
+ async function replaceManagedFile(
1539
1811
  destination: string,
1540
1812
  bytes: Uint8Array,
1541
1813
  root: ManagedDirectoryRoot,
@@ -1570,7 +1842,6 @@ export async function acquireManagedLock(
1570
1842
  ensureManagedDirectory(locksDirectory, root, policy);
1571
1843
  const lockPath = path.join(locksDirectory, `${name}.lock`);
1572
1844
  const deadline = Date.now() + LOCK_WAIT_MS;
1573
- let staleObservedAt: number | undefined;
1574
1845
  while (true) {
1575
1846
  const attemptId = randomUUID();
1576
1847
  const now = Date.now();
@@ -1595,7 +1866,11 @@ export async function acquireManagedLock(
1595
1866
  fs.fsyncSync(fd);
1596
1867
  fsyncDirectory(locksDirectory);
1597
1868
  } catch (error) {
1598
- fs.closeSync(fd);
1869
+ try {
1870
+ retireCreatedManagedLockExact(lockPath, fd);
1871
+ } finally {
1872
+ fs.closeSync(fd);
1873
+ }
1599
1874
  throw error;
1600
1875
  }
1601
1876
  const lockIdentity = fs.fstatSync(fd, { bigint: true });
@@ -1603,12 +1878,8 @@ export async function acquireManagedLock(
1603
1878
  let descriptorClosed = false;
1604
1879
  const closeDescriptor = (): void => {
1605
1880
  if (descriptorClosed) return;
1606
- try {
1607
- secureFileDescriptor(lockPath, fd, "verify");
1608
- } finally {
1609
- fs.closeSync(fd);
1610
- descriptorClosed = true;
1611
- }
1881
+ fs.closeSync(fd);
1882
+ descriptorClosed = true;
1612
1883
  };
1613
1884
  const assertOwned = (): void => {
1614
1885
  const current = parseLock(lockPath);
@@ -1623,8 +1894,7 @@ export async function acquireManagedLock(
1623
1894
  descriptorClosed ||
1624
1895
  !current ||
1625
1896
  !sameFileIdentity(lockIdentity, named) ||
1626
- current.attemptId !== attemptId ||
1627
- current.leaseExpiresAt < Date.now()
1897
+ current.attemptId !== attemptId
1628
1898
  )
1629
1899
  throw new Error("migration_busy");
1630
1900
  };
@@ -1642,42 +1912,160 @@ export async function acquireManagedLock(
1642
1912
  attemptId,
1643
1913
  assertOwned,
1644
1914
  async release(): Promise<void> {
1645
- clearInterval(heartbeat);
1646
- try {
1647
- assertOwned();
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 {
1915
+ const finalize = (): void => {
1916
+ clearInterval(heartbeat);
1654
1917
  released = true;
1655
1918
  closeDescriptor();
1919
+ };
1920
+ try {
1921
+ assertOwned();
1922
+ } catch (error) {
1923
+ finalize();
1924
+ throw error;
1656
1925
  }
1926
+ try {
1927
+ secureFileDescriptor(lockPath, fd, "verify");
1928
+ } catch (error) {
1929
+ if (lockDescriptorOwnershipLost(error)) finalize();
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;
1941
+ }
1942
+ finalize();
1657
1943
  },
1658
1944
  };
1659
1945
  } catch (error) {
1660
1946
  if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
1661
1947
  const owner = parseLock(lockPath);
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`;
1948
+ if (owner && owner.leaseExpiresAt < Date.now() && ownerDefinitelyGone(owner)) {
1949
+ try {
1950
+ retireManagedLockExact(lockPath, { attemptId: owner.attemptId });
1951
+ } catch {
1952
+ /* retry owner observation */
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
+ };
1667
2032
  try {
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 */
2033
+ assertOwned();
2034
+ } catch (error) {
2035
+ finalize();
2036
+ throw error;
1675
2037
  }
1676
- staleObservedAt = undefined;
2038
+ try {
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. */
1677
2065
  }
1678
- } else staleObservedAt = undefined;
2066
+ }
1679
2067
  if (Date.now() >= deadline) throw new Error("migration_busy");
1680
- await new Promise<void>(resolve => setTimeout(resolve, 50));
2068
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.min(50, deadline - Date.now()));
1681
2069
  }
1682
2070
  }
1683
2071
  }