@aopslabs/aops 0.3.16 → 0.3.17

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 (38) hide show
  1. package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/user-guides/chatv3.md +16 -1
  2. package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/manifest.json +5 -5
  3. package/agent-assets-release/agent-assets/inventory.json +5 -5
  4. package/agent-assets-release/agent-assets/projection.json +1 -1
  5. package/agent-assets-release/agent-assets-release.json +36 -36
  6. package/agent-assets-release/agent-assets-release.sigstore.json +26 -29
  7. package/assets/agent-assets/core/user-guides/chatv3.md +16 -1
  8. package/dist/commands/chatv3.js +297 -8
  9. package/dist/commands/community-server.js +19 -0
  10. package/dist/commands/global-update.js +17 -1
  11. package/dist/lib/agent-assets/native-fs.js +2 -2
  12. package/dist/lib/cli-error-format.js +13 -0
  13. package/dist/lib/community-native-lifecycle.js +1 -1
  14. package/dist/main.js +2 -1
  15. package/dist/utils/chatv3-session-store.js +476 -47
  16. package/native/manifest.json +1 -1
  17. package/native/qualifications/win32-x64.json +1 -1
  18. package/package.json +5 -5
  19. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/SKILL.md +0 -0
  20. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/agentspace/SKILL.md +0 -0
  21. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/aops-cli-core/SKILL.md +0 -0
  22. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/chatv3/SKILL.md +0 -0
  23. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/collaborative-work/SKILL.md +0 -0
  24. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/discuss/SKILL.md +0 -0
  25. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/docman/SKILL.md +0 -0
  26. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/mission/SKILL.md +0 -0
  27. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/projectman/SKILL.md +0 -0
  28. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/sys/SKILL.md +0 -0
  29. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/view/SKILL.md +0 -0
  30. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/working-disciplines/SKILL.md +0 -0
  31. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/user-guides/agent-assets.md +0 -0
  32. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/user-guides/agentspace.md +0 -0
  33. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/user-guides/aops-cli.md +0 -0
  34. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/user-guides/aops-system.md +0 -0
  35. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/user-guides/docman.md +0 -0
  36. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/user-guides/projectman.md +0 -0
  37. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/user-guides/sys.md +0 -0
  38. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/user-guides/working-disciplines.md +0 -0
@@ -1,10 +1,32 @@
1
1
  import crypto from 'node:crypto';
2
2
  import fs from 'node:fs/promises';
3
3
  import fsSync from 'node:fs';
4
+ import net from 'node:net';
4
5
  import os from 'node:os';
5
6
  import path from 'node:path';
6
7
  import { getAopsCliConfigFilePath } from './config.js';
7
8
  import { decryptWithPassword, encryptWithPassword, } from './password-crypto.js';
9
+ export class Chatv3SessionCryptoError extends Error {
10
+ storePath;
11
+ secretPath;
12
+ sessionId;
13
+ field;
14
+ constructor(args) {
15
+ super([
16
+ `ChatV3 session "${args.sessionId}" could not be decrypted (field: ${args.field}).`,
17
+ `Session store: ${args.storePath}`,
18
+ `At-rest key: ${args.secretPath}`,
19
+ 'The record was encrypted with a different at-rest key than the one on disk',
20
+ '(older CLI versions could overwrite session.key during a concurrent first save).',
21
+ 'Repair: delete the saved session with "aops chat session forget --session <id>" and re-join the channel with the same handle.',
22
+ ].join(' '), { cause: args.cause instanceof Error ? args.cause : undefined });
23
+ this.name = 'Chatv3SessionCryptoError';
24
+ this.storePath = args.storePath;
25
+ this.secretPath = args.secretPath;
26
+ this.sessionId = args.sessionId;
27
+ this.field = args.field;
28
+ }
29
+ }
8
30
  const STORE_FILENAME = 'chatv3-sessions.json';
9
31
  const SECRET_FILENAME = 'chatv3-session.key';
10
32
  const OWNER_STORE_DIRECTORY = 'chatv3';
@@ -102,34 +124,390 @@ async function readStore(storePath, owner) {
102
124
  throw error;
103
125
  }
104
126
  }
127
+ function sleepMs(ms) {
128
+ return new Promise((resolve) => setTimeout(resolve, ms));
129
+ }
130
+ const STORE_RENAME_RETRY_ATTEMPTS = 12;
105
131
  async function writeStore(storePath, store) {
106
132
  await ensureDir(storePath);
107
- await fs.writeFile(storePath, `${JSON.stringify(store, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
133
+ const tempPath = `${storePath}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
134
+ await fs.writeFile(tempPath, `${JSON.stringify(store, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
135
+ let lastError;
136
+ for (let attempt = 0; attempt < STORE_RENAME_RETRY_ATTEMPTS; attempt += 1) {
137
+ try {
138
+ await fs.rename(tempPath, storePath);
139
+ lastError = undefined;
140
+ break;
141
+ }
142
+ catch (error) {
143
+ const code = error.code;
144
+ lastError = error;
145
+ if (code !== 'EPERM' && code !== 'EACCES' && code !== 'EBUSY')
146
+ break;
147
+ await sleepMs(15 + attempt * 15);
148
+ }
149
+ }
150
+ if (lastError) {
151
+ await fs.rm(tempPath, { force: true }).catch(() => undefined);
152
+ throw lastError;
153
+ }
108
154
  await enforceOwnerOnlyFileMode(storePath);
109
155
  }
156
+ const STORE_LOCK_STALE_MS = 15_000;
157
+ const STORE_LOCK_LIVE_TAKEOVER_MS = 600_000;
158
+ const STORE_LOCK_TIMEOUT_MS = 20_000;
159
+ function isProcessAlive(pid) {
160
+ if (!Number.isInteger(pid) || pid <= 0)
161
+ return false;
162
+ try {
163
+ process.kill(pid, 0);
164
+ return true;
165
+ }
166
+ catch (error) {
167
+ return error.code === 'EPERM';
168
+ }
169
+ }
170
+ function parseStoreLockHolder(raw) {
171
+ if (raw === undefined)
172
+ return undefined;
173
+ try {
174
+ return JSON.parse(raw);
175
+ }
176
+ catch {
177
+ return undefined;
178
+ }
179
+ }
180
+ async function restoreForeignLock(claimPath, lockPath) {
181
+ for (let attempt = 0; attempt < 5; attempt += 1) {
182
+ try {
183
+ await fs.link(claimPath, lockPath);
184
+ await fs.rm(claimPath, { force: true }).catch(() => undefined);
185
+ return;
186
+ }
187
+ catch (error) {
188
+ const code = error.code;
189
+ if (code === 'EEXIST')
190
+ return;
191
+ if (!isTransientFsError(error))
192
+ return;
193
+ await sleepMs(10 + attempt * 10);
194
+ }
195
+ }
196
+ }
197
+ async function reapStaleLock(lockPath, expectedRaw) {
198
+ const claimPath = `${lockPath}.reap-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
199
+ try {
200
+ await fs.rename(lockPath, claimPath);
201
+ }
202
+ catch (error) {
203
+ if (error.code === 'ENOENT' || isTransientFsError(error))
204
+ return;
205
+ throw error;
206
+ }
207
+ const claimedRaw = await fs.readFile(claimPath, 'utf8').catch(() => undefined);
208
+ if (expectedRaw !== undefined && claimedRaw !== undefined && claimedRaw !== expectedRaw) {
209
+ await restoreForeignLock(claimPath, lockPath);
210
+ return;
211
+ }
212
+ await fs.rm(claimPath, { force: true }).catch(() => undefined);
213
+ }
214
+ async function releaseOwnedStoreLock(lockPath, token) {
215
+ const claimPath = `${lockPath}.rel-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
216
+ try {
217
+ await fs.rename(lockPath, claimPath);
218
+ }
219
+ catch {
220
+ return;
221
+ }
222
+ const holder = parseStoreLockHolder(await fs.readFile(claimPath, 'utf8').catch(() => undefined));
223
+ if (holder?.token === token) {
224
+ await fs.rm(claimPath, { force: true }).catch(() => undefined);
225
+ return;
226
+ }
227
+ await restoreForeignLock(claimPath, lockPath);
228
+ }
229
+ const UV_FS_O_EXLOCK_WIN32 = 0x10000000;
230
+ const DARWIN_O_EXLOCK = 0x20;
231
+ const DARWIN_O_NONBLOCK = 0x0004;
232
+ const EXCLUSIVE_LOCK_CONTENTION_CODES = new Set(['EBUSY', 'EAGAIN', 'EWOULDBLOCK', 'EPERM', 'EACCES', 'ETXTBSY']);
233
+ export function getChatv3SessionStoreLockStrategy() {
234
+ if (process.platform === 'win32')
235
+ return 'kernel-exclusive-handle';
236
+ if (process.platform === 'darwin')
237
+ return 'kernel-flock-handle';
238
+ if (process.platform === 'linux')
239
+ return 'kernel-abstract-socket';
240
+ return 'token-file';
241
+ }
242
+ async function acquireAbstractSocketLock(storePath, timeoutMs) {
243
+ const name = `\0aops-chatv3-store-lock-${crypto
244
+ .createHash('sha256')
245
+ .update(path.resolve(storePath))
246
+ .digest('hex')
247
+ .slice(0, 32)}`;
248
+ const deadline = Date.now() + timeoutMs;
249
+ let delay = 10;
250
+ for (;;) {
251
+ try {
252
+ const server = await new Promise((resolve, reject) => {
253
+ const candidate = net.createServer();
254
+ candidate.once('error', reject);
255
+ candidate.listen({ path: name }, () => {
256
+ candidate.removeListener('error', reject);
257
+ resolve(candidate);
258
+ });
259
+ });
260
+ let released = false;
261
+ return async () => {
262
+ if (released)
263
+ return;
264
+ released = true;
265
+ await new Promise((resolve) => server.close(() => resolve()));
266
+ };
267
+ }
268
+ catch (error) {
269
+ const code = String(error.code);
270
+ if (code !== 'EADDRINUSE')
271
+ throw error;
272
+ if (Date.now() > deadline) {
273
+ throw new Error(`ChatV3 session store lock timeout: the abstract-socket lock for ${storePath} is held by a live process (kernel lock); retry after it finishes.`);
274
+ }
275
+ await sleepMs(delay + Math.floor(Math.random() * 10));
276
+ delay = Math.min(delay * 2, 250);
277
+ }
278
+ }
279
+ }
280
+ function exclusiveOpenFlags() {
281
+ if (process.platform === 'win32') {
282
+ return fsSync.constants.O_RDWR | fsSync.constants.O_CREAT | UV_FS_O_EXLOCK_WIN32;
283
+ }
284
+ const exlock = fsSync.constants.O_EXLOCK ?? DARWIN_O_EXLOCK;
285
+ const nonblock = fsSync.constants.O_NONBLOCK ?? DARWIN_O_NONBLOCK;
286
+ return fsSync.constants.O_RDWR | fsSync.constants.O_CREAT | exlock | nonblock;
287
+ }
288
+ async function acquireExclusiveHandleLock(lockPath, timeoutMs) {
289
+ const deadline = Date.now() + timeoutMs;
290
+ let delay = 10;
291
+ for (;;) {
292
+ try {
293
+ const handle = await fs.open(lockPath, exclusiveOpenFlags(), 0o600);
294
+ await handle.truncate(0).catch(() => undefined);
295
+ await handle
296
+ .write(`${JSON.stringify({ pid: process.pid, at: Date.now() })}\n`)
297
+ .catch(() => undefined);
298
+ let released = false;
299
+ return async () => {
300
+ if (released)
301
+ return;
302
+ released = true;
303
+ await handle.close().catch(() => undefined);
304
+ };
305
+ }
306
+ catch (error) {
307
+ const code = String(error.code);
308
+ if (!EXCLUSIVE_LOCK_CONTENTION_CODES.has(code))
309
+ throw error;
310
+ if (Date.now() > deadline) {
311
+ const holderRaw = await fs.readFile(lockPath, 'utf8').catch(() => undefined);
312
+ const holder = parseStoreLockHolder(holderRaw);
313
+ throw new Error(`ChatV3 session store lock timeout: ${lockPath} is exclusively held by a live process${holder?.pid ? ` (pid ${holder.pid})` : ''} (kernel lock); retry after it finishes.`);
314
+ }
315
+ await sleepMs(delay + Math.floor(Math.random() * 10));
316
+ delay = Math.min(delay * 2, 250);
317
+ }
318
+ }
319
+ }
320
+ export async function acquireChatv3SessionStoreLock(storePath, options = {}) {
321
+ const timeoutMs = options.timeoutMs ?? STORE_LOCK_TIMEOUT_MS;
322
+ const staleMs = options.staleMs ?? STORE_LOCK_STALE_MS;
323
+ const liveTakeoverMs = options.liveTakeoverMs ?? STORE_LOCK_LIVE_TAKEOVER_MS;
324
+ const strategy = getChatv3SessionStoreLockStrategy();
325
+ if (strategy === 'kernel-abstract-socket') {
326
+ return acquireAbstractSocketLock(storePath, timeoutMs);
327
+ }
328
+ const lockPath = `${storePath}.lock`;
329
+ await ensureDir(lockPath);
330
+ if (strategy !== 'token-file') {
331
+ return acquireExclusiveHandleLock(lockPath, timeoutMs);
332
+ }
333
+ const token = crypto.randomUUID();
334
+ const deadline = Date.now() + timeoutMs;
335
+ let delay = 10;
336
+ for (;;) {
337
+ const payload = `${JSON.stringify({ token, pid: process.pid, at: Date.now() })}\n`;
338
+ try {
339
+ await fs.writeFile(lockPath, payload, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
340
+ return async () => releaseOwnedStoreLock(lockPath, token);
341
+ }
342
+ catch (error) {
343
+ const code = error.code;
344
+ if (code !== 'EEXIST') {
345
+ if (!isTransientFsError(error))
346
+ throw error;
347
+ if (Date.now() > deadline) {
348
+ throw new Error(`ChatV3 session store lock timeout: ${lockPath} could not be opened (${code}); delete the file if its holder is gone.`);
349
+ }
350
+ await sleepMs(delay + Math.floor(Math.random() * 10));
351
+ delay = Math.min(delay * 2, 250);
352
+ continue;
353
+ }
354
+ }
355
+ let holderRaw;
356
+ let holderReadExhausted = false;
357
+ try {
358
+ holderRaw = await readFileWithTransientRetry(lockPath);
359
+ }
360
+ catch (error) {
361
+ if (!isTransientFsError(error))
362
+ throw error;
363
+ holderReadExhausted = true;
364
+ }
365
+ if (holderReadExhausted) {
366
+ if (Date.now() > deadline) {
367
+ throw new Error(`ChatV3 session store lock timeout: ${lockPath} could not be read; delete the file if its holder is gone.`);
368
+ }
369
+ await sleepMs(delay + Math.floor(Math.random() * 10));
370
+ delay = Math.min(delay * 2, 250);
371
+ continue;
372
+ }
373
+ if (holderRaw === undefined)
374
+ continue;
375
+ const holder = parseStoreLockHolder(holderRaw);
376
+ let reapEligible;
377
+ if (!holder) {
378
+ const stats = await fs.stat(lockPath).catch(() => null);
379
+ reapEligible = !stats || Date.now() - stats.mtimeMs > staleMs;
380
+ }
381
+ else {
382
+ const age = Date.now() - (typeof holder.at === 'number' ? holder.at : 0);
383
+ reapEligible = isProcessAlive(holder.pid ?? -1) ? age > liveTakeoverMs : true;
384
+ }
385
+ if (reapEligible) {
386
+ await reapStaleLock(lockPath, holderRaw);
387
+ continue;
388
+ }
389
+ if (Date.now() > deadline) {
390
+ throw new Error(`ChatV3 session store lock timeout: ${lockPath} is held by pid ${holder?.pid ?? 'unknown'}; delete the file if that process is gone.`);
391
+ }
392
+ await sleepMs(delay + Math.floor(Math.random() * 10));
393
+ delay = Math.min(delay * 2, 250);
394
+ }
395
+ }
396
+ const storeWriteLocks = new Map();
397
+ async function withStoreLock(storePath, run) {
398
+ const key = path.resolve(storePath);
399
+ const task = async () => {
400
+ const release = await acquireChatv3SessionStoreLock(storePath);
401
+ try {
402
+ return await run();
403
+ }
404
+ finally {
405
+ await release();
406
+ }
407
+ };
408
+ const previous = storeWriteLocks.get(key) ?? Promise.resolve();
409
+ const next = previous.then(task, task);
410
+ storeWriteLocks.set(key, next.then(() => undefined, () => undefined));
411
+ return next;
412
+ }
413
+ const TRANSIENT_FS_CODES = new Set(['EPERM', 'EACCES', 'EBUSY']);
414
+ function isTransientFsError(error) {
415
+ return TRANSIENT_FS_CODES.has(String(error?.code));
416
+ }
417
+ async function readFileWithTransientRetry(filePath) {
418
+ for (let attempt = 0;; attempt += 1) {
419
+ try {
420
+ return await fs.readFile(filePath, 'utf8');
421
+ }
422
+ catch (error) {
423
+ const code = error.code;
424
+ if (code === 'ENOENT')
425
+ return undefined;
426
+ if (!isTransientFsError(error) || attempt >= 8)
427
+ throw error;
428
+ await sleepMs(10 + attempt * 10);
429
+ }
430
+ }
431
+ }
432
+ async function readExistingSessionSecret(secretPath) {
433
+ const raw = await readFileWithTransientRetry(secretPath);
434
+ const secret = raw === undefined ? undefined : normalizeNonEmpty(raw);
435
+ if (secret) {
436
+ await enforceOwnerOnlyFileMode(secretPath);
437
+ return secret;
438
+ }
439
+ return undefined;
440
+ }
441
+ async function mintSessionSecret(secretPath) {
442
+ const secret = crypto.randomBytes(32).toString('base64');
443
+ await ensureDir(secretPath);
444
+ const tempPath = `${secretPath}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
445
+ await fs.writeFile(tempPath, `${secret}\n`, { encoding: 'utf8', mode: 0o600 });
446
+ try {
447
+ for (let attempt = 0;; attempt += 1) {
448
+ try {
449
+ await fs.link(tempPath, secretPath);
450
+ await enforceOwnerOnlyFileMode(secretPath);
451
+ return secret;
452
+ }
453
+ catch (error) {
454
+ const code = error.code;
455
+ if (code !== 'EEXIST') {
456
+ if (isTransientFsError(error) && attempt < 20) {
457
+ await sleepMs(10 + attempt * 10);
458
+ continue;
459
+ }
460
+ throw error;
461
+ }
462
+ }
463
+ const existing = await readExistingSessionSecret(secretPath);
464
+ if (existing)
465
+ return existing;
466
+ const claimPath = `${secretPath}.bad-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
467
+ try {
468
+ await fs.rename(secretPath, claimPath);
469
+ }
470
+ catch (error) {
471
+ const code = error.code;
472
+ if (code !== 'ENOENT' && !isTransientFsError(error))
473
+ throw error;
474
+ await sleepMs(10);
475
+ continue;
476
+ }
477
+ const claimedRaw = await fs.readFile(claimPath, 'utf8').catch(() => undefined);
478
+ if (claimedRaw !== undefined && normalizeNonEmpty(claimedRaw)) {
479
+ await restoreForeignLock(claimPath, secretPath);
480
+ const adopted = await readExistingSessionSecret(secretPath);
481
+ if (adopted)
482
+ return adopted;
483
+ continue;
484
+ }
485
+ await fs.rm(claimPath, { force: true }).catch(() => undefined);
486
+ }
487
+ }
488
+ finally {
489
+ await fs.rm(tempPath, { force: true }).catch(() => undefined);
490
+ }
491
+ }
492
+ const pendingSecretResolutions = new Map();
110
493
  async function resolveSessionSecret(secretPath) {
111
494
  const envSecret = normalizeNonEmpty(process.env.AOPS_CHATV3_SESSION_SECRET) ??
112
495
  normalizeNonEmpty(process.env.AOPS_MCP_TOKEN_SECRET) ??
113
496
  normalizeNonEmpty(process.env.AOPS_MCP_CONFIG_SECRET);
114
497
  if (envSecret)
115
498
  return envSecret;
499
+ const cacheKey = path.resolve(secretPath);
500
+ const pending = pendingSecretResolutions.get(cacheKey);
501
+ if (pending)
502
+ return pending;
503
+ const resolution = (async () => (await readExistingSessionSecret(secretPath)) ?? mintSessionSecret(secretPath))();
504
+ pendingSecretResolutions.set(cacheKey, resolution);
116
505
  try {
117
- const raw = await fs.readFile(secretPath, 'utf8');
118
- const secret = normalizeNonEmpty(raw);
119
- if (secret) {
120
- await enforceOwnerOnlyFileMode(secretPath);
121
- return secret;
122
- }
506
+ return await resolution;
123
507
  }
124
- catch (error) {
125
- if (error.code !== 'ENOENT')
126
- throw error;
508
+ finally {
509
+ pendingSecretResolutions.delete(cacheKey);
127
510
  }
128
- const secret = crypto.randomBytes(32).toString('base64');
129
- await ensureDir(secretPath);
130
- await fs.writeFile(secretPath, `${secret}\n`, { encoding: 'utf8', mode: 0o600 });
131
- await enforceOwnerOnlyFileMode(secretPath);
132
- return secret;
133
511
  }
134
512
  function encodeEncryptedPayload(payload) {
135
513
  return Buffer.from(JSON.stringify(payload), 'utf8').toString('base64');
@@ -154,34 +532,44 @@ async function encryptEpochKeys(keys, secretPath) {
154
532
  }
155
533
  async function decryptEpochKeys(keys, secretPath) {
156
534
  if (!keys || keys.length === 0)
157
- return undefined;
158
- return Promise.all(keys.map(async ({ rawEpochKeyEnc, ...rest }) => ({
159
- ...rest,
160
- rawEpochKey: await decryptSecret(rawEpochKeyEnc, secretPath),
161
- })));
535
+ return { keys: undefined, stale: [] };
536
+ const decrypted = [];
537
+ const stale = [];
538
+ for (const { rawEpochKeyEnc, ...rest } of keys) {
539
+ try {
540
+ decrypted.push({ ...rest, rawEpochKey: await decryptSecret(rawEpochKeyEnc, secretPath) });
541
+ }
542
+ catch {
543
+ stale.push({ roomId: rest.roomId, epoch: rest.epoch });
544
+ }
545
+ }
546
+ return { keys: decrypted.length > 0 ? decrypted : undefined, stale };
162
547
  }
163
548
  export async function saveChatv3Session(input, secrets, options = {}) {
164
549
  const owner = resolveChatv3SessionOwner(options.owner);
165
550
  const storePath = resolveChatv3SessionStorePath(options.storePath, options.owner);
166
551
  const secretPath = resolveChatv3SessionSecretPath(storePath);
167
- const store = await readStore(storePath, owner);
168
- const existing = store.sessions[input.id];
169
- const now = new Date().toISOString();
170
- const wrapSecretEnc = secrets.wrapSecret ? await encryptSecret(secrets.wrapSecret, secretPath) : undefined;
171
- const epochKeysEnc = await encryptEpochKeys(secrets.epochKeys, secretPath);
172
- const record = {
173
- version: 1,
174
- ...input,
175
- rooms: input.rooms,
176
- createdAt: input.createdAt ?? existing?.createdAt ?? now,
177
- updatedAt: input.updatedAt ?? now,
178
- memberTokenEnc: await encryptSecret(secrets.memberToken, secretPath),
179
- ...(wrapSecretEnc ? { wrapSecretEnc } : {}),
180
- ...(epochKeysEnc ? { epochKeysEnc } : {}),
181
- };
182
- store.sessions[input.id] = record;
183
- await writeStore(storePath, store);
184
- return record;
552
+ return withStoreLock(storePath, async () => {
553
+ const wrapSecretEnc = secrets.wrapSecret ? await encryptSecret(secrets.wrapSecret, secretPath) : undefined;
554
+ const epochKeysEnc = await encryptEpochKeys(secrets.epochKeys, secretPath);
555
+ const memberTokenEnc = await encryptSecret(secrets.memberToken, secretPath);
556
+ const store = await readStore(storePath, owner);
557
+ const existing = store.sessions[input.id];
558
+ const now = new Date().toISOString();
559
+ const record = {
560
+ version: 1,
561
+ ...input,
562
+ rooms: input.rooms,
563
+ createdAt: input.createdAt ?? existing?.createdAt ?? now,
564
+ updatedAt: input.updatedAt ?? now,
565
+ memberTokenEnc,
566
+ ...(wrapSecretEnc ? { wrapSecretEnc } : {}),
567
+ ...(epochKeysEnc ? { epochKeysEnc } : {}),
568
+ };
569
+ store.sessions[input.id] = record;
570
+ await writeStore(storePath, store);
571
+ return record;
572
+ });
185
573
  }
186
574
  async function readLegacySessionRecord(id) {
187
575
  const legacyStorePath = resolveLegacyChatv3SessionStorePath();
@@ -228,7 +616,7 @@ async function claimLegacySession(id, owner, options) {
228
616
  wrapSecret: legacyRecord.wrapSecretEnc
229
617
  ? await decryptSecret(legacyRecord.wrapSecretEnc, legacySecretPath)
230
618
  : undefined,
231
- epochKeys: await decryptEpochKeys(legacyRecord.epochKeysEnc, legacySecretPath),
619
+ epochKeys: (await decryptEpochKeys(legacyRecord.epochKeysEnc, legacySecretPath)).keys,
232
620
  },
233
621
  };
234
622
  await saveChatv3Session({
@@ -257,13 +645,52 @@ export async function loadChatv3Session(id, options = {}) {
257
645
  record = migrated.sessions[id];
258
646
  }
259
647
  const secretPath = resolveChatv3SessionSecretPath(storePath);
648
+ let memberToken;
649
+ try {
650
+ memberToken = await decryptSecret(record.memberTokenEnc, secretPath);
651
+ }
652
+ catch (error) {
653
+ throw new Chatv3SessionCryptoError({ storePath, secretPath, sessionId: id, field: 'memberToken', cause: error });
654
+ }
655
+ let wrapSecret;
656
+ if (record.wrapSecretEnc) {
657
+ try {
658
+ wrapSecret = await decryptSecret(record.wrapSecretEnc, secretPath);
659
+ }
660
+ catch (error) {
661
+ throw new Chatv3SessionCryptoError({ storePath, secretPath, sessionId: id, field: 'wrapSecret', cause: error });
662
+ }
663
+ }
664
+ const epochKeys = await decryptEpochKeys(record.epochKeysEnc, secretPath);
665
+ if (epochKeys.stale.length > 0) {
666
+ const staleRefs = new Set(epochKeys.stale.map((entry) => `${entry.roomId}:${entry.epoch}`));
667
+ const pruneRecord = (target) => {
668
+ const pruned = (target.epochKeysEnc ?? []).filter((entry) => !staleRefs.has(`${entry.roomId}:${entry.epoch}`));
669
+ const next = { ...target };
670
+ if (pruned.length > 0)
671
+ next.epochKeysEnc = pruned;
672
+ else
673
+ delete next.epochKeysEnc;
674
+ return next;
675
+ };
676
+ record = pruneRecord(record);
677
+ await withStoreLock(storePath, async () => {
678
+ const fresh = await readStore(storePath, owner);
679
+ const current = fresh.sessions[id];
680
+ if (!current)
681
+ return;
682
+ fresh.sessions[id] = pruneRecord(current);
683
+ await writeStore(storePath, fresh);
684
+ });
685
+ }
260
686
  return {
261
687
  record,
262
688
  secrets: {
263
- memberToken: await decryptSecret(record.memberTokenEnc, secretPath),
264
- wrapSecret: record.wrapSecretEnc ? await decryptSecret(record.wrapSecretEnc, secretPath) : undefined,
265
- epochKeys: await decryptEpochKeys(record.epochKeysEnc, secretPath),
689
+ memberToken,
690
+ wrapSecret,
691
+ epochKeys: epochKeys.keys,
266
692
  },
693
+ ...(epochKeys.stale.length > 0 ? { staleEpochKeys: epochKeys.stale } : {}),
267
694
  };
268
695
  }
269
696
  export async function getChatv3SessionRecord(id, options = {}) {
@@ -284,11 +711,13 @@ export async function listChatv3SessionRecords(options = {}) {
284
711
  export async function deleteChatv3Session(id, options = {}) {
285
712
  const owner = resolveChatv3SessionOwner(options.owner);
286
713
  const storePath = resolveChatv3SessionStorePath(options.storePath, options.owner);
287
- const store = await readStore(storePath, owner);
288
- const existed = Boolean(store.sessions[id]);
289
- delete store.sessions[id];
290
- await writeStore(storePath, store);
291
- return existed;
714
+ return withStoreLock(storePath, async () => {
715
+ const store = await readStore(storePath, owner);
716
+ const existed = Boolean(store.sessions[id]);
717
+ delete store.sessions[id];
718
+ await writeStore(storePath, store);
719
+ return existed;
720
+ });
292
721
  }
293
722
  export function summarizeChatv3Session(record) {
294
723
  return {
@@ -42,7 +42,7 @@
42
42
  "capabilityClass": "windows-ntfs-crash-recoverable-v1",
43
43
  "qualification": {
44
44
  "relativePath": "qualifications/win32-x64.json",
45
- "sha256": "b6d7f7beb5e36dfd092286a3858724bf7d50d952832cbe2934cfe03d1d407434"
45
+ "sha256": "08d7ccc099bcc86e1e4a8cea6ea0d7b50eee76f3a126d6020bf11b2b2edacb3c"
46
46
  }
47
47
  }
48
48
  ]
@@ -7,7 +7,7 @@
7
7
  "filesystem": "NTFS",
8
8
  "primitiveProfile": "windows-createfile-flush-movefileex-v1",
9
9
  "capabilityClass": "windows-ntfs-crash-recoverable-v1",
10
- "evidenceSha256": "16b36958eb0a25b26cbebe2fc6d456ed90429069b16db7fbe8409691dff1d6bb",
10
+ "evidenceSha256": "dd80c815d10feb9499911bc5eb9c3bf56964afe66faa894ebc9de478cb0e5311",
11
11
  "crashInjectionPoints": [
12
12
  "authority-history",
13
13
  "authority-current",
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@aopslabs/aops",
3
- "version": "0.3.16",
3
+ "version": "0.3.17",
4
4
  "type": "module",
5
5
  "description": "AOPS CLI and terminal setup application.",
6
- "aopsDockerServerVersion": "0.2.14",
6
+ "aopsDockerServerVersion": "0.2.15",
7
7
  "license": "SEE LICENSE IN LICENSE",
8
8
  "repository": {
9
9
  "type": "git",
@@ -43,7 +43,7 @@
43
43
  "test:commercial-performance": "node --test test/commercial-performance.test.mjs",
44
44
  "test:package-identity": "node --test test/package-identity.test.mjs test/npm-tar-entry-mode.test.mjs",
45
45
  "test:tui-launcher": "node --test test/tui-launcher.test.mjs",
46
- "test:global-update": "node --test test/global-update.test.mjs",
46
+ "test:global-update": "node --test test/global-update.test.mjs test/cli-error-format.test.mjs",
47
47
  "test:skill-discovery": "node --test test/skill-discovery.test.mjs",
48
48
  "test:target-auth": "node --test test/target-auth.test.mjs test/device-login.test.mjs",
49
49
  "test:target-transport": "node --test test/target-transport.test.mjs",
@@ -59,11 +59,11 @@
59
59
  "start": "node dist/main.js"
60
60
  },
61
61
  "dependencies": {
62
- "@aopslabs/aops-server": "0.2.14",
62
+ "@aopslabs/aops-server": "0.2.15",
63
63
  "@aopslabs/artifact-trust-contracts": "0.3.1",
64
64
  "@aopslabs/cli-kit": "0.1.0",
65
65
  "@aopslabs/aops-host-registration": "0.2.1",
66
- "@aopslabs/aops-pg-bootstrap": "0.2.2",
66
+ "@aopslabs/aops-pg-bootstrap": "0.2.3",
67
67
  "@aopslabs/aops-runtime-config": "0.2.1",
68
68
  "@sigstore/bundle": "4.0.0",
69
69
  "@sigstore/protobuf-specs": "0.5.1",