agentlas 1.0.66 → 1.0.67

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.
@@ -24,7 +24,6 @@ const {
24
24
  CLOUD_PACKAGE_HASH_V1,
25
25
  CLOUD_RESTORE_MARKER_PATH,
26
26
  cloudSlug,
27
- cloudApplyPortableFileMode,
28
27
  cloudFsyncDirectory,
29
28
  normalizeCloudAssetDescriptor,
30
29
  } = require("../hub/install.cjs");
@@ -39,6 +38,265 @@ function waitSync(milliseconds) {
39
38
  Atomics.wait(signal, 0, 0, milliseconds);
40
39
  }
41
40
 
41
+ const STATE_NOFOLLOW = fs.constants.O_NOFOLLOW || 0;
42
+
43
+ function stateSameDirectoryIdentity(left, right) {
44
+ return Boolean(
45
+ left && right && left.isDirectory() && right.isDirectory() &&
46
+ !left.isSymbolicLink() && !right.isSymbolicLink() &&
47
+ left.dev === right.dev && left.ino === right.ino,
48
+ );
49
+ }
50
+
51
+ function stateDirectoryAnchor(target, label, { allowMissing = false, containedBy = null } = {}) {
52
+ let stat;
53
+ try { stat = fs.lstatSync(target); }
54
+ catch (error) {
55
+ if (allowMissing && error && error.code === "ENOENT") return null;
56
+ throw error;
57
+ }
58
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
59
+ throw new Error(`${label} is not a safe managed directory`);
60
+ }
61
+ let realpath;
62
+ try { realpath = fs.realpathSync.native(target); }
63
+ catch (error) { throw new Error(`${label} could not be canonicalized: ${error.message || error}`); }
64
+ if (containedBy && !(
65
+ realpath === containedBy.realpath || realpath.startsWith(`${containedBy.realpath}${path.sep}`)
66
+ )) {
67
+ throw new Error(`${label} escapes its managed root`);
68
+ }
69
+ return { path: target, realpath, dev: stat.dev, ino: stat.ino, stat };
70
+ }
71
+
72
+ function stateAssertDirectoryAnchor(anchor, label, containedBy = null) {
73
+ const current = stateDirectoryAnchor(anchor.path, label, { allowMissing: false, containedBy });
74
+ if (
75
+ !stateSameDirectoryIdentity(anchor.stat || anchor, current.stat || current) ||
76
+ current.realpath !== anchor.realpath
77
+ ) {
78
+ throw new Error(`${label} changed while it was being used`);
79
+ }
80
+ return current;
81
+ }
82
+
83
+ function stateEnsureDirectory(target, label) {
84
+ fs.mkdirSync(target, { recursive: true, mode: 0o700 });
85
+ const anchor = stateDirectoryAnchor(target, label);
86
+ try { fs.chmodSync(target, 0o700); } catch { /* Windows/best-effort */ }
87
+ stateAssertDirectoryAnchor(anchor, label);
88
+ return anchor;
89
+ }
90
+
91
+ function stateSameFileIdentity(left, right) {
92
+ return Boolean(
93
+ left && right && left.isFile() && right.isFile() &&
94
+ !left.isSymbolicLink() && !right.isSymbolicLink() &&
95
+ left.dev === right.dev && left.ino === right.ino,
96
+ );
97
+ }
98
+
99
+ function stateSameFileSnapshot(left, right) {
100
+ return stateSameFileIdentity(left, right) && left.nlink === right.nlink &&
101
+ left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
102
+ }
103
+
104
+ function stateFileSnapshot(file, label, { allowMissing = false, maxBytes = CLOUD_ASSET_STATE_MAX_BYTES, allowHardLinks = false } = {}) {
105
+ let stat;
106
+ try { stat = fs.lstatSync(file); }
107
+ catch (error) {
108
+ if (allowMissing && error && error.code === "ENOENT") return null;
109
+ throw error;
110
+ }
111
+ if (!stat.isFile() || stat.isSymbolicLink() || (!allowHardLinks && stat.nlink !== 1) || stat.size > maxBytes) {
112
+ throw new Error(`${label} is not a bounded private file`);
113
+ }
114
+ return stat;
115
+ }
116
+
117
+ function stateRemoveOwnedFile(file, expected, { allowLinked = false } = {}) {
118
+ try {
119
+ const current = fs.lstatSync(file);
120
+ if (stateSameFileIdentity(current, expected) && (current.nlink === 1 || (allowLinked && current.nlink >= 2))) {
121
+ fs.unlinkSync(file);
122
+ }
123
+ } catch { /* leave unknown successors and recovery artifacts untouched */ }
124
+ }
125
+
126
+ function stateWriteTemp(directory, name, payload, label) {
127
+ stateAssertDirectoryAnchor(directory, `${label} directory`);
128
+ const file = path.join(directory.realpath, name);
129
+ let fd;
130
+ let created;
131
+ try {
132
+ fd = fs.openSync(file, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | STATE_NOFOLLOW, 0o600);
133
+ created = fs.fstatSync(fd);
134
+ if (!created.isFile() || created.isSymbolicLink() || created.nlink !== 1) {
135
+ throw new Error(`${label} temporary file is unsafe`);
136
+ }
137
+ const bytes = Buffer.from(payload, "utf8");
138
+ let offset = 0;
139
+ while (offset < bytes.length) {
140
+ const written = fs.writeSync(fd, bytes, offset, bytes.length - offset, null);
141
+ if (!Number.isInteger(written) || written <= 0) throw new Error(`${label} temporary file write stalled`);
142
+ offset += written;
143
+ }
144
+ try { fs.fchmodSync(fd, 0o600); } catch { /* Windows/best-effort */ }
145
+ fs.fsyncSync(fd);
146
+ const written = fs.fstatSync(fd);
147
+ if (!stateSameFileIdentity(created, written) || written.nlink !== 1 || written.size !== bytes.length) {
148
+ throw new Error(`${label} temporary file changed while writing`);
149
+ }
150
+ return { path: file, stat: written };
151
+ } catch (error) {
152
+ if (created) stateRemoveOwnedFile(file, created);
153
+ throw error;
154
+ } finally {
155
+ if (fd !== undefined) try { fs.closeSync(fd); } catch { /* preserve original failure */ }
156
+ }
157
+ }
158
+
159
+ function stateRestoreBackup(backup, target, expected, label) {
160
+ try {
161
+ if (stateFileSnapshot(target, `${label} successor`, { allowMissing: true })) return false;
162
+ const current = stateFileSnapshot(backup, `${label} backup`);
163
+ if (!current || !stateSameFileIdentity(current, expected) || current.nlink !== 1) return false;
164
+ fs.linkSync(backup, target);
165
+ const restored = stateFileSnapshot(target, `${label} restored`);
166
+ if (!restored || !stateSameFileIdentity(restored, expected) || restored.nlink < 2) return false;
167
+ fs.unlinkSync(backup);
168
+ return true;
169
+ } catch {
170
+ return false;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Publish a state/marker file only after the managed directory and the
176
+ * previously observed target have remained the same. Existing targets are
177
+ * quarantined first, then the new file is linked with no-replace semantics;
178
+ * an unexpected successor is never overwritten.
179
+ */
180
+ function statePublishFile(directory, targetName, temporary, expected, label) {
181
+ stateAssertDirectoryAnchor(directory, `${label} directory`);
182
+ const target = path.join(directory.realpath, targetName);
183
+ const current = stateFileSnapshot(target, `${label} target`, { allowMissing: true });
184
+ if ((expected && (!current || !stateSameFileSnapshot(current, expected))) || (!expected && current)) {
185
+ throw new Error(`${label} changed before publication`);
186
+ }
187
+ let backup = null;
188
+ let linked = false;
189
+ try {
190
+ if (expected) {
191
+ backup = `${target}.previous-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
192
+ fs.renameSync(target, backup);
193
+ const moved = stateFileSnapshot(backup, `${label} backup`);
194
+ if (!moved || !stateSameFileIdentity(moved, expected) || moved.nlink !== 1) {
195
+ stateRestoreBackup(backup, target, expected, label);
196
+ throw new Error(`${label} target changed before publication`);
197
+ }
198
+ stateAssertDirectoryAnchor(directory, `${label} directory`);
199
+ if (stateFileSnapshot(target, `${label} successor`, { allowMissing: true })) {
200
+ throw new Error(`${label} successor appeared during publication`);
201
+ }
202
+ }
203
+ stateAssertDirectoryAnchor(directory, `${label} directory`);
204
+ if (stateFileSnapshot(target, `${label} successor`, { allowMissing: true })) {
205
+ throw new Error(`${label} successor appeared during publication`);
206
+ }
207
+ fs.linkSync(temporary.path, target);
208
+ linked = true;
209
+ const linkedTarget = stateFileSnapshot(target, `${label} target`, { allowHardLinks: true });
210
+ if (!linkedTarget || !stateSameFileIdentity(linkedTarget, temporary.stat) || linkedTarget.nlink < 2) {
211
+ throw new Error(`${label} publication produced an unsafe target`);
212
+ }
213
+ stateAssertDirectoryAnchor(directory, `${label} directory`);
214
+ stateRemoveOwnedFile(temporary.path, temporary.stat, { allowLinked: true });
215
+ const installed = stateFileSnapshot(target, `${label} target`);
216
+ if (!installed || !stateSameFileIdentity(installed, temporary.stat) || installed.nlink !== 1) {
217
+ throw new Error(`${label} identity changed after publication`);
218
+ }
219
+ try { fs.chmodSync(target, 0o600); } catch { /* Windows/best-effort */ }
220
+ const final = stateFileSnapshot(target, `${label} target`);
221
+ if (!final || !stateSameFileIdentity(final, temporary.stat) || final.nlink !== 1 ||
222
+ (process.platform !== "win32" && (final.mode & 0o777) !== 0o600)) {
223
+ throw new Error(`${label} mode or identity changed after publication`);
224
+ }
225
+ stateAssertDirectoryAnchor(directory, `${label} directory`);
226
+ if (backup) {
227
+ const backupStat = stateFileSnapshot(backup, `${label} backup`, { allowMissing: true });
228
+ if (backupStat && stateSameFileIdentity(backupStat, expected) && backupStat.nlink === 1) {
229
+ fs.unlinkSync(backup);
230
+ }
231
+ }
232
+ return target;
233
+ } catch (error) {
234
+ if (linked) stateRemoveOwnedFile(target, temporary.stat);
235
+ stateRemoveOwnedFile(temporary.path, temporary.stat);
236
+ if (backup) stateRestoreBackup(backup, target, expected, label);
237
+ throw error;
238
+ }
239
+ }
240
+
241
+ function stateWriteLockOwner(lockParent, lock, owner) {
242
+ stateAssertDirectoryAnchor(lockParent, "Cloud asset lock parent");
243
+ stateAssertDirectoryAnchor(lock, "Cloud asset lock", lockParent);
244
+ const ownerPath = path.join(lock.realpath, "owner.json");
245
+ let fd;
246
+ let created;
247
+ try {
248
+ fd = fs.openSync(ownerPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | STATE_NOFOLLOW, 0o600);
249
+ created = fs.fstatSync(fd);
250
+ if (!created.isFile() || created.isSymbolicLink() || created.nlink !== 1) {
251
+ throw new Error("lock owner is not a bounded private file");
252
+ }
253
+ const payload = Buffer.from(JSON.stringify(owner) + "\n", "utf8");
254
+ let offset = 0;
255
+ while (offset < payload.length) {
256
+ const written = fs.writeSync(fd, payload, offset, payload.length - offset, null);
257
+ if (!Number.isInteger(written) || written <= 0) throw new Error("lock owner write stalled");
258
+ offset += written;
259
+ }
260
+ try { fs.fchmodSync(fd, 0o600); } catch { /* Windows/best-effort */ }
261
+ fs.fsyncSync(fd);
262
+ const written = fs.fstatSync(fd);
263
+ if (!stateSameFileIdentity(created, written) || written.nlink !== 1 || written.size !== payload.length) {
264
+ throw new Error("lock owner changed while it was written");
265
+ }
266
+ } catch (error) {
267
+ if (created) stateRemoveOwnedFile(ownerPath, created);
268
+ throw error;
269
+ } finally {
270
+ if (fd !== undefined) try { fs.closeSync(fd); } catch { /* preserve original failure */ }
271
+ }
272
+ stateAssertDirectoryAnchor(lockParent, "Cloud asset lock parent");
273
+ stateAssertDirectoryAnchor(lock, "Cloud asset lock", lockParent);
274
+ const written = stateFileSnapshot(ownerPath, "Cloud asset lock owner", { maxBytes: 512 });
275
+ if (!written || !stateSameFileIdentity(written, created) || written.nlink !== 1 || (written.mode & 0o777) !== 0o600) {
276
+ throw new Error("lock owner changed while it was written");
277
+ }
278
+ }
279
+
280
+ function stateRemoveOwnedLockDirectory(directoryPath, expected) {
281
+ let current;
282
+ try { current = stateDirectoryAnchor(directoryPath, "Cloud asset lock cleanup"); }
283
+ catch { return false; }
284
+ if (!stateSameDirectoryIdentity(current.stat, expected.stat || expected)) return false;
285
+ const ownerPath = path.join(current.realpath, "owner.json");
286
+ const owner = stateFileSnapshot(ownerPath, "Cloud asset lock owner", { allowMissing: true, maxBytes: 512 });
287
+ if (owner) {
288
+ try { fs.unlinkSync(ownerPath); } catch { return false; }
289
+ }
290
+ try {
291
+ fs.rmdirSync(current.realpath);
292
+ return true;
293
+ } catch {
294
+ // Never recursively delete a lock directory after its identity is no
295
+ // longer provable; leave the artifact for bounded stale-lock recovery.
296
+ return false;
297
+ }
298
+ }
299
+
42
300
  function processIsAlive(pid) {
43
301
  if (!Number.isSafeInteger(pid) || pid <= 0) return false;
44
302
  try {
@@ -52,13 +310,16 @@ function processIsAlive(pid) {
52
310
  }
53
311
  }
54
312
 
55
- function readLockOwner(lockPath) {
56
- const ownerPath = path.join(lockPath, "owner.json");
313
+ function readLockOwner(lockPath, lockAnchor = null) {
314
+ if (lockAnchor) stateAssertDirectoryAnchor(lockAnchor, "Cloud asset lock");
315
+ const ownerPath = path.join(lockAnchor ? lockAnchor.realpath : lockPath, "owner.json");
57
316
  let fd;
58
317
  try {
318
+ const listed = stateFileSnapshot(ownerPath, "Cloud asset lock owner", { allowMissing: true, maxBytes: 512 });
319
+ if (!listed) return null;
59
320
  fd = fs.openSync(ownerPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
60
321
  const before = fs.fstatSync(fd);
61
- if (!before.isFile() || before.nlink !== 1 || before.size <= 0 || before.size > 512) {
322
+ if (!before.isFile() || before.nlink !== 1 || before.size <= 0 || before.size > 512 || !stateSameFileIdentity(before, listed)) {
62
323
  throw new Error("lock owner is not a bounded private file");
63
324
  }
64
325
  const raw = fs.readFileSync(fd, "utf8");
@@ -90,67 +351,98 @@ function readLockOwner(lockPath) {
90
351
  function withCloudAssetLock(targetPath, label, action) {
91
352
  const lockPath = `${targetPath}.lock`;
92
353
  const lockParent = path.dirname(lockPath);
93
- fs.mkdirSync(lockParent, { recursive: true, mode: 0o700 });
94
- try { fs.chmodSync(lockParent, 0o700); } catch { /* best effort */ }
354
+ const lockParentAnchor = stateEnsureDirectory(lockParent, `${label} lock parent`);
355
+ const lockName = path.basename(lockPath);
356
+ const lockActualPath = path.join(lockParentAnchor.realpath, lockName);
95
357
  const deadline = Date.now() + CLOUD_ASSET_LOCK_WAIT_MS;
96
358
  let acquired = false;
359
+ let lockAnchor = null;
97
360
  while (!acquired) {
361
+ let createdLock = false;
98
362
  try {
99
- fs.mkdirSync(lockPath, { mode: 0o700 });
100
- } catch (error) {
101
- if (!error || (error.code !== "EEXIST" && error.code !== "ENOENT")) throw error;
102
- if (error.code === "ENOENT") continue;
103
- let stat;
104
- try { stat = fs.lstatSync(lockPath); } catch (statError) {
105
- if (statError && statError.code === "ENOENT") continue;
106
- throw statError;
107
- }
108
- if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`${label} lock is unsafe`);
109
- let owner = null;
110
- try { owner = readLockOwner(lockPath); } catch { owner = null; }
111
- if (Date.now() - stat.mtimeMs > CLOUD_ASSET_LOCK_STALE_MS && (!owner || !processIsAlive(owner.pid))) {
112
- const quarantine = `${lockPath}.stale-${process.pid}-${crypto.randomBytes(6).toString("hex")}`;
113
- try {
114
- fs.renameSync(lockPath, quarantine);
115
- fs.rmSync(quarantine, { recursive: true, force: true });
116
- continue;
117
- } catch (reclaimError) {
118
- if (reclaimError && reclaimError.code === "ENOENT") continue;
119
- throw reclaimError;
120
- }
121
- }
122
- if (Date.now() >= deadline) throw new Error(`${label} is busy; retry after the active operation finishes`);
123
- waitSync(25);
124
- continue;
125
- }
126
- try {
363
+ stateAssertDirectoryAnchor(lockParentAnchor, `${label} lock parent`);
364
+ fs.mkdirSync(lockActualPath, { mode: 0o700 });
365
+ createdLock = true;
366
+ lockAnchor = stateDirectoryAnchor(lockActualPath, `${label} lock`, { containedBy: lockParentAnchor });
367
+ try { fs.chmodSync(lockActualPath, 0o700); } catch { /* Windows/best-effort */ }
368
+ stateAssertDirectoryAnchor(lockParentAnchor, `${label} lock parent`);
369
+ stateAssertDirectoryAnchor(lockAnchor, `${label} lock`, lockParentAnchor);
127
370
  const owner = {
128
371
  pid: process.pid,
129
372
  nonce: crypto.randomBytes(16).toString("hex"),
130
373
  createdAt: new Date().toISOString(),
131
374
  };
132
- fs.writeFileSync(path.join(lockPath, "owner.json"), JSON.stringify(owner) + "\n", {
133
- encoding: "utf8",
134
- mode: 0o600,
135
- flag: "wx",
136
- });
375
+ stateWriteLockOwner(lockParentAnchor, lockAnchor, owner);
137
376
  acquired = true;
138
377
  } catch (error) {
139
- const abandoned = `${lockPath}.abandoned-${process.pid}-${crypto.randomBytes(6).toString("hex")}`;
140
- try {
141
- fs.renameSync(lockPath, abandoned);
142
- fs.rmSync(abandoned, { recursive: true, force: true });
143
- } catch { /* original owner-write error remains authoritative */ }
378
+ if (!createdLock && error && (error.code === "EEXIST" || error.code === "ENOENT")) {
379
+ if (error.code === "ENOENT") continue;
380
+ let lock = null;
381
+ try {
382
+ lock = stateDirectoryAnchor(lockActualPath, `${label} lock`, {
383
+ allowMissing: true,
384
+ containedBy: lockParentAnchor,
385
+ });
386
+ } catch (statError) {
387
+ if (statError && statError.code === "ENOENT") continue;
388
+ throw statError;
389
+ }
390
+ if (!lock) continue;
391
+ let owner = null;
392
+ try { owner = readLockOwner(lockActualPath, lock); } catch { owner = null; }
393
+ if (Date.now() - lock.stat.mtimeMs > CLOUD_ASSET_LOCK_STALE_MS && (!owner || !processIsAlive(owner.pid))) {
394
+ stateAssertDirectoryAnchor(lockParentAnchor, `${label} lock parent`);
395
+ stateAssertDirectoryAnchor(lock, `${label} lock`, lockParentAnchor);
396
+ const quarantine = path.join(
397
+ lockParentAnchor.realpath,
398
+ `${lockName}.stale-${process.pid}-${crypto.randomBytes(6).toString("hex")}`,
399
+ );
400
+ try {
401
+ fs.renameSync(lockActualPath, quarantine);
402
+ const moved = stateDirectoryAnchor(quarantine, `${label} stale lock`, { containedBy: lockParentAnchor });
403
+ if (stateSameDirectoryIdentity(moved.stat, lock.stat)) stateRemoveOwnedLockDirectory(quarantine, moved);
404
+ } catch (reclaimError) {
405
+ if (reclaimError && reclaimError.code === "ENOENT") continue;
406
+ throw reclaimError;
407
+ }
408
+ continue;
409
+ }
410
+ if (Date.now() >= deadline) throw new Error(`${label} is busy; retry after the active operation finishes`);
411
+ waitSync(25);
412
+ continue;
413
+ }
414
+ if (lockAnchor && !acquired) {
415
+ const abandoned = path.join(
416
+ lockParentAnchor.realpath,
417
+ `${lockName}.abandoned-${process.pid}-${crypto.randomBytes(6).toString("hex")}`,
418
+ );
419
+ try {
420
+ stateAssertDirectoryAnchor(lockParentAnchor, `${label} lock parent`);
421
+ stateAssertDirectoryAnchor(lockAnchor, `${label} lock`, lockParentAnchor);
422
+ fs.renameSync(lockActualPath, abandoned);
423
+ const moved = stateDirectoryAnchor(abandoned, `${label} abandoned lock`, { containedBy: lockParentAnchor });
424
+ if (stateSameDirectoryIdentity(moved.stat, lockAnchor.stat)) stateRemoveOwnedLockDirectory(abandoned, moved);
425
+ } catch { /* original owner-write error remains authoritative */ }
426
+ }
144
427
  throw error;
145
428
  }
146
429
  }
147
430
  try {
148
431
  return action();
149
432
  } finally {
150
- const cleanup = `${lockPath}.done-${process.pid}-${crypto.randomBytes(6).toString("hex")}`;
433
+ const cleanup = path.join(
434
+ lockParentAnchor.realpath,
435
+ `${lockName}.done-${process.pid}-${crypto.randomBytes(6).toString("hex")}`,
436
+ );
151
437
  try {
152
- fs.renameSync(lockPath, cleanup);
153
- fs.rmSync(cleanup, { recursive: true, force: true });
438
+ stateAssertDirectoryAnchor(lockParentAnchor, `${label} lock parent`);
439
+ stateAssertDirectoryAnchor(lockAnchor, `${label} lock`, lockParentAnchor);
440
+ fs.renameSync(lockActualPath, cleanup);
441
+ const moved = stateDirectoryAnchor(cleanup, `${label} completed lock`, { containedBy: lockParentAnchor });
442
+ if (!stateSameDirectoryIdentity(moved.stat, lockAnchor.stat)) {
443
+ throw new Error(`${label} lock changed while it was being released`);
444
+ }
445
+ stateRemoveOwnedLockDirectory(cleanup, moved);
154
446
  } catch (error) {
155
447
  if (!error || error.code !== "ENOENT") throw error;
156
448
  }
@@ -244,23 +536,25 @@ function readCloudAssetState() {
244
536
  function writeCloudAssetStateUnlocked(state) {
245
537
  const statePath = cloudAssetStatePath();
246
538
  const normalized = normalizeCloudAssetState(state);
247
- fs.mkdirSync(path.dirname(statePath), { recursive: true, mode: 0o700 });
248
- try { fs.chmodSync(path.dirname(statePath), 0o700); } catch { /* best effort */ }
249
- const temp = `${statePath}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
250
- let fd;
539
+ const directory = stateEnsureDirectory(path.dirname(statePath), "Cloud asset state directory");
540
+ const targetName = path.basename(statePath);
541
+ const target = path.join(directory.realpath, targetName);
542
+ const expected = stateFileSnapshot(target, "Cloud asset state", { allowMissing: true });
543
+ const payload = JSON.stringify(normalized, null, 2) + "\n";
544
+ if (Buffer.byteLength(payload, "utf8") > CLOUD_ASSET_STATE_MAX_BYTES) {
545
+ throw new Error("Cloud asset state exceeds its safety limit");
546
+ }
547
+ const temporary = stateWriteTemp(
548
+ directory,
549
+ `.${targetName}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`,
550
+ payload,
551
+ "Cloud asset state",
552
+ );
251
553
  try {
252
- try {
253
- fd = fs.openSync(temp, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
254
- fs.writeFileSync(fd, JSON.stringify(normalized, null, 2) + "\n", "utf8");
255
- fs.fsyncSync(fd);
256
- } finally {
257
- if (fd !== undefined) try { fs.closeSync(fd); } catch { /* best effort */ }
258
- }
259
- fs.renameSync(temp, statePath);
260
- cloudApplyPortableFileMode(statePath, 0o600);
261
- cloudFsyncDirectory(path.dirname(statePath));
554
+ statePublishFile(directory, targetName, temporary, expected, "Cloud asset state");
555
+ cloudFsyncDirectory(directory.realpath);
262
556
  } finally {
263
- try { fs.rmSync(temp, { force: true }); } catch { /* best effort */ }
557
+ stateRemoveOwnedFile(temporary.path, temporary.stat);
264
558
  }
265
559
  return normalized;
266
560
  }
@@ -398,14 +692,22 @@ function cloudBaseDescriptorForSource(marker, rootPath, slug, scope) {
398
692
  }
399
693
 
400
694
  function writeCloudSourceMarker(rootPath, scan, descriptor, options = {}) {
401
- const markerPath = path.join(rootPath, CLOUD_RESTORE_MARKER_PATH);
402
695
  const lockTarget = path.join(
403
696
  userDataDir(),
404
697
  "cloud-source-marker-locks",
405
698
  crypto.createHash("sha256").update(path.resolve(rootPath)).digest("hex"),
406
699
  );
407
700
  return withCloudAssetLock(lockTarget, "Agent Cloud source marker", () => {
408
- const previousMarker = readCloudSourceMarker(rootPath);
701
+ const rootAnchor = stateDirectoryAnchor(rootPath, "Cloud source marker root");
702
+ const markerPath = path.join(rootAnchor.realpath, CLOUD_RESTORE_MARKER_PATH);
703
+ const expectedMarker = stateFileSnapshot(markerPath, "Cloud source marker", { allowMissing: true });
704
+ const previousMarker = readCloudSourceMarker(rootAnchor.realpath);
705
+ stateAssertDirectoryAnchor(rootAnchor, "Cloud source marker root");
706
+ const currentMarker = stateFileSnapshot(markerPath, "Cloud source marker", { allowMissing: true });
707
+ if ((expectedMarker && (!currentMarker || !stateSameFileSnapshot(currentMarker, expectedMarker))) ||
708
+ (!expectedMarker && currentMarker)) {
709
+ throw new Error("Cloud source marker changed while it was read");
710
+ }
409
711
  const descriptors = cloudMarkerDescriptors(previousMarker);
410
712
  if (descriptor) descriptors[descriptor.scope] = descriptor;
411
713
  if (options.removeDescriptor) {
@@ -430,21 +732,17 @@ function writeCloudSourceMarker(rootPath, scan, descriptor, options = {}) {
430
732
  savedAt: new Date().toISOString(),
431
733
  };
432
734
  for (const key of Object.keys(marker)) if (marker[key] === undefined) delete marker[key];
433
- const temp = path.join(rootPath, `.${CLOUD_RESTORE_MARKER_PATH}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`);
434
- let fd;
735
+ const temporary = stateWriteTemp(
736
+ rootAnchor,
737
+ `.${CLOUD_RESTORE_MARKER_PATH}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`,
738
+ JSON.stringify(marker, null, 2) + "\n",
739
+ "Cloud source marker",
740
+ );
435
741
  try {
436
- try {
437
- fd = fs.openSync(temp, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
438
- fs.writeFileSync(fd, JSON.stringify(marker, null, 2) + "\n", "utf8");
439
- fs.fsyncSync(fd);
440
- } finally {
441
- if (fd !== undefined) try { fs.closeSync(fd); } catch { /* best effort */ }
442
- }
443
- fs.renameSync(temp, markerPath);
444
- cloudApplyPortableFileMode(markerPath, 0o600);
445
- cloudFsyncDirectory(rootPath);
742
+ statePublishFile(rootAnchor, CLOUD_RESTORE_MARKER_PATH, temporary, expectedMarker, "Cloud source marker");
743
+ cloudFsyncDirectory(rootAnchor.realpath);
446
744
  } finally {
447
- try { fs.rmSync(temp, { force: true }); } catch { /* best effort */ }
745
+ stateRemoveOwnedFile(temporary.path, temporary.stat);
448
746
  }
449
747
  return marker;
450
748
  });
@@ -27,6 +27,12 @@ function usage(ko) {
27
27
  : "Usage: agentlas build \"<the agent you want>\" [--runtime <kind>] [--print] [-- <request starting like an option>]";
28
28
  }
29
29
 
30
+ function buildError(message, code = "INVALID_ARGUMENT") {
31
+ const error = new Error(message);
32
+ error.code = code;
33
+ return error;
34
+ }
35
+
30
36
  function parseArgs(args) {
31
37
  const flags = { runtime: null, print: false };
32
38
  const rest = [];
@@ -67,10 +73,22 @@ async function run(ctx, args) {
67
73
 
68
74
  let parsed;
69
75
  try { parsed = parseArgs(args); }
70
- catch (error) { ctx.err(String((error && error.message) || error)); return 1; }
76
+ catch (error) {
77
+ const typed = error && typeof error === "object" && typeof error.code === "string"
78
+ ? error
79
+ : buildError(String((error && error.message) || error));
80
+ if (typeof ctx.fail === "function") ctx.fail(typed);
81
+ else ctx.err(String(typed.message || typed));
82
+ return 1;
83
+ }
71
84
  const { flags, rest } = parsed;
72
85
  const request = rest.join(" ").trim();
73
- if (!request) { ctx.err("✖ " + usage(ko)); return 1; }
86
+ if (!request) {
87
+ const error = buildError("✖ " + usage(ko));
88
+ if (typeof ctx.fail === "function") ctx.fail(error);
89
+ else ctx.err(error.message);
90
+ return 1;
91
+ }
74
92
 
75
93
  const db = ctx.db();
76
94
  const cwd = projectCwd();
@@ -83,7 +101,12 @@ async function run(ctx, args) {
83
101
  });
84
102
  } catch (e) {
85
103
  // no_runtime 등 정직 정지 그대로.
86
- ctx.err(String((e && e.message) || e));
104
+ const error = e && typeof e === "object" && typeof e.message === "string"
105
+ ? e
106
+ : buildError(String(e), "RUNTIME_RESOLUTION_FAILED");
107
+ if (typeof error.code !== "string") error.code = "RUNTIME_RESOLUTION_FAILED";
108
+ if (typeof ctx.fail === "function") ctx.fail(error);
109
+ else ctx.err(error.message);
87
110
  return 1;
88
111
  }
89
112
 
@@ -107,7 +130,12 @@ async function run(ctx, args) {
107
130
  if (flags.print && finalText) process.stdout.write(finalText.trimEnd() + "\n");
108
131
 
109
132
  if (session.status === "failed") {
110
- if (session.lastError) ctx.err(session.lastError);
133
+ const error = session.lastError && typeof session.lastError === "object" && typeof session.lastError.message === "string"
134
+ ? session.lastError
135
+ : buildError(String(session.lastError || (ko ? "빌드 실행에 실패했습니다." : "The build session failed.")), "BUILD_FAILED");
136
+ if (typeof error.code !== "string") error.code = "BUILD_FAILED";
137
+ if (typeof ctx.fail === "function") ctx.fail(error);
138
+ else ctx.err(error.message);
111
139
  return 1;
112
140
  }
113
141