@ouro.bot/cli 0.1.0-alpha.807 → 0.1.0-alpha.809

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.
@@ -91,7 +91,90 @@ function isSqliteBusy(error) {
91
91
  return error instanceof Error && "code" in error
92
92
  && error.code === "SQLITE_BUSY";
93
93
  }
94
- function openLeaseDatabase(lockPath, busyTimeoutMs = 0) {
94
+ function refuseConfinement() {
95
+ (0, runtime_1.emitNervesEvent)({
96
+ level: "warn", component: "mind", event: "mind.session_confinement_refused",
97
+ message: "session confinement is invalid or changed", meta: { action: "refused" },
98
+ });
99
+ throw new SessionTransactionError("session confinement is invalid or changed");
100
+ }
101
+ function confinedFile(filePath) {
102
+ let stat;
103
+ try {
104
+ stat = fs.lstatSync(filePath, { bigint: true });
105
+ }
106
+ catch (error) {
107
+ if (error?.code === "ENOENT")
108
+ return null;
109
+ return refuseConfinement();
110
+ }
111
+ if (!stat.isFile() || stat.isSymbolicLink())
112
+ return refuseConfinement();
113
+ return stat;
114
+ }
115
+ function checkConfinement(pin) {
116
+ if (!pin)
117
+ return;
118
+ for (const directory of pin.directories) {
119
+ let stat;
120
+ try {
121
+ stat = fs.lstatSync(directory.path, { bigint: true });
122
+ }
123
+ catch {
124
+ return refuseConfinement();
125
+ }
126
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.dev !== directory.dev || stat.ino !== directory.ino)
127
+ return refuseConfinement();
128
+ }
129
+ for (const suffix of ["", ".turn.lock", ".turn.lock-journal", ".turn.lock-wal", ".turn.lock-shm"]) {
130
+ confinedFile(`${pin.sessionPath}${suffix}`);
131
+ }
132
+ }
133
+ function pinConfinement(sessionPath, root) {
134
+ if (root === undefined)
135
+ return null;
136
+ if (typeof root !== "string" || !path.isAbsolute(root) || path.resolve(root) !== root)
137
+ return refuseConfinement();
138
+ const relative = path.relative(root, sessionPath);
139
+ if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
140
+ return refuseConfinement();
141
+ const parent = path.dirname(sessionPath);
142
+ const volumeRoot = path.parse(parent).root;
143
+ let current = volumeRoot;
144
+ const paths = [current];
145
+ for (const segment of parent.slice(volumeRoot.length).split(path.sep).filter(Boolean)) {
146
+ current = path.join(current, segment);
147
+ paths.push(current);
148
+ }
149
+ const directories = paths.map((directory) => {
150
+ let stat;
151
+ try {
152
+ stat = fs.lstatSync(directory, { bigint: true });
153
+ }
154
+ catch {
155
+ return refuseConfinement();
156
+ }
157
+ if (!stat.isDirectory() || stat.isSymbolicLink())
158
+ return refuseConfinement();
159
+ return { path: directory, dev: stat.dev, ino: stat.ino };
160
+ });
161
+ const pin = { root, sessionPath, directories };
162
+ checkConfinement(pin);
163
+ return pin;
164
+ }
165
+ function retainConfinement(held, requestedRoot) {
166
+ if (requestedRoot !== undefined && requestedRoot !== held.confinement?.root)
167
+ return refuseConfinement();
168
+ checkConfinement(held.confinement);
169
+ }
170
+ function checkTemporary(pin, filePath, identity) {
171
+ checkConfinement(pin);
172
+ const current = confinedFile(filePath);
173
+ if (!current || !identity || current.dev !== identity.dev || current.ino !== identity.ino)
174
+ return refuseConfinement();
175
+ }
176
+ function openLeaseDatabase(lockPath, busyTimeoutMs = 0, confinement = null) {
177
+ checkConfinement(confinement);
95
178
  const database = new better_sqlite3_1.default(lockPath);
96
179
  database.pragma(`busy_timeout = ${busyTimeoutMs}`);
97
180
  database.exec(`
@@ -123,14 +206,15 @@ function acquisitionRecord(options) {
123
206
  processStartedAt,
124
207
  };
125
208
  }
126
- function claimLeaseRecord(lockPath, record, isProcessAlive, getProcessStartedAt) {
127
- const database = openLeaseDatabase(lockPath);
209
+ function claimLeaseRecord(lockPath, record, isProcessAlive, getProcessStartedAt, confinement) {
210
+ const database = openLeaseDatabase(lockPath, 0, confinement);
128
211
  try {
129
212
  return database.transaction(() => {
130
213
  const current = database.prepare(`
131
214
  SELECT pid, owner_id, owner_token, boot_identity, process_started_at FROM session_turn_lease WHERE singleton = 1
132
215
  `).get();
133
216
  if (!current) {
217
+ checkConfinement(confinement);
134
218
  database.prepare(`
135
219
  INSERT INTO session_turn_lease (singleton, pid, owner_id, owner_token, boot_identity, process_started_at) VALUES (1, ?, ?, ?, ?, ?)
136
220
  `).run(record.pid, record.ownerId, record.ownerToken, record.bootIdentity, record.processStartedAt);
@@ -161,6 +245,7 @@ function claimLeaseRecord(lockPath, record, isProcessAlive, getProcessStartedAt)
161
245
  }
162
246
  if (!differentBoot && !differentProcess && isProcessAlive(observed.pid))
163
247
  return { acquired: false, stale: null };
248
+ checkConfinement(confinement);
164
249
  const changed = database.prepare(`
165
250
  UPDATE session_turn_lease SET pid = ?, owner_id = ?, owner_token = ?, boot_identity = ?, process_started_at = ?
166
251
  WHERE singleton = 1 AND pid = ? AND owner_id = ? AND owner_token = ?
@@ -174,7 +259,7 @@ function claimLeaseRecord(lockPath, record, isProcessAlive, getProcessStartedAt)
174
259
  }
175
260
  }
176
261
  function releaseLeaseRecord(lockPath, record) {
177
- const database = openLeaseDatabase(lockPath, 5_000);
262
+ const database = openLeaseDatabase(lockPath, 5_000, record.confinement);
178
263
  try {
179
264
  return database.prepare(`
180
265
  DELETE FROM session_turn_lease WHERE singleton = 1 AND pid = ? AND owner_id = ? AND owner_token = ?
@@ -187,27 +272,42 @@ function releaseLeaseRecord(lockPath, record) {
187
272
  }
188
273
  function makeLease(held) {
189
274
  let localReleased = false;
190
- return {
191
- sessionPath: held.sessionPath,
192
- ownerId: held.ownerId,
193
- ownerToken: held.ownerToken,
194
- release: async () => {
195
- if (localReleased)
196
- return;
197
- localReleased = true;
198
- held.depth -= 1;
199
- if (held.depth > 0 || held.released)
200
- return;
201
- held.released = true;
275
+ const release = () => {
276
+ if (localReleased)
277
+ return;
278
+ localReleased = true;
279
+ held.depth -= 1;
280
+ if (held.depth > 0 || held.released)
281
+ return;
282
+ held.released = true;
283
+ try {
202
284
  releaseLeaseRecord(held.lockPath, held);
203
- heldLeases.delete(held.sessionPath);
285
+ }
286
+ catch (error) {
204
287
  (0, runtime_1.emitNervesEvent)({
205
- component: "mind",
206
- event: "mind.session_turn_lease_released",
207
- message: "released session turn lease",
288
+ level: "warn", component: "mind", event: "mind.session_turn_lease_release_failed",
289
+ message: "durable lease release failed; original recovery evidence retained",
208
290
  meta: { sessionPath: held.sessionPath, ownerId: held.ownerId },
209
291
  });
292
+ throw error;
293
+ }
294
+ finally {
295
+ if (heldLeases.get(held.sessionPath) === held)
296
+ heldLeases.delete(held.sessionPath);
297
+ }
298
+ (0, runtime_1.emitNervesEvent)({
299
+ component: "mind",
300
+ event: "mind.session_turn_lease_released",
301
+ message: "released session turn lease",
302
+ meta: { sessionPath: held.sessionPath, ownerId: held.ownerId },
303
+ });
304
+ };
305
+ return {
306
+ lease: {
307
+ sessionPath: held.sessionPath, ownerId: held.ownerId, ownerToken: held.ownerToken,
308
+ release: async () => release(),
210
309
  },
310
+ release,
211
311
  };
212
312
  }
213
313
  async function acquireSessionTurnLease(sessionPath, options = {}) {
@@ -215,10 +315,12 @@ async function acquireSessionTurnLease(sessionPath, options = {}) {
215
315
  const existing = heldLeases.get(canonical);
216
316
  if (existing && !existing.released) {
217
317
  if (options.ownerId === existing.ownerId && options.ownerToken === existing.ownerToken) {
318
+ retainConfinement(existing, options.confinementRoot);
218
319
  existing.depth += 1;
219
- return makeLease(existing);
320
+ return makeLease(existing).lease;
220
321
  }
221
322
  }
323
+ const confinement = pinConfinement(canonical, options.confinementRoot);
222
324
  const record = acquisitionRecord(options);
223
325
  const { ownerId } = record;
224
326
  const timeoutMs = options.timeoutMs ?? 5_000;
@@ -226,18 +328,19 @@ async function acquireSessionTurnLease(sessionPath, options = {}) {
226
328
  const isProcessAlive = options.isProcessAlive ?? processAlive;
227
329
  const lockPath = `${canonical}.turn.lock`;
228
330
  const started = Date.now();
331
+ checkConfinement(confinement);
229
332
  fs.mkdirSync(path.dirname(canonical), { recursive: true });
230
333
  for (;;) {
231
334
  let claim = null;
232
335
  try {
233
- claim = claimLeaseRecord(lockPath, record, isProcessAlive, options.getProcessStartedAt ?? habit_lifecycle_1.probeHabitProcessStartedAt);
336
+ claim = claimLeaseRecord(lockPath, record, isProcessAlive, options.getProcessStartedAt ?? habit_lifecycle_1.probeHabitProcessStartedAt, confinement);
234
337
  }
235
338
  catch (error) {
236
339
  if (!isSqliteBusy(error))
237
340
  throw error;
238
341
  }
239
342
  if (claim?.acquired) {
240
- const held = { sessionPath: canonical, lockPath, ...record, depth: 1, released: false };
343
+ const held = { sessionPath: canonical, lockPath, ...record, depth: 1, released: false, confinement };
241
344
  heldLeases.set(canonical, held);
242
345
  (0, runtime_1.emitNervesEvent)({
243
346
  component: "mind",
@@ -255,7 +358,7 @@ async function acquireSessionTurnLease(sessionPath, options = {}) {
255
358
  meta: { sessionPath: canonical, stalePid: claim.stale.pid, staleOwnerId: claim.stale.ownerId },
256
359
  });
257
360
  }
258
- return makeLease(held);
361
+ return makeLease(held).lease;
259
362
  }
260
363
  if (Date.now() - started >= timeoutMs) {
261
364
  (0, runtime_1.emitNervesEvent)({
@@ -272,25 +375,43 @@ async function acquireSessionTurnLease(sessionPath, options = {}) {
272
375
  }
273
376
  async function withSessionTurnLease(sessionPath, work, options = {}) {
274
377
  const lease = await acquireSessionTurnLease(sessionPath, options);
378
+ let failed = false;
275
379
  try {
276
380
  return await leaseContext.run(lease, () => work(lease));
277
381
  }
382
+ catch (error) {
383
+ failed = true;
384
+ throw error;
385
+ }
278
386
  finally {
279
- await lease.release();
387
+ try {
388
+ await lease.release();
389
+ }
390
+ catch (error) {
391
+ if (!failed)
392
+ throw error;
393
+ }
280
394
  }
281
395
  }
282
396
  function withImmediateSessionTurnLease(sessionPath, work, options = {}) {
283
397
  const canonical = canonicalSessionPath(sessionPath);
284
398
  const contextual = currentSessionTurnLease(canonical);
285
- if (contextual)
399
+ if (contextual) {
400
+ const held = heldLeases.get(canonical);
401
+ if (!held || held.released)
402
+ throw new SessionTransactionError("session lease owner token mismatch");
403
+ retainConfinement(held, options.confinementRoot);
286
404
  return work(contextual);
405
+ }
406
+ const confinement = pinConfinement(canonical, options.confinementRoot);
287
407
  const record = acquisitionRecord(options);
288
408
  const lockPath = `${canonical}.turn.lock`;
409
+ checkConfinement(confinement);
289
410
  fs.mkdirSync(path.dirname(canonical), { recursive: true });
290
411
  const tryAcquire = () => {
291
412
  let claim;
292
413
  try {
293
- claim = claimLeaseRecord(lockPath, record, options.isProcessAlive ?? processAlive, options.getProcessStartedAt ?? habit_lifecycle_1.probeHabitProcessStartedAt);
414
+ claim = claimLeaseRecord(lockPath, record, options.isProcessAlive ?? processAlive, options.getProcessStartedAt ?? habit_lifecycle_1.probeHabitProcessStartedAt, confinement);
294
415
  }
295
416
  catch (error) {
296
417
  if (isSqliteBusy(error))
@@ -298,7 +419,7 @@ function withImmediateSessionTurnLease(sessionPath, work, options = {}) {
298
419
  throw error;
299
420
  }
300
421
  if (claim.acquired) {
301
- const held = { sessionPath: canonical, lockPath, ...record, depth: 1, released: false };
422
+ const held = { sessionPath: canonical, lockPath, ...record, depth: 1, released: false, confinement };
302
423
  heldLeases.set(canonical, held);
303
424
  if (claim.stale) {
304
425
  options.onStaleLease?.(claim.stale);
@@ -317,12 +438,23 @@ function withImmediateSessionTurnLease(sessionPath, work, options = {}) {
317
438
  const held = tryAcquire();
318
439
  if (!held)
319
440
  throw new SessionTurnBusyError(`session turn busy: ${canonical}`);
320
- const lease = makeLease(held);
441
+ const { lease, release } = makeLease(held);
442
+ let failed = false;
321
443
  try {
322
444
  return leaseContext.run(lease, () => work(lease));
323
445
  }
446
+ catch (error) {
447
+ failed = true;
448
+ throw error;
449
+ }
324
450
  finally {
325
- void lease.release();
451
+ try {
452
+ release();
453
+ }
454
+ catch (error) {
455
+ if (!failed)
456
+ throw error;
457
+ }
326
458
  }
327
459
  }
328
460
  function currentSessionTurnLease(sessionPath) {
@@ -337,6 +469,7 @@ function assertSessionTurnLease(sessionPath, lease) {
337
469
  if (!held || held.released || held.ownerId !== lease.ownerId || held.ownerToken !== lease.ownerToken) {
338
470
  throw new SessionTransactionError("session lease owner token mismatch");
339
471
  }
472
+ checkConfinement(held.confinement);
340
473
  }
341
474
  function readSessionTransaction(sessionPath, lease) {
342
475
  assertSessionTurnLease(sessionPath, lease);
@@ -353,6 +486,7 @@ function readSessionTransaction(sessionPath, lease) {
353
486
  function writeSessionTransaction(sessionPath, value, options) {
354
487
  assertSessionTurnLease(sessionPath, options.lease);
355
488
  const canonical = canonicalSessionPath(sessionPath);
489
+ const confinement = heldLeases.get(canonical).confinement;
356
490
  const current = readSessionTransaction(canonical, options.lease);
357
491
  if (current.revision !== options.expectedRevision)
358
492
  throw new SessionTransactionError("session revision changed");
@@ -360,14 +494,20 @@ function writeSessionTransaction(sessionPath, value, options) {
360
494
  const bytes = JSON.stringify(value, null, 2);
361
495
  const tempPath = path.join(path.dirname(canonical), `.${path.basename(canonical)}.tmp-${process.pid}-${(0, node_crypto_1.randomUUID)()}`);
362
496
  let fd = null;
497
+ let temporaryIdentity = null;
363
498
  try {
364
499
  fd = fs.openSync(tempPath, "wx", 0o600);
500
+ if (confinement)
501
+ temporaryIdentity = fs.fstatSync(fd, { bigint: true });
365
502
  fs.writeFileSync(fd, bytes, "utf8");
366
503
  fs.fsyncSync(fd);
367
504
  fs.closeSync(fd);
368
505
  fd = null;
369
506
  options.hooks?.beforeRename?.();
507
+ if (confinement)
508
+ checkTemporary(confinement, tempPath, temporaryIdentity);
370
509
  fs.renameSync(tempPath, canonical);
510
+ checkConfinement(confinement);
371
511
  const directoryFd = fs.openSync(path.dirname(canonical), "r");
372
512
  try {
373
513
  fs.fsyncSync(directoryFd);
@@ -384,9 +524,18 @@ function writeSessionTransaction(sessionPath, value, options) {
384
524
  }
385
525
  catch { /* best effort */ }
386
526
  try {
527
+ if (confinement)
528
+ checkTemporary(confinement, tempPath, temporaryIdentity);
387
529
  fs.unlinkSync(tempPath);
388
530
  }
389
- catch { /* best effort */ }
531
+ catch {
532
+ if (confinement)
533
+ (0, runtime_1.emitNervesEvent)({
534
+ level: "warn", component: "mind", event: "mind.session_transaction_cleanup_refused",
535
+ message: "temporary cleanup unavailable; original evidence retained",
536
+ meta: { sessionPath: canonical },
537
+ });
538
+ }
390
539
  throw error;
391
540
  }
392
541
  (0, runtime_1.emitNervesEvent)({
@@ -253,7 +253,7 @@ function causalSessionEventIds(view, attempts, finalAttemptIndex, finalCoordinat
253
253
  function currentIngressEventId(events, existingEventIds, userMessage, precommittedIngress, ingressRelations) {
254
254
  const reference = ingressRelations?.references[0];
255
255
  const carriesReference = (event, expected) => Array.isArray(event.relations?.references) && event.relations.references.includes(expected);
256
- const matches = events.filter((event) => (event.role === "user"
256
+ const matches = (0, session_events_1.selectEffectiveSessionEvents)(events).filter((event) => (event.role === "user"
257
257
  && event.content === userMessage
258
258
  && event.provenance?.captureKind === "live"
259
259
  && (precommittedIngress
@@ -273,6 +273,7 @@ function exactProjectedIngressMessage(existing, messages, eventId) {
273
273
  if (eventsById.size !== existing.events.length)
274
274
  return null;
275
275
  const seenProjectionIds = new Set();
276
+ const effectiveIds = new Set((0, session_events_1.selectEffectiveSessionEvents)(existing.events).map((event) => event.id));
276
277
  const projectedEvents = [];
277
278
  for (const projectedId of existing.projectionEventIds) {
278
279
  if (typeof projectedId !== "string" || !projectedId.trim() || seenProjectionIds.has(projectedId))
@@ -281,7 +282,8 @@ function exactProjectedIngressMessage(existing, messages, eventId) {
281
282
  if (!event)
282
283
  return null;
283
284
  seenProjectionIds.add(projectedId);
284
- projectedEvents.push(event);
285
+ if (effectiveIds.has(event.id))
286
+ projectedEvents.push(event);
285
287
  }
286
288
  if (existing.projectionEventIds.filter((projectedId) => projectedId === eventId).length !== 1)
287
289
  return null;
@@ -404,7 +406,7 @@ async function runSenseTurnExclusive(options) {
404
406
  ? existing?.events?.find((candidate) => candidate.id === options.precommittedIngress.eventId)
405
407
  : undefined;
406
408
  if (options.precommittedIngress) {
407
- const latestUserEvent = existing?.events?.filter((candidate) => candidate.role === "user").at(-1);
409
+ const latestUserEvent = (0, session_events_1.selectEffectiveSessionEvents)(existing?.events ?? []).filter((candidate) => candidate.role === "user").at(-1);
408
410
  if (!precommittedIngressEvent || precommittedIngressEvent !== latestUserEvent || precommittedIngressEvent.role !== "user" || precommittedIngressEvent.content !== userMessage
409
411
  || !precommittedIngressEvent.relations.references.includes(options.precommittedIngress.reference)) {
410
412
  throw new Error("shared turn precommitted ingress is missing, mismatched, or no longer current");