@dsh-enhanced/plugin-control-plane 0.1.48 → 0.1.53
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 +187 -15
- package/bin/dsh-live-qualification-authority.js +15 -0
- package/bin/dsh-local-release-adapter.js +331 -9
- package/bin/dsh-systemd-host-attestor.js +359 -9
- package/bin/dsh-systemd-host-authority.js +12 -0
- package/lib/adoption-coordinator.d.ts +3 -1
- package/lib/adoption-coordinator.d.ts.map +1 -1
- package/lib/adoption-coordinator.js +24 -9
- package/lib/adoption-coordinator.js.map +1 -1
- package/lib/cli.d.ts.map +1 -1
- package/lib/cli.js +33 -14
- package/lib/cli.js.map +1 -1
- package/lib/creation-capability-feedback.d.ts +23 -0
- package/lib/creation-capability-feedback.d.ts.map +1 -0
- package/lib/creation-capability-feedback.js +132 -0
- package/lib/creation-capability-feedback.js.map +1 -0
- package/lib/creation-capability-journal.d.ts +66 -0
- package/lib/creation-capability-journal.d.ts.map +1 -0
- package/lib/creation-capability-journal.js +775 -0
- package/lib/creation-capability-journal.js.map +1 -0
- package/lib/creation-capability-runtime.d.ts +63 -0
- package/lib/creation-capability-runtime.d.ts.map +1 -0
- package/lib/creation-capability-runtime.js +578 -0
- package/lib/creation-capability-runtime.js.map +1 -0
- package/lib/creation-capability-source.d.ts +37 -0
- package/lib/creation-capability-source.d.ts.map +1 -0
- package/lib/creation-capability-source.js +328 -0
- package/lib/creation-capability-source.js.map +1 -0
- package/lib/creation-capability-types.d.ts +265 -0
- package/lib/creation-capability-types.d.ts.map +1 -0
- package/lib/creation-capability-types.js +2 -0
- package/lib/creation-capability-types.js.map +1 -0
- package/lib/deployment-readiness.d.ts +16 -0
- package/lib/deployment-readiness.d.ts.map +1 -1
- package/lib/deployment-readiness.js +48 -1
- package/lib/deployment-readiness.js.map +1 -1
- package/lib/effect-blocked-replay.js +2 -2
- package/lib/effect-blocked-replay.js.map +1 -1
- package/lib/foreground-deployment-runtime.d.ts +3 -0
- package/lib/foreground-deployment-runtime.d.ts.map +1 -1
- package/lib/foreground-deployment-runtime.js +55 -13
- package/lib/foreground-deployment-runtime.js.map +1 -1
- package/lib/foreground-deployment.d.ts.map +1 -1
- package/lib/foreground-deployment.js +5 -0
- package/lib/foreground-deployment.js.map +1 -1
- package/lib/host-attestor.d.ts +2 -0
- package/lib/host-attestor.d.ts.map +1 -1
- package/lib/host-attestor.js +8 -1
- package/lib/host-attestor.js.map +1 -1
- package/lib/host-inputs.d.ts +13 -0
- package/lib/host-inputs.d.ts.map +1 -0
- package/lib/host-inputs.js +162 -0
- package/lib/host-inputs.js.map +1 -0
- package/lib/host-maintenance.d.ts +61 -0
- package/lib/host-maintenance.d.ts.map +1 -0
- package/lib/host-maintenance.js +158 -0
- package/lib/host-maintenance.js.map +1 -0
- package/lib/host-readiness.d.ts +5 -0
- package/lib/host-readiness.d.ts.map +1 -0
- package/lib/host-readiness.js +22 -0
- package/lib/host-readiness.js.map +1 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +7 -0
- package/lib/index.js.map +1 -1
- package/lib/live-qualification-authority.d.ts +32 -0
- package/lib/live-qualification-authority.d.ts.map +1 -0
- package/lib/live-qualification-authority.js +280 -0
- package/lib/live-qualification-authority.js.map +1 -0
- package/lib/live-qualification-client.d.ts +4 -0
- package/lib/live-qualification-client.d.ts.map +1 -0
- package/lib/live-qualification-client.js +22 -0
- package/lib/live-qualification-client.js.map +1 -0
- package/lib/live-qualification-runtime.d.ts +78 -0
- package/lib/live-qualification-runtime.d.ts.map +1 -0
- package/lib/live-qualification-runtime.js +367 -0
- package/lib/live-qualification-runtime.js.map +1 -0
- package/lib/live-qualification.d.ts +73 -0
- package/lib/live-qualification.d.ts.map +1 -0
- package/lib/live-qualification.js +164 -0
- package/lib/live-qualification.js.map +1 -0
- package/lib/native-source-build-worker.d.ts +4 -0
- package/lib/native-source-build-worker.d.ts.map +1 -0
- package/lib/native-source-build-worker.js +87 -0
- package/lib/native-source-build-worker.js.map +1 -0
- package/lib/native-source-build.d.ts +36 -0
- package/lib/native-source-build.d.ts.map +1 -0
- package/lib/native-source-build.js +143 -0
- package/lib/native-source-build.js.map +1 -0
- package/lib/native-source-pack.d.ts +11 -0
- package/lib/native-source-pack.d.ts.map +1 -0
- package/lib/native-source-pack.js +83 -0
- package/lib/native-source-pack.js.map +1 -0
- package/lib/owner-task-gaps.d.ts +2 -0
- package/lib/owner-task-gaps.d.ts.map +1 -1
- package/lib/owner-task-gaps.js +9 -1
- package/lib/owner-task-gaps.js.map +1 -1
- package/lib/runtime-epoch.d.ts +69 -0
- package/lib/runtime-epoch.d.ts.map +1 -0
- package/lib/runtime-epoch.js +139 -0
- package/lib/runtime-epoch.js.map +1 -0
- package/lib/runtime-observer-protocol.d.ts +3 -3
- package/lib/runtime-observer-protocol.js +5 -5
- package/lib/runtime-observer-protocol.js.map +1 -1
- package/lib/runtime-observer.d.ts.map +1 -1
- package/lib/runtime-observer.js +42 -6
- package/lib/runtime-observer.js.map +1 -1
- package/lib/service.d.ts +248 -4
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +1431 -64
- package/lib/service.js.map +1 -1
- package/lib/source-adoption-authority.d.ts +3 -0
- package/lib/source-adoption-authority.d.ts.map +1 -1
- package/lib/source-adoption-authority.js +13 -3
- package/lib/source-adoption-authority.js.map +1 -1
- package/lib/source-adoption-runner.d.ts +3 -0
- package/lib/source-adoption-runner.d.ts.map +1 -1
- package/lib/source-adoption-runner.js +16 -4
- package/lib/source-adoption-runner.js.map +1 -1
- package/lib/source-approval-authority.d.ts.map +1 -1
- package/lib/source-approval-authority.js +1 -0
- package/lib/source-approval-authority.js.map +1 -1
- package/lib/source-baseline.d.ts +37 -0
- package/lib/source-baseline.d.ts.map +1 -0
- package/lib/source-baseline.js +250 -0
- package/lib/source-baseline.js.map +1 -0
- package/lib/source-build.d.ts +43 -9
- package/lib/source-build.d.ts.map +1 -1
- package/lib/source-build.js +670 -57
- package/lib/source-build.js.map +1 -1
- package/lib/source-context.d.ts +18 -0
- package/lib/source-context.d.ts.map +1 -1
- package/lib/source-context.js +85 -11
- package/lib/source-context.js.map +1 -1
- package/lib/source-creation.d.ts +106 -0
- package/lib/source-creation.d.ts.map +1 -0
- package/lib/source-creation.js +664 -0
- package/lib/source-creation.js.map +1 -0
- package/lib/source-job-types.d.ts +28 -1
- package/lib/source-job-types.d.ts.map +1 -1
- package/lib/source-jobs.d.ts +33 -3
- package/lib/source-jobs.d.ts.map +1 -1
- package/lib/source-jobs.js +420 -43
- package/lib/source-jobs.js.map +1 -1
- package/lib/source-maintenance.d.ts +66 -0
- package/lib/source-maintenance.d.ts.map +1 -0
- package/lib/source-maintenance.js +177 -0
- package/lib/source-maintenance.js.map +1 -0
- package/lib/source-revision.d.ts +57 -0
- package/lib/source-revision.d.ts.map +1 -0
- package/lib/source-revision.js +175 -0
- package/lib/source-revision.js.map +1 -0
- package/lib/source-workspace.d.ts +19 -14
- package/lib/source-workspace.d.ts.map +1 -1
- package/lib/source-workspace.js +90 -17
- package/lib/source-workspace.js.map +1 -1
- package/lib/sqlite.d.ts +13 -3
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +362 -1
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +258 -3
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +2257 -96
- package/lib/store.js.map +1 -1
- package/lib/systemd-host-authority.d.ts +68 -0
- package/lib/systemd-host-authority.d.ts.map +1 -0
- package/lib/systemd-host-authority.js +424 -0
- package/lib/systemd-host-authority.js.map +1 -0
- package/lib/task-observation-evidence.d.ts +32 -0
- package/lib/task-observation-evidence.d.ts.map +1 -0
- package/lib/task-observation-evidence.js +62 -0
- package/lib/task-observation-evidence.js.map +1 -0
- package/lib/task-observation-runtime.d.ts +2 -3
- package/lib/task-observation-runtime.d.ts.map +1 -1
- package/lib/task-observation-runtime.js +7 -39
- package/lib/task-observation-runtime.js.map +1 -1
- package/lib/task-observation-store.js +1 -1
- package/lib/trust.d.ts.map +1 -1
- package/lib/trust.js +0 -0
- package/lib/trust.js.map +1 -1
- package/lib/types.d.ts +57 -7
- package/lib/types.d.ts.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +22 -14
package/lib/service.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { AdoptionCoordinatorRuntime, validateAdoptionCoordinatorConfig } from './adoption-coordinator.js';
|
|
2
|
-
import {
|
|
2
|
+
import { CreationCapabilityJournal, creationCapabilityPublicKey, validateCreationCapabilityConfig } from './creation-capability-journal.js';
|
|
3
|
+
import { CreationCapabilityRuntime } from './creation-capability-runtime.js';
|
|
4
|
+
import { inspectCreationCapabilityTaskAssociations } from './creation-capability-feedback.js';
|
|
5
|
+
import { captureCreationCapabilitySource } from './creation-capability-source.js';
|
|
6
|
+
import { lstat, readFile, realpath } from 'node:fs/promises';
|
|
3
7
|
import { realpathSync } from 'node:fs';
|
|
4
8
|
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
9
|
+
import { createHash, createPublicKey, randomBytes } from 'node:crypto';
|
|
10
|
+
import { isSourceOwnerContinuation, sourceGrowthRunDigest, validateCreationAcceptanceAuthorityRef, verifyPluginCreationVerificationCertificate, validateRevisionAcceptanceAuthorityRef, verifyPluginRevisionVerificationCertificate, validateRevisionRegressionAcceptanceAuthorityRef, verifyPluginRevisionRegressionCertificate, validateSourceGrowthRunBinding } from '@dsh-enhanced/assistant-growth-contract';
|
|
5
11
|
import { TaskObservationRuntime, validateTaskObservationConfig } from './task-observation-runtime.js';
|
|
12
|
+
import { LiveQualificationRuntime, validateLiveQualificationConfig } from './live-qualification-runtime.js';
|
|
6
13
|
import { rollbackPluginWatch } from './cli.js';
|
|
7
14
|
import { OwnerTaskFailureGaps } from './owner-task-gaps.js';
|
|
8
15
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
9
16
|
import Schema from '@deepseek-ai/schemastery';
|
|
10
17
|
import { discover, loadCatalogWithMetadata } from './catalog.js';
|
|
11
18
|
import { ControlPlaneCliError } from './errors.js';
|
|
12
|
-
import { assertPluginModificationAllowed, createIsolatedWorktree, gcPreparedModifyWorktrees, runLocalCommand, validateScopedPluginFiles, verifyPreparedSourceWorktree, writeScopedPluginFiles, } from './source-workspace.js';
|
|
19
|
+
import { assertPluginModificationAllowed, createIsolatedWorktree, gcPreparedModifyWorktrees, inspectPreparedCreationPatch, runLocalCommand, validateScopedPluginFiles, verifyPreparedSourceWorktree, writeScopedPluginFiles, } from './source-workspace.js';
|
|
13
20
|
import { assertManagedVersionPaths, managedPatchVersionFiles, verifyManagedPatchVersion } from './source-versioning.js';
|
|
14
21
|
import { requestSourceApproval, validateSourceApprovalClientConfig } from './source-approval-client.js';
|
|
15
22
|
import { requestSourceReleaseAuthorization, validateSourceReleaseClientConfig } from './source-release-client.js';
|
|
@@ -19,19 +26,34 @@ import { adoptSourceRelease, validateSourceAdoptionConfig } from './source-adopt
|
|
|
19
26
|
import { Ed25519ApprovalAuthority } from './approval.js';
|
|
20
27
|
import { ControlPlaneStore, MODIFY_GENERATOR_DIGEST, controlPlaneDigest } from './store.js';
|
|
21
28
|
import { runDockerPreparedChecks, validateSourceBuildConfig } from './source-build.js';
|
|
22
|
-
import {
|
|
29
|
+
import { runNativePreparedChecks } from './native-source-build.js';
|
|
30
|
+
import { NativeMacVerifierRunner } from '@dsh-enhanced/assistant-isolation';
|
|
31
|
+
import { NATIVE_SOURCE_COMPILER } from './native-source-build-worker.js';
|
|
32
|
+
import { awaitSourceSignal, inspectSourceContext, inspectSourceTargetsContext } from './source-context.js';
|
|
33
|
+
import { inspectSourceRevisionContext, prepareRevisedPluginWorkspace, verifyRevisedPluginWorkspace } from './source-revision.js';
|
|
34
|
+
import { inspectSourceCreationContext, prepareCreatedPluginWorkspace, validateSourceCreationFiles, verifyCreatedPluginWorkspace } from './source-creation.js';
|
|
35
|
+
import { resolveSourceBaseline } from './source-baseline.js';
|
|
23
36
|
import { inheritedEnvironment, loadTrustConfig, resolveTrustKey } from './trust.js';
|
|
24
37
|
import { registerPluginControlTools } from './tools.js';
|
|
25
|
-
import { SourceJobRuntime, validateSourceJobsConfig } from './source-jobs.js';
|
|
38
|
+
import { SourceGrowthRunUnavailableError, SourceJobRuntime, validateSourceJobsConfig } from './source-jobs.js';
|
|
26
39
|
import { installRuntimeObserver, validateRuntimeObserverConfig } from './runtime-observer.js';
|
|
27
40
|
import { installReplayEndpoint, validateReplayEndpointConfig } from './replay-endpoint.js';
|
|
28
41
|
import { readPrivateRuntimeObserverKey } from './runtime-observer-protocol.js';
|
|
29
|
-
import { createForegroundDeploymentObserver, foregroundTrustSnapshot, validateForegroundDeploymentConfig } from './foreground-deployment-runtime.js';
|
|
42
|
+
import { createForegroundDeploymentObserver, foregroundTrustSnapshot, queueRuntimeEpoch, validateForegroundDeploymentConfig } from './foreground-deployment-runtime.js';
|
|
43
|
+
import { captureRetainedDeploymentReadiness } from './deployment-readiness.js';
|
|
44
|
+
import { installHostReadiness, onHostReady } from './host-readiness.js';
|
|
45
|
+
const CREATION_UNKNOWN_CODES = new Set(['schema-observation-unknown', 'case-observation-unknown', 'contract-insufficient',
|
|
46
|
+
'interrupted', 'stale-source', 'verification-unknown', 'previous-unknown']);
|
|
47
|
+
const CREATION_REJECTED_CODES = new Set(['case-mismatch', 'source-review-rejected', 'request-invalid']);
|
|
30
48
|
const schema = Schema.object({
|
|
31
49
|
catalogPath: Schema.string().required(), statePath: Schema.string().required(), trustPath: Schema.string().required(),
|
|
32
50
|
proposalTtlMs: Schema.number().step(1).min(60_000).max(86_400_000).default(900_000),
|
|
33
51
|
sourceBuild: Schema.any(),
|
|
34
52
|
sourceJobs: Schema.any(),
|
|
53
|
+
creationVerifications: Schema.any(),
|
|
54
|
+
revisionVerifications: Schema.any(),
|
|
55
|
+
revisionRegressions: Schema.any(),
|
|
56
|
+
creationCapabilities: Schema.any(),
|
|
35
57
|
sourceApprovals: Schema.any(),
|
|
36
58
|
sourceReleases: Schema.any(),
|
|
37
59
|
sourceReleaseExecution: Schema.any(),
|
|
@@ -40,6 +62,7 @@ const schema = Schema.object({
|
|
|
40
62
|
runtimeObserver: Schema.any(),
|
|
41
63
|
foregroundDeployments: Schema.any(),
|
|
42
64
|
taskObservations: Schema.any(),
|
|
65
|
+
liveQualification: Schema.any(),
|
|
43
66
|
replayEndpoint: Schema.any(),
|
|
44
67
|
});
|
|
45
68
|
async function canonicalTarget(dshHome, profile) {
|
|
@@ -81,6 +104,17 @@ export function normalizeControlPlaneConfig(input) {
|
|
|
81
104
|
throw new Error('plugin-control-plane: taskObservations requires foregroundDeployments on the same profile');
|
|
82
105
|
}
|
|
83
106
|
}
|
|
107
|
+
if (config.liveQualification !== undefined) {
|
|
108
|
+
validateLiveQualificationConfig(config.liveQualification);
|
|
109
|
+
if (!config.foregroundDeployments || config.liveQualification.profilePath !== config.runtimeObserver?.profilePath) {
|
|
110
|
+
throw new Error('plugin-control-plane: liveQualification requires foregroundDeployments on the same profile');
|
|
111
|
+
}
|
|
112
|
+
if (!config.sourceAdoptions?.liveQualification || !config.sourceReleaseExecution?.independentReview
|
|
113
|
+
|| !config.taskObservations || config.taskObservations.profilePath !== config.liveQualification.profilePath
|
|
114
|
+
|| config.sourceAdoptions.profile !== basename(config.liveQualification.profilePath)) {
|
|
115
|
+
throw new Error('plugin-control-plane: liveQualification requires independently reviewed sourceAdoptions');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
84
118
|
if (config.replayEndpoint !== undefined) {
|
|
85
119
|
validateReplayEndpointConfig(config.replayEndpoint);
|
|
86
120
|
if (config.runtimeObserver !== undefined) {
|
|
@@ -102,6 +136,13 @@ export function normalizeControlPlaneConfig(input) {
|
|
|
102
136
|
validateSourceApprovalClientConfig(config.sourceApprovals);
|
|
103
137
|
if (config.sourceBuild !== undefined)
|
|
104
138
|
validateSourceBuildConfig(config.sourceBuild);
|
|
139
|
+
if (config.sourceBuild !== undefined && 'backend' in config.sourceBuild) {
|
|
140
|
+
if (!config.sourceJobs?.allowedModes || config.sourceJobs.allowedModes.some(mode => mode === 'modify')
|
|
141
|
+
|| config.sourceReleases !== undefined || config.sourceReleaseExecution !== undefined
|
|
142
|
+
|| config.sourceAdoptions !== undefined || config.sourceApprovals !== undefined) {
|
|
143
|
+
throw new Error('plugin-control-plane: native source build only supports Host-scaffolded create/revise-created');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
105
146
|
if (config.sourceReleases !== undefined) {
|
|
106
147
|
validateSourceReleaseClientConfig(config.sourceReleases);
|
|
107
148
|
if (!config.sourceApprovals || config.sourceBuild?.versioning !== 'patch')
|
|
@@ -122,12 +163,84 @@ export function normalizeControlPlaneConfig(input) {
|
|
|
122
163
|
validateSourceAdoptionConfig(config.sourceAdoptions);
|
|
123
164
|
if (!config.sourceReleaseExecution)
|
|
124
165
|
throw new Error('plugin-control-plane: sourceAdoptions requires sourceReleaseExecution');
|
|
166
|
+
if (config.sourceAdoptions.liveQualification && !config.liveQualification) {
|
|
167
|
+
throw new Error('plugin-control-plane: bounded-live sourceAdoptions requires liveQualification runtime');
|
|
168
|
+
}
|
|
125
169
|
}
|
|
126
170
|
if (config.sourceJobs !== undefined) {
|
|
127
171
|
validateSourceJobsConfig(config.sourceJobs, config.sourceBuild);
|
|
128
172
|
if (realpathSync(config.sourceJobs.repository) !== config.sourceJobs.repository)
|
|
129
173
|
throw new Error('plugin-control-plane: sourceJobs.repository must be canonical');
|
|
130
174
|
}
|
|
175
|
+
if (config.creationVerifications !== undefined) {
|
|
176
|
+
const policy = config.creationVerifications;
|
|
177
|
+
if (!policy || typeof policy !== 'object' || Array.isArray(policy)
|
|
178
|
+
|| Object.keys(policy).sort().join(',') !== 'authority,publicKey'
|
|
179
|
+
|| typeof policy.publicKey !== 'string' || policy.publicKey.length > 4096) {
|
|
180
|
+
throw new Error('plugin-control-plane: invalid creation verification configuration');
|
|
181
|
+
}
|
|
182
|
+
validateCreationAcceptanceAuthorityRef(policy.authority);
|
|
183
|
+
let key;
|
|
184
|
+
try {
|
|
185
|
+
key = createPublicKey(policy.publicKey);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
throw new Error('plugin-control-plane: creation verification public key is invalid');
|
|
189
|
+
}
|
|
190
|
+
if (!policy.publicKey.startsWith('-----BEGIN PUBLIC KEY-----\n') || key.asymmetricKeyType !== 'ed25519' || key.type !== 'public'
|
|
191
|
+
|| key.export({ format: 'pem', type: 'spki' }).toString() !== policy.publicKey
|
|
192
|
+
|| (policy.authority.expiresAt <= Date.now() && config.creationCapabilities?.retention === undefined)
|
|
193
|
+
|| !config.sourceJobs?.creation || policy.authority.namePrefix !== config.sourceJobs.creation.namePrefix
|
|
194
|
+
|| policy.authority.expiresAt > Math.min(config.sourceJobs.creation.expiresAt, config.sourceJobs.expiresAt)) {
|
|
195
|
+
throw new Error('plugin-control-plane: creation verification authority does not match finite source creation grant');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (config.revisionVerifications !== undefined || config.sourceJobs?.revision !== undefined) {
|
|
199
|
+
const policy = config.revisionVerifications, grant = config.sourceJobs?.revision;
|
|
200
|
+
if (!policy || !grant || !config.creationCapabilities || !config.sourceJobs
|
|
201
|
+
|| Object.keys(policy).sort().join(',') !== 'authority,publicKey' || typeof policy.publicKey !== 'string') {
|
|
202
|
+
throw new Error('plugin-control-plane: revision verification requires its separate finite grant and retained adopted source');
|
|
203
|
+
}
|
|
204
|
+
validateRevisionAcceptanceAuthorityRef(policy.authority);
|
|
205
|
+
const key = createPublicKey(policy.publicKey);
|
|
206
|
+
if (key.asymmetricKeyType !== 'ed25519' || policy.authority.namePrefix !== grant.namePrefix
|
|
207
|
+
|| policy.authority.expiresAt > Math.min(grant.expiresAt, config.sourceJobs.expiresAt)
|
|
208
|
+
|| createPublicKey(creationCapabilityPublicKey(config.creationCapabilities)).export({ format: 'der', type: 'spki' })
|
|
209
|
+
.equals(key.export({ format: 'der', type: 'spki' }))) {
|
|
210
|
+
throw new Error('plugin-control-plane: invalid independent revision verification authority');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (config.revisionRegressions !== undefined) {
|
|
214
|
+
const policy = config.revisionRegressions, jobs = config.sourceJobs, adoption = config.creationCapabilities;
|
|
215
|
+
if (!jobs?.revision || !config.revisionVerifications || !adoption
|
|
216
|
+
|| Object.keys(policy).sort().join(',') !== 'authority,publicKey' || typeof policy.publicKey !== 'string') {
|
|
217
|
+
throw new Error('plugin-control-plane: regression requires independently configured review and retained parent source');
|
|
218
|
+
}
|
|
219
|
+
validateRevisionRegressionAcceptanceAuthorityRef(policy.authority);
|
|
220
|
+
const key = createPublicKey(policy.publicKey);
|
|
221
|
+
if (key.asymmetricKeyType !== 'ed25519' || policy.authority.namePrefix !== jobs.revision.namePrefix
|
|
222
|
+
|| policy.authority.expiresAt > Math.min(jobs.expiresAt, jobs.revision.expiresAt, config.revisionVerifications.authority.expiresAt)
|
|
223
|
+
|| createPublicKey(creationCapabilityPublicKey(adoption)).export({ format: 'der', type: 'spki' })
|
|
224
|
+
.equals(key.export({ format: 'der', type: 'spki' }))) {
|
|
225
|
+
throw new Error('plugin-control-plane: invalid independent revision regression authority');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (config.creationCapabilities !== undefined) {
|
|
229
|
+
validateCreationCapabilityConfig(config.creationCapabilities);
|
|
230
|
+
const capability = config.creationCapabilities;
|
|
231
|
+
const verification = config.creationVerifications;
|
|
232
|
+
const jobs = config.sourceJobs;
|
|
233
|
+
if (!verification || !jobs?.creation || capability.namePrefix !== jobs.creation.namePrefix
|
|
234
|
+
|| capability.owner.authorityId !== jobs.ownerRouteId || capability.owner.principalId !== jobs.principalId
|
|
235
|
+
|| capability.owner.workspace !== jobs.workspace || capability.owner.agentPreset !== jobs.preset
|
|
236
|
+
|| capability.keyId === verification.authority.keyId) {
|
|
237
|
+
throw new Error('plugin-control-plane: created capability authority does not match its authenticated creation lane');
|
|
238
|
+
}
|
|
239
|
+
const adoptionKey = createPublicKey(creationCapabilityPublicKey(capability)).export({ format: 'der', type: 'spki' });
|
|
240
|
+
const verificationKey = createPublicKey(verification.publicKey).export({ format: 'der', type: 'spki' });
|
|
241
|
+
if (adoptionKey.equals(verificationKey))
|
|
242
|
+
throw new Error('plugin-control-plane: adoption and verification signing keys must be separate');
|
|
243
|
+
}
|
|
131
244
|
if (![config.catalogPath, config.statePath, config.trustPath].every(isAbsolute))
|
|
132
245
|
throw new Error('plugin-control-plane: catalogPath, statePath and trustPath must be absolute');
|
|
133
246
|
return config;
|
|
@@ -145,8 +258,17 @@ export class PluginControlPlaneService extends Service {
|
|
|
145
258
|
sourceReleaseAdvances = new Map();
|
|
146
259
|
sourceAdoptionFlights = new Map();
|
|
147
260
|
foregroundObservers = new Set();
|
|
261
|
+
liveRuntime;
|
|
262
|
+
assertLiveRuntime;
|
|
148
263
|
sourceRuntime;
|
|
264
|
+
nativeSourceRunner;
|
|
149
265
|
sourceRuntimes = new Set();
|
|
266
|
+
creationCapabilityRuntime;
|
|
267
|
+
creationCapabilityRuntimes = new Set();
|
|
268
|
+
creationCapabilityFlights = new Set();
|
|
269
|
+
creationCapabilityReconcileRequested = false;
|
|
270
|
+
growthRunProducer;
|
|
271
|
+
growthProviderDraining = false;
|
|
150
272
|
constructor(ctx, input) {
|
|
151
273
|
super(ctx, 'pluginControlPlane');
|
|
152
274
|
this.config = normalizeControlPlaneConfig(input);
|
|
@@ -164,8 +286,20 @@ export class PluginControlPlaneService extends Service {
|
|
|
164
286
|
this.abort.abort();
|
|
165
287
|
await Promise.allSettled([...this.sourceRuntimes].map(runtime => runtime.close()));
|
|
166
288
|
await Promise.allSettled([...this.sourceBuilds, ...this.sourceInspections, ...this.sourceApprovalFlights, ...this.sourceReleaseFlights, ...this.sourceAdoptionFlights.values()]);
|
|
167
|
-
this.
|
|
289
|
+
await Promise.allSettled(this.creationCapabilityFlights);
|
|
290
|
+
const closed = await Promise.allSettled([...this.creationCapabilityRuntimes].map(runtime => runtime.close()));
|
|
291
|
+
const failures = closed.filter((result) => result.status === 'rejected')
|
|
292
|
+
.map(result => result.reason);
|
|
293
|
+
try {
|
|
294
|
+
this.store.close();
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
failures.push(error);
|
|
298
|
+
}
|
|
299
|
+
if (failures.length)
|
|
300
|
+
throw new AggregateError(failures, 'created capability resource release unconfirmed');
|
|
168
301
|
}, 'plugin-control-plane.store');
|
|
302
|
+
installHostReadiness(ctx);
|
|
169
303
|
ctx.inject(['tools'], toolsCtx => registerPluginControlTools(toolsCtx, this));
|
|
170
304
|
if (this.config.runtimeObserver !== undefined)
|
|
171
305
|
installRuntimeObserver(ctx, this.config.runtimeObserver, this.config.foregroundDeployments ? (observerCtx, sample) => {
|
|
@@ -180,16 +314,54 @@ export class PluginControlPlaneService extends Service {
|
|
|
180
314
|
throw new Error('foreground deployment trust changed');
|
|
181
315
|
};
|
|
182
316
|
assertCurrent();
|
|
317
|
+
const assertLiveRuntime = (planId) => {
|
|
318
|
+
assertCurrent();
|
|
319
|
+
const plan = this.store.getPlan(planId);
|
|
320
|
+
const operation = this.store.getLiveQualificationReadiness(planId);
|
|
321
|
+
if (!operation.receipt)
|
|
322
|
+
throw new Error('live qualification has no retained signed readiness');
|
|
323
|
+
const binding = captureRetainedDeploymentReadiness({ plan, operation, receipt: operation.receipt, trust,
|
|
324
|
+
journalPath: this.config.foregroundDeployments.attestorJournalPath,
|
|
325
|
+
runtime: sample(randomBytes(32).toString('hex')) });
|
|
326
|
+
const window = this.store.getLiveQualificationWindow(planId);
|
|
327
|
+
if (binding.receiptDigest !== window.readinessDigest || binding.hostGeneration !== window.hostGeneration) {
|
|
328
|
+
throw new Error('live qualification runtime differs from retained readiness');
|
|
329
|
+
}
|
|
330
|
+
assertCurrent();
|
|
331
|
+
};
|
|
332
|
+
const requestRuntimeEpoch = (runtime) => {
|
|
333
|
+
assertCurrent();
|
|
334
|
+
queueRuntimeEpoch(this.store, trust, runtime, (gapId, owner, callback) => this.taskGaps.withCurrent(gapId, owner, callback));
|
|
335
|
+
};
|
|
183
336
|
const registration = createForegroundDeploymentObserver({ config: this.config.foregroundDeployments,
|
|
184
|
-
profilePath: this.config.runtimeObserver.profilePath, store: this.store, trust, sample, assertCurrent, owner: this });
|
|
337
|
+
profilePath: this.config.runtimeObserver.profilePath, store: this.store, trust, sample, assertCurrent, owner: this, requestRuntimeEpoch });
|
|
338
|
+
onHostReady(deliveryCtx, () => {
|
|
339
|
+
try {
|
|
340
|
+
requestRuntimeEpoch(sample(randomBytes(32).toString('hex')));
|
|
341
|
+
}
|
|
342
|
+
catch { /* Initial deployment and inactive/retracted sources have no continuity work. */ }
|
|
343
|
+
});
|
|
185
344
|
this.foregroundObservers.add(registration);
|
|
186
345
|
try {
|
|
187
346
|
const delivery = deliveryCtx.get('assistantDelivery');
|
|
188
347
|
const remove = delivery.registerForegroundTaskObserver(registration);
|
|
189
|
-
|
|
348
|
+
if (this.config.liveQualification) {
|
|
349
|
+
this.assertLiveRuntime = assertLiveRuntime;
|
|
350
|
+
this.liveRuntime?.scan();
|
|
351
|
+
}
|
|
352
|
+
return () => {
|
|
353
|
+
live = false;
|
|
354
|
+
if (this.assertLiveRuntime === assertLiveRuntime)
|
|
355
|
+
this.assertLiveRuntime = undefined;
|
|
356
|
+
this.liveRuntime?.scan();
|
|
357
|
+
this.foregroundObservers.delete(registration);
|
|
358
|
+
remove();
|
|
359
|
+
};
|
|
190
360
|
}
|
|
191
361
|
catch (error) {
|
|
192
362
|
live = false;
|
|
363
|
+
if (this.assertLiveRuntime === assertLiveRuntime)
|
|
364
|
+
this.assertLiveRuntime = undefined;
|
|
193
365
|
this.foregroundObservers.delete(registration);
|
|
194
366
|
throw error;
|
|
195
367
|
}
|
|
@@ -220,7 +392,16 @@ export class PluginControlPlaneService extends Service {
|
|
|
220
392
|
inspectSystemOwnedActivation: input => automations().inspectSystemOwnedActivation(input),
|
|
221
393
|
}, });
|
|
222
394
|
runtime.start();
|
|
223
|
-
|
|
395
|
+
const owner = 'plugin-control-plane-adoption-coordinator';
|
|
396
|
+
const automationId = `adoption-coordinator-${controlPlaneDigest({
|
|
397
|
+
coordinatorId: this.config.adoptionCoordinator.coordinatorId,
|
|
398
|
+
scope: this.config.adoptionCoordinator.scope,
|
|
399
|
+
}).slice(0, 40)}`;
|
|
400
|
+
const activationNonce = automations().inspectSystemOwnedActivation({ owner, automationId })?.activationNonce;
|
|
401
|
+
if (activationNonce === undefined)
|
|
402
|
+
throw new Error('plugin-control-plane: adoption coordinator registration is missing');
|
|
403
|
+
const stop = automations().registerHostShutdown(coordinatorCtx, { owner, automationId, activationNonce }, () => runtime.close({ skipPause: true }));
|
|
404
|
+
return stop;
|
|
224
405
|
}
|
|
225
406
|
catch (error) {
|
|
226
407
|
if (runtime)
|
|
@@ -276,61 +457,281 @@ export class PluginControlPlaneService extends Service {
|
|
|
276
457
|
}
|
|
277
458
|
}, 'plugin-control-plane.task-observations');
|
|
278
459
|
});
|
|
460
|
+
if (this.config.liveQualification !== undefined)
|
|
461
|
+
ctx.inject(['assistantAutomations', 'assistantDelivery', 'assistantEvaluation'], observerCtx => {
|
|
462
|
+
observerCtx.effect(async () => {
|
|
463
|
+
const snapshot = foregroundTrustSnapshot(this.config.trustPath), trust = await this.boundTrust();
|
|
464
|
+
this.abort.signal.throwIfAborted();
|
|
465
|
+
const store = new ControlPlaneStore({ path: trust.ledger.path });
|
|
466
|
+
const evaluation = () => observerCtx.get('assistantEvaluation');
|
|
467
|
+
const delivery = () => observerCtx.get('assistantDelivery');
|
|
468
|
+
const automations = () => observerCtx.get('assistantAutomations');
|
|
469
|
+
let runtime;
|
|
470
|
+
try {
|
|
471
|
+
runtime = new LiveQualificationRuntime({ config: this.config.liveQualification, store, trust,
|
|
472
|
+
assertCurrent: () => {
|
|
473
|
+
this.abort.signal.throwIfAborted();
|
|
474
|
+
if (foregroundTrustSnapshot(this.config.trustPath) !== snapshot)
|
|
475
|
+
throw new Error('live qualification trust changed');
|
|
476
|
+
},
|
|
477
|
+
assertRuntime: planId => {
|
|
478
|
+
if (!this.assertLiveRuntime)
|
|
479
|
+
throw new Error('live qualification Host observer unavailable');
|
|
480
|
+
this.assertLiveRuntime(planId);
|
|
481
|
+
},
|
|
482
|
+
runtimeAvailable: () => this.assertLiveRuntime !== undefined,
|
|
483
|
+
qualificationSource: planId => {
|
|
484
|
+
const plan = this.store.getPlan(planId);
|
|
485
|
+
const reference = this.store.getOwnerTaskFailureReference(plan.gapId);
|
|
486
|
+
if (!reference)
|
|
487
|
+
throw new Error('live qualification original owner failure unavailable');
|
|
488
|
+
return this.taskGaps.inspectCurrent(plan.gapId, reference.owner);
|
|
489
|
+
},
|
|
490
|
+
evaluation: {
|
|
491
|
+
canonicalHostScope: input => evaluation().canonicalHostScope(input),
|
|
492
|
+
getTrustedForegroundLearningProjection: input => evaluation().getTrustedForegroundLearningProjection(input),
|
|
493
|
+
withTrustedCanonicalTaskWriterFence: (input, callback) => evaluation().withTrustedCanonicalTaskWriterFence(input, callback),
|
|
494
|
+
onTrustedTaskChange: callback => evaluation().onTrustedTaskChange(callback),
|
|
495
|
+
},
|
|
496
|
+
delivery: {
|
|
497
|
+
validateOwnerRoute: input => delivery().validateOwnerRoute(input),
|
|
498
|
+
inspectOwnerForegroundLearningTask: input => delivery().inspectOwnerForegroundLearningTask(input),
|
|
499
|
+
},
|
|
500
|
+
automations: {
|
|
501
|
+
registerHostExecutor: input => automations().registerHostExecutor(input),
|
|
502
|
+
reconcileSystem: input => automations().reconcileSystem(input),
|
|
503
|
+
inspectSystemOwnedActivation: input => automations().inspectSystemOwnedActivation(input),
|
|
504
|
+
}, });
|
|
505
|
+
runtime.start();
|
|
506
|
+
this.liveRuntime = runtime;
|
|
507
|
+
return async () => {
|
|
508
|
+
if (this.liveRuntime === runtime)
|
|
509
|
+
this.liveRuntime = undefined;
|
|
510
|
+
await runtime.close();
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
if (runtime)
|
|
515
|
+
await runtime.close();
|
|
516
|
+
else
|
|
517
|
+
store.close();
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
}, 'plugin-control-plane.live-qualification');
|
|
521
|
+
});
|
|
279
522
|
if (this.config.sourceJobs !== undefined)
|
|
280
523
|
ctx.inject(['assistantAutomations', 'assistantDelivery',
|
|
281
524
|
...(this.config.sourceApprovals ? ['assistantEvaluation'] : []),
|
|
282
|
-
...(this.config.sourceReleaseExecution?.independentReview ? ['assistantVerifier', 'agents', 'sessions', 'tools', 'llm', 'systemPrompt', 'assistantPolicy'] : [])
|
|
525
|
+
...(this.config.sourceReleaseExecution?.independentReview ? ['assistantVerifier', 'agents', 'sessions', 'tools', 'llm', 'systemPrompt', 'assistantPolicy'] : []),
|
|
526
|
+
...(this.config.creationVerifications || this.config.revisionVerifications || this.config.revisionRegressions ? ['assistantVerifier'] : []),
|
|
527
|
+
...(this.config.creationCapabilities ? ['tools', 'assistantEvaluation', 'assistantPolicy'] : [])], jobsCtx => {
|
|
283
528
|
jobsCtx.effect(async () => {
|
|
284
529
|
this.abort.signal.throwIfAborted();
|
|
285
530
|
const current = (key) => jobsCtx.get((key === 'automations' ? 'assistantAutomations' : 'assistantDelivery'));
|
|
286
|
-
for (const method of ['registerHostExecutor', 'reconcileSystem', '
|
|
531
|
+
for (const method of ['registerHostExecutor', 'reconcileSystem', 'reconcileSystemExact', 'pauseSystemOwned',
|
|
532
|
+
'inspectSystemOwnedActivation', 'inspectSystemOwned']) {
|
|
287
533
|
if (typeof current('automations')[method] !== 'function')
|
|
288
534
|
throw new Error(`plugin-control-plane: durable source jobs require assistantAutomations.${method}`);
|
|
289
535
|
}
|
|
290
536
|
if (typeof current('delivery').validateOwnerRoute !== 'function')
|
|
291
537
|
throw new Error('plugin-control-plane: durable source jobs require Delivery v2 owner validation');
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
538
|
+
let capabilities;
|
|
539
|
+
let unsubscribeTaskChanges;
|
|
540
|
+
if (this.config.creationCapabilities) {
|
|
541
|
+
if (typeof current('delivery').validateOwnerAgentForRoute !== 'function') {
|
|
542
|
+
throw new Error('plugin-control-plane: created capabilities require authenticated live owner Agent admission');
|
|
543
|
+
}
|
|
544
|
+
const evaluation = jobsCtx.get('assistantEvaluation');
|
|
545
|
+
if (typeof evaluation.onTrustedTaskChange !== 'function') {
|
|
546
|
+
throw new Error('created capabilities require canonical task change notifications');
|
|
547
|
+
}
|
|
548
|
+
const config = this.config.creationCapabilities;
|
|
549
|
+
const journal = new CreationCapabilityJournal({ path: join(this.config.statePath, 'creation-adoptions.sqlite'), config });
|
|
550
|
+
try {
|
|
551
|
+
capabilities = new CreationCapabilityRuntime({ ctx: jobsCtx, config, journal, ports: {
|
|
552
|
+
inspect: planId => {
|
|
553
|
+
const certificate = this.inspectVerifiedCreation(planId);
|
|
554
|
+
if (!certificate)
|
|
555
|
+
throw new Error('created capability requires a fresh independent certificate');
|
|
556
|
+
const prepared = this.inspectPreparedCreation(planId);
|
|
557
|
+
const owner = config.owner;
|
|
558
|
+
if (Object.keys(owner).some(key => prepared.reference.owner[key] !== owner[key])) {
|
|
559
|
+
throw new Error('created capability owner differs from the finite grant');
|
|
560
|
+
}
|
|
561
|
+
return { certificate, artifact: prepared.artifact, owner };
|
|
562
|
+
},
|
|
563
|
+
recheck: async (planId, signal) => {
|
|
564
|
+
await this.inspectPreparedCreationReviewContext(planId, signal);
|
|
565
|
+
if (!this.inspectVerifiedCreation(planId))
|
|
566
|
+
throw new Error('created capability independent certificate changed');
|
|
567
|
+
},
|
|
568
|
+
captureSource: async (planId, signal) => {
|
|
569
|
+
const combined = AbortSignal.any([this.abort.signal, signal, AbortSignal.timeout(120_000)]);
|
|
570
|
+
const prepared = this.inspectPreparedCreation(planId);
|
|
571
|
+
const certificate = this.inspectVerifiedCreation(planId);
|
|
572
|
+
if (!certificate)
|
|
573
|
+
throw new Error('created capability source lacks a fresh certificate');
|
|
574
|
+
const trust = await this.boundTrust();
|
|
575
|
+
combined.throwIfAborted();
|
|
576
|
+
const source = await captureCreationCapabilitySource({ worktree: prepared.plan.worktree,
|
|
577
|
+
baseCommit: prepared.plan.baseCommit, scope: [...prepared.plan.scope].sort(), certificate,
|
|
578
|
+
environment: inheritedEnvironment(trust), signal: combined });
|
|
579
|
+
combined.throwIfAborted();
|
|
580
|
+
const latest = this.inspectVerifiedCreation(planId);
|
|
581
|
+
if (!latest || controlPlaneDigest(latest) !== controlPlaneDigest(certificate)) {
|
|
582
|
+
throw new Error('created capability source changed during capture');
|
|
583
|
+
}
|
|
584
|
+
return source;
|
|
585
|
+
},
|
|
586
|
+
inspectRetained: record => this.withRetainedCreatedCapabilityCurrent(record, () => ({
|
|
587
|
+
certificate: record.certificate, artifact: record.artifact, owner: config.owner
|
|
588
|
+
})),
|
|
589
|
+
recheckRetained: async (record, signal) => {
|
|
590
|
+
signal.throwIfAborted();
|
|
591
|
+
this.withRetainedCreatedCapabilityCurrent(record, () => undefined);
|
|
592
|
+
signal.throwIfAborted();
|
|
593
|
+
},
|
|
594
|
+
withCurrent: (record, callback) => this.withCreatedCapabilityCurrent(record, callback),
|
|
595
|
+
inspectCall: (_record, execution, toolAlias, argumentsJson) => {
|
|
596
|
+
if (!execution.agent)
|
|
597
|
+
return undefined;
|
|
598
|
+
const delivery = current('delivery');
|
|
599
|
+
if (typeof delivery.inspectOwnerForegroundToolCall !== 'function')
|
|
600
|
+
return undefined;
|
|
601
|
+
return delivery.inspectOwnerForegroundToolCall({ agent: execution.agent,
|
|
602
|
+
authorityId: config.owner.authorityId, callId: String(execution.callId),
|
|
603
|
+
toolName: toolAlias, argumentsJson });
|
|
604
|
+
},
|
|
605
|
+
assertCaller: (_record, execution) => {
|
|
606
|
+
if (!execution.agent)
|
|
607
|
+
throw new Error('created capability requires an authenticated owner Agent');
|
|
608
|
+
const delivery = current('delivery');
|
|
609
|
+
const receipt = delivery.validateOwnerAgentForRoute(execution.agent, config.owner.authorityId);
|
|
610
|
+
if (!receipt || Object.keys(config.owner).some(key => receipt[key] !== config.owner[key])) {
|
|
611
|
+
throw new Error('created capability caller is not the current authorized owner');
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
} });
|
|
615
|
+
this.creationCapabilityRuntimes.add(capabilities);
|
|
616
|
+
await capabilities.start();
|
|
617
|
+
this.abort.signal.throwIfAborted();
|
|
618
|
+
this.creationCapabilityRuntime = capabilities;
|
|
619
|
+
unsubscribeTaskChanges = evaluation.onTrustedTaskChange(() => this.reconcileCreatedCapabilities());
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
unsubscribeTaskChanges?.();
|
|
623
|
+
if (capabilities) {
|
|
624
|
+
await capabilities.close();
|
|
625
|
+
this.creationCapabilityRuntimes.delete(capabilities);
|
|
626
|
+
}
|
|
627
|
+
else
|
|
628
|
+
journal.close();
|
|
629
|
+
throw error;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const sourceBuild = this.config.sourceBuild;
|
|
633
|
+
let nativeRunner;
|
|
634
|
+
try {
|
|
635
|
+
nativeRunner = 'backend' in sourceBuild ? new NativeMacVerifierRunner({ backend: 'macos-seatbelt',
|
|
636
|
+
stateRoot: sourceBuild.stateRoot, authorityDigest: sourceBuild.authorityDigest,
|
|
637
|
+
runtime: sourceBuild.runtime, source: NATIVE_SOURCE_COMPILER, expiresAt: sourceBuild.expiresAt,
|
|
638
|
+
maxRuns: sourceBuild.maxRuns, maxTotalDurationMs: sourceBuild.maxTotalDurationMs,
|
|
639
|
+
maxDurationMs: sourceBuild.timeoutMs, maxOutputBytes: sourceBuild.outputBytes }) : undefined;
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
unsubscribeTaskChanges?.();
|
|
643
|
+
if (capabilities) {
|
|
644
|
+
await capabilities.close();
|
|
645
|
+
this.creationCapabilityRuntimes.delete(capabilities);
|
|
646
|
+
}
|
|
647
|
+
throw error;
|
|
648
|
+
}
|
|
649
|
+
this.nativeSourceRunner = nativeRunner;
|
|
650
|
+
let runtime;
|
|
651
|
+
try {
|
|
652
|
+
runtime = new SourceJobRuntime({ config: this.config.sourceJobs, build: sourceBuild, statePath: this.config.statePath, store: this.store,
|
|
653
|
+
ports: {
|
|
654
|
+
automations: {
|
|
655
|
+
registerHostExecutor: executor => current('automations').registerHostExecutor(executor),
|
|
656
|
+
reconcileSystem: request => current('automations').reconcileSystem(request),
|
|
657
|
+
reconcileSystemExact: request => current('automations').reconcileSystemExact(request),
|
|
658
|
+
pauseSystemOwned: request => current('automations').pauseSystemOwned(request),
|
|
659
|
+
inspectSystemOwnedActivation: request => current('automations').inspectSystemOwnedActivation(request),
|
|
660
|
+
inspectSystemOwned: request => current('automations').inspectSystemOwned(request),
|
|
661
|
+
},
|
|
662
|
+
delivery: { validateOwnerRoute: request => current('delivery').validateOwnerRoute(request) },
|
|
663
|
+
}, withGapSourceFence: (gapId, owner, callback) => this.taskGaps.withCurrent(gapId, owner, callback),
|
|
664
|
+
assertGrowthRun: (run, gapId, owner, generation) => this.assertSourceGrowthRun(run, gapId, owner, generation),
|
|
665
|
+
...(this.config.revisionVerifications ? { inspectRevisionParent: this.inspectRevisionParent,
|
|
666
|
+
verifyPreparedRevision: (job, signal) => this.verifyPreparedRevision(job, signal, () => jobsCtx.get('assistantVerifier')) } : {}),
|
|
667
|
+
...(this.config.revisionRegressions ? { verifyPreparedRevisionRegression: (job, signal) => this.verifyPreparedRevisionRegression(job, signal, () => jobsCtx.get('assistantVerifier')) } : {}),
|
|
668
|
+
...(this.config.creationVerifications ? { verifyPreparedCreation: (job, signal) => this.verifyPreparedCreation(job, signal, () => jobsCtx.get('assistantVerifier')) } : {}),
|
|
669
|
+
...(capabilities ? { creationAdoptionEligible: (planId) => capabilities.eligible(planId),
|
|
670
|
+
adoptVerifiedCreation: async (job, signal) => {
|
|
671
|
+
if (!job.planId)
|
|
672
|
+
throw new Error('created capability has no prepared source plan');
|
|
673
|
+
await capabilities.adopt(job.planId, signal);
|
|
674
|
+
} } : {}),
|
|
675
|
+
...(this.config.sourceApprovals ? { approvePrepared: async (job, signal) => {
|
|
676
|
+
if (!job.planId)
|
|
677
|
+
throw new Error('source job has no prepared plan');
|
|
678
|
+
await this.requestOwnerSourceApproval({ planId: job.planId, signal, expectedTrustDigest: job.intent.trustDigest });
|
|
679
|
+
} } : {}),
|
|
680
|
+
...(this.config.sourceReleases ? { releasePrepared: async (job, signal) => {
|
|
681
|
+
if (!job.planId)
|
|
682
|
+
throw new Error('source job has no prepared plan');
|
|
683
|
+
await this.requestOwnerSourceRelease({ planId: job.planId, signal, expectedTrustDigest: job.intent.trustDigest });
|
|
684
|
+
} } : {}),
|
|
685
|
+
...(this.config.sourceReleaseExecution ? { releaseTimeoutMs: this.config.sourceReleaseExecution.timeoutMs,
|
|
686
|
+
advanceReleased: async (job, signal) => {
|
|
687
|
+
if (!job.planId)
|
|
688
|
+
throw new Error('source job has no release plan');
|
|
689
|
+
await this.advanceOwnerSourceRelease({ planId: job.planId, signal, expectedTrustDigest: job.intent.trustDigest });
|
|
690
|
+
} } : {}),
|
|
691
|
+
...(this.config.sourceAdoptions ? { adoptionTimeoutMs: this.config.sourceAdoptions.timeoutMs,
|
|
692
|
+
adoptReleased: async (job, signal, assertCurrent) => {
|
|
693
|
+
if (!job.planId)
|
|
694
|
+
throw new Error('source job has no released plan');
|
|
695
|
+
await this.adoptOwnerSourceRelease({ sourcePlanId: job.planId, signal, expectedTrustDigest: job.intent.trustDigest, assertCurrent });
|
|
696
|
+
} } : {}),
|
|
697
|
+
trust: () => this.boundTrust(), prepare: (job, signal, assertCurrent) => this.prepareSourceJob(job, signal, assertCurrent),
|
|
698
|
+
...(nativeRunner ? { inspectNativeQuiescence: (key) => nativeRunner.inspectQuiescence(key) } : {}),
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
if (this.nativeSourceRunner === nativeRunner)
|
|
703
|
+
this.nativeSourceRunner = undefined;
|
|
704
|
+
await nativeRunner?.close();
|
|
705
|
+
unsubscribeTaskChanges?.();
|
|
706
|
+
if (capabilities) {
|
|
707
|
+
await capabilities.close();
|
|
708
|
+
this.creationCapabilityRuntimes.delete(capabilities);
|
|
709
|
+
}
|
|
710
|
+
throw error;
|
|
711
|
+
}
|
|
325
712
|
this.sourceRuntimes.add(runtime);
|
|
326
713
|
const close = async () => {
|
|
714
|
+
unsubscribeTaskChanges?.();
|
|
327
715
|
if (this.sourceRuntime === runtime)
|
|
328
716
|
this.sourceRuntime = undefined;
|
|
717
|
+
let closeError;
|
|
329
718
|
try {
|
|
330
719
|
await runtime.close();
|
|
331
720
|
}
|
|
332
|
-
|
|
333
|
-
|
|
721
|
+
catch (error) {
|
|
722
|
+
closeError = error;
|
|
723
|
+
}
|
|
724
|
+
if (this.nativeSourceRunner === nativeRunner)
|
|
725
|
+
this.nativeSourceRunner = undefined;
|
|
726
|
+
this.sourceRuntimes.delete(runtime);
|
|
727
|
+
if (this.creationCapabilityRuntime === capabilities)
|
|
728
|
+
this.creationCapabilityRuntime = undefined;
|
|
729
|
+
const results = await Promise.allSettled([nativeRunner?.close(), capabilities?.close()]);
|
|
730
|
+
if (capabilities)
|
|
731
|
+
this.creationCapabilityRuntimes.delete(capabilities);
|
|
732
|
+
const failures = results.filter((result) => result.status === 'rejected');
|
|
733
|
+
if (closeError !== undefined || failures.length) {
|
|
734
|
+
throw new AggregateError([...(closeError === undefined ? [] : [closeError]), ...failures.map(result => result.reason)], 'source runtime cleanup failed');
|
|
334
735
|
}
|
|
335
736
|
};
|
|
336
737
|
try {
|
|
@@ -367,7 +768,9 @@ export class PluginControlPlaneService extends Service {
|
|
|
367
768
|
};
|
|
368
769
|
async boundTrust() {
|
|
369
770
|
const trust = await loadTrustConfig(this.config.trustPath);
|
|
370
|
-
|
|
771
|
+
const localLedgerPath = resolve(join(this.config.statePath, 'control.sqlite'));
|
|
772
|
+
if ((this.config.adoptionCoordinator === undefined
|
|
773
|
+
? localLedgerPath !== trust.ledger.path : localLedgerPath === trust.ledger.path)
|
|
371
774
|
|| resolve(this.config.catalogPath) !== trust.catalog.path) {
|
|
372
775
|
throw new Error('plugin-control-plane: configured ledger/catalog do not match the owner trust binding');
|
|
373
776
|
}
|
|
@@ -474,7 +877,7 @@ export class PluginControlPlaneService extends Service {
|
|
|
474
877
|
const key = resolveTrustKey(trust, 'release-authorization', authorization.authority, authorization.keyId);
|
|
475
878
|
return (await this.store.startSourceRelease({ planId: plan.id, expectedRevision: plan.revision, authorization,
|
|
476
879
|
resolveAuthority: () => new Ed25519SourceReleaseAuthorizationAuthority(key.publicKeyPem, key.authority, key.keyId),
|
|
477
|
-
idempotencyKey: `source-release-authorization:${authorization.authorizationId}`, withSourceFence })).result;
|
|
880
|
+
idempotencyKey: `source-release-authorization:${authorization.authorizationId}`, withSourceFence, trust })).result;
|
|
478
881
|
})();
|
|
479
882
|
this.sourceReleaseFlights.add(operation);
|
|
480
883
|
try {
|
|
@@ -581,6 +984,11 @@ export class PluginControlPlaneService extends Service {
|
|
|
581
984
|
if (gapId !== plan.gapId)
|
|
582
985
|
throw new Error('source adoption escaped its owner task');
|
|
583
986
|
return current(callback);
|
|
987
|
+
},
|
|
988
|
+
withLiveQualificationFence: (planId, callback) => {
|
|
989
|
+
if (!this.liveRuntime)
|
|
990
|
+
throw new Error('live qualification runtime unavailable');
|
|
991
|
+
return this.liveRuntime.withQualificationFence(planId, callback);
|
|
584
992
|
} });
|
|
585
993
|
const withSourceFence = (callback) => current(() => store.withOwnerTaskFailureGapAdmission(plan.gapId, callback));
|
|
586
994
|
try {
|
|
@@ -610,6 +1018,652 @@ export class PluginControlPlaneService extends Service {
|
|
|
610
1018
|
health() { return this.store.health(); }
|
|
611
1019
|
canPrepareSource() { return this.config.sourceBuild !== undefined; }
|
|
612
1020
|
canEnqueueSource() { return this.sourceRuntime?.available() === true && !this.abort.signal.aborted; }
|
|
1021
|
+
/** Host-only durable Usage reader; the caller's binding is always an expectation. */
|
|
1022
|
+
registerSourceGrowthRunProducer = (producer) => {
|
|
1023
|
+
this.abort.signal.throwIfAborted();
|
|
1024
|
+
if (producer?.protocol !== 'assistant-growth-source-run-producer/v1' || typeof producer.inspect !== 'function'
|
|
1025
|
+
|| this.growthRunProducer !== undefined || this.growthProviderDraining)
|
|
1026
|
+
throw new Error('plugin-control-plane: source growth run producer is invalid or already registered');
|
|
1027
|
+
this.growthRunProducer = producer;
|
|
1028
|
+
try {
|
|
1029
|
+
this.sourceRuntime?.reconcileQueued();
|
|
1030
|
+
this.reconcileCreatedCapabilities();
|
|
1031
|
+
}
|
|
1032
|
+
catch (error) {
|
|
1033
|
+
this.growthRunProducer = undefined;
|
|
1034
|
+
throw error;
|
|
1035
|
+
}
|
|
1036
|
+
return async () => {
|
|
1037
|
+
if (this.growthRunProducer !== producer)
|
|
1038
|
+
return;
|
|
1039
|
+
this.growthRunProducer = undefined;
|
|
1040
|
+
this.growthProviderDraining = true;
|
|
1041
|
+
try {
|
|
1042
|
+
await this.sourceRuntime?.growthProviderDisposed();
|
|
1043
|
+
await this.creationCapabilityRuntime?.reconcile();
|
|
1044
|
+
}
|
|
1045
|
+
finally {
|
|
1046
|
+
this.growthProviderDraining = false;
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
};
|
|
1050
|
+
/** Host readiness nudge after durable Usage becomes available. */
|
|
1051
|
+
reconcileSourceGrowthRuns = () => {
|
|
1052
|
+
this.abort.signal.throwIfAborted();
|
|
1053
|
+
if (this.growthRunProducer !== undefined)
|
|
1054
|
+
this.sourceRuntime?.reconcileQueued();
|
|
1055
|
+
this.reconcileCreatedCapabilities();
|
|
1056
|
+
};
|
|
1057
|
+
reconcileCreatedCapabilities() {
|
|
1058
|
+
const runtime = this.creationCapabilityRuntime;
|
|
1059
|
+
if (!runtime || this.abort.signal.aborted)
|
|
1060
|
+
return;
|
|
1061
|
+
this.creationCapabilityReconcileRequested = true;
|
|
1062
|
+
if (this.creationCapabilityFlights.size)
|
|
1063
|
+
return;
|
|
1064
|
+
this.creationCapabilityReconcileRequested = false;
|
|
1065
|
+
const operation = runtime.reconcile();
|
|
1066
|
+
this.creationCapabilityFlights.add(operation);
|
|
1067
|
+
void operation.catch(error => this.ctx.logger.warn('created capability reconciliation failed', error))
|
|
1068
|
+
.finally(() => {
|
|
1069
|
+
this.creationCapabilityFlights.delete(operation);
|
|
1070
|
+
if (this.creationCapabilityReconcileRequested)
|
|
1071
|
+
this.reconcileCreatedCapabilities();
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
/** Content-free Host diagnostics; never register adoption as a model tool. */
|
|
1075
|
+
inspectCreatedCapability = (planId) => {
|
|
1076
|
+
this.abort.signal.throwIfAborted();
|
|
1077
|
+
return this.creationCapabilityRuntime?.inspectStatus(planId);
|
|
1078
|
+
};
|
|
1079
|
+
/** Exact immutable adoption and actual call evidence; no arguments, results or quality rating. */
|
|
1080
|
+
inspectCreatedCapabilityCalls = (planId) => {
|
|
1081
|
+
this.abort.signal.throwIfAborted();
|
|
1082
|
+
return this.creationCapabilityRuntime?.inspectCallEvidence(planId) ?? [];
|
|
1083
|
+
};
|
|
1084
|
+
/** Host-only historical source data. It grants no continuation, activation or replacement. */
|
|
1085
|
+
inspectCreatedCapabilitySource = (planId) => {
|
|
1086
|
+
this.abort.signal.throwIfAborted();
|
|
1087
|
+
const owner = this.config.creationCapabilities?.owner;
|
|
1088
|
+
const delivery = this.ctx.get('assistantDelivery', false);
|
|
1089
|
+
if (!owner || !delivery || typeof delivery.validateOwnerRoute !== 'function')
|
|
1090
|
+
return undefined;
|
|
1091
|
+
try {
|
|
1092
|
+
const before = delivery.validateOwnerRoute(owner);
|
|
1093
|
+
const snapshot = this.creationCapabilityRuntime?.inspectSourceEvidence(planId);
|
|
1094
|
+
if (!snapshot)
|
|
1095
|
+
return undefined;
|
|
1096
|
+
this.assertCreatedCapabilityAssociationSource(snapshot.record);
|
|
1097
|
+
const { plan, reference } = this.store.getRetainedPreparedCreation(planId);
|
|
1098
|
+
if (!isSourceOwnerContinuation(before, reference.owner)
|
|
1099
|
+
|| snapshot.archive.source.baseCommit !== plan.baseCommit
|
|
1100
|
+
|| controlPlaneDigest(snapshot.archive.source.scope) !== controlPlaneDigest([...plan.scope].sort())
|
|
1101
|
+
|| controlPlaneDigest(before) !== controlPlaneDigest(delivery.validateOwnerRoute(owner)))
|
|
1102
|
+
return undefined;
|
|
1103
|
+
const source = snapshot.archive.source;
|
|
1104
|
+
return Object.freeze({ ...snapshot.archive, source: Object.freeze({ ...source,
|
|
1105
|
+
scope: Object.freeze([...source.scope]), entries: Object.freeze(source.entries.map(entry => Object.freeze({ ...entry }))),
|
|
1106
|
+
files: Object.freeze(source.files.map(file => Object.freeze({ ...file }))),
|
|
1107
|
+
}) });
|
|
1108
|
+
}
|
|
1109
|
+
catch {
|
|
1110
|
+
return undefined;
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
inspectRevisionParent = (planId) => {
|
|
1114
|
+
const archive = this.inspectCreatedCapabilitySource(planId);
|
|
1115
|
+
const certificate = this.store.getCreationVerification(planId);
|
|
1116
|
+
const grant = this.config.sourceJobs?.revision;
|
|
1117
|
+
if (!archive || !certificate || !grant || !certificate.plan.name.startsWith(grant.namePrefix)
|
|
1118
|
+
|| archive.certificateDigest !== controlPlaneDigest(certificate) || archive.artifactSha256 !== certificate.plan.artifactSha256) {
|
|
1119
|
+
throw new Error('adopted revision parent source unavailable or changed');
|
|
1120
|
+
}
|
|
1121
|
+
return { name: certificate.plan.name, certificate, source: archive.source,
|
|
1122
|
+
parent: { planId, certificateDigest: archive.certificateDigest, artifactSha256: archive.artifactSha256,
|
|
1123
|
+
sourceArchiveDigest: controlPlaneDigest(archive), sourceDigest: archive.source.digest } };
|
|
1124
|
+
};
|
|
1125
|
+
/** Bounded current-owner inventory. These identities are hints, never causal evidence or use grants. */
|
|
1126
|
+
inspectCreatedCapabilityRevisionTargets = () => {
|
|
1127
|
+
if (!this.inspectSourceRevisionAcceptanceAuthority())
|
|
1128
|
+
return [];
|
|
1129
|
+
const targets = [];
|
|
1130
|
+
for (const plan of this.store.listPreparedSourcePlans().filter(plan => plan.mode === 'prepared-create').slice(-64)) {
|
|
1131
|
+
try {
|
|
1132
|
+
const parent = this.inspectRevisionParent(plan.id);
|
|
1133
|
+
targets.push({ parentPlanId: plan.id, name: parent.name, parentSourceDigest: parent.source.digest });
|
|
1134
|
+
}
|
|
1135
|
+
catch { /* an unavailable archive is not a revision target */ }
|
|
1136
|
+
}
|
|
1137
|
+
return Object.freeze(targets.map(target => Object.freeze(target)));
|
|
1138
|
+
};
|
|
1139
|
+
getSourceRevisionNamespace = () => {
|
|
1140
|
+
const policy = this.inspectSourceRevisionAcceptanceAuthority();
|
|
1141
|
+
return policy ? { namePrefix: policy.namePrefix } : undefined;
|
|
1142
|
+
};
|
|
1143
|
+
/** Host-only current association of exact adopted call versions with authenticated task heads. */
|
|
1144
|
+
inspectCreatedCapabilityTaskAssociations = (planId) => {
|
|
1145
|
+
this.abort.signal.throwIfAborted();
|
|
1146
|
+
const snapshot = this.creationCapabilityRuntime?.inspectAssociationEvidence(planId);
|
|
1147
|
+
if (!snapshot || !this.config.creationCapabilities)
|
|
1148
|
+
return [];
|
|
1149
|
+
const owner = this.config.creationCapabilities.owner;
|
|
1150
|
+
const evaluation = this.ctx.get('assistantEvaluation', false);
|
|
1151
|
+
const delivery = this.ctx.get('assistantDelivery', false);
|
|
1152
|
+
if (!evaluation || !delivery)
|
|
1153
|
+
return [];
|
|
1154
|
+
try {
|
|
1155
|
+
const historical = this.store.getRetainedPreparedCreation(planId);
|
|
1156
|
+
return inspectCreationCapabilityTaskAssociations({ ...snapshot, owner, evaluation, delivery,
|
|
1157
|
+
triggerInboxId: historical.reference.projection.subjectRef,
|
|
1158
|
+
readSourceCurrent: () => {
|
|
1159
|
+
this.assertCreatedCapabilityAssociationSource(snapshot.record);
|
|
1160
|
+
const current = this.taskGaps.inspectCurrent(historical.plan.gapId, historical.reference.owner);
|
|
1161
|
+
return { projection: current.canonical.projection, scopeWatermark: current.canonical.scopeWatermark };
|
|
1162
|
+
} });
|
|
1163
|
+
}
|
|
1164
|
+
catch {
|
|
1165
|
+
return [];
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
/** Exact historical certificate and source check without nesting an Evaluation writer fence. */
|
|
1169
|
+
assertCreatedCapabilityAssociationSource(record) {
|
|
1170
|
+
this.abort.signal.throwIfAborted();
|
|
1171
|
+
const receipt = record.receipt;
|
|
1172
|
+
const config = this.config.creationVerifications;
|
|
1173
|
+
const ownerGrant = this.config.creationCapabilities?.owner;
|
|
1174
|
+
if (!receipt || !config || !ownerGrant
|
|
1175
|
+
|| !verifyPluginCreationVerificationCertificate(record.certificate, config.authority, config.publicKey, receipt.adoptedAt))
|
|
1176
|
+
throw new Error('created capability association certificate invalid');
|
|
1177
|
+
const { plan, job, reference } = this.store.getRetainedPreparedCreation(record.planId);
|
|
1178
|
+
const certificate = this.store.getCreationVerification(record.planId);
|
|
1179
|
+
const growth = plan.creation?.growthRun;
|
|
1180
|
+
const artifactSha256 = createHash('sha256').update(record.artifact).digest('hex');
|
|
1181
|
+
if (!certificate || controlPlaneDigest(certificate) !== controlPlaneDigest(record.certificate)
|
|
1182
|
+
|| !growth || !plan.creation || !job.intent.creation?.growthRun || !plan.sourceCheck || !plan.preparedEvidence
|
|
1183
|
+
|| plan.id !== receipt.planId || plan.digest !== receipt.planDigest
|
|
1184
|
+
|| plan.name !== record.certificate.plan.name || plan.generatorDigest !== record.certificate.plan.generatorDigest
|
|
1185
|
+
|| plan.sourceCheck.treeDigest !== record.certificate.plan.sourceTreeDigest
|
|
1186
|
+
|| plan.sourceCheck.patchDigest !== record.certificate.plan.sourcePatchDigest
|
|
1187
|
+
|| plan.preparedEvidence.pack.sha256 !== record.certificate.plan.artifactSha256
|
|
1188
|
+
|| plan.preparedEvidence.pack.sizeBytes !== record.certificate.plan.artifactBytes
|
|
1189
|
+
|| artifactSha256 !== record.certificate.plan.artifactSha256
|
|
1190
|
+
|| record.artifact.length !== record.certificate.plan.artifactBytes
|
|
1191
|
+
|| controlPlaneDigest(reference) !== record.certificate.source.referenceDigest
|
|
1192
|
+
|| controlPlaneDigest(reference.owner) !== record.certificate.source.ownerDigest
|
|
1193
|
+
|| sourceGrowthRunDigest(growth) !== record.certificate.source.growthRunDigest
|
|
1194
|
+
|| sourceGrowthRunDigest(job.intent.creation.growthRun) !== record.certificate.source.growthRunDigest
|
|
1195
|
+
|| controlPlaneDigest(growth.model) !== controlPlaneDigest(record.certificate.model)
|
|
1196
|
+
|| controlPlaneDigest(growth.creationAcceptance) !== controlPlaneDigest(config.authority)
|
|
1197
|
+
|| controlPlaneDigest(plan.creation.grant) !== controlPlaneDigest(this.config.sourceJobs?.creation)
|
|
1198
|
+
|| Object.keys(ownerGrant).some(key => reference.owner[key]
|
|
1199
|
+
!== ownerGrant[key]))
|
|
1200
|
+
throw new Error('created capability association source changed');
|
|
1201
|
+
}
|
|
1202
|
+
withCreatedCapabilityCurrent(record, callback) {
|
|
1203
|
+
this.abort.signal.throwIfAborted();
|
|
1204
|
+
if (record.status === 'active' && record.receipt?.protocol === 'dsh-created-capability-adoption/v2') {
|
|
1205
|
+
return this.withRetainedCreatedCapabilityCurrent(record, callback);
|
|
1206
|
+
}
|
|
1207
|
+
const certificate = this.inspectVerifiedCreation(record.planId);
|
|
1208
|
+
if (!certificate || controlPlaneDigest(certificate) !== controlPlaneDigest(record.certificate)) {
|
|
1209
|
+
throw new Error('created capability certificate or current source changed');
|
|
1210
|
+
}
|
|
1211
|
+
return this.withPreparedCreationFence({ planId: record.planId, planDigest: certificate.plan.digest,
|
|
1212
|
+
artifactSha256: certificate.plan.artifactSha256, growthRunDigest: certificate.source.growthRunDigest,
|
|
1213
|
+
referenceDigest: certificate.source.referenceDigest }, callback);
|
|
1214
|
+
}
|
|
1215
|
+
/** Historical signed evidence and immutable job are checked under the current canonical owner writer fence. */
|
|
1216
|
+
withRetainedCreatedCapabilityCurrent(record, callback) {
|
|
1217
|
+
this.abort.signal.throwIfAborted();
|
|
1218
|
+
const receipt = record.receipt;
|
|
1219
|
+
const config = this.config.creationVerifications;
|
|
1220
|
+
if (!receipt || receipt.protocol !== 'dsh-created-capability-adoption/v2' || !config
|
|
1221
|
+
|| !verifyPluginCreationVerificationCertificate(record.certificate, config.authority, config.publicKey, receipt.adoptedAt)) {
|
|
1222
|
+
throw new Error('created capability retained certificate is invalid');
|
|
1223
|
+
}
|
|
1224
|
+
const historical = this.store.getRetainedPreparedCreation(record.planId);
|
|
1225
|
+
return this.taskGaps.withCurrent(historical.plan.gapId, historical.reference.owner, () => {
|
|
1226
|
+
this.abort.signal.throwIfAborted();
|
|
1227
|
+
const { plan, job, reference } = this.store.getRetainedPreparedCreation(record.planId);
|
|
1228
|
+
const certificate = this.store.getCreationVerification(record.planId);
|
|
1229
|
+
const growth = plan.creation?.growthRun;
|
|
1230
|
+
const creation = plan.creation;
|
|
1231
|
+
const ownerGrant = this.config.creationCapabilities?.owner;
|
|
1232
|
+
const artifactSha256 = createHash('sha256').update(record.artifact).digest('hex');
|
|
1233
|
+
if (!certificate || controlPlaneDigest(certificate) !== controlPlaneDigest(record.certificate)
|
|
1234
|
+
|| !growth || !creation || !ownerGrant || !job.intent.creation?.growthRun
|
|
1235
|
+
|| !plan.sourceCheck || !plan.preparedEvidence
|
|
1236
|
+
|| plan.id !== receipt.planId || plan.digest !== receipt.planDigest
|
|
1237
|
+
|| plan.name !== record.certificate.plan.name || plan.generatorDigest !== record.certificate.plan.generatorDigest
|
|
1238
|
+
|| plan.sourceCheck.treeDigest !== record.certificate.plan.sourceTreeDigest
|
|
1239
|
+
|| plan.sourceCheck.patchDigest !== record.certificate.plan.sourcePatchDigest
|
|
1240
|
+
|| plan.preparedEvidence.pack.sha256 !== record.certificate.plan.artifactSha256
|
|
1241
|
+
|| plan.preparedEvidence.pack.sizeBytes !== record.certificate.plan.artifactBytes
|
|
1242
|
+
|| artifactSha256 !== record.certificate.plan.artifactSha256
|
|
1243
|
+
|| record.artifact.length !== record.certificate.plan.artifactBytes
|
|
1244
|
+
|| controlPlaneDigest(reference) !== record.certificate.source.referenceDigest
|
|
1245
|
+
|| controlPlaneDigest(reference.owner) !== record.certificate.source.ownerDigest
|
|
1246
|
+
|| sourceGrowthRunDigest(growth) !== record.certificate.source.growthRunDigest
|
|
1247
|
+
|| sourceGrowthRunDigest(job.intent.creation.growthRun) !== record.certificate.source.growthRunDigest
|
|
1248
|
+
|| controlPlaneDigest(growth.model) !== controlPlaneDigest(record.certificate.model)
|
|
1249
|
+
|| controlPlaneDigest(growth.creationAcceptance) !== controlPlaneDigest(config.authority)
|
|
1250
|
+
|| controlPlaneDigest(creation.grant) !== controlPlaneDigest(this.config.sourceJobs?.creation)
|
|
1251
|
+
|| Object.keys(ownerGrant).some(key => reference.owner[key]
|
|
1252
|
+
!== ownerGrant[key])) {
|
|
1253
|
+
throw new Error('created capability retained source binding changed');
|
|
1254
|
+
}
|
|
1255
|
+
return callback();
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
assertSourceGrowthRun(expected, gapId, owner, generation) {
|
|
1259
|
+
validateSourceGrowthRunBinding(expected);
|
|
1260
|
+
const producer = this.growthRunProducer;
|
|
1261
|
+
if (!producer)
|
|
1262
|
+
throw new SourceGrowthRunUnavailableError('source growth run producer unavailable');
|
|
1263
|
+
const actual = producer.inspect({ runId: expected.runId, intentDigest: expected.intentDigest });
|
|
1264
|
+
if (!actual)
|
|
1265
|
+
throw new Error('source growth run is absent or no longer current');
|
|
1266
|
+
validateSourceGrowthRunBinding(actual);
|
|
1267
|
+
if (sourceGrowthRunDigest(actual) !== sourceGrowthRunDigest(expected)
|
|
1268
|
+
|| expected.ownerDigest !== controlPlaneDigest(owner))
|
|
1269
|
+
throw new Error('source growth run immutable binding changed');
|
|
1270
|
+
const policy = this.config.creationVerifications?.authority;
|
|
1271
|
+
if (expected.revisionAcceptance !== undefined && (this.config.revisionVerifications === undefined
|
|
1272
|
+
|| controlPlaneDigest(expected.revisionAcceptance) !== controlPlaneDigest(this.config.revisionVerifications.authority))) {
|
|
1273
|
+
throw new Error('source growth run revision acceptance policy changed or was not pinned before authoring');
|
|
1274
|
+
}
|
|
1275
|
+
if ((expected.revisionRegressionAcceptance !== undefined || (this.config.revisionRegressions && expected.revisionAcceptance))
|
|
1276
|
+
&& (this.config.revisionRegressions === undefined || expected.revisionRegressionAcceptance === undefined
|
|
1277
|
+
|| controlPlaneDigest(expected.revisionRegressionAcceptance) !== controlPlaneDigest(this.config.revisionRegressions.authority))) {
|
|
1278
|
+
throw new Error('source growth run regression policy changed or was not pinned before authoring');
|
|
1279
|
+
}
|
|
1280
|
+
if (expected.creationAcceptance !== undefined
|
|
1281
|
+
&& (policy === undefined || controlPlaneDigest(policy) !== controlPlaneDigest(expected.creationAcceptance))) {
|
|
1282
|
+
throw new Error('source growth run creation acceptance policy changed or was not pinned before authoring');
|
|
1283
|
+
}
|
|
1284
|
+
const reference = this.store.getOwnerTaskFailureReference(gapId);
|
|
1285
|
+
if (!reference || expected.source.outcomeId !== reference.outcomeId
|
|
1286
|
+
|| expected.source.sourceDigest !== reference.sourceDigest
|
|
1287
|
+
|| controlPlaneDigest(expected.source.projection) !== controlPlaneDigest(reference.projection)
|
|
1288
|
+
|| expected.ownerDigest !== controlPlaneDigest(reference.owner))
|
|
1289
|
+
throw new Error('source growth run task reference changed');
|
|
1290
|
+
const current = this.taskGaps.inspectCurrent(gapId, owner);
|
|
1291
|
+
if (expected.modelOrigin === 'inherited-owner-task'
|
|
1292
|
+
&& (current.source.modelSelectionState !== 'frozen'
|
|
1293
|
+
|| controlPlaneDigest(current.source.modelSelection) !== controlPlaneDigest(expected.model))) {
|
|
1294
|
+
throw new Error('source growth run inherited model changed');
|
|
1295
|
+
}
|
|
1296
|
+
if (Date.now() >= expected.expiresAt || (generation && Date.now() >= expected.generationDeadlineAt)) {
|
|
1297
|
+
throw new Error('source growth run window expired');
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
/** Private Host handoff. Never register this package reader as a model tool. */
|
|
1301
|
+
inspectPreparedCreation = (planId) => {
|
|
1302
|
+
this.abort.signal.throwIfAborted();
|
|
1303
|
+
const plan = this.store.getSourcePlan(planId);
|
|
1304
|
+
const reference = this.store.getOwnerTaskFailureReference(plan.gapId);
|
|
1305
|
+
if (!reference || plan.mode !== 'prepared-create' || plan.status !== 'pending-approval'
|
|
1306
|
+
|| Date.now() >= plan.expiresAt || !plan.creation || Date.now() >= plan.creation.grant.expiresAt) {
|
|
1307
|
+
throw new Error('prepared creation package is unavailable for independent verification');
|
|
1308
|
+
}
|
|
1309
|
+
return this.taskGaps.withCurrent(plan.gapId, reference.owner, () => {
|
|
1310
|
+
this.abort.signal.throwIfAborted();
|
|
1311
|
+
const job = this.store.getPreparedSourceJob(plan.id);
|
|
1312
|
+
if (!plan.creation?.growthRun || !job.intent.creation?.growthRun)
|
|
1313
|
+
throw new Error('prepared creation lacks a frozen source growth run');
|
|
1314
|
+
this.assertSourceGrowthRun(plan.creation.growthRun, plan.gapId, reference.owner, false);
|
|
1315
|
+
return {
|
|
1316
|
+
protocol: 'dsh-prepared-creation/v1',
|
|
1317
|
+
plan: structuredClone(plan), job: structuredClone(job), reference: structuredClone(reference),
|
|
1318
|
+
source: this.taskGaps.inspectCurrent(plan.gapId, reference.owner),
|
|
1319
|
+
artifact: this.store.readPreparedSourceArtifact(plan.id),
|
|
1320
|
+
};
|
|
1321
|
+
});
|
|
1322
|
+
};
|
|
1323
|
+
/** Host-only pre-author policy reference; never grants candidate or model authority. */
|
|
1324
|
+
inspectSourceCreationAcceptanceAuthority = () => {
|
|
1325
|
+
const policy = this.config.creationVerifications?.authority;
|
|
1326
|
+
if (this.abort.signal.aborted || !policy || Date.now() >= policy.expiresAt
|
|
1327
|
+
|| Date.now() >= (this.config.sourceJobs?.creation?.expiresAt ?? 0)
|
|
1328
|
+
|| Date.now() >= (this.config.sourceJobs?.expiresAt ?? 0))
|
|
1329
|
+
return undefined;
|
|
1330
|
+
return structuredClone(policy);
|
|
1331
|
+
};
|
|
1332
|
+
/** Synchronous Evaluation writer fence for an exact independently reviewed creation. */
|
|
1333
|
+
withPreparedCreationFence = (input, callback) => {
|
|
1334
|
+
this.abort.signal.throwIfAborted();
|
|
1335
|
+
const policy = this.inspectSourceCreationAcceptanceAuthority();
|
|
1336
|
+
if (!policy)
|
|
1337
|
+
throw new Error('creation verification authority unavailable');
|
|
1338
|
+
const plan = this.store.getSourcePlan(input.planId);
|
|
1339
|
+
const reference = this.store.getOwnerTaskFailureReference(plan.gapId);
|
|
1340
|
+
if (!reference)
|
|
1341
|
+
throw new Error('creation verification source unavailable');
|
|
1342
|
+
return this.taskGaps.withCurrent(plan.gapId, reference.owner, () => {
|
|
1343
|
+
this.abort.signal.throwIfAborted();
|
|
1344
|
+
const current = this.store.getSourcePlan(input.planId);
|
|
1345
|
+
const job = this.store.getPreparedSourceJob(input.planId);
|
|
1346
|
+
if (current.digest !== input.planDigest || current.mode !== 'prepared-create' || !current.creation?.growthRun
|
|
1347
|
+
|| !job.intent.creation?.growthRun || !current.sourceCheck || !current.preparedEvidence
|
|
1348
|
+
|| current.preparedEvidence.pack.sha256 !== input.artifactSha256
|
|
1349
|
+
|| controlPlaneDigest(reference) !== input.referenceDigest
|
|
1350
|
+
|| sourceGrowthRunDigest(current.creation.growthRun) !== input.growthRunDigest
|
|
1351
|
+
|| sourceGrowthRunDigest(job.intent.creation.growthRun) !== input.growthRunDigest
|
|
1352
|
+
|| !current.creation.growthRun.creationAcceptance
|
|
1353
|
+
|| controlPlaneDigest(current.creation.growthRun.creationAcceptance) !== controlPlaneDigest(policy)
|
|
1354
|
+
|| controlPlaneDigest(current.creation.grant) !== controlPlaneDigest(this.config.sourceJobs?.creation)) {
|
|
1355
|
+
throw new Error('creation verification exact source binding changed');
|
|
1356
|
+
}
|
|
1357
|
+
this.assertSourceGrowthRun(current.creation.growthRun, current.gapId, reference.owner, false);
|
|
1358
|
+
const artifact = this.store.readPreparedSourceArtifact(input.planId);
|
|
1359
|
+
if (createHash('sha256').update(artifact).digest('hex') !== input.artifactSha256) {
|
|
1360
|
+
throw new Error('creation verification artifact changed');
|
|
1361
|
+
}
|
|
1362
|
+
return callback();
|
|
1363
|
+
});
|
|
1364
|
+
};
|
|
1365
|
+
/** Independent reviewer reads the actual checked tree through a disposable Git index. */
|
|
1366
|
+
inspectPreparedCreationReviewContext = async (planId, signal) => {
|
|
1367
|
+
const combined = AbortSignal.any([this.abort.signal, signal, AbortSignal.timeout(120_000)]);
|
|
1368
|
+
combined.throwIfAborted();
|
|
1369
|
+
const prepared = this.inspectPreparedCreation(planId);
|
|
1370
|
+
const current = () => this.withPreparedCreationFence({ planId, planDigest: prepared.plan.digest,
|
|
1371
|
+
artifactSha256: prepared.plan.preparedEvidence.pack.sha256,
|
|
1372
|
+
growthRunDigest: sourceGrowthRunDigest(prepared.plan.creation.growthRun),
|
|
1373
|
+
referenceDigest: controlPlaneDigest(prepared.reference) }, () => undefined);
|
|
1374
|
+
current();
|
|
1375
|
+
const trust = await this.boundTrust();
|
|
1376
|
+
const environment = inheritedEnvironment(trust);
|
|
1377
|
+
const job = prepared.job;
|
|
1378
|
+
await verifyCreatedPluginWorkspace({ worktree: prepared.plan.worktree, baseCommit: prepared.plan.baseCommit,
|
|
1379
|
+
name: prepared.plan.name, environment, signal: combined, assertCurrent: current,
|
|
1380
|
+
creation: prepared.plan.creation, files: job.intent.files });
|
|
1381
|
+
const review = await inspectPreparedCreationPatch(prepared.plan, environment, combined);
|
|
1382
|
+
current();
|
|
1383
|
+
return review;
|
|
1384
|
+
};
|
|
1385
|
+
/** Signed behavior evidence over stored pack/checks. Adoption must await async review-context recheck. */
|
|
1386
|
+
inspectVerifiedCreation = (planId) => {
|
|
1387
|
+
const certificate = this.store.getCreationVerification(planId);
|
|
1388
|
+
if (!certificate)
|
|
1389
|
+
return undefined;
|
|
1390
|
+
const config = this.config.creationVerifications;
|
|
1391
|
+
if (!config || !verifyPluginCreationVerificationCertificate(certificate, config.authority, config.publicKey))
|
|
1392
|
+
return undefined;
|
|
1393
|
+
this.withPreparedCreationFence({ planId, planDigest: certificate.plan.digest,
|
|
1394
|
+
artifactSha256: certificate.plan.artifactSha256, growthRunDigest: certificate.source.growthRunDigest,
|
|
1395
|
+
referenceDigest: certificate.source.referenceDigest }, () => {
|
|
1396
|
+
const plan = this.store.getSourcePlan(planId);
|
|
1397
|
+
if (!plan.sourceCheck || !plan.preparedEvidence || certificate.plan.name !== plan.name
|
|
1398
|
+
|| certificate.plan.generatorDigest !== plan.generatorDigest
|
|
1399
|
+
|| certificate.plan.sourceTreeDigest !== plan.sourceCheck.treeDigest
|
|
1400
|
+
|| certificate.plan.sourcePatchDigest !== plan.sourceCheck.patchDigest
|
|
1401
|
+
|| certificate.plan.artifactBytes !== plan.preparedEvidence.pack.sizeBytes) {
|
|
1402
|
+
throw new Error('creation verification exact plan evidence changed');
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
return structuredClone(certificate);
|
|
1406
|
+
};
|
|
1407
|
+
/** Private Host diagnostic; no cases, prompts, source bytes or verifier output. */
|
|
1408
|
+
inspectCreationVerification = (planId) => {
|
|
1409
|
+
this.abort.signal.throwIfAborted();
|
|
1410
|
+
const plan = this.store.getSourcePlan(planId);
|
|
1411
|
+
const reference = this.store.getOwnerTaskFailureReference(plan.gapId);
|
|
1412
|
+
if (!reference || plan.mode !== 'prepared-create')
|
|
1413
|
+
throw new Error('creation verification owner source unavailable');
|
|
1414
|
+
return this.taskGaps.withCurrent(plan.gapId, reference.owner, () => {
|
|
1415
|
+
const current = this.store.getSourcePlan(planId);
|
|
1416
|
+
if (current.mode !== 'prepared-create' || current.digest !== plan.digest) {
|
|
1417
|
+
throw new Error('creation verification diagnostic plan changed');
|
|
1418
|
+
}
|
|
1419
|
+
return this.store.inspectCreationVerificationRecord(planId);
|
|
1420
|
+
});
|
|
1421
|
+
};
|
|
1422
|
+
/** Private Host handoff. Never register this package reader as a model tool. */
|
|
1423
|
+
inspectPreparedRevision = (planId) => {
|
|
1424
|
+
this.abort.signal.throwIfAborted();
|
|
1425
|
+
const plan = this.store.getSourcePlan(planId);
|
|
1426
|
+
const reference = this.store.getOwnerTaskFailureReference(plan.gapId);
|
|
1427
|
+
if (!reference || plan.mode !== 'prepared-revise' || plan.status !== 'pending-approval'
|
|
1428
|
+
|| Date.now() >= plan.expiresAt || !plan.sourceRevision || Date.now() >= plan.sourceRevision.grant.expiresAt) {
|
|
1429
|
+
throw new Error('prepared revision package is unavailable for independent verification');
|
|
1430
|
+
}
|
|
1431
|
+
return this.taskGaps.withCurrent(plan.gapId, reference.owner, () => {
|
|
1432
|
+
this.abort.signal.throwIfAborted();
|
|
1433
|
+
const job = this.store.getPreparedRevisionJob(plan.id);
|
|
1434
|
+
if (!plan.sourceRevision?.growthRun || !job.intent.revision?.growthRun)
|
|
1435
|
+
throw new Error('prepared revision lacks a frozen source growth run');
|
|
1436
|
+
this.assertSourceGrowthRun(plan.sourceRevision.growthRun, plan.gapId, reference.owner, false);
|
|
1437
|
+
this.inspectRevisionParent(plan.sourceRevision.parent.planId);
|
|
1438
|
+
return {
|
|
1439
|
+
protocol: 'dsh-prepared-revision/v1',
|
|
1440
|
+
plan: structuredClone(plan), job: structuredClone(job), reference: structuredClone(reference),
|
|
1441
|
+
source: this.taskGaps.inspectCurrent(plan.gapId, reference.owner),
|
|
1442
|
+
artifact: this.store.readPreparedSourceArtifact(plan.id),
|
|
1443
|
+
};
|
|
1444
|
+
});
|
|
1445
|
+
};
|
|
1446
|
+
/** Host-only pre-author policy reference; never grants candidate or model authority. */
|
|
1447
|
+
inspectSourceRevisionAcceptanceAuthority = () => {
|
|
1448
|
+
const policy = this.config.revisionVerifications?.authority;
|
|
1449
|
+
if (this.abort.signal.aborted || !policy || Date.now() >= policy.expiresAt
|
|
1450
|
+
|| Date.now() >= (this.config.sourceJobs?.revision?.expiresAt ?? 0)
|
|
1451
|
+
|| Date.now() >= (this.config.sourceJobs?.expiresAt ?? 0))
|
|
1452
|
+
return undefined;
|
|
1453
|
+
return structuredClone(policy);
|
|
1454
|
+
};
|
|
1455
|
+
/** Host-only pre-author reference, separate from candidate verification and execution grants. */
|
|
1456
|
+
inspectSourceRevisionRegressionAcceptanceAuthority = () => {
|
|
1457
|
+
const policy = this.config.revisionRegressions?.authority;
|
|
1458
|
+
if (!policy)
|
|
1459
|
+
return undefined;
|
|
1460
|
+
if (this.abort.signal.aborted || Date.now() >= policy.expiresAt
|
|
1461
|
+
|| !this.inspectSourceRevisionAcceptanceAuthority()) {
|
|
1462
|
+
throw new Error('configured revision regression authority unavailable');
|
|
1463
|
+
}
|
|
1464
|
+
return structuredClone(policy);
|
|
1465
|
+
};
|
|
1466
|
+
/** Immutable artifacts only. Historical parent permission never grants a new invocation. */
|
|
1467
|
+
inspectPreparedRevisionRegression = (planId) => {
|
|
1468
|
+
const prepared = this.inspectPreparedRevision(planId);
|
|
1469
|
+
const certificate = this.inspectVerifiedRevision(planId);
|
|
1470
|
+
const policy = this.inspectSourceRevisionRegressionAcceptanceAuthority();
|
|
1471
|
+
const run = prepared.plan.sourceRevision.growthRun;
|
|
1472
|
+
const source = this.store.readRevisionSource(planId);
|
|
1473
|
+
if (!certificate || !source || !policy || !run.revisionRegressionAcceptance
|
|
1474
|
+
|| controlPlaneDigest(run.revisionRegressionAcceptance) !== controlPlaneDigest(policy)) {
|
|
1475
|
+
throw new Error('regression requires fresh independently verified candidate and pre-author policy');
|
|
1476
|
+
}
|
|
1477
|
+
const parent = this.inspectRevisionParent(certificate.parent.planId);
|
|
1478
|
+
const evidence = this.creationCapabilityRuntime?.inspectSourceEvidence(parent.parent.planId);
|
|
1479
|
+
if (!evidence || !evidence.record.receipt
|
|
1480
|
+
|| controlPlaneDigest(evidence.archive) !== parent.parent.sourceArchiveDigest
|
|
1481
|
+
|| controlPlaneDigest(evidence.record.certificate) !== parent.parent.certificateDigest
|
|
1482
|
+
|| createHash('sha256').update(evidence.record.artifact).digest('hex') !== parent.parent.artifactSha256
|
|
1483
|
+
|| evidence.record.artifact.length !== parent.certificate.plan.artifactBytes) {
|
|
1484
|
+
throw new Error('regression parent immutable artifact unavailable or changed');
|
|
1485
|
+
}
|
|
1486
|
+
this.inspectRevisionParent(parent.parent.planId);
|
|
1487
|
+
return { protocol: 'dsh-prepared-revision-regression/v1',
|
|
1488
|
+
plan: prepared.plan, job: prepared.job, reference: prepared.reference,
|
|
1489
|
+
candidate: { certificate, artifact: Buffer.from(prepared.artifact), sourceDigest: source.digest },
|
|
1490
|
+
parent: { binding: parent.parent, certificate: parent.certificate, artifact: Buffer.from(evidence.record.artifact) } };
|
|
1491
|
+
};
|
|
1492
|
+
withPreparedRevisionRegressionFence = (input, callback) => {
|
|
1493
|
+
// Resolve read-only snapshots before acquiring Evaluation's non-reentrant
|
|
1494
|
+
// writer transaction; inside it, compare durable data without another fence.
|
|
1495
|
+
const snapshot = this.inspectPreparedRevisionRegression(input.planId);
|
|
1496
|
+
return this.withPreparedRevisionFence(input, () => {
|
|
1497
|
+
const certificate = this.store.getRevisionVerification(input.planId);
|
|
1498
|
+
const source = this.store.readRevisionSource(input.planId);
|
|
1499
|
+
const policy = this.inspectSourceRevisionRegressionAcceptanceAuthority();
|
|
1500
|
+
const candidatePolicy = this.config.revisionVerifications;
|
|
1501
|
+
if (!certificate || !source || !policy || !candidatePolicy
|
|
1502
|
+
|| !verifyPluginRevisionVerificationCertificate(certificate, candidatePolicy.authority, candidatePolicy.publicKey)
|
|
1503
|
+
|| controlPlaneDigest(certificate) !== input.candidateVerificationDigest
|
|
1504
|
+
|| controlPlaneDigest(snapshot.candidate.certificate) !== input.candidateVerificationDigest
|
|
1505
|
+
|| source.digest !== input.sourceDigest || snapshot.candidate.sourceDigest !== input.sourceDigest
|
|
1506
|
+
|| controlPlaneDigest(policy) !== input.regressionAuthorityDigest
|
|
1507
|
+
|| controlPlaneDigest(snapshot.plan.sourceRevision.growthRun.revisionRegressionAcceptance) !== input.regressionAuthorityDigest
|
|
1508
|
+
|| controlPlaneDigest(snapshot.parent.binding) !== input.parentDigest) {
|
|
1509
|
+
throw new Error('regression parent, candidate or policy changed');
|
|
1510
|
+
}
|
|
1511
|
+
return callback();
|
|
1512
|
+
});
|
|
1513
|
+
};
|
|
1514
|
+
inspectVerifiedRevisionRegression = (planId) => {
|
|
1515
|
+
const certificate = this.store.getRevisionRegression(planId), policy = this.config.revisionRegressions;
|
|
1516
|
+
if (!certificate || !policy || !verifyPluginRevisionRegressionCertificate(certificate, policy.authority, policy.publicKey))
|
|
1517
|
+
return undefined;
|
|
1518
|
+
const prepared = this.inspectPreparedRevisionRegression(planId);
|
|
1519
|
+
this.withPreparedRevisionRegressionFence({ planId, planDigest: certificate.plan.digest, artifactSha256: certificate.plan.artifactSha256,
|
|
1520
|
+
growthRunDigest: certificate.source.growthRunDigest, referenceDigest: certificate.source.referenceDigest,
|
|
1521
|
+
parentDigest: controlPlaneDigest(certificate.parent), candidateVerificationDigest: certificate.candidateVerificationDigest,
|
|
1522
|
+
sourceDigest: certificate.sourceDigest, regressionAuthorityDigest: controlPlaneDigest(certificate.authority) }, () => {
|
|
1523
|
+
if (certificate.contractDigest !== prepared.parent.certificate.contractDigest
|
|
1524
|
+
|| controlPlaneDigest(certificate.plan) !== controlPlaneDigest(prepared.candidate.certificate.plan)
|
|
1525
|
+
|| controlPlaneDigest(certificate.model) !== controlPlaneDigest(prepared.candidate.certificate.model)) {
|
|
1526
|
+
throw new Error('regression result does not match original held-out and exact candidate');
|
|
1527
|
+
}
|
|
1528
|
+
});
|
|
1529
|
+
return structuredClone(certificate);
|
|
1530
|
+
};
|
|
1531
|
+
/** Synchronous Evaluation writer fence for an exact independently reviewed creation. */
|
|
1532
|
+
withPreparedRevisionFence = (input, callback) => {
|
|
1533
|
+
this.abort.signal.throwIfAborted();
|
|
1534
|
+
const policy = this.inspectSourceRevisionAcceptanceAuthority();
|
|
1535
|
+
if (!policy)
|
|
1536
|
+
throw new Error('revision verification authority unavailable');
|
|
1537
|
+
const plan = this.store.getSourcePlan(input.planId);
|
|
1538
|
+
const reference = this.store.getOwnerTaskFailureReference(plan.gapId);
|
|
1539
|
+
if (!reference)
|
|
1540
|
+
throw new Error('revision verification source unavailable');
|
|
1541
|
+
return this.taskGaps.withCurrent(plan.gapId, reference.owner, () => {
|
|
1542
|
+
this.abort.signal.throwIfAborted();
|
|
1543
|
+
const current = this.store.getSourcePlan(input.planId);
|
|
1544
|
+
const job = this.store.getPreparedRevisionJob(input.planId);
|
|
1545
|
+
if (current.digest !== input.planDigest || current.mode !== 'prepared-revise' || !current.sourceRevision?.growthRun
|
|
1546
|
+
|| !job.intent.revision?.growthRun || !current.sourceCheck || !current.preparedEvidence
|
|
1547
|
+
|| current.preparedEvidence.pack.sha256 !== input.artifactSha256
|
|
1548
|
+
|| controlPlaneDigest(reference) !== input.referenceDigest
|
|
1549
|
+
|| sourceGrowthRunDigest(current.sourceRevision.growthRun) !== input.growthRunDigest
|
|
1550
|
+
|| sourceGrowthRunDigest(job.intent.revision.growthRun) !== input.growthRunDigest
|
|
1551
|
+
|| !current.sourceRevision.growthRun.revisionAcceptance
|
|
1552
|
+
|| controlPlaneDigest(current.sourceRevision.growthRun.revisionAcceptance) !== controlPlaneDigest(policy)
|
|
1553
|
+
|| controlPlaneDigest(current.sourceRevision.grant) !== controlPlaneDigest(this.config.sourceJobs?.revision)
|
|
1554
|
+
|| controlPlaneDigest(current.sourceRevision.parent) !== input.parentDigest
|
|
1555
|
+
|| controlPlaneDigest(this.inspectRevisionParent(current.sourceRevision.parent.planId).parent) !== input.parentDigest) {
|
|
1556
|
+
throw new Error('revision verification exact source binding changed');
|
|
1557
|
+
}
|
|
1558
|
+
this.assertSourceGrowthRun(current.sourceRevision.growthRun, current.gapId, reference.owner, false);
|
|
1559
|
+
const artifact = this.store.readPreparedSourceArtifact(input.planId);
|
|
1560
|
+
if (createHash('sha256').update(artifact).digest('hex') !== input.artifactSha256) {
|
|
1561
|
+
throw new Error('revision verification artifact changed');
|
|
1562
|
+
}
|
|
1563
|
+
return callback();
|
|
1564
|
+
});
|
|
1565
|
+
};
|
|
1566
|
+
/** Independent reviewer reads the actual checked tree through a disposable Git index. */
|
|
1567
|
+
inspectPreparedRevisionReviewContext = async (planId, signal) => {
|
|
1568
|
+
const combined = AbortSignal.any([this.abort.signal, signal, AbortSignal.timeout(120_000)]);
|
|
1569
|
+
combined.throwIfAborted();
|
|
1570
|
+
const prepared = this.inspectPreparedRevision(planId);
|
|
1571
|
+
const current = () => this.withPreparedRevisionFence({ planId, planDigest: prepared.plan.digest,
|
|
1572
|
+
artifactSha256: prepared.plan.preparedEvidence.pack.sha256,
|
|
1573
|
+
growthRunDigest: sourceGrowthRunDigest(prepared.plan.sourceRevision.growthRun),
|
|
1574
|
+
referenceDigest: controlPlaneDigest(prepared.reference), parentDigest: controlPlaneDigest(prepared.plan.sourceRevision.parent) }, () => undefined);
|
|
1575
|
+
current();
|
|
1576
|
+
const trust = await this.boundTrust();
|
|
1577
|
+
const environment = inheritedEnvironment(trust);
|
|
1578
|
+
const job = prepared.job;
|
|
1579
|
+
const parent = this.inspectRevisionParent(prepared.plan.sourceRevision.parent.planId);
|
|
1580
|
+
await verifyRevisedPluginWorkspace({ worktree: prepared.plan.worktree, baseCommit: prepared.plan.baseCommit,
|
|
1581
|
+
name: prepared.plan.name, environment, signal: combined, assertCurrent: current,
|
|
1582
|
+
revision: prepared.plan.sourceRevision, parentSource: parent.source, parentCertificate: parent.certificate, files: job.intent.files });
|
|
1583
|
+
const review = await inspectPreparedCreationPatch(prepared.plan, environment, combined);
|
|
1584
|
+
current();
|
|
1585
|
+
return review;
|
|
1586
|
+
};
|
|
1587
|
+
/** Signed behavior evidence over stored pack/checks. Adoption must await async review-context recheck. */
|
|
1588
|
+
inspectVerifiedRevision = (planId) => {
|
|
1589
|
+
const certificate = this.store.getRevisionVerification(planId);
|
|
1590
|
+
if (!certificate)
|
|
1591
|
+
return undefined;
|
|
1592
|
+
const config = this.config.revisionVerifications;
|
|
1593
|
+
if (!config || !verifyPluginRevisionVerificationCertificate(certificate, config.authority, config.publicKey))
|
|
1594
|
+
return undefined;
|
|
1595
|
+
this.withPreparedRevisionFence({ planId, planDigest: certificate.plan.digest,
|
|
1596
|
+
artifactSha256: certificate.plan.artifactSha256, growthRunDigest: certificate.source.growthRunDigest,
|
|
1597
|
+
referenceDigest: certificate.source.referenceDigest, parentDigest: controlPlaneDigest(certificate.parent) }, () => {
|
|
1598
|
+
const plan = this.store.getSourcePlan(planId);
|
|
1599
|
+
if (!plan.sourceCheck || !plan.preparedEvidence || certificate.plan.name !== plan.name
|
|
1600
|
+
|| certificate.plan.generatorDigest !== plan.generatorDigest
|
|
1601
|
+
|| certificate.plan.sourceTreeDigest !== plan.sourceCheck.treeDigest
|
|
1602
|
+
|| certificate.plan.sourcePatchDigest !== plan.sourceCheck.patchDigest
|
|
1603
|
+
|| certificate.plan.artifactBytes !== plan.preparedEvidence.pack.sizeBytes) {
|
|
1604
|
+
throw new Error('revision verification exact plan evidence changed');
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
return structuredClone(certificate);
|
|
1608
|
+
};
|
|
1609
|
+
/** Private Host diagnostic; no cases, prompts, source bytes or verifier output. */
|
|
1610
|
+
inspectRevisionVerification = (planId) => {
|
|
1611
|
+
this.abort.signal.throwIfAborted();
|
|
1612
|
+
const plan = this.store.getSourcePlan(planId);
|
|
1613
|
+
const reference = this.store.getOwnerTaskFailureReference(plan.gapId);
|
|
1614
|
+
if (!reference || plan.mode !== 'prepared-revise')
|
|
1615
|
+
throw new Error('revision verification owner source unavailable');
|
|
1616
|
+
return this.taskGaps.withCurrent(plan.gapId, reference.owner, () => {
|
|
1617
|
+
const current = this.store.getSourcePlan(planId);
|
|
1618
|
+
if (current.mode !== 'prepared-revise' || current.digest !== plan.digest) {
|
|
1619
|
+
throw new Error('revision verification diagnostic plan changed');
|
|
1620
|
+
}
|
|
1621
|
+
return this.store.inspectRevisionVerificationRecord(planId);
|
|
1622
|
+
});
|
|
1623
|
+
};
|
|
1624
|
+
/** Public naming rule only; never disclose grant identity or mutation authority. */
|
|
1625
|
+
getSourceCreationNamespace() {
|
|
1626
|
+
const config = this.config.sourceJobs;
|
|
1627
|
+
if (this.abort.signal.aborted || config?.creation === undefined
|
|
1628
|
+
|| Date.now() >= Math.min(config.expiresAt, config.creation.expiresAt))
|
|
1629
|
+
return undefined;
|
|
1630
|
+
return { namePrefix: config.creation.namePrefix };
|
|
1631
|
+
}
|
|
1632
|
+
inspectCreatedCapabilityRevisionCandidate = (planId) => {
|
|
1633
|
+
this.abort.signal.throwIfAborted();
|
|
1634
|
+
const policy = this.config.revisionVerifications;
|
|
1635
|
+
const owner = this.config.creationCapabilities?.owner;
|
|
1636
|
+
const delivery = this.ctx.get('assistantDelivery', false);
|
|
1637
|
+
if (!policy || !owner || !delivery)
|
|
1638
|
+
return undefined;
|
|
1639
|
+
try {
|
|
1640
|
+
const before = delivery.validateOwnerRoute(owner);
|
|
1641
|
+
const { plan, reference, job } = this.store.getRetainedPreparedRevision(planId);
|
|
1642
|
+
const certificate = this.store.getRevisionVerification(planId);
|
|
1643
|
+
if (!certificate || !plan.sourceRevision || !isSourceOwnerContinuation(before, reference.owner)
|
|
1644
|
+
|| !verifyPluginRevisionVerificationCertificate(certificate, policy.authority, policy.publicKey, certificate.verifiedAt)
|
|
1645
|
+
|| certificate.plan.digest !== plan.digest || certificate.source.referenceDigest !== controlPlaneDigest(reference)
|
|
1646
|
+
|| certificate.source.ownerDigest !== job.intent.ownerDigest
|
|
1647
|
+
|| certificate.source.growthRunDigest !== sourceGrowthRunDigest(plan.sourceRevision.growthRun)
|
|
1648
|
+
|| controlPlaneDigest(certificate.parent) !== controlPlaneDigest(plan.sourceRevision.parent)
|
|
1649
|
+
|| controlPlaneDigest(certificate.model) !== controlPlaneDigest(plan.sourceRevision.growthRun.model)
|
|
1650
|
+
|| controlPlaneDigest(certificate.authority) !== controlPlaneDigest(plan.sourceRevision.growthRun.revisionAcceptance))
|
|
1651
|
+
return undefined;
|
|
1652
|
+
const parent = this.inspectRevisionParent(certificate.parent.planId);
|
|
1653
|
+
if (controlPlaneDigest(parent.parent) !== controlPlaneDigest(certificate.parent))
|
|
1654
|
+
return undefined;
|
|
1655
|
+
const source = this.store.readRevisionSource(planId);
|
|
1656
|
+
if (!source || controlPlaneDigest(before) !== controlPlaneDigest(delivery.validateOwnerRoute(owner)))
|
|
1657
|
+
return undefined;
|
|
1658
|
+
return Object.freeze({ protocol: 'dsh-created-capability-revision-candidate/v1',
|
|
1659
|
+
certificate: Object.freeze(structuredClone(certificate)), source: Object.freeze({ ...source,
|
|
1660
|
+
scope: Object.freeze([...source.scope]), entries: Object.freeze(source.entries.map(entry => Object.freeze({ ...entry }))),
|
|
1661
|
+
files: Object.freeze(source.files.map(file => Object.freeze({ ...file }))) }) });
|
|
1662
|
+
}
|
|
1663
|
+
catch {
|
|
1664
|
+
return undefined;
|
|
1665
|
+
}
|
|
1666
|
+
};
|
|
613
1667
|
// Bind Host entry points: Cordis service proxies must not become resource owners.
|
|
614
1668
|
enqueueSourceJob = async (input) => {
|
|
615
1669
|
this.abort.signal.throwIfAborted();
|
|
@@ -642,7 +1696,7 @@ export class PluginControlPlaneService extends Service {
|
|
|
642
1696
|
if (this.sourceBuilds.size !== 0 || this.sourceInspections.size !== 0)
|
|
643
1697
|
throw new Error('plugin-control-plane: another source operation is draining');
|
|
644
1698
|
const intent = job.intent;
|
|
645
|
-
const operation = this.
|
|
1699
|
+
const operation = this.prepareSourcePlanOwned({ gapId: intent.gapId, name: intent.name, repository: intent.repository, files: intent.files,
|
|
646
1700
|
idempotencyKey: `source-job-plan:${job.id}`, expectedBaseCommit: intent.baseCommit, ttlMs: intent.ttlMs, timeoutMs: intent.build.timeoutMs, offline: true, signal, assertCurrent }, job);
|
|
647
1701
|
this.sourceBuilds.add(operation);
|
|
648
1702
|
try {
|
|
@@ -652,6 +1706,190 @@ export class PluginControlPlaneService extends Service {
|
|
|
652
1706
|
this.sourceBuilds.delete(operation);
|
|
653
1707
|
}
|
|
654
1708
|
}
|
|
1709
|
+
async verifyPreparedCreation(job, signal, verifier) {
|
|
1710
|
+
signal.throwIfAborted();
|
|
1711
|
+
if (!job.planId || !this.config.creationVerifications || this.store.getCreationVerificationStatus(job.planId) !== undefined)
|
|
1712
|
+
return;
|
|
1713
|
+
const prepared = this.inspectPreparedCreation(job.planId);
|
|
1714
|
+
if (prepared.job.id !== job.id || !prepared.plan.creation?.growthRun || !prepared.plan.preparedEvidence) {
|
|
1715
|
+
throw new Error('creation verification job is not the exact prepared source');
|
|
1716
|
+
}
|
|
1717
|
+
const binding = { planId: prepared.plan.id, planDigest: prepared.plan.digest,
|
|
1718
|
+
artifactSha256: prepared.plan.preparedEvidence.pack.sha256,
|
|
1719
|
+
growthRunDigest: sourceGrowthRunDigest(prepared.plan.creation.growthRun),
|
|
1720
|
+
referenceDigest: controlPlaneDigest(prepared.reference) };
|
|
1721
|
+
this.withPreparedCreationFence(binding, () => this.store.claimCreationVerification(job.planId));
|
|
1722
|
+
try {
|
|
1723
|
+
const current = verifier();
|
|
1724
|
+
if (typeof current?.verifyPluginCreation !== 'function')
|
|
1725
|
+
throw new Error('independent creation verifier unavailable');
|
|
1726
|
+
const result = await current.verifyPluginCreation({ protocol: 'assistant-growth/creation-verification-request/v1', planId: job.planId }, signal);
|
|
1727
|
+
signal.throwIfAborted();
|
|
1728
|
+
if (result.status === 'verified' && !verifyPluginCreationVerificationCertificate(result.certificate, this.config.creationVerifications.authority, this.config.creationVerifications.publicKey)) {
|
|
1729
|
+
throw new Error('creation verification certificate signature or policy invalid');
|
|
1730
|
+
}
|
|
1731
|
+
if (controlPlaneDigest(await this.boundTrust()) !== job.intent.trustDigest)
|
|
1732
|
+
throw new Error('creation verification trust changed');
|
|
1733
|
+
await this.inspectPreparedCreationReviewContext(job.planId, signal);
|
|
1734
|
+
this.withPreparedCreationFence(binding, () => {
|
|
1735
|
+
if (result.status === 'verified') {
|
|
1736
|
+
this.store.recordCreationVerification(result.certificate);
|
|
1737
|
+
}
|
|
1738
|
+
else
|
|
1739
|
+
this.store.settleCreationVerification(job.planId, result.status, typeof result.reason === 'string' && (result.status === 'unknown' ? CREATION_UNKNOWN_CODES : CREATION_REJECTED_CODES).has(result.reason)
|
|
1740
|
+
? result.reason : `independent-verifier-${result.status}`);
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
catch (error) {
|
|
1744
|
+
// The claimed row is a durable no-replay fence even if the process dies
|
|
1745
|
+
// before settlement. A later native tick cannot dispatch another review.
|
|
1746
|
+
try {
|
|
1747
|
+
this.withPreparedCreationFence(binding, () => {
|
|
1748
|
+
if (this.store.getCreationVerificationStatus(job.planId) === 'claimed') {
|
|
1749
|
+
this.store.settleCreationVerification(job.planId, 'unknown', 'independent-verification-unsettled');
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
catch { /* stale owner/source remains permanently claimed */ }
|
|
1754
|
+
throw error;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
async verifyPreparedRevision(job, signal, verifier) {
|
|
1758
|
+
signal.throwIfAborted();
|
|
1759
|
+
if (!job.planId || !this.config.revisionVerifications || this.store.getRevisionVerificationStatus(job.planId) !== undefined)
|
|
1760
|
+
return;
|
|
1761
|
+
const prepared = this.inspectPreparedRevision(job.planId);
|
|
1762
|
+
if (prepared.job.id !== job.id || !prepared.plan.sourceRevision?.growthRun || !prepared.plan.preparedEvidence) {
|
|
1763
|
+
throw new Error('revision verification job is not the exact prepared source');
|
|
1764
|
+
}
|
|
1765
|
+
const binding = { planId: prepared.plan.id, planDigest: prepared.plan.digest,
|
|
1766
|
+
artifactSha256: prepared.plan.preparedEvidence.pack.sha256,
|
|
1767
|
+
growthRunDigest: sourceGrowthRunDigest(prepared.plan.sourceRevision.growthRun),
|
|
1768
|
+
referenceDigest: controlPlaneDigest(prepared.reference), parentDigest: controlPlaneDigest(prepared.plan.sourceRevision.parent) };
|
|
1769
|
+
this.withPreparedRevisionFence(binding, () => this.store.claimRevisionVerification(job.planId));
|
|
1770
|
+
try {
|
|
1771
|
+
const current = verifier();
|
|
1772
|
+
if (typeof current?.verifyPluginRevision !== 'function')
|
|
1773
|
+
throw new Error('independent revision verifier unavailable');
|
|
1774
|
+
const result = await current.verifyPluginRevision({ protocol: 'assistant-growth/revision-verification-request/v1', planId: job.planId }, signal);
|
|
1775
|
+
signal.throwIfAborted();
|
|
1776
|
+
if (result.status === 'verified' && !verifyPluginRevisionVerificationCertificate(result.certificate, this.config.revisionVerifications.authority, this.config.revisionVerifications.publicKey)) {
|
|
1777
|
+
throw new Error('revision verification certificate signature or policy invalid');
|
|
1778
|
+
}
|
|
1779
|
+
if (controlPlaneDigest(await this.boundTrust()) !== job.intent.trustDigest)
|
|
1780
|
+
throw new Error('revision verification trust changed');
|
|
1781
|
+
await this.inspectPreparedRevisionReviewContext(job.planId, signal);
|
|
1782
|
+
const source = result.status === 'verified' ? await captureCreationCapabilitySource({ worktree: prepared.plan.worktree,
|
|
1783
|
+
baseCommit: prepared.plan.baseCommit, scope: [...prepared.plan.scope].sort(), certificate: result.certificate,
|
|
1784
|
+
environment: inheritedEnvironment(await this.boundTrust()), signal }) : undefined;
|
|
1785
|
+
this.withPreparedRevisionFence(binding, () => {
|
|
1786
|
+
if (result.status === 'verified') {
|
|
1787
|
+
if (!source)
|
|
1788
|
+
throw new Error('verified revision source archive unavailable');
|
|
1789
|
+
this.store.recordRevisionVerification(result.certificate, source);
|
|
1790
|
+
}
|
|
1791
|
+
else
|
|
1792
|
+
this.store.settleRevisionVerification(job.planId, result.status, typeof result.reason === 'string' && (result.status === 'unknown' ? CREATION_UNKNOWN_CODES : CREATION_REJECTED_CODES).has(result.reason)
|
|
1793
|
+
? result.reason : `independent-verifier-${result.status}`);
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
catch (error) {
|
|
1797
|
+
// The claimed row is a durable no-replay fence even if the process dies
|
|
1798
|
+
// before settlement. A later native tick cannot dispatch another review.
|
|
1799
|
+
try {
|
|
1800
|
+
this.withPreparedRevisionFence(binding, () => {
|
|
1801
|
+
if (this.store.getRevisionVerificationStatus(job.planId) === 'claimed') {
|
|
1802
|
+
this.store.settleRevisionVerification(job.planId, 'unknown', 'independent-verification-unsettled');
|
|
1803
|
+
}
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
catch { /* stale owner/source remains permanently claimed */ }
|
|
1807
|
+
throw error;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
async verifyPreparedRevisionRegression(job, signal, verifier) {
|
|
1811
|
+
signal.throwIfAborted();
|
|
1812
|
+
if (!job.planId || !this.config.revisionRegressions || this.store.getRevisionRegressionStatus(job.planId) !== undefined)
|
|
1813
|
+
return;
|
|
1814
|
+
const prepared = this.inspectPreparedRevisionRegression(job.planId);
|
|
1815
|
+
if (prepared.job.id !== job.id)
|
|
1816
|
+
throw new Error('regression job is not exact prepared revision');
|
|
1817
|
+
const certificate = prepared.candidate.certificate;
|
|
1818
|
+
const binding = { planId: prepared.plan.id, planDigest: prepared.plan.digest, artifactSha256: certificate.plan.artifactSha256,
|
|
1819
|
+
growthRunDigest: certificate.source.growthRunDigest, referenceDigest: certificate.source.referenceDigest,
|
|
1820
|
+
parentDigest: controlPlaneDigest(certificate.parent), candidateVerificationDigest: controlPlaneDigest(certificate),
|
|
1821
|
+
sourceDigest: prepared.candidate.sourceDigest,
|
|
1822
|
+
regressionAuthorityDigest: controlPlaneDigest(prepared.plan.sourceRevision.growthRun.revisionRegressionAcceptance) };
|
|
1823
|
+
this.withPreparedRevisionRegressionFence(binding, () => this.store.claimRevisionRegression(job.planId));
|
|
1824
|
+
try {
|
|
1825
|
+
const current = verifier();
|
|
1826
|
+
if (typeof current?.verifyPluginRevisionRegression !== 'function')
|
|
1827
|
+
throw new Error('independent regression verifier unavailable');
|
|
1828
|
+
const result = await current.verifyPluginRevisionRegression({ protocol: 'assistant-growth/revision-regression-request/v1', planId: job.planId }, signal);
|
|
1829
|
+
signal.throwIfAborted();
|
|
1830
|
+
if (result.status === 'verified' && !verifyPluginRevisionRegressionCertificate(result.certificate, this.config.revisionRegressions.authority, this.config.revisionRegressions.publicKey)) {
|
|
1831
|
+
throw new Error('regression signature or independent policy invalid');
|
|
1832
|
+
}
|
|
1833
|
+
if (controlPlaneDigest(await this.boundTrust()) !== job.intent.trustDigest)
|
|
1834
|
+
throw new Error('regression trust changed');
|
|
1835
|
+
await this.inspectPreparedRevisionReviewContext(job.planId, signal);
|
|
1836
|
+
this.withPreparedRevisionRegressionFence(binding, () => {
|
|
1837
|
+
if (result.status === 'verified') {
|
|
1838
|
+
if (!verifyPluginRevisionRegressionCertificate(result.certificate, this.config.revisionRegressions.authority, this.config.revisionRegressions.publicKey))
|
|
1839
|
+
throw new Error('regression certificate expired before settlement');
|
|
1840
|
+
this.store.recordRevisionRegression(result.certificate);
|
|
1841
|
+
}
|
|
1842
|
+
else
|
|
1843
|
+
this.store.settleRevisionRegression(job.planId, result.status, typeof result.reason === 'string' && /^[a-z][a-z0-9-]{0,79}$/u.test(result.reason)
|
|
1844
|
+
? result.reason : `independent-regression-${result.status}`);
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
catch (error) {
|
|
1848
|
+
try {
|
|
1849
|
+
this.withPreparedRevisionRegressionFence(binding, () => {
|
|
1850
|
+
if (this.store.getRevisionRegressionStatus(job.planId) === 'claimed') {
|
|
1851
|
+
this.store.settleRevisionRegression(job.planId, 'unknown', 'independent-regression-unsettled');
|
|
1852
|
+
}
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
catch { /* changed source retains its durable no-replay claim */ }
|
|
1856
|
+
throw error;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
async inspectRevisionSource(input) {
|
|
1860
|
+
this.abort.signal.throwIfAborted();
|
|
1861
|
+
const config = this.config.sourceJobs;
|
|
1862
|
+
if (!config?.revision || !this.inspectSourceRevisionAcceptanceAuthority() || input.repository !== config.repository
|
|
1863
|
+
|| this.sourceBuilds.size !== 0 || this.sourceInspections.size !== 0)
|
|
1864
|
+
throw new Error('revision source inspection unavailable');
|
|
1865
|
+
const signal = AbortSignal.any([this.abort.signal, ...(input.signal ? [input.signal] : []), AbortSignal.timeout(15_000)]);
|
|
1866
|
+
const parent = this.inspectRevisionParent(input.parentPlanId);
|
|
1867
|
+
if (parent.name !== input.name)
|
|
1868
|
+
throw new Error('revision source name differs from adopted parent');
|
|
1869
|
+
const assertCurrent = async () => {
|
|
1870
|
+
signal.throwIfAborted();
|
|
1871
|
+
await input.assertCurrent?.();
|
|
1872
|
+
signal.throwIfAborted();
|
|
1873
|
+
if (!this.inspectSourceRevisionAcceptanceAuthority() || controlPlaneDigest(this.inspectRevisionParent(input.parentPlanId).parent) !== controlPlaneDigest(parent.parent))
|
|
1874
|
+
throw new Error('revision source authority or parent changed');
|
|
1875
|
+
};
|
|
1876
|
+
const operation = (async () => {
|
|
1877
|
+
const trust = await this.boundTrust(), environment = inheritedEnvironment(trust);
|
|
1878
|
+
const baselineCommit = config.baseline === undefined ? undefined : await resolveSourceBaseline({ repository: config.repository,
|
|
1879
|
+
config: config.baseline, environment, signal, assertCurrent, trust,
|
|
1880
|
+
readHistory: () => this.store.getSourceBaselineHistory(config.repository), readMaintenance: () => this.store.getSourceMaintenanceRecords(config.repository) });
|
|
1881
|
+
return inspectSourceRevisionContext({ repository: config.repository, name: input.name, paths: input.paths,
|
|
1882
|
+
...(input.baseCommit === undefined ? {} : { baseCommit: input.baseCommit }), ...(baselineCommit === undefined ? {} : { baselineCommit }),
|
|
1883
|
+
environment, signal, assertCurrent, grant: config.revision, parent: parent.parent, parentSource: parent.source, parentCertificate: parent.certificate });
|
|
1884
|
+
})();
|
|
1885
|
+
this.sourceInspections.add(operation);
|
|
1886
|
+
try {
|
|
1887
|
+
return await operation;
|
|
1888
|
+
}
|
|
1889
|
+
finally {
|
|
1890
|
+
this.sourceInspections.delete(operation);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
655
1893
|
async inspectSource(input) {
|
|
656
1894
|
this.abort.signal.throwIfAborted();
|
|
657
1895
|
if (this.sourceBuilds.size !== 0 || this.sourceInspections.size !== 0)
|
|
@@ -662,12 +1900,83 @@ export class PluginControlPlaneService extends Service {
|
|
|
662
1900
|
void operation.then(() => this.sourceInspections.delete(operation), () => this.sourceInspections.delete(operation));
|
|
663
1901
|
return operation;
|
|
664
1902
|
}
|
|
1903
|
+
async inspectSourceTargets(input) {
|
|
1904
|
+
this.abort.signal.throwIfAborted();
|
|
1905
|
+
if (this.sourceBuilds.size !== 0 || this.sourceInspections.size !== 0)
|
|
1906
|
+
throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'another source operation is still draining');
|
|
1907
|
+
const signal = AbortSignal.any([this.abort.signal, ...(input.signal === undefined ? [] : [input.signal]), AbortSignal.timeout(15_000)]);
|
|
1908
|
+
const operation = this.inspectSourceTargetsOwned({ ...input, signal });
|
|
1909
|
+
this.sourceInspections.add(operation);
|
|
1910
|
+
void operation.then(() => this.sourceInspections.delete(operation), () => this.sourceInspections.delete(operation));
|
|
1911
|
+
return operation;
|
|
1912
|
+
}
|
|
1913
|
+
async inspectSourceTargetsOwned(input) {
|
|
1914
|
+
const signal = input.signal === undefined ? this.abort.signal : AbortSignal.any([this.abort.signal, input.signal]);
|
|
1915
|
+
const assertCurrent = async () => { signal.throwIfAborted(); await awaitSourceSignal(signal, () => input.assertCurrent?.()); signal.throwIfAborted(); };
|
|
1916
|
+
await assertCurrent();
|
|
1917
|
+
const trust = await awaitSourceSignal(signal, () => this.boundTrust());
|
|
1918
|
+
const environment = inheritedEnvironment(trust);
|
|
1919
|
+
const baseline = this.config.sourceJobs?.repository === input.repository ? this.config.sourceJobs.baseline : undefined;
|
|
1920
|
+
const baselineCommit = baseline === undefined ? undefined : await resolveSourceBaseline({ repository: input.repository,
|
|
1921
|
+
config: baseline, environment, signal, assertCurrent, trust,
|
|
1922
|
+
readHistory: () => this.store.getSourceBaselineHistory(input.repository),
|
|
1923
|
+
readMaintenance: () => this.store.getSourceMaintenanceRecords(input.repository) });
|
|
1924
|
+
return inspectSourceTargetsContext({ repository: input.repository,
|
|
1925
|
+
...(input.baseCommit === undefined ? {} : { baseCommit: input.baseCommit }),
|
|
1926
|
+
...(baselineCommit === undefined ? {} : { baselineCommit }), environment, signal, assertCurrent });
|
|
1927
|
+
}
|
|
665
1928
|
async inspectSourceOwned(input) {
|
|
666
1929
|
const signal = input.signal === undefined ? this.abort.signal : AbortSignal.any([this.abort.signal, input.signal]);
|
|
667
1930
|
const assertCurrent = async () => { signal.throwIfAborted(); await awaitSourceSignal(signal, () => input.assertCurrent?.()); signal.throwIfAborted(); };
|
|
668
1931
|
await assertCurrent();
|
|
669
1932
|
const trust = await awaitSourceSignal(signal, () => this.boundTrust());
|
|
670
|
-
|
|
1933
|
+
const environment = inheritedEnvironment(trust);
|
|
1934
|
+
const baseline = this.config.sourceJobs?.repository === input.repository ? this.config.sourceJobs.baseline : undefined;
|
|
1935
|
+
const baselineCommit = baseline === undefined ? undefined : await resolveSourceBaseline({ repository: input.repository,
|
|
1936
|
+
config: baseline, environment, signal, assertCurrent, trust,
|
|
1937
|
+
readHistory: () => this.store.getSourceBaselineHistory(input.repository),
|
|
1938
|
+
readMaintenance: () => this.store.getSourceMaintenanceRecords(input.repository) });
|
|
1939
|
+
return inspectSourceContext({ repository: input.repository, name: input.name, paths: input.paths,
|
|
1940
|
+
...(input.baseCommit === undefined ? {} : { baseCommit: input.baseCommit }),
|
|
1941
|
+
...(baselineCommit === undefined ? {} : { baselineCommit }), environment, signal, assertCurrent });
|
|
1942
|
+
}
|
|
1943
|
+
async inspectCreateSource(input) {
|
|
1944
|
+
this.abort.signal.throwIfAborted();
|
|
1945
|
+
if (this.sourceBuilds.size !== 0 || this.sourceInspections.size !== 0)
|
|
1946
|
+
throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'another source operation is still draining');
|
|
1947
|
+
const signal = AbortSignal.any([this.abort.signal, ...(input.signal === undefined ? [] : [input.signal]), AbortSignal.timeout(15_000)]);
|
|
1948
|
+
const operation = this.inspectCreateSourceOwned({ ...input, signal });
|
|
1949
|
+
this.sourceInspections.add(operation);
|
|
1950
|
+
try {
|
|
1951
|
+
return await operation;
|
|
1952
|
+
}
|
|
1953
|
+
finally {
|
|
1954
|
+
this.sourceInspections.delete(operation);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
async inspectCreateSourceOwned(input) {
|
|
1958
|
+
const config = this.config.sourceJobs;
|
|
1959
|
+
if (!config?.creation || config.repository !== input.repository)
|
|
1960
|
+
throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'plugin creation authority is not configured for this repository');
|
|
1961
|
+
const signal = input.signal === undefined ? this.abort.signal : AbortSignal.any([this.abort.signal, input.signal]);
|
|
1962
|
+
const assertCurrent = async () => {
|
|
1963
|
+
signal.throwIfAborted();
|
|
1964
|
+
await awaitSourceSignal(signal, () => input.assertCurrent?.());
|
|
1965
|
+
signal.throwIfAborted();
|
|
1966
|
+
if (Date.now() >= config.creation.expiresAt || Date.now() >= config.expiresAt)
|
|
1967
|
+
throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'plugin creation authority expired');
|
|
1968
|
+
};
|
|
1969
|
+
await assertCurrent();
|
|
1970
|
+
const trust = await awaitSourceSignal(signal, () => this.boundTrust());
|
|
1971
|
+
const environment = inheritedEnvironment(trust);
|
|
1972
|
+
const baselineCommit = config.baseline === undefined ? undefined : await resolveSourceBaseline({ repository: input.repository,
|
|
1973
|
+
config: config.baseline, environment, signal, assertCurrent, trust,
|
|
1974
|
+
readHistory: () => this.store.getSourceBaselineHistory(input.repository),
|
|
1975
|
+
readMaintenance: () => this.store.getSourceMaintenanceRecords(input.repository) });
|
|
1976
|
+
const inspected = await inspectSourceCreationContext({ repository: input.repository, name: input.name, paths: input.paths,
|
|
1977
|
+
...(input.baseCommit === undefined ? {} : { baseCommit: input.baseCommit }),
|
|
1978
|
+
...(baselineCommit === undefined ? {} : { baselineCommit }), environment, signal, assertCurrent, grant: config.creation });
|
|
1979
|
+
return { name: inspected.name, baseCommit: inspected.baseCommit, files: inspected.files, contents: inspected.contents };
|
|
671
1980
|
}
|
|
672
1981
|
async plan(candidateId, profile, idempotencyKey, gapId) {
|
|
673
1982
|
const gap = this.store.getGap(gapId);
|
|
@@ -702,19 +2011,32 @@ export class PluginControlPlaneService extends Service {
|
|
|
702
2011
|
this.abort.signal.throwIfAborted();
|
|
703
2012
|
if (this.sourceBuilds.size !== 0 || this.sourceInspections.size !== 0)
|
|
704
2013
|
throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'another source operation is still draining');
|
|
705
|
-
const operation = this.
|
|
2014
|
+
const operation = this.prepareSourcePlanOwned(input);
|
|
706
2015
|
this.sourceBuilds.add(operation);
|
|
707
2016
|
void operation.then(() => this.sourceBuilds.delete(operation), () => this.sourceBuilds.delete(operation));
|
|
708
2017
|
return operation;
|
|
709
2018
|
}
|
|
710
|
-
async
|
|
2019
|
+
async prepareSourcePlanOwned(input, sourceJob) {
|
|
711
2020
|
const signal = input.signal === undefined ? this.abort.signal : AbortSignal.any([this.abort.signal, input.signal]);
|
|
712
2021
|
const gapOwner = sourceJob?.intent.owner ?? input.owner;
|
|
2022
|
+
const creating = sourceJob?.intent.mode === 'create';
|
|
2023
|
+
const creation = creating ? sourceJob.intent.creation : undefined;
|
|
2024
|
+
const revising = sourceJob?.intent.mode === 'revise-created';
|
|
2025
|
+
const revision = revising ? sourceJob.intent.revision : undefined;
|
|
2026
|
+
const parent = revision === undefined ? undefined : this.inspectRevisionParent(revision.parent.planId);
|
|
713
2027
|
const assertCurrent = async () => {
|
|
714
2028
|
signal.throwIfAborted();
|
|
715
2029
|
await input.assertCurrent?.();
|
|
716
2030
|
signal.throwIfAborted();
|
|
717
2031
|
this.taskGaps.withCurrent(input.gapId, gapOwner, () => { });
|
|
2032
|
+
if (revising && (!revision || !this.config.sourceJobs?.revision || Date.now() >= revision.grant.expiresAt
|
|
2033
|
+
|| controlPlaneDigest(revision.grant) !== controlPlaneDigest(this.config.sourceJobs.revision)
|
|
2034
|
+
|| controlPlaneDigest(this.inspectRevisionParent(revision.parent.planId).parent) !== controlPlaneDigest(revision.parent)))
|
|
2035
|
+
throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'plugin revision authority or parent changed');
|
|
2036
|
+
if (creating && (!creation || !this.config.sourceJobs?.creation || Date.now() >= creation.grant.expiresAt
|
|
2037
|
+
|| controlPlaneDigest(creation.grant) !== controlPlaneDigest(this.config.sourceJobs.creation))) {
|
|
2038
|
+
throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'plugin creation authority changed or expired');
|
|
2039
|
+
}
|
|
718
2040
|
};
|
|
719
2041
|
await assertCurrent();
|
|
720
2042
|
const trust = await this.boundTrust();
|
|
@@ -722,15 +2044,19 @@ export class PluginControlPlaneService extends Service {
|
|
|
722
2044
|
if (!/^(?=.{1,64}$)[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(name))
|
|
723
2045
|
throw new ControlPlaneCliError('INVALID_ARGUMENT', 'plugin name is invalid');
|
|
724
2046
|
assertPluginModificationAllowed(name);
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
2047
|
+
if (creating || revising)
|
|
2048
|
+
validateSourceCreationFiles(input.files);
|
|
2049
|
+
else {
|
|
2050
|
+
validateScopedPluginFiles(input.files);
|
|
2051
|
+
if (this.config.sourceBuild?.versioning === 'patch')
|
|
2052
|
+
assertManagedVersionPaths(input.files);
|
|
2053
|
+
}
|
|
728
2054
|
// Re-validate the gap reservation immediately before doing the work: the
|
|
729
2055
|
// store re-checks under BEGIN IMMEDIATE, but failing early avoids building
|
|
730
2056
|
// a patch against a gap that is already matched or closed.
|
|
731
2057
|
const gap = this.store.getGap(input.gapId);
|
|
732
2058
|
if (gap.status !== 'open' || gap.candidateId !== undefined) {
|
|
733
|
-
throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'only an unreserved
|
|
2059
|
+
throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'only an unreserved current control-plane open gap can receive a source proposal');
|
|
734
2060
|
}
|
|
735
2061
|
const repository = await realpath(resolve(input.repository));
|
|
736
2062
|
if (resolve(repository) !== repository)
|
|
@@ -739,14 +2065,20 @@ export class PluginControlPlaneService extends Service {
|
|
|
739
2065
|
if (!Number.isSafeInteger(ttlMs) || ttlMs < 900_000 || ttlMs > 86_400_000) {
|
|
740
2066
|
throw new ControlPlaneCliError('INVALID_ARGUMENT', 'ttlMs must be an integer within 900000..86400000');
|
|
741
2067
|
}
|
|
742
|
-
const maximumTimeoutMs = this.config.sourceBuild
|
|
2068
|
+
const maximumTimeoutMs = this.config.sourceBuild !== undefined && !('backend' in this.config.sourceBuild)
|
|
2069
|
+
&& ['repository', 'portable-repository'].includes(this.config.sourceBuild.profile ?? '') ? 1_800_000 : 240_000;
|
|
743
2070
|
const timeoutMs = input.timeoutMs ?? 180_000;
|
|
744
2071
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 60_000 || timeoutMs > maximumTimeoutMs) {
|
|
745
2072
|
throw new ControlPlaneCliError('INVALID_ARGUMENT', `timeoutMs must be an integer within 60000..${maximumTimeoutMs}`);
|
|
746
2073
|
}
|
|
747
2074
|
const offline = input.offline ?? true;
|
|
748
2075
|
const environment = inheritedEnvironment(trust);
|
|
749
|
-
const
|
|
2076
|
+
const baseline = this.config.sourceJobs?.repository === repository ? this.config.sourceJobs.baseline : undefined;
|
|
2077
|
+
const baseCommit = baseline === undefined
|
|
2078
|
+
? (await runLocalCommand('git', ['rev-parse', 'HEAD'], repository, environment, { capture: true })).trim()
|
|
2079
|
+
: await resolveSourceBaseline({ repository, config: baseline, environment, signal, assertCurrent, trust,
|
|
2080
|
+
readHistory: () => this.store.getSourceBaselineHistory(repository),
|
|
2081
|
+
readMaintenance: () => this.store.getSourceMaintenanceRecords(repository) });
|
|
750
2082
|
await assertCurrent();
|
|
751
2083
|
if (!/^[a-f0-9]{40}$/u.test(baseCommit))
|
|
752
2084
|
throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'repository HEAD is not a 40-hex commit id');
|
|
@@ -756,7 +2088,12 @@ export class PluginControlPlaneService extends Service {
|
|
|
756
2088
|
const isolated = await createIsolatedWorktree({ stateRoot, repository, baseCommit, environment,
|
|
757
2089
|
...(sourceJob === undefined ? {} : { worktreeName: basename(sourceJob.intent.worktree) }) });
|
|
758
2090
|
try {
|
|
759
|
-
await
|
|
2091
|
+
const generated = revision !== undefined ? await prepareRevisedPluginWorkspace({ worktree: isolated.worktree,
|
|
2092
|
+
baseCommit, name, files: input.files, environment, signal, assertCurrent, revision, parentSource: parent.source, parentCertificate: parent.certificate })
|
|
2093
|
+
: creation === undefined ? undefined : await prepareCreatedPluginWorkspace({ worktree: isolated.worktree,
|
|
2094
|
+
baseCommit, name, files: input.files, environment, signal, assertCurrent, creation });
|
|
2095
|
+
if (generated === undefined)
|
|
2096
|
+
await writeScopedPluginFiles({ worktree: isolated.worktree, name, files: input.files });
|
|
760
2097
|
await assertCurrent();
|
|
761
2098
|
if (this.config.sourceBuild === undefined)
|
|
762
2099
|
throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'isolated source build runner is not configured');
|
|
@@ -764,16 +2101,43 @@ export class PluginControlPlaneService extends Service {
|
|
|
764
2101
|
throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'isolated source builds must remain offline');
|
|
765
2102
|
const configured = this.config.sourceBuild;
|
|
766
2103
|
const versionInput = { worktree: isolated.worktree, baseCommit, name, environment, signal, assertCurrent };
|
|
767
|
-
const managed = configured.versioning === 'patch' ? await managedPatchVersionFiles(versionInput) : undefined;
|
|
2104
|
+
const managed = !creating && !revising && configured.versioning === 'patch' ? await managedPatchVersionFiles(versionInput) : undefined;
|
|
768
2105
|
if (managed !== undefined) {
|
|
769
2106
|
await assertCurrent();
|
|
770
2107
|
await writeScopedPluginFiles({ worktree: isolated.worktree, name, files: managed.files });
|
|
771
2108
|
await verifyManagedPatchVersion(versionInput);
|
|
772
2109
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
2110
|
+
// Only owner-bound creation jobs retain the exact package for the
|
|
2111
|
+
// independent verifier. Ordinary source tools never receive these bytes.
|
|
2112
|
+
const capturePack = (creating || revising) && this.store.getOwnerTaskFailureReference(input.gapId) !== undefined;
|
|
2113
|
+
const common = { worktree: isolated.worktree, baseCommit, name, scope: generated?.scope ?? [`plugins/${name}`],
|
|
2114
|
+
environment, signal, assertCurrent, preparedAt: Date.now(), ...(creation === undefined ? {} : { creation }),
|
|
2115
|
+
...(revision === undefined ? {} : { revision: { binding: revision, parentSource: parent.source,
|
|
2116
|
+
parentCertificate: parent.certificate, files: input.files } }), ...(capturePack ? { capturePack: true } : {}) };
|
|
2117
|
+
const checked = 'backend' in configured
|
|
2118
|
+
? await (async () => {
|
|
2119
|
+
if ((!creating && !revising) || sourceJob === undefined || this.nativeSourceRunner === undefined
|
|
2120
|
+
|| sourceJob.intent.nativeResource?.key !== sourceJob.id) {
|
|
2121
|
+
throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'native source preparation requires a durable Host-scaffolded creation job');
|
|
2122
|
+
}
|
|
2123
|
+
return runNativePreparedChecks({ ...common, config: { ...configured, timeoutMs: Math.min(timeoutMs, configured.timeoutMs) },
|
|
2124
|
+
runner: this.nativeSourceRunner, sourceJob: { id: sourceJob.id } });
|
|
2125
|
+
})()
|
|
2126
|
+
: await runDockerPreparedChecks({ ...common, config: { ...configured, timeoutMs: Math.min(timeoutMs, configured.timeoutMs) },
|
|
2127
|
+
...(sourceJob === undefined ? {} : { sourceJob: { id: sourceJob.id, containerName: sourceJob.intent.containerName } }) });
|
|
776
2128
|
await assertCurrent();
|
|
2129
|
+
if (capturePack && checked.packArtifact === undefined)
|
|
2130
|
+
throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'prepared creation artifact was not captured');
|
|
2131
|
+
if (revision !== undefined) {
|
|
2132
|
+
await verifyRevisedPluginWorkspace({ worktree: isolated.worktree, baseCommit, name, files: input.files, environment, signal, assertCurrent, revision, parentSource: parent.source, parentCertificate: parent.certificate });
|
|
2133
|
+
}
|
|
2134
|
+
if (creation !== undefined) {
|
|
2135
|
+
await verifyCreatedPluginWorkspace({ worktree: isolated.worktree, baseCommit, name,
|
|
2136
|
+
files: input.files, environment, signal, assertCurrent, creation });
|
|
2137
|
+
const manifest = JSON.parse(await readFile(join(isolated.worktree, 'plugins', name, 'package.json'), 'utf8'));
|
|
2138
|
+
if (checked.evidence.pack.version !== manifest.version)
|
|
2139
|
+
throw new ControlPlaneCliError('SOURCE_BOUNDARY', 'new plugin artifact does not carry the generated version');
|
|
2140
|
+
}
|
|
777
2141
|
if (managed !== undefined) {
|
|
778
2142
|
await verifyManagedPatchVersion(versionInput);
|
|
779
2143
|
if (checked.evidence.pack.version !== managed.version)
|
|
@@ -782,7 +2146,10 @@ export class PluginControlPlaneService extends Service {
|
|
|
782
2146
|
// The worktree survives success: the owner recomputes its digests on this
|
|
783
2147
|
// exact directory during `source verify-prepared`.
|
|
784
2148
|
return this.taskGaps.withCurrent(input.gapId, gapOwner, () => this.store.createSourcePlan({ gapId: input.gapId, repository, worktree: isolated.worktree, baseCommit,
|
|
785
|
-
name, generatorDigest:
|
|
2149
|
+
name, generatorDigest: generated?.generatorDigest ?? MODIFY_GENERATOR_DIGEST,
|
|
2150
|
+
scope: generated?.scope ?? [`plugins/${name}`], mode: creating ? 'prepared-create' : revising ? 'prepared-revise' : 'modify', ttlMs,
|
|
2151
|
+
...(creation === undefined ? {} : { creation }), ...(revision === undefined ? {} : { revision }),
|
|
2152
|
+
...(capturePack && checked.packArtifact !== undefined ? { preparedArtifact: checked.packArtifact } : {}),
|
|
786
2153
|
idempotencyKey: input.idempotencyKey,
|
|
787
2154
|
...(sourceJob === undefined ? {} : { sourceJob: { jobId: sourceJob.id, jobRevision: sourceJob.revision, occurrenceId: sourceJob.occurrenceId } }),
|
|
788
2155
|
prepared: { treeDigest: checked.treeDigest, patchDigest: checked.patchDigest, checkedAt: checked.checkedAt, evidence: checked.evidence } }).result);
|