@dsh-enhanced/plugin-control-plane 0.1.66 → 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.
- package/README.md +96 -22
- package/lib/cli.d.ts.map +1 -1
- package/lib/cli.js +66 -11
- package/lib/cli.js.map +1 -1
- package/lib/creation-capability-execution.d.ts +5 -1
- package/lib/creation-capability-execution.d.ts.map +1 -1
- package/lib/creation-capability-execution.js +42 -0
- package/lib/creation-capability-execution.js.map +1 -1
- package/lib/creation-capability-feedback.d.ts +16 -1
- package/lib/creation-capability-feedback.d.ts.map +1 -1
- package/lib/creation-capability-feedback.js +82 -5
- package/lib/creation-capability-feedback.js.map +1 -1
- package/lib/creation-capability-journal.d.ts.map +1 -1
- package/lib/creation-capability-journal.js +2 -0
- package/lib/creation-capability-journal.js.map +1 -1
- package/lib/creation-capability-runtime.d.ts +47 -1
- package/lib/creation-capability-runtime.d.ts.map +1 -1
- package/lib/creation-capability-runtime.js +105 -1
- package/lib/creation-capability-runtime.js.map +1 -1
- package/lib/creation-capability-types.d.ts +1 -0
- package/lib/creation-capability-types.d.ts.map +1 -1
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/linux-source-build.d.ts +13 -2
- package/lib/linux-source-build.d.ts.map +1 -1
- package/lib/linux-source-build.js.map +1 -1
- package/lib/live-qualification-authority.js +2 -2
- package/lib/live-qualification-authority.js.map +1 -1
- package/lib/owner-instruction-drain.d.ts +60 -0
- package/lib/owner-instruction-drain.d.ts.map +1 -0
- package/lib/owner-instruction-drain.js +101 -0
- package/lib/owner-instruction-drain.js.map +1 -0
- package/lib/revision-capability-feedback.d.ts +165 -0
- package/lib/revision-capability-feedback.d.ts.map +1 -0
- package/lib/revision-capability-feedback.js +316 -0
- package/lib/revision-capability-feedback.js.map +1 -0
- package/lib/revision-capability-journal.d.ts +126 -0
- package/lib/revision-capability-journal.d.ts.map +1 -0
- package/lib/revision-capability-journal.js +981 -0
- package/lib/revision-capability-journal.js.map +1 -0
- package/lib/revision-capability-runtime.d.ts +92 -0
- package/lib/revision-capability-runtime.d.ts.map +1 -0
- package/lib/revision-capability-runtime.js +635 -0
- package/lib/revision-capability-runtime.js.map +1 -0
- package/lib/revision-capability-types.d.ts +335 -0
- package/lib/revision-capability-types.d.ts.map +1 -0
- package/lib/revision-capability-types.js +2 -0
- package/lib/revision-capability-types.js.map +1 -0
- package/lib/service.d.ts +199 -4
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +925 -70
- package/lib/service.js.map +1 -1
- package/lib/source-adoption-authority.js +2 -2
- package/lib/source-adoption-authority.js.map +1 -1
- package/lib/source-approval-authority.js +3 -3
- package/lib/source-approval-authority.js.map +1 -1
- package/lib/source-build.d.ts +33 -53
- package/lib/source-build.d.ts.map +1 -1
- package/lib/source-build.js +28 -408
- package/lib/source-build.js.map +1 -1
- package/lib/source-job-types.d.ts +111 -4
- package/lib/source-job-types.d.ts.map +1 -1
- package/lib/source-jobs.d.ts +72 -3
- package/lib/source-jobs.d.ts.map +1 -1
- package/lib/source-jobs.js +314 -43
- package/lib/source-jobs.js.map +1 -1
- package/lib/source-release-authority.js +2 -2
- package/lib/source-release-authority.js.map +1 -1
- package/lib/source-revision.d.ts +29 -3
- package/lib/source-revision.d.ts.map +1 -1
- package/lib/source-revision.js +40 -9
- package/lib/source-revision.js.map +1 -1
- package/lib/source-workspace.d.ts +11 -4
- package/lib/source-workspace.d.ts.map +1 -1
- package/lib/source-workspace.js +51 -18
- package/lib/source-workspace.js.map +1 -1
- package/lib/sqlite.d.ts +6 -7
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +255 -17
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +143 -4
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +739 -66
- package/lib/store.js.map +1 -1
- package/lib/systemd-host-authority.js +4 -4
- package/lib/systemd-host-authority.js.map +1 -1
- package/lib/task-observation-authority.js +2 -2
- package/lib/task-observation-authority.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/work-item-creation-contract.d.ts +3 -1
- package/lib/work-item-creation-contract.d.ts.map +1 -1
- package/lib/work-item-creation-contract.js +10 -1
- package/lib/work-item-creation-contract.js.map +1 -1
- package/package.json +11 -8
package/lib/service.js
CHANGED
|
@@ -2,17 +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 {
|
|
5
|
+
import { RevisionCapabilityJournal, revisionCapabilityAuthorityDigest, revisionCapabilityPublicKey, validateRevisionCapabilityConfig } from './revision-capability-journal.js';
|
|
6
|
+
import { RevisionCapabilityRuntime } from './revision-capability-runtime.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';
|
|
6
9
|
import { captureCreationCapabilitySource } from './creation-capability-source.js';
|
|
7
10
|
import { lstat, readFile, realpath } from 'node:fs/promises';
|
|
8
11
|
import { realpathSync } from 'node:fs';
|
|
9
12
|
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
10
13
|
import { createHash, createPublicKey, randomBytes } from 'node:crypto';
|
|
11
|
-
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';
|
|
12
15
|
import { TaskObservationRuntime, validateTaskObservationConfig } from './task-observation-runtime.js';
|
|
13
16
|
import { LiveQualificationRuntime, validateLiveQualificationConfig } from './live-qualification-runtime.js';
|
|
14
17
|
import { rollbackPluginWatch } from './cli.js';
|
|
15
|
-
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';
|
|
16
19
|
import { OwnerTaskFailureGaps } from './owner-task-gaps.js';
|
|
17
20
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
18
21
|
import Schema from '@deepseek-ai/schemastery';
|
|
@@ -26,8 +29,8 @@ import { Ed25519SourceReleaseAuthorizationAuthority } from './release.js';
|
|
|
26
29
|
import { advanceSourceRelease, sourceReleaseAuthorities, validateSourceReleaseExecutionConfig } from './source-release-runner.js';
|
|
27
30
|
import { adoptSourceRelease, validateSourceAdoptionConfig } from './source-adoption-runner.js';
|
|
28
31
|
import { Ed25519ApprovalAuthority } from './approval.js';
|
|
29
|
-
import { ControlPlaneStore, MODIFY_GENERATOR_DIGEST, controlPlaneDigest } from './store.js';
|
|
30
|
-
import {
|
|
32
|
+
import { ControlPlaneStore, MODIFY_GENERATOR_DIGEST, controlPlaneDigest, createOwnerInstructionReviewFreeze } from './store.js';
|
|
33
|
+
import { validateSourceBuildConfig } from './source-build.js';
|
|
31
34
|
import { runNativePreparedChecks } from './native-source-build.js';
|
|
32
35
|
import { LINUX_PREPARED_SOURCE_BUILD_SCRIPT, LINUX_SOURCE_PACK_PATH, runLinuxPreparedChecks } from './linux-source-build.js';
|
|
33
36
|
import { LinuxRepositoryRunner, NativeMacVerifierRunner, linuxRuntimeDigest, verifyLinuxRuntime } from '@dsh-enhanced/assistant-isolation';
|
|
@@ -38,7 +41,8 @@ import { inspectSourceCreationContext, prepareCreatedPluginWorkspace, validateSo
|
|
|
38
41
|
import { resolveSourceBaseline } from './source-baseline.js';
|
|
39
42
|
import { inheritedEnvironment, loadTrustConfig, resolveTrustKey } from './trust.js';
|
|
40
43
|
import { registerPluginControlTools } from './tools.js';
|
|
41
|
-
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';
|
|
42
46
|
import { installRuntimeObserver, validateRuntimeObserverConfig } from './runtime-observer.js';
|
|
43
47
|
import { installReplayEndpoint, validateReplayEndpointConfig } from './replay-endpoint.js';
|
|
44
48
|
import { readPrivateRuntimeObserverKey } from './runtime-observer-protocol.js';
|
|
@@ -57,6 +61,7 @@ const schema = Schema.object({
|
|
|
57
61
|
revisionVerifications: Schema.any(),
|
|
58
62
|
revisionRegressions: Schema.any(),
|
|
59
63
|
creationCapabilities: Schema.any(),
|
|
64
|
+
revisionCapabilities: Schema.any(),
|
|
60
65
|
sourceApprovals: Schema.any(),
|
|
61
66
|
sourceReleases: Schema.any(),
|
|
62
67
|
sourceReleaseExecution: Schema.any(),
|
|
@@ -149,7 +154,8 @@ export function normalizeControlPlaneConfig(input) {
|
|
|
149
154
|
if (config.sourceBuild !== undefined && 'backend' in config.sourceBuild && config.sourceBuild.backend === 'linux-native') {
|
|
150
155
|
if (!config.sourceJobs)
|
|
151
156
|
throw new Error('plugin-control-plane: Linux repository preparation requires durable sourceJobs');
|
|
152
|
-
if ((config.creationVerifications || config.creationCapabilities || config.revisionVerifications || config.revisionRegressions
|
|
157
|
+
if ((config.creationVerifications || config.creationCapabilities || config.revisionVerifications || config.revisionRegressions
|
|
158
|
+
|| config.revisionCapabilities)
|
|
153
159
|
&& (!config.sourceBuild.userNamespace || !config.sourceBuild.systemSnapshot)) {
|
|
154
160
|
throw new Error('plugin-control-plane: Linux independent verification requires the dual-UID repository boundary');
|
|
155
161
|
}
|
|
@@ -246,9 +252,15 @@ export function normalizeControlPlaneConfig(input) {
|
|
|
246
252
|
const verification = config.creationVerifications;
|
|
247
253
|
const jobs = config.sourceJobs;
|
|
248
254
|
const build = config.sourceBuild;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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) {
|
|
252
264
|
throw new Error('plugin-control-plane: creation build, verification and adoption require matching execution backends');
|
|
253
265
|
}
|
|
254
266
|
if (!verification || !jobs?.creation || capability.namePrefix !== jobs.creation.namePrefix
|
|
@@ -262,6 +274,39 @@ export function normalizeControlPlaneConfig(input) {
|
|
|
262
274
|
if (adoptionKey.equals(verificationKey))
|
|
263
275
|
throw new Error('plugin-control-plane: adoption and verification signing keys must be separate');
|
|
264
276
|
}
|
|
277
|
+
if (config.revisionCapabilities !== undefined) {
|
|
278
|
+
validateRevisionCapabilityConfig(config.revisionCapabilities);
|
|
279
|
+
const capability = config.revisionCapabilities;
|
|
280
|
+
const creation = config.creationCapabilities;
|
|
281
|
+
const verification = config.revisionVerifications;
|
|
282
|
+
const jobs = config.sourceJobs;
|
|
283
|
+
const build = config.sourceBuild;
|
|
284
|
+
if (!creation || !verification || !jobs?.revision || !build) {
|
|
285
|
+
throw new Error('plugin-control-plane: revision adoption requires creation capabilities, revision verification and finite revision source');
|
|
286
|
+
}
|
|
287
|
+
const buildBackend = 'backend' in build ? build.backend : 'docker';
|
|
288
|
+
const runnerBackend = 'backend' in creation.runner ? creation.runner.backend : 'docker';
|
|
289
|
+
if (buildBackend !== runnerBackend
|
|
290
|
+
|| capability.namePrefix !== creation.namePrefix || capability.namePrefix !== jobs.revision.namePrefix
|
|
291
|
+
|| controlPlaneDigest(capability.owner) !== controlPlaneDigest(creation.owner)
|
|
292
|
+
|| capability.expiresAt > creation.expiresAt) {
|
|
293
|
+
throw new Error('plugin-control-plane: revision adoption authority does not match its authenticated creation lane');
|
|
294
|
+
}
|
|
295
|
+
if (capability.authorityId === creation.authorityId || capability.authorityId === verification.authority.authorityId
|
|
296
|
+
|| capability.rollback.authorityId === capability.authorityId || capability.rollback.authorityId === creation.authorityId
|
|
297
|
+
|| capability.rollback.authorityId === verification.authority.authorityId) {
|
|
298
|
+
throw new Error('plugin-control-plane: revision adoption, rollback, verification and creation authorities must be separate');
|
|
299
|
+
}
|
|
300
|
+
if (capability.keyId === creation.keyId || capability.keyId === verification.authority.keyId) {
|
|
301
|
+
throw new Error('plugin-control-plane: revision adoption signing key must differ from acceptance and creation keys');
|
|
302
|
+
}
|
|
303
|
+
const revisionKey = createPublicKey(revisionCapabilityPublicKey(capability)).export({ format: 'der', type: 'spki' });
|
|
304
|
+
const creationKey = createPublicKey(creationCapabilityPublicKey(creation)).export({ format: 'der', type: 'spki' });
|
|
305
|
+
const verificationKey = createPublicKey(verification.publicKey).export({ format: 'der', type: 'spki' });
|
|
306
|
+
if (revisionKey.equals(creationKey) || revisionKey.equals(verificationKey)) {
|
|
307
|
+
throw new Error('plugin-control-plane: revision adoption signing key material must be separate');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
265
310
|
if (![config.catalogPath, config.statePath, config.trustPath].every(isAbsolute))
|
|
266
311
|
throw new Error('plugin-control-plane: catalogPath, statePath and trustPath must be absolute');
|
|
267
312
|
return config;
|
|
@@ -285,12 +330,14 @@ export class PluginControlPlaneService extends Service {
|
|
|
285
330
|
nativeSourceRunner;
|
|
286
331
|
sourceRuntimes = new Set();
|
|
287
332
|
creationCapabilityRuntime;
|
|
333
|
+
revisionCapabilityRuntime;
|
|
288
334
|
creationCapabilityRuntimes = new Set();
|
|
289
335
|
creationCapabilityFlights = new Set();
|
|
290
336
|
creationCapabilityReconcileRequested = false;
|
|
291
337
|
growthRunProducer;
|
|
292
338
|
growthProviderDraining = false;
|
|
293
339
|
workItemCreationFrame;
|
|
340
|
+
workItemRevisionFrame;
|
|
294
341
|
constructor(ctx, input) {
|
|
295
342
|
super(ctx, 'pluginControlPlane');
|
|
296
343
|
this.config = normalizeControlPlaneConfig(input);
|
|
@@ -565,6 +612,11 @@ export class PluginControlPlaneService extends Service {
|
|
|
565
612
|
if (typeof current('automations')[method] !== 'function')
|
|
566
613
|
throw new Error(`plugin-control-plane: durable source jobs require assistantAutomations.${method}`);
|
|
567
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
|
+
}
|
|
568
620
|
if (typeof current('delivery').validateOwnerRoute !== 'function')
|
|
569
621
|
throw new Error('plugin-control-plane: durable source jobs require Delivery v2 owner validation');
|
|
570
622
|
let capabilities;
|
|
@@ -662,30 +714,133 @@ export class PluginControlPlaneService extends Service {
|
|
|
662
714
|
throw error;
|
|
663
715
|
}
|
|
664
716
|
}
|
|
717
|
+
let revisionCapabilities;
|
|
718
|
+
if (this.config.revisionCapabilities) {
|
|
719
|
+
const config = this.config.revisionCapabilities;
|
|
720
|
+
const delivery = current('delivery');
|
|
721
|
+
if (typeof delivery.validateOwnerAgentForRoute !== 'function') {
|
|
722
|
+
throw new Error('plugin-control-plane: revision adoption requires authenticated live owner Agent admission');
|
|
723
|
+
}
|
|
724
|
+
const journal = new RevisionCapabilityJournal({ path: join(this.config.statePath, 'revision-adoptions.sqlite'), config });
|
|
725
|
+
try {
|
|
726
|
+
revisionCapabilities = new RevisionCapabilityRuntime({ ctx: jobsCtx, config, journal,
|
|
727
|
+
regressionRequired: this.config.revisionRegressions !== undefined,
|
|
728
|
+
ports: {
|
|
729
|
+
inspect: planId => {
|
|
730
|
+
const certificate = this.inspectVerifiedRevision(planId);
|
|
731
|
+
if (!certificate)
|
|
732
|
+
throw new Error('revision adoption requires a fresh independent certificate');
|
|
733
|
+
const prepared = this.inspectPreparedRevision(planId);
|
|
734
|
+
const source = this.store.readRevisionSource(planId);
|
|
735
|
+
if (!source)
|
|
736
|
+
throw new Error('revision adoption source archive unavailable');
|
|
737
|
+
const regression = this.config.revisionRegressions === undefined
|
|
738
|
+
? undefined : this.inspectVerifiedRevisionRegression(planId);
|
|
739
|
+
return { certificate, artifact: Buffer.from(prepared.artifact), source,
|
|
740
|
+
plan: { id: prepared.plan.id, digest: prepared.plan.digest, name: prepared.plan.name },
|
|
741
|
+
...(regression === undefined ? {} : { regression }), owner: config.owner };
|
|
742
|
+
},
|
|
743
|
+
recheck: async (planId, signal) => {
|
|
744
|
+
await this.inspectPreparedRevisionReviewContext(planId, signal);
|
|
745
|
+
if (this.config.sourceBuild && 'backend' in this.config.sourceBuild
|
|
746
|
+
&& this.config.sourceBuild.backend === 'linux-native') {
|
|
747
|
+
await this.inspectPreparedRevisionLinuxBuild(planId, signal);
|
|
748
|
+
}
|
|
749
|
+
if (!this.inspectVerifiedRevision(planId))
|
|
750
|
+
throw new Error('revision adoption independent certificate changed');
|
|
751
|
+
},
|
|
752
|
+
parent: planId => {
|
|
753
|
+
const adoption = capabilities?.inspectAdoptionVersion(planId);
|
|
754
|
+
if (!adoption)
|
|
755
|
+
throw new Error('revision adoption parent adoption unavailable');
|
|
756
|
+
return adoption;
|
|
757
|
+
},
|
|
758
|
+
recheckParent: async (planId, signal) => {
|
|
759
|
+
if (!capabilities)
|
|
760
|
+
throw new Error('revision adoption parent adoption unavailable');
|
|
761
|
+
await capabilities.recheckAdoptionVersion(planId, signal);
|
|
762
|
+
},
|
|
763
|
+
inFlightParentCalls: planId => capabilities?.inspectInFlightCalls(planId) ?? 0,
|
|
764
|
+
suspendParent: async (planId) => {
|
|
765
|
+
if (!capabilities)
|
|
766
|
+
throw new Error('revision adoption parent adoption unavailable');
|
|
767
|
+
await capabilities.suspendEntry(planId);
|
|
768
|
+
},
|
|
769
|
+
resumeParent: async (planId) => {
|
|
770
|
+
if (!capabilities)
|
|
771
|
+
throw new Error('revision adoption parent adoption unavailable');
|
|
772
|
+
await capabilities.resumeEntry(planId);
|
|
773
|
+
},
|
|
774
|
+
runVersion: input => {
|
|
775
|
+
if (!capabilities)
|
|
776
|
+
throw new Error('revision adoption runner unavailable');
|
|
777
|
+
return capabilities.runRevisionArtifact(input);
|
|
778
|
+
},
|
|
779
|
+
recheckExecution: async (certificate, signal) => {
|
|
780
|
+
if (!capabilities)
|
|
781
|
+
throw new Error('revision adoption runner unavailable');
|
|
782
|
+
await capabilities.verifyArtifactExecution(certificate, signal);
|
|
783
|
+
},
|
|
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
|
+
},
|
|
792
|
+
assertCaller: execution => {
|
|
793
|
+
if (!execution.agent)
|
|
794
|
+
throw new Error('revision capability requires an authenticated owner Agent');
|
|
795
|
+
const receipt = delivery.validateOwnerAgentForRoute(execution.agent, config.owner.authorityId);
|
|
796
|
+
if (!receipt || Object.keys(config.owner).some(key => receipt[key] !== config.owner[key])) {
|
|
797
|
+
throw new Error('revision capability caller is not the current authorized owner');
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
} });
|
|
801
|
+
}
|
|
802
|
+
catch (error) {
|
|
803
|
+
journal.close();
|
|
804
|
+
throw error;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
const closeRevision = async () => {
|
|
808
|
+
const currentRuntime = revisionCapabilities;
|
|
809
|
+
revisionCapabilities = undefined;
|
|
810
|
+
if (this.revisionCapabilityRuntime === currentRuntime)
|
|
811
|
+
this.revisionCapabilityRuntime = undefined;
|
|
812
|
+
if (currentRuntime)
|
|
813
|
+
await currentRuntime.close();
|
|
814
|
+
};
|
|
665
815
|
const sourceBuild = this.config.sourceBuild;
|
|
666
816
|
let nativeRunner;
|
|
667
817
|
try {
|
|
668
|
-
nativeRunner = !('backend' in sourceBuild) ? undefined
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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 } }) };
|
|
686
837
|
}
|
|
687
838
|
catch (error) {
|
|
688
839
|
unsubscribeTaskChanges?.();
|
|
840
|
+
try {
|
|
841
|
+
await closeRevision();
|
|
842
|
+
}
|
|
843
|
+
catch { /* original error wins */ }
|
|
689
844
|
if (capabilities) {
|
|
690
845
|
await capabilities.close();
|
|
691
846
|
this.creationCapabilityRuntimes.delete(capabilities);
|
|
@@ -699,11 +854,13 @@ export class PluginControlPlaneService extends Service {
|
|
|
699
854
|
// Restored fresh adoptions read their original Linux build receipt.
|
|
700
855
|
// Their source runner must exist before capability reconciliation.
|
|
701
856
|
await capabilities?.start();
|
|
857
|
+
await revisionCapabilities?.start();
|
|
702
858
|
this.abort.signal.throwIfAborted();
|
|
703
859
|
runtime = new SourceJobRuntime({ config: this.config.sourceJobs, build: sourceBuild, statePath: this.config.statePath, store: this.store,
|
|
704
860
|
ports: {
|
|
705
861
|
automations: {
|
|
706
862
|
registerHostExecutor: executor => current('automations').registerHostExecutor(executor),
|
|
863
|
+
onSchedulerWake: listener => current('automations').onSchedulerWake(listener),
|
|
707
864
|
reconcileSystem: request => current('automations').reconcileSystem(request),
|
|
708
865
|
reconcileSystemExact: request => current('automations').reconcileSystemExact(request),
|
|
709
866
|
pauseSystemOwned: request => current('automations').pauseSystemOwned(request),
|
|
@@ -712,7 +869,7 @@ export class PluginControlPlaneService extends Service {
|
|
|
712
869
|
},
|
|
713
870
|
delivery: { validateOwnerRoute: request => current('delivery').validateOwnerRoute(request) },
|
|
714
871
|
}, withGapSourceFence: (gapId, owner, callback) => this.taskGaps.withCurrent(gapId, owner, callback),
|
|
715
|
-
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),
|
|
716
873
|
assertGrowthRun: (run, gapId, owner, generation) => this.assertSourceGrowthRun(run, gapId, owner, generation),
|
|
717
874
|
...(this.config.revisionVerifications ? { inspectRevisionParent: this.inspectRevisionParent,
|
|
718
875
|
verifyPreparedRevision: (job, signal) => this.verifyPreparedRevision(job, signal, () => jobsCtx.get('assistantVerifier')) } : {}),
|
|
@@ -724,6 +881,12 @@ export class PluginControlPlaneService extends Service {
|
|
|
724
881
|
throw new Error('created capability has no prepared source plan');
|
|
725
882
|
await capabilities.adopt(job.planId, signal);
|
|
726
883
|
} } : {}),
|
|
884
|
+
...(revisionCapabilities ? { revisionAdoptionEligible: (planId) => revisionCapabilities.eligible(planId),
|
|
885
|
+
adoptVerifiedRevision: async (job, signal) => {
|
|
886
|
+
if (!job.planId)
|
|
887
|
+
throw new Error('revision adoption has no prepared source plan');
|
|
888
|
+
await revisionCapabilities.adopt(job.planId, signal);
|
|
889
|
+
} } : {}),
|
|
727
890
|
...(this.config.sourceApprovals ? { approvePrepared: async (job, signal) => {
|
|
728
891
|
if (!job.planId)
|
|
729
892
|
throw new Error('source job has no prepared plan');
|
|
@@ -746,6 +909,14 @@ export class PluginControlPlaneService extends Service {
|
|
|
746
909
|
throw new Error('source job has no released plan');
|
|
747
910
|
await this.adoptOwnerSourceRelease({ sourcePlanId: job.planId, signal, expectedTrustDigest: job.intent.trustDigest, assertCurrent });
|
|
748
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
|
+
} : {}),
|
|
749
920
|
trust: () => this.boundTrust(), prepare: (job, signal, assertCurrent) => this.prepareSourceJob(job, signal, assertCurrent, nativeRunner?.backend === 'linux-native' ? { runner: nativeRunner.runner, generation: preparationGeneration } : undefined),
|
|
750
921
|
...(nativeRunner?.backend === 'linux-native' ? { isPreparationNotStarted: (error, job) => consumeCleanedLinuxSourcePreflight(error, job, preparationGeneration),
|
|
751
922
|
deterministicBuildFailure: (error, job) => consumeCleanedLinuxSourceBuildFailure(error, job, preparationGeneration) } : {}),
|
|
@@ -757,6 +928,10 @@ export class PluginControlPlaneService extends Service {
|
|
|
757
928
|
this.nativeSourceRunner = undefined;
|
|
758
929
|
await nativeRunner?.runner.close();
|
|
759
930
|
unsubscribeTaskChanges?.();
|
|
931
|
+
try {
|
|
932
|
+
await closeRevision();
|
|
933
|
+
}
|
|
934
|
+
catch { /* original error wins */ }
|
|
760
935
|
if (capabilities) {
|
|
761
936
|
await capabilities.close();
|
|
762
937
|
this.creationCapabilityRuntimes.delete(capabilities);
|
|
@@ -765,6 +940,7 @@ export class PluginControlPlaneService extends Service {
|
|
|
765
940
|
}
|
|
766
941
|
this.sourceRuntimes.add(runtime);
|
|
767
942
|
this.creationCapabilityRuntime = capabilities;
|
|
943
|
+
this.revisionCapabilityRuntime = revisionCapabilities;
|
|
768
944
|
const close = async () => {
|
|
769
945
|
unsubscribeTaskChanges?.();
|
|
770
946
|
if (this.sourceRuntime === runtime)
|
|
@@ -781,12 +957,20 @@ export class PluginControlPlaneService extends Service {
|
|
|
781
957
|
this.sourceRuntimes.delete(runtime);
|
|
782
958
|
if (this.creationCapabilityRuntime === capabilities)
|
|
783
959
|
this.creationCapabilityRuntime = undefined;
|
|
960
|
+
let revisionCloseError;
|
|
961
|
+
try {
|
|
962
|
+
await closeRevision();
|
|
963
|
+
}
|
|
964
|
+
catch (error) {
|
|
965
|
+
revisionCloseError = error;
|
|
966
|
+
}
|
|
784
967
|
const results = await Promise.allSettled([nativeRunner?.runner.close(), capabilities?.close()]);
|
|
785
968
|
if (capabilities)
|
|
786
969
|
this.creationCapabilityRuntimes.delete(capabilities);
|
|
787
970
|
const failures = results.filter((result) => result.status === 'rejected');
|
|
788
|
-
if (closeError !== undefined || failures.length) {
|
|
789
|
-
throw new AggregateError([...(closeError === undefined ? [] : [closeError]),
|
|
971
|
+
if (closeError !== undefined || revisionCloseError !== undefined || failures.length) {
|
|
972
|
+
throw new AggregateError([...(closeError === undefined ? [] : [closeError]),
|
|
973
|
+
...(revisionCloseError === undefined ? [] : [revisionCloseError]), ...failures.map(result => result.reason)], 'source runtime cleanup failed');
|
|
790
974
|
}
|
|
791
975
|
};
|
|
792
976
|
try {
|
|
@@ -1145,6 +1329,78 @@ export class PluginControlPlaneService extends Service {
|
|
|
1145
1329
|
this.abort.signal.throwIfAborted();
|
|
1146
1330
|
return this.creationCapabilityRuntime?.inspectCallEvidence(planId) ?? [];
|
|
1147
1331
|
};
|
|
1332
|
+
/** Host-only read-only revision adoption readiness; undefined when not configured or not admitting. */
|
|
1333
|
+
inspectRevisionAdoptionReadiness = () => {
|
|
1334
|
+
this.abort.signal.throwIfAborted();
|
|
1335
|
+
return this.revisionCapabilityRuntime?.inspectReadiness();
|
|
1336
|
+
};
|
|
1337
|
+
/** Host-only current stable entry version; no certificate, artifact bytes or key material escape. */
|
|
1338
|
+
inspectRevisionCapabilityEntry = (entryId) => {
|
|
1339
|
+
this.abort.signal.throwIfAborted();
|
|
1340
|
+
return this.revisionCapabilityRuntime?.inspectEntry(entryId);
|
|
1341
|
+
};
|
|
1342
|
+
/** Host-only bounded call attribution of one stable entry, including the exact version per call. */
|
|
1343
|
+
inspectRevisionCapabilityCalls = (entryId) => {
|
|
1344
|
+
this.abort.signal.throwIfAborted();
|
|
1345
|
+
return this.revisionCapabilityRuntime?.inspectCallEvidence(entryId) ?? [];
|
|
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
|
+
};
|
|
1381
|
+
/**
|
|
1382
|
+
* Host-only authenticated-owner exact parent rollback under its own finite grant. The parent
|
|
1383
|
+
* adoption receipt must still be current and the entry must be on the exact expected revision.
|
|
1384
|
+
*/
|
|
1385
|
+
rollbackRevisionCapabilityAdoption = async (input) => {
|
|
1386
|
+
this.abort.signal.throwIfAborted();
|
|
1387
|
+
const runtime = this.revisionCapabilityRuntime;
|
|
1388
|
+
const config = this.config.revisionCapabilities;
|
|
1389
|
+
if (!runtime || !config)
|
|
1390
|
+
throw new Error('revision adoption is not configured');
|
|
1391
|
+
const delivery = this.ctx.get('assistantDelivery', false);
|
|
1392
|
+
if (!delivery || typeof delivery.validateOwnerRoute !== 'function')
|
|
1393
|
+
throw new Error('revision rollback requires Delivery owner validation');
|
|
1394
|
+
const receipt = delivery.validateOwnerRoute({ authorityId: config.owner.authorityId, principalId: config.owner.principalId,
|
|
1395
|
+
workspace: config.owner.workspace, agentPreset: config.owner.agentPreset });
|
|
1396
|
+
if (!receipt || receipt.authorityId !== config.owner.authorityId || receipt.authorityHash !== config.owner.authorityHash
|
|
1397
|
+
|| receipt.principalId !== config.owner.principalId || receipt.principalRecordId !== config.owner.principalRecordId
|
|
1398
|
+
|| receipt.principalVersion !== config.owner.principalVersion || receipt.workspace !== config.owner.workspace
|
|
1399
|
+
|| receipt.agentPreset !== config.owner.agentPreset) {
|
|
1400
|
+
throw new Error('revision rollback caller is not the current authorized owner');
|
|
1401
|
+
}
|
|
1402
|
+
return runtime.rollback({ entryId: input.entryId, expectedCurrentPlanId: input.expectedVersionPlanId });
|
|
1403
|
+
};
|
|
1148
1404
|
/** Host-only historical source data. It grants no continuation, activation or replacement. */
|
|
1149
1405
|
inspectCreatedCapabilitySource = (planId) => {
|
|
1150
1406
|
this.abort.signal.throwIfAborted();
|
|
@@ -1261,6 +1517,143 @@ export class PluginControlPlaneService extends Service {
|
|
|
1261
1517
|
return undefined;
|
|
1262
1518
|
}
|
|
1263
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
|
+
}
|
|
1264
1657
|
/**
|
|
1265
1658
|
* Host-only, owner-invoked: seal the current ordinary-use trend of one adoption into the append-only,
|
|
1266
1659
|
* content-addressed snapshot ledger. Re-sealing an unchanged trend returns the existing row (`created: false`);
|
|
@@ -1323,7 +1716,8 @@ export class PluginControlPlaneService extends Service {
|
|
|
1323
1716
|
const growth = plan.creation?.growthRun;
|
|
1324
1717
|
const artifactSha256 = createHash('sha256').update(record.artifact).digest('hex');
|
|
1325
1718
|
if (!certificate || controlPlaneDigest(certificate) !== controlPlaneDigest(record.certificate)
|
|
1326
|
-
|| !growth ||
|
|
1719
|
+
|| !growth || growth.protocol === 'assistant-growth/source-run/v3'
|
|
1720
|
+
|| !plan.creation || !job.intent.creation?.growthRun || !plan.sourceCheck || !plan.preparedEvidence
|
|
1327
1721
|
|| plan.id !== receipt.planId || plan.digest !== receipt.planDigest
|
|
1328
1722
|
|| plan.name !== record.certificate.plan.name || plan.generatorDigest !== record.certificate.plan.generatorDigest
|
|
1329
1723
|
|| plan.sourceCheck.treeDigest !== record.certificate.plan.sourceTreeDigest
|
|
@@ -1375,7 +1769,8 @@ export class PluginControlPlaneService extends Service {
|
|
|
1375
1769
|
const ownerGrant = this.config.creationCapabilities?.owner;
|
|
1376
1770
|
const artifactSha256 = createHash('sha256').update(record.artifact).digest('hex');
|
|
1377
1771
|
if (!certificate || controlPlaneDigest(certificate) !== controlPlaneDigest(record.certificate)
|
|
1378
|
-
|| !growth ||
|
|
1772
|
+
|| !growth || growth.protocol === 'assistant-growth/source-run/v3'
|
|
1773
|
+
|| !creation || !ownerGrant || !job.intent.creation?.growthRun
|
|
1379
1774
|
|| !plan.sourceCheck || !plan.preparedEvidence
|
|
1380
1775
|
|| plan.id !== receipt.planId || plan.digest !== receipt.planDigest
|
|
1381
1776
|
|| plan.name !== record.certificate.plan.name || plan.generatorDigest !== record.certificate.plan.generatorDigest
|
|
@@ -1483,7 +1878,7 @@ export class PluginControlPlaneService extends Service {
|
|
|
1483
1878
|
throw new Error('work item growth authority pins changed');
|
|
1484
1879
|
const source = this.taskGaps.engineeringSnapshot(gapId, owner);
|
|
1485
1880
|
return producer.withWorkItemRunFence({ runId: run.runId, intentDigest: run.intentDigest, expectedGrowthRunDigest: sourceGrowthRunDigest(run) }, actual => {
|
|
1486
|
-
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)
|
|
1487
1882
|
throw new Error('work item producer changed its true binding');
|
|
1488
1883
|
this.workItemCreationFrame = { gapId, run: actual, source };
|
|
1489
1884
|
try {
|
|
@@ -1494,6 +1889,121 @@ export class PluginControlPlaneService extends Service {
|
|
|
1494
1889
|
}
|
|
1495
1890
|
});
|
|
1496
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
|
+
}
|
|
1497
2007
|
assertSourceGrowthRun(expected, gapId, owner, generation) {
|
|
1498
2008
|
validateSourceGrowthRunBinding(expected);
|
|
1499
2009
|
if (expected.protocol === 'assistant-growth/source-run/v2') {
|
|
@@ -1503,6 +2013,13 @@ export class PluginControlPlaneService extends Service {
|
|
|
1503
2013
|
}
|
|
1504
2014
|
return this.withWorkItemGrowthFrame(expected, gapId, owner, generation, () => { });
|
|
1505
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
|
+
}
|
|
1506
2023
|
const producer = this.growthRunProducer;
|
|
1507
2024
|
if (!producer)
|
|
1508
2025
|
throw new SourceGrowthRunUnavailableError('source growth run producer unavailable');
|
|
@@ -1525,7 +2042,7 @@ export class PluginControlPlaneService extends Service {
|
|
|
1525
2042
|
|| controlPlaneDigest(expected.revisionRegressionAcceptance) !== controlPlaneDigest(this.config.revisionRegressions.authority))) {
|
|
1526
2043
|
throw new Error('source growth run regression policy changed or was not pinned before authoring');
|
|
1527
2044
|
}
|
|
1528
|
-
if (expected.creationAcceptance !== undefined
|
|
2045
|
+
if ('creationAcceptance' in expected && expected.creationAcceptance !== undefined
|
|
1529
2046
|
&& (policy === undefined || controlPlaneDigest(policy) !== controlPlaneDigest(expected.creationAcceptance))) {
|
|
1530
2047
|
throw new Error('source growth run creation acceptance policy changed or was not pinned before authoring');
|
|
1531
2048
|
}
|
|
@@ -1609,6 +2126,7 @@ export class PluginControlPlaneService extends Service {
|
|
|
1609
2126
|
const current = this.store.getSourcePlan(input.planId);
|
|
1610
2127
|
const job = this.store.getPreparedSourceJob(input.planId);
|
|
1611
2128
|
if (current.digest !== input.planDigest || current.mode !== 'prepared-create' || !current.creation?.growthRun
|
|
2129
|
+
|| current.creation.growthRun.protocol === 'assistant-growth/source-run/v3'
|
|
1612
2130
|
|| !job.intent.creation?.growthRun || !current.sourceCheck || !current.preparedEvidence
|
|
1613
2131
|
|| current.preparedEvidence.pack.sha256 !== input.artifactSha256
|
|
1614
2132
|
|| controlPlaneDigest(reference) !== input.referenceDigest
|
|
@@ -1701,6 +2219,9 @@ export class PluginControlPlaneService extends Service {
|
|
|
1701
2219
|
const combined = AbortSignal.any([this.abort.signal, signal, AbortSignal.timeout(120_000)]);
|
|
1702
2220
|
combined.throwIfAborted();
|
|
1703
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
|
+
}
|
|
1704
2225
|
const native = this.nativeSourceRunner, evidence = prepared.plan.preparedEvidence;
|
|
1705
2226
|
if (!build || !('backend' in build) || build.backend !== 'linux-native' || !build.userNamespace || !build.systemSnapshot
|
|
1706
2227
|
|| native?.backend !== 'linux-native'
|
|
@@ -1788,10 +2309,34 @@ export class PluginControlPlaneService extends Service {
|
|
|
1788
2309
|
this.abort.signal.throwIfAborted();
|
|
1789
2310
|
const plan = this.store.getSourcePlan(planId);
|
|
1790
2311
|
const reference = this.store.getOwnerTaskFailureReference(plan.gapId);
|
|
1791
|
-
if (
|
|
2312
|
+
if (plan.mode !== 'prepared-revise' || plan.status !== 'pending-approval'
|
|
1792
2313
|
|| Date.now() >= plan.expiresAt || !plan.sourceRevision || Date.now() >= plan.sourceRevision.grant.expiresAt) {
|
|
1793
2314
|
throw new Error('prepared revision package is unavailable for independent verification');
|
|
1794
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
|
+
}
|
|
1795
2340
|
return this.taskGaps.withCurrent(plan.gapId, reference.owner, () => {
|
|
1796
2341
|
this.abort.signal.throwIfAborted();
|
|
1797
2342
|
const job = this.store.getPreparedRevisionJob(plan.id);
|
|
@@ -1808,6 +2353,104 @@ export class PluginControlPlaneService extends Service {
|
|
|
1808
2353
|
});
|
|
1809
2354
|
};
|
|
1810
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
|
+
}
|
|
1811
2454
|
inspectSourceRevisionAcceptanceAuthority = () => {
|
|
1812
2455
|
const policy = this.config.revisionVerifications?.authority;
|
|
1813
2456
|
if (this.abort.signal.aborted || !policy || Date.now() >= policy.expiresAt
|
|
@@ -1834,7 +2477,7 @@ export class PluginControlPlaneService extends Service {
|
|
|
1834
2477
|
const policy = this.inspectSourceRevisionRegressionAcceptanceAuthority();
|
|
1835
2478
|
const run = prepared.plan.sourceRevision.growthRun;
|
|
1836
2479
|
const source = this.store.readRevisionSource(planId);
|
|
1837
|
-
if (!certificate || !source || !policy || !run.revisionRegressionAcceptance
|
|
2480
|
+
if (!certificate || !source || !policy || !run || !run.revisionRegressionAcceptance
|
|
1838
2481
|
|| controlPlaneDigest(run.revisionRegressionAcceptance) !== controlPlaneDigest(policy)) {
|
|
1839
2482
|
throw new Error('regression requires fresh independently verified candidate and pre-author policy');
|
|
1840
2483
|
}
|
|
@@ -1848,6 +2491,9 @@ export class PluginControlPlaneService extends Service {
|
|
|
1848
2491
|
throw new Error('regression parent immutable artifact unavailable or changed');
|
|
1849
2492
|
}
|
|
1850
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
|
+
}
|
|
1851
2497
|
return { protocol: 'dsh-prepared-revision-regression/v1',
|
|
1852
2498
|
plan: prepared.plan, job: prepared.job, reference: prepared.reference,
|
|
1853
2499
|
candidate: { certificate, artifact: Buffer.from(prepared.artifact), sourceDigest: source.digest },
|
|
@@ -1868,6 +2514,7 @@ export class PluginControlPlaneService extends Service {
|
|
|
1868
2514
|
|| controlPlaneDigest(snapshot.candidate.certificate) !== input.candidateVerificationDigest
|
|
1869
2515
|
|| source.digest !== input.sourceDigest || snapshot.candidate.sourceDigest !== input.sourceDigest
|
|
1870
2516
|
|| controlPlaneDigest(policy) !== input.regressionAuthorityDigest
|
|
2517
|
+
|| snapshot.plan.sourceRevision.growthRun?.revisionRegressionAcceptance === undefined
|
|
1871
2518
|
|| controlPlaneDigest(snapshot.plan.sourceRevision.growthRun.revisionRegressionAcceptance) !== input.regressionAuthorityDigest
|
|
1872
2519
|
|| controlPlaneDigest(snapshot.parent.binding) !== input.parentDigest) {
|
|
1873
2520
|
throw new Error('regression parent, candidate or policy changed');
|
|
@@ -1879,6 +2526,9 @@ export class PluginControlPlaneService extends Service {
|
|
|
1879
2526
|
const certificate = this.store.getRevisionRegression(planId), policy = this.config.revisionRegressions;
|
|
1880
2527
|
if (!certificate || !policy || !verifyPluginRevisionRegressionCertificate(certificate, policy.authority, policy.publicKey))
|
|
1881
2528
|
return undefined;
|
|
2529
|
+
// Instruction revisions carry no regression acceptance and never enter this corridor.
|
|
2530
|
+
if (!isPluginRevisionTaskSource(certificate.source))
|
|
2531
|
+
return undefined;
|
|
1882
2532
|
const prepared = this.inspectPreparedRevisionRegression(planId);
|
|
1883
2533
|
this.withPreparedRevisionRegressionFence({ planId, planDigest: certificate.plan.digest, artifactSha256: certificate.plan.artifactSha256,
|
|
1884
2534
|
growthRunDigest: certificate.source.growthRunDigest, referenceDigest: certificate.source.referenceDigest,
|
|
@@ -1902,6 +2552,17 @@ export class PluginControlPlaneService extends Service {
|
|
|
1902
2552
|
const reference = this.store.getOwnerTaskFailureReference(plan.gapId);
|
|
1903
2553
|
if (!reference)
|
|
1904
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
|
+
}
|
|
1905
2566
|
return this.taskGaps.withCurrent(plan.gapId, reference.owner, () => {
|
|
1906
2567
|
this.abort.signal.throwIfAborted();
|
|
1907
2568
|
const current = this.store.getSourcePlan(input.planId);
|
|
@@ -1927,15 +2588,57 @@ export class PluginControlPlaneService extends Service {
|
|
|
1927
2588
|
return callback();
|
|
1928
2589
|
});
|
|
1929
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
|
+
};
|
|
1930
2627
|
/** Independent reviewer reads the actual checked tree through a disposable Git index. */
|
|
1931
2628
|
inspectPreparedRevisionReviewContext = async (planId, signal) => {
|
|
1932
2629
|
const combined = AbortSignal.any([this.abort.signal, signal, AbortSignal.timeout(120_000)]);
|
|
1933
2630
|
combined.throwIfAborted();
|
|
1934
2631
|
const prepared = this.inspectPreparedRevision(planId);
|
|
1935
|
-
const
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
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;
|
|
1939
2642
|
current();
|
|
1940
2643
|
const trust = await this.boundTrust();
|
|
1941
2644
|
const environment = inheritedEnvironment(trust);
|
|
@@ -1956,6 +2659,21 @@ export class PluginControlPlaneService extends Service {
|
|
|
1956
2659
|
const config = this.config.revisionVerifications;
|
|
1957
2660
|
if (!config || !verifyPluginRevisionVerificationCertificate(certificate, config.authority, config.publicKey))
|
|
1958
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
|
+
}
|
|
1959
2677
|
this.withPreparedRevisionFence({ planId, planDigest: certificate.plan.digest,
|
|
1960
2678
|
artifactSha256: certificate.plan.artifactSha256, growthRunDigest: certificate.source.growthRunDigest,
|
|
1961
2679
|
referenceDigest: certificate.source.referenceDigest, parentDigest: controlPlaneDigest(certificate.parent) }, () => {
|
|
@@ -1975,8 +2693,17 @@ export class PluginControlPlaneService extends Service {
|
|
|
1975
2693
|
this.abort.signal.throwIfAborted();
|
|
1976
2694
|
const plan = this.store.getSourcePlan(planId);
|
|
1977
2695
|
const reference = this.store.getOwnerTaskFailureReference(plan.gapId);
|
|
1978
|
-
if (
|
|
2696
|
+
if (plan.mode !== 'prepared-revise')
|
|
1979
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
|
+
}
|
|
1980
2707
|
return this.taskGaps.withCurrent(plan.gapId, reference.owner, () => {
|
|
1981
2708
|
const current = this.store.getSourcePlan(planId);
|
|
1982
2709
|
if (current.mode !== 'prepared-revise' || current.digest !== plan.digest) {
|
|
@@ -2005,6 +2732,8 @@ export class PluginControlPlaneService extends Service {
|
|
|
2005
2732
|
const { plan, reference, job } = this.store.getRetainedPreparedRevision(planId);
|
|
2006
2733
|
const certificate = this.store.getRevisionVerification(planId);
|
|
2007
2734
|
if (!certificate || !plan.sourceRevision || !isSourceOwnerContinuation(before, reference.owner)
|
|
2735
|
+
|| !isPluginRevisionTaskSource(certificate.source)
|
|
2736
|
+
|| plan.sourceRevision.growthRun === undefined
|
|
2008
2737
|
|| !verifyPluginRevisionVerificationCertificate(certificate, policy.authority, policy.publicKey, certificate.verifiedAt)
|
|
2009
2738
|
|| certificate.plan.digest !== plan.digest || certificate.source.referenceDigest !== controlPlaneDigest(reference)
|
|
2010
2739
|
|| certificate.source.ownerDigest !== job.intent.ownerDigest
|
|
@@ -2050,6 +2779,104 @@ export class PluginControlPlaneService extends Service {
|
|
|
2050
2779
|
throw new Error('plugin-control-plane: durable source jobs unavailable');
|
|
2051
2780
|
return this.sourceRuntime.inspect(input);
|
|
2052
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
|
+
};
|
|
2053
2880
|
reconcileSourceJob = (input) => {
|
|
2054
2881
|
if (this.sourceRuntime === undefined || this.sourceBuilds.size !== 0)
|
|
2055
2882
|
throw new Error('plugin-control-plane: durable source jobs unavailable or still draining');
|
|
@@ -2131,19 +2958,42 @@ export class PluginControlPlaneService extends Service {
|
|
|
2131
2958
|
if (!job.planId || !this.config.revisionVerifications || this.store.getRevisionVerificationStatus(job.planId) !== undefined)
|
|
2132
2959
|
return;
|
|
2133
2960
|
const prepared = this.inspectPreparedRevision(job.planId);
|
|
2134
|
-
if (prepared.job.id !== job.id || !prepared.plan.
|
|
2961
|
+
if (prepared.job.id !== job.id || !prepared.plan.preparedEvidence) {
|
|
2135
2962
|
throw new Error('revision verification job is not the exact prepared source');
|
|
2136
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');
|
|
2137
2977
|
const binding = { planId: prepared.plan.id, planDigest: prepared.plan.digest,
|
|
2138
2978
|
artifactSha256: prepared.plan.preparedEvidence.pack.sha256,
|
|
2139
2979
|
growthRunDigest: sourceGrowthRunDigest(prepared.plan.sourceRevision.growthRun),
|
|
2140
2980
|
referenceDigest: controlPlaneDigest(prepared.reference), parentDigest: controlPlaneDigest(prepared.plan.sourceRevision.parent) };
|
|
2141
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');
|
|
2142
2992
|
try {
|
|
2143
2993
|
const current = verifier();
|
|
2144
2994
|
if (typeof current?.verifyPluginRevision !== 'function')
|
|
2145
2995
|
throw new Error('independent revision verifier unavailable');
|
|
2146
|
-
const result = await current.verifyPluginRevision({ protocol: 'assistant-growth/revision-verification-request/v1', planId
|
|
2996
|
+
const result = await current.verifyPluginRevision({ protocol: 'assistant-growth/revision-verification-request/v1', planId }, signal);
|
|
2147
2997
|
signal.throwIfAborted();
|
|
2148
2998
|
if (result.status === 'verified' && !verifyPluginRevisionVerificationCertificate(result.certificate, this.config.revisionVerifications.authority, this.config.revisionVerifications.publicKey)) {
|
|
2149
2999
|
throw new Error('revision verification certificate signature or policy invalid');
|
|
@@ -2154,7 +3004,7 @@ export class PluginControlPlaneService extends Service {
|
|
|
2154
3004
|
const source = result.status === 'verified' ? await captureCreationCapabilitySource({ worktree: prepared.plan.worktree,
|
|
2155
3005
|
baseCommit: prepared.plan.baseCommit, scope: [...prepared.plan.scope].sort(), certificate: result.certificate,
|
|
2156
3006
|
environment: inheritedEnvironment(await this.boundTrust()), signal }) : undefined;
|
|
2157
|
-
|
|
3007
|
+
inFence(() => {
|
|
2158
3008
|
if (result.status === 'verified') {
|
|
2159
3009
|
if (!source)
|
|
2160
3010
|
throw new Error('verified revision source archive unavailable');
|
|
@@ -2169,7 +3019,7 @@ export class PluginControlPlaneService extends Service {
|
|
|
2169
3019
|
// The claimed row is a durable no-replay fence even if the process dies
|
|
2170
3020
|
// before settlement. A later native tick cannot dispatch another review.
|
|
2171
3021
|
try {
|
|
2172
|
-
|
|
3022
|
+
inFence(() => {
|
|
2173
3023
|
if (this.store.getRevisionVerificationStatus(job.planId) === 'claimed') {
|
|
2174
3024
|
this.store.settleRevisionVerification(job.planId, 'unknown', 'independent-verification-unsettled');
|
|
2175
3025
|
}
|
|
@@ -2187,11 +3037,17 @@ export class PluginControlPlaneService extends Service {
|
|
|
2187
3037
|
if (prepared.job.id !== job.id)
|
|
2188
3038
|
throw new Error('regression job is not exact prepared revision');
|
|
2189
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
|
+
}
|
|
2190
3046
|
const binding = { planId: prepared.plan.id, planDigest: prepared.plan.digest, artifactSha256: certificate.plan.artifactSha256,
|
|
2191
3047
|
growthRunDigest: certificate.source.growthRunDigest, referenceDigest: certificate.source.referenceDigest,
|
|
2192
3048
|
parentDigest: controlPlaneDigest(certificate.parent), candidateVerificationDigest: controlPlaneDigest(certificate),
|
|
2193
3049
|
sourceDigest: prepared.candidate.sourceDigest,
|
|
2194
|
-
regressionAuthorityDigest: controlPlaneDigest(
|
|
3050
|
+
regressionAuthorityDigest: controlPlaneDigest(acceptance) };
|
|
2195
3051
|
this.withPreparedRevisionRegressionFence(binding, () => this.store.claimRevisionRegression(job.planId));
|
|
2196
3052
|
try {
|
|
2197
3053
|
const current = verifier();
|
|
@@ -2438,8 +3294,7 @@ export class PluginControlPlaneService extends Service {
|
|
|
2438
3294
|
throw new ControlPlaneCliError('INVALID_ARGUMENT', 'ttlMs must be an integer within 900000..86400000');
|
|
2439
3295
|
}
|
|
2440
3296
|
const buildConfig = this.config.sourceBuild;
|
|
2441
|
-
const repositoryBuild = buildConfig !== undefined &&
|
|
2442
|
-
? buildConfig.backend === 'linux-native' : ['repository', 'portable-repository'].includes(buildConfig.profile ?? ''));
|
|
3297
|
+
const repositoryBuild = buildConfig !== undefined && 'backend' in buildConfig && buildConfig.backend === 'linux-native';
|
|
2443
3298
|
const maximumTimeoutMs = repositoryBuild ? 1_800_000 : 240_000;
|
|
2444
3299
|
const timeoutMs = input.timeoutMs ?? 180_000;
|
|
2445
3300
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 60_000 || timeoutMs > maximumTimeoutMs) {
|
|
@@ -2484,31 +3339,26 @@ export class PluginControlPlaneService extends Service {
|
|
|
2484
3339
|
}
|
|
2485
3340
|
// Only owner-bound creation jobs retain the exact package for the
|
|
2486
3341
|
// independent verifier. Ordinary source tools never receive these bytes.
|
|
2487
|
-
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);
|
|
2488
3344
|
const common = { worktree: isolated.worktree, baseCommit, name, scope: generated?.scope ?? [`plugins/${name}`],
|
|
2489
3345
|
environment, signal, assertCurrent, preparedAt: Date.now(), ...(creation === undefined ? {} : { creation }),
|
|
2490
3346
|
...(revision === undefined ? {} : { revision: { binding: revision, parentSource: parent.source,
|
|
2491
3347
|
parentCertificate: parent.certificate, files: input.files } }), ...(capturePack ? { capturePack: true } : {}) };
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
}
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
}
|
|
2507
|
-
return runNativePreparedChecks({ ...common, config: { ...configured, timeoutMs: Math.min(timeoutMs, configured.timeoutMs) },
|
|
2508
|
-
runner: native.runner, sourceJob: { id: sourceJob.id } });
|
|
2509
|
-
})()
|
|
2510
|
-
: await runDockerPreparedChecks({ ...common, config: { ...configured, timeoutMs: Math.min(timeoutMs, configured.timeoutMs) },
|
|
2511
|
-
...(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'); })();
|
|
2512
3362
|
await assertCurrent();
|
|
2513
3363
|
if (capturePack && checked.packArtifact === undefined)
|
|
2514
3364
|
throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'prepared creation artifact was not captured');
|
|
@@ -2535,6 +3385,11 @@ export class PluginControlPlaneService extends Service {
|
|
|
2535
3385
|
throw new Error('work item creation owner unavailable');
|
|
2536
3386
|
return this.withWorkItemGrowthFrame(creation.growthRun, input.gapId, gapOwner, false, callback);
|
|
2537
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
|
+
}
|
|
2538
3393
|
return this.taskGaps.withCurrent(input.gapId, gapOwner, callback);
|
|
2539
3394
|
};
|
|
2540
3395
|
return commitSource(() => this.store.createSourcePlan({ gapId: input.gapId, repository, worktree: isolated.worktree, baseCommit,
|