kiokuko-dsh 0.2.21 → 0.2.22

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 (113) hide show
  1. package/dist/dsh/clarification-context.d.ts +24 -0
  2. package/dist/dsh/clarification-context.d.ts.map +1 -0
  3. package/dist/dsh/clarification-context.js +31 -0
  4. package/dist/dsh/clarification-context.js.map +1 -0
  5. package/dist/dsh/coding-question.d.ts +26 -0
  6. package/dist/dsh/coding-question.d.ts.map +1 -0
  7. package/dist/dsh/coding-question.js +48 -0
  8. package/dist/dsh/coding-question.js.map +1 -0
  9. package/dist/dsh/coding-workflow/contracts.d.ts +2 -2
  10. package/dist/dsh/coding-workflow/host.d.ts +2 -4
  11. package/dist/dsh/coding-workflow/host.d.ts.map +1 -1
  12. package/dist/dsh/coding-workflow/host.js +13 -29
  13. package/dist/dsh/coding-workflow/host.js.map +1 -1
  14. package/dist/dsh/coding-workflow/intent.d.ts.map +1 -1
  15. package/dist/dsh/coding-workflow/intent.js +2 -1
  16. package/dist/dsh/coding-workflow/intent.js.map +1 -1
  17. package/dist/dsh/coding-workflow/service.d.ts +6 -6
  18. package/dist/dsh/coding-workflow/service.d.ts.map +1 -1
  19. package/dist/dsh/coding-workflow/service.js +5 -3
  20. package/dist/dsh/coding-workflow/service.js.map +1 -1
  21. package/dist/dsh/guardrail/contracts.d.ts +33 -1
  22. package/dist/dsh/guardrail/contracts.d.ts.map +1 -1
  23. package/dist/dsh/guardrail/contracts.js +1 -1
  24. package/dist/dsh/guardrail/contracts.js.map +1 -1
  25. package/dist/dsh/guardrail/engine.d.ts +2 -0
  26. package/dist/dsh/guardrail/engine.d.ts.map +1 -1
  27. package/dist/dsh/guardrail/engine.js +18 -7
  28. package/dist/dsh/guardrail/engine.js.map +1 -1
  29. package/dist/dsh/guardrail/evidence.d.ts.map +1 -1
  30. package/dist/dsh/guardrail/evidence.js +29 -1
  31. package/dist/dsh/guardrail/evidence.js.map +1 -1
  32. package/dist/dsh/guardrail/service.d.ts +2 -0
  33. package/dist/dsh/guardrail/service.d.ts.map +1 -1
  34. package/dist/dsh/guardrail/service.js +44 -2
  35. package/dist/dsh/guardrail/service.js.map +1 -1
  36. package/dist/dsh/lisp/ci.d.ts +7 -0
  37. package/dist/dsh/lisp/ci.d.ts.map +1 -1
  38. package/dist/dsh/lisp/ci.js +27 -10
  39. package/dist/dsh/lisp/ci.js.map +1 -1
  40. package/dist/dsh/lisp/coding-choice.d.ts +2 -0
  41. package/dist/dsh/lisp/coding-choice.d.ts.map +1 -1
  42. package/dist/dsh/lisp/coding-choice.js +5 -1
  43. package/dist/dsh/lisp/coding-choice.js.map +1 -1
  44. package/dist/dsh/lisp/coding-preparation.d.ts +4 -5
  45. package/dist/dsh/lisp/coding-preparation.d.ts.map +1 -1
  46. package/dist/dsh/lisp/coding-preparation.js +57 -18
  47. package/dist/dsh/lisp/coding-preparation.js.map +1 -1
  48. package/dist/dsh/lisp/manager.d.ts +3 -0
  49. package/dist/dsh/lisp/manager.d.ts.map +1 -1
  50. package/dist/dsh/lisp/manager.js +114 -13
  51. package/dist/dsh/lisp/manager.js.map +1 -1
  52. package/dist/dsh/lisp/surface.d.ts.map +1 -1
  53. package/dist/dsh/lisp/surface.js +36 -18
  54. package/dist/dsh/lisp/surface.js.map +1 -1
  55. package/dist/dsh/lisp/worker.d.ts +10 -2
  56. package/dist/dsh/lisp/worker.d.ts.map +1 -1
  57. package/dist/dsh/lisp/worker.js +21 -11
  58. package/dist/dsh/lisp/worker.js.map +1 -1
  59. package/dist/dsh/managed-operation-journal.d.ts +115 -0
  60. package/dist/dsh/managed-operation-journal.d.ts.map +1 -0
  61. package/dist/dsh/managed-operation-journal.js +336 -0
  62. package/dist/dsh/managed-operation-journal.js.map +1 -0
  63. package/dist/dsh/managed-operation-runner.d.ts +33 -0
  64. package/dist/dsh/managed-operation-runner.d.ts.map +1 -0
  65. package/dist/dsh/managed-operation-runner.js +104 -0
  66. package/dist/dsh/managed-operation-runner.js.map +1 -0
  67. package/dist/dsh/models/vendor/opencode-adapter.d.ts.map +1 -1
  68. package/dist/dsh/models/vendor/opencode-adapter.js +2 -0
  69. package/dist/dsh/models/vendor/opencode-adapter.js.map +1 -1
  70. package/dist/dsh/on-demand-intake.d.ts +5 -0
  71. package/dist/dsh/on-demand-intake.d.ts.map +1 -1
  72. package/dist/dsh/on-demand-intake.js +30 -8
  73. package/dist/dsh/on-demand-intake.js.map +1 -1
  74. package/dist/dsh/owned-execution.d.ts +2 -0
  75. package/dist/dsh/owned-execution.d.ts.map +1 -1
  76. package/dist/dsh/owned-execution.js +242 -110
  77. package/dist/dsh/owned-execution.js.map +1 -1
  78. package/dist/dsh/request-interpretation-guidance.d.ts +26 -0
  79. package/dist/dsh/request-interpretation-guidance.d.ts.map +1 -0
  80. package/dist/dsh/request-interpretation-guidance.js +35 -0
  81. package/dist/dsh/request-interpretation-guidance.js.map +1 -0
  82. package/dist/dsh/task-completion-guidance.d.ts +25 -0
  83. package/dist/dsh/task-completion-guidance.d.ts.map +1 -0
  84. package/dist/dsh/task-completion-guidance.js +36 -0
  85. package/dist/dsh/task-completion-guidance.js.map +1 -0
  86. package/dist/dsh/task-completion-host.d.ts.map +1 -1
  87. package/dist/dsh/task-completion-host.js +17 -7
  88. package/dist/dsh/task-completion-host.js.map +1 -1
  89. package/dist/dsh/task-completion.d.ts +26 -2
  90. package/dist/dsh/task-completion.d.ts.map +1 -1
  91. package/dist/dsh/task-completion.js +27 -13
  92. package/dist/dsh/task-completion.js.map +1 -1
  93. package/dist/enno-oduno/coding-workflow.d.ts +1 -1
  94. package/dist/locale/en.json +1 -1
  95. package/dist/locale/ja.json +1 -1
  96. package/dist/locale/ko.json +1 -1
  97. package/dist/locale/zh-CN.json +1 -1
  98. package/docs/guardrail.md +11 -4
  99. package/docs/lisp-prototype-planning.md +1 -1
  100. package/docs/lisp.md +1 -1
  101. package/docs/managed-operation-journal.md +21 -0
  102. package/guardrail/aarch64-apple-darwin/kiokuko-guardrail +0 -0
  103. package/guardrail/aarch64-apple-darwin/manifest.json +14 -18
  104. package/guardrail/aarch64-unknown-linux-musl/kiokuko-guardrail +0 -0
  105. package/guardrail/aarch64-unknown-linux-musl/manifest.json +14 -16
  106. package/guardrail/build-records.json +198 -0
  107. package/guardrail/manifest.json +25 -137
  108. package/guardrail/x86_64-unknown-linux-musl/kiokuko-guardrail +0 -0
  109. package/guardrail/x86_64-unknown-linux-musl/manifest.json +14 -15
  110. package/migrations/047_managed_operation_journal.sql +21 -0
  111. package/package.json +2 -2
  112. package/scripts/guardrail-artifacts.d.mts +3 -0
  113. package/scripts/guardrail-artifacts.mjs +34 -2
@@ -1,3 +1,5 @@
1
+ import { ManagedOperationJournal } from './managed-operation-journal.js';
2
+ import { runManagedOperation, currentManagedOperation, observeManagedResult, mirrorManagedParent, managedAdmissionDenied, ManagedDispatchDenied } from './managed-operation-runner.js';
1
3
  import { renameSync, openSync, closeSync, writeFileSync, fsyncSync, fchmodSync, rmSync, statSync } from 'node:fs';
2
4
  import { assertProtocolRoute, reserveProtocolFile, applyProtocolFile, withProtocolEffect, reserveProtocolTest, authorizeProtocolTest, finishProtocolTest } from './guardrail/service.js';
3
5
  import { runProtocolTests } from './guardrail/engine.js';
@@ -64,6 +66,7 @@ export class OwnedExecutionService {
64
66
  host;
65
67
  #operations = new Map();
66
68
  #legacyRuns = new Set();
69
+ #managedCalls = new Map();
67
70
  constructor(host) {
68
71
  this.host = host;
69
72
  const services = runtimeServices.get(host.runtime) ?? new Set();
@@ -272,7 +275,7 @@ export class OwnedExecutionService {
272
275
  }
273
276
  async approve(identity, detail, signal) {
274
277
  if (!await this.host.confirm?.(identity, detail, signal))
275
- throw new Error('Destructive operation was not approved');
278
+ throw new ManagedDispatchDenied('Destructive operation was not approved');
276
279
  signal.throwIfAborted();
277
280
  }
278
281
  async reply(receipt, value = {}, replayed = false) {
@@ -284,6 +287,47 @@ export class OwnedExecutionService {
284
287
  return { ...value, receipt, ...(replayed ? { replayed: true } : {}), status: { work: receipt.state, verification: receipt.verification ?? 'unknown', receiptPersistence: receipt.persistence, memoryStorage } };
285
288
  }
286
289
  async execute(name, raw, execution) {
290
+ if (!['kioku_exec', 'kioku_write', 'kioku_edit', 'kioku_remove'].includes(name))
291
+ return this.executeManaged(name, raw, execution);
292
+ const parentOperation = currentManagedOperation();
293
+ const identity = this.current(execution);
294
+ const database = (fn) => this.host.runtime.withDatabase(fn);
295
+ const originalRef = canonicalContentHash({ session: identity.sessionId, call: execution.callId });
296
+ const request = { name, raw, cwd: identity.repositoryRoot, runId: identity.runId, generation: identity.generation, environment: { ...process.env }, runtime: process.version };
297
+ const digest = canonicalContentHash(request);
298
+ const pending = this.#managedCalls.get(originalRef);
299
+ if (pending) {
300
+ if (pending.digest !== digest)
301
+ throw new Error('Operation ID reused with changed input');
302
+ return pending.result;
303
+ }
304
+ const result = (async () => {
305
+ const previous = await database(db => new ManagedOperationJournal(db).lookupOriginalOperation(originalRef));
306
+ if (previous) {
307
+ await database(db => new ManagedOperationJournal(db).claim(previous.operationId, request));
308
+ const saved = await database(db => db.prepare('SELECT operation_id FROM dsh_owned_operations WHERE operation_id=?').get(originalRef));
309
+ if (saved)
310
+ return { ...await this.result(identity, { operationId: originalRef, cancel: false }), replayed: true };
311
+ return { state: previous.state, operationId: previous.operationId, replayed: true, executed: false };
312
+ }
313
+ const scope = await database(db => new ManagedOperationJournal(db).createScope({ sessionId: identity.sessionId,
314
+ requestId: execution.callId, parentId: parentOperation?.operationId ?? execution.callId, runtimeIdentity: `owned:${identity.generation}` }));
315
+ try {
316
+ return await runManagedOperation({ database, scopeId: scope.scopeId, direct: true, nested: true, ...(parentOperation ? { parentOperation } : {}) }, { route: 'owned', kind: name === 'kioku_exec' ? 'process' : 'file', request, originalRef }, () => this.executeManaged(name, raw, execution));
317
+ }
318
+ finally {
319
+ await database(db => new ManagedOperationJournal(db).seal(scope.scopeId));
320
+ }
321
+ })();
322
+ this.#managedCalls.set(originalRef, { digest, result });
323
+ try {
324
+ return await result;
325
+ }
326
+ finally {
327
+ this.#managedCalls.delete(originalRef);
328
+ }
329
+ }
330
+ async executeManaged(name, raw, execution) {
287
331
  const identity = (name === 'kioku_read' || name === 'kioku_result') && this.host.resolveRead
288
332
  ? await this.host.resolveRead(execution).then(value => {
289
333
  execution.signal.throwIfAborted();
@@ -304,7 +348,21 @@ export class OwnedExecutionService {
304
348
  if (name === 'kioku_result')
305
349
  return this.result(identity, resultInput.parse(raw));
306
350
  const input = name === 'kioku_exec' ? commandInput.parse(raw) : fileInput.parse(raw);
307
- const lease = name === 'kioku_read' ? await this.captureAuthority(execution, identity).catch(() => undefined) : await this.captureAuthority(execution, identity);
351
+ const lease = name === 'kioku_read' ? await this.captureAuthority(execution, identity).catch(() => undefined) : await this.captureAuthority(execution, identity).catch(managedAdmissionDenied);
352
+ const journalOperation = currentManagedOperation();
353
+ if (lease && journalOperation)
354
+ await journalOperation.database(db => new ManagedOperationJournal(db).bindOperation(journalOperation.operationId, {
355
+ binding: { taskId: lease.taskId, workerGeneration: identity.generation, leaseAuthorityGeneration: lease.generation, leaseRevision: lease.revision },
356
+ runtimeIdentity: `owned:${identity.generation}`,
357
+ }));
358
+ if (lease && journalOperation?.parentOperation)
359
+ await journalOperation.parentOperation.database(db => {
360
+ const journal = new ManagedOperationJournal(db), parent = journal.readOperation(journalOperation.parentOperation.operationId);
361
+ journal.bindOperation(parent.operationId, {
362
+ binding: { taskId: lease.taskId, workerGeneration: parent.binding?.workerGeneration ?? identity.generation, leaseAuthorityGeneration: lease.generation, leaseRevision: lease.revision },
363
+ runtimeIdentity: parent.runtimeIdentity,
364
+ });
365
+ });
308
366
  if (name !== 'kioku_read') {
309
367
  try {
310
368
  await this.host.runtime.withDatabase(db => {
@@ -324,11 +382,11 @@ export class OwnedExecutionService {
324
382
  }
325
383
  catch (error) {
326
384
  if (error instanceof KiokukoError || !this.#legacyRuns.has(identity.runId))
327
- throw error;
385
+ managedAdmissionDenied(error);
328
386
  }
329
387
  }
330
388
  if (name !== 'kioku_read' && this.host.planActive?.(identity.agent))
331
- throw new Error('Plan is active. An explicit human implementation instruction or plan approval must exit it first.');
389
+ managedAdmissionDenied(new Error('Plan is active. An explicit human implementation instruction or plan approval must exit it first.'));
332
390
  // Native call identity survives a generation change. Rebinding a replay to
333
391
  // a new run/generation must conflict rather than repeat its side effects.
334
392
  const operationId = canonicalContentHash({ session: identity.sessionId, call: execution.callId });
@@ -374,6 +432,9 @@ export class OwnedExecutionService {
374
432
  throw new Error('Operation ID reused with changed input');
375
433
  return concurrent.done ?? { receipt: concurrent.receipt };
376
434
  }
435
+ const managed = currentManagedOperation();
436
+ if (managed)
437
+ await managed.database(db => new ManagedOperationJournal(db).attachOriginalRef(managed.operationId, operationId));
377
438
  this.#operations.set(operationId, operation);
378
439
  const signal = AbortSignal.any([execution.signal, controller.signal]);
379
440
  const journal = this.journal(identity.repositoryRoot);
@@ -409,12 +470,28 @@ export class OwnedExecutionService {
409
470
  receipt.state = 'completed';
410
471
  }
411
472
  catch (error) {
473
+ if (error instanceof ManagedDispatchDenied && managed) {
474
+ await managed.database(db => {
475
+ const journal = new ManagedOperationJournal(db), prior = journal.readOperation(managed.operationId);
476
+ if (prior.state === 'unknown')
477
+ journal.reconcile(managed.operationId, { state: 'denied', receiptRef: canonicalContentHash(receipt), cause: 'permission-denied' });
478
+ else
479
+ journal.transition(managed.operationId, { state: 'denied', cause: 'permission-denied' });
480
+ });
481
+ await mirrorManagedParent(managed);
482
+ }
483
+ ;
412
484
  receipt.state = 'unknown';
413
485
  receipt.error = safeText(error instanceof Error ? error.message : String(error));
414
486
  operation.value = { error: receipt.error };
415
487
  receipt.verification = 'unknown';
416
488
  }
417
489
  await this.persist(receipt, digest);
490
+ if (managed) {
491
+ await observeManagedResult(managed, { ...operation.value, receipt });
492
+ if (managed.direct)
493
+ await managed.database(db => new ManagedOperationJournal(db).seal(managed.scopeId));
494
+ }
418
495
  if (name === 'kioku_exec') {
419
496
  try {
420
497
  await finishProtocolTest(action => this.host.runtime.withDatabase(action), receipt);
@@ -427,134 +504,152 @@ export class OwnedExecutionService {
427
504
  }
428
505
  }
429
506
  await this.host.runtime.withDatabase(db => withImmediateTransaction(db, () => recordOwnedCompletionObservation(db, receipt))).catch(() => { });
430
- return this.reply(receipt, operation.value);
507
+ const result = await this.reply(receipt, operation.value);
508
+ if (managed && name === 'kioku_exec' && input.background)
509
+ await observeManagedResult(managed, result);
510
+ return result;
431
511
  })();
432
512
  operation.done = done;
433
- if (name === 'kioku_exec' && input.background)
434
- return this.reply(receipt);
513
+ if (name === 'kioku_exec' && input.background) {
514
+ const value = await this.reply(receipt);
515
+ if (managed)
516
+ await observeManagedResult(managed, value);
517
+ return value;
518
+ }
435
519
  return done;
436
520
  }
437
521
  async file(identity, receipt, name, input, signal, lease) {
438
- const target = path.resolve(identity.repositoryRoot, input.path);
439
- // Resolve the parent as well as existing target to identify symlink exits.
440
- let actual;
522
+ let effectAttempted = false;
441
523
  try {
442
- actual = await realpath(target);
443
- }
444
- catch (error) {
445
- if (error.code !== 'ENOENT')
446
- throw error;
447
- let parent = path.dirname(target), suffix = path.basename(target);
448
- for (;;) {
449
- try {
450
- actual = path.join(await realpath(parent), suffix);
451
- break;
452
- }
453
- catch (cause) {
454
- if (cause.code !== 'ENOENT')
455
- throw cause;
456
- suffix = path.join(path.basename(parent), suffix);
457
- parent = path.dirname(parent);
458
- }
524
+ const target = path.resolve(identity.repositoryRoot, input.path);
525
+ // Resolve the parent as well as existing target to identify symlink exits.
526
+ let actual;
527
+ try {
528
+ actual = await realpath(target);
459
529
  }
460
- }
461
- const before = await this.snapshot(actual);
462
- receipt.targets = [{ path: actual, before, after: before }];
463
- await this.persist(receipt, this.#operations.get(receipt.operationId).digest);
464
- if (input.expectedHash !== undefined && input.expectedHash !== before)
465
- throw new Error('File changed before the operation');
466
- if (name === 'kioku_read') {
467
- const content = await readFile(actual, 'utf8'), observedHash = hash(content);
468
- if (lease && observedHash === before) {
469
- try {
470
- await this.checkAuthority(identity, lease);
471
- receipt.authority = 'current';
472
- receipt.verification = 'passed';
530
+ catch (error) {
531
+ if (error.code !== 'ENOENT')
532
+ throw error;
533
+ let parent = path.dirname(target), suffix = path.basename(target);
534
+ for (;;) {
535
+ try {
536
+ actual = path.join(await realpath(parent), suffix);
537
+ break;
538
+ }
539
+ catch (cause) {
540
+ if (cause.code !== 'ENOENT')
541
+ throw cause;
542
+ suffix = path.join(path.basename(parent), suffix);
543
+ parent = path.dirname(parent);
544
+ }
473
545
  }
474
- catch {
475
- receipt.authority = 'stale';
546
+ }
547
+ const before = await this.snapshot(actual);
548
+ receipt.targets = [{ path: actual, before, after: before }];
549
+ await this.persist(receipt, this.#operations.get(receipt.operationId).digest);
550
+ if (input.expectedHash !== undefined && input.expectedHash !== before)
551
+ throw new Error('File changed before the operation');
552
+ if (name === 'kioku_read') {
553
+ const content = await readFile(actual, 'utf8'), observedHash = hash(content);
554
+ if (lease && observedHash === before) {
555
+ try {
556
+ await this.checkAuthority(identity, lease);
557
+ receipt.authority = 'current';
558
+ receipt.verification = 'passed';
559
+ }
560
+ catch {
561
+ receipt.authority = 'stale';
562
+ }
476
563
  }
564
+ return { content, hash: observedHash };
477
565
  }
478
- return { content, hash: observedHash };
479
- }
480
- const disposable = await this.disposable(identity, actual);
481
- const database = before !== null && (/\.(?:db|sqlite3?)(?:-(?:wal|shm|journal))?$/iu.test(actual) || (await readFile(actual)).subarray(0, 16).equals(Buffer.from('SQLite format 3\0')));
482
- if (!disposable && (!within(identity.repositoryRoot, actual) || name === 'kioku_remove' || database))
483
- await this.approve(identity, `Operation: ${name}\nTarget: ${actual}\nCurrent SHA256: ${before}`, signal);
484
- signal.throwIfAborted();
485
- if (await this.snapshot(actual) !== before)
486
- throw new Error('File changed during operation approval');
487
- const protocol = await this.host.runtime.withDatabase(db => reserveProtocolFile(db, lease, receipt.operationId, name, actual, this.#operations.get(receipt.operationId).digest));
488
- if (name === 'kioku_remove') {
489
- await this.checkAuthority(identity, lease);
490
- await rm(actual, { recursive: false, force: false });
491
- }
492
- else {
493
- const existing = before === null ? '' : await readFile(actual, 'utf8');
494
- let content = input.content;
495
- if (name === 'kioku_edit') {
496
- if (!input.oldText || input.newText === undefined || existing.split(input.oldText).length !== 2)
497
- throw new Error('Edit requires exactly one matching oldText');
498
- content = existing.replace(input.oldText, input.newText);
566
+ const disposable = await this.disposable(identity, actual);
567
+ const database = before !== null && (/\.(?:db|sqlite3?)(?:-(?:wal|shm|journal))?$/iu.test(actual) || (await readFile(actual)).subarray(0, 16).equals(Buffer.from('SQLite format 3\0')));
568
+ if (!disposable && (!within(identity.repositoryRoot, actual) || name === 'kioku_remove' || database))
569
+ await this.approve(identity, `Operation: ${name}\nTarget: ${actual}\nCurrent SHA256: ${before}`, signal);
570
+ signal.throwIfAborted();
571
+ if (await this.snapshot(actual) !== before)
572
+ throw new Error('File changed during operation approval');
573
+ const protocol = await this.host.runtime.withDatabase(db => reserveProtocolFile(db, lease, receipt.operationId, name, actual, this.#operations.get(receipt.operationId).digest));
574
+ if (name === 'kioku_remove') {
575
+ await this.checkAuthority(identity, lease);
576
+ effectAttempted = true;
577
+ await rm(actual, { recursive: false, force: false });
499
578
  }
500
- if (content === undefined)
501
- throw new Error('Write content is required');
502
- if (protocol && Buffer.byteLength(content) > 8 * 1024 * 1024)
503
- throw new Error('Protocol file byte limit');
504
- if (protocol) {
505
- // Existing parents were bound at enrollment. Stage and commit inside the same final
506
- // synchronous authority boundary; cancellation committed before it prevents all writes.
507
- await this.host.runtime.withDatabase(db => applyProtocolFile(db, lease, receipt.operationId, signal, () => {
508
- const mode = before === null ? 0o644 : statSync(actual).mode & 0o7777;
579
+ else {
580
+ const existing = before === null ? '' : await readFile(actual, 'utf8');
581
+ let content = input.content;
582
+ if (name === 'kioku_edit') {
583
+ if (!input.oldText || input.newText === undefined || existing.split(input.oldText).length !== 2)
584
+ throw new Error('Edit requires exactly one matching oldText');
585
+ content = existing.replace(input.oldText, input.newText);
586
+ }
587
+ if (content === undefined)
588
+ throw new Error('Write content is required');
589
+ if (protocol && Buffer.byteLength(content) > 8 * 1024 * 1024)
590
+ throw new Error('Protocol file byte limit');
591
+ if (protocol) {
592
+ // Existing parents were bound at enrollment. Stage and commit inside the same final
593
+ // synchronous authority boundary; cancellation committed before it prevents all writes.
594
+ await this.host.runtime.withDatabase(db => applyProtocolFile(db, lease, receipt.operationId, signal, () => {
595
+ effectAttempted = true;
596
+ const mode = before === null ? 0o644 : statSync(actual).mode & 0o7777;
597
+ const temporary = path.join(path.dirname(actual), `.kioku-${randomUUID()}.tmp`);
598
+ try {
599
+ const file = openSync(temporary, 'wx', mode);
600
+ try {
601
+ fchmodSync(file, mode);
602
+ writeFileSync(file, content);
603
+ fsyncSync(file);
604
+ }
605
+ finally {
606
+ closeSync(file);
607
+ }
608
+ renameSync(temporary, actual);
609
+ }
610
+ finally {
611
+ rmSync(temporary, { force: true });
612
+ }
613
+ }));
614
+ }
615
+ else {
616
+ await this.checkAuthority(identity, lease);
617
+ effectAttempted = true;
618
+ await mkdir(path.dirname(actual), { recursive: true });
619
+ const mode = before === null ? 0o644 : (await stat(actual)).mode;
509
620
  const temporary = path.join(path.dirname(actual), `.kioku-${randomUUID()}.tmp`);
510
621
  try {
511
- const file = openSync(temporary, 'wx', mode);
622
+ const file = await open(temporary, 'wx', mode);
512
623
  try {
513
- fchmodSync(file, mode);
514
- writeFileSync(file, content);
515
- fsyncSync(file);
624
+ await file.chmod(mode & 0o7777);
625
+ await file.writeFile(content);
626
+ await file.sync();
516
627
  }
517
628
  finally {
518
- closeSync(file);
629
+ await file.close();
519
630
  }
520
- renameSync(temporary, actual);
631
+ signal.throwIfAborted();
632
+ await this.checkAuthority(identity, lease);
633
+ await rename(temporary, actual);
521
634
  }
522
635
  finally {
523
- rmSync(temporary, { force: true });
636
+ await rm(temporary, { force: true });
524
637
  }
525
- }));
526
- }
527
- else {
528
- await this.checkAuthority(identity, lease);
529
- await mkdir(path.dirname(actual), { recursive: true });
530
- const mode = before === null ? 0o644 : (await stat(actual)).mode;
531
- const temporary = path.join(path.dirname(actual), `.kioku-${randomUUID()}.tmp`);
532
- try {
533
- const file = await open(temporary, 'wx', mode);
534
- try {
535
- await file.chmod(mode & 0o7777);
536
- await file.writeFile(content);
537
- await file.sync();
538
- }
539
- finally {
540
- await file.close();
541
- }
542
- signal.throwIfAborted();
543
- await this.checkAuthority(identity, lease);
544
- await rename(temporary, actual);
545
- }
546
- finally {
547
- await rm(temporary, { force: true });
548
638
  }
639
+ if (await this.snapshot(actual) !== hash(content))
640
+ throw new Error('Written content differs from requested content');
549
641
  }
550
- if (await this.snapshot(actual) !== hash(content))
551
- throw new Error('Written content differs from requested content');
642
+ receipt.targets[0].after = await this.snapshot(actual);
643
+ await this.checkAuthority(identity, lease);
644
+ receipt.verification = 'passed';
645
+ receipt.authority = 'current';
646
+ return { applied: true };
647
+ }
648
+ catch (error) {
649
+ if (!effectAttempted)
650
+ throw new ManagedDispatchDenied(error instanceof Error ? error.message : 'File admission denied');
651
+ throw error;
552
652
  }
553
- receipt.targets[0].after = await this.snapshot(actual);
554
- await this.checkAuthority(identity, lease);
555
- receipt.verification = 'passed';
556
- receipt.authority = 'current';
557
- return { applied: true };
558
653
  }
559
654
  async disposable(identity, target) {
560
655
  if (within(identity.repositoryRoot, target))
@@ -589,6 +684,16 @@ export class OwnedExecutionService {
589
684
  if (input.destructiveTargets?.length || durableSql || removals?.length && !disposableRemoval || !disposableRemoval && /(?:^|[;&|]\s*)(?:rm\s|git\s+(?:reset\s+--hard|clean\s+-[a-z]*f)|(?:drop\s+(?:table|database)|truncate\s+table)\b)/iu.test(input.command))
590
685
  await this.approve(identity, `Command: ${safeText(input.command)}\nCwd: ${receipt.cwd}\nTargets: ${input.destructiveTargets?.join(', ') ?? 'see command'}`, signal);
591
686
  const protocol = await this.host.runtime.withDatabase(db => reserveProtocolTest(db, lease, receipt.operationId, input.command, receipt.cwd));
687
+ const managed = currentManagedOperation();
688
+ if (protocol && managed) {
689
+ if (!managed.direct)
690
+ throw new Error('Protocol proof requires its direct owned operation scope');
691
+ await managed.database(db => new ManagedOperationJournal(db).bindOperation(managed.operationId, {
692
+ binding: { taskId: lease.taskId, unitId: protocol.recipe.unitId, workerGeneration: identity.generation,
693
+ protocolBindGeneration: protocol.generation, leaseAuthorityGeneration: lease.generation, leaseRevision: lease.revision },
694
+ runtimeIdentity: protocol.runtimeDigest, bindScope: true,
695
+ }));
696
+ }
592
697
  return await this.evidence(identity, receipt, input.command, lease, async () => {
593
698
  signal.throwIfAborted();
594
699
  await this.checkAuthority(identity, lease);
@@ -622,8 +727,10 @@ export class OwnedExecutionService {
622
727
  catch (error) {
623
728
  // Before the only launch boundary, cancellation proves that no child ran.
624
729
  // Once launch was attempted, preserve unknown outcomes and reconcile them.
625
- if (!signal.aborted || launchAttempted)
730
+ if (launchAttempted)
626
731
  throw error;
732
+ if (!signal.aborted)
733
+ throw new ManagedDispatchDenied(error instanceof Error ? error.message : 'Process admission denied');
627
734
  receipt.processStarted = false;
628
735
  receipt.processStopped = true;
629
736
  receipt.cancelled = true;
@@ -634,6 +741,29 @@ export class OwnedExecutionService {
634
741
  return { exitCode: null, output: '' };
635
742
  }
636
743
  }
744
+ async reconcileManagedReceipt(receipt) {
745
+ if (receipt.state !== 'completed' || receipt.kind === 'kioku_exec' && receipt.processStopped !== true)
746
+ return;
747
+ const database = (fn) => this.host.runtime.withDatabase(fn);
748
+ const context = await database(db => {
749
+ const journal = new ManagedOperationJournal(db), operation = journal.lookupOriginalOperation(receipt.operationId);
750
+ if (!operation)
751
+ return undefined; // Legacy owned originals remain readable.
752
+ const saved = db.prepare('SELECT receipt_json FROM dsh_owned_operations WHERE operation_id=? AND run_id=? AND session_id=?')
753
+ .get(receipt.operationId, receipt.runId, receipt.sessionId);
754
+ if (!saved || canonicalContentHash(JSON.parse(saved.receipt_json)) !== canonicalContentHash(receipt))
755
+ throw new Error('Owned original is not durably verified');
756
+ const scope = journal.readScope(operation.scopeId);
757
+ if (scope.sessionId !== receipt.sessionId || operation.binding?.taskId !== receipt.runId)
758
+ throw new Error('Managed original owner mismatch');
759
+ const parentRow = db.prepare('SELECT operation_json FROM dsh_managed_operations WHERE operation_id=?').get(scope.parentId);
760
+ const parent = parentRow ? JSON.parse(parentRow.operation_json) : undefined;
761
+ return { database, scopeId: operation.scopeId, operationId: operation.operationId, kind: operation.kind,
762
+ ...(parent ? { parentOperation: { database, scopeId: parent.scopeId, operationId: parent.operationId, kind: parent.kind } } : {}) };
763
+ });
764
+ if (context)
765
+ await observeManagedResult(context, { receipt, exitCode: receipt.exitCode });
766
+ }
637
767
  async result(identity, input) {
638
768
  const operation = this.#operations.get(input.operationId);
639
769
  if (operation) {
@@ -644,6 +774,7 @@ export class OwnedExecutionService {
644
774
  if (operation.receipt.state === 'started')
645
775
  return this.reply(operation.receipt);
646
776
  await this.persist(operation.receipt, operation.digest);
777
+ await this.reconcileManagedReceipt(operation.receipt);
647
778
  return this.reply(operation.receipt, operation.value);
648
779
  }
649
780
  const saved = await this.host.runtime.withDatabase(db => db.prepare('SELECT receipt_json FROM dsh_owned_operations WHERE operation_id=? AND run_id=? AND session_id=?')
@@ -654,6 +785,7 @@ export class OwnedExecutionService {
654
785
  throw new Error('No result for this run/session');
655
786
  if (journal)
656
787
  await this.persist(receipt, journal.digest);
788
+ await this.reconcileManagedReceipt(receipt);
657
789
  return this.reply(receipt.state === 'started' ? { ...receipt, state: 'unknown', error: 'Host restarted before final process result was saved; effects will not be replayed.' } : receipt);
658
790
  }
659
791
  async stopTask(identity, signal) {