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.
- package/CHANGELOG.md +16 -0
- package/README.md +6 -0
- package/bin/agentlas.cjs +17 -4
- package/engine/acp/server.cjs +7 -2
- package/engine/agentlas-workforce.cjs +1 -0
- package/engine/agentlas.cjs +11 -1
- package/engine/cli-output.cjs +16 -2
- package/engine/cloud-assets/state.cjs +375 -77
- package/engine/commands/build.cjs +32 -4
- package/engine/commands/mcp.cjs +144 -37
- package/engine/experience/intents.cjs +6 -0
- package/engine/hub/install.cjs +280 -28
- package/engine/mcp/inventory.cjs +31 -5
- package/engine/mcp/plan.cjs +5 -0
- package/engine/project/credentials.cjs +197 -27
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
133
|
-
encoding: "utf8",
|
|
134
|
-
mode: 0o600,
|
|
135
|
-
flag: "wx",
|
|
136
|
-
});
|
|
375
|
+
stateWriteLockOwner(lockParentAnchor, lockAnchor, owner);
|
|
137
376
|
acquired = true;
|
|
138
377
|
} catch (error) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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 =
|
|
433
|
+
const cleanup = path.join(
|
|
434
|
+
lockParentAnchor.realpath,
|
|
435
|
+
`${lockName}.done-${process.pid}-${crypto.randomBytes(6).toString("hex")}`,
|
|
436
|
+
);
|
|
151
437
|
try {
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
434
|
-
|
|
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
|
-
|
|
437
|
-
|
|
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
|
-
|
|
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) {
|
|
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) {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|