@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.
- package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/user-guides/chatv3.md +16 -1
- package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/manifest.json +5 -5
- package/agent-assets-release/agent-assets/inventory.json +5 -5
- package/agent-assets-release/agent-assets/projection.json +1 -1
- package/agent-assets-release/agent-assets-release.json +36 -36
- package/agent-assets-release/agent-assets-release.sigstore.json +26 -29
- package/assets/agent-assets/core/user-guides/chatv3.md +16 -1
- package/dist/commands/chatv3.js +297 -8
- package/dist/commands/community-server.js +19 -0
- package/dist/commands/global-update.js +17 -1
- package/dist/lib/agent-assets/native-fs.js +2 -2
- package/dist/lib/cli-error-format.js +13 -0
- package/dist/lib/community-native-lifecycle.js +1 -1
- package/dist/main.js +2 -1
- package/dist/utils/chatv3-session-store.js +476 -47
- package/native/manifest.json +1 -1
- package/native/qualifications/win32-x64.json +1 -1
- package/package.json +5 -5
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/agentspace/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/aops-cli-core/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/chatv3/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/collaborative-work/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/discuss/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/docman/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/mission/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/projectman/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/sys/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/view/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/references/working-disciplines/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/user-guides/agent-assets.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/user-guides/agentspace.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/user-guides/aops-cli.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/user-guides/aops-system.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/user-guides/docman.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/user-guides/projectman.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1}/files/user-guides/sys.md +0 -0
- /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
|
-
|
|
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
|
-
|
|
118
|
-
const secret = normalizeNonEmpty(raw);
|
|
119
|
-
if (secret) {
|
|
120
|
-
await enforceOwnerOnlyFileMode(secretPath);
|
|
121
|
-
return secret;
|
|
122
|
-
}
|
|
506
|
+
return await resolution;
|
|
123
507
|
}
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
264
|
-
wrapSecret
|
|
265
|
-
epochKeys:
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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 {
|
package/native/manifest.json
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"capabilityClass": "windows-ntfs-crash-recoverable-v1",
|
|
43
43
|
"qualification": {
|
|
44
44
|
"relativePath": "qualifications/win32-x64.json",
|
|
45
|
-
"sha256": "
|
|
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": "
|
|
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.
|
|
3
|
+
"version": "0.3.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AOPS CLI and terminal setup application.",
|
|
6
|
-
"aopsDockerServerVersion": "0.2.
|
|
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.
|
|
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.
|
|
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",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|