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