kiokuko-dsh 0.2.21 → 0.2.23
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/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/deep-thinker/current-model.d.ts.map +1 -1
- package/dist/deep-thinker/current-model.js +4 -0
- package/dist/deep-thinker/current-model.js.map +1 -1
- package/dist/dsh/answer-review/contracts.d.ts +1 -0
- package/dist/dsh/answer-review/contracts.d.ts.map +1 -1
- package/dist/dsh/answer-review/coordinator.js +3 -3
- package/dist/dsh/answer-review/coordinator.js.map +1 -1
- package/dist/dsh/clarification-context.d.ts +24 -0
- package/dist/dsh/clarification-context.d.ts.map +1 -0
- package/dist/dsh/clarification-context.js +31 -0
- package/dist/dsh/clarification-context.js.map +1 -0
- package/dist/dsh/coding-question.d.ts +26 -0
- package/dist/dsh/coding-question.d.ts.map +1 -0
- package/dist/dsh/coding-question.js +48 -0
- package/dist/dsh/coding-question.js.map +1 -0
- package/dist/dsh/coding-workflow/contracts.d.ts +2 -2
- package/dist/dsh/coding-workflow/host.d.ts +2 -4
- package/dist/dsh/coding-workflow/host.d.ts.map +1 -1
- package/dist/dsh/coding-workflow/host.js +13 -29
- package/dist/dsh/coding-workflow/host.js.map +1 -1
- package/dist/dsh/coding-workflow/intent.d.ts.map +1 -1
- package/dist/dsh/coding-workflow/intent.js +2 -1
- package/dist/dsh/coding-workflow/intent.js.map +1 -1
- package/dist/dsh/coding-workflow/service.d.ts +6 -6
- package/dist/dsh/coding-workflow/service.d.ts.map +1 -1
- package/dist/dsh/coding-workflow/service.js +5 -3
- package/dist/dsh/coding-workflow/service.js.map +1 -1
- package/dist/dsh/guardrail/contracts.d.ts +33 -1
- package/dist/dsh/guardrail/contracts.d.ts.map +1 -1
- package/dist/dsh/guardrail/contracts.js +1 -1
- package/dist/dsh/guardrail/contracts.js.map +1 -1
- package/dist/dsh/guardrail/engine.d.ts +2 -0
- package/dist/dsh/guardrail/engine.d.ts.map +1 -1
- package/dist/dsh/guardrail/engine.js +18 -7
- package/dist/dsh/guardrail/engine.js.map +1 -1
- package/dist/dsh/guardrail/evidence.d.ts.map +1 -1
- package/dist/dsh/guardrail/evidence.js +29 -1
- package/dist/dsh/guardrail/evidence.js.map +1 -1
- package/dist/dsh/guardrail/service.d.ts +2 -0
- package/dist/dsh/guardrail/service.d.ts.map +1 -1
- package/dist/dsh/guardrail/service.js +44 -2
- package/dist/dsh/guardrail/service.js.map +1 -1
- package/dist/dsh/lisp/ci.d.ts +7 -0
- package/dist/dsh/lisp/ci.d.ts.map +1 -1
- package/dist/dsh/lisp/ci.js +27 -10
- package/dist/dsh/lisp/ci.js.map +1 -1
- package/dist/dsh/lisp/coding-choice.d.ts +2 -0
- package/dist/dsh/lisp/coding-choice.d.ts.map +1 -1
- package/dist/dsh/lisp/coding-choice.js +5 -1
- package/dist/dsh/lisp/coding-choice.js.map +1 -1
- package/dist/dsh/lisp/coding-preparation.d.ts +4 -5
- package/dist/dsh/lisp/coding-preparation.d.ts.map +1 -1
- package/dist/dsh/lisp/coding-preparation.js +57 -18
- package/dist/dsh/lisp/coding-preparation.js.map +1 -1
- package/dist/dsh/lisp/compiled-cache.d.ts.map +1 -1
- package/dist/dsh/lisp/compiled-cache.js +4 -3
- package/dist/dsh/lisp/compiled-cache.js.map +1 -1
- package/dist/dsh/lisp/host-tool-dispatch.d.ts +19 -0
- package/dist/dsh/lisp/host-tool-dispatch.d.ts.map +1 -0
- package/dist/dsh/lisp/host-tool-dispatch.js +23 -0
- package/dist/dsh/lisp/host-tool-dispatch.js.map +1 -0
- package/dist/dsh/lisp/manager.d.ts +3 -0
- package/dist/dsh/lisp/manager.d.ts.map +1 -1
- package/dist/dsh/lisp/manager.js +114 -13
- package/dist/dsh/lisp/manager.js.map +1 -1
- package/dist/dsh/lisp/sandbox.d.ts.map +1 -1
- package/dist/dsh/lisp/sandbox.js +4 -2
- package/dist/dsh/lisp/sandbox.js.map +1 -1
- package/dist/dsh/lisp/surface.d.ts.map +1 -1
- package/dist/dsh/lisp/surface.js +62 -25
- package/dist/dsh/lisp/surface.js.map +1 -1
- package/dist/dsh/lisp/worker.d.ts +10 -2
- package/dist/dsh/lisp/worker.d.ts.map +1 -1
- package/dist/dsh/lisp/worker.js +21 -11
- package/dist/dsh/lisp/worker.js.map +1 -1
- package/dist/dsh/managed-operation-journal.d.ts +115 -0
- package/dist/dsh/managed-operation-journal.d.ts.map +1 -0
- package/dist/dsh/managed-operation-journal.js +336 -0
- package/dist/dsh/managed-operation-journal.js.map +1 -0
- package/dist/dsh/managed-operation-runner.d.ts +33 -0
- package/dist/dsh/managed-operation-runner.d.ts.map +1 -0
- package/dist/dsh/managed-operation-runner.js +104 -0
- package/dist/dsh/managed-operation-runner.js.map +1 -0
- package/dist/dsh/model-routing.d.ts +8 -4
- package/dist/dsh/model-routing.d.ts.map +1 -1
- package/dist/dsh/model-routing.js +81 -22
- package/dist/dsh/model-routing.js.map +1 -1
- package/dist/dsh/models/surface.d.ts.map +1 -1
- package/dist/dsh/models/surface.js +7 -1
- package/dist/dsh/models/surface.js.map +1 -1
- package/dist/dsh/models/vendor/opencode-adapter.d.ts.map +1 -1
- package/dist/dsh/models/vendor/opencode-adapter.js +2 -0
- package/dist/dsh/models/vendor/opencode-adapter.js.map +1 -1
- package/dist/dsh/on-demand-intake.d.ts +12 -0
- package/dist/dsh/on-demand-intake.d.ts.map +1 -1
- package/dist/dsh/on-demand-intake.js +61 -9
- package/dist/dsh/on-demand-intake.js.map +1 -1
- package/dist/dsh/owned-execution.d.ts +2 -0
- package/dist/dsh/owned-execution.d.ts.map +1 -1
- package/dist/dsh/owned-execution.js +242 -110
- package/dist/dsh/owned-execution.js.map +1 -1
- package/dist/dsh/request-interpretation-guidance.d.ts +26 -0
- package/dist/dsh/request-interpretation-guidance.d.ts.map +1 -0
- package/dist/dsh/request-interpretation-guidance.js +35 -0
- package/dist/dsh/request-interpretation-guidance.js.map +1 -0
- package/dist/dsh/semantic-compaction/coordinator.d.ts.map +1 -1
- package/dist/dsh/semantic-compaction/coordinator.js +15 -1
- package/dist/dsh/semantic-compaction/coordinator.js.map +1 -1
- package/dist/dsh/task-completion-guidance.d.ts +25 -0
- package/dist/dsh/task-completion-guidance.d.ts.map +1 -0
- package/dist/dsh/task-completion-guidance.js +36 -0
- package/dist/dsh/task-completion-guidance.js.map +1 -0
- package/dist/dsh/task-completion-host.d.ts.map +1 -1
- package/dist/dsh/task-completion-host.js +17 -7
- package/dist/dsh/task-completion-host.js.map +1 -1
- package/dist/dsh/task-completion.d.ts +26 -2
- package/dist/dsh/task-completion.d.ts.map +1 -1
- package/dist/dsh/task-completion.js +27 -13
- package/dist/dsh/task-completion.js.map +1 -1
- package/dist/enno-oduno/coding-workflow.d.ts +1 -1
- package/dist/locale/en.json +1 -1
- package/dist/locale/ja.json +1 -1
- package/dist/locale/ko.json +1 -1
- package/dist/locale/zh-CN.json +1 -1
- package/docs/current-upstream-compatibility-report.md +85 -0
- package/docs/dsh-plugin.md +6 -2
- package/docs/getting-started.ja.md +1 -1
- package/docs/getting-started.md +1 -1
- package/docs/guardrail.md +11 -4
- package/docs/lisp-prototype-planning.md +1 -1
- package/docs/lisp.md +1 -1
- package/docs/managed-operation-journal.md +21 -0
- package/docs/memory-lifecycle-verification.md +5 -4
- package/docs/tool-exposure.md +2 -0
- package/guardrail/aarch64-apple-darwin/kiokuko-guardrail +0 -0
- package/guardrail/aarch64-apple-darwin/manifest.json +14 -18
- package/guardrail/aarch64-unknown-linux-musl/kiokuko-guardrail +0 -0
- package/guardrail/aarch64-unknown-linux-musl/manifest.json +14 -16
- package/guardrail/build-records.json +198 -0
- package/guardrail/manifest.json +25 -137
- package/guardrail/x86_64-unknown-linux-musl/kiokuko-guardrail +0 -0
- package/guardrail/x86_64-unknown-linux-musl/manifest.json +14 -15
- package/migrations/047_managed_operation_journal.sql +21 -0
- package/package.json +7 -3
- package/scripts/guardrail-artifacts.d.mts +3 -0
- package/scripts/guardrail-artifacts.mjs +34 -2
- package/scripts/repair-continuation-source.mjs +11 -25
|
@@ -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
|
|
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
|
-
|
|
385
|
+
managedAdmissionDenied(error);
|
|
328
386
|
}
|
|
329
387
|
}
|
|
330
388
|
if (name !== 'kioku_read' && this.host.planActive?.(identity.agent))
|
|
331
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
439
|
-
// Resolve the parent as well as existing target to identify symlink exits.
|
|
440
|
-
let actual;
|
|
522
|
+
let effectAttempted = false;
|
|
441
523
|
try {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
475
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
await this.
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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 =
|
|
622
|
+
const file = await open(temporary, 'wx', mode);
|
|
512
623
|
try {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
624
|
+
await file.chmod(mode & 0o7777);
|
|
625
|
+
await file.writeFile(content);
|
|
626
|
+
await file.sync();
|
|
516
627
|
}
|
|
517
628
|
finally {
|
|
518
|
-
|
|
629
|
+
await file.close();
|
|
519
630
|
}
|
|
520
|
-
|
|
631
|
+
signal.throwIfAborted();
|
|
632
|
+
await this.checkAuthority(identity, lease);
|
|
633
|
+
await rename(temporary, actual);
|
|
521
634
|
}
|
|
522
635
|
finally {
|
|
523
|
-
|
|
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
|
-
|
|
551
|
-
|
|
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 (
|
|
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) {
|