@dsh-enhanced/plugin-control-plane 0.1.65 → 0.1.66
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 +81 -5
- package/lib/attestation.d.ts.map +1 -1
- package/lib/attestation.js +33 -1
- package/lib/attestation.js.map +1 -1
- package/lib/creation-capability-empty-handoff.d.ts +57 -0
- package/lib/creation-capability-empty-handoff.d.ts.map +1 -0
- package/lib/creation-capability-empty-handoff.js +147 -0
- package/lib/creation-capability-empty-handoff.js.map +1 -0
- package/lib/creation-capability-execution.d.ts +9 -0
- package/lib/creation-capability-execution.d.ts.map +1 -0
- package/lib/creation-capability-execution.js +70 -0
- package/lib/creation-capability-execution.js.map +1 -0
- package/lib/creation-capability-feedback.d.ts +58 -1
- package/lib/creation-capability-feedback.d.ts.map +1 -1
- package/lib/creation-capability-feedback.js +532 -50
- package/lib/creation-capability-feedback.js.map +1 -1
- package/lib/creation-capability-handoff-common.d.ts +60 -0
- package/lib/creation-capability-handoff-common.d.ts.map +1 -0
- package/lib/creation-capability-handoff-common.js +195 -0
- package/lib/creation-capability-handoff-common.js.map +1 -0
- package/lib/creation-capability-journal.d.ts +19 -0
- package/lib/creation-capability-journal.d.ts.map +1 -1
- package/lib/creation-capability-journal.js +62 -21
- package/lib/creation-capability-journal.js.map +1 -1
- package/lib/creation-capability-runtime.d.ts +18 -0
- package/lib/creation-capability-runtime.d.ts.map +1 -1
- package/lib/creation-capability-runtime.js +76 -18
- package/lib/creation-capability-runtime.js.map +1 -1
- package/lib/creation-capability-sealed-audit.d.ts +19 -0
- package/lib/creation-capability-sealed-audit.d.ts.map +1 -0
- package/lib/creation-capability-sealed-audit.js +114 -0
- package/lib/creation-capability-sealed-audit.js.map +1 -0
- package/lib/creation-capability-sealed-handoff.d.ts +160 -0
- package/lib/creation-capability-sealed-handoff.d.ts.map +1 -0
- package/lib/creation-capability-sealed-handoff.js +695 -0
- package/lib/creation-capability-sealed-handoff.js.map +1 -0
- package/lib/creation-capability-trend-snapshot.d.ts +70 -0
- package/lib/creation-capability-trend-snapshot.d.ts.map +1 -0
- package/lib/creation-capability-trend-snapshot.js +117 -0
- package/lib/creation-capability-trend-snapshot.js.map +1 -0
- package/lib/creation-capability-types.d.ts +254 -4
- package/lib/creation-capability-types.d.ts.map +1 -1
- package/lib/in-process-reload.d.ts +191 -0
- package/lib/in-process-reload.d.ts.map +1 -0
- package/lib/in-process-reload.js +284 -0
- package/lib/in-process-reload.js.map +1 -0
- package/lib/index.d.ts +12 -4
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +8 -2
- package/lib/index.js.map +1 -1
- package/lib/linux-source-build.d.ts +16 -0
- package/lib/linux-source-build.d.ts.map +1 -0
- package/lib/linux-source-build.js +268 -0
- package/lib/linux-source-build.js.map +1 -0
- package/lib/linux-source-preflight.d.ts +31 -0
- package/lib/linux-source-preflight.d.ts.map +1 -0
- package/lib/linux-source-preflight.js +101 -0
- package/lib/linux-source-preflight.js.map +1 -0
- package/lib/live-qualification-runtime.d.ts +6 -2
- package/lib/live-qualification-runtime.d.ts.map +1 -1
- package/lib/live-qualification-runtime.js +65 -11
- package/lib/live-qualification-runtime.js.map +1 -1
- package/lib/owner-task-gap-types.d.ts +23 -0
- package/lib/owner-task-gap-types.d.ts.map +1 -1
- package/lib/owner-task-gaps.d.ts +4 -0
- package/lib/owner-task-gaps.d.ts.map +1 -1
- package/lib/owner-task-gaps.js +35 -1
- package/lib/owner-task-gaps.js.map +1 -1
- package/lib/scanner-owner-runtime.d.ts +34 -0
- package/lib/scanner-owner-runtime.d.ts.map +1 -0
- package/lib/scanner-owner-runtime.js +99 -0
- package/lib/scanner-owner-runtime.js.map +1 -0
- package/lib/service.d.ts +82 -5
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +450 -45
- package/lib/service.js.map +1 -1
- package/lib/source-approval-authority.d.ts.map +1 -1
- package/lib/source-approval-authority.js +25 -7
- package/lib/source-approval-authority.js.map +1 -1
- package/lib/source-build-diagnostics.d.ts +31 -0
- package/lib/source-build-diagnostics.d.ts.map +1 -0
- package/lib/source-build-diagnostics.js +110 -0
- package/lib/source-build-diagnostics.js.map +1 -0
- package/lib/source-build.d.ts +25 -2
- package/lib/source-build.d.ts.map +1 -1
- package/lib/source-build.js +145 -26
- package/lib/source-build.js.map +1 -1
- package/lib/source-creation.d.ts +6 -2
- package/lib/source-creation.d.ts.map +1 -1
- package/lib/source-creation.js +11 -2
- package/lib/source-creation.js.map +1 -1
- package/lib/source-job-types.d.ts +3 -1
- package/lib/source-job-types.d.ts.map +1 -1
- package/lib/source-jobs.d.ts +28 -1
- package/lib/source-jobs.d.ts.map +1 -1
- package/lib/source-jobs.js +117 -26
- package/lib/source-jobs.js.map +1 -1
- package/lib/sqlite.d.ts +1 -1
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +36 -1
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +21 -1
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +470 -174
- package/lib/store.js.map +1 -1
- package/lib/systemd-host-authority.d.ts.map +1 -1
- package/lib/systemd-host-authority.js +4 -0
- package/lib/systemd-host-authority.js.map +1 -1
- package/lib/task-observation-runtime.d.ts +6 -2
- package/lib/task-observation-runtime.d.ts.map +1 -1
- package/lib/task-observation-runtime.js +69 -11
- package/lib/task-observation-runtime.js.map +1 -1
- package/lib/types.d.ts +47 -1
- package/lib/types.d.ts.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/work-item-creation-contract.d.ts +17 -0
- package/lib/work-item-creation-contract.d.ts.map +1 -0
- package/lib/work-item-creation-contract.js +61 -0
- package/lib/work-item-creation-contract.js.map +1 -0
- package/package.json +10 -10
package/lib/store.js
CHANGED
|
@@ -2,6 +2,7 @@ import { validateLiveQualificationTerms, assertLiveQualificationBatch, parseLive
|
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { createPublicKey, verify } from 'node:crypto';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { types as utilTypes } from 'node:util';
|
|
5
6
|
import { realpathSync } from 'node:fs';
|
|
6
7
|
import { basename, isAbsolute, posix, resolve, win32 } from 'node:path';
|
|
7
8
|
import { discover, parseCatalog } from './catalog.js';
|
|
@@ -13,11 +14,12 @@ import { validateSourceBaselineConfig, verifySourceBaselineHistory } from './sou
|
|
|
13
14
|
import { validateScopedPluginFiles } from './source-workspace.js';
|
|
14
15
|
import { validateCreationCapabilitySource } from './creation-capability-source.js';
|
|
15
16
|
import { validateSourceRevisionBinding } from './source-revision.js';
|
|
16
|
-
import { validateSourceCreationFiles, validateSourceCreationGrant } from './source-creation.js';
|
|
17
|
-
import { sourceGrowthRunDigest, validatePluginRevisionRegressionCertificate, validatePluginRevisionVerificationCertificate, validatePluginCreationVerificationCertificate
|
|
17
|
+
import { validateSourceCreationFiles, validateSourceCreationGrant, normalizeSourceCreationGrant } from './source-creation.js';
|
|
18
|
+
import { anySourceGrowthRunDigest as sourceGrowthRunDigest, isSourceOwnerContinuation, validateAnySourceGrowthRunBinding, validatePluginRevisionRegressionCertificate, validatePluginRevisionVerificationCertificate, validatePluginCreationVerificationCertificate } from '@dsh-enhanced/assistant-growth-contract';
|
|
18
19
|
import { validateAdoptionHandoffTerms } from './adoption-handoff.js';
|
|
19
20
|
import { assertForegroundDeployment, assertForegroundTask } from './foreground-deployment.js';
|
|
20
21
|
import { assertTaskObservationBatch, getTaskObservationRecord, readTaskObservationContext } from './task-observation-store.js';
|
|
22
|
+
import { createCreationCapabilityTrendSnapshot, listCreationCapabilityTrendSnapshots, sealCreationCapabilityTrendSnapshot } from './creation-capability-trend-snapshot.js';
|
|
21
23
|
import { parseRuntimeEpochRequest, parseRuntimeEpochReceipt, verifyRuntimeEpochReceipt } from './runtime-epoch.js';
|
|
22
24
|
import { runtimeIdentityDigest } from './foreground-deployment.js';
|
|
23
25
|
import { readHostMaintenanceRecords, verifyHostMaintenanceChain, hostMaintenanceCanonical, hostMaintenanceDigest } from './host-maintenance.js';
|
|
@@ -123,6 +125,61 @@ function canonical(value) {
|
|
|
123
125
|
export function controlPlaneDigest(value) {
|
|
124
126
|
return createHash('sha256').update(canonical(value)).digest('hex');
|
|
125
127
|
}
|
|
128
|
+
/** Structural validation precedes this exact prepared-build binding check. */
|
|
129
|
+
function creationVerificationMatchesPreparedEvidence(certificate, evidence) {
|
|
130
|
+
if (certificate.protocol === 'assistant-growth/creation-verification/v1') {
|
|
131
|
+
return evidence.schemaVersion === 1 && !Object.hasOwn(certificate, 'execution');
|
|
132
|
+
}
|
|
133
|
+
const execution = certificate.execution;
|
|
134
|
+
if (certificate.protocol === 'assistant-growth/creation-verification/v2') {
|
|
135
|
+
return evidence.schemaVersion === 2 && evidence.boundary === 'macos-seatbelt-single-process-v1'
|
|
136
|
+
&& execution?.boundary === 'macos-seatbelt-single-process-v1'
|
|
137
|
+
&& execution.runtimeDigest === evidence.runtimeDigest
|
|
138
|
+
&& execution.buildEvidenceDigest === controlPlaneDigest(evidence);
|
|
139
|
+
}
|
|
140
|
+
return certificate.protocol === 'assistant-growth/creation-verification/v3'
|
|
141
|
+
&& evidence.schemaVersion === 3 && evidence.boundary === 'linux-native-repository-v1'
|
|
142
|
+
&& execution?.boundary === 'linux-native-process-seccomp-v1'
|
|
143
|
+
&& execution.buildRuntimeDigest === evidence.runtimeDigest
|
|
144
|
+
&& execution.buildEvidenceDigest === controlPlaneDigest(evidence);
|
|
145
|
+
}
|
|
146
|
+
/** Legacy revision v1 has no execution claim; Linux requires its own signed build provenance. */
|
|
147
|
+
function revisionVerificationMatchesPreparedEvidence(certificate, evidence) {
|
|
148
|
+
if (certificate.protocol === 'assistant-growth/revision-verification/v1') {
|
|
149
|
+
return evidence.schemaVersion !== 3 && !Object.hasOwn(certificate, 'execution');
|
|
150
|
+
}
|
|
151
|
+
const execution = certificate.execution;
|
|
152
|
+
return certificate.protocol === 'assistant-growth/revision-verification/v2'
|
|
153
|
+
&& evidence.schemaVersion === 3 && evidence.boundary === 'linux-native-repository-v1'
|
|
154
|
+
&& execution?.boundary === 'linux-native-process-seccomp-v1'
|
|
155
|
+
&& execution.buildRuntimeDigest === evidence.runtimeDigest
|
|
156
|
+
&& execution.buildEvidenceDigest === controlPlaneDigest(evidence);
|
|
157
|
+
}
|
|
158
|
+
/** Retained certificates must refer to the exact candidate and one parent/candidate observer family. */
|
|
159
|
+
function revisionRegressionMatchesCandidate(certificate, candidate, parent) {
|
|
160
|
+
if (certificate.candidateVerificationDigest !== controlPlaneDigest(candidate)
|
|
161
|
+
|| controlPlaneDigest(certificate.plan) !== controlPlaneDigest(candidate.plan)
|
|
162
|
+
|| controlPlaneDigest(certificate.source) !== controlPlaneDigest(candidate.source)
|
|
163
|
+
|| controlPlaneDigest(certificate.parent) !== controlPlaneDigest(candidate.parent)
|
|
164
|
+
|| certificate.contractDigest !== parent.contractDigest
|
|
165
|
+
|| candidate.parent.planId !== parent.plan.id
|
|
166
|
+
|| candidate.parent.certificateDigest !== controlPlaneDigest(parent)
|
|
167
|
+
|| candidate.parent.artifactSha256 !== parent.plan.artifactSha256)
|
|
168
|
+
return false;
|
|
169
|
+
if (candidate.protocol === 'assistant-growth/revision-verification/v1') {
|
|
170
|
+
return certificate.protocol === 'assistant-growth/revision-regression/v1'
|
|
171
|
+
&& !Object.hasOwn(certificate, 'execution') && parent.protocol !== 'assistant-growth/creation-verification/v3';
|
|
172
|
+
}
|
|
173
|
+
const execution = certificate.execution, candidateExecution = candidate.execution, parentExecution = parent.execution;
|
|
174
|
+
return certificate.protocol === 'assistant-growth/revision-regression/v2'
|
|
175
|
+
&& parent.protocol === 'assistant-growth/creation-verification/v3'
|
|
176
|
+
&& execution?.boundary === 'linux-native-process-seccomp-v1'
|
|
177
|
+
&& candidateExecution?.boundary === execution.boundary && parentExecution?.boundary === execution.boundary
|
|
178
|
+
&& execution.runtimeDigest === candidateExecution.runtimeDigest && execution.runtimeDigest === parentExecution.runtimeDigest
|
|
179
|
+
&& execution.observerPreparationDigest === candidateExecution.observerPreparationDigest
|
|
180
|
+
&& execution.observerPreparationDigest === parentExecution.observerPreparationDigest
|
|
181
|
+
&& candidateExecution.buildRuntimeDigest === parentExecution.buildRuntimeDigest;
|
|
182
|
+
}
|
|
126
183
|
function bounded(value, field, maximum = 1_000) {
|
|
127
184
|
if (typeof value !== 'string')
|
|
128
185
|
throw new ControlPlaneStoreError('invalid-input', `${field} must be text`);
|
|
@@ -172,14 +229,16 @@ function creationBindingFromStored(value) {
|
|
|
172
229
|
let growthRun;
|
|
173
230
|
if (Object.hasOwn(binding, 'growthRun')) {
|
|
174
231
|
try {
|
|
175
|
-
|
|
232
|
+
validateAnySourceGrowthRunBinding(binding['growthRun']);
|
|
176
233
|
}
|
|
177
234
|
catch {
|
|
178
235
|
throw new ControlPlaneStoreError('invalid-input', 'source creation growth run is invalid');
|
|
179
236
|
}
|
|
180
237
|
growthRun = structuredClone(binding['growthRun']);
|
|
181
238
|
}
|
|
182
|
-
|
|
239
|
+
if (growthRun?.protocol === 'assistant-growth/source-run/v2' && binding['grant'].workItemFailures !== true)
|
|
240
|
+
throw new ControlPlaneStoreError('invalid-input', 'work item creation grant must explicitly permit work item failures');
|
|
241
|
+
return Object.freeze({ grant: Object.freeze(normalizeSourceCreationGrant(binding['grant'])), generatorDigest: binding['generatorDigest'],
|
|
183
242
|
...(growthRun === undefined ? {} : { growthRun: Object.freeze(growthRun) }) });
|
|
184
243
|
}
|
|
185
244
|
function revisionBindingFromStored(value) {
|
|
@@ -643,6 +702,51 @@ function activationFromRow(row) {
|
|
|
643
702
|
}
|
|
644
703
|
function preparedEvidenceFromStored(value) {
|
|
645
704
|
const item = objectRecord(value, 'stored source prepared evidence');
|
|
705
|
+
if (item['schemaVersion'] === 3) {
|
|
706
|
+
exactKeys(item, ['schemaVersion', 'kind', 'boundary', 'runtimeDigest', 'executionContractDigest', 'buildScriptSha256',
|
|
707
|
+
'archive', 'receipt', 'environment', 'checks', 'pack', 'logDigest', 'durationMs', 'preparedAt'], 'stored Linux source prepared evidence');
|
|
708
|
+
const archive = objectRecord(item['archive'], 'stored Linux source archive');
|
|
709
|
+
exactKeys(archive, ['sizeBytes', 'sha256'], 'stored Linux source archive');
|
|
710
|
+
const receipt = objectRecord(item['receipt'], 'stored Linux source receipt binding');
|
|
711
|
+
exactKeys(receipt, ['requestDigest', 'nativeNonce', 'nativeResourceId', 'intentDigest', 'resourcesDigest', 'receiptDigest'], 'stored Linux source receipt binding');
|
|
712
|
+
const environment = objectRecord(item['environment'], 'stored Linux source environment');
|
|
713
|
+
exactKeys(environment, ['npmConfigIgnoreScripts', 'frozenLockfile', 'offline', 'nodeVersion', 'pnpmVersion'], 'stored Linux source environment');
|
|
714
|
+
const pack = objectRecord(item['pack'], 'stored Linux source pack');
|
|
715
|
+
exactKeys(pack, ['name', 'version', 'sizeBytes', 'sha256'], 'stored Linux source pack');
|
|
716
|
+
const digest = (value) => typeof value === 'string' && DIGEST.test(value);
|
|
717
|
+
const size = (value, maximum) => Number.isSafeInteger(value) && Number(value) >= 1 && Number(value) <= maximum;
|
|
718
|
+
const storedChecks = item['checks'];
|
|
719
|
+
const checks = ['source-archive-integrity', 'frozen-offline-install', 'repository-pnpm-check', 'source-input-recheck', 'package-integrity'];
|
|
720
|
+
if (item['kind'] !== 'dsh-source-prepared-evidence' || item['boundary'] !== 'linux-native-repository-v1'
|
|
721
|
+
|| !digest(item['runtimeDigest']) || !digest(item['executionContractDigest']) || !digest(item['buildScriptSha256'])
|
|
722
|
+
|| !size(archive['sizeBytes'], 64 * 1024 * 1024) || !digest(archive['sha256'])
|
|
723
|
+
|| !['requestDigest', 'intentDigest', 'resourcesDigest', 'receiptDigest'].every(key => digest(receipt[key]))
|
|
724
|
+
|| typeof receipt['nativeNonce'] !== 'string' || !/^[a-f0-9]{32}$/u.test(receipt['nativeNonce'])
|
|
725
|
+
|| typeof receipt['nativeResourceId'] !== 'string' || !/^linux-[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/u.test(receipt['nativeResourceId'])
|
|
726
|
+
|| environment['npmConfigIgnoreScripts'] !== true || environment['frozenLockfile'] !== true || environment['offline'] !== true
|
|
727
|
+
|| !['nodeVersion', 'pnpmVersion'].every(key => typeof environment[key] === 'string' && environment[key] !== '' && environment[key].length <= 64)
|
|
728
|
+
|| !Array.isArray(storedChecks) || storedChecks.length !== checks.length || checks.some((check, index) => storedChecks[index] !== check)
|
|
729
|
+
|| typeof pack['name'] !== 'string' || pack['name'] === '' || pack['name'].length > 214
|
|
730
|
+
|| typeof pack['version'] !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._+=-]{0,63}$/u.test(pack['version'])
|
|
731
|
+
|| !size(pack['sizeBytes'], MAX_PREPARED_ARTIFACT_BYTES) || !digest(pack['sha256']) || !digest(item['logDigest'])
|
|
732
|
+
|| !Number.isSafeInteger(item['durationMs']) || Number(item['durationMs']) < 0
|
|
733
|
+
|| !Number.isSafeInteger(item['preparedAt']) || Number(item['preparedAt']) < 0) {
|
|
734
|
+
throw new ControlPlaneStoreError('invalid-state', 'stored Linux source prepared evidence is corrupt');
|
|
735
|
+
}
|
|
736
|
+
// Pure structural validation keeps the independent authority runtime free of
|
|
737
|
+
// an isolation executor import. The runner verifies the private receipt.
|
|
738
|
+
return Object.freeze({ schemaVersion: 3, kind: 'dsh-source-prepared-evidence',
|
|
739
|
+
boundary: 'linux-native-repository-v1', runtimeDigest: item['runtimeDigest'],
|
|
740
|
+
executionContractDigest: item['executionContractDigest'], buildScriptSha256: item['buildScriptSha256'],
|
|
741
|
+
archive: Object.freeze({ sizeBytes: Number(archive['sizeBytes']), sha256: String(archive['sha256']) }),
|
|
742
|
+
receipt: Object.freeze({ requestDigest: String(receipt['requestDigest']), nativeNonce: String(receipt['nativeNonce']),
|
|
743
|
+
nativeResourceId: String(receipt['nativeResourceId']), intentDigest: String(receipt['intentDigest']),
|
|
744
|
+
resourcesDigest: String(receipt['resourcesDigest']), receiptDigest: String(receipt['receiptDigest']) }),
|
|
745
|
+
environment: Object.freeze({ npmConfigIgnoreScripts: true, frozenLockfile: true, offline: true,
|
|
746
|
+
nodeVersion: String(environment['nodeVersion']), pnpmVersion: String(environment['pnpmVersion']) }), checks: Object.freeze(checks),
|
|
747
|
+
pack: Object.freeze({ name: pack['name'], version: pack['version'], sizeBytes: Number(pack['sizeBytes']), sha256: pack['sha256'] }),
|
|
748
|
+
logDigest: item['logDigest'], durationMs: Number(item['durationMs']), preparedAt: Number(item['preparedAt']) });
|
|
749
|
+
}
|
|
646
750
|
if (item['schemaVersion'] === 2) {
|
|
647
751
|
exactKeys(item, ['schemaVersion', 'kind', 'boundary', 'runtimeDigest', 'fixedCompilerSha256', 'checks', 'pack', 'preparedAt'], 'stored native source prepared evidence');
|
|
648
752
|
const pack = objectRecord(item['pack'], 'stored native source pack');
|
|
@@ -759,7 +863,7 @@ function sourceFromRow(row) {
|
|
|
759
863
|
}
|
|
760
864
|
const revision = row.revision_json === null ? undefined : revisionBindingFromStored(JSON.parse(row.revision_json));
|
|
761
865
|
const prepared = row.mode === 'modify' || row.mode === 'prepared-create' || row.mode === 'prepared-revise';
|
|
762
|
-
if (prepared !== (preparedEvidence !== undefined) || (row.mode === 'modify' && preparedEvidence?.schemaVersion
|
|
866
|
+
if (prepared !== (preparedEvidence !== undefined) || (row.mode === 'modify' && preparedEvidence?.schemaVersion === 2)
|
|
763
867
|
|| (row.mode === 'prepared-create') !== (creation !== undefined) || (row.mode === 'prepared-revise') !== (revision !== undefined)
|
|
764
868
|
|| ((row.mode === 'prepared-create' || row.mode === 'prepared-revise') && (!['pending-approval', 'expired'].includes(row.status) || (creation ?? revision)?.generatorDigest !== row.generator_digest
|
|
765
869
|
|| row.approval_json !== null || row.release_authorization_json !== null || row.release_id !== null))
|
|
@@ -1110,10 +1214,23 @@ function releaseArtifact(evidence) {
|
|
|
1110
1214
|
provenanceSha256: evidence.provenanceSha256, mergedCommit: evidence.mergedCommit, dshBaseline: evidence.dshBaseline,
|
|
1111
1215
|
capabilities: evidence.capabilities, authorities: evidence.authorities, requires: evidence.requires };
|
|
1112
1216
|
}
|
|
1217
|
+
function sourceBuildEvidenceSchema(build) {
|
|
1218
|
+
if (!('backend' in build))
|
|
1219
|
+
return 1;
|
|
1220
|
+
if (build.backend === 'macos-seatbelt')
|
|
1221
|
+
return 2;
|
|
1222
|
+
if (build.backend === 'linux-native')
|
|
1223
|
+
return 3;
|
|
1224
|
+
throw new ControlPlaneStoreError('invalid-state', 'source job build backend is unknown');
|
|
1225
|
+
}
|
|
1113
1226
|
function sourceJobIntentFromStored(value) {
|
|
1114
1227
|
const intent = objectRecord(value, 'source job intent');
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1228
|
+
const build = objectRecord(intent['build'], 'source job build');
|
|
1229
|
+
const backend = build['backend'];
|
|
1230
|
+
if (Object.hasOwn(build, 'backend') && backend !== 'macos-seatbelt' && backend !== 'linux-native') {
|
|
1231
|
+
throw new ControlPlaneStoreError('invalid-input', 'source job build backend is unknown');
|
|
1232
|
+
}
|
|
1233
|
+
const native = backend === 'macos-seatbelt' || backend === 'linux-native';
|
|
1117
1234
|
exactKeys(intent, ['authority', 'owner', 'ownerDigest', 'trustDigest', 'repository', 'name', 'gapId', 'gapRevision', 'gapDigest',
|
|
1118
1235
|
'baseCommit', 'files', 'ttlMs', 'build', 'worktree', native ? 'nativeResource' : 'containerName', ...(Object.hasOwn(intent, 'baseline') ? ['baseline'] : []),
|
|
1119
1236
|
...(Object.hasOwn(intent, 'mode') ? ['mode'] : []), ...(Object.hasOwn(intent, 'creation') ? ['creation'] : []), ...(Object.hasOwn(intent, 'revision') ? ['revision'] : [])], 'source job intent');
|
|
@@ -1139,7 +1256,7 @@ function sourceJobIntentFromStored(value) {
|
|
|
1139
1256
|
|| !Array.isArray(intent['files']) || intent['files'].length === 0 || intent['files'].length > 64
|
|
1140
1257
|
|| !Number.isSafeInteger(intent['ttlMs']) || Number(intent['ttlMs']) < 60_000 || Number(intent['ttlMs']) > 86_400_000
|
|
1141
1258
|
|| (!native && (typeof intent['containerName'] !== 'string' || !/^dsh-source-job-[a-f0-9]{64}$/u.test(intent['containerName'])))
|
|
1142
|
-
|| (
|
|
1259
|
+
|| (backend === 'macos-seatbelt' && (intent['mode'] !== 'create' && intent['mode'] !== 'revise-created'))
|
|
1143
1260
|
|| (intent['mode'] !== undefined && intent['mode'] !== 'create' && intent['mode'] !== 'revise-created')
|
|
1144
1261
|
|| (intent['mode'] === 'create') !== Object.hasOwn(intent, 'creation')
|
|
1145
1262
|
|| (intent['mode'] === 'revise-created') !== Object.hasOwn(intent, 'revision')) {
|
|
@@ -1148,7 +1265,7 @@ function sourceJobIntentFromStored(value) {
|
|
|
1148
1265
|
if (native) {
|
|
1149
1266
|
const resource = objectRecord(intent['nativeResource'], 'native source job resource');
|
|
1150
1267
|
exactKeys(resource, ['backend', 'key'], 'native source job resource');
|
|
1151
|
-
if (resource['backend'] !==
|
|
1268
|
+
if (resource['backend'] !== backend || typeof resource['key'] !== 'string'
|
|
1152
1269
|
|| !/^source-job-[a-f0-9]{64}$/u.test(resource['key'])) {
|
|
1153
1270
|
throw new ControlPlaneStoreError('invalid-input', 'native source job resource is invalid');
|
|
1154
1271
|
}
|
|
@@ -1208,7 +1325,7 @@ function sourceJobIntentFromStored(value) {
|
|
|
1208
1325
|
name: intent['name'], gapId: intent['gapId'], gapRevision: Number(intent['gapRevision']), gapDigest: intent['gapDigest'],
|
|
1209
1326
|
baseCommit: intent['baseCommit'], files: Object.freeze(files), ttlMs: Number(intent['ttlMs']), build: intent['build'],
|
|
1210
1327
|
worktree: intent['worktree'],
|
|
1211
|
-
...(native ? { nativeResource: Object.freeze({ backend
|
|
1328
|
+
...(native ? { nativeResource: Object.freeze({ backend,
|
|
1212
1329
|
key: intent['nativeResource'].key }) }
|
|
1213
1330
|
: { containerName: intent['containerName'] }),
|
|
1214
1331
|
...(creation === undefined ? {} : { mode: 'create', creation }),
|
|
@@ -1236,6 +1353,21 @@ function ownerTaskFailureReferenceFromStored(value, errorCode = 'invalid-input')
|
|
|
1236
1353
|
try {
|
|
1237
1354
|
const reference = errorCode === 'invalid-input' ? exactInputRecord(value, 'owner task failure reference') : objectRecord(value, 'owner task failure reference');
|
|
1238
1355
|
const keys = errorCode === 'invalid-input' ? exactInputKeys : exactKeys;
|
|
1356
|
+
if (reference['schemaVersion'] === 2) {
|
|
1357
|
+
keys(reference, ['schemaVersion', 'sourceProtocol', 'owner', 'outcomeId', 'projection', 'sourceDigest', 'presentationDigest', 'context'], 'work item failure reference');
|
|
1358
|
+
const projection = errorCode === 'invalid-input' ? exactInputRecord(reference['projection'], 'work item projection') : objectRecord(reference['projection'], 'work item projection');
|
|
1359
|
+
const context = errorCode === 'invalid-input' ? exactInputRecord(reference['context'], 'work item context') : objectRecord(reference['context'], 'work item context');
|
|
1360
|
+
keys(context, ['manifestDigest', 'objectiveDigest', 'objectiveBytes'], 'work item context');
|
|
1361
|
+
if (reference['sourceProtocol'] !== 'assistant-delivery/work-item-engineering-context/v1' || projection['subjectKind'] !== 'work-item'
|
|
1362
|
+
|| typeof reference['presentationDigest'] !== 'string' || !DIGEST.test(reference['presentationDigest'])
|
|
1363
|
+
|| typeof context['manifestDigest'] !== 'string' || !DIGEST.test(context['manifestDigest']) || typeof context['objectiveDigest'] !== 'string' || !DIGEST.test(context['objectiveDigest'])
|
|
1364
|
+
|| !Number.isSafeInteger(context['objectiveBytes']) || Number(context['objectiveBytes']) < 1 || Number(context['objectiveBytes']) > 16384)
|
|
1365
|
+
throw new ControlPlaneStoreError(errorCode, 'work item failure reference is invalid');
|
|
1366
|
+
const legacy = ownerTaskFailureReferenceFromStored({ schemaVersion: 1, owner: reference['owner'], outcomeId: reference['outcomeId'], projection: { ...projection, subjectKind: 'foreground-turn' }, sourceDigest: reference['sourceDigest'] }, errorCode);
|
|
1367
|
+
return Object.freeze({ schemaVersion: 2, sourceProtocol: 'assistant-delivery/work-item-engineering-context/v1', owner: legacy.owner, outcomeId: legacy.outcomeId,
|
|
1368
|
+
projection: Object.freeze({ ...legacy.projection, subjectKind: 'work-item' }), sourceDigest: legacy.sourceDigest, presentationDigest: reference['presentationDigest'],
|
|
1369
|
+
context: Object.freeze({ manifestDigest: context['manifestDigest'], objectiveDigest: context['objectiveDigest'], objectiveBytes: Number(context['objectiveBytes']) }) });
|
|
1370
|
+
}
|
|
1239
1371
|
keys(reference, ['schemaVersion', 'owner', 'outcomeId', 'projection', 'sourceDigest'], 'owner task failure reference');
|
|
1240
1372
|
const owner = errorCode === 'invalid-input' ? exactInputRecord(reference['owner'], 'owner task failure owner') : objectRecord(reference['owner'], 'owner task failure owner');
|
|
1241
1373
|
keys(owner, ['receiptVersion', 'authorityId', 'authorityHash', 'principalId', 'principalRecordId', 'principalVersion',
|
|
@@ -1733,6 +1865,9 @@ export class ControlPlaneStore {
|
|
|
1733
1865
|
#database;
|
|
1734
1866
|
#now;
|
|
1735
1867
|
#ownerTaskFailureGapAdmission;
|
|
1868
|
+
#workItemWriter = false;
|
|
1869
|
+
#workItemSavepoints = [];
|
|
1870
|
+
#workItemSavepointSequence = 0;
|
|
1736
1871
|
#foregroundDeploymentAdmission;
|
|
1737
1872
|
#liveQualificationAdmission;
|
|
1738
1873
|
#withLiveQualificationFence;
|
|
@@ -1804,14 +1939,41 @@ export class ControlPlaneStore {
|
|
|
1804
1939
|
const reference = ownerTaskFailureReferenceFromStored(referenceInput);
|
|
1805
1940
|
const referenceDigest = controlPlaneDigest(reference);
|
|
1806
1941
|
const idempotencyKey = `owner-task-failure:${referenceDigest}`;
|
|
1807
|
-
const normalized = { idempotencyKey, capability: 'foreground-task-repair',
|
|
1808
|
-
context: 'Owner-verified foreground task failure. Private source reference retained.',
|
|
1942
|
+
const normalized = { idempotencyKey, capability: reference.schemaVersion === 2 ? 'work-item-task-repair' : 'foreground-task-repair',
|
|
1943
|
+
context: reference.schemaVersion === 2 ? 'Owner-verified background work-item failure. Private engineering reference retained.' : 'Owner-verified foreground task failure. Private source reference retained.',
|
|
1809
1944
|
expectedValue: 0, frequency: 1, estimatedCost: 1, risk: 1 };
|
|
1810
1945
|
const inputDigest = controlPlaneDigest(normalized);
|
|
1811
1946
|
const now = this.#now();
|
|
1812
1947
|
const id = `gap-${randomUUID()}`;
|
|
1813
|
-
this.#
|
|
1948
|
+
this.#beginMutation();
|
|
1814
1949
|
try {
|
|
1950
|
+
if (reference.schemaVersion === 2) {
|
|
1951
|
+
// /new changes only a route generation. Preserve the first frozen owner
|
|
1952
|
+
// bytes for the same authenticated source rather than minting another gap.
|
|
1953
|
+
const stable = (value) => {
|
|
1954
|
+
const { bindingVersion: _bindingVersion, generation: _generation, ...owner } = value.owner;
|
|
1955
|
+
return controlPlaneDigest({ ...value, owner });
|
|
1956
|
+
};
|
|
1957
|
+
const candidates = this.#database.prepare(`SELECT gap_id FROM owner_task_failure_gaps
|
|
1958
|
+
WHERE json_extract(reference_json, '$.schemaVersion') = 2
|
|
1959
|
+
AND json_extract(reference_json, '$.sourceDigest') = ?
|
|
1960
|
+
AND json_extract(reference_json, '$.outcomeId') = ?
|
|
1961
|
+
AND json_extract(reference_json, '$.projection.subjectRef') = ? LIMIT 1001`)
|
|
1962
|
+
.all(reference.sourceDigest, reference.outcomeId, reference.projection.subjectRef);
|
|
1963
|
+
if (candidates.length > 1000)
|
|
1964
|
+
throw new ControlPlaneStoreError('conflict', 'work item failure historical lookup exceeds bound');
|
|
1965
|
+
const matches = candidates.filter(row => { const prior = this.getOwnerTaskFailureReference(row.gap_id); return prior?.schemaVersion === 2 && stable(prior) === stable(reference); });
|
|
1966
|
+
if (matches.length > 1)
|
|
1967
|
+
throw new ControlPlaneStoreError('conflict', 'work item failure source has ambiguous historical gaps');
|
|
1968
|
+
if (matches[0]) {
|
|
1969
|
+
const original = this.getOwnerTaskFailureReference(matches[0].gap_id);
|
|
1970
|
+
if (!isSourceOwnerContinuation(reference.owner, original.owner))
|
|
1971
|
+
throw new ControlPlaneStoreError('conflict', 'work item failure owner generation regressed');
|
|
1972
|
+
const gap = this.getGap(matches[0].gap_id);
|
|
1973
|
+
this.#commitMutation();
|
|
1974
|
+
return gap;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1815
1977
|
const prior = this.#database.prepare('SELECT * FROM capability_gaps WHERE idempotency_key = ?').get(idempotencyKey);
|
|
1816
1978
|
if (prior !== undefined) {
|
|
1817
1979
|
if (prior.input_digest !== inputDigest)
|
|
@@ -1820,7 +1982,7 @@ export class ControlPlaneStore {
|
|
|
1820
1982
|
if (stored === undefined || controlPlaneDigest(stored) !== referenceDigest) {
|
|
1821
1983
|
throw new ControlPlaneStoreError('invalid-state', 'owner task failure gap sidecar is corrupt');
|
|
1822
1984
|
}
|
|
1823
|
-
this.#
|
|
1985
|
+
this.#commitMutation();
|
|
1824
1986
|
return gapFromRow(prior);
|
|
1825
1987
|
}
|
|
1826
1988
|
this.#database.prepare(`INSERT INTO capability_gaps (id, idempotency_key, input_digest, capability, context,
|
|
@@ -1829,14 +1991,70 @@ export class ControlPlaneStore {
|
|
|
1829
1991
|
this.#database.prepare(`INSERT INTO owner_task_failure_gaps (gap_id, reference_json, reference_digest)
|
|
1830
1992
|
VALUES (?, ?, ?)`).run(id, JSON.stringify(reference), referenceDigest);
|
|
1831
1993
|
const gap = this.getGap(id);
|
|
1832
|
-
this.#
|
|
1994
|
+
this.#commitMutation();
|
|
1833
1995
|
return gap;
|
|
1834
1996
|
}
|
|
1835
1997
|
catch (error) {
|
|
1836
|
-
this.#
|
|
1998
|
+
this.#rollbackMutation();
|
|
1837
1999
|
throw error;
|
|
1838
2000
|
}
|
|
1839
2001
|
}
|
|
2002
|
+
#assertWorkItemSynchronous(result) {
|
|
2003
|
+
const refuse = () => { throw new ControlPlaneStoreError('invalid-input', 'owner task failure gap admission must be synchronous'); };
|
|
2004
|
+
if (result === null || (typeof result !== 'object' && typeof result !== 'function'))
|
|
2005
|
+
return;
|
|
2006
|
+
if (utilTypes.isProxy(result))
|
|
2007
|
+
refuse();
|
|
2008
|
+
if (utilTypes.isPromise(result)) {
|
|
2009
|
+
// Best effort only for ordinary native Promises. Do not execute an own
|
|
2010
|
+
// constructor or subclass species while refusing an asynchronous callback.
|
|
2011
|
+
if (Object.getPrototypeOf(result) === Promise.prototype && !Object.hasOwn(result, 'constructor')) {
|
|
2012
|
+
try {
|
|
2013
|
+
void Promise.prototype.then.call(result, undefined, () => undefined);
|
|
2014
|
+
}
|
|
2015
|
+
catch { /* synchronous refusal remains authoritative */ }
|
|
2016
|
+
}
|
|
2017
|
+
refuse();
|
|
2018
|
+
}
|
|
2019
|
+
let prototype = result;
|
|
2020
|
+
for (let depth = 0; prototype !== null && depth < 64; depth++) {
|
|
2021
|
+
if (utilTypes.isProxy(prototype) || Object.getOwnPropertyDescriptor(prototype, 'then'))
|
|
2022
|
+
refuse();
|
|
2023
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
2024
|
+
}
|
|
2025
|
+
if (prototype !== null)
|
|
2026
|
+
refuse();
|
|
2027
|
+
}
|
|
2028
|
+
#beginMutation() {
|
|
2029
|
+
if (!this.#workItemWriter) {
|
|
2030
|
+
this.#database.exec('BEGIN IMMEDIATE');
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
const name = `cp_work_item_${++this.#workItemSavepointSequence}`;
|
|
2034
|
+
this.#database.exec(`SAVEPOINT ${name}`);
|
|
2035
|
+
this.#workItemSavepoints.push(name);
|
|
2036
|
+
}
|
|
2037
|
+
#commitMutation() {
|
|
2038
|
+
if (!this.#workItemWriter) {
|
|
2039
|
+
this.#database.exec('COMMIT');
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
const name = this.#workItemSavepoints.pop();
|
|
2043
|
+
if (!name)
|
|
2044
|
+
throw new ControlPlaneStoreError('invalid-state', 'work item nested writer is unbalanced');
|
|
2045
|
+
this.#database.exec(`RELEASE SAVEPOINT ${name}`);
|
|
2046
|
+
}
|
|
2047
|
+
#rollbackMutation() {
|
|
2048
|
+
if (!this.#workItemWriter) {
|
|
2049
|
+
this.#database.exec('ROLLBACK');
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
const name = this.#workItemSavepoints.pop();
|
|
2053
|
+
if (name) {
|
|
2054
|
+
this.#database.exec(`ROLLBACK TO SAVEPOINT ${name}`);
|
|
2055
|
+
this.#database.exec(`RELEASE SAVEPOINT ${name}`);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
1840
2058
|
getOwnerTaskFailureReference(gapId) {
|
|
1841
2059
|
return readOwnerTaskFailureReference(this.#database, gapId);
|
|
1842
2060
|
}
|
|
@@ -1846,22 +2064,48 @@ export class ControlPlaneStore {
|
|
|
1846
2064
|
if (typeof gapId !== 'string' || !KEY.test(gapId) || typeof callback !== 'function') {
|
|
1847
2065
|
throw new ControlPlaneStoreError('invalid-input', 'owner task failure gap admission is invalid');
|
|
1848
2066
|
}
|
|
1849
|
-
if (callback.constructor.name === 'AsyncFunction')
|
|
1850
|
-
throw new ControlPlaneStoreError('invalid-input', 'owner task failure gap admission must be synchronous');
|
|
1851
2067
|
if (this.#ownerTaskFailureGapAdmission !== undefined)
|
|
1852
2068
|
throw new ControlPlaneStoreError('conflict', 'owner task failure gap admission is already active');
|
|
1853
|
-
|
|
2069
|
+
const reference = this.getOwnerTaskFailureReference(gapId);
|
|
2070
|
+
if (reference === undefined)
|
|
1854
2071
|
throw new ControlPlaneStoreError('not-found', 'owner task failure gap is absent');
|
|
2072
|
+
const workItem = reference.schemaVersion === 2;
|
|
2073
|
+
if (workItem ? utilTypes.isProxy(callback) || utilTypes.isAsyncFunction(callback) : callback.constructor.name === 'AsyncFunction')
|
|
2074
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner task failure gap admission must be synchronous');
|
|
2075
|
+
if (workItem) {
|
|
2076
|
+
if (this.#database.isTransaction)
|
|
2077
|
+
throw new ControlPlaneStoreError('conflict', 'work item writer must be entered before CP mutation');
|
|
2078
|
+
this.#database.exec('BEGIN IMMEDIATE');
|
|
2079
|
+
this.#workItemWriter = true;
|
|
2080
|
+
}
|
|
1855
2081
|
this.#ownerTaskFailureGapAdmission = gapId;
|
|
1856
2082
|
try {
|
|
2083
|
+
if (workItem && controlPlaneDigest(this.getOwnerTaskFailureReference(gapId)) !== controlPlaneDigest(reference))
|
|
2084
|
+
throw new ControlPlaneStoreError('conflict', 'work item reference changed at writer boundary');
|
|
1857
2085
|
const result = callback();
|
|
1858
|
-
if (
|
|
2086
|
+
if (workItem)
|
|
2087
|
+
this.#assertWorkItemSynchronous(result);
|
|
2088
|
+
else if (typeof result?.then === 'function') {
|
|
1859
2089
|
throw new ControlPlaneStoreError('invalid-input', 'owner task failure gap admission must be synchronous');
|
|
1860
2090
|
}
|
|
2091
|
+
if (workItem) {
|
|
2092
|
+
if (this.#workItemSavepoints.length)
|
|
2093
|
+
throw new ControlPlaneStoreError('invalid-state', 'work item nested writer was not retired');
|
|
2094
|
+
this.#database.exec('COMMIT');
|
|
2095
|
+
}
|
|
1861
2096
|
return result;
|
|
1862
2097
|
}
|
|
2098
|
+
catch (error) {
|
|
2099
|
+
if (workItem && this.#database.isTransaction)
|
|
2100
|
+
this.#database.exec('ROLLBACK');
|
|
2101
|
+
throw error;
|
|
2102
|
+
}
|
|
1863
2103
|
finally {
|
|
1864
2104
|
this.#ownerTaskFailureGapAdmission = undefined;
|
|
2105
|
+
if (workItem) {
|
|
2106
|
+
this.#workItemWriter = false;
|
|
2107
|
+
this.#workItemSavepoints = [];
|
|
2108
|
+
}
|
|
1865
2109
|
}
|
|
1866
2110
|
}
|
|
1867
2111
|
#assertOwnerTaskFailureGapAdmission(gapId) {
|
|
@@ -1916,16 +2160,16 @@ export class ControlPlaneStore {
|
|
|
1916
2160
|
return this.#withActivationSource(planId, () => {
|
|
1917
2161
|
const own = !this.#database.isTransaction;
|
|
1918
2162
|
if (own)
|
|
1919
|
-
this.#
|
|
2163
|
+
this.#beginMutation();
|
|
1920
2164
|
try {
|
|
1921
2165
|
const result = this.#withActivationSource(planId, callback, recovery);
|
|
1922
2166
|
if (own)
|
|
1923
|
-
this.#
|
|
2167
|
+
this.#commitMutation();
|
|
1924
2168
|
return result;
|
|
1925
2169
|
}
|
|
1926
2170
|
catch (error) {
|
|
1927
2171
|
if (own)
|
|
1928
|
-
this.#
|
|
2172
|
+
this.#rollbackMutation();
|
|
1929
2173
|
throw error;
|
|
1930
2174
|
}
|
|
1931
2175
|
}, recovery);
|
|
@@ -2004,11 +2248,11 @@ export class ControlPlaneStore {
|
|
|
2004
2248
|
target: input.target, executor: input.executor,
|
|
2005
2249
|
createdAt: now, expiresAt };
|
|
2006
2250
|
const planDigest = controlPlaneDigest(immutable);
|
|
2007
|
-
this.#
|
|
2251
|
+
this.#beginMutation();
|
|
2008
2252
|
try {
|
|
2009
2253
|
const raced = this.#activationPlanReceiptByKey(idempotencyKey, 'create-activation-plan', inputDigest);
|
|
2010
2254
|
if (raced !== undefined) {
|
|
2011
|
-
this.#
|
|
2255
|
+
this.#commitMutation();
|
|
2012
2256
|
return raced;
|
|
2013
2257
|
}
|
|
2014
2258
|
const currentGap = this.getGap(gap.id);
|
|
@@ -2031,11 +2275,11 @@ export class ControlPlaneStore {
|
|
|
2031
2275
|
}
|
|
2032
2276
|
const receipt = { idempotencyKey, operation: 'create-activation-plan', inputDigest, result: plan, createdAt: now };
|
|
2033
2277
|
this.#insertReceipt(receipt);
|
|
2034
|
-
this.#
|
|
2278
|
+
this.#commitMutation();
|
|
2035
2279
|
return receipt;
|
|
2036
2280
|
}
|
|
2037
2281
|
catch (error) {
|
|
2038
|
-
this.#
|
|
2282
|
+
this.#rollbackMutation();
|
|
2039
2283
|
throw error;
|
|
2040
2284
|
}
|
|
2041
2285
|
}
|
|
@@ -2071,7 +2315,7 @@ export class ControlPlaneStore {
|
|
|
2071
2315
|
if (this.#adoptionCoordinatorId !== undefined)
|
|
2072
2316
|
throw new ControlPlaneStoreError('invalid-state', 'adoption coordinator cannot prepare a target handoff');
|
|
2073
2317
|
return this.#withActivationSource(input.planId, () => {
|
|
2074
|
-
this.#
|
|
2318
|
+
this.#beginMutation();
|
|
2075
2319
|
try {
|
|
2076
2320
|
this.assertOwnerActivationSource(input.planId);
|
|
2077
2321
|
const plan = this.getPlan(input.planId), terms = plan.dossier.handoff;
|
|
@@ -2092,7 +2336,7 @@ export class ControlPlaneStore {
|
|
|
2092
2336
|
if (prior.planDigest !== plan.digest || prior.coordinatorId !== terms.coordinatorId) {
|
|
2093
2337
|
throw new ControlPlaneStoreError('invalid-state', 'stored adoption handoff is corrupt');
|
|
2094
2338
|
}
|
|
2095
|
-
this.#
|
|
2339
|
+
this.#commitMutation();
|
|
2096
2340
|
return prior;
|
|
2097
2341
|
}
|
|
2098
2342
|
const now = this.#now(), expiresAt = Math.min(now + terms.maximumWindowMs, plan.expiresAt, plan.approval.expiresAt);
|
|
@@ -2101,11 +2345,11 @@ export class ControlPlaneStore {
|
|
|
2101
2345
|
this.#database.prepare('INSERT INTO adoption_handoffs (plan_id,plan_digest,coordinator_id,created_at,expires_at,revoked_at) VALUES (?,?,?,?,?,NULL)')
|
|
2102
2346
|
.run(plan.id, plan.digest, terms.coordinatorId, now, expiresAt);
|
|
2103
2347
|
const handoff = this.getAdoptionHandoff(plan.id);
|
|
2104
|
-
this.#
|
|
2348
|
+
this.#commitMutation();
|
|
2105
2349
|
return handoff;
|
|
2106
2350
|
}
|
|
2107
2351
|
catch (error) {
|
|
2108
|
-
this.#
|
|
2352
|
+
this.#rollbackMutation();
|
|
2109
2353
|
throw error;
|
|
2110
2354
|
}
|
|
2111
2355
|
});
|
|
@@ -2147,6 +2391,14 @@ export class ControlPlaneStore {
|
|
|
2147
2391
|
throw new ControlPlaneStoreError('conflict', 'adoption handoff is inactive');
|
|
2148
2392
|
}
|
|
2149
2393
|
assertAdoptionHandoff(planId) { this.#assertAdoptionHandoff(planId, this.#adoptionCoordinatorId); }
|
|
2394
|
+
inspectSourceJobQuota(input) {
|
|
2395
|
+
const a = input.authority, c = input.creation;
|
|
2396
|
+
const row = this.#database.prepare('SELECT * FROM source_job_authorities WHERE authority_id=?').get(a.id);
|
|
2397
|
+
const grant = this.#database.prepare('SELECT * FROM source_creation_grants WHERE grant_id=?').get(c.id);
|
|
2398
|
+
if (row && (row.authority_digest !== a.digest || row.expires_at !== a.expiresAt || row.max_submissions !== a.maxSubmissions) || grant && (grant.grant_digest !== controlPlaneDigest(c) || grant.expires_at !== c.expiresAt || grant.max_creates !== c.maxCreates))
|
|
2399
|
+
throw new ControlPlaneStoreError('conflict', 'source job quota authority changed');
|
|
2400
|
+
return { sourceSubmissions: Math.max(0, a.maxSubmissions - (row?.submissions ?? 0)), creates: Math.max(0, c.maxCreates - (grant?.creates ?? 0)) };
|
|
2401
|
+
}
|
|
2150
2402
|
enqueueSourceJob(input) {
|
|
2151
2403
|
if (!/^source-job-[a-f0-9]{64}$/u.test(input.id) || input.automationId !== input.id) {
|
|
2152
2404
|
throw new ControlPlaneStoreError('invalid-input', 'source job identity is invalid');
|
|
@@ -2175,7 +2427,7 @@ export class ControlPlaneStore {
|
|
|
2175
2427
|
const growthRun = intent.creation?.growthRun ?? intent.revision?.growthRun;
|
|
2176
2428
|
if (growthRun !== undefined && (now >= growthRun.generationDeadlineAt || now >= growthRun.expiresAt))
|
|
2177
2429
|
throw new ControlPlaneStoreError('expired', 'source growth generation window is expired');
|
|
2178
|
-
this.#
|
|
2430
|
+
this.#beginMutation();
|
|
2179
2431
|
try {
|
|
2180
2432
|
const prior = this.#database.prepare('SELECT * FROM source_jobs WHERE authority_id = ? AND idempotency_key = ?')
|
|
2181
2433
|
.get(intent.authority.id, idempotencyKey);
|
|
@@ -2184,7 +2436,7 @@ export class ControlPlaneStore {
|
|
|
2184
2436
|
if (record.intentDigest !== intentDigest || record.id !== input.id || record.automationId !== input.automationId) {
|
|
2185
2437
|
throw new ControlPlaneStoreError('conflict', 'source job idempotency key was reused with different input');
|
|
2186
2438
|
}
|
|
2187
|
-
this.#
|
|
2439
|
+
this.#commitMutation();
|
|
2188
2440
|
return record;
|
|
2189
2441
|
}
|
|
2190
2442
|
if (intent.baseline !== undefined) {
|
|
@@ -2260,11 +2512,11 @@ export class ControlPlaneStore {
|
|
|
2260
2512
|
const record = this.getSourceJob(input.id);
|
|
2261
2513
|
if (record === undefined)
|
|
2262
2514
|
throw new ControlPlaneStoreError('invalid-state', 'inserted source job could not be read');
|
|
2263
|
-
this.#
|
|
2515
|
+
this.#commitMutation();
|
|
2264
2516
|
return record;
|
|
2265
2517
|
}
|
|
2266
2518
|
catch (error) {
|
|
2267
|
-
this.#
|
|
2519
|
+
this.#rollbackMutation();
|
|
2268
2520
|
throw error;
|
|
2269
2521
|
}
|
|
2270
2522
|
}
|
|
@@ -2411,9 +2663,9 @@ export class ControlPlaneStore {
|
|
|
2411
2663
|
throw new ControlPlaneStoreError('invalid-input', 'modify source plan prepared digests are invalid');
|
|
2412
2664
|
}
|
|
2413
2665
|
preparedEvidence = preparedEvidenceFromStored(input.prepared.evidence);
|
|
2414
|
-
if ((mode === 'modify' && preparedEvidence.schemaVersion
|
|
2415
|
-
|| (preparedEvidence.schemaVersion
|
|
2416
|
-
throw new ControlPlaneStoreError('invalid-input', 'native source evidence requires a durable
|
|
2666
|
+
if ((mode === 'modify' && preparedEvidence.schemaVersion === 2)
|
|
2667
|
+
|| (preparedEvidence.schemaVersion !== 1 && input.sourceJob === undefined)) {
|
|
2668
|
+
throw new ControlPlaneStoreError('invalid-input', 'native source evidence requires a matching durable source job');
|
|
2417
2669
|
}
|
|
2418
2670
|
}
|
|
2419
2671
|
else if (input.prepared !== undefined) {
|
|
@@ -2482,7 +2734,7 @@ export class ControlPlaneStore {
|
|
|
2482
2734
|
? { ...immutable, mode, ...(creation === undefined ? {} : { creation }), ...(revision === undefined ? {} : { sourceRevision: revision }), sourceCheck: { treeDigest: input.prepared.treeDigest, patchDigest: input.prepared.patchDigest,
|
|
2483
2735
|
checkedAt: input.prepared.checkedAt }, preparedEvidence }
|
|
2484
2736
|
: immutable);
|
|
2485
|
-
this.#
|
|
2737
|
+
this.#beginMutation();
|
|
2486
2738
|
try {
|
|
2487
2739
|
const currentGap = this.getGap(gap.id);
|
|
2488
2740
|
if (currentGap.revision !== gap.revision || currentGap.inputDigest !== gap.inputDigest || currentGap.status !== 'open'
|
|
@@ -2497,7 +2749,7 @@ export class ControlPlaneStore {
|
|
|
2497
2749
|
|| sourceJob.intent.baseCommit !== input.baseCommit || sourceJob.intent.name !== name
|
|
2498
2750
|
|| sourceJob.intent.repository !== input.repository || sourceJob.intent.worktree !== input.worktree || sourceJob.intent.ttlMs !== input.ttlMs
|
|
2499
2751
|
|| (mode === 'prepared-create') !== (sourceJob.intent.mode === 'create')
|
|
2500
|
-
|| (preparedEvidence !== undefined &&
|
|
2752
|
+
|| (preparedEvidence !== undefined && preparedEvidence.schemaVersion !== sourceBuildEvidenceSchema(sourceJob.intent.build))
|
|
2501
2753
|
|| (mode === 'prepared-create' && controlPlaneDigest(sourceJob.intent.creation) !== controlPlaneDigest(creation))
|
|
2502
2754
|
|| (mode === 'prepared-revise') !== (sourceJob.intent.mode === 'revise-created')
|
|
2503
2755
|
|| (mode === 'prepared-revise' && controlPlaneDigest(sourceJob.intent.revision) !== controlPlaneDigest(revision))) {
|
|
@@ -2560,11 +2812,11 @@ export class ControlPlaneStore {
|
|
|
2560
2812
|
const plan = this.getSourcePlan(id);
|
|
2561
2813
|
const receipt = { idempotencyKey: key, operation: 'create-source-plan', inputDigest, result: plan, createdAt: now };
|
|
2562
2814
|
this.#insertReceipt(receipt);
|
|
2563
|
-
this.#
|
|
2815
|
+
this.#commitMutation();
|
|
2564
2816
|
return receipt;
|
|
2565
2817
|
}
|
|
2566
2818
|
catch (error) {
|
|
2567
|
-
this.#
|
|
2819
|
+
this.#rollbackMutation();
|
|
2568
2820
|
throw error;
|
|
2569
2821
|
}
|
|
2570
2822
|
}
|
|
@@ -2614,6 +2866,7 @@ export class ControlPlaneStore {
|
|
|
2614
2866
|
|| job.intent.creation.generatorDigest !== plan.generatorDigest
|
|
2615
2867
|
|| job.intent.repository !== plan.repository || job.intent.worktree !== plan.worktree
|
|
2616
2868
|
|| job.intent.baseCommit !== plan.baseCommit || job.intent.name !== plan.name
|
|
2869
|
+
|| plan.preparedEvidence.schemaVersion !== sourceBuildEvidenceSchema(job.intent.build)
|
|
2617
2870
|
|| plan.expiresAt !== plan.createdAt + job.intent.ttlMs) {
|
|
2618
2871
|
throw new ControlPlaneStoreError('invalid-state', 'prepared creation source job binding is corrupt');
|
|
2619
2872
|
}
|
|
@@ -2659,6 +2912,7 @@ export class ControlPlaneStore {
|
|
|
2659
2912
|
|| job.intent.revision.generatorDigest !== plan.generatorDigest
|
|
2660
2913
|
|| job.intent.repository !== plan.repository || job.intent.worktree !== plan.worktree
|
|
2661
2914
|
|| job.intent.baseCommit !== plan.baseCommit || job.intent.name !== plan.name
|
|
2915
|
+
|| plan.preparedEvidence.schemaVersion !== sourceBuildEvidenceSchema(job.intent.build)
|
|
2662
2916
|
|| plan.expiresAt !== plan.createdAt + job.intent.ttlMs) {
|
|
2663
2917
|
throw new ControlPlaneStoreError('invalid-state', 'prepared revision source job binding is corrupt');
|
|
2664
2918
|
}
|
|
@@ -2693,6 +2947,7 @@ export class ControlPlaneStore {
|
|
|
2693
2947
|
const job = this.getPreparedSourceJob(plan.id);
|
|
2694
2948
|
const reference = this.getOwnerTaskFailureReference(plan.gapId);
|
|
2695
2949
|
if (!reference || !plan.sourceCheck || !plan.preparedEvidence || !plan.creation?.growthRun
|
|
2950
|
+
|| !creationVerificationMatchesPreparedEvidence(certificate, plan.preparedEvidence)
|
|
2696
2951
|
|| !job.intent.creation?.growthRun || plan.digest !== certificate.plan.digest || plan.name !== certificate.plan.name
|
|
2697
2952
|
|| plan.sourceCheck.treeDigest !== certificate.plan.sourceTreeDigest
|
|
2698
2953
|
|| plan.sourceCheck.patchDigest !== certificate.plan.sourcePatchDigest
|
|
@@ -2709,7 +2964,7 @@ export class ControlPlaneStore {
|
|
|
2709
2964
|
}
|
|
2710
2965
|
this.readPreparedSourceArtifact(plan.id);
|
|
2711
2966
|
const serialized = JSON.stringify(certificate), certificateDigest = controlPlaneDigest(certificate);
|
|
2712
|
-
this.#
|
|
2967
|
+
this.#beginMutation();
|
|
2713
2968
|
try {
|
|
2714
2969
|
const previous = this.#database.prepare('SELECT status,certificate_digest FROM source_creation_verifications WHERE plan_id=?')
|
|
2715
2970
|
.get(plan.id);
|
|
@@ -2719,10 +2974,10 @@ export class ControlPlaneStore {
|
|
|
2719
2974
|
else
|
|
2720
2975
|
this.#database.prepare(`UPDATE source_creation_verifications SET status='verified',certificate_json=?,certificate_digest=?,updated_at=?
|
|
2721
2976
|
WHERE plan_id=? AND status='claimed'`).run(serialized, certificateDigest, this.#now(), plan.id);
|
|
2722
|
-
this.#
|
|
2977
|
+
this.#commitMutation();
|
|
2723
2978
|
}
|
|
2724
2979
|
catch (error) {
|
|
2725
|
-
this.#
|
|
2980
|
+
this.#rollbackMutation();
|
|
2726
2981
|
throw error;
|
|
2727
2982
|
}
|
|
2728
2983
|
}
|
|
@@ -2743,6 +2998,11 @@ export class ControlPlaneStore {
|
|
|
2743
2998
|
if (certificate.plan.id !== planId || controlPlaneDigest(certificate) !== row.certificate_digest) {
|
|
2744
2999
|
throw new ControlPlaneStoreError('invalid-state', 'stored creation verification certificate digest changed');
|
|
2745
3000
|
}
|
|
3001
|
+
const plan = this.getSourcePlan(planId);
|
|
3002
|
+
if (certificate.plan.digest !== plan.digest || !plan.preparedEvidence
|
|
3003
|
+
|| !creationVerificationMatchesPreparedEvidence(certificate, plan.preparedEvidence)) {
|
|
3004
|
+
throw new ControlPlaneStoreError('invalid-state', 'stored creation verification certificate does not match exact prepared source');
|
|
3005
|
+
}
|
|
2746
3006
|
return certificate;
|
|
2747
3007
|
}
|
|
2748
3008
|
getCreationVerificationStatus(planId) {
|
|
@@ -2769,7 +3029,7 @@ export class ControlPlaneStore {
|
|
|
2769
3029
|
if (!pinned || this.#now() >= pinned.expiresAt) {
|
|
2770
3030
|
throw new ControlPlaneStoreError('invalid-state', 'creation verification requires a current pre-author policy binding');
|
|
2771
3031
|
}
|
|
2772
|
-
this.#
|
|
3032
|
+
this.#beginMutation();
|
|
2773
3033
|
try {
|
|
2774
3034
|
const count = this.#database.prepare('SELECT count(*) AS n FROM source_creation_verifications').get();
|
|
2775
3035
|
if (count.n >= 256)
|
|
@@ -2778,10 +3038,10 @@ export class ControlPlaneStore {
|
|
|
2778
3038
|
this.#database.prepare(`INSERT INTO source_creation_verifications
|
|
2779
3039
|
(plan_id,status,certificate_json,certificate_digest,reason,created_at,updated_at) VALUES (?,'claimed',NULL,NULL,NULL,?,?)`)
|
|
2780
3040
|
.run(planId, now, now);
|
|
2781
|
-
this.#
|
|
3041
|
+
this.#commitMutation();
|
|
2782
3042
|
}
|
|
2783
3043
|
catch (error) {
|
|
2784
|
-
this.#
|
|
3044
|
+
this.#rollbackMutation();
|
|
2785
3045
|
throw error;
|
|
2786
3046
|
}
|
|
2787
3047
|
}
|
|
@@ -2802,6 +3062,7 @@ export class ControlPlaneStore {
|
|
|
2802
3062
|
const reference = this.getOwnerTaskFailureReference(plan.gapId);
|
|
2803
3063
|
validateCreationCapabilitySource(source, certificate);
|
|
2804
3064
|
if (!reference || controlPlaneDigest(plan.sourceRevision?.parent) !== controlPlaneDigest(certificate.parent) || !plan.sourceCheck || !plan.preparedEvidence || !plan.sourceRevision?.growthRun
|
|
3065
|
+
|| !revisionVerificationMatchesPreparedEvidence(certificate, plan.preparedEvidence)
|
|
2805
3066
|
|| !job.intent.revision?.growthRun || plan.digest !== certificate.plan.digest || plan.name !== certificate.plan.name
|
|
2806
3067
|
|| plan.sourceCheck.treeDigest !== certificate.plan.sourceTreeDigest
|
|
2807
3068
|
|| plan.sourceCheck.patchDigest !== certificate.plan.sourcePatchDigest
|
|
@@ -2823,7 +3084,7 @@ export class ControlPlaneStore {
|
|
|
2823
3084
|
if (Buffer.byteLength(sourceJson) > 2_097_152)
|
|
2824
3085
|
throw new ControlPlaneStoreError('invalid-input', 'revision source archive exceeds bound');
|
|
2825
3086
|
const serialized = JSON.stringify(certificate), certificateDigest = controlPlaneDigest(certificate);
|
|
2826
|
-
this.#
|
|
3087
|
+
this.#beginMutation();
|
|
2827
3088
|
try {
|
|
2828
3089
|
const previous = this.#database.prepare('SELECT status,certificate_digest FROM source_revision_verifications WHERE plan_id=?')
|
|
2829
3090
|
.get(plan.id);
|
|
@@ -2838,10 +3099,10 @@ export class ControlPlaneStore {
|
|
|
2838
3099
|
throw new ControlPlaneStoreError('conflict', 'revision source archive changed');
|
|
2839
3100
|
if (!oldSource)
|
|
2840
3101
|
this.#database.prepare('INSERT INTO source_revision_sources VALUES(?,?)').run(plan.id, sourceJson);
|
|
2841
|
-
this.#
|
|
3102
|
+
this.#commitMutation();
|
|
2842
3103
|
}
|
|
2843
3104
|
catch (error) {
|
|
2844
|
-
this.#
|
|
3105
|
+
this.#rollbackMutation();
|
|
2845
3106
|
throw error;
|
|
2846
3107
|
}
|
|
2847
3108
|
}
|
|
@@ -2862,6 +3123,19 @@ export class ControlPlaneStore {
|
|
|
2862
3123
|
if (certificate.plan.id !== planId || controlPlaneDigest(certificate) !== row.certificate_digest) {
|
|
2863
3124
|
throw new ControlPlaneStoreError('invalid-state', 'stored revision verification certificate digest changed');
|
|
2864
3125
|
}
|
|
3126
|
+
// Check retained immutable metadata without reopening expired build/grant authority.
|
|
3127
|
+
const plan = this.getSourcePlan(planId);
|
|
3128
|
+
if (plan.mode !== 'prepared-revise' || !plan.sourceRevision || !plan.sourceCheck
|
|
3129
|
+
|| certificate.plan.digest !== plan.digest || certificate.plan.name !== plan.name
|
|
3130
|
+
|| certificate.plan.generatorDigest !== plan.generatorDigest
|
|
3131
|
+
|| certificate.plan.sourceTreeDigest !== plan.sourceCheck.treeDigest
|
|
3132
|
+
|| certificate.plan.sourcePatchDigest !== plan.sourceCheck.patchDigest
|
|
3133
|
+
|| controlPlaneDigest(certificate.parent) !== controlPlaneDigest(plan.sourceRevision.parent)
|
|
3134
|
+
|| !plan.preparedEvidence || certificate.plan.artifactSha256 !== plan.preparedEvidence.pack.sha256
|
|
3135
|
+
|| certificate.plan.artifactBytes !== plan.preparedEvidence.pack.sizeBytes
|
|
3136
|
+
|| !revisionVerificationMatchesPreparedEvidence(certificate, plan.preparedEvidence)) {
|
|
3137
|
+
throw new ControlPlaneStoreError('invalid-state', 'stored revision verification certificate does not match exact prepared source');
|
|
3138
|
+
}
|
|
2865
3139
|
return certificate;
|
|
2866
3140
|
}
|
|
2867
3141
|
getRevisionVerificationStatus(planId) {
|
|
@@ -2888,7 +3162,7 @@ export class ControlPlaneStore {
|
|
|
2888
3162
|
if (!pinned || this.#now() >= pinned.expiresAt) {
|
|
2889
3163
|
throw new ControlPlaneStoreError('invalid-state', 'revision verification requires a current pre-author policy binding');
|
|
2890
3164
|
}
|
|
2891
|
-
this.#
|
|
3165
|
+
this.#beginMutation();
|
|
2892
3166
|
try {
|
|
2893
3167
|
const count = this.#database.prepare('SELECT count(*) AS n FROM source_revision_verifications').get();
|
|
2894
3168
|
if (count.n >= 256)
|
|
@@ -2897,10 +3171,10 @@ export class ControlPlaneStore {
|
|
|
2897
3171
|
this.#database.prepare(`INSERT INTO source_revision_verifications
|
|
2898
3172
|
(plan_id,status,certificate_json,certificate_digest,reason,created_at,updated_at) VALUES (?,'claimed',NULL,NULL,NULL,?,?)`)
|
|
2899
3173
|
.run(planId, now, now);
|
|
2900
|
-
this.#
|
|
3174
|
+
this.#commitMutation();
|
|
2901
3175
|
}
|
|
2902
3176
|
catch (error) {
|
|
2903
|
-
this.#
|
|
3177
|
+
this.#rollbackMutation();
|
|
2904
3178
|
throw error;
|
|
2905
3179
|
}
|
|
2906
3180
|
}
|
|
@@ -2926,6 +3200,7 @@ export class ControlPlaneStore {
|
|
|
2926
3200
|
const previousJobs = new Set([...(parent?.observations ?? []), ...(candidate?.observations ?? [])].map(item => item.jobId));
|
|
2927
3201
|
if (certificate.verifiedAt > this.#now() || certificate.expiresAt <= this.#now()
|
|
2928
3202
|
|| !candidate || !parent || !source || !reference || !run?.revisionRegressionAcceptance
|
|
3203
|
+
|| !revisionRegressionMatchesCandidate(certificate, candidate, parent)
|
|
2929
3204
|
|| !job.intent.revision?.growthRun.revisionRegressionAcceptance
|
|
2930
3205
|
|| controlPlaneDigest(certificate.plan) !== controlPlaneDigest(candidate.plan)
|
|
2931
3206
|
|| controlPlaneDigest(certificate.source) !== controlPlaneDigest(candidate.source)
|
|
@@ -2951,7 +3226,7 @@ export class ControlPlaneStore {
|
|
|
2951
3226
|
}
|
|
2952
3227
|
this.readPreparedSourceArtifact(plan.id);
|
|
2953
3228
|
const serialized = JSON.stringify(certificate), digest = controlPlaneDigest(certificate);
|
|
2954
|
-
this.#
|
|
3229
|
+
this.#beginMutation();
|
|
2955
3230
|
try {
|
|
2956
3231
|
const old = this.#database.prepare('SELECT status,certificate_digest FROM source_revision_regressions WHERE plan_id=?')
|
|
2957
3232
|
.get(plan.id);
|
|
@@ -2961,10 +3236,10 @@ export class ControlPlaneStore {
|
|
|
2961
3236
|
else
|
|
2962
3237
|
this.#database.prepare(`UPDATE source_revision_regressions SET status='verified',certificate_json=?,certificate_digest=?,updated_at=?
|
|
2963
3238
|
WHERE plan_id=? AND status='claimed'`).run(serialized, digest, this.#now(), plan.id);
|
|
2964
|
-
this.#
|
|
3239
|
+
this.#commitMutation();
|
|
2965
3240
|
}
|
|
2966
3241
|
catch (error) {
|
|
2967
|
-
this.#
|
|
3242
|
+
this.#rollbackMutation();
|
|
2968
3243
|
throw error;
|
|
2969
3244
|
}
|
|
2970
3245
|
}
|
|
@@ -2985,6 +3260,10 @@ export class ControlPlaneStore {
|
|
|
2985
3260
|
if (certificate.plan.id !== planId || controlPlaneDigest(certificate) !== row.certificate_digest) {
|
|
2986
3261
|
throw new ControlPlaneStoreError('invalid-state', 'stored revision regression certificate digest changed');
|
|
2987
3262
|
}
|
|
3263
|
+
const candidate = this.getRevisionVerification(planId), parent = this.getCreationVerification(certificate.parent.planId);
|
|
3264
|
+
if (!candidate || !parent || !revisionRegressionMatchesCandidate(certificate, candidate, parent)) {
|
|
3265
|
+
throw new ControlPlaneStoreError('invalid-state', 'stored revision regression certificate does not match exact candidate execution');
|
|
3266
|
+
}
|
|
2988
3267
|
return certificate;
|
|
2989
3268
|
}
|
|
2990
3269
|
getRevisionRegressionStatus(planId) {
|
|
@@ -3011,7 +3290,7 @@ export class ControlPlaneStore {
|
|
|
3011
3290
|
if (!pinned || this.#now() >= pinned.expiresAt) {
|
|
3012
3291
|
throw new ControlPlaneStoreError('invalid-state', 'revision regression requires a current pre-author policy binding');
|
|
3013
3292
|
}
|
|
3014
|
-
this.#
|
|
3293
|
+
this.#beginMutation();
|
|
3015
3294
|
try {
|
|
3016
3295
|
const count = this.#database.prepare('SELECT count(*) AS n FROM source_revision_regressions').get();
|
|
3017
3296
|
if (count.n >= 256)
|
|
@@ -3020,10 +3299,10 @@ export class ControlPlaneStore {
|
|
|
3020
3299
|
this.#database.prepare(`INSERT INTO source_revision_regressions
|
|
3021
3300
|
(plan_id,status,certificate_json,certificate_digest,reason,created_at,updated_at) VALUES (?,'claimed',NULL,NULL,NULL,?,?)`)
|
|
3022
3301
|
.run(planId, now, now);
|
|
3023
|
-
this.#
|
|
3302
|
+
this.#commitMutation();
|
|
3024
3303
|
}
|
|
3025
3304
|
catch (error) {
|
|
3026
|
-
this.#
|
|
3305
|
+
this.#rollbackMutation();
|
|
3027
3306
|
throw error;
|
|
3028
3307
|
}
|
|
3029
3308
|
}
|
|
@@ -3054,7 +3333,7 @@ export class ControlPlaneStore {
|
|
|
3054
3333
|
deleteExpiredPreparedSourceArtifacts(now) {
|
|
3055
3334
|
if (!Number.isSafeInteger(now) || now < 0)
|
|
3056
3335
|
throw new ControlPlaneStoreError('invalid-input', 'artifact expiry time is invalid');
|
|
3057
|
-
this.#
|
|
3336
|
+
this.#beginMutation();
|
|
3058
3337
|
try {
|
|
3059
3338
|
const references = this.#database.prepare('SELECT plan_id, pack_sha256 FROM source_prepared_artifact_refs')
|
|
3060
3339
|
.all();
|
|
@@ -3076,11 +3355,11 @@ export class ControlPlaneStore {
|
|
|
3076
3355
|
.run(artifact.pack_sha256);
|
|
3077
3356
|
deleted += Number(result.changes);
|
|
3078
3357
|
}
|
|
3079
|
-
this.#
|
|
3358
|
+
this.#commitMutation();
|
|
3080
3359
|
return deleted;
|
|
3081
3360
|
}
|
|
3082
3361
|
catch (error) {
|
|
3083
|
-
this.#
|
|
3362
|
+
this.#rollbackMutation();
|
|
3084
3363
|
throw error;
|
|
3085
3364
|
}
|
|
3086
3365
|
}
|
|
@@ -3341,7 +3620,7 @@ export class ControlPlaneStore {
|
|
|
3341
3620
|
throw new ControlPlaneStoreError('invalid-state', 'expired source receipt is missing');
|
|
3342
3621
|
return receipt.result;
|
|
3343
3622
|
}
|
|
3344
|
-
this.#
|
|
3623
|
+
this.#beginMutation();
|
|
3345
3624
|
try {
|
|
3346
3625
|
const retired = this.#database.prepare(`UPDATE source_plans SET status = 'expired', revision = revision + 1, updated_at = ?
|
|
3347
3626
|
WHERE id = ? AND revision = ? AND ((mode = 'modify' AND status IN ('pending-approval', 'approved'))
|
|
@@ -3358,11 +3637,11 @@ export class ControlPlaneStore {
|
|
|
3358
3637
|
const result = this.getSourcePlan(plan.id);
|
|
3359
3638
|
this.#insertReceipt({ idempotencyKey: `source-expired:${plan.id}`, operation: 'source-expired',
|
|
3360
3639
|
inputDigest: expiryDigest, result, createdAt: input.now });
|
|
3361
|
-
this.#
|
|
3640
|
+
this.#commitMutation();
|
|
3362
3641
|
return result;
|
|
3363
3642
|
}
|
|
3364
3643
|
catch (error) {
|
|
3365
|
-
this.#
|
|
3644
|
+
this.#rollbackMutation();
|
|
3366
3645
|
throw error;
|
|
3367
3646
|
}
|
|
3368
3647
|
}
|
|
@@ -3420,7 +3699,7 @@ export class ControlPlaneStore {
|
|
|
3420
3699
|
throw new ControlPlaneStoreError(verified.decision === 'approved' ? 'expired' : 'invalid-state', 'plan approval is not currently applicable');
|
|
3421
3700
|
const table = kind === 'activation' ? 'activation_plans' : 'source_plans';
|
|
3422
3701
|
const now = this.#now();
|
|
3423
|
-
this.#
|
|
3702
|
+
this.#beginMutation();
|
|
3424
3703
|
try {
|
|
3425
3704
|
const statement = this.#database.prepare(`UPDATE ${table} SET status = 'approved', revision = revision + 1, approval_json = ?,
|
|
3426
3705
|
${kind === 'activation' ? 'approval_receipt_json = ?,' : ''} updated_at = ?
|
|
@@ -3433,11 +3712,11 @@ export class ControlPlaneStore {
|
|
|
3433
3712
|
const output = kind === 'activation' ? this.getPlan(plan.id) : this.getSourcePlan(plan.id);
|
|
3434
3713
|
const receipt = { idempotencyKey: input.idempotencyKey, operation: 'approve-plan', inputDigest, result: output, createdAt: now };
|
|
3435
3714
|
this.#insertReceipt(receipt);
|
|
3436
|
-
this.#
|
|
3715
|
+
this.#commitMutation();
|
|
3437
3716
|
return receipt;
|
|
3438
3717
|
}
|
|
3439
3718
|
catch (error) {
|
|
3440
|
-
this.#
|
|
3719
|
+
this.#rollbackMutation();
|
|
3441
3720
|
throw error;
|
|
3442
3721
|
}
|
|
3443
3722
|
});
|
|
@@ -3448,7 +3727,7 @@ export class ControlPlaneStore {
|
|
|
3448
3727
|
if (input.leaseMs < 5_000 || input.leaseMs > 300_000)
|
|
3449
3728
|
throw new ControlPlaneStoreError('invalid-input', 'leaseMs is invalid');
|
|
3450
3729
|
this.assertOwnerActivationSource(input.planId);
|
|
3451
|
-
this.#
|
|
3730
|
+
this.#beginMutation();
|
|
3452
3731
|
try {
|
|
3453
3732
|
const row = this.#database.prepare('SELECT * FROM activation_plans WHERE id = ?').get(input.planId);
|
|
3454
3733
|
if (row === undefined)
|
|
@@ -3493,12 +3772,12 @@ export class ControlPlaneStore {
|
|
|
3493
3772
|
if (Number(result.changes) !== 1)
|
|
3494
3773
|
throw new ControlPlaneStoreError('conflict', 'activation plan changed while being claimed');
|
|
3495
3774
|
const claimed = this.getPlan(plan.id);
|
|
3496
|
-
this.#
|
|
3775
|
+
this.#commitMutation();
|
|
3497
3776
|
return claimed;
|
|
3498
3777
|
});
|
|
3499
3778
|
}
|
|
3500
3779
|
catch (error) {
|
|
3501
|
-
this.#
|
|
3780
|
+
this.#rollbackMutation();
|
|
3502
3781
|
throw error;
|
|
3503
3782
|
}
|
|
3504
3783
|
}
|
|
@@ -3516,7 +3795,7 @@ export class ControlPlaneStore {
|
|
|
3516
3795
|
recordActivationTargetBaseline(input) {
|
|
3517
3796
|
return this.#withActivationSource(input.planId, () => {
|
|
3518
3797
|
const now = this.#now();
|
|
3519
|
-
this.#
|
|
3798
|
+
this.#beginMutation();
|
|
3520
3799
|
try {
|
|
3521
3800
|
this.assertOwnerActivationSource(input.planId);
|
|
3522
3801
|
const row = this.#database.prepare('SELECT * FROM activation_plans WHERE id = ?').get(input.planId);
|
|
@@ -3540,11 +3819,11 @@ export class ControlPlaneStore {
|
|
|
3540
3819
|
WHERE id = ? AND revision = ? AND activation_fence = ? AND activation_target_existed IS NULL AND activation_target_baseline_json IS NULL`).run(input.existed ? 1 : 0, baseline, now, plan.id, plan.revision, input.fence);
|
|
3541
3820
|
}
|
|
3542
3821
|
const result = this.getPlan(plan.id);
|
|
3543
|
-
this.#
|
|
3822
|
+
this.#commitMutation();
|
|
3544
3823
|
return result;
|
|
3545
3824
|
}
|
|
3546
3825
|
catch (error) {
|
|
3547
|
-
this.#
|
|
3826
|
+
this.#rollbackMutation();
|
|
3548
3827
|
throw error;
|
|
3549
3828
|
}
|
|
3550
3829
|
});
|
|
@@ -3558,7 +3837,7 @@ export class ControlPlaneStore {
|
|
|
3558
3837
|
recordActivationInstalledBaseline(input) {
|
|
3559
3838
|
return this.#withActivationSource(input.planId, () => {
|
|
3560
3839
|
const now = this.#now();
|
|
3561
|
-
this.#
|
|
3840
|
+
this.#beginMutation();
|
|
3562
3841
|
try {
|
|
3563
3842
|
this.assertOwnerActivationSource(input.planId);
|
|
3564
3843
|
const row = this.#database.prepare('SELECT * FROM activation_plans WHERE id = ?').get(input.planId);
|
|
@@ -3599,11 +3878,11 @@ export class ControlPlaneStore {
|
|
|
3599
3878
|
successful_order, recorded_at, succeeded_at) VALUES (?, ?, ?, NULL, ?, NULL)`).run(plan.id, baseline, sequence.next_exposure_order, now);
|
|
3600
3879
|
}
|
|
3601
3880
|
const output = this.getPlan(plan.id);
|
|
3602
|
-
this.#
|
|
3881
|
+
this.#commitMutation();
|
|
3603
3882
|
return output;
|
|
3604
3883
|
}
|
|
3605
3884
|
catch (error) {
|
|
3606
|
-
this.#
|
|
3885
|
+
this.#rollbackMutation();
|
|
3607
3886
|
throw error;
|
|
3608
3887
|
}
|
|
3609
3888
|
});
|
|
@@ -3626,7 +3905,7 @@ export class ControlPlaneStore {
|
|
|
3626
3905
|
/** Move a signed closed deployment watch into the existing physical rollback lifecycle. */
|
|
3627
3906
|
beginPostActivationRollback(input) {
|
|
3628
3907
|
const now = this.#now();
|
|
3629
|
-
this.#
|
|
3908
|
+
this.#beginMutation();
|
|
3630
3909
|
try {
|
|
3631
3910
|
const row = this.#database.prepare('SELECT * FROM activation_plans WHERE id = ?').get(input.planId);
|
|
3632
3911
|
if (row === undefined)
|
|
@@ -3637,7 +3916,7 @@ export class ControlPlaneStore {
|
|
|
3637
3916
|
// A restart or retry must retain the original rollback identity and never
|
|
3638
3917
|
// manufacture another rollback from a now-closed watch.
|
|
3639
3918
|
if ((plan.status === 'rollback-pending' || plan.status === 'rolled-back') && this.#hasPostActivationRollbackProvenance(plan)) {
|
|
3640
|
-
this.#
|
|
3919
|
+
this.#commitMutation();
|
|
3641
3920
|
return plan;
|
|
3642
3921
|
}
|
|
3643
3922
|
if (plan.status !== 'activated' || plan.activation === undefined) {
|
|
@@ -3675,11 +3954,11 @@ export class ControlPlaneStore {
|
|
|
3675
3954
|
if (Number(updated.changes) !== 1)
|
|
3676
3955
|
throw new ControlPlaneStoreError('conflict', 'post-activation rollback lost its activation CAS');
|
|
3677
3956
|
const output = this.getPlan(plan.id);
|
|
3678
|
-
this.#
|
|
3957
|
+
this.#commitMutation();
|
|
3679
3958
|
return output;
|
|
3680
3959
|
}
|
|
3681
3960
|
catch (error) {
|
|
3682
|
-
this.#
|
|
3961
|
+
this.#rollbackMutation();
|
|
3683
3962
|
throw error;
|
|
3684
3963
|
}
|
|
3685
3964
|
}
|
|
@@ -3800,7 +4079,7 @@ export class ControlPlaneStore {
|
|
|
3800
4079
|
async withActivationFileSystemGuard(input, action) {
|
|
3801
4080
|
positiveInteger(input.leaseMs, 'leaseMs');
|
|
3802
4081
|
try {
|
|
3803
|
-
this.#
|
|
4082
|
+
this.#beginMutation();
|
|
3804
4083
|
}
|
|
3805
4084
|
catch {
|
|
3806
4085
|
throw new ControlPlaneStoreError('conflict', 'activation filesystem mutex is held');
|
|
@@ -3825,30 +4104,30 @@ export class ControlPlaneStore {
|
|
|
3825
4104
|
assertOwner(false);
|
|
3826
4105
|
const finished = this.#now();
|
|
3827
4106
|
this.#database.prepare('UPDATE activation_plans SET activation_lease_until = ?, updated_at = ? WHERE id = ?').run(finished + input.leaseMs, finished, input.planId);
|
|
3828
|
-
this.#
|
|
4107
|
+
this.#commitMutation();
|
|
3829
4108
|
return result;
|
|
3830
4109
|
});
|
|
3831
4110
|
}
|
|
3832
4111
|
catch (error) {
|
|
3833
|
-
this.#
|
|
4112
|
+
this.#rollbackMutation();
|
|
3834
4113
|
throw error;
|
|
3835
4114
|
}
|
|
3836
4115
|
}
|
|
3837
4116
|
/** Serialize lock-file lifecycle operations even after a plan became terminal. */
|
|
3838
4117
|
async withExclusiveWrite(action) {
|
|
3839
4118
|
try {
|
|
3840
|
-
this.#
|
|
4119
|
+
this.#beginMutation();
|
|
3841
4120
|
}
|
|
3842
4121
|
catch {
|
|
3843
4122
|
throw new ControlPlaneStoreError('conflict', 'control-plane mutex is held');
|
|
3844
4123
|
}
|
|
3845
4124
|
try {
|
|
3846
4125
|
const result = await action();
|
|
3847
|
-
this.#
|
|
4126
|
+
this.#commitMutation();
|
|
3848
4127
|
return result;
|
|
3849
4128
|
}
|
|
3850
4129
|
catch (error) {
|
|
3851
|
-
this.#
|
|
4130
|
+
this.#rollbackMutation();
|
|
3852
4131
|
throw error;
|
|
3853
4132
|
}
|
|
3854
4133
|
}
|
|
@@ -3987,7 +4266,7 @@ export class ControlPlaneStore {
|
|
|
3987
4266
|
ORDER BY epoch.created_at,epoch.operation_id LIMIT 100`).all(coordinatorId, this.#now()).map(runtimeEpochFromRow);
|
|
3988
4267
|
}
|
|
3989
4268
|
claimRuntimeEpoch(operationId, coordinatorId) {
|
|
3990
|
-
this.#
|
|
4269
|
+
this.#beginMutation();
|
|
3991
4270
|
try {
|
|
3992
4271
|
const record = this.getRuntimeEpoch(operationId);
|
|
3993
4272
|
if (!['pending', 'claimed'].includes(record.status) || this.#now() >= record.request.requestedAt + record.request.receiptTtlMs) {
|
|
@@ -3998,17 +4277,17 @@ export class ControlPlaneStore {
|
|
|
3998
4277
|
if (context.handoff.coordinatorId !== coordinatorId || (this.#adoptionCoordinatorId !== undefined && this.#adoptionCoordinatorId !== coordinatorId)) {
|
|
3999
4278
|
throw new ControlPlaneStoreError('conflict', 'runtime epoch coordinator changed');
|
|
4000
4279
|
}
|
|
4001
|
-
this.#
|
|
4280
|
+
this.#commitMutation();
|
|
4002
4281
|
return { ...record, status: 'claimed' };
|
|
4003
4282
|
}
|
|
4004
4283
|
catch (error) {
|
|
4005
|
-
this.#
|
|
4284
|
+
this.#rollbackMutation();
|
|
4006
4285
|
throw error;
|
|
4007
4286
|
}
|
|
4008
4287
|
}
|
|
4009
4288
|
applyRuntimeEpoch(receiptInput, trust) {
|
|
4010
4289
|
const receipt = parseRuntimeEpochReceipt(receiptInput);
|
|
4011
|
-
this.#
|
|
4290
|
+
this.#beginMutation();
|
|
4012
4291
|
try {
|
|
4013
4292
|
const context = readOwnerRuntimeEpochContext(this.#database, receipt.operationId);
|
|
4014
4293
|
if (this.#now() >= context.request.requestedAt + context.request.receiptTtlMs)
|
|
@@ -4022,18 +4301,18 @@ export class ControlPlaneStore {
|
|
|
4022
4301
|
this.#database.prepare("UPDATE deployment_runtime_epochs SET status='applied',receipt_json=? WHERE operation_id=? AND status='claimed'")
|
|
4023
4302
|
.run(JSON.stringify(receipt), receipt.operationId);
|
|
4024
4303
|
const result = this.getRuntimeEpoch(receipt.operationId);
|
|
4025
|
-
this.#
|
|
4304
|
+
this.#commitMutation();
|
|
4026
4305
|
return result;
|
|
4027
4306
|
}
|
|
4028
4307
|
catch (error) {
|
|
4029
|
-
this.#
|
|
4308
|
+
this.#rollbackMutation();
|
|
4030
4309
|
throw error;
|
|
4031
4310
|
}
|
|
4032
4311
|
}
|
|
4033
4312
|
prepareHostAttestationOperation(input) {
|
|
4034
4313
|
return this.#withActivationSource(input.planId, () => {
|
|
4035
4314
|
const now = this.#now();
|
|
4036
|
-
this.#
|
|
4315
|
+
this.#beginMutation();
|
|
4037
4316
|
try {
|
|
4038
4317
|
this.assertOwnerActivationSource(input.planId);
|
|
4039
4318
|
const plan = this.getPlan(input.planId);
|
|
@@ -4085,18 +4364,18 @@ export class ControlPlaneStore {
|
|
|
4085
4364
|
}
|
|
4086
4365
|
if (prior.bindingDigest !== bindingDigest)
|
|
4087
4366
|
throw new ControlPlaneStoreError('conflict', 'durable Host operation payload changed for the same phase');
|
|
4088
|
-
this.#
|
|
4367
|
+
this.#commitMutation();
|
|
4089
4368
|
return prior;
|
|
4090
4369
|
}
|
|
4091
4370
|
this.#database.prepare(`INSERT INTO host_attestation_operations (plan_id, phase, operation_id, binding_digest,
|
|
4092
4371
|
request_digest, request_json, status, receipt_digest, receipt_json, created_at, completed_at, applied_at)
|
|
4093
4372
|
VALUES (?, ?, ?, ?, ?, ?, 'pending', NULL, NULL, ?, NULL, NULL)`).run(plan.id, expected.phase, operationId, bindingDigest, controlPlaneDigest(request), JSON.stringify(request), now);
|
|
4094
4373
|
const operation = this.getHostAttestationOperation(operationId);
|
|
4095
|
-
this.#
|
|
4374
|
+
this.#commitMutation();
|
|
4096
4375
|
return operation;
|
|
4097
4376
|
}
|
|
4098
4377
|
catch (error) {
|
|
4099
|
-
this.#
|
|
4378
|
+
this.#rollbackMutation();
|
|
4100
4379
|
throw error;
|
|
4101
4380
|
}
|
|
4102
4381
|
});
|
|
@@ -4143,7 +4422,7 @@ export class ControlPlaneStore {
|
|
|
4143
4422
|
if (operation.phase === 'rollback' && input.receipt.outcome !== 'passed')
|
|
4144
4423
|
throw new ControlPlaneStoreError('conflict', 'failed physical rollback receipt cannot settle recovery');
|
|
4145
4424
|
const commit = () => {
|
|
4146
|
-
this.#
|
|
4425
|
+
this.#beginMutation();
|
|
4147
4426
|
try {
|
|
4148
4427
|
if (input.requireCurrentSource)
|
|
4149
4428
|
this.assertOwnerActivationSource(plan.id);
|
|
@@ -4157,7 +4436,7 @@ export class ControlPlaneStore {
|
|
|
4157
4436
|
if (controlPlaneDigest(currentOperation.receipt) !== controlPlaneDigest(input.receipt)) {
|
|
4158
4437
|
throw new ControlPlaneStoreError('conflict', 'Host attestation receipt changed after completion');
|
|
4159
4438
|
}
|
|
4160
|
-
this.#
|
|
4439
|
+
this.#commitMutation();
|
|
4161
4440
|
return currentOperation.receipt;
|
|
4162
4441
|
}
|
|
4163
4442
|
const dispatch = this.getHostAttestationDispatchStatus(operation.operationId);
|
|
@@ -4172,11 +4451,11 @@ export class ControlPlaneStore {
|
|
|
4172
4451
|
if (Number(completed.changes) !== 1)
|
|
4173
4452
|
throw new ControlPlaneStoreError('conflict', 'Host attestation dispatch completion lost its claim');
|
|
4174
4453
|
}
|
|
4175
|
-
this.#
|
|
4454
|
+
this.#commitMutation();
|
|
4176
4455
|
return input.receipt;
|
|
4177
4456
|
}
|
|
4178
4457
|
catch (error) {
|
|
4179
|
-
this.#
|
|
4458
|
+
this.#rollbackMutation();
|
|
4180
4459
|
throw error;
|
|
4181
4460
|
}
|
|
4182
4461
|
};
|
|
@@ -4228,7 +4507,7 @@ export class ControlPlaneStore {
|
|
|
4228
4507
|
}
|
|
4229
4508
|
putLiveQualification(batch) {
|
|
4230
4509
|
assertLiveQualificationBatch(batch);
|
|
4231
|
-
this.#
|
|
4510
|
+
this.#beginMutation();
|
|
4232
4511
|
try {
|
|
4233
4512
|
const plan = this.getPlan(batch.planId);
|
|
4234
4513
|
if (plan.status !== 'awaiting-live-tasks' || this.#now() >= batch.deadlineAt || this.#now() >= batch.expiresAt)
|
|
@@ -4242,10 +4521,10 @@ export class ControlPlaneStore {
|
|
|
4242
4521
|
this.#database.prepare(`INSERT INTO live_qualification_batches
|
|
4243
4522
|
(id,lane,plan_id,batch_json,batch_digest,state,created_at) VALUES (?,?,?,?,?,'pending',?)`).run(batch.id, batch.lane, batch.planId, JSON.stringify(batch), controlPlaneDigest(batch), this.#now());
|
|
4244
4523
|
readLiveQualificationContext(this.#database, batch.id);
|
|
4245
|
-
this.#
|
|
4524
|
+
this.#commitMutation();
|
|
4246
4525
|
}
|
|
4247
4526
|
catch (error) {
|
|
4248
|
-
this.#
|
|
4527
|
+
this.#rollbackMutation();
|
|
4249
4528
|
throw error;
|
|
4250
4529
|
}
|
|
4251
4530
|
}
|
|
@@ -4258,13 +4537,13 @@ export class ControlPlaneStore {
|
|
|
4258
4537
|
throw new ControlPlaneStoreError('not-found', 'live qualification batch absent');
|
|
4259
4538
|
const receipt = verifyLiveQualificationReceipt(input.receipt, record.batch, input.publicKeyPem, this.#now());
|
|
4260
4539
|
return input.withSourceFence(() => {
|
|
4261
|
-
this.#
|
|
4540
|
+
this.#beginMutation();
|
|
4262
4541
|
try {
|
|
4263
4542
|
const current = this.getLiveQualification(record.batch.id);
|
|
4264
4543
|
if (current.state === 'applied') {
|
|
4265
4544
|
if (controlPlaneDigest(current.receipt) !== controlPlaneDigest(receipt))
|
|
4266
4545
|
throw new ControlPlaneStoreError('conflict', 'qualification receipt changed');
|
|
4267
|
-
this.#
|
|
4546
|
+
this.#commitMutation();
|
|
4268
4547
|
return this.getPlan(record.batch.planId);
|
|
4269
4548
|
}
|
|
4270
4549
|
this.assertCurrentLiveQualification(record.batch.id);
|
|
@@ -4277,11 +4556,11 @@ export class ControlPlaneStore {
|
|
|
4277
4556
|
throw new ControlPlaneStoreError('conflict', 'qualification lost plan CAS');
|
|
4278
4557
|
this.#database.prepare("UPDATE live_qualification_batches SET state='applied',receipt_json=?,receipt_digest=? WHERE id=? AND state IN ('pending','signed')")
|
|
4279
4558
|
.run(JSON.stringify(receipt), controlPlaneDigest(receipt), record.batch.id);
|
|
4280
|
-
this.#
|
|
4559
|
+
this.#commitMutation();
|
|
4281
4560
|
return this.getPlan(plan.id);
|
|
4282
4561
|
}
|
|
4283
4562
|
catch (error) {
|
|
4284
|
-
this.#
|
|
4563
|
+
this.#rollbackMutation();
|
|
4285
4564
|
throw error;
|
|
4286
4565
|
}
|
|
4287
4566
|
});
|
|
@@ -4289,7 +4568,7 @@ export class ControlPlaneStore {
|
|
|
4289
4568
|
/** Negative-only Host seam. The signed terms permit conservative removal of unconfirmable qualification. */
|
|
4290
4569
|
invalidateLiveQualification(input) {
|
|
4291
4570
|
const reason = bounded(input.reason, 'qualification invalidation reason', 160);
|
|
4292
|
-
this.#
|
|
4571
|
+
this.#beginMutation();
|
|
4293
4572
|
try {
|
|
4294
4573
|
const plan = this.getPlan(input.planId), record = this.getLiveQualification(input.batchId);
|
|
4295
4574
|
if (!record || record.batch.planId !== plan.id || record.state !== 'applied' || record.receipt?.disposition !== 'qualified'
|
|
@@ -4297,7 +4576,7 @@ export class ControlPlaneStore {
|
|
|
4297
4576
|
throw new ControlPlaneStoreError('conflict', 'qualification invalidation lacks exact accepted evidence');
|
|
4298
4577
|
}
|
|
4299
4578
|
if (plan.status === 'rollback-pending' || plan.status === 'rolled-back') {
|
|
4300
|
-
this.#
|
|
4579
|
+
this.#commitMutation();
|
|
4301
4580
|
return plan;
|
|
4302
4581
|
}
|
|
4303
4582
|
if (!['commit-pending', 'activated'].includes(plan.status) || record.batch.fence !== plan.activation.fence)
|
|
@@ -4321,11 +4600,11 @@ export class ControlPlaneStore {
|
|
|
4321
4600
|
this.#database.prepare(`UPDATE activation_plans SET status='rollback-pending',revision=revision+1,
|
|
4322
4601
|
activation_lease_until=NULL,rollback_profile_restored=0,failure_code='live-qualification-invalidated',updated_at=?
|
|
4323
4602
|
WHERE id=? AND revision=? AND activation_fence=?`).run(now, plan.id, plan.revision, plan.activation.fence);
|
|
4324
|
-
this.#
|
|
4603
|
+
this.#commitMutation();
|
|
4325
4604
|
return this.getPlan(plan.id);
|
|
4326
4605
|
}
|
|
4327
4606
|
catch (error) {
|
|
4328
|
-
this.#
|
|
4607
|
+
this.#rollbackMutation();
|
|
4329
4608
|
throw error;
|
|
4330
4609
|
}
|
|
4331
4610
|
}
|
|
@@ -4343,7 +4622,7 @@ export class ControlPlaneStore {
|
|
|
4343
4622
|
/** Same synchronous transaction owns current-profile selection and capture. */
|
|
4344
4623
|
withForegroundDeployment(task, profilePath, callback) {
|
|
4345
4624
|
assertForegroundTask(task);
|
|
4346
|
-
this.#
|
|
4625
|
+
this.#beginMutation();
|
|
4347
4626
|
try {
|
|
4348
4627
|
const active = this.#database.prepare(`SELECT id FROM activation_plans WHERE target_path = ? AND status IN (
|
|
4349
4628
|
'staging', 'awaiting-reload', 'awaiting-readiness', 'awaiting-live-tasks', 'awaiting-effect-blocked-replay', 'awaiting-shadow',
|
|
@@ -4387,11 +4666,11 @@ export class ControlPlaneStore {
|
|
|
4387
4666
|
const result = callback(plan, hostOperationFromRow(row));
|
|
4388
4667
|
if (result && (typeof result === 'object' || typeof result === 'function') && 'then' in result)
|
|
4389
4668
|
throw new ControlPlaneStoreError('invalid-input', 'foreground capture must be synchronous');
|
|
4390
|
-
this.#
|
|
4669
|
+
this.#commitMutation();
|
|
4391
4670
|
return result;
|
|
4392
4671
|
}
|
|
4393
4672
|
catch (error) {
|
|
4394
|
-
this.#
|
|
4673
|
+
this.#rollbackMutation();
|
|
4395
4674
|
throw error;
|
|
4396
4675
|
}
|
|
4397
4676
|
finally {
|
|
@@ -4458,7 +4737,7 @@ export class ControlPlaneStore {
|
|
|
4458
4737
|
}
|
|
4459
4738
|
putTaskObservation(batch) {
|
|
4460
4739
|
assertTaskObservationBatch(batch);
|
|
4461
|
-
this.#
|
|
4740
|
+
this.#beginMutation();
|
|
4462
4741
|
try {
|
|
4463
4742
|
if (batch.createdAt > this.#now() || batch.expiresAt <= this.#now())
|
|
4464
4743
|
throw new ControlPlaneStoreError('expired', 'task observation is outside its validity interval');
|
|
@@ -4466,22 +4745,39 @@ export class ControlPlaneStore {
|
|
|
4466
4745
|
if (prior) {
|
|
4467
4746
|
if (prior.batch.digest !== batch.digest)
|
|
4468
4747
|
throw new ControlPlaneStoreError('conflict', 'task observation identity reused');
|
|
4469
|
-
this.#
|
|
4748
|
+
this.#commitMutation();
|
|
4470
4749
|
return prior;
|
|
4471
4750
|
}
|
|
4472
4751
|
const encoded = JSON.stringify(batch);
|
|
4473
4752
|
this.#database.prepare('INSERT INTO task_observation_batches (id,lane,plan_id,batch_json,batch_digest,state,created_at) VALUES (?,?,?,?,?,?,?)').run(batch.id, batch.lane, batch.planId, encoded, controlPlaneDigest(batch), 'pending', batch.createdAt);
|
|
4474
4753
|
readTaskObservationContext(this.#database, batch.id);
|
|
4475
|
-
this.#
|
|
4754
|
+
this.#commitMutation();
|
|
4476
4755
|
return Object.freeze({ batch, state: 'pending' });
|
|
4477
4756
|
}
|
|
4478
4757
|
catch (error) {
|
|
4479
|
-
this.#
|
|
4758
|
+
this.#rollbackMutation();
|
|
4480
4759
|
throw error;
|
|
4481
4760
|
}
|
|
4482
4761
|
}
|
|
4483
4762
|
getTaskObservation(id) { return getTaskObservationRecord(this.#database, id); }
|
|
4484
4763
|
assertCurrentTaskObservation(id) { readTaskObservationContext(this.#database, id); }
|
|
4764
|
+
/** Seal one observational trend result into the append-only snapshot ledger; same trend, same row. */
|
|
4765
|
+
sealCreatedCapabilityTrendSnapshot(trend) {
|
|
4766
|
+
const snapshot = createCreationCapabilityTrendSnapshot(trend);
|
|
4767
|
+
this.#beginMutation();
|
|
4768
|
+
try {
|
|
4769
|
+
const result = sealCreationCapabilityTrendSnapshot(this.#database, snapshot, this.#now());
|
|
4770
|
+
this.#commitMutation();
|
|
4771
|
+
return result;
|
|
4772
|
+
}
|
|
4773
|
+
catch (error) {
|
|
4774
|
+
this.#rollbackMutation();
|
|
4775
|
+
throw error;
|
|
4776
|
+
}
|
|
4777
|
+
}
|
|
4778
|
+
listCreatedCapabilityTrendSnapshots(query = {}) {
|
|
4779
|
+
return listCreationCapabilityTrendSnapshots(this.#database, query);
|
|
4780
|
+
}
|
|
4485
4781
|
listTaskObservations(lane, limit = 100) {
|
|
4486
4782
|
if (!KEY.test(lane) || !Number.isSafeInteger(limit) || limit < 1 || limit > 1000)
|
|
4487
4783
|
throw new ControlPlaneStoreError('invalid-input', 'task observation list is invalid');
|
|
@@ -4640,7 +4936,7 @@ export class ControlPlaneStore {
|
|
|
4640
4936
|
const claim = () => {
|
|
4641
4937
|
const candidate = this.getHostAttestationOperation(input.operationId);
|
|
4642
4938
|
return this.#withActivationSource(candidate.planId, () => {
|
|
4643
|
-
this.#
|
|
4939
|
+
this.#beginMutation();
|
|
4644
4940
|
try {
|
|
4645
4941
|
this.assertOwnerActivationSource(candidate.planId);
|
|
4646
4942
|
const operation = this.getHostAttestationOperation(input.operationId), plan = this.getPlan(operation.planId), expected = expectedAttestation[plan.status];
|
|
@@ -4648,17 +4944,17 @@ export class ControlPlaneStore {
|
|
|
4648
4944
|
throw new ControlPlaneStoreError('conflict', 'Host attestation operation lost its plan revision/fence/phase');
|
|
4649
4945
|
const request = this.#assertHostAttestationChain(operation, plan);
|
|
4650
4946
|
if (operation.receipt) {
|
|
4651
|
-
this.#
|
|
4947
|
+
this.#commitMutation();
|
|
4652
4948
|
return { operation, plan, request };
|
|
4653
4949
|
}
|
|
4654
4950
|
if (this.getHostAttestationDispatchStatus(operation.operationId) !== undefined)
|
|
4655
4951
|
throw new ControlPlaneStoreError('conflict', 'Host attestation outcome is unknown; receipt reconciliation is required');
|
|
4656
4952
|
this.#database.prepare("INSERT INTO host_attestation_dispatches (operation_id,status,claimed_at,completed_at) VALUES (?,'claimed',?,NULL)").run(operation.operationId, this.#now());
|
|
4657
|
-
this.#
|
|
4953
|
+
this.#commitMutation();
|
|
4658
4954
|
return { operation, plan, request };
|
|
4659
4955
|
}
|
|
4660
4956
|
catch (error) {
|
|
4661
|
-
this.#
|
|
4957
|
+
this.#rollbackMutation();
|
|
4662
4958
|
throw error;
|
|
4663
4959
|
}
|
|
4664
4960
|
});
|
|
@@ -4725,7 +5021,7 @@ export class ControlPlaneStore {
|
|
|
4725
5021
|
this.#assertAdoptionHandoff(input.planId);
|
|
4726
5022
|
}
|
|
4727
5023
|
const now = this.#now();
|
|
4728
|
-
this.#
|
|
5024
|
+
this.#beginMutation();
|
|
4729
5025
|
try {
|
|
4730
5026
|
this.#withActivationSource(input.planId, () => { }, input.to === 'rollback-pending' || input.to === 'rolled-back');
|
|
4731
5027
|
const result = this.#database.prepare(`UPDATE activation_plans SET status = ?, revision = revision + 1,
|
|
@@ -4744,11 +5040,11 @@ export class ControlPlaneStore {
|
|
|
4744
5040
|
this.#database.prepare(`UPDATE capability_gaps SET status = 'closed', revision = revision + 1, updated_at = ? WHERE id = ?`).run(now, plan.gapId);
|
|
4745
5041
|
this.#finishActivation(plan, input.fence, now);
|
|
4746
5042
|
}
|
|
4747
|
-
this.#
|
|
5043
|
+
this.#commitMutation();
|
|
4748
5044
|
return plan;
|
|
4749
5045
|
}
|
|
4750
5046
|
catch (error) {
|
|
4751
|
-
this.#
|
|
5047
|
+
this.#rollbackMutation();
|
|
4752
5048
|
throw error;
|
|
4753
5049
|
}
|
|
4754
5050
|
}, input.to === 'rollback-pending' || input.to === 'rolled-back');
|
|
@@ -4791,7 +5087,7 @@ export class ControlPlaneStore {
|
|
|
4791
5087
|
const now = this.#now();
|
|
4792
5088
|
const nextStatus = verified.outcome === 'passed' ? (verified.phase === 'readiness' && plan.dossier.liveQualification ? 'awaiting-live-tasks' : expected.next) : 'rollback-pending';
|
|
4793
5089
|
return this.#withActivationSource(input.planId, () => {
|
|
4794
|
-
this.#
|
|
5090
|
+
this.#beginMutation();
|
|
4795
5091
|
try {
|
|
4796
5092
|
this.#withActivationSource(input.planId, () => { }, recovery);
|
|
4797
5093
|
const lockedOperation = this.getHostAttestationOperation(input.receipt.operationId);
|
|
@@ -4822,11 +5118,11 @@ export class ControlPlaneStore {
|
|
|
4822
5118
|
this.#finishActivation(output, input.expectedFence, now);
|
|
4823
5119
|
const operationReceipt = { idempotencyKey: input.idempotencyKey, operation: 'host-attestation', inputDigest, result: output, createdAt: now };
|
|
4824
5120
|
this.#insertReceipt(operationReceipt);
|
|
4825
|
-
this.#
|
|
5121
|
+
this.#commitMutation();
|
|
4826
5122
|
return operationReceipt;
|
|
4827
5123
|
}
|
|
4828
5124
|
catch (error) {
|
|
4829
|
-
this.#
|
|
5125
|
+
this.#rollbackMutation();
|
|
4830
5126
|
throw error;
|
|
4831
5127
|
}
|
|
4832
5128
|
}, recovery);
|
|
@@ -4884,7 +5180,7 @@ export class ControlPlaneStore {
|
|
|
4884
5180
|
}
|
|
4885
5181
|
}
|
|
4886
5182
|
const commit = () => {
|
|
4887
|
-
this.#
|
|
5183
|
+
this.#beginMutation();
|
|
4888
5184
|
try {
|
|
4889
5185
|
if (input.taskObservationId !== undefined) {
|
|
4890
5186
|
const context = readTaskObservationContext(this.#database, input.taskObservationId);
|
|
@@ -4922,11 +5218,11 @@ export class ControlPlaneStore {
|
|
|
4922
5218
|
}
|
|
4923
5219
|
const operationReceipt = { idempotencyKey: key, operation: 'post-activation-observation', inputDigest, result: output, createdAt: now };
|
|
4924
5220
|
this.#insertReceipt(operationReceipt);
|
|
4925
|
-
this.#
|
|
5221
|
+
this.#commitMutation();
|
|
4926
5222
|
return operationReceipt;
|
|
4927
5223
|
}
|
|
4928
5224
|
catch (error) {
|
|
4929
|
-
this.#
|
|
5225
|
+
this.#rollbackMutation();
|
|
4930
5226
|
throw error;
|
|
4931
5227
|
}
|
|
4932
5228
|
};
|
|
@@ -4953,7 +5249,7 @@ export class ControlPlaneStore {
|
|
|
4953
5249
|
}
|
|
4954
5250
|
const verified = await input.resolveAuthority(input.receipt).verify(input.receipt, plan, watch.exact);
|
|
4955
5251
|
const now = this.#now();
|
|
4956
|
-
this.#
|
|
5252
|
+
this.#beginMutation();
|
|
4957
5253
|
try {
|
|
4958
5254
|
const current = this.#database.prepare('SELECT * FROM activation_watch WHERE plan_id = ?').get(plan.id);
|
|
4959
5255
|
if (current.state === 'closed-retracted')
|
|
@@ -4970,11 +5266,11 @@ export class ControlPlaneStore {
|
|
|
4970
5266
|
const output = this.getActivationWatch(plan.id);
|
|
4971
5267
|
const operationReceipt = { idempotencyKey: key, operation: 'activation-retraction', inputDigest, result: output, createdAt: now };
|
|
4972
5268
|
this.#insertReceipt(operationReceipt);
|
|
4973
|
-
this.#
|
|
5269
|
+
this.#commitMutation();
|
|
4974
5270
|
return operationReceipt;
|
|
4975
5271
|
}
|
|
4976
5272
|
catch (error) {
|
|
4977
|
-
this.#
|
|
5273
|
+
this.#rollbackMutation();
|
|
4978
5274
|
throw error;
|
|
4979
5275
|
}
|
|
4980
5276
|
}
|
|
@@ -5060,7 +5356,7 @@ export class ControlPlaneStore {
|
|
|
5060
5356
|
if (!input.succeeded && (input.checkedTreeDigest !== undefined || input.checkedPatchDigest !== undefined)) {
|
|
5061
5357
|
throw new ControlPlaneStoreError('invalid-input', 'failed source checks cannot carry successful check evidence');
|
|
5062
5358
|
}
|
|
5063
|
-
this.#
|
|
5359
|
+
this.#beginMutation();
|
|
5064
5360
|
try {
|
|
5065
5361
|
const result = this.#database.prepare(`UPDATE source_plans SET status = ?, revision = revision + 1,
|
|
5066
5362
|
checked_tree_digest = ?, checked_patch_digest = ?, checked_at = ?, updated_at = ?
|
|
@@ -5074,11 +5370,11 @@ export class ControlPlaneStore {
|
|
|
5074
5370
|
}
|
|
5075
5371
|
const digest = controlPlaneDigest({ planId: output.id, planDigest: output.digest, status, sourceCheck: output.sourceCheck });
|
|
5076
5372
|
this.#insertReceipt({ idempotencyKey: `source-checks:${output.id}`, operation: 'source-local-checks', inputDigest: digest, result: output, createdAt: now });
|
|
5077
|
-
this.#
|
|
5373
|
+
this.#commitMutation();
|
|
5078
5374
|
return output;
|
|
5079
5375
|
}
|
|
5080
5376
|
catch (error) {
|
|
5081
|
-
this.#
|
|
5377
|
+
this.#rollbackMutation();
|
|
5082
5378
|
throw error;
|
|
5083
5379
|
}
|
|
5084
5380
|
}
|
|
@@ -5111,7 +5407,7 @@ export class ControlPlaneStore {
|
|
|
5111
5407
|
}
|
|
5112
5408
|
const commit = () => {
|
|
5113
5409
|
this.#assertOwnerTaskFailureGapAdmission(plan.gapId);
|
|
5114
|
-
this.#
|
|
5410
|
+
this.#beginMutation();
|
|
5115
5411
|
try {
|
|
5116
5412
|
const result = this.#database.prepare(`UPDATE source_plans SET status = 'ready-for-human-review', revision = revision + 1, updated_at = ?
|
|
5117
5413
|
WHERE id = ? AND revision = ? AND status = 'approved' AND mode = 'modify'
|
|
@@ -5124,11 +5420,11 @@ export class ControlPlaneStore {
|
|
|
5124
5420
|
const receipt = { idempotencyKey: `source-verify-prepared:${output.id}:${output.revision}`,
|
|
5125
5421
|
operation: 'source-verify-prepared', inputDigest, result: output, createdAt: now };
|
|
5126
5422
|
this.#insertReceipt(receipt);
|
|
5127
|
-
this.#
|
|
5423
|
+
this.#commitMutation();
|
|
5128
5424
|
return receipt;
|
|
5129
5425
|
}
|
|
5130
5426
|
catch (error) {
|
|
5131
|
-
this.#
|
|
5427
|
+
this.#rollbackMutation();
|
|
5132
5428
|
throw error;
|
|
5133
5429
|
}
|
|
5134
5430
|
};
|
|
@@ -5188,7 +5484,7 @@ export class ControlPlaneStore {
|
|
|
5188
5484
|
lineageDigest = controlPlaneDigest({ history: prehistory, maintenance: premaintenance, trust: input.trust, baseline: prebaseline });
|
|
5189
5485
|
}
|
|
5190
5486
|
return withCurrentSource(plan.gapId, () => {
|
|
5191
|
-
this.#
|
|
5487
|
+
this.#beginMutation();
|
|
5192
5488
|
try {
|
|
5193
5489
|
const linkedRows = this.#database.prepare('SELECT * FROM source_jobs WHERE plan_id = ? LIMIT 2')
|
|
5194
5490
|
.all(plan.id);
|
|
@@ -5243,11 +5539,11 @@ export class ControlPlaneStore {
|
|
|
5243
5539
|
const output = this.getSourcePlan(plan.id);
|
|
5244
5540
|
const operationReceipt = { idempotencyKey: key, operation: 'start-source-release', inputDigest, result: output, createdAt: now };
|
|
5245
5541
|
this.#insertReceipt(operationReceipt);
|
|
5246
|
-
this.#
|
|
5542
|
+
this.#commitMutation();
|
|
5247
5543
|
return operationReceipt;
|
|
5248
5544
|
}
|
|
5249
5545
|
catch (error) {
|
|
5250
|
-
this.#
|
|
5546
|
+
this.#rollbackMutation();
|
|
5251
5547
|
throw error;
|
|
5252
5548
|
}
|
|
5253
5549
|
});
|
|
@@ -5271,7 +5567,7 @@ export class ControlPlaneStore {
|
|
|
5271
5567
|
await reverifySourceReleaseAuthorization(plan.releaseAuthorization, plan, input.resolveAuthorizationAuthority);
|
|
5272
5568
|
return withCurrentSource(plan.gapId, () => {
|
|
5273
5569
|
const now = this.#now();
|
|
5274
|
-
this.#
|
|
5570
|
+
this.#beginMutation();
|
|
5275
5571
|
try {
|
|
5276
5572
|
const current = this.getSourcePlan(plan.id);
|
|
5277
5573
|
const currentExpected = expectedSourceRelease(current.status);
|
|
@@ -5291,7 +5587,7 @@ export class ControlPlaneStore {
|
|
|
5291
5587
|
if (prior.bindingDigest !== controlPlaneDigest(releaseRequestBinding(expectedRequest))) {
|
|
5292
5588
|
throw new ControlPlaneStoreError('conflict', 'durable release operation payload changed for the same phase/fence');
|
|
5293
5589
|
}
|
|
5294
|
-
this.#
|
|
5590
|
+
this.#commitMutation();
|
|
5295
5591
|
return prior;
|
|
5296
5592
|
}
|
|
5297
5593
|
const operationId = `release-operation-${randomUUID()}`;
|
|
@@ -5301,11 +5597,11 @@ export class ControlPlaneStore {
|
|
|
5301
5597
|
operation_id, binding_digest, request_digest, request_json, status, receipt_digest, receipt_json, created_at, completed_at, applied_at)
|
|
5302
5598
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', NULL, NULL, ?, NULL, NULL)`).run(current.id, expected.phase, current.release.id, current.release.fence, attempt, operationId, bindingDigest, controlPlaneDigest(request), JSON.stringify(request), now);
|
|
5303
5599
|
const operation = this.getSourceReleaseOperation(operationId);
|
|
5304
|
-
this.#
|
|
5600
|
+
this.#commitMutation();
|
|
5305
5601
|
return operation;
|
|
5306
5602
|
}
|
|
5307
5603
|
catch (error) {
|
|
5308
|
-
this.#
|
|
5604
|
+
this.#rollbackMutation();
|
|
5309
5605
|
throw error;
|
|
5310
5606
|
}
|
|
5311
5607
|
});
|
|
@@ -5370,13 +5666,13 @@ export class ControlPlaneStore {
|
|
|
5370
5666
|
withCurrentSource(plan.gapId, () => { });
|
|
5371
5667
|
await reverifySourceReleaseAuthorization(operation.request.authorization, plan, input.resolveAuthorizationAuthority);
|
|
5372
5668
|
const claimed = withCurrentSource(plan.gapId, () => {
|
|
5373
|
-
this.#
|
|
5669
|
+
this.#beginMutation();
|
|
5374
5670
|
try {
|
|
5375
5671
|
const current = this.getSourcePlan(plan.id);
|
|
5376
5672
|
const currentOperation = this.getSourceReleaseOperation(operation.operationId);
|
|
5377
5673
|
assertCurrent(current, currentOperation);
|
|
5378
5674
|
if (currentOperation.receipt !== undefined) {
|
|
5379
|
-
this.#
|
|
5675
|
+
this.#commitMutation();
|
|
5380
5676
|
return false;
|
|
5381
5677
|
}
|
|
5382
5678
|
const dispatch = this.#database.prepare('SELECT * FROM source_release_dispatches WHERE operation_id = ?').get(operation.operationId);
|
|
@@ -5386,11 +5682,11 @@ export class ControlPlaneStore {
|
|
|
5386
5682
|
}
|
|
5387
5683
|
this.#database.prepare(`INSERT INTO source_release_dispatches (operation_id, status, claimed_at, completed_at)
|
|
5388
5684
|
VALUES (?, 'claimed', ?, NULL)`).run(operation.operationId, this.#now());
|
|
5389
|
-
this.#
|
|
5685
|
+
this.#commitMutation();
|
|
5390
5686
|
return true;
|
|
5391
5687
|
}
|
|
5392
5688
|
catch (error) {
|
|
5393
|
-
this.#
|
|
5689
|
+
this.#rollbackMutation();
|
|
5394
5690
|
throw error;
|
|
5395
5691
|
}
|
|
5396
5692
|
});
|
|
@@ -5399,7 +5695,7 @@ export class ControlPlaneStore {
|
|
|
5399
5695
|
const receipt = await input.execute(operation.request);
|
|
5400
5696
|
await input.resolveAuthority(receipt).verify(receipt, plan, operation.request);
|
|
5401
5697
|
return withCurrentSource(plan.gapId, () => {
|
|
5402
|
-
this.#
|
|
5698
|
+
this.#beginMutation();
|
|
5403
5699
|
try {
|
|
5404
5700
|
const current = this.getSourcePlan(plan.id);
|
|
5405
5701
|
const currentOperation = this.getSourceReleaseOperation(operation.operationId);
|
|
@@ -5417,11 +5713,11 @@ export class ControlPlaneStore {
|
|
|
5417
5713
|
WHERE operation_id = ? AND status = 'claimed'`).run(now, operation.operationId);
|
|
5418
5714
|
if (Number(completed.changes) !== 1)
|
|
5419
5715
|
throw new ControlPlaneStoreError('conflict', 'source release completion lost its dispatch claim');
|
|
5420
|
-
this.#
|
|
5716
|
+
this.#commitMutation();
|
|
5421
5717
|
return receipt;
|
|
5422
5718
|
}
|
|
5423
5719
|
catch (error) {
|
|
5424
|
-
this.#
|
|
5720
|
+
this.#rollbackMutation();
|
|
5425
5721
|
throw error;
|
|
5426
5722
|
}
|
|
5427
5723
|
});
|
|
@@ -5455,14 +5751,14 @@ export class ControlPlaneStore {
|
|
|
5455
5751
|
await reverifySourceReleaseAuthorization(operation.request.authorization, plan, input.resolveAuthorizationAuthority);
|
|
5456
5752
|
await input.resolveAuthority(input.receipt).verify(input.receipt, plan, operation.request);
|
|
5457
5753
|
return withCurrentSource(plan.gapId, () => {
|
|
5458
|
-
this.#
|
|
5754
|
+
this.#beginMutation();
|
|
5459
5755
|
try {
|
|
5460
5756
|
const current = this.getSourcePlan(plan.id), currentOperation = this.getSourceReleaseOperation(operation.operationId);
|
|
5461
5757
|
assertCurrent(current, currentOperation);
|
|
5462
5758
|
if (currentOperation.receipt !== undefined) {
|
|
5463
5759
|
if (controlPlaneDigest(currentOperation.receipt) !== controlPlaneDigest(input.receipt))
|
|
5464
5760
|
throw new ControlPlaneStoreError('conflict', 'source release receipt differs from completed operation');
|
|
5465
|
-
this.#
|
|
5761
|
+
this.#commitMutation();
|
|
5466
5762
|
return currentOperation.receipt;
|
|
5467
5763
|
}
|
|
5468
5764
|
const dispatch = this.#database.prepare('SELECT * FROM source_release_dispatches WHERE operation_id = ?').get(operation.operationId);
|
|
@@ -5479,11 +5775,11 @@ export class ControlPlaneStore {
|
|
|
5479
5775
|
if (Number(completed.changes) !== 1)
|
|
5480
5776
|
throw new ControlPlaneStoreError('conflict', 'source release receipt lost its dispatch claim');
|
|
5481
5777
|
}
|
|
5482
|
-
this.#
|
|
5778
|
+
this.#commitMutation();
|
|
5483
5779
|
return input.receipt;
|
|
5484
5780
|
}
|
|
5485
5781
|
catch (error) {
|
|
5486
|
-
this.#
|
|
5782
|
+
this.#rollbackMutation();
|
|
5487
5783
|
throw error;
|
|
5488
5784
|
}
|
|
5489
5785
|
});
|
|
@@ -5549,7 +5845,7 @@ export class ControlPlaneStore {
|
|
|
5549
5845
|
}
|
|
5550
5846
|
const now = this.#now();
|
|
5551
5847
|
return withCurrentSource(plan.gapId, () => {
|
|
5552
|
-
this.#
|
|
5848
|
+
this.#beginMutation();
|
|
5553
5849
|
try {
|
|
5554
5850
|
const current = this.getSourcePlan(plan.id);
|
|
5555
5851
|
const currentExpected = expectedSourceRelease(current.status);
|
|
@@ -5589,18 +5885,18 @@ export class ControlPlaneStore {
|
|
|
5589
5885
|
}
|
|
5590
5886
|
const operationReceipt = { idempotencyKey: key, operation: 'source-release', inputDigest, result: output, createdAt: now };
|
|
5591
5887
|
this.#insertReceipt(operationReceipt);
|
|
5592
|
-
this.#
|
|
5888
|
+
this.#commitMutation();
|
|
5593
5889
|
return operationReceipt;
|
|
5594
5890
|
}
|
|
5595
5891
|
catch (error) {
|
|
5596
|
-
this.#
|
|
5892
|
+
this.#rollbackMutation();
|
|
5597
5893
|
throw error;
|
|
5598
5894
|
}
|
|
5599
5895
|
});
|
|
5600
5896
|
}
|
|
5601
5897
|
async prepareSourcePublishReconciliation(input) {
|
|
5602
5898
|
const now = this.#now();
|
|
5603
|
-
this.#
|
|
5899
|
+
this.#beginMutation();
|
|
5604
5900
|
try {
|
|
5605
5901
|
const plan = this.getSourcePlan(input.planId);
|
|
5606
5902
|
if (plan.status !== 'publish-ambiguous' || plan.revision !== input.expectedRevision || plan.release?.fence !== input.expectedFence) {
|
|
@@ -5643,18 +5939,18 @@ export class ControlPlaneStore {
|
|
|
5643
5939
|
const prior = sourcePublishReconciliationFromRow(priorRow);
|
|
5644
5940
|
if (prior.bindingDigest !== bindingDigest)
|
|
5645
5941
|
throw new ControlPlaneStoreError('conflict', 'durable publish reconciliation payload changed');
|
|
5646
|
-
this.#
|
|
5942
|
+
this.#commitMutation();
|
|
5647
5943
|
return prior;
|
|
5648
5944
|
}
|
|
5649
5945
|
this.#database.prepare(`INSERT INTO source_publish_reconciliations (plan_id, release_id, release_fence, attempt, operation_id,
|
|
5650
5946
|
binding_digest, request_digest, request_json, status, receipt_digest, receipt_json, created_at, completed_at, applied_at)
|
|
5651
5947
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', NULL, NULL, ?, NULL, NULL)`).run(plan.id, plan.release.id, plan.release.fence, attempt, operationId, bindingDigest, controlPlaneDigest(parsed), JSON.stringify(parsed), now);
|
|
5652
5948
|
const operation = this.getSourcePublishReconciliationOperation(operationId);
|
|
5653
|
-
this.#
|
|
5949
|
+
this.#commitMutation();
|
|
5654
5950
|
return operation;
|
|
5655
5951
|
}
|
|
5656
5952
|
catch (error) {
|
|
5657
|
-
this.#
|
|
5953
|
+
this.#rollbackMutation();
|
|
5658
5954
|
throw error;
|
|
5659
5955
|
}
|
|
5660
5956
|
}
|
|
@@ -5666,7 +5962,7 @@ export class ControlPlaneStore {
|
|
|
5666
5962
|
}
|
|
5667
5963
|
async runSourcePublishReconciliation(input) {
|
|
5668
5964
|
try {
|
|
5669
|
-
this.#
|
|
5965
|
+
this.#beginMutation();
|
|
5670
5966
|
}
|
|
5671
5967
|
catch {
|
|
5672
5968
|
throw new ControlPlaneStoreError('conflict', 'publish reconciliation single-flight is held');
|
|
@@ -5678,7 +5974,7 @@ export class ControlPlaneStore {
|
|
|
5678
5974
|
|| operation.fence !== input.expectedFence)
|
|
5679
5975
|
throw new ControlPlaneStoreError('conflict', 'publish reconciliation lost its revision/fence');
|
|
5680
5976
|
if (operation.receipt !== undefined) {
|
|
5681
|
-
this.#
|
|
5977
|
+
this.#commitMutation();
|
|
5682
5978
|
return operation.receipt;
|
|
5683
5979
|
}
|
|
5684
5980
|
await reverifySourceReleaseAuthorization(operation.request.authorization, plan, input.resolveAuthorizationAuthority);
|
|
@@ -5689,11 +5985,11 @@ export class ControlPlaneStore {
|
|
|
5689
5985
|
receipt_digest = ?, receipt_json = ?, completed_at = ? WHERE operation_id = ? AND status = 'pending' AND request_digest = ?`).run(controlPlaneDigest(receipt), JSON.stringify(receipt), now, operation.operationId, operation.requestDigest);
|
|
5690
5986
|
if (Number(result.changes) !== 1)
|
|
5691
5987
|
throw new ControlPlaneStoreError('conflict', 'publish reconciliation completion lost single-flight');
|
|
5692
|
-
this.#
|
|
5988
|
+
this.#commitMutation();
|
|
5693
5989
|
return receipt;
|
|
5694
5990
|
}
|
|
5695
5991
|
catch (error) {
|
|
5696
|
-
this.#
|
|
5992
|
+
this.#rollbackMutation();
|
|
5697
5993
|
throw error;
|
|
5698
5994
|
}
|
|
5699
5995
|
}
|
|
@@ -5726,7 +6022,7 @@ export class ControlPlaneStore {
|
|
|
5726
6022
|
const verified = await input.resolveAuthority(input.receipt).verify(input.receipt, plan, operation.request);
|
|
5727
6023
|
const outcome = verified.evidence.outcome;
|
|
5728
6024
|
const now = this.#now();
|
|
5729
|
-
this.#
|
|
6025
|
+
this.#beginMutation();
|
|
5730
6026
|
try {
|
|
5731
6027
|
let nextStatus = 'publish-ambiguous';
|
|
5732
6028
|
let nextFence = input.expectedFence;
|
|
@@ -5759,11 +6055,11 @@ export class ControlPlaneStore {
|
|
|
5759
6055
|
const output = this.getSourcePlan(plan.id);
|
|
5760
6056
|
const operationReceipt = { idempotencyKey: input.idempotencyKey, operation: 'reconcile-source-publish', inputDigest, result: output, createdAt: now };
|
|
5761
6057
|
this.#insertReceipt(operationReceipt);
|
|
5762
|
-
this.#
|
|
6058
|
+
this.#commitMutation();
|
|
5763
6059
|
return operationReceipt;
|
|
5764
6060
|
}
|
|
5765
6061
|
catch (error) {
|
|
5766
|
-
this.#
|
|
6062
|
+
this.#rollbackMutation();
|
|
5767
6063
|
throw error;
|
|
5768
6064
|
}
|
|
5769
6065
|
}
|