@dsh-enhanced/plugin-control-plane 0.1.67 → 0.1.68

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 (86) hide show
  1. package/README.md +54 -20
  2. package/lib/cli.d.ts.map +1 -1
  3. package/lib/cli.js +66 -11
  4. package/lib/cli.js.map +1 -1
  5. package/lib/creation-capability-feedback.d.ts +16 -1
  6. package/lib/creation-capability-feedback.d.ts.map +1 -1
  7. package/lib/creation-capability-feedback.js +82 -5
  8. package/lib/creation-capability-feedback.js.map +1 -1
  9. package/lib/creation-capability-journal.d.ts.map +1 -1
  10. package/lib/creation-capability-journal.js +2 -0
  11. package/lib/creation-capability-journal.js.map +1 -1
  12. package/lib/creation-capability-runtime.d.ts.map +1 -1
  13. package/lib/creation-capability-runtime.js +2 -0
  14. package/lib/creation-capability-runtime.js.map +1 -1
  15. package/lib/creation-capability-types.d.ts +1 -0
  16. package/lib/creation-capability-types.d.ts.map +1 -1
  17. package/lib/linux-source-build.d.ts +13 -2
  18. package/lib/linux-source-build.d.ts.map +1 -1
  19. package/lib/linux-source-build.js.map +1 -1
  20. package/lib/live-qualification-authority.js +2 -2
  21. package/lib/live-qualification-authority.js.map +1 -1
  22. package/lib/owner-instruction-drain.d.ts +60 -0
  23. package/lib/owner-instruction-drain.d.ts.map +1 -0
  24. package/lib/owner-instruction-drain.js +101 -0
  25. package/lib/owner-instruction-drain.js.map +1 -0
  26. package/lib/revision-capability-feedback.d.ts +165 -0
  27. package/lib/revision-capability-feedback.d.ts.map +1 -0
  28. package/lib/revision-capability-feedback.js +316 -0
  29. package/lib/revision-capability-feedback.js.map +1 -0
  30. package/lib/revision-capability-journal.d.ts +23 -0
  31. package/lib/revision-capability-journal.d.ts.map +1 -1
  32. package/lib/revision-capability-journal.js +124 -15
  33. package/lib/revision-capability-journal.js.map +1 -1
  34. package/lib/revision-capability-runtime.d.ts +19 -1
  35. package/lib/revision-capability-runtime.d.ts.map +1 -1
  36. package/lib/revision-capability-runtime.js +80 -7
  37. package/lib/revision-capability-runtime.js.map +1 -1
  38. package/lib/revision-capability-types.d.ts +50 -4
  39. package/lib/revision-capability-types.d.ts.map +1 -1
  40. package/lib/service.d.ts +174 -3
  41. package/lib/service.d.ts.map +1 -1
  42. package/lib/service.js +732 -68
  43. package/lib/service.js.map +1 -1
  44. package/lib/source-adoption-authority.js +2 -2
  45. package/lib/source-adoption-authority.js.map +1 -1
  46. package/lib/source-approval-authority.js +3 -3
  47. package/lib/source-approval-authority.js.map +1 -1
  48. package/lib/source-build.d.ts +33 -53
  49. package/lib/source-build.d.ts.map +1 -1
  50. package/lib/source-build.js +28 -408
  51. package/lib/source-build.js.map +1 -1
  52. package/lib/source-job-types.d.ts +111 -4
  53. package/lib/source-job-types.d.ts.map +1 -1
  54. package/lib/source-jobs.d.ts +68 -4
  55. package/lib/source-jobs.d.ts.map +1 -1
  56. package/lib/source-jobs.js +296 -46
  57. package/lib/source-jobs.js.map +1 -1
  58. package/lib/source-release-authority.js +2 -2
  59. package/lib/source-release-authority.js.map +1 -1
  60. package/lib/source-revision.d.ts +29 -3
  61. package/lib/source-revision.d.ts.map +1 -1
  62. package/lib/source-revision.js +40 -9
  63. package/lib/source-revision.js.map +1 -1
  64. package/lib/source-workspace.d.ts +11 -4
  65. package/lib/source-workspace.d.ts.map +1 -1
  66. package/lib/source-workspace.js +51 -17
  67. package/lib/source-workspace.js.map +1 -1
  68. package/lib/sqlite.d.ts +6 -7
  69. package/lib/sqlite.d.ts.map +1 -1
  70. package/lib/sqlite.js +255 -17
  71. package/lib/sqlite.js.map +1 -1
  72. package/lib/store.d.ts +142 -3
  73. package/lib/store.d.ts.map +1 -1
  74. package/lib/store.js +732 -63
  75. package/lib/store.js.map +1 -1
  76. package/lib/systemd-host-authority.js +4 -4
  77. package/lib/systemd-host-authority.js.map +1 -1
  78. package/lib/task-observation-authority.js +2 -2
  79. package/lib/task-observation-authority.js.map +1 -1
  80. package/lib/version.d.ts +1 -1
  81. package/lib/version.js +1 -1
  82. package/lib/work-item-creation-contract.d.ts +3 -1
  83. package/lib/work-item-creation-contract.d.ts.map +1 -1
  84. package/lib/work-item-creation-contract.js +10 -1
  85. package/lib/work-item-creation-contract.js.map +1 -1
  86. package/package.json +11 -8
package/lib/service.js CHANGED
@@ -2,19 +2,20 @@ import { cleanedLinuxSourceBuildFailure, cleanedLinuxSourcePreflight, consumeCle
2
2
  import { AdoptionCoordinatorRuntime, validateAdoptionCoordinatorConfig } from './adoption-coordinator.js';
3
3
  import { CreationCapabilityJournal, creationCapabilityAuthorityDigest, creationCapabilityPublicKey, validateCreationCapabilityConfig } from './creation-capability-journal.js';
4
4
  import { CreationCapabilityRuntime } from './creation-capability-runtime.js';
5
- import { RevisionCapabilityJournal, revisionCapabilityPublicKey, validateRevisionCapabilityConfig } from './revision-capability-journal.js';
5
+ import { RevisionCapabilityJournal, revisionCapabilityAuthorityDigest, revisionCapabilityPublicKey, validateRevisionCapabilityConfig } from './revision-capability-journal.js';
6
6
  import { RevisionCapabilityRuntime } from './revision-capability-runtime.js';
7
- import { inspectCreationCapabilityAvailabilityAttribution, inspectCreationCapabilityOrdinaryUseReport, inspectCreationCapabilityOrdinaryUseTrend, inspectCreationCapabilityTaskAssociations } from './creation-capability-feedback.js';
7
+ import { inspectCreationCapabilityAvailabilityAttribution, inspectCreationCapabilityBenefitWindow, inspectCreationCapabilityOrdinaryUseReport, inspectCreationCapabilityOrdinaryUseTrend, inspectCreationCapabilityTaskAssociations } from './creation-capability-feedback.js';
8
+ import { autoRollbackInferiorRevision, evaluateRevisionBenefitEvidence, inspectRevisionBenefitTerminal, inspectRevisionCapabilityBenefitWindow, mergeRevisionBenefitWindows, revisionBenefitObservationDeadline, revisionBenefitPairedWindows } from './revision-capability-feedback.js';
8
9
  import { captureCreationCapabilitySource } from './creation-capability-source.js';
9
10
  import { lstat, readFile, realpath } from 'node:fs/promises';
10
11
  import { realpathSync } from 'node:fs';
11
12
  import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
12
13
  import { createHash, createPublicKey, randomBytes } from 'node:crypto';
13
- import { isSourceOwnerContinuation, anySourceGrowthRunDigest as sourceGrowthRunDigest, validateAnySourceGrowthRunBinding as validateSourceGrowthRunBinding, validateWorkItemGrowthAuthorityPins, validateCreationAcceptanceAuthorityRef, verifyPluginCreationVerificationCertificate, validateRevisionAcceptanceAuthorityRef, verifyPluginRevisionVerificationCertificate, validateRevisionRegressionAcceptanceAuthorityRef, verifyPluginRevisionRegressionCertificate } from '@dsh-enhanced/assistant-growth-contract';
14
+ import { isSourceOwnerContinuation, isPluginRevisionTaskSource, anySourceGrowthRunDigest as sourceGrowthRunDigest, validateAnySourceGrowthRunBinding as validateSourceGrowthRunBinding, validateWorkItemGrowthAuthorityPins, validateCreationAcceptanceAuthorityRef, verifyPluginCreationVerificationCertificate, validateRevisionAcceptanceAuthorityRef, verifyPluginRevisionVerificationCertificate, validateRevisionRegressionAcceptanceAuthorityRef, verifyPluginRevisionRegressionCertificate, validateWorkItemRevisionAuthorityPins, revisionBenefitContractDigest } from '@dsh-enhanced/assistant-growth-contract';
14
15
  import { TaskObservationRuntime, validateTaskObservationConfig } from './task-observation-runtime.js';
15
16
  import { LiveQualificationRuntime, validateLiveQualificationConfig } from './live-qualification-runtime.js';
16
17
  import { rollbackPluginWatch } from './cli.js';
17
- import { assertWorkItemRunReference, inertCreationData, WORK_ITEM_ENGINEERING_PROTOCOL } from './work-item-creation-contract.js';
18
+ import { assertWorkItemRunReference, assertWorkItemRevisionRunReference, inertCreationData, WORK_ITEM_ENGINEERING_PROTOCOL } from './work-item-creation-contract.js';
18
19
  import { OwnerTaskFailureGaps } from './owner-task-gaps.js';
19
20
  import { Context, Service } from '@deepseek-ai/cordis';
20
21
  import Schema from '@deepseek-ai/schemastery';
@@ -28,8 +29,8 @@ import { Ed25519SourceReleaseAuthorizationAuthority } from './release.js';
28
29
  import { advanceSourceRelease, sourceReleaseAuthorities, validateSourceReleaseExecutionConfig } from './source-release-runner.js';
29
30
  import { adoptSourceRelease, validateSourceAdoptionConfig } from './source-adoption-runner.js';
30
31
  import { Ed25519ApprovalAuthority } from './approval.js';
31
- import { ControlPlaneStore, MODIFY_GENERATOR_DIGEST, controlPlaneDigest } from './store.js';
32
- import { runDockerPreparedChecks, validateSourceBuildConfig } from './source-build.js';
32
+ import { ControlPlaneStore, MODIFY_GENERATOR_DIGEST, controlPlaneDigest, createOwnerInstructionReviewFreeze } from './store.js';
33
+ import { validateSourceBuildConfig } from './source-build.js';
33
34
  import { runNativePreparedChecks } from './native-source-build.js';
34
35
  import { LINUX_PREPARED_SOURCE_BUILD_SCRIPT, LINUX_SOURCE_PACK_PATH, runLinuxPreparedChecks } from './linux-source-build.js';
35
36
  import { LinuxRepositoryRunner, NativeMacVerifierRunner, linuxRuntimeDigest, verifyLinuxRuntime } from '@dsh-enhanced/assistant-isolation';
@@ -40,7 +41,8 @@ import { inspectSourceCreationContext, prepareCreatedPluginWorkspace, validateSo
40
41
  import { resolveSourceBaseline } from './source-baseline.js';
41
42
  import { inheritedEnvironment, loadTrustConfig, resolveTrustKey } from './trust.js';
42
43
  import { registerPluginControlTools } from './tools.js';
43
- import { SourceGrowthRunUnavailableError, SourceJobRuntime, normalizeSourceJobsConfig } from './source-jobs.js';
44
+ import { SourceGrowthRunUnavailableError, SourceJobRuntime, normalizeSourceJobsConfig, instructionReviewWindowFor } from './source-jobs.js';
45
+ import { drainOwnerInstructionRequests as drainPendingOwnerInstructionRequests, OWNER_INSTRUCTION_CLAIM_TIMEOUT_MS } from './owner-instruction-drain.js';
44
46
  import { installRuntimeObserver, validateRuntimeObserverConfig } from './runtime-observer.js';
45
47
  import { installReplayEndpoint, validateReplayEndpointConfig } from './replay-endpoint.js';
46
48
  import { readPrivateRuntimeObserverKey } from './runtime-observer-protocol.js';
@@ -250,9 +252,15 @@ export function normalizeControlPlaneConfig(input) {
250
252
  const verification = config.creationVerifications;
251
253
  const jobs = config.sourceJobs;
252
254
  const build = config.sourceBuild;
253
- const buildBackend = build && 'backend' in build ? build.backend : 'docker';
254
- const adoptionBackend = 'backend' in capability.runner ? capability.runner.backend : 'docker';
255
- if (!build || buildBackend !== adoptionBackend) {
255
+ if (!build || !('backend' in build)) {
256
+ throw new Error('plugin-control-plane: creation build requires a native execution backend');
257
+ }
258
+ const buildBackend = build.backend;
259
+ if (!('backend' in capability.runner)) {
260
+ throw new Error('plugin-control-plane: creation adoption runner requires a native execution backend');
261
+ }
262
+ const adoptionBackend = capability.runner.backend;
263
+ if (buildBackend !== adoptionBackend) {
256
264
  throw new Error('plugin-control-plane: creation build, verification and adoption require matching execution backends');
257
265
  }
258
266
  if (!verification || !jobs?.creation || capability.namePrefix !== jobs.creation.namePrefix
@@ -329,6 +337,7 @@ export class PluginControlPlaneService extends Service {
329
337
  growthRunProducer;
330
338
  growthProviderDraining = false;
331
339
  workItemCreationFrame;
340
+ workItemRevisionFrame;
332
341
  constructor(ctx, input) {
333
342
  super(ctx, 'pluginControlPlane');
334
343
  this.config = normalizeControlPlaneConfig(input);
@@ -603,6 +612,11 @@ export class PluginControlPlaneService extends Service {
603
612
  if (typeof current('automations')[method] !== 'function')
604
613
  throw new Error(`plugin-control-plane: durable source jobs require assistantAutomations.${method}`);
605
614
  }
615
+ const instructionDrainEnabled = this.config.sourceJobs?.revision !== undefined
616
+ && this.config.sourceJobs.allowedModes?.includes('revise-created') !== false;
617
+ if (instructionDrainEnabled && typeof current('automations').onSchedulerWake !== 'function') {
618
+ throw new Error('plugin-control-plane: owner instruction drain requires assistantAutomations.onSchedulerWake');
619
+ }
606
620
  if (typeof current('delivery').validateOwnerRoute !== 'function')
607
621
  throw new Error('plugin-control-plane: durable source jobs require Delivery v2 owner validation');
608
622
  let capabilities;
@@ -768,6 +782,13 @@ export class PluginControlPlaneService extends Service {
768
782
  await capabilities.verifyArtifactExecution(certificate, signal);
769
783
  },
770
784
  matchesExecution: certificate => capabilities?.matchesArtifactExecution(certificate) === true,
785
+ inspectCall: (_record, execution, toolAlias, argumentsJson) => {
786
+ if (!execution.agent || typeof delivery.inspectOwnerForegroundToolCall !== 'function')
787
+ return undefined;
788
+ return delivery.inspectOwnerForegroundToolCall({ agent: execution.agent,
789
+ authorityId: config.owner.authorityId, callId: String(execution.callId),
790
+ toolName: toolAlias, argumentsJson });
791
+ },
771
792
  assertCaller: execution => {
772
793
  if (!execution.agent)
773
794
  throw new Error('revision capability requires an authenticated owner Agent');
@@ -794,24 +815,25 @@ export class PluginControlPlaneService extends Service {
794
815
  const sourceBuild = this.config.sourceBuild;
795
816
  let nativeRunner;
796
817
  try {
797
- nativeRunner = !('backend' in sourceBuild) ? undefined : sourceBuild.backend === 'macos-seatbelt'
798
- ? { backend: 'macos-seatbelt', runner: new NativeMacVerifierRunner({ backend: 'macos-seatbelt',
799
- stateRoot: sourceBuild.stateRoot, authorityDigest: sourceBuild.authorityDigest,
800
- runtime: sourceBuild.runtime, source: NATIVE_SOURCE_COMPILER, expiresAt: sourceBuild.expiresAt,
801
- maxRuns: sourceBuild.maxRuns, maxTotalDurationMs: sourceBuild.maxTotalDurationMs,
802
- maxDurationMs: sourceBuild.timeoutMs, maxOutputBytes: sourceBuild.outputBytes }) }
803
- : { backend: 'linux-native', runner: new LinuxRepositoryRunner({ backend: 'linux-native',
804
- stateRoot: sourceBuild.stateRoot, authorityDigest: sourceBuild.authorityDigest,
805
- runtime: sourceBuild.runtime, cgroupRoots: sourceBuild.cgroupRoots, expiresAt: sourceBuild.expiresAt,
806
- maxRuns: sourceBuild.maxRuns, maxTotalDurationMs: sourceBuild.maxTotalDurationMs,
807
- ...(sourceBuild.stateSeal === undefined ? {} : { stateSeal: sourceBuild.stateSeal }),
808
- ...(sourceBuild.userNamespace === undefined ? {} : { userNamespace: sourceBuild.userNamespace,
809
- systemSnapshot: sourceBuild.systemSnapshot }),
810
- limits: { maxDurationMs: sourceBuild.timeoutMs, maxInputBytes: 64 * 1024 ** 2,
811
- maxOutputBytes: sourceBuild.outputBytes, maxArtifactBytes: 32 * 1024 ** 2, maxFiles: 128,
812
- memoryMiB: sourceBuild.memoryMiB, cpus: sourceBuild.cpus, pidsLimit: sourceBuild.pidsLimit,
813
- workspaceMiB: sourceBuild.workspaceMiB, workspaceInodes: sourceBuild.workspaceInodes,
814
- tmpMiB: sourceBuild.temporaryMiB, tmpInodes: sourceBuild.temporaryInodes, inputMiB: 65, inputInodes: 16 } }) };
818
+ nativeRunner = !('backend' in sourceBuild) ? undefined
819
+ : sourceBuild.backend === 'macos-seatbelt'
820
+ ? { backend: 'macos-seatbelt', runner: new NativeMacVerifierRunner({ backend: 'macos-seatbelt',
821
+ stateRoot: sourceBuild.stateRoot, authorityDigest: sourceBuild.authorityDigest,
822
+ runtime: sourceBuild.runtime, source: NATIVE_SOURCE_COMPILER, expiresAt: sourceBuild.expiresAt,
823
+ maxRuns: sourceBuild.maxRuns, maxTotalDurationMs: sourceBuild.maxTotalDurationMs,
824
+ maxDurationMs: sourceBuild.timeoutMs, maxOutputBytes: sourceBuild.outputBytes }) }
825
+ : { backend: 'linux-native', runner: new LinuxRepositoryRunner({ backend: 'linux-native',
826
+ stateRoot: sourceBuild.stateRoot, authorityDigest: sourceBuild.authorityDigest,
827
+ runtime: sourceBuild.runtime, cgroupRoots: sourceBuild.cgroupRoots, expiresAt: sourceBuild.expiresAt,
828
+ maxRuns: sourceBuild.maxRuns, maxTotalDurationMs: sourceBuild.maxTotalDurationMs,
829
+ ...(sourceBuild.stateSeal === undefined ? {} : { stateSeal: sourceBuild.stateSeal }),
830
+ ...(sourceBuild.userNamespace === undefined ? {} : { userNamespace: sourceBuild.userNamespace,
831
+ systemSnapshot: sourceBuild.systemSnapshot }),
832
+ limits: { maxDurationMs: sourceBuild.timeoutMs, maxInputBytes: 64 * 1024 ** 2,
833
+ maxOutputBytes: sourceBuild.outputBytes, maxArtifactBytes: 32 * 1024 ** 2, maxFiles: 128,
834
+ memoryMiB: sourceBuild.memoryMiB, cpus: sourceBuild.cpus, pidsLimit: sourceBuild.pidsLimit,
835
+ workspaceMiB: sourceBuild.workspaceMiB, workspaceInodes: sourceBuild.workspaceInodes,
836
+ tmpMiB: sourceBuild.temporaryMiB, tmpInodes: sourceBuild.temporaryInodes, inputMiB: 65, inputInodes: 16 } }) };
815
837
  }
816
838
  catch (error) {
817
839
  unsubscribeTaskChanges?.();
@@ -838,6 +860,7 @@ export class PluginControlPlaneService extends Service {
838
860
  ports: {
839
861
  automations: {
840
862
  registerHostExecutor: executor => current('automations').registerHostExecutor(executor),
863
+ onSchedulerWake: listener => current('automations').onSchedulerWake(listener),
841
864
  reconcileSystem: request => current('automations').reconcileSystem(request),
842
865
  reconcileSystemExact: request => current('automations').reconcileSystemExact(request),
843
866
  pauseSystemOwned: request => current('automations').pauseSystemOwned(request),
@@ -846,7 +869,7 @@ export class PluginControlPlaneService extends Service {
846
869
  },
847
870
  delivery: { validateOwnerRoute: request => current('delivery').validateOwnerRoute(request) },
848
871
  }, withGapSourceFence: (gapId, owner, callback) => this.taskGaps.withCurrent(gapId, owner, callback),
849
- withWorkItemGrowthRunFence: (run, gapId, owner, generation, callback) => this.withWorkItemGrowthFrame(run, gapId, owner, generation, callback),
872
+ withWorkItemGrowthRunFence: (run, gapId, owner, generation, callback) => run.protocol === 'assistant-growth/source-run/v3' ? this.withWorkItemRevisionFrame(run, gapId, owner, generation, callback) : this.withWorkItemGrowthFrame(run, gapId, owner, generation, callback),
850
873
  assertGrowthRun: (run, gapId, owner, generation) => this.assertSourceGrowthRun(run, gapId, owner, generation),
851
874
  ...(this.config.revisionVerifications ? { inspectRevisionParent: this.inspectRevisionParent,
852
875
  verifyPreparedRevision: (job, signal) => this.verifyPreparedRevision(job, signal, () => jobsCtx.get('assistantVerifier')) } : {}),
@@ -886,6 +909,14 @@ export class PluginControlPlaneService extends Service {
886
909
  throw new Error('source job has no released plan');
887
910
  await this.adoptOwnerSourceRelease({ sourcePlanId: job.planId, signal, expectedTrustDigest: job.intent.trustDigest, assertCurrent });
888
911
  } } : {}),
912
+ ...(this.config.sourceJobs?.revisionBenefit === undefined ? {} : {
913
+ reconcileRevisionBenefits: () => this.reconcileRevisionBenefits(),
914
+ allowReviewOnlyFirstAdoption: this.config.sourceJobs.allowReviewOnlyFirstAdoption === true,
915
+ }),
916
+ ...(instructionDrainEnabled ? {
917
+ hasOwnerInstructionRequests: () => this.store.hasOwnerInstructionRequestWork(Date.now(), OWNER_INSTRUCTION_CLAIM_TIMEOUT_MS),
918
+ drainOwnerInstructionRequests: (signal) => this.drainOwnerInstructionRequests(signal),
919
+ } : {}),
889
920
  trust: () => this.boundTrust(), prepare: (job, signal, assertCurrent) => this.prepareSourceJob(job, signal, assertCurrent, nativeRunner?.backend === 'linux-native' ? { runner: nativeRunner.runner, generation: preparationGeneration } : undefined),
890
921
  ...(nativeRunner?.backend === 'linux-native' ? { isPreparationNotStarted: (error, job) => consumeCleanedLinuxSourcePreflight(error, job, preparationGeneration),
891
922
  deterministicBuildFailure: (error, job) => consumeCleanedLinuxSourceBuildFailure(error, job, preparationGeneration) } : {}),
@@ -1313,6 +1344,40 @@ export class PluginControlPlaneService extends Service {
1313
1344
  this.abort.signal.throwIfAborted();
1314
1345
  return this.revisionCapabilityRuntime?.inspectCallEvidence(entryId) ?? [];
1315
1346
  };
1347
+ /**
1348
+ * Host-only P1-4 negative-observation rollback of one active revision: after
1349
+ * the observation window, a recorded `inferior` benefit outcome rolls the
1350
+ * stable entry back to its exact parent under the separate rollback grant,
1351
+ * and stops this lineage from preparing further spontaneous revisions until
1352
+ * the owner re-authorizes. Anything else leaves the adoption untouched.
1353
+ */
1354
+ autoRollbackRevisionIfInferior = async (planId, options) => {
1355
+ this.abort.signal.throwIfAborted();
1356
+ const runtime = this.revisionCapabilityRuntime;
1357
+ const contract = this.config.sourceJobs?.revisionBenefit;
1358
+ if (runtime === undefined || contract === undefined) {
1359
+ return { rolledBack: false, reason: 'benefit-not-configured' };
1360
+ }
1361
+ const intent = this.store.inspectRevisionBenefitIntent(planId);
1362
+ const pinned = intent?.pinnedContractDigest === undefined ? undefined : { contractDigest: intent.pinnedContractDigest };
1363
+ const liveDigest = revisionBenefitContractDigest(contract);
1364
+ const plan = this.store.getSourcePlan(planId);
1365
+ const result = await autoRollbackInferiorRevision({
1366
+ runtime: { listEntries: () => runtime.listEntries(), inspectEntry: entryId => runtime.inspectEntry(entryId),
1367
+ rollback: input => runtime.rollback(input) },
1368
+ planId, benefitConfigured: true,
1369
+ pinnedContractDigest: pinned !== undefined && pinned.contractDigest === liveDigest ? pinned.contractDigest : undefined,
1370
+ growthRunCreatedAt: plan.sourceRevision?.growthRun?.createdAt,
1371
+ observeWindowMs: options.observeWindowMs,
1372
+ ...(options.observationDeadline === undefined ? {} : { observationDeadline: options.observationDeadline }),
1373
+ outcome: this.store.inspectRevisionBenefitOutcome(planId), now: Date.now(),
1374
+ ...(options.signal === undefined ? {} : {
1375
+ rollback: (target, input) => target.rollback({ ...input, signal: options.signal }),
1376
+ }),
1377
+ });
1378
+ return { rolledBack: result.rolledBack, reason: result.reason,
1379
+ ...(result.record === undefined ? {} : { record: result.record }) };
1380
+ };
1316
1381
  /**
1317
1382
  * Host-only authenticated-owner exact parent rollback under its own finite grant. The parent
1318
1383
  * adoption receipt must still be current and the entry must be on the exact expected revision.
@@ -1452,6 +1517,143 @@ export class PluginControlPlaneService extends Service {
1452
1517
  return undefined;
1453
1518
  }
1454
1519
  };
1520
+ /**
1521
+ * Host-only P1-4 benefit evaluation of one prepared revision. Windows come
1522
+ * from the revision journal's foreground-attributed entry calls (parent
1523
+ * version before adoption, candidate version after it), never from the
1524
+ * creation-capability association ledger. The outcome is recorded only for a
1525
+ * revision job whose grant pins this exact contract digest. Never adoption
1526
+ * evidence by itself and never model-callable.
1527
+ */
1528
+ /**
1529
+ * Host-only terminal read: the benefit ledger outcome/reason plus the latest
1530
+ * exact-parent rollback from the revision journal. Not a model-callable tool.
1531
+ */
1532
+ inspectRevisionBenefitTerminal = (planId) => inspectRevisionBenefitTerminal({ planId, outcome: this.store.inspectRevisionBenefitOutcome(planId),
1533
+ runtime: this.revisionCapabilityRuntime ?? { inspectBenefitEvidence: () => undefined, listEntries: () => [],
1534
+ inspectLatestRollback: () => undefined } });
1535
+ evaluateRevisionBenefit = (planId) => {
1536
+ const contract = this.config.sourceJobs?.revisionBenefit;
1537
+ const plan = this.store.getSourcePlan(planId);
1538
+ const runtime = this.revisionCapabilityRuntime;
1539
+ // The benefit rows are owner-scope evidence; adoption and creation
1540
+ // capabilities are validated to carry the same owner when both are set.
1541
+ const owner = this.config.revisionCapabilities?.owner ?? this.config.creationCapabilities?.owner;
1542
+ if (!contract || !runtime || !owner || plan.mode !== 'prepared-revise'
1543
+ || !plan.sourceRevision?.parent)
1544
+ return undefined;
1545
+ const intent = this.store.inspectRevisionBenefitIntent(planId);
1546
+ if (intent === undefined || intent.pinnedContractDigest === undefined || !intent.growthRunAcceptance)
1547
+ return undefined;
1548
+ const pinned = { contractDigest: intent.pinnedContractDigest };
1549
+ const evaluationPort = this.ctx.get('assistantEvaluation', false);
1550
+ const delivery = this.ctx.get('assistantDelivery', false);
1551
+ if (!evaluationPort || !delivery)
1552
+ return undefined;
1553
+ // Candidate evidence is the revision journal's post-adoption foreground
1554
+ // calls. Parent evidence combines its creation-adoption associations (before
1555
+ // the switch) with revision-journal parent-version calls (after a rollback or
1556
+ // for a chained revision parent); the two sources are disjoint by entry
1557
+ // lifetime. Windows are equal-length and adjacent around the adoption.
1558
+ const candidate = runtime.inspectBenefitEvidence(planId);
1559
+ if (!candidate)
1560
+ return undefined;
1561
+ const parentPlanId = plan.sourceRevision.parent.planId;
1562
+ const parentRevision = runtime.inspectBenefitEvidence(parentPlanId);
1563
+ const parentInput = this.createdCapabilityFeedbackInput(parentPlanId);
1564
+ const parentVersion = parentRevision?.version ?? (() => {
1565
+ const receipt = parentInput?.().record.receipt;
1566
+ return receipt === undefined ? undefined : { adoptedAt: receipt.adoptedAt, expiresAt: receipt.expiresAt };
1567
+ })();
1568
+ if (parentVersion === undefined)
1569
+ return undefined;
1570
+ const now = Date.now();
1571
+ const windows = revisionBenefitPairedWindows({ candidate, parent: parentVersion, now });
1572
+ if (windows === undefined)
1573
+ return undefined;
1574
+ const creationStats = parentInput === undefined ? undefined
1575
+ : inspectCreationCapabilityBenefitWindow({ ...parentInput(), window: windows.parent });
1576
+ const revisionParentStats = parentRevision === undefined ? undefined
1577
+ : inspectRevisionCapabilityBenefitWindow({ sample: parentRevision, windowStart: windows.parent.start,
1578
+ windowEnd: windows.parent.end, owner, evaluation: evaluationPort, delivery, now });
1579
+ const parent = creationStats === undefined ? revisionParentStats
1580
+ : revisionParentStats === undefined ? creationStats : mergeRevisionBenefitWindows(creationStats, revisionParentStats);
1581
+ if (parent === undefined)
1582
+ return undefined;
1583
+ const result = evaluateRevisionBenefitEvidence({ contract, candidate, parent, windows, owner,
1584
+ evaluation: evaluationPort, delivery, now });
1585
+ if (result === undefined)
1586
+ return undefined;
1587
+ if (result.contractDigest === pinned.contractDigest) {
1588
+ this.store.recordRevisionBenefit(planId, { contractDigest: result.contractDigest,
1589
+ status: result.evaluation.verdict.status, reason: result.evaluation.verdict.reason,
1590
+ evaluation: result.evaluation });
1591
+ }
1592
+ return { planId, contractDigest: result.contractDigest,
1593
+ pinned: result.contractDigest === pinned.contractDigest, evaluation: result.evaluation };
1594
+ };
1595
+ /**
1596
+ * Host-only P1-4 reconciliation over every prepared revision plan: after its
1597
+ * observation window, evaluate the benefit outcome and roll back an active
1598
+ * inferior revision. Driven by the SourceJobs continuation cron occurrence;
1599
+ * never a new loop and never model-callable.
1600
+ */
1601
+ reconcileRevisionBenefits = () => {
1602
+ const contract = this.config.sourceJobs?.revisionBenefit;
1603
+ const runtime = this.revisionCapabilityRuntime;
1604
+ if (!contract || !runtime)
1605
+ return;
1606
+ const contractDigest = revisionBenefitContractDigest(contract);
1607
+ for (const plan of this.store.listPreparedSourcePlans()) {
1608
+ if (plan.mode !== 'prepared-revise')
1609
+ continue;
1610
+ try {
1611
+ const intent = this.store.inspectRevisionBenefitIntent(plan.id);
1612
+ if (intent?.pinnedContractDigest === undefined || intent.pinnedContractDigest !== contractDigest)
1613
+ continue;
1614
+ const evidence = runtime.inspectBenefitEvidence(plan.id);
1615
+ if (evidence === undefined)
1616
+ continue;
1617
+ const now = Date.now();
1618
+ const parentVersion = this.revisionBenefitParentVersion(plan);
1619
+ const deadline = parentVersion === undefined ? undefined
1620
+ : revisionBenefitObservationDeadline({ candidate: evidence, parent: parentVersion, now });
1621
+ if (deadline === undefined)
1622
+ continue;
1623
+ const evaluation = this.evaluateRevisionBenefit(plan.id);
1624
+ const outcome = this.store.inspectRevisionBenefitOutcome(plan.id);
1625
+ if (evaluation !== undefined && evaluation.pinned && evaluation.evaluation.verdict.status === 'inferior') {
1626
+ void this.autoRollbackRevisionIfInferior(plan.id, { observeWindowMs: 0 })
1627
+ .catch(() => { });
1628
+ continue;
1629
+ }
1630
+ // Bounded terminal: once the observation window elapsed without a
1631
+ // non-inferior verdict, record the owner-visible unverified result and
1632
+ // roll the stable entry back instead of leaving a silent mount.
1633
+ if (now < deadline || outcome?.status === 'non-inferior')
1634
+ continue;
1635
+ if (outcome === undefined || outcome.status === 'insufficient') {
1636
+ this.store.recordRevisionBenefit(plan.id, { contractDigest, status: 'insufficient',
1637
+ reason: 'observation-expired-unverified',
1638
+ evaluation: evaluation?.evaluation ?? { verdict: { status: 'insufficient', reason: 'observation-expired-unverified' } } });
1639
+ }
1640
+ void this.autoRollbackRevisionIfInferior(plan.id, { observeWindowMs: 0, observationDeadline: deadline })
1641
+ .catch(() => { });
1642
+ }
1643
+ catch { /* a plan without a current job or admission is skipped this tick */ }
1644
+ }
1645
+ };
1646
+ /** Parent adoption version for the same equal-length paired window as evaluation. */
1647
+ revisionBenefitParentVersion(plan) {
1648
+ const parentPlanId = plan.sourceRevision?.parent?.planId;
1649
+ if (parentPlanId === undefined)
1650
+ return undefined;
1651
+ const revision = this.revisionCapabilityRuntime?.inspectBenefitEvidence(parentPlanId);
1652
+ if (revision !== undefined)
1653
+ return revision.version;
1654
+ const receipt = this.createdCapabilityFeedbackInput(parentPlanId)?.().record.receipt;
1655
+ return receipt === undefined ? undefined : { adoptedAt: receipt.adoptedAt, expiresAt: receipt.expiresAt };
1656
+ }
1455
1657
  /**
1456
1658
  * Host-only, owner-invoked: seal the current ordinary-use trend of one adoption into the append-only,
1457
1659
  * content-addressed snapshot ledger. Re-sealing an unchanged trend returns the existing row (`created: false`);
@@ -1514,7 +1716,8 @@ export class PluginControlPlaneService extends Service {
1514
1716
  const growth = plan.creation?.growthRun;
1515
1717
  const artifactSha256 = createHash('sha256').update(record.artifact).digest('hex');
1516
1718
  if (!certificate || controlPlaneDigest(certificate) !== controlPlaneDigest(record.certificate)
1517
- || !growth || !plan.creation || !job.intent.creation?.growthRun || !plan.sourceCheck || !plan.preparedEvidence
1719
+ || !growth || growth.protocol === 'assistant-growth/source-run/v3'
1720
+ || !plan.creation || !job.intent.creation?.growthRun || !plan.sourceCheck || !plan.preparedEvidence
1518
1721
  || plan.id !== receipt.planId || plan.digest !== receipt.planDigest
1519
1722
  || plan.name !== record.certificate.plan.name || plan.generatorDigest !== record.certificate.plan.generatorDigest
1520
1723
  || plan.sourceCheck.treeDigest !== record.certificate.plan.sourceTreeDigest
@@ -1566,7 +1769,8 @@ export class PluginControlPlaneService extends Service {
1566
1769
  const ownerGrant = this.config.creationCapabilities?.owner;
1567
1770
  const artifactSha256 = createHash('sha256').update(record.artifact).digest('hex');
1568
1771
  if (!certificate || controlPlaneDigest(certificate) !== controlPlaneDigest(record.certificate)
1569
- || !growth || !creation || !ownerGrant || !job.intent.creation?.growthRun
1772
+ || !growth || growth.protocol === 'assistant-growth/source-run/v3'
1773
+ || !creation || !ownerGrant || !job.intent.creation?.growthRun
1570
1774
  || !plan.sourceCheck || !plan.preparedEvidence
1571
1775
  || plan.id !== receipt.planId || plan.digest !== receipt.planDigest
1572
1776
  || plan.name !== record.certificate.plan.name || plan.generatorDigest !== record.certificate.plan.generatorDigest
@@ -1674,7 +1878,7 @@ export class PluginControlPlaneService extends Service {
1674
1878
  throw new Error('work item growth authority pins changed');
1675
1879
  const source = this.taskGaps.engineeringSnapshot(gapId, owner);
1676
1880
  return producer.withWorkItemRunFence({ runId: run.runId, intentDigest: run.intentDigest, expectedGrowthRunDigest: sourceGrowthRunDigest(run) }, actual => {
1677
- if (this.growthRunProducer !== producer || sourceGrowthRunDigest(actual) !== sourceGrowthRunDigest(run) || this.workItemCreationFrame)
1881
+ if (this.growthRunProducer !== producer || sourceGrowthRunDigest(actual) !== sourceGrowthRunDigest(run) || actual.protocol !== 'assistant-growth/source-run/v2' || this.workItemCreationFrame)
1678
1882
  throw new Error('work item producer changed its true binding');
1679
1883
  this.workItemCreationFrame = { gapId, run: actual, source };
1680
1884
  try {
@@ -1685,6 +1889,121 @@ export class PluginControlPlaneService extends Service {
1685
1889
  }
1686
1890
  });
1687
1891
  }
1892
+ inspectWorkItemPreparedRevisionProtocol = () => {
1893
+ return !this.abort.signal.aborted && this.config.sourceJobs?.revision?.workItemFailures === true && this.sourceRuntime?.available() ? 'dsh-prepared-revision/v2' : undefined;
1894
+ };
1895
+ inspectWorkItemRevisionReadiness = (input) => {
1896
+ try {
1897
+ this.abort.signal.throwIfAborted();
1898
+ const owner = inertCreationData(input).owner, config = this.config, source = this.sourceRuntime?.inspectWorkItemRevisionReadiness(owner);
1899
+ if (!source || !config.sourceJobs || !config.sourceBuild || config.sourceJobs.revision?.workItemFailures !== true || !config.revisionVerifications)
1900
+ return undefined;
1901
+ const delivery = this.ctx.get('assistantDelivery', false);
1902
+ const verifier = this.ctx.get('assistantVerifier', false);
1903
+ if (typeof delivery?.withOwnerWorkItemEngineeringContextFence !== 'function' || typeof verifier?.inspectWorkItemRevisionReviewReadiness !== 'function')
1904
+ return undefined;
1905
+ const actual = delivery.validateOwnerRoute({ authorityId: owner.authorityId, principalId: owner.principalId, workspace: owner.workspace, agentPreset: owner.agentPreset });
1906
+ if (!isSourceOwnerContinuation(actual, owner))
1907
+ return undefined;
1908
+ const review = verifier.inspectWorkItemRevisionReviewReadiness();
1909
+ if (!review || review.protocol !== 'assistant-growth/work-item-revision-review-readiness/v1'
1910
+ || review.sourceProtocol !== WORK_ITEM_ENGINEERING_PROTOCOL || review.runProtocol !== 'assistant-growth/source-run/v3'
1911
+ || review.workItemFailures !== true || controlPlaneDigest(review.authority) !== controlPlaneDigest(config.revisionVerifications.authority)
1912
+ || !createPublicKey(review.publicKey).export({ type: 'spki', format: 'der' }).equals(createPublicKey(config.revisionVerifications.publicKey).export({ type: 'spki', format: 'der' })))
1913
+ return undefined;
1914
+ const adoption = this.revisionCapabilityRuntime?.inspectReadiness();
1915
+ const adoptionPins = config.revisionCapabilities && adoption ? {
1916
+ authorityId: adoption.authority.authorityId,
1917
+ authorityDigest: revisionCapabilityAuthorityDigest({ path: join(config.statePath, 'revision-adoptions.sqlite'), config: config.revisionCapabilities }),
1918
+ expiresAt: adoption.authority.expiresAt
1919
+ } : undefined;
1920
+ if (adoption !== undefined && !adoptionPins)
1921
+ return undefined;
1922
+ const pins = { protocol: 'assistant-growth/work-item-revision-authorities/v1',
1923
+ sourceJobs: { authorityId: source.authority.id, authorityDigest: source.authority.digest, expiresAt: source.authority.expiresAt },
1924
+ revisionGrant: { id: source.revision.id, digest: controlPlaneDigest(source.revision), expiresAt: source.revision.expiresAt, workItemFailures: true },
1925
+ revisionAcceptance: structuredClone(review.authority),
1926
+ ...(adoptionPins ? { revisionAdoption: adoptionPins } : {}) };
1927
+ validateWorkItemRevisionAuthorityPins(pins);
1928
+ if (source.authority.digest !== controlPlaneDigest({ config: config.sourceJobs, build: config.sourceBuild }))
1929
+ return undefined;
1930
+ const expiries = [pins.sourceJobs.expiresAt, pins.revisionGrant.expiresAt, pins.revisionAcceptance.expiresAt, ...(pins.revisionAdoption ? [pins.revisionAdoption.expiresAt] : [])];
1931
+ if (expiries.some(expiry => Date.now() >= expiry))
1932
+ return undefined;
1933
+ const remaining = { ...source.remaining, verifications: review.remainingVerifications, ...(adoption ? { adoptions: adoption.remainingAdoptions } : {}) };
1934
+ if (Object.values(remaining).some(count => !Number.isSafeInteger(count) || count < 0))
1935
+ return undefined;
1936
+ return { protocol: 'assistant-growth/work-item-revision-readiness/v1', sourceProtocol: WORK_ITEM_ENGINEERING_PROTOCOL, runProtocol: 'assistant-growth/source-run/v3', workItemFailures: true, pins, remaining, available: Object.values(remaining).every(count => count > 0) && review.available && (adoption ? adoption.available : true) };
1937
+ }
1938
+ catch {
1939
+ return undefined;
1940
+ }
1941
+ };
1942
+ assertWorkItemRevisionPinsLocal(run) {
1943
+ const jobs = this.config.sourceJobs, verification = this.config.revisionVerifications;
1944
+ if (!jobs || !this.config.sourceBuild || jobs.revision?.workItemFailures !== true || !verification
1945
+ || run.authorities.sourceJobs.authorityId !== jobs.authorityId || run.authorities.sourceJobs.authorityDigest !== controlPlaneDigest({ config: jobs, build: this.config.sourceBuild }) || run.authorities.sourceJobs.expiresAt !== jobs.expiresAt
1946
+ || run.authorities.revisionGrant.id !== jobs.revision.id || run.authorities.revisionGrant.digest !== controlPlaneDigest(jobs.revision) || run.authorities.revisionGrant.expiresAt !== jobs.revision.expiresAt
1947
+ || run.authorities.revisionGrant.workItemFailures !== true
1948
+ || controlPlaneDigest(run.revisionAcceptance) !== controlPlaneDigest(verification.authority) || controlPlaneDigest(run.authorities.revisionAcceptance) !== controlPlaneDigest(verification.authority)
1949
+ || (run.authorities.revisionAdoption !== undefined) !== (this.config.revisionCapabilities !== undefined))
1950
+ throw new Error('work item revision authority pins changed');
1951
+ if (run.authorities.revisionAdoption !== undefined && this.config.revisionCapabilities !== undefined) {
1952
+ const adoption = this.config.revisionCapabilities;
1953
+ if (run.authorities.revisionAdoption.authorityId !== adoption.authorityId
1954
+ || run.authorities.revisionAdoption.authorityDigest !== revisionCapabilityAuthorityDigest({ path: join(this.config.statePath, 'revision-adoptions.sqlite'), config: adoption })
1955
+ || run.authorities.revisionAdoption.expiresAt !== adoption.expiresAt)
1956
+ throw new Error('work item revision authority pins changed');
1957
+ }
1958
+ if (run.authorities.revisionRegressionAcceptance !== undefined) {
1959
+ const regression = this.config.revisionRegressions;
1960
+ if (!regression || controlPlaneDigest(run.authorities.revisionRegressionAcceptance) !== controlPlaneDigest(regression.authority))
1961
+ throw new Error('work item revision authority pins changed');
1962
+ }
1963
+ const expiries = [run.authorities.sourceJobs.expiresAt, run.authorities.revisionGrant.expiresAt, run.authorities.revisionAcceptance.expiresAt, ...(run.authorities.revisionAdoption ? [run.authorities.revisionAdoption.expiresAt] : [])];
1964
+ if (expiries.some(expiry => Date.now() >= expiry))
1965
+ throw new Error('work item revision authority pins changed or expired');
1966
+ }
1967
+ assertWorkItemRevisionRunLocal(run, gapId, owner, generation) {
1968
+ const reference = this.store.getOwnerTaskFailureReference(gapId);
1969
+ if (!reference || reference.schemaVersion !== 2 || run.ownerDigest !== controlPlaneDigest(reference.owner) || !isSourceOwnerContinuation(owner, reference.owner))
1970
+ throw new Error('work item revision owner reference changed');
1971
+ assertWorkItemRevisionRunReference(run, reference);
1972
+ this.assertWorkItemRevisionPinsLocal(run);
1973
+ if (Date.now() >= run.expiresAt || generation && Date.now() >= run.generationDeadlineAt)
1974
+ throw new Error('work item revision source run expired');
1975
+ if (run.modelOrigin === 'inherited-owner-task' && this.workItemRevisionFrame && controlPlaneDigest(this.workItemRevisionFrame.source.task.proofIdentity.supplier) !== controlPlaneDigest(run.model))
1976
+ throw new Error('work item revision source supplier changed');
1977
+ }
1978
+ withWorkItemRevisionFrame(run, gapId, owner, generation, callback) {
1979
+ this.abort.signal.throwIfAborted();
1980
+ if (this.workItemRevisionFrame) {
1981
+ if (this.workItemRevisionFrame.gapId !== gapId || sourceGrowthRunDigest(this.workItemRevisionFrame.run) !== sourceGrowthRunDigest(run))
1982
+ throw new Error('work item revision frame reentry');
1983
+ this.assertWorkItemRevisionRunLocal(run, gapId, owner, generation);
1984
+ return callback();
1985
+ }
1986
+ const producer = this.growthRunProducer;
1987
+ if (!producer || producer.protocol !== 'assistant-growth-source-run-producer/v2')
1988
+ throw new SourceGrowthRunUnavailableError('work item revision growth producer unavailable');
1989
+ const readiness = this.inspectWorkItemRevisionReadiness({ owner });
1990
+ if (!readiness)
1991
+ throw new SourceGrowthRunUnavailableError('work item revision growth peers unavailable');
1992
+ if (controlPlaneDigest(readiness.pins) !== controlPlaneDigest(run.authorities))
1993
+ throw new Error('work item revision authority pins changed');
1994
+ const source = this.taskGaps.engineeringSnapshot(gapId, owner);
1995
+ return producer.withWorkItemRunFence({ runId: run.runId, intentDigest: run.intentDigest, expectedGrowthRunDigest: sourceGrowthRunDigest(run) }, actual => {
1996
+ if (this.growthRunProducer !== producer || sourceGrowthRunDigest(actual) !== sourceGrowthRunDigest(run) || actual.protocol !== 'assistant-growth/source-run/v3' || this.workItemRevisionFrame)
1997
+ throw new Error('work item revision producer changed its true binding');
1998
+ this.workItemRevisionFrame = { gapId, run: actual, source };
1999
+ try {
2000
+ return this.store.withOwnerTaskFailureGapAdmission(gapId, () => { this.assertWorkItemRevisionRunLocal(actual, gapId, owner, generation); return callback(); });
2001
+ }
2002
+ finally {
2003
+ this.workItemRevisionFrame = undefined;
2004
+ }
2005
+ });
2006
+ }
1688
2007
  assertSourceGrowthRun(expected, gapId, owner, generation) {
1689
2008
  validateSourceGrowthRunBinding(expected);
1690
2009
  if (expected.protocol === 'assistant-growth/source-run/v2') {
@@ -1694,6 +2013,13 @@ export class PluginControlPlaneService extends Service {
1694
2013
  }
1695
2014
  return this.withWorkItemGrowthFrame(expected, gapId, owner, generation, () => { });
1696
2015
  }
2016
+ if (expected.protocol === 'assistant-growth/source-run/v3') {
2017
+ if (this.workItemRevisionFrame) {
2018
+ this.assertWorkItemRevisionRunLocal(expected, gapId, owner, generation);
2019
+ return;
2020
+ }
2021
+ return this.withWorkItemRevisionFrame(expected, gapId, owner, generation, () => { });
2022
+ }
1697
2023
  const producer = this.growthRunProducer;
1698
2024
  if (!producer)
1699
2025
  throw new SourceGrowthRunUnavailableError('source growth run producer unavailable');
@@ -1716,7 +2042,7 @@ export class PluginControlPlaneService extends Service {
1716
2042
  || controlPlaneDigest(expected.revisionRegressionAcceptance) !== controlPlaneDigest(this.config.revisionRegressions.authority))) {
1717
2043
  throw new Error('source growth run regression policy changed or was not pinned before authoring');
1718
2044
  }
1719
- if (expected.creationAcceptance !== undefined
2045
+ if ('creationAcceptance' in expected && expected.creationAcceptance !== undefined
1720
2046
  && (policy === undefined || controlPlaneDigest(policy) !== controlPlaneDigest(expected.creationAcceptance))) {
1721
2047
  throw new Error('source growth run creation acceptance policy changed or was not pinned before authoring');
1722
2048
  }
@@ -1800,6 +2126,7 @@ export class PluginControlPlaneService extends Service {
1800
2126
  const current = this.store.getSourcePlan(input.planId);
1801
2127
  const job = this.store.getPreparedSourceJob(input.planId);
1802
2128
  if (current.digest !== input.planDigest || current.mode !== 'prepared-create' || !current.creation?.growthRun
2129
+ || current.creation.growthRun.protocol === 'assistant-growth/source-run/v3'
1803
2130
  || !job.intent.creation?.growthRun || !current.sourceCheck || !current.preparedEvidence
1804
2131
  || current.preparedEvidence.pack.sha256 !== input.artifactSha256
1805
2132
  || controlPlaneDigest(reference) !== input.referenceDigest
@@ -1892,6 +2219,9 @@ export class PluginControlPlaneService extends Service {
1892
2219
  const combined = AbortSignal.any([this.abort.signal, signal, AbortSignal.timeout(120_000)]);
1893
2220
  combined.throwIfAborted();
1894
2221
  const prepared = this.inspectPreparedRevision(planId), build = this.config.sourceBuild;
2222
+ if (prepared.protocol === 'dsh-prepared-revision/v3') {
2223
+ throw new Error('owner-instruction revisions do not use the task-failure Linux build binding');
2224
+ }
1895
2225
  const native = this.nativeSourceRunner, evidence = prepared.plan.preparedEvidence;
1896
2226
  if (!build || !('backend' in build) || build.backend !== 'linux-native' || !build.userNamespace || !build.systemSnapshot
1897
2227
  || native?.backend !== 'linux-native'
@@ -1979,10 +2309,34 @@ export class PluginControlPlaneService extends Service {
1979
2309
  this.abort.signal.throwIfAborted();
1980
2310
  const plan = this.store.getSourcePlan(planId);
1981
2311
  const reference = this.store.getOwnerTaskFailureReference(plan.gapId);
1982
- if (!reference || plan.mode !== 'prepared-revise' || plan.status !== 'pending-approval'
2312
+ if (plan.mode !== 'prepared-revise' || plan.status !== 'pending-approval'
1983
2313
  || Date.now() >= plan.expiresAt || !plan.sourceRevision || Date.now() >= plan.sourceRevision.grant.expiresAt) {
1984
2314
  throw new Error('prepared revision package is unavailable for independent verification');
1985
2315
  }
2316
+ if (!reference) {
2317
+ // Owner-instruction revisions have no task reference; the frozen
2318
+ // instruction is their whole provenance.
2319
+ const job = this.store.getPreparedRevisionJob(plan.id);
2320
+ const binding = job.intent.ownerInstruction;
2321
+ const stored = binding === undefined ? undefined : this.store.getOwnerInstruction(binding.instructionId);
2322
+ if (!binding || stored === undefined || controlPlaneDigest(stored) !== binding.digest
2323
+ || stored.expiresAt <= Date.now() || plan.sourceRevision.growthRun !== undefined
2324
+ || stored.planId !== plan.sourceRevision.parent.planId || stored.pluginName !== plan.name
2325
+ || job.intent.revision?.growthRun !== undefined) {
2326
+ throw new Error('prepared instruction revision is unavailable or changed');
2327
+ }
2328
+ this.assertInstructionRevisionOwner(stored.ownerReceipt, plan);
2329
+ return { protocol: 'dsh-prepared-revision/v3', plan: structuredClone(plan), job: structuredClone(job),
2330
+ source: { instruction: binding, owner: job.intent.owner,
2331
+ reviewModel: this.resolveInstructionReviewModel(stored, job.intent.ownerInstruction) },
2332
+ artifact: this.store.readPreparedSourceArtifact(plan.id) };
2333
+ }
2334
+ if (reference.schemaVersion === 2) {
2335
+ const run = plan.sourceRevision.growthRun;
2336
+ if (run?.protocol !== 'assistant-growth/source-run/v3')
2337
+ throw new Error('work item prepared revision run unavailable');
2338
+ return this.withWorkItemRevisionFrame(run, plan.gapId, reference.owner, false, () => ({ protocol: 'dsh-prepared-revision/v2', plan: structuredClone(this.store.getSourcePlan(plan.id)), job: structuredClone(this.store.getPreparedRevisionJob(plan.id)), reference: structuredClone(reference), source: structuredClone(this.workItemRevisionFrame.source), artifact: this.store.readPreparedSourceArtifact(plan.id) }));
2339
+ }
1986
2340
  return this.taskGaps.withCurrent(plan.gapId, reference.owner, () => {
1987
2341
  this.abort.signal.throwIfAborted();
1988
2342
  const job = this.store.getPreparedRevisionJob(plan.id);
@@ -1999,6 +2353,104 @@ export class PluginControlPlaneService extends Service {
1999
2353
  });
2000
2354
  };
2001
2355
  /** Host-only pre-author policy reference; never grants candidate or model authority. */
2356
+ assertInstructionRevisionOwner(owner, plan) {
2357
+ const config = this.config.sourceJobs;
2358
+ const revision = plan.sourceRevision;
2359
+ const delivery = this.ctx.get('assistantDelivery', false);
2360
+ if (!config?.revision || !revision || !delivery || config.expiresAt <= Date.now()
2361
+ || controlPlaneDigest(config.revision) !== controlPlaneDigest(revision.grant)) {
2362
+ throw new Error('instruction revision owner authority unavailable');
2363
+ }
2364
+ const current = delivery.validateOwnerRoute({ authorityId: config.ownerRouteId, principalId: config.principalId,
2365
+ workspace: config.workspace, agentPreset: config.preset });
2366
+ if (!isSourceOwnerContinuation(current, owner)
2367
+ || controlPlaneDigest(this.inspectRevisionParent(revision.parent.planId).parent) !== controlPlaneDigest(revision.parent)) {
2368
+ throw new Error('instruction revision owner or adopted parent changed');
2369
+ }
2370
+ }
2371
+ /**
2372
+ * Reads the review supplier frozen when the owner instruction was enqueued.
2373
+ * Missing or drifted freeze fails closed: the live owner route and live
2374
+ * verifier configuration are never substituted for the recorded values.
2375
+ */
2376
+ resolveInstructionReviewModel(stored, expected) {
2377
+ const freeze = this.store.getOwnerInstructionReviewFreeze(stored.instructionId);
2378
+ if (freeze === undefined)
2379
+ throw new Error('owner instruction has no frozen review supplier');
2380
+ if (expected !== undefined && (expected.reviewFreezeDigest === undefined || expected.reviewWindow === undefined
2381
+ || expected.reviewFreezeDigest !== freeze.digest
2382
+ || controlPlaneDigest(expected.reviewWindow) !== controlPlaneDigest(freeze.budget.window))) {
2383
+ throw new Error('owner instruction frozen review supplier or budget changed');
2384
+ }
2385
+ const live = this.ownerInstructionReviewBudget();
2386
+ if (controlPlaneDigest(freeze.budget.authority) !== controlPlaneDigest(live.authority)
2387
+ || freeze.budget.verifierKeyDigest !== live.verifierKeyDigest) {
2388
+ throw new Error('frozen review budget no longer matches the configured revision verifier');
2389
+ }
2390
+ if (freeze.budget.authority.expiresAt <= Date.now())
2391
+ throw new Error('frozen review authority expired');
2392
+ return { provider: freeze.supplier.provider, model: freeze.supplier.model,
2393
+ ...(freeze.supplier.reasoningEffort === undefined ? {} : { reasoningEffort: freeze.supplier.reasoningEffort }) };
2394
+ }
2395
+ /**
2396
+ * The independent-review budget identity configured for this Host, evaluated
2397
+ * once per use. The budget itself (maxOutputTokens/maxDurationMs/maxCases/
2398
+ * maxVerifications/runner/policy) is committed by
2399
+ * `RevisionAcceptanceAuthorityRef.authorityDigest`, so freezing this exact
2400
+ * authority ref plus the verifier key digest pins the review budget identity.
2401
+ * Narrowing the budget for one request (a per-plan token cap) is NOT possible
2402
+ * under the current review request protocol, which carries only the plan id
2403
+ * and lets the verifier compile its bounds once; that would require a
2404
+ * verifier request/response protocol v2. Do not emulate it by re-resolving
2405
+ * live configuration during recovery.
2406
+ */
2407
+ ownerInstructionReviewBudget() {
2408
+ const verification = this.config.revisionVerifications;
2409
+ if (verification === undefined)
2410
+ throw new Error('owner-instruction revision requires a configured independent review authority');
2411
+ const build = this.config.sourceBuild;
2412
+ const window = instructionReviewWindowFor({
2413
+ linuxBackend: build !== undefined && 'backend' in build && build.backend === 'linux-native',
2414
+ verification: true,
2415
+ regression: this.config.revisionRegressions !== undefined,
2416
+ adoption: this.config.revisionCapabilities !== undefined,
2417
+ });
2418
+ return Object.freeze({ protocol: 'plugin-control-plane/owner-instruction-review-budget/v1',
2419
+ authority: Object.freeze(structuredClone(verification.authority)),
2420
+ verifierKeyDigest: createHash('sha256').update(verification.publicKey).digest('hex'), window });
2421
+ }
2422
+ /** Live owner-route supplier resolution is allowed once, when the instruction is created. */
2423
+ resolveLiveInstructionReviewSupplier(receipt) {
2424
+ const delivery = this.ctx.get('assistantDelivery', false);
2425
+ const selection = delivery?.inspectOwnerModelSelection?.({ authorityId: receipt.authorityId,
2426
+ principalId: receipt.principalId, workspace: receipt.workspace,
2427
+ agentPreset: receipt.agentPreset });
2428
+ if (selection?.provider === undefined || selection.model === undefined) {
2429
+ throw new Error('owner-instruction revision requires a live owner model selection');
2430
+ }
2431
+ return { provider: selection.provider, model: selection.model,
2432
+ ...(selection.reasoningEffort === undefined ? {} : { reasoningEffort: selection.reasoningEffort }) };
2433
+ }
2434
+ /** Builds the write-once freeze for a first instruction write. */
2435
+ frozenInstructionReview(receipt) {
2436
+ return createOwnerInstructionReviewFreeze({ supplier: this.resolveLiveInstructionReviewSupplier(receipt),
2437
+ budget: this.ownerInstructionReviewBudget() });
2438
+ }
2439
+ /** Re-reads an existing freeze and fails closed if the review capability drifted. */
2440
+ reusedInstructionReview(stored) {
2441
+ const freeze = this.store.getOwnerInstructionReviewFreeze(stored.instructionId);
2442
+ if (freeze === undefined)
2443
+ throw new Error('owner instruction was recorded without a frozen review supplier; request a new revision');
2444
+ const live = this.ownerInstructionReviewBudget();
2445
+ if (controlPlaneDigest(freeze.budget.authority) !== controlPlaneDigest(live.authority)
2446
+ || freeze.budget.verifierKeyDigest !== live.verifierKeyDigest
2447
+ || controlPlaneDigest(freeze.budget.window) !== controlPlaneDigest(live.window)) {
2448
+ throw new Error('frozen review budget no longer matches the configured revision verifier');
2449
+ }
2450
+ if (freeze.budget.authority.expiresAt <= Date.now())
2451
+ throw new Error('frozen review authority expired');
2452
+ return freeze;
2453
+ }
2002
2454
  inspectSourceRevisionAcceptanceAuthority = () => {
2003
2455
  const policy = this.config.revisionVerifications?.authority;
2004
2456
  if (this.abort.signal.aborted || !policy || Date.now() >= policy.expiresAt
@@ -2025,7 +2477,7 @@ export class PluginControlPlaneService extends Service {
2025
2477
  const policy = this.inspectSourceRevisionRegressionAcceptanceAuthority();
2026
2478
  const run = prepared.plan.sourceRevision.growthRun;
2027
2479
  const source = this.store.readRevisionSource(planId);
2028
- if (!certificate || !source || !policy || !run.revisionRegressionAcceptance
2480
+ if (!certificate || !source || !policy || !run || !run.revisionRegressionAcceptance
2029
2481
  || controlPlaneDigest(run.revisionRegressionAcceptance) !== controlPlaneDigest(policy)) {
2030
2482
  throw new Error('regression requires fresh independently verified candidate and pre-author policy');
2031
2483
  }
@@ -2039,6 +2491,9 @@ export class PluginControlPlaneService extends Service {
2039
2491
  throw new Error('regression parent immutable artifact unavailable or changed');
2040
2492
  }
2041
2493
  this.inspectRevisionParent(parent.parent.planId);
2494
+ if (prepared.protocol === 'dsh-prepared-revision/v3') {
2495
+ throw new Error('owner-instruction revisions carry no regression acceptance');
2496
+ }
2042
2497
  return { protocol: 'dsh-prepared-revision-regression/v1',
2043
2498
  plan: prepared.plan, job: prepared.job, reference: prepared.reference,
2044
2499
  candidate: { certificate, artifact: Buffer.from(prepared.artifact), sourceDigest: source.digest },
@@ -2059,6 +2514,7 @@ export class PluginControlPlaneService extends Service {
2059
2514
  || controlPlaneDigest(snapshot.candidate.certificate) !== input.candidateVerificationDigest
2060
2515
  || source.digest !== input.sourceDigest || snapshot.candidate.sourceDigest !== input.sourceDigest
2061
2516
  || controlPlaneDigest(policy) !== input.regressionAuthorityDigest
2517
+ || snapshot.plan.sourceRevision.growthRun?.revisionRegressionAcceptance === undefined
2062
2518
  || controlPlaneDigest(snapshot.plan.sourceRevision.growthRun.revisionRegressionAcceptance) !== input.regressionAuthorityDigest
2063
2519
  || controlPlaneDigest(snapshot.parent.binding) !== input.parentDigest) {
2064
2520
  throw new Error('regression parent, candidate or policy changed');
@@ -2070,6 +2526,9 @@ export class PluginControlPlaneService extends Service {
2070
2526
  const certificate = this.store.getRevisionRegression(planId), policy = this.config.revisionRegressions;
2071
2527
  if (!certificate || !policy || !verifyPluginRevisionRegressionCertificate(certificate, policy.authority, policy.publicKey))
2072
2528
  return undefined;
2529
+ // Instruction revisions carry no regression acceptance and never enter this corridor.
2530
+ if (!isPluginRevisionTaskSource(certificate.source))
2531
+ return undefined;
2073
2532
  const prepared = this.inspectPreparedRevisionRegression(planId);
2074
2533
  this.withPreparedRevisionRegressionFence({ planId, planDigest: certificate.plan.digest, artifactSha256: certificate.plan.artifactSha256,
2075
2534
  growthRunDigest: certificate.source.growthRunDigest, referenceDigest: certificate.source.referenceDigest,
@@ -2093,6 +2552,17 @@ export class PluginControlPlaneService extends Service {
2093
2552
  const reference = this.store.getOwnerTaskFailureReference(plan.gapId);
2094
2553
  if (!reference)
2095
2554
  throw new Error('revision verification source unavailable');
2555
+ if (reference.schemaVersion === 2) {
2556
+ const run = plan.sourceRevision?.growthRun;
2557
+ if (run?.protocol !== 'assistant-growth/source-run/v3')
2558
+ throw new Error('work item revision verification run unavailable');
2559
+ return this.withWorkItemRevisionFrame(run, plan.gapId, reference.owner, false, () => {
2560
+ const current = this.store.getSourcePlan(input.planId), job = this.store.getPreparedRevisionJob(input.planId);
2561
+ if (current.digest !== input.planDigest || current.preparedEvidence?.pack.sha256 !== input.artifactSha256 || controlPlaneDigest(reference) !== input.referenceDigest || sourceGrowthRunDigest(run) !== input.growthRunDigest || !job.intent.revision?.growthRun || sourceGrowthRunDigest(job.intent.revision.growthRun) !== input.growthRunDigest || controlPlaneDigest(current.sourceRevision.parent) !== input.parentDigest || createHash('sha256').update(this.store.readPreparedSourceArtifact(input.planId)).digest('hex') !== input.artifactSha256)
2562
+ throw new Error('work item prepared revision exact binding changed');
2563
+ return callback();
2564
+ });
2565
+ }
2096
2566
  return this.taskGaps.withCurrent(plan.gapId, reference.owner, () => {
2097
2567
  this.abort.signal.throwIfAborted();
2098
2568
  const current = this.store.getSourcePlan(input.planId);
@@ -2118,15 +2588,57 @@ export class PluginControlPlaneService extends Service {
2118
2588
  return callback();
2119
2589
  });
2120
2590
  };
2591
+ /**
2592
+ * Exact-source fence for owner-instruction revisions: the frozen instruction
2593
+ * row replaces the task reference and growth run, and every comparison is
2594
+ * replayed against durable state inside the Evaluation writer transaction.
2595
+ */
2596
+ withPreparedInstructionRevisionFence = (input, callback) => {
2597
+ this.abort.signal.throwIfAborted();
2598
+ const policy = this.inspectSourceRevisionAcceptanceAuthority();
2599
+ if (!policy)
2600
+ throw new Error('revision verification authority unavailable');
2601
+ const plan = this.store.getSourcePlan(input.planId);
2602
+ if (this.store.getOwnerTaskFailureReference(plan.gapId) !== undefined) {
2603
+ throw new Error('instruction revision fence cannot ride a task failure');
2604
+ }
2605
+ const job = this.store.getPreparedRevisionJob(input.planId);
2606
+ const instruction = job.intent.ownerInstruction;
2607
+ const current = this.store.getSourcePlan(input.planId);
2608
+ const stored = instruction === undefined ? undefined : this.store.getOwnerInstruction(instruction.instructionId);
2609
+ if (!instruction || stored === undefined || controlPlaneDigest(stored) !== instruction.digest
2610
+ || instruction.digest !== input.instructionDigest
2611
+ || current.digest !== input.planDigest || current.mode !== 'prepared-revise' || !current.sourceCheck || !current.preparedEvidence
2612
+ || current.preparedEvidence.pack.sha256 !== input.artifactSha256
2613
+ || current.sourceRevision?.growthRun !== undefined || job.intent.revision?.growthRun !== undefined
2614
+ || controlPlaneDigest(current.sourceRevision?.parent) !== input.parentDigest
2615
+ || controlPlaneDigest(current.sourceRevision?.grant) !== controlPlaneDigest(this.config.sourceJobs?.revision)) {
2616
+ throw new Error('instruction revision exact source binding changed');
2617
+ }
2618
+ if (stored.expiresAt <= Date.now())
2619
+ throw new Error('owner instruction expired');
2620
+ this.assertInstructionRevisionOwner(stored.ownerReceipt, current);
2621
+ const artifact = this.store.readPreparedSourceArtifact(input.planId);
2622
+ if (createHash('sha256').update(artifact).digest('hex') !== input.artifactSha256) {
2623
+ throw new Error('instruction revision artifact changed');
2624
+ }
2625
+ return callback();
2626
+ };
2121
2627
  /** Independent reviewer reads the actual checked tree through a disposable Git index. */
2122
2628
  inspectPreparedRevisionReviewContext = async (planId, signal) => {
2123
2629
  const combined = AbortSignal.any([this.abort.signal, signal, AbortSignal.timeout(120_000)]);
2124
2630
  combined.throwIfAborted();
2125
2631
  const prepared = this.inspectPreparedRevision(planId);
2126
- const current = () => this.withPreparedRevisionFence({ planId, planDigest: prepared.plan.digest,
2127
- artifactSha256: prepared.plan.preparedEvidence.pack.sha256,
2128
- growthRunDigest: sourceGrowthRunDigest(prepared.plan.sourceRevision.growthRun),
2129
- referenceDigest: controlPlaneDigest(prepared.reference), parentDigest: controlPlaneDigest(prepared.plan.sourceRevision.parent) }, () => undefined);
2632
+ const assertCurrent = prepared.protocol === 'dsh-prepared-revision/v3'
2633
+ ? () => this.withPreparedInstructionRevisionFence({ planId, planDigest: prepared.plan.digest,
2634
+ artifactSha256: prepared.plan.preparedEvidence.pack.sha256,
2635
+ instructionDigest: prepared.source.instruction.digest,
2636
+ parentDigest: controlPlaneDigest(prepared.plan.sourceRevision.parent) }, () => undefined)
2637
+ : () => this.withPreparedRevisionFence({ planId, planDigest: prepared.plan.digest,
2638
+ artifactSha256: prepared.plan.preparedEvidence.pack.sha256,
2639
+ growthRunDigest: sourceGrowthRunDigest(prepared.plan.sourceRevision.growthRun),
2640
+ referenceDigest: controlPlaneDigest(prepared.reference), parentDigest: controlPlaneDigest(prepared.plan.sourceRevision.parent) }, () => undefined);
2641
+ const current = assertCurrent;
2130
2642
  current();
2131
2643
  const trust = await this.boundTrust();
2132
2644
  const environment = inheritedEnvironment(trust);
@@ -2147,6 +2659,21 @@ export class PluginControlPlaneService extends Service {
2147
2659
  const config = this.config.revisionVerifications;
2148
2660
  if (!config || !verifyPluginRevisionVerificationCertificate(certificate, config.authority, config.publicKey))
2149
2661
  return undefined;
2662
+ if (!isPluginRevisionTaskSource(certificate.source)) {
2663
+ this.withPreparedInstructionRevisionFence({ planId, planDigest: certificate.plan.digest,
2664
+ artifactSha256: certificate.plan.artifactSha256,
2665
+ instructionDigest: certificate.source.instructionDigest, parentDigest: controlPlaneDigest(certificate.parent) }, () => {
2666
+ const plan = this.store.getSourcePlan(planId);
2667
+ if (!plan.sourceCheck || !plan.preparedEvidence || certificate.plan.name !== plan.name
2668
+ || certificate.plan.generatorDigest !== plan.generatorDigest
2669
+ || certificate.plan.sourceTreeDigest !== plan.sourceCheck.treeDigest
2670
+ || certificate.plan.sourcePatchDigest !== plan.sourceCheck.patchDigest
2671
+ || certificate.plan.artifactBytes !== plan.preparedEvidence.pack.sizeBytes) {
2672
+ throw new Error('revision verification exact plan evidence changed');
2673
+ }
2674
+ });
2675
+ return structuredClone(certificate);
2676
+ }
2150
2677
  this.withPreparedRevisionFence({ planId, planDigest: certificate.plan.digest,
2151
2678
  artifactSha256: certificate.plan.artifactSha256, growthRunDigest: certificate.source.growthRunDigest,
2152
2679
  referenceDigest: certificate.source.referenceDigest, parentDigest: controlPlaneDigest(certificate.parent) }, () => {
@@ -2166,8 +2693,17 @@ export class PluginControlPlaneService extends Service {
2166
2693
  this.abort.signal.throwIfAborted();
2167
2694
  const plan = this.store.getSourcePlan(planId);
2168
2695
  const reference = this.store.getOwnerTaskFailureReference(plan.gapId);
2169
- if (!reference || plan.mode !== 'prepared-revise')
2696
+ if (plan.mode !== 'prepared-revise')
2170
2697
  throw new Error('revision verification owner source unavailable');
2698
+ if (!reference) {
2699
+ const job = this.store.getPreparedRevisionJob(planId);
2700
+ const instruction = job.intent.ownerInstruction;
2701
+ if (!instruction || !plan.preparedEvidence || !plan.sourceRevision)
2702
+ throw new Error('revision verification owner source unavailable');
2703
+ return this.withPreparedInstructionRevisionFence({ planId, planDigest: plan.digest,
2704
+ artifactSha256: plan.preparedEvidence.pack.sha256, instructionDigest: instruction.digest,
2705
+ parentDigest: controlPlaneDigest(plan.sourceRevision.parent) }, () => this.store.inspectRevisionVerificationRecord(planId));
2706
+ }
2171
2707
  return this.taskGaps.withCurrent(plan.gapId, reference.owner, () => {
2172
2708
  const current = this.store.getSourcePlan(planId);
2173
2709
  if (current.mode !== 'prepared-revise' || current.digest !== plan.digest) {
@@ -2196,6 +2732,8 @@ export class PluginControlPlaneService extends Service {
2196
2732
  const { plan, reference, job } = this.store.getRetainedPreparedRevision(planId);
2197
2733
  const certificate = this.store.getRevisionVerification(planId);
2198
2734
  if (!certificate || !plan.sourceRevision || !isSourceOwnerContinuation(before, reference.owner)
2735
+ || !isPluginRevisionTaskSource(certificate.source)
2736
+ || plan.sourceRevision.growthRun === undefined
2199
2737
  || !verifyPluginRevisionVerificationCertificate(certificate, policy.authority, policy.publicKey, certificate.verifiedAt)
2200
2738
  || certificate.plan.digest !== plan.digest || certificate.source.referenceDigest !== controlPlaneDigest(reference)
2201
2739
  || certificate.source.ownerDigest !== job.intent.ownerDigest
@@ -2241,6 +2779,104 @@ export class PluginControlPlaneService extends Service {
2241
2779
  throw new Error('plugin-control-plane: durable source jobs unavailable');
2242
2780
  return this.sourceRuntime.inspect(input);
2243
2781
  };
2782
+ async drainOwnerInstructionRequests(signal) {
2783
+ await drainPendingOwnerInstructionRequests({
2784
+ store: this.store,
2785
+ now: Date.now(),
2786
+ readNow: Date.now,
2787
+ signal,
2788
+ maxPerPass: 1,
2789
+ admit: (request, assertCurrent) => this.requestOwnerInstructionRevision({ planId: request.planId, pluginName: request.pluginName,
2790
+ files: request.files, ttlMs: request.ttlMs, idempotencyKey: request.idempotencyKey,
2791
+ expectedBaseCommit: request.expectedBaseCommit, window: { createdAt: request.createdAt, expiresAt: request.expiresAt },
2792
+ signal, assertCurrent }),
2793
+ });
2794
+ }
2795
+ /**
2796
+ * Host-only owner instruction entry. Freezes one authenticated owner
2797
+ * receipt, the adopted revision parent and the exact submitted files into an
2798
+ * expiring instruction row, then enqueues one `revise-created` source job
2799
+ * against the synthetic instruction gap. The model tool surface never
2800
+ * reaches this method; without a valid instruction the admission still
2801
+ * requires an owner task failure.
2802
+ */
2803
+ requestOwnerInstructionRevision = (input) => {
2804
+ const assertCurrent = () => {
2805
+ this.abort.signal.throwIfAborted();
2806
+ input.signal?.throwIfAborted();
2807
+ input.assertCurrent?.();
2808
+ };
2809
+ assertCurrent();
2810
+ const config = this.config.sourceJobs;
2811
+ const delivery = this.ctx.get('assistantDelivery', false);
2812
+ if (config === undefined || config.revision === undefined || delivery === undefined) {
2813
+ throw new Error('plugin-control-plane: owner instruction revisions require source jobs, a revision grant and a live owner route');
2814
+ }
2815
+ if (config.revision.expiresAt <= Date.now())
2816
+ throw new Error('plugin-control-plane: the source revision grant is expired');
2817
+ if (!Number.isSafeInteger(input.ttlMs) || input.ttlMs < 900_000 || input.ttlMs > 86_400_000) {
2818
+ throw new Error('plugin-control-plane: owner instruction ttl is invalid');
2819
+ }
2820
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u.test(input.idempotencyKey)) {
2821
+ throw new Error('plugin-control-plane: owner instruction idempotency key is invalid');
2822
+ }
2823
+ validateSourceCreationFiles(input.files);
2824
+ const receipt = delivery.validateOwnerRoute({ authorityId: config.ownerRouteId,
2825
+ principalId: config.principalId, workspace: config.workspace, agentPreset: config.preset });
2826
+ const parent = this.inspectRevisionParent(input.planId);
2827
+ if (parent.name !== input.pluginName)
2828
+ throw new Error('plugin-control-plane: owner instruction target does not match the adopted parent');
2829
+ const instructionId = `instruction-${createHash('sha256').update(config.authorityId).update('\0')
2830
+ .update(input.idempotencyKey).digest('hex').slice(0, 32)}`;
2831
+ const prior = this.store.getOwnerInstruction(instructionId);
2832
+ const now = Date.now();
2833
+ const createdAt = input.window?.createdAt ?? prior?.createdAt ?? now;
2834
+ const expiresAt = input.window?.expiresAt ?? prior?.expiresAt ?? createdAt + input.ttlMs;
2835
+ if (!Number.isSafeInteger(createdAt) || !Number.isSafeInteger(expiresAt)
2836
+ || expiresAt - createdAt !== input.ttlMs || expiresAt <= now) {
2837
+ throw new Error('plugin-control-plane: owner instruction request expired or invalid');
2838
+ }
2839
+ if (prior && !isSourceOwnerContinuation(receipt, prior.ownerReceipt))
2840
+ throw new Error('plugin-control-plane: owner route changed');
2841
+ // Resolve the independent review supplier and budget exactly once. An
2842
+ // existing instruction reuses its write-once freeze; a changed owner model
2843
+ // selection or changed verifier identity can never silently replace it.
2844
+ const review = prior === undefined ? this.frozenInstructionReview(receipt) : this.reusedInstructionReview(prior);
2845
+ const gap = this.store.recordGap({
2846
+ idempotencyKey: `owner-instruction:${instructionId}`, capability: `plugin-revision:${input.pluginName}`,
2847
+ context: 'Owner instruction revision request', expectedValue: 1, frequency: 1, estimatedCost: 1, risk: 0,
2848
+ });
2849
+ const instruction = this.store.recordOwnerInstruction({
2850
+ instructionId, ownerReceipt: prior?.ownerReceipt ?? receipt, ownerRouteId: config.ownerRouteId, planId: input.planId,
2851
+ pluginName: input.pluginName, filesDigest: controlPlaneDigest(input.files),
2852
+ createdAt, expiresAt, idempotencyKey: input.idempotencyKey, review,
2853
+ });
2854
+ const signal = AbortSignal.any([this.abort.signal, AbortSignal.timeout(Math.max(1, expiresAt - Date.now())),
2855
+ ...(input.signal === undefined ? [] : [input.signal])]);
2856
+ return this.enqueueSourceJob({
2857
+ gapId: gap.id, mode: 'revise-created', name: input.pluginName, files: input.files,
2858
+ repository: config.repository, parentPlanId: input.planId,
2859
+ ownerInstruction: { instructionId: instruction.instructionId,
2860
+ digest: controlPlaneDigest(instruction) },
2861
+ expectedBaseCommit: input.expectedBaseCommit, ttlMs: input.ttlMs,
2862
+ idempotencyKey: input.idempotencyKey,
2863
+ owner: { ownerRouteId: receipt.authorityId, principalId: receipt.principalId,
2864
+ principalRecordId: receipt.principalRecordId, principalVersion: receipt.principalVersion,
2865
+ workspace: receipt.workspace, preset: receipt.agentPreset },
2866
+ signal, assertCurrent,
2867
+ }).then(job => {
2868
+ const persisted = this.store.getSourceJob(job.id);
2869
+ // A queued result from enqueue has passed native activation. For an
2870
+ // already settled job, the bound definition alone only proves the
2871
+ // paused registration existed. A durable occurrence proves dispatch.
2872
+ if (persisted?.intent.ownerInstruction?.instructionId !== instruction.instructionId
2873
+ || persisted.intent.ownerInstruction.digest !== controlPlaneDigest(instruction)
2874
+ || !persisted.definitionHash || (persisted.status !== 'queued' && persisted.occurrenceId === undefined)) {
2875
+ throw new Error('plugin-control-plane: source-job-registration-rejected');
2876
+ }
2877
+ return { instructionId: instruction.instructionId, jobId: job.id };
2878
+ });
2879
+ };
2244
2880
  reconcileSourceJob = (input) => {
2245
2881
  if (this.sourceRuntime === undefined || this.sourceBuilds.size !== 0)
2246
2882
  throw new Error('plugin-control-plane: durable source jobs unavailable or still draining');
@@ -2322,19 +2958,42 @@ export class PluginControlPlaneService extends Service {
2322
2958
  if (!job.planId || !this.config.revisionVerifications || this.store.getRevisionVerificationStatus(job.planId) !== undefined)
2323
2959
  return;
2324
2960
  const prepared = this.inspectPreparedRevision(job.planId);
2325
- if (prepared.job.id !== job.id || !prepared.plan.sourceRevision?.growthRun || !prepared.plan.preparedEvidence) {
2961
+ if (prepared.job.id !== job.id || !prepared.plan.preparedEvidence) {
2326
2962
  throw new Error('revision verification job is not the exact prepared source');
2327
2963
  }
2964
+ if (prepared.protocol === 'dsh-prepared-revision/v3') {
2965
+ // Owner-instruction revisions claim through their instruction binding.
2966
+ const instruction = prepared.source.instruction;
2967
+ this.withPreparedInstructionRevisionFence({ planId: prepared.plan.id, planDigest: prepared.plan.digest,
2968
+ artifactSha256: prepared.plan.preparedEvidence.pack.sha256,
2969
+ instructionDigest: instruction.digest, parentDigest: controlPlaneDigest(prepared.plan.sourceRevision.parent) }, () => this.store.claimRevisionVerification(job.planId));
2970
+ return this.runPreparedRevisionVerifier(job, prepared, signal, verifier, { planId: prepared.plan.id, planDigest: prepared.plan.digest,
2971
+ artifactSha256: prepared.plan.preparedEvidence.pack.sha256,
2972
+ instructionDigest: instruction.digest, parentDigest: controlPlaneDigest(prepared.plan.sourceRevision.parent),
2973
+ ownerDigest: controlPlaneDigest(prepared.source.owner) });
2974
+ }
2975
+ if (!prepared.plan.sourceRevision?.growthRun)
2976
+ throw new Error('prepared revision lacks a frozen source growth run');
2328
2977
  const binding = { planId: prepared.plan.id, planDigest: prepared.plan.digest,
2329
2978
  artifactSha256: prepared.plan.preparedEvidence.pack.sha256,
2330
2979
  growthRunDigest: sourceGrowthRunDigest(prepared.plan.sourceRevision.growthRun),
2331
2980
  referenceDigest: controlPlaneDigest(prepared.reference), parentDigest: controlPlaneDigest(prepared.plan.sourceRevision.parent) };
2332
2981
  this.withPreparedRevisionFence(binding, () => this.store.claimRevisionVerification(job.planId));
2982
+ return this.runPreparedRevisionVerifier(job, prepared, signal, verifier, binding);
2983
+ }
2984
+ /** Runs the independent reviewer and settles the claimed row inside the caller's exact-source fence. */
2985
+ async runPreparedRevisionVerifier(job, prepared, signal, verifier, fenceInput) {
2986
+ const inFence = (callback) => 'growthRunDigest' in fenceInput
2987
+ ? this.withPreparedRevisionFence(fenceInput, callback)
2988
+ : this.withPreparedInstructionRevisionFence(fenceInput, callback);
2989
+ const planId = job.planId;
2990
+ if (!planId)
2991
+ throw new Error('revision verification job has no plan');
2333
2992
  try {
2334
2993
  const current = verifier();
2335
2994
  if (typeof current?.verifyPluginRevision !== 'function')
2336
2995
  throw new Error('independent revision verifier unavailable');
2337
- const result = await current.verifyPluginRevision({ protocol: 'assistant-growth/revision-verification-request/v1', planId: job.planId }, signal);
2996
+ const result = await current.verifyPluginRevision({ protocol: 'assistant-growth/revision-verification-request/v1', planId }, signal);
2338
2997
  signal.throwIfAborted();
2339
2998
  if (result.status === 'verified' && !verifyPluginRevisionVerificationCertificate(result.certificate, this.config.revisionVerifications.authority, this.config.revisionVerifications.publicKey)) {
2340
2999
  throw new Error('revision verification certificate signature or policy invalid');
@@ -2345,7 +3004,7 @@ export class PluginControlPlaneService extends Service {
2345
3004
  const source = result.status === 'verified' ? await captureCreationCapabilitySource({ worktree: prepared.plan.worktree,
2346
3005
  baseCommit: prepared.plan.baseCommit, scope: [...prepared.plan.scope].sort(), certificate: result.certificate,
2347
3006
  environment: inheritedEnvironment(await this.boundTrust()), signal }) : undefined;
2348
- this.withPreparedRevisionFence(binding, () => {
3007
+ inFence(() => {
2349
3008
  if (result.status === 'verified') {
2350
3009
  if (!source)
2351
3010
  throw new Error('verified revision source archive unavailable');
@@ -2360,7 +3019,7 @@ export class PluginControlPlaneService extends Service {
2360
3019
  // The claimed row is a durable no-replay fence even if the process dies
2361
3020
  // before settlement. A later native tick cannot dispatch another review.
2362
3021
  try {
2363
- this.withPreparedRevisionFence(binding, () => {
3022
+ inFence(() => {
2364
3023
  if (this.store.getRevisionVerificationStatus(job.planId) === 'claimed') {
2365
3024
  this.store.settleRevisionVerification(job.planId, 'unknown', 'independent-verification-unsettled');
2366
3025
  }
@@ -2378,11 +3037,17 @@ export class PluginControlPlaneService extends Service {
2378
3037
  if (prepared.job.id !== job.id)
2379
3038
  throw new Error('regression job is not exact prepared revision');
2380
3039
  const certificate = prepared.candidate.certificate;
3040
+ const acceptance = prepared.plan.sourceRevision?.growthRun?.revisionRegressionAcceptance;
3041
+ if (acceptance === undefined)
3042
+ throw new Error('owner-instruction revisions carry no regression acceptance');
3043
+ if (!isPluginRevisionTaskSource(certificate.source)) {
3044
+ throw new Error('owner-instruction revisions carry no regression acceptance');
3045
+ }
2381
3046
  const binding = { planId: prepared.plan.id, planDigest: prepared.plan.digest, artifactSha256: certificate.plan.artifactSha256,
2382
3047
  growthRunDigest: certificate.source.growthRunDigest, referenceDigest: certificate.source.referenceDigest,
2383
3048
  parentDigest: controlPlaneDigest(certificate.parent), candidateVerificationDigest: controlPlaneDigest(certificate),
2384
3049
  sourceDigest: prepared.candidate.sourceDigest,
2385
- regressionAuthorityDigest: controlPlaneDigest(prepared.plan.sourceRevision.growthRun.revisionRegressionAcceptance) };
3050
+ regressionAuthorityDigest: controlPlaneDigest(acceptance) };
2386
3051
  this.withPreparedRevisionRegressionFence(binding, () => this.store.claimRevisionRegression(job.planId));
2387
3052
  try {
2388
3053
  const current = verifier();
@@ -2629,8 +3294,7 @@ export class PluginControlPlaneService extends Service {
2629
3294
  throw new ControlPlaneCliError('INVALID_ARGUMENT', 'ttlMs must be an integer within 900000..86400000');
2630
3295
  }
2631
3296
  const buildConfig = this.config.sourceBuild;
2632
- const repositoryBuild = buildConfig !== undefined && ('backend' in buildConfig
2633
- ? buildConfig.backend === 'linux-native' : ['repository', 'portable-repository'].includes(buildConfig.profile ?? ''));
3297
+ const repositoryBuild = buildConfig !== undefined && 'backend' in buildConfig && buildConfig.backend === 'linux-native';
2634
3298
  const maximumTimeoutMs = repositoryBuild ? 1_800_000 : 240_000;
2635
3299
  const timeoutMs = input.timeoutMs ?? 180_000;
2636
3300
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 60_000 || timeoutMs > maximumTimeoutMs) {
@@ -2675,31 +3339,26 @@ export class PluginControlPlaneService extends Service {
2675
3339
  }
2676
3340
  // Only owner-bound creation jobs retain the exact package for the
2677
3341
  // independent verifier. Ordinary source tools never receive these bytes.
2678
- const capturePack = (creating || revising) && this.store.getOwnerTaskFailureReference(input.gapId) !== undefined;
3342
+ const capturePack = (creating || revising) && (this.store.getOwnerTaskFailureReference(input.gapId) !== undefined
3343
+ || sourceJob?.intent.ownerInstruction !== undefined);
2679
3344
  const common = { worktree: isolated.worktree, baseCommit, name, scope: generated?.scope ?? [`plugins/${name}`],
2680
3345
  environment, signal, assertCurrent, preparedAt: Date.now(), ...(creation === undefined ? {} : { creation }),
2681
3346
  ...(revision === undefined ? {} : { revision: { binding: revision, parentSource: parent.source,
2682
3347
  parentCertificate: parent.certificate, files: input.files } }), ...(capturePack ? { capturePack: true } : {}) };
2683
- const checked = 'backend' in configured
2684
- ? await (async () => {
2685
- const native = this.nativeSourceRunner;
2686
- if (sourceJob === undefined || native === undefined || native.backend !== configured.backend
2687
- || sourceJob.intent.nativeResource?.backend !== configured.backend || sourceJob.intent.nativeResource.key !== sourceJob.id) {
2688
- throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'native source preparation requires its bound durable source job');
2689
- }
2690
- if (configured.backend === 'linux-native' && native.backend === 'linux-native') {
2691
- linuxBuildInvoked = true;
2692
- return runLinuxPreparedChecks({ ...common, config: { ...configured, timeoutMs: Math.min(timeoutMs, configured.timeoutMs) },
2693
- runner: native.runner, sourceJob: { id: sourceJob.id } });
2694
- }
2695
- if (configured.backend !== 'macos-seatbelt' || native.backend !== 'macos-seatbelt' || !creating && !revising) {
2696
- throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'macOS source preparation requires a Host-scaffolded creation job');
2697
- }
2698
- return runNativePreparedChecks({ ...common, config: { ...configured, timeoutMs: Math.min(timeoutMs, configured.timeoutMs) },
2699
- runner: native.runner, sourceJob: { id: sourceJob.id } });
2700
- })()
2701
- : await runDockerPreparedChecks({ ...common, config: { ...configured, timeoutMs: Math.min(timeoutMs, configured.timeoutMs) },
2702
- ...(sourceJob === undefined ? {} : { sourceJob: { id: sourceJob.id, containerName: sourceJob.intent.containerName } }) });
3348
+ if (!('backend' in configured))
3349
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'source build requires a native backend');
3350
+ const native = this.nativeSourceRunner;
3351
+ if (sourceJob === undefined || native === undefined || native.backend !== configured.backend
3352
+ || sourceJob.intent.nativeResource?.backend !== configured.backend || sourceJob.intent.nativeResource.key !== sourceJob.id) {
3353
+ throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'native source preparation requires its bound durable source job');
3354
+ }
3355
+ const checked = configured.backend === 'linux-native' && native.backend === 'linux-native'
3356
+ ? await (linuxBuildInvoked = true, runLinuxPreparedChecks({ ...common, config: { ...configured, timeoutMs: Math.min(timeoutMs, configured.timeoutMs) },
3357
+ runner: native.runner, sourceJob: { id: sourceJob.id } }))
3358
+ : configured.backend === 'macos-seatbelt' && native.backend === 'macos-seatbelt' && (creating || revising)
3359
+ ? await runNativePreparedChecks({ ...common, config: { ...configured, timeoutMs: Math.min(timeoutMs, configured.timeoutMs) },
3360
+ runner: native.runner, sourceJob: { id: sourceJob.id } })
3361
+ : (() => { throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'native source preparation backend is unavailable'); })();
2703
3362
  await assertCurrent();
2704
3363
  if (capturePack && checked.packArtifact === undefined)
2705
3364
  throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'prepared creation artifact was not captured');
@@ -2726,6 +3385,11 @@ export class PluginControlPlaneService extends Service {
2726
3385
  throw new Error('work item creation owner unavailable');
2727
3386
  return this.withWorkItemGrowthFrame(creation.growthRun, input.gapId, gapOwner, false, callback);
2728
3387
  }
3388
+ if (revision?.growthRun?.protocol === 'assistant-growth/source-run/v3') {
3389
+ if (!gapOwner || !('receiptVersion' in gapOwner))
3390
+ throw new Error('work item revision owner unavailable');
3391
+ return this.withWorkItemRevisionFrame(revision.growthRun, input.gapId, gapOwner, false, callback);
3392
+ }
2729
3393
  return this.taskGaps.withCurrent(input.gapId, gapOwner, callback);
2730
3394
  };
2731
3395
  return commitSource(() => this.store.createSourcePlan({ gapId: input.gapId, repository, worktree: isolated.worktree, baseCommit,