@dsh-enhanced/plugin-control-plane 0.1.65 → 0.1.67
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 +123 -7
- 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 +13 -0
- package/lib/creation-capability-execution.d.ts.map +1 -0
- package/lib/creation-capability-execution.js +112 -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 +65 -1
- package/lib/creation-capability-runtime.d.ts.map +1 -1
- package/lib/creation-capability-runtime.js +178 -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 +15 -4
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +10 -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/revision-capability-journal.d.ts +103 -0
- package/lib/revision-capability-journal.d.ts.map +1 -0
- package/lib/revision-capability-journal.js +872 -0
- package/lib/revision-capability-journal.js.map +1 -0
- package/lib/revision-capability-runtime.d.ts +74 -0
- package/lib/revision-capability-runtime.d.ts.map +1 -0
- package/lib/revision-capability-runtime.js +562 -0
- package/lib/revision-capability-runtime.js.map +1 -0
- package/lib/revision-capability-types.d.ts +289 -0
- package/lib/revision-capability-types.d.ts.map +1 -0
- package/lib/revision-capability-types.js +2 -0
- package/lib/revision-capability-types.js.map +1 -0
- package/lib/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 +107 -6
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +643 -47
- 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 +33 -1
- package/lib/source-jobs.d.ts.map +1 -1
- package/lib/source-jobs.js +141 -29
- package/lib/source-jobs.js.map +1 -1
- package/lib/source-workspace.d.ts.map +1 -1
- package/lib/source-workspace.js +0 -1
- package/lib/source-workspace.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 +22 -2
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +477 -177
- 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
|
}
|
|
@@ -2284,7 +2536,7 @@ export class ControlPlaneStore {
|
|
|
2284
2536
|
created_at DESC, id DESC LIMIT ?`).all(limit).map(sourceJobFromRow);
|
|
2285
2537
|
}
|
|
2286
2538
|
/** Prepared owner continuations have a bounded recovery query, independent of job history. */
|
|
2287
|
-
listPreparedSourceApprovalJobs(includeRelease = false, includeExecution = false, includeAdoption = false, includeCreation = false, includeCreationAdoption = false, includeRevision = false, includeRegression = false) {
|
|
2539
|
+
listPreparedSourceApprovalJobs(includeRelease = false, includeExecution = false, includeAdoption = false, includeCreation = false, includeCreationAdoption = false, includeRevision = false, includeRegression = false, includeRevisionAdoption = false) {
|
|
2288
2540
|
return this.#database.prepare(`SELECT j.* FROM source_jobs j JOIN source_plans p ON p.id = j.plan_id
|
|
2289
2541
|
JOIN owner_task_failure_gaps g ON g.gap_id = p.gap_id
|
|
2290
2542
|
LEFT JOIN source_adoptions a ON a.source_plan_id = p.id
|
|
@@ -2304,9 +2556,13 @@ export class ControlPlaneStore {
|
|
|
2304
2556
|
OR (? = 1 AND p.mode = 'prepared-revise' AND p.status = 'pending-approval'
|
|
2305
2557
|
AND json_type(j.intent_json, '$.revision.growthRun.revisionRegressionAcceptance') IS NOT NULL
|
|
2306
2558
|
AND EXISTS (SELECT 1 FROM source_revision_verifications v WHERE v.plan_id=p.id AND v.status='verified')
|
|
2307
|
-
AND NOT EXISTS (SELECT 1 FROM source_revision_regressions v WHERE v.plan_id=p.id))
|
|
2559
|
+
AND NOT EXISTS (SELECT 1 FROM source_revision_regressions v WHERE v.plan_id=p.id))
|
|
2560
|
+
OR (? = 1 AND p.mode = 'prepared-revise' AND p.status = 'pending-approval'
|
|
2561
|
+
AND EXISTS (SELECT 1 FROM source_revision_verifications v WHERE v.plan_id=p.id AND v.status='verified')
|
|
2562
|
+
AND (json_type(j.intent_json, '$.revision.growthRun.revisionRegressionAcceptance') IS NULL
|
|
2563
|
+
OR EXISTS (SELECT 1 FROM source_revision_regressions r WHERE r.plan_id=p.id AND r.status='verified'))))
|
|
2308
2564
|
AND (p.expires_at > ? OR ap.status IN ('staging', 'awaiting-reload', 'awaiting-readiness', 'awaiting-live-tasks', 'awaiting-effect-blocked-replay', 'awaiting-shadow', 'awaiting-canary', 'awaiting-soak', 'awaiting-health', 'commit-pending', 'rollback-pending'))
|
|
2309
|
-
ORDER BY j.created_at, j.id LIMIT 1000`).all(includeRelease ? 1 : 0, includeExecution ? 1 : 0, includeAdoption ? 1 : 0, includeCreation ? 1 : 0, includeCreationAdoption ? 1 : 0, includeRevision ? 1 : 0, includeRegression ? 1 : 0, this.#now()).map(sourceJobFromRow);
|
|
2565
|
+
ORDER BY j.created_at, j.id LIMIT 1000`).all(includeRelease ? 1 : 0, includeExecution ? 1 : 0, includeAdoption ? 1 : 0, includeCreation ? 1 : 0, includeCreationAdoption ? 1 : 0, includeRevision ? 1 : 0, includeRegression ? 1 : 0, includeRevisionAdoption ? 1 : 0, this.#now()).map(sourceJobFromRow);
|
|
2310
2566
|
}
|
|
2311
2567
|
bindSourceJobDefinition(input) {
|
|
2312
2568
|
if (!Number.isSafeInteger(input.revision) || input.revision < 1 || !DIGEST.test(input.definitionHash))
|
|
@@ -2411,9 +2667,9 @@ export class ControlPlaneStore {
|
|
|
2411
2667
|
throw new ControlPlaneStoreError('invalid-input', 'modify source plan prepared digests are invalid');
|
|
2412
2668
|
}
|
|
2413
2669
|
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
|
|
2670
|
+
if ((mode === 'modify' && preparedEvidence.schemaVersion === 2)
|
|
2671
|
+
|| (preparedEvidence.schemaVersion !== 1 && input.sourceJob === undefined)) {
|
|
2672
|
+
throw new ControlPlaneStoreError('invalid-input', 'native source evidence requires a matching durable source job');
|
|
2417
2673
|
}
|
|
2418
2674
|
}
|
|
2419
2675
|
else if (input.prepared !== undefined) {
|
|
@@ -2482,7 +2738,7 @@ export class ControlPlaneStore {
|
|
|
2482
2738
|
? { ...immutable, mode, ...(creation === undefined ? {} : { creation }), ...(revision === undefined ? {} : { sourceRevision: revision }), sourceCheck: { treeDigest: input.prepared.treeDigest, patchDigest: input.prepared.patchDigest,
|
|
2483
2739
|
checkedAt: input.prepared.checkedAt }, preparedEvidence }
|
|
2484
2740
|
: immutable);
|
|
2485
|
-
this.#
|
|
2741
|
+
this.#beginMutation();
|
|
2486
2742
|
try {
|
|
2487
2743
|
const currentGap = this.getGap(gap.id);
|
|
2488
2744
|
if (currentGap.revision !== gap.revision || currentGap.inputDigest !== gap.inputDigest || currentGap.status !== 'open'
|
|
@@ -2497,7 +2753,7 @@ export class ControlPlaneStore {
|
|
|
2497
2753
|
|| sourceJob.intent.baseCommit !== input.baseCommit || sourceJob.intent.name !== name
|
|
2498
2754
|
|| sourceJob.intent.repository !== input.repository || sourceJob.intent.worktree !== input.worktree || sourceJob.intent.ttlMs !== input.ttlMs
|
|
2499
2755
|
|| (mode === 'prepared-create') !== (sourceJob.intent.mode === 'create')
|
|
2500
|
-
|| (preparedEvidence !== undefined &&
|
|
2756
|
+
|| (preparedEvidence !== undefined && preparedEvidence.schemaVersion !== sourceBuildEvidenceSchema(sourceJob.intent.build))
|
|
2501
2757
|
|| (mode === 'prepared-create' && controlPlaneDigest(sourceJob.intent.creation) !== controlPlaneDigest(creation))
|
|
2502
2758
|
|| (mode === 'prepared-revise') !== (sourceJob.intent.mode === 'revise-created')
|
|
2503
2759
|
|| (mode === 'prepared-revise' && controlPlaneDigest(sourceJob.intent.revision) !== controlPlaneDigest(revision))) {
|
|
@@ -2560,11 +2816,11 @@ export class ControlPlaneStore {
|
|
|
2560
2816
|
const plan = this.getSourcePlan(id);
|
|
2561
2817
|
const receipt = { idempotencyKey: key, operation: 'create-source-plan', inputDigest, result: plan, createdAt: now };
|
|
2562
2818
|
this.#insertReceipt(receipt);
|
|
2563
|
-
this.#
|
|
2819
|
+
this.#commitMutation();
|
|
2564
2820
|
return receipt;
|
|
2565
2821
|
}
|
|
2566
2822
|
catch (error) {
|
|
2567
|
-
this.#
|
|
2823
|
+
this.#rollbackMutation();
|
|
2568
2824
|
throw error;
|
|
2569
2825
|
}
|
|
2570
2826
|
}
|
|
@@ -2614,6 +2870,7 @@ export class ControlPlaneStore {
|
|
|
2614
2870
|
|| job.intent.creation.generatorDigest !== plan.generatorDigest
|
|
2615
2871
|
|| job.intent.repository !== plan.repository || job.intent.worktree !== plan.worktree
|
|
2616
2872
|
|| job.intent.baseCommit !== plan.baseCommit || job.intent.name !== plan.name
|
|
2873
|
+
|| plan.preparedEvidence.schemaVersion !== sourceBuildEvidenceSchema(job.intent.build)
|
|
2617
2874
|
|| plan.expiresAt !== plan.createdAt + job.intent.ttlMs) {
|
|
2618
2875
|
throw new ControlPlaneStoreError('invalid-state', 'prepared creation source job binding is corrupt');
|
|
2619
2876
|
}
|
|
@@ -2659,6 +2916,7 @@ export class ControlPlaneStore {
|
|
|
2659
2916
|
|| job.intent.revision.generatorDigest !== plan.generatorDigest
|
|
2660
2917
|
|| job.intent.repository !== plan.repository || job.intent.worktree !== plan.worktree
|
|
2661
2918
|
|| job.intent.baseCommit !== plan.baseCommit || job.intent.name !== plan.name
|
|
2919
|
+
|| plan.preparedEvidence.schemaVersion !== sourceBuildEvidenceSchema(job.intent.build)
|
|
2662
2920
|
|| plan.expiresAt !== plan.createdAt + job.intent.ttlMs) {
|
|
2663
2921
|
throw new ControlPlaneStoreError('invalid-state', 'prepared revision source job binding is corrupt');
|
|
2664
2922
|
}
|
|
@@ -2693,6 +2951,7 @@ export class ControlPlaneStore {
|
|
|
2693
2951
|
const job = this.getPreparedSourceJob(plan.id);
|
|
2694
2952
|
const reference = this.getOwnerTaskFailureReference(plan.gapId);
|
|
2695
2953
|
if (!reference || !plan.sourceCheck || !plan.preparedEvidence || !plan.creation?.growthRun
|
|
2954
|
+
|| !creationVerificationMatchesPreparedEvidence(certificate, plan.preparedEvidence)
|
|
2696
2955
|
|| !job.intent.creation?.growthRun || plan.digest !== certificate.plan.digest || plan.name !== certificate.plan.name
|
|
2697
2956
|
|| plan.sourceCheck.treeDigest !== certificate.plan.sourceTreeDigest
|
|
2698
2957
|
|| plan.sourceCheck.patchDigest !== certificate.plan.sourcePatchDigest
|
|
@@ -2709,7 +2968,7 @@ export class ControlPlaneStore {
|
|
|
2709
2968
|
}
|
|
2710
2969
|
this.readPreparedSourceArtifact(plan.id);
|
|
2711
2970
|
const serialized = JSON.stringify(certificate), certificateDigest = controlPlaneDigest(certificate);
|
|
2712
|
-
this.#
|
|
2971
|
+
this.#beginMutation();
|
|
2713
2972
|
try {
|
|
2714
2973
|
const previous = this.#database.prepare('SELECT status,certificate_digest FROM source_creation_verifications WHERE plan_id=?')
|
|
2715
2974
|
.get(plan.id);
|
|
@@ -2719,10 +2978,10 @@ export class ControlPlaneStore {
|
|
|
2719
2978
|
else
|
|
2720
2979
|
this.#database.prepare(`UPDATE source_creation_verifications SET status='verified',certificate_json=?,certificate_digest=?,updated_at=?
|
|
2721
2980
|
WHERE plan_id=? AND status='claimed'`).run(serialized, certificateDigest, this.#now(), plan.id);
|
|
2722
|
-
this.#
|
|
2981
|
+
this.#commitMutation();
|
|
2723
2982
|
}
|
|
2724
2983
|
catch (error) {
|
|
2725
|
-
this.#
|
|
2984
|
+
this.#rollbackMutation();
|
|
2726
2985
|
throw error;
|
|
2727
2986
|
}
|
|
2728
2987
|
}
|
|
@@ -2743,6 +3002,11 @@ export class ControlPlaneStore {
|
|
|
2743
3002
|
if (certificate.plan.id !== planId || controlPlaneDigest(certificate) !== row.certificate_digest) {
|
|
2744
3003
|
throw new ControlPlaneStoreError('invalid-state', 'stored creation verification certificate digest changed');
|
|
2745
3004
|
}
|
|
3005
|
+
const plan = this.getSourcePlan(planId);
|
|
3006
|
+
if (certificate.plan.digest !== plan.digest || !plan.preparedEvidence
|
|
3007
|
+
|| !creationVerificationMatchesPreparedEvidence(certificate, plan.preparedEvidence)) {
|
|
3008
|
+
throw new ControlPlaneStoreError('invalid-state', 'stored creation verification certificate does not match exact prepared source');
|
|
3009
|
+
}
|
|
2746
3010
|
return certificate;
|
|
2747
3011
|
}
|
|
2748
3012
|
getCreationVerificationStatus(planId) {
|
|
@@ -2769,7 +3033,7 @@ export class ControlPlaneStore {
|
|
|
2769
3033
|
if (!pinned || this.#now() >= pinned.expiresAt) {
|
|
2770
3034
|
throw new ControlPlaneStoreError('invalid-state', 'creation verification requires a current pre-author policy binding');
|
|
2771
3035
|
}
|
|
2772
|
-
this.#
|
|
3036
|
+
this.#beginMutation();
|
|
2773
3037
|
try {
|
|
2774
3038
|
const count = this.#database.prepare('SELECT count(*) AS n FROM source_creation_verifications').get();
|
|
2775
3039
|
if (count.n >= 256)
|
|
@@ -2778,10 +3042,10 @@ export class ControlPlaneStore {
|
|
|
2778
3042
|
this.#database.prepare(`INSERT INTO source_creation_verifications
|
|
2779
3043
|
(plan_id,status,certificate_json,certificate_digest,reason,created_at,updated_at) VALUES (?,'claimed',NULL,NULL,NULL,?,?)`)
|
|
2780
3044
|
.run(planId, now, now);
|
|
2781
|
-
this.#
|
|
3045
|
+
this.#commitMutation();
|
|
2782
3046
|
}
|
|
2783
3047
|
catch (error) {
|
|
2784
|
-
this.#
|
|
3048
|
+
this.#rollbackMutation();
|
|
2785
3049
|
throw error;
|
|
2786
3050
|
}
|
|
2787
3051
|
}
|
|
@@ -2802,6 +3066,7 @@ export class ControlPlaneStore {
|
|
|
2802
3066
|
const reference = this.getOwnerTaskFailureReference(plan.gapId);
|
|
2803
3067
|
validateCreationCapabilitySource(source, certificate);
|
|
2804
3068
|
if (!reference || controlPlaneDigest(plan.sourceRevision?.parent) !== controlPlaneDigest(certificate.parent) || !plan.sourceCheck || !plan.preparedEvidence || !plan.sourceRevision?.growthRun
|
|
3069
|
+
|| !revisionVerificationMatchesPreparedEvidence(certificate, plan.preparedEvidence)
|
|
2805
3070
|
|| !job.intent.revision?.growthRun || plan.digest !== certificate.plan.digest || plan.name !== certificate.plan.name
|
|
2806
3071
|
|| plan.sourceCheck.treeDigest !== certificate.plan.sourceTreeDigest
|
|
2807
3072
|
|| plan.sourceCheck.patchDigest !== certificate.plan.sourcePatchDigest
|
|
@@ -2823,7 +3088,7 @@ export class ControlPlaneStore {
|
|
|
2823
3088
|
if (Buffer.byteLength(sourceJson) > 2_097_152)
|
|
2824
3089
|
throw new ControlPlaneStoreError('invalid-input', 'revision source archive exceeds bound');
|
|
2825
3090
|
const serialized = JSON.stringify(certificate), certificateDigest = controlPlaneDigest(certificate);
|
|
2826
|
-
this.#
|
|
3091
|
+
this.#beginMutation();
|
|
2827
3092
|
try {
|
|
2828
3093
|
const previous = this.#database.prepare('SELECT status,certificate_digest FROM source_revision_verifications WHERE plan_id=?')
|
|
2829
3094
|
.get(plan.id);
|
|
@@ -2838,10 +3103,10 @@ export class ControlPlaneStore {
|
|
|
2838
3103
|
throw new ControlPlaneStoreError('conflict', 'revision source archive changed');
|
|
2839
3104
|
if (!oldSource)
|
|
2840
3105
|
this.#database.prepare('INSERT INTO source_revision_sources VALUES(?,?)').run(plan.id, sourceJson);
|
|
2841
|
-
this.#
|
|
3106
|
+
this.#commitMutation();
|
|
2842
3107
|
}
|
|
2843
3108
|
catch (error) {
|
|
2844
|
-
this.#
|
|
3109
|
+
this.#rollbackMutation();
|
|
2845
3110
|
throw error;
|
|
2846
3111
|
}
|
|
2847
3112
|
}
|
|
@@ -2862,6 +3127,19 @@ export class ControlPlaneStore {
|
|
|
2862
3127
|
if (certificate.plan.id !== planId || controlPlaneDigest(certificate) !== row.certificate_digest) {
|
|
2863
3128
|
throw new ControlPlaneStoreError('invalid-state', 'stored revision verification certificate digest changed');
|
|
2864
3129
|
}
|
|
3130
|
+
// Check retained immutable metadata without reopening expired build/grant authority.
|
|
3131
|
+
const plan = this.getSourcePlan(planId);
|
|
3132
|
+
if (plan.mode !== 'prepared-revise' || !plan.sourceRevision || !plan.sourceCheck
|
|
3133
|
+
|| certificate.plan.digest !== plan.digest || certificate.plan.name !== plan.name
|
|
3134
|
+
|| certificate.plan.generatorDigest !== plan.generatorDigest
|
|
3135
|
+
|| certificate.plan.sourceTreeDigest !== plan.sourceCheck.treeDigest
|
|
3136
|
+
|| certificate.plan.sourcePatchDigest !== plan.sourceCheck.patchDigest
|
|
3137
|
+
|| controlPlaneDigest(certificate.parent) !== controlPlaneDigest(plan.sourceRevision.parent)
|
|
3138
|
+
|| !plan.preparedEvidence || certificate.plan.artifactSha256 !== plan.preparedEvidence.pack.sha256
|
|
3139
|
+
|| certificate.plan.artifactBytes !== plan.preparedEvidence.pack.sizeBytes
|
|
3140
|
+
|| !revisionVerificationMatchesPreparedEvidence(certificate, plan.preparedEvidence)) {
|
|
3141
|
+
throw new ControlPlaneStoreError('invalid-state', 'stored revision verification certificate does not match exact prepared source');
|
|
3142
|
+
}
|
|
2865
3143
|
return certificate;
|
|
2866
3144
|
}
|
|
2867
3145
|
getRevisionVerificationStatus(planId) {
|
|
@@ -2888,7 +3166,7 @@ export class ControlPlaneStore {
|
|
|
2888
3166
|
if (!pinned || this.#now() >= pinned.expiresAt) {
|
|
2889
3167
|
throw new ControlPlaneStoreError('invalid-state', 'revision verification requires a current pre-author policy binding');
|
|
2890
3168
|
}
|
|
2891
|
-
this.#
|
|
3169
|
+
this.#beginMutation();
|
|
2892
3170
|
try {
|
|
2893
3171
|
const count = this.#database.prepare('SELECT count(*) AS n FROM source_revision_verifications').get();
|
|
2894
3172
|
if (count.n >= 256)
|
|
@@ -2897,10 +3175,10 @@ export class ControlPlaneStore {
|
|
|
2897
3175
|
this.#database.prepare(`INSERT INTO source_revision_verifications
|
|
2898
3176
|
(plan_id,status,certificate_json,certificate_digest,reason,created_at,updated_at) VALUES (?,'claimed',NULL,NULL,NULL,?,?)`)
|
|
2899
3177
|
.run(planId, now, now);
|
|
2900
|
-
this.#
|
|
3178
|
+
this.#commitMutation();
|
|
2901
3179
|
}
|
|
2902
3180
|
catch (error) {
|
|
2903
|
-
this.#
|
|
3181
|
+
this.#rollbackMutation();
|
|
2904
3182
|
throw error;
|
|
2905
3183
|
}
|
|
2906
3184
|
}
|
|
@@ -2926,6 +3204,7 @@ export class ControlPlaneStore {
|
|
|
2926
3204
|
const previousJobs = new Set([...(parent?.observations ?? []), ...(candidate?.observations ?? [])].map(item => item.jobId));
|
|
2927
3205
|
if (certificate.verifiedAt > this.#now() || certificate.expiresAt <= this.#now()
|
|
2928
3206
|
|| !candidate || !parent || !source || !reference || !run?.revisionRegressionAcceptance
|
|
3207
|
+
|| !revisionRegressionMatchesCandidate(certificate, candidate, parent)
|
|
2929
3208
|
|| !job.intent.revision?.growthRun.revisionRegressionAcceptance
|
|
2930
3209
|
|| controlPlaneDigest(certificate.plan) !== controlPlaneDigest(candidate.plan)
|
|
2931
3210
|
|| controlPlaneDigest(certificate.source) !== controlPlaneDigest(candidate.source)
|
|
@@ -2951,7 +3230,7 @@ export class ControlPlaneStore {
|
|
|
2951
3230
|
}
|
|
2952
3231
|
this.readPreparedSourceArtifact(plan.id);
|
|
2953
3232
|
const serialized = JSON.stringify(certificate), digest = controlPlaneDigest(certificate);
|
|
2954
|
-
this.#
|
|
3233
|
+
this.#beginMutation();
|
|
2955
3234
|
try {
|
|
2956
3235
|
const old = this.#database.prepare('SELECT status,certificate_digest FROM source_revision_regressions WHERE plan_id=?')
|
|
2957
3236
|
.get(plan.id);
|
|
@@ -2961,10 +3240,10 @@ export class ControlPlaneStore {
|
|
|
2961
3240
|
else
|
|
2962
3241
|
this.#database.prepare(`UPDATE source_revision_regressions SET status='verified',certificate_json=?,certificate_digest=?,updated_at=?
|
|
2963
3242
|
WHERE plan_id=? AND status='claimed'`).run(serialized, digest, this.#now(), plan.id);
|
|
2964
|
-
this.#
|
|
3243
|
+
this.#commitMutation();
|
|
2965
3244
|
}
|
|
2966
3245
|
catch (error) {
|
|
2967
|
-
this.#
|
|
3246
|
+
this.#rollbackMutation();
|
|
2968
3247
|
throw error;
|
|
2969
3248
|
}
|
|
2970
3249
|
}
|
|
@@ -2985,6 +3264,10 @@ export class ControlPlaneStore {
|
|
|
2985
3264
|
if (certificate.plan.id !== planId || controlPlaneDigest(certificate) !== row.certificate_digest) {
|
|
2986
3265
|
throw new ControlPlaneStoreError('invalid-state', 'stored revision regression certificate digest changed');
|
|
2987
3266
|
}
|
|
3267
|
+
const candidate = this.getRevisionVerification(planId), parent = this.getCreationVerification(certificate.parent.planId);
|
|
3268
|
+
if (!candidate || !parent || !revisionRegressionMatchesCandidate(certificate, candidate, parent)) {
|
|
3269
|
+
throw new ControlPlaneStoreError('invalid-state', 'stored revision regression certificate does not match exact candidate execution');
|
|
3270
|
+
}
|
|
2988
3271
|
return certificate;
|
|
2989
3272
|
}
|
|
2990
3273
|
getRevisionRegressionStatus(planId) {
|
|
@@ -3011,7 +3294,7 @@ export class ControlPlaneStore {
|
|
|
3011
3294
|
if (!pinned || this.#now() >= pinned.expiresAt) {
|
|
3012
3295
|
throw new ControlPlaneStoreError('invalid-state', 'revision regression requires a current pre-author policy binding');
|
|
3013
3296
|
}
|
|
3014
|
-
this.#
|
|
3297
|
+
this.#beginMutation();
|
|
3015
3298
|
try {
|
|
3016
3299
|
const count = this.#database.prepare('SELECT count(*) AS n FROM source_revision_regressions').get();
|
|
3017
3300
|
if (count.n >= 256)
|
|
@@ -3020,10 +3303,10 @@ export class ControlPlaneStore {
|
|
|
3020
3303
|
this.#database.prepare(`INSERT INTO source_revision_regressions
|
|
3021
3304
|
(plan_id,status,certificate_json,certificate_digest,reason,created_at,updated_at) VALUES (?,'claimed',NULL,NULL,NULL,?,?)`)
|
|
3022
3305
|
.run(planId, now, now);
|
|
3023
|
-
this.#
|
|
3306
|
+
this.#commitMutation();
|
|
3024
3307
|
}
|
|
3025
3308
|
catch (error) {
|
|
3026
|
-
this.#
|
|
3309
|
+
this.#rollbackMutation();
|
|
3027
3310
|
throw error;
|
|
3028
3311
|
}
|
|
3029
3312
|
}
|
|
@@ -3054,7 +3337,7 @@ export class ControlPlaneStore {
|
|
|
3054
3337
|
deleteExpiredPreparedSourceArtifacts(now) {
|
|
3055
3338
|
if (!Number.isSafeInteger(now) || now < 0)
|
|
3056
3339
|
throw new ControlPlaneStoreError('invalid-input', 'artifact expiry time is invalid');
|
|
3057
|
-
this.#
|
|
3340
|
+
this.#beginMutation();
|
|
3058
3341
|
try {
|
|
3059
3342
|
const references = this.#database.prepare('SELECT plan_id, pack_sha256 FROM source_prepared_artifact_refs')
|
|
3060
3343
|
.all();
|
|
@@ -3076,11 +3359,11 @@ export class ControlPlaneStore {
|
|
|
3076
3359
|
.run(artifact.pack_sha256);
|
|
3077
3360
|
deleted += Number(result.changes);
|
|
3078
3361
|
}
|
|
3079
|
-
this.#
|
|
3362
|
+
this.#commitMutation();
|
|
3080
3363
|
return deleted;
|
|
3081
3364
|
}
|
|
3082
3365
|
catch (error) {
|
|
3083
|
-
this.#
|
|
3366
|
+
this.#rollbackMutation();
|
|
3084
3367
|
throw error;
|
|
3085
3368
|
}
|
|
3086
3369
|
}
|
|
@@ -3341,7 +3624,7 @@ export class ControlPlaneStore {
|
|
|
3341
3624
|
throw new ControlPlaneStoreError('invalid-state', 'expired source receipt is missing');
|
|
3342
3625
|
return receipt.result;
|
|
3343
3626
|
}
|
|
3344
|
-
this.#
|
|
3627
|
+
this.#beginMutation();
|
|
3345
3628
|
try {
|
|
3346
3629
|
const retired = this.#database.prepare(`UPDATE source_plans SET status = 'expired', revision = revision + 1, updated_at = ?
|
|
3347
3630
|
WHERE id = ? AND revision = ? AND ((mode = 'modify' AND status IN ('pending-approval', 'approved'))
|
|
@@ -3358,11 +3641,11 @@ export class ControlPlaneStore {
|
|
|
3358
3641
|
const result = this.getSourcePlan(plan.id);
|
|
3359
3642
|
this.#insertReceipt({ idempotencyKey: `source-expired:${plan.id}`, operation: 'source-expired',
|
|
3360
3643
|
inputDigest: expiryDigest, result, createdAt: input.now });
|
|
3361
|
-
this.#
|
|
3644
|
+
this.#commitMutation();
|
|
3362
3645
|
return result;
|
|
3363
3646
|
}
|
|
3364
3647
|
catch (error) {
|
|
3365
|
-
this.#
|
|
3648
|
+
this.#rollbackMutation();
|
|
3366
3649
|
throw error;
|
|
3367
3650
|
}
|
|
3368
3651
|
}
|
|
@@ -3420,7 +3703,7 @@ export class ControlPlaneStore {
|
|
|
3420
3703
|
throw new ControlPlaneStoreError(verified.decision === 'approved' ? 'expired' : 'invalid-state', 'plan approval is not currently applicable');
|
|
3421
3704
|
const table = kind === 'activation' ? 'activation_plans' : 'source_plans';
|
|
3422
3705
|
const now = this.#now();
|
|
3423
|
-
this.#
|
|
3706
|
+
this.#beginMutation();
|
|
3424
3707
|
try {
|
|
3425
3708
|
const statement = this.#database.prepare(`UPDATE ${table} SET status = 'approved', revision = revision + 1, approval_json = ?,
|
|
3426
3709
|
${kind === 'activation' ? 'approval_receipt_json = ?,' : ''} updated_at = ?
|
|
@@ -3433,11 +3716,11 @@ export class ControlPlaneStore {
|
|
|
3433
3716
|
const output = kind === 'activation' ? this.getPlan(plan.id) : this.getSourcePlan(plan.id);
|
|
3434
3717
|
const receipt = { idempotencyKey: input.idempotencyKey, operation: 'approve-plan', inputDigest, result: output, createdAt: now };
|
|
3435
3718
|
this.#insertReceipt(receipt);
|
|
3436
|
-
this.#
|
|
3719
|
+
this.#commitMutation();
|
|
3437
3720
|
return receipt;
|
|
3438
3721
|
}
|
|
3439
3722
|
catch (error) {
|
|
3440
|
-
this.#
|
|
3723
|
+
this.#rollbackMutation();
|
|
3441
3724
|
throw error;
|
|
3442
3725
|
}
|
|
3443
3726
|
});
|
|
@@ -3448,7 +3731,7 @@ export class ControlPlaneStore {
|
|
|
3448
3731
|
if (input.leaseMs < 5_000 || input.leaseMs > 300_000)
|
|
3449
3732
|
throw new ControlPlaneStoreError('invalid-input', 'leaseMs is invalid');
|
|
3450
3733
|
this.assertOwnerActivationSource(input.planId);
|
|
3451
|
-
this.#
|
|
3734
|
+
this.#beginMutation();
|
|
3452
3735
|
try {
|
|
3453
3736
|
const row = this.#database.prepare('SELECT * FROM activation_plans WHERE id = ?').get(input.planId);
|
|
3454
3737
|
if (row === undefined)
|
|
@@ -3493,12 +3776,12 @@ export class ControlPlaneStore {
|
|
|
3493
3776
|
if (Number(result.changes) !== 1)
|
|
3494
3777
|
throw new ControlPlaneStoreError('conflict', 'activation plan changed while being claimed');
|
|
3495
3778
|
const claimed = this.getPlan(plan.id);
|
|
3496
|
-
this.#
|
|
3779
|
+
this.#commitMutation();
|
|
3497
3780
|
return claimed;
|
|
3498
3781
|
});
|
|
3499
3782
|
}
|
|
3500
3783
|
catch (error) {
|
|
3501
|
-
this.#
|
|
3784
|
+
this.#rollbackMutation();
|
|
3502
3785
|
throw error;
|
|
3503
3786
|
}
|
|
3504
3787
|
}
|
|
@@ -3516,7 +3799,7 @@ export class ControlPlaneStore {
|
|
|
3516
3799
|
recordActivationTargetBaseline(input) {
|
|
3517
3800
|
return this.#withActivationSource(input.planId, () => {
|
|
3518
3801
|
const now = this.#now();
|
|
3519
|
-
this.#
|
|
3802
|
+
this.#beginMutation();
|
|
3520
3803
|
try {
|
|
3521
3804
|
this.assertOwnerActivationSource(input.planId);
|
|
3522
3805
|
const row = this.#database.prepare('SELECT * FROM activation_plans WHERE id = ?').get(input.planId);
|
|
@@ -3540,11 +3823,11 @@ export class ControlPlaneStore {
|
|
|
3540
3823
|
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
3824
|
}
|
|
3542
3825
|
const result = this.getPlan(plan.id);
|
|
3543
|
-
this.#
|
|
3826
|
+
this.#commitMutation();
|
|
3544
3827
|
return result;
|
|
3545
3828
|
}
|
|
3546
3829
|
catch (error) {
|
|
3547
|
-
this.#
|
|
3830
|
+
this.#rollbackMutation();
|
|
3548
3831
|
throw error;
|
|
3549
3832
|
}
|
|
3550
3833
|
});
|
|
@@ -3558,7 +3841,7 @@ export class ControlPlaneStore {
|
|
|
3558
3841
|
recordActivationInstalledBaseline(input) {
|
|
3559
3842
|
return this.#withActivationSource(input.planId, () => {
|
|
3560
3843
|
const now = this.#now();
|
|
3561
|
-
this.#
|
|
3844
|
+
this.#beginMutation();
|
|
3562
3845
|
try {
|
|
3563
3846
|
this.assertOwnerActivationSource(input.planId);
|
|
3564
3847
|
const row = this.#database.prepare('SELECT * FROM activation_plans WHERE id = ?').get(input.planId);
|
|
@@ -3599,11 +3882,11 @@ export class ControlPlaneStore {
|
|
|
3599
3882
|
successful_order, recorded_at, succeeded_at) VALUES (?, ?, ?, NULL, ?, NULL)`).run(plan.id, baseline, sequence.next_exposure_order, now);
|
|
3600
3883
|
}
|
|
3601
3884
|
const output = this.getPlan(plan.id);
|
|
3602
|
-
this.#
|
|
3885
|
+
this.#commitMutation();
|
|
3603
3886
|
return output;
|
|
3604
3887
|
}
|
|
3605
3888
|
catch (error) {
|
|
3606
|
-
this.#
|
|
3889
|
+
this.#rollbackMutation();
|
|
3607
3890
|
throw error;
|
|
3608
3891
|
}
|
|
3609
3892
|
});
|
|
@@ -3626,7 +3909,7 @@ export class ControlPlaneStore {
|
|
|
3626
3909
|
/** Move a signed closed deployment watch into the existing physical rollback lifecycle. */
|
|
3627
3910
|
beginPostActivationRollback(input) {
|
|
3628
3911
|
const now = this.#now();
|
|
3629
|
-
this.#
|
|
3912
|
+
this.#beginMutation();
|
|
3630
3913
|
try {
|
|
3631
3914
|
const row = this.#database.prepare('SELECT * FROM activation_plans WHERE id = ?').get(input.planId);
|
|
3632
3915
|
if (row === undefined)
|
|
@@ -3637,7 +3920,7 @@ export class ControlPlaneStore {
|
|
|
3637
3920
|
// A restart or retry must retain the original rollback identity and never
|
|
3638
3921
|
// manufacture another rollback from a now-closed watch.
|
|
3639
3922
|
if ((plan.status === 'rollback-pending' || plan.status === 'rolled-back') && this.#hasPostActivationRollbackProvenance(plan)) {
|
|
3640
|
-
this.#
|
|
3923
|
+
this.#commitMutation();
|
|
3641
3924
|
return plan;
|
|
3642
3925
|
}
|
|
3643
3926
|
if (plan.status !== 'activated' || plan.activation === undefined) {
|
|
@@ -3675,11 +3958,11 @@ export class ControlPlaneStore {
|
|
|
3675
3958
|
if (Number(updated.changes) !== 1)
|
|
3676
3959
|
throw new ControlPlaneStoreError('conflict', 'post-activation rollback lost its activation CAS');
|
|
3677
3960
|
const output = this.getPlan(plan.id);
|
|
3678
|
-
this.#
|
|
3961
|
+
this.#commitMutation();
|
|
3679
3962
|
return output;
|
|
3680
3963
|
}
|
|
3681
3964
|
catch (error) {
|
|
3682
|
-
this.#
|
|
3965
|
+
this.#rollbackMutation();
|
|
3683
3966
|
throw error;
|
|
3684
3967
|
}
|
|
3685
3968
|
}
|
|
@@ -3800,7 +4083,7 @@ export class ControlPlaneStore {
|
|
|
3800
4083
|
async withActivationFileSystemGuard(input, action) {
|
|
3801
4084
|
positiveInteger(input.leaseMs, 'leaseMs');
|
|
3802
4085
|
try {
|
|
3803
|
-
this.#
|
|
4086
|
+
this.#beginMutation();
|
|
3804
4087
|
}
|
|
3805
4088
|
catch {
|
|
3806
4089
|
throw new ControlPlaneStoreError('conflict', 'activation filesystem mutex is held');
|
|
@@ -3825,30 +4108,30 @@ export class ControlPlaneStore {
|
|
|
3825
4108
|
assertOwner(false);
|
|
3826
4109
|
const finished = this.#now();
|
|
3827
4110
|
this.#database.prepare('UPDATE activation_plans SET activation_lease_until = ?, updated_at = ? WHERE id = ?').run(finished + input.leaseMs, finished, input.planId);
|
|
3828
|
-
this.#
|
|
4111
|
+
this.#commitMutation();
|
|
3829
4112
|
return result;
|
|
3830
4113
|
});
|
|
3831
4114
|
}
|
|
3832
4115
|
catch (error) {
|
|
3833
|
-
this.#
|
|
4116
|
+
this.#rollbackMutation();
|
|
3834
4117
|
throw error;
|
|
3835
4118
|
}
|
|
3836
4119
|
}
|
|
3837
4120
|
/** Serialize lock-file lifecycle operations even after a plan became terminal. */
|
|
3838
4121
|
async withExclusiveWrite(action) {
|
|
3839
4122
|
try {
|
|
3840
|
-
this.#
|
|
4123
|
+
this.#beginMutation();
|
|
3841
4124
|
}
|
|
3842
4125
|
catch {
|
|
3843
4126
|
throw new ControlPlaneStoreError('conflict', 'control-plane mutex is held');
|
|
3844
4127
|
}
|
|
3845
4128
|
try {
|
|
3846
4129
|
const result = await action();
|
|
3847
|
-
this.#
|
|
4130
|
+
this.#commitMutation();
|
|
3848
4131
|
return result;
|
|
3849
4132
|
}
|
|
3850
4133
|
catch (error) {
|
|
3851
|
-
this.#
|
|
4134
|
+
this.#rollbackMutation();
|
|
3852
4135
|
throw error;
|
|
3853
4136
|
}
|
|
3854
4137
|
}
|
|
@@ -3987,7 +4270,7 @@ export class ControlPlaneStore {
|
|
|
3987
4270
|
ORDER BY epoch.created_at,epoch.operation_id LIMIT 100`).all(coordinatorId, this.#now()).map(runtimeEpochFromRow);
|
|
3988
4271
|
}
|
|
3989
4272
|
claimRuntimeEpoch(operationId, coordinatorId) {
|
|
3990
|
-
this.#
|
|
4273
|
+
this.#beginMutation();
|
|
3991
4274
|
try {
|
|
3992
4275
|
const record = this.getRuntimeEpoch(operationId);
|
|
3993
4276
|
if (!['pending', 'claimed'].includes(record.status) || this.#now() >= record.request.requestedAt + record.request.receiptTtlMs) {
|
|
@@ -3998,17 +4281,17 @@ export class ControlPlaneStore {
|
|
|
3998
4281
|
if (context.handoff.coordinatorId !== coordinatorId || (this.#adoptionCoordinatorId !== undefined && this.#adoptionCoordinatorId !== coordinatorId)) {
|
|
3999
4282
|
throw new ControlPlaneStoreError('conflict', 'runtime epoch coordinator changed');
|
|
4000
4283
|
}
|
|
4001
|
-
this.#
|
|
4284
|
+
this.#commitMutation();
|
|
4002
4285
|
return { ...record, status: 'claimed' };
|
|
4003
4286
|
}
|
|
4004
4287
|
catch (error) {
|
|
4005
|
-
this.#
|
|
4288
|
+
this.#rollbackMutation();
|
|
4006
4289
|
throw error;
|
|
4007
4290
|
}
|
|
4008
4291
|
}
|
|
4009
4292
|
applyRuntimeEpoch(receiptInput, trust) {
|
|
4010
4293
|
const receipt = parseRuntimeEpochReceipt(receiptInput);
|
|
4011
|
-
this.#
|
|
4294
|
+
this.#beginMutation();
|
|
4012
4295
|
try {
|
|
4013
4296
|
const context = readOwnerRuntimeEpochContext(this.#database, receipt.operationId);
|
|
4014
4297
|
if (this.#now() >= context.request.requestedAt + context.request.receiptTtlMs)
|
|
@@ -4022,18 +4305,18 @@ export class ControlPlaneStore {
|
|
|
4022
4305
|
this.#database.prepare("UPDATE deployment_runtime_epochs SET status='applied',receipt_json=? WHERE operation_id=? AND status='claimed'")
|
|
4023
4306
|
.run(JSON.stringify(receipt), receipt.operationId);
|
|
4024
4307
|
const result = this.getRuntimeEpoch(receipt.operationId);
|
|
4025
|
-
this.#
|
|
4308
|
+
this.#commitMutation();
|
|
4026
4309
|
return result;
|
|
4027
4310
|
}
|
|
4028
4311
|
catch (error) {
|
|
4029
|
-
this.#
|
|
4312
|
+
this.#rollbackMutation();
|
|
4030
4313
|
throw error;
|
|
4031
4314
|
}
|
|
4032
4315
|
}
|
|
4033
4316
|
prepareHostAttestationOperation(input) {
|
|
4034
4317
|
return this.#withActivationSource(input.planId, () => {
|
|
4035
4318
|
const now = this.#now();
|
|
4036
|
-
this.#
|
|
4319
|
+
this.#beginMutation();
|
|
4037
4320
|
try {
|
|
4038
4321
|
this.assertOwnerActivationSource(input.planId);
|
|
4039
4322
|
const plan = this.getPlan(input.planId);
|
|
@@ -4085,18 +4368,18 @@ export class ControlPlaneStore {
|
|
|
4085
4368
|
}
|
|
4086
4369
|
if (prior.bindingDigest !== bindingDigest)
|
|
4087
4370
|
throw new ControlPlaneStoreError('conflict', 'durable Host operation payload changed for the same phase');
|
|
4088
|
-
this.#
|
|
4371
|
+
this.#commitMutation();
|
|
4089
4372
|
return prior;
|
|
4090
4373
|
}
|
|
4091
4374
|
this.#database.prepare(`INSERT INTO host_attestation_operations (plan_id, phase, operation_id, binding_digest,
|
|
4092
4375
|
request_digest, request_json, status, receipt_digest, receipt_json, created_at, completed_at, applied_at)
|
|
4093
4376
|
VALUES (?, ?, ?, ?, ?, ?, 'pending', NULL, NULL, ?, NULL, NULL)`).run(plan.id, expected.phase, operationId, bindingDigest, controlPlaneDigest(request), JSON.stringify(request), now);
|
|
4094
4377
|
const operation = this.getHostAttestationOperation(operationId);
|
|
4095
|
-
this.#
|
|
4378
|
+
this.#commitMutation();
|
|
4096
4379
|
return operation;
|
|
4097
4380
|
}
|
|
4098
4381
|
catch (error) {
|
|
4099
|
-
this.#
|
|
4382
|
+
this.#rollbackMutation();
|
|
4100
4383
|
throw error;
|
|
4101
4384
|
}
|
|
4102
4385
|
});
|
|
@@ -4143,7 +4426,7 @@ export class ControlPlaneStore {
|
|
|
4143
4426
|
if (operation.phase === 'rollback' && input.receipt.outcome !== 'passed')
|
|
4144
4427
|
throw new ControlPlaneStoreError('conflict', 'failed physical rollback receipt cannot settle recovery');
|
|
4145
4428
|
const commit = () => {
|
|
4146
|
-
this.#
|
|
4429
|
+
this.#beginMutation();
|
|
4147
4430
|
try {
|
|
4148
4431
|
if (input.requireCurrentSource)
|
|
4149
4432
|
this.assertOwnerActivationSource(plan.id);
|
|
@@ -4157,7 +4440,7 @@ export class ControlPlaneStore {
|
|
|
4157
4440
|
if (controlPlaneDigest(currentOperation.receipt) !== controlPlaneDigest(input.receipt)) {
|
|
4158
4441
|
throw new ControlPlaneStoreError('conflict', 'Host attestation receipt changed after completion');
|
|
4159
4442
|
}
|
|
4160
|
-
this.#
|
|
4443
|
+
this.#commitMutation();
|
|
4161
4444
|
return currentOperation.receipt;
|
|
4162
4445
|
}
|
|
4163
4446
|
const dispatch = this.getHostAttestationDispatchStatus(operation.operationId);
|
|
@@ -4172,11 +4455,11 @@ export class ControlPlaneStore {
|
|
|
4172
4455
|
if (Number(completed.changes) !== 1)
|
|
4173
4456
|
throw new ControlPlaneStoreError('conflict', 'Host attestation dispatch completion lost its claim');
|
|
4174
4457
|
}
|
|
4175
|
-
this.#
|
|
4458
|
+
this.#commitMutation();
|
|
4176
4459
|
return input.receipt;
|
|
4177
4460
|
}
|
|
4178
4461
|
catch (error) {
|
|
4179
|
-
this.#
|
|
4462
|
+
this.#rollbackMutation();
|
|
4180
4463
|
throw error;
|
|
4181
4464
|
}
|
|
4182
4465
|
};
|
|
@@ -4228,7 +4511,7 @@ export class ControlPlaneStore {
|
|
|
4228
4511
|
}
|
|
4229
4512
|
putLiveQualification(batch) {
|
|
4230
4513
|
assertLiveQualificationBatch(batch);
|
|
4231
|
-
this.#
|
|
4514
|
+
this.#beginMutation();
|
|
4232
4515
|
try {
|
|
4233
4516
|
const plan = this.getPlan(batch.planId);
|
|
4234
4517
|
if (plan.status !== 'awaiting-live-tasks' || this.#now() >= batch.deadlineAt || this.#now() >= batch.expiresAt)
|
|
@@ -4242,10 +4525,10 @@ export class ControlPlaneStore {
|
|
|
4242
4525
|
this.#database.prepare(`INSERT INTO live_qualification_batches
|
|
4243
4526
|
(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
4527
|
readLiveQualificationContext(this.#database, batch.id);
|
|
4245
|
-
this.#
|
|
4528
|
+
this.#commitMutation();
|
|
4246
4529
|
}
|
|
4247
4530
|
catch (error) {
|
|
4248
|
-
this.#
|
|
4531
|
+
this.#rollbackMutation();
|
|
4249
4532
|
throw error;
|
|
4250
4533
|
}
|
|
4251
4534
|
}
|
|
@@ -4258,13 +4541,13 @@ export class ControlPlaneStore {
|
|
|
4258
4541
|
throw new ControlPlaneStoreError('not-found', 'live qualification batch absent');
|
|
4259
4542
|
const receipt = verifyLiveQualificationReceipt(input.receipt, record.batch, input.publicKeyPem, this.#now());
|
|
4260
4543
|
return input.withSourceFence(() => {
|
|
4261
|
-
this.#
|
|
4544
|
+
this.#beginMutation();
|
|
4262
4545
|
try {
|
|
4263
4546
|
const current = this.getLiveQualification(record.batch.id);
|
|
4264
4547
|
if (current.state === 'applied') {
|
|
4265
4548
|
if (controlPlaneDigest(current.receipt) !== controlPlaneDigest(receipt))
|
|
4266
4549
|
throw new ControlPlaneStoreError('conflict', 'qualification receipt changed');
|
|
4267
|
-
this.#
|
|
4550
|
+
this.#commitMutation();
|
|
4268
4551
|
return this.getPlan(record.batch.planId);
|
|
4269
4552
|
}
|
|
4270
4553
|
this.assertCurrentLiveQualification(record.batch.id);
|
|
@@ -4277,11 +4560,11 @@ export class ControlPlaneStore {
|
|
|
4277
4560
|
throw new ControlPlaneStoreError('conflict', 'qualification lost plan CAS');
|
|
4278
4561
|
this.#database.prepare("UPDATE live_qualification_batches SET state='applied',receipt_json=?,receipt_digest=? WHERE id=? AND state IN ('pending','signed')")
|
|
4279
4562
|
.run(JSON.stringify(receipt), controlPlaneDigest(receipt), record.batch.id);
|
|
4280
|
-
this.#
|
|
4563
|
+
this.#commitMutation();
|
|
4281
4564
|
return this.getPlan(plan.id);
|
|
4282
4565
|
}
|
|
4283
4566
|
catch (error) {
|
|
4284
|
-
this.#
|
|
4567
|
+
this.#rollbackMutation();
|
|
4285
4568
|
throw error;
|
|
4286
4569
|
}
|
|
4287
4570
|
});
|
|
@@ -4289,7 +4572,7 @@ export class ControlPlaneStore {
|
|
|
4289
4572
|
/** Negative-only Host seam. The signed terms permit conservative removal of unconfirmable qualification. */
|
|
4290
4573
|
invalidateLiveQualification(input) {
|
|
4291
4574
|
const reason = bounded(input.reason, 'qualification invalidation reason', 160);
|
|
4292
|
-
this.#
|
|
4575
|
+
this.#beginMutation();
|
|
4293
4576
|
try {
|
|
4294
4577
|
const plan = this.getPlan(input.planId), record = this.getLiveQualification(input.batchId);
|
|
4295
4578
|
if (!record || record.batch.planId !== plan.id || record.state !== 'applied' || record.receipt?.disposition !== 'qualified'
|
|
@@ -4297,7 +4580,7 @@ export class ControlPlaneStore {
|
|
|
4297
4580
|
throw new ControlPlaneStoreError('conflict', 'qualification invalidation lacks exact accepted evidence');
|
|
4298
4581
|
}
|
|
4299
4582
|
if (plan.status === 'rollback-pending' || plan.status === 'rolled-back') {
|
|
4300
|
-
this.#
|
|
4583
|
+
this.#commitMutation();
|
|
4301
4584
|
return plan;
|
|
4302
4585
|
}
|
|
4303
4586
|
if (!['commit-pending', 'activated'].includes(plan.status) || record.batch.fence !== plan.activation.fence)
|
|
@@ -4321,11 +4604,11 @@ export class ControlPlaneStore {
|
|
|
4321
4604
|
this.#database.prepare(`UPDATE activation_plans SET status='rollback-pending',revision=revision+1,
|
|
4322
4605
|
activation_lease_until=NULL,rollback_profile_restored=0,failure_code='live-qualification-invalidated',updated_at=?
|
|
4323
4606
|
WHERE id=? AND revision=? AND activation_fence=?`).run(now, plan.id, plan.revision, plan.activation.fence);
|
|
4324
|
-
this.#
|
|
4607
|
+
this.#commitMutation();
|
|
4325
4608
|
return this.getPlan(plan.id);
|
|
4326
4609
|
}
|
|
4327
4610
|
catch (error) {
|
|
4328
|
-
this.#
|
|
4611
|
+
this.#rollbackMutation();
|
|
4329
4612
|
throw error;
|
|
4330
4613
|
}
|
|
4331
4614
|
}
|
|
@@ -4343,7 +4626,7 @@ export class ControlPlaneStore {
|
|
|
4343
4626
|
/** Same synchronous transaction owns current-profile selection and capture. */
|
|
4344
4627
|
withForegroundDeployment(task, profilePath, callback) {
|
|
4345
4628
|
assertForegroundTask(task);
|
|
4346
|
-
this.#
|
|
4629
|
+
this.#beginMutation();
|
|
4347
4630
|
try {
|
|
4348
4631
|
const active = this.#database.prepare(`SELECT id FROM activation_plans WHERE target_path = ? AND status IN (
|
|
4349
4632
|
'staging', 'awaiting-reload', 'awaiting-readiness', 'awaiting-live-tasks', 'awaiting-effect-blocked-replay', 'awaiting-shadow',
|
|
@@ -4387,11 +4670,11 @@ export class ControlPlaneStore {
|
|
|
4387
4670
|
const result = callback(plan, hostOperationFromRow(row));
|
|
4388
4671
|
if (result && (typeof result === 'object' || typeof result === 'function') && 'then' in result)
|
|
4389
4672
|
throw new ControlPlaneStoreError('invalid-input', 'foreground capture must be synchronous');
|
|
4390
|
-
this.#
|
|
4673
|
+
this.#commitMutation();
|
|
4391
4674
|
return result;
|
|
4392
4675
|
}
|
|
4393
4676
|
catch (error) {
|
|
4394
|
-
this.#
|
|
4677
|
+
this.#rollbackMutation();
|
|
4395
4678
|
throw error;
|
|
4396
4679
|
}
|
|
4397
4680
|
finally {
|
|
@@ -4458,7 +4741,7 @@ export class ControlPlaneStore {
|
|
|
4458
4741
|
}
|
|
4459
4742
|
putTaskObservation(batch) {
|
|
4460
4743
|
assertTaskObservationBatch(batch);
|
|
4461
|
-
this.#
|
|
4744
|
+
this.#beginMutation();
|
|
4462
4745
|
try {
|
|
4463
4746
|
if (batch.createdAt > this.#now() || batch.expiresAt <= this.#now())
|
|
4464
4747
|
throw new ControlPlaneStoreError('expired', 'task observation is outside its validity interval');
|
|
@@ -4466,22 +4749,39 @@ export class ControlPlaneStore {
|
|
|
4466
4749
|
if (prior) {
|
|
4467
4750
|
if (prior.batch.digest !== batch.digest)
|
|
4468
4751
|
throw new ControlPlaneStoreError('conflict', 'task observation identity reused');
|
|
4469
|
-
this.#
|
|
4752
|
+
this.#commitMutation();
|
|
4470
4753
|
return prior;
|
|
4471
4754
|
}
|
|
4472
4755
|
const encoded = JSON.stringify(batch);
|
|
4473
4756
|
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
4757
|
readTaskObservationContext(this.#database, batch.id);
|
|
4475
|
-
this.#
|
|
4758
|
+
this.#commitMutation();
|
|
4476
4759
|
return Object.freeze({ batch, state: 'pending' });
|
|
4477
4760
|
}
|
|
4478
4761
|
catch (error) {
|
|
4479
|
-
this.#
|
|
4762
|
+
this.#rollbackMutation();
|
|
4480
4763
|
throw error;
|
|
4481
4764
|
}
|
|
4482
4765
|
}
|
|
4483
4766
|
getTaskObservation(id) { return getTaskObservationRecord(this.#database, id); }
|
|
4484
4767
|
assertCurrentTaskObservation(id) { readTaskObservationContext(this.#database, id); }
|
|
4768
|
+
/** Seal one observational trend result into the append-only snapshot ledger; same trend, same row. */
|
|
4769
|
+
sealCreatedCapabilityTrendSnapshot(trend) {
|
|
4770
|
+
const snapshot = createCreationCapabilityTrendSnapshot(trend);
|
|
4771
|
+
this.#beginMutation();
|
|
4772
|
+
try {
|
|
4773
|
+
const result = sealCreationCapabilityTrendSnapshot(this.#database, snapshot, this.#now());
|
|
4774
|
+
this.#commitMutation();
|
|
4775
|
+
return result;
|
|
4776
|
+
}
|
|
4777
|
+
catch (error) {
|
|
4778
|
+
this.#rollbackMutation();
|
|
4779
|
+
throw error;
|
|
4780
|
+
}
|
|
4781
|
+
}
|
|
4782
|
+
listCreatedCapabilityTrendSnapshots(query = {}) {
|
|
4783
|
+
return listCreationCapabilityTrendSnapshots(this.#database, query);
|
|
4784
|
+
}
|
|
4485
4785
|
listTaskObservations(lane, limit = 100) {
|
|
4486
4786
|
if (!KEY.test(lane) || !Number.isSafeInteger(limit) || limit < 1 || limit > 1000)
|
|
4487
4787
|
throw new ControlPlaneStoreError('invalid-input', 'task observation list is invalid');
|
|
@@ -4640,7 +4940,7 @@ export class ControlPlaneStore {
|
|
|
4640
4940
|
const claim = () => {
|
|
4641
4941
|
const candidate = this.getHostAttestationOperation(input.operationId);
|
|
4642
4942
|
return this.#withActivationSource(candidate.planId, () => {
|
|
4643
|
-
this.#
|
|
4943
|
+
this.#beginMutation();
|
|
4644
4944
|
try {
|
|
4645
4945
|
this.assertOwnerActivationSource(candidate.planId);
|
|
4646
4946
|
const operation = this.getHostAttestationOperation(input.operationId), plan = this.getPlan(operation.planId), expected = expectedAttestation[plan.status];
|
|
@@ -4648,17 +4948,17 @@ export class ControlPlaneStore {
|
|
|
4648
4948
|
throw new ControlPlaneStoreError('conflict', 'Host attestation operation lost its plan revision/fence/phase');
|
|
4649
4949
|
const request = this.#assertHostAttestationChain(operation, plan);
|
|
4650
4950
|
if (operation.receipt) {
|
|
4651
|
-
this.#
|
|
4951
|
+
this.#commitMutation();
|
|
4652
4952
|
return { operation, plan, request };
|
|
4653
4953
|
}
|
|
4654
4954
|
if (this.getHostAttestationDispatchStatus(operation.operationId) !== undefined)
|
|
4655
4955
|
throw new ControlPlaneStoreError('conflict', 'Host attestation outcome is unknown; receipt reconciliation is required');
|
|
4656
4956
|
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.#
|
|
4957
|
+
this.#commitMutation();
|
|
4658
4958
|
return { operation, plan, request };
|
|
4659
4959
|
}
|
|
4660
4960
|
catch (error) {
|
|
4661
|
-
this.#
|
|
4961
|
+
this.#rollbackMutation();
|
|
4662
4962
|
throw error;
|
|
4663
4963
|
}
|
|
4664
4964
|
});
|
|
@@ -4725,7 +5025,7 @@ export class ControlPlaneStore {
|
|
|
4725
5025
|
this.#assertAdoptionHandoff(input.planId);
|
|
4726
5026
|
}
|
|
4727
5027
|
const now = this.#now();
|
|
4728
|
-
this.#
|
|
5028
|
+
this.#beginMutation();
|
|
4729
5029
|
try {
|
|
4730
5030
|
this.#withActivationSource(input.planId, () => { }, input.to === 'rollback-pending' || input.to === 'rolled-back');
|
|
4731
5031
|
const result = this.#database.prepare(`UPDATE activation_plans SET status = ?, revision = revision + 1,
|
|
@@ -4744,11 +5044,11 @@ export class ControlPlaneStore {
|
|
|
4744
5044
|
this.#database.prepare(`UPDATE capability_gaps SET status = 'closed', revision = revision + 1, updated_at = ? WHERE id = ?`).run(now, plan.gapId);
|
|
4745
5045
|
this.#finishActivation(plan, input.fence, now);
|
|
4746
5046
|
}
|
|
4747
|
-
this.#
|
|
5047
|
+
this.#commitMutation();
|
|
4748
5048
|
return plan;
|
|
4749
5049
|
}
|
|
4750
5050
|
catch (error) {
|
|
4751
|
-
this.#
|
|
5051
|
+
this.#rollbackMutation();
|
|
4752
5052
|
throw error;
|
|
4753
5053
|
}
|
|
4754
5054
|
}, input.to === 'rollback-pending' || input.to === 'rolled-back');
|
|
@@ -4791,7 +5091,7 @@ export class ControlPlaneStore {
|
|
|
4791
5091
|
const now = this.#now();
|
|
4792
5092
|
const nextStatus = verified.outcome === 'passed' ? (verified.phase === 'readiness' && plan.dossier.liveQualification ? 'awaiting-live-tasks' : expected.next) : 'rollback-pending';
|
|
4793
5093
|
return this.#withActivationSource(input.planId, () => {
|
|
4794
|
-
this.#
|
|
5094
|
+
this.#beginMutation();
|
|
4795
5095
|
try {
|
|
4796
5096
|
this.#withActivationSource(input.planId, () => { }, recovery);
|
|
4797
5097
|
const lockedOperation = this.getHostAttestationOperation(input.receipt.operationId);
|
|
@@ -4822,11 +5122,11 @@ export class ControlPlaneStore {
|
|
|
4822
5122
|
this.#finishActivation(output, input.expectedFence, now);
|
|
4823
5123
|
const operationReceipt = { idempotencyKey: input.idempotencyKey, operation: 'host-attestation', inputDigest, result: output, createdAt: now };
|
|
4824
5124
|
this.#insertReceipt(operationReceipt);
|
|
4825
|
-
this.#
|
|
5125
|
+
this.#commitMutation();
|
|
4826
5126
|
return operationReceipt;
|
|
4827
5127
|
}
|
|
4828
5128
|
catch (error) {
|
|
4829
|
-
this.#
|
|
5129
|
+
this.#rollbackMutation();
|
|
4830
5130
|
throw error;
|
|
4831
5131
|
}
|
|
4832
5132
|
}, recovery);
|
|
@@ -4884,7 +5184,7 @@ export class ControlPlaneStore {
|
|
|
4884
5184
|
}
|
|
4885
5185
|
}
|
|
4886
5186
|
const commit = () => {
|
|
4887
|
-
this.#
|
|
5187
|
+
this.#beginMutation();
|
|
4888
5188
|
try {
|
|
4889
5189
|
if (input.taskObservationId !== undefined) {
|
|
4890
5190
|
const context = readTaskObservationContext(this.#database, input.taskObservationId);
|
|
@@ -4922,11 +5222,11 @@ export class ControlPlaneStore {
|
|
|
4922
5222
|
}
|
|
4923
5223
|
const operationReceipt = { idempotencyKey: key, operation: 'post-activation-observation', inputDigest, result: output, createdAt: now };
|
|
4924
5224
|
this.#insertReceipt(operationReceipt);
|
|
4925
|
-
this.#
|
|
5225
|
+
this.#commitMutation();
|
|
4926
5226
|
return operationReceipt;
|
|
4927
5227
|
}
|
|
4928
5228
|
catch (error) {
|
|
4929
|
-
this.#
|
|
5229
|
+
this.#rollbackMutation();
|
|
4930
5230
|
throw error;
|
|
4931
5231
|
}
|
|
4932
5232
|
};
|
|
@@ -4953,7 +5253,7 @@ export class ControlPlaneStore {
|
|
|
4953
5253
|
}
|
|
4954
5254
|
const verified = await input.resolveAuthority(input.receipt).verify(input.receipt, plan, watch.exact);
|
|
4955
5255
|
const now = this.#now();
|
|
4956
|
-
this.#
|
|
5256
|
+
this.#beginMutation();
|
|
4957
5257
|
try {
|
|
4958
5258
|
const current = this.#database.prepare('SELECT * FROM activation_watch WHERE plan_id = ?').get(plan.id);
|
|
4959
5259
|
if (current.state === 'closed-retracted')
|
|
@@ -4970,11 +5270,11 @@ export class ControlPlaneStore {
|
|
|
4970
5270
|
const output = this.getActivationWatch(plan.id);
|
|
4971
5271
|
const operationReceipt = { idempotencyKey: key, operation: 'activation-retraction', inputDigest, result: output, createdAt: now };
|
|
4972
5272
|
this.#insertReceipt(operationReceipt);
|
|
4973
|
-
this.#
|
|
5273
|
+
this.#commitMutation();
|
|
4974
5274
|
return operationReceipt;
|
|
4975
5275
|
}
|
|
4976
5276
|
catch (error) {
|
|
4977
|
-
this.#
|
|
5277
|
+
this.#rollbackMutation();
|
|
4978
5278
|
throw error;
|
|
4979
5279
|
}
|
|
4980
5280
|
}
|
|
@@ -5060,7 +5360,7 @@ export class ControlPlaneStore {
|
|
|
5060
5360
|
if (!input.succeeded && (input.checkedTreeDigest !== undefined || input.checkedPatchDigest !== undefined)) {
|
|
5061
5361
|
throw new ControlPlaneStoreError('invalid-input', 'failed source checks cannot carry successful check evidence');
|
|
5062
5362
|
}
|
|
5063
|
-
this.#
|
|
5363
|
+
this.#beginMutation();
|
|
5064
5364
|
try {
|
|
5065
5365
|
const result = this.#database.prepare(`UPDATE source_plans SET status = ?, revision = revision + 1,
|
|
5066
5366
|
checked_tree_digest = ?, checked_patch_digest = ?, checked_at = ?, updated_at = ?
|
|
@@ -5074,11 +5374,11 @@ export class ControlPlaneStore {
|
|
|
5074
5374
|
}
|
|
5075
5375
|
const digest = controlPlaneDigest({ planId: output.id, planDigest: output.digest, status, sourceCheck: output.sourceCheck });
|
|
5076
5376
|
this.#insertReceipt({ idempotencyKey: `source-checks:${output.id}`, operation: 'source-local-checks', inputDigest: digest, result: output, createdAt: now });
|
|
5077
|
-
this.#
|
|
5377
|
+
this.#commitMutation();
|
|
5078
5378
|
return output;
|
|
5079
5379
|
}
|
|
5080
5380
|
catch (error) {
|
|
5081
|
-
this.#
|
|
5381
|
+
this.#rollbackMutation();
|
|
5082
5382
|
throw error;
|
|
5083
5383
|
}
|
|
5084
5384
|
}
|
|
@@ -5111,7 +5411,7 @@ export class ControlPlaneStore {
|
|
|
5111
5411
|
}
|
|
5112
5412
|
const commit = () => {
|
|
5113
5413
|
this.#assertOwnerTaskFailureGapAdmission(plan.gapId);
|
|
5114
|
-
this.#
|
|
5414
|
+
this.#beginMutation();
|
|
5115
5415
|
try {
|
|
5116
5416
|
const result = this.#database.prepare(`UPDATE source_plans SET status = 'ready-for-human-review', revision = revision + 1, updated_at = ?
|
|
5117
5417
|
WHERE id = ? AND revision = ? AND status = 'approved' AND mode = 'modify'
|
|
@@ -5124,11 +5424,11 @@ export class ControlPlaneStore {
|
|
|
5124
5424
|
const receipt = { idempotencyKey: `source-verify-prepared:${output.id}:${output.revision}`,
|
|
5125
5425
|
operation: 'source-verify-prepared', inputDigest, result: output, createdAt: now };
|
|
5126
5426
|
this.#insertReceipt(receipt);
|
|
5127
|
-
this.#
|
|
5427
|
+
this.#commitMutation();
|
|
5128
5428
|
return receipt;
|
|
5129
5429
|
}
|
|
5130
5430
|
catch (error) {
|
|
5131
|
-
this.#
|
|
5431
|
+
this.#rollbackMutation();
|
|
5132
5432
|
throw error;
|
|
5133
5433
|
}
|
|
5134
5434
|
};
|
|
@@ -5188,7 +5488,7 @@ export class ControlPlaneStore {
|
|
|
5188
5488
|
lineageDigest = controlPlaneDigest({ history: prehistory, maintenance: premaintenance, trust: input.trust, baseline: prebaseline });
|
|
5189
5489
|
}
|
|
5190
5490
|
return withCurrentSource(plan.gapId, () => {
|
|
5191
|
-
this.#
|
|
5491
|
+
this.#beginMutation();
|
|
5192
5492
|
try {
|
|
5193
5493
|
const linkedRows = this.#database.prepare('SELECT * FROM source_jobs WHERE plan_id = ? LIMIT 2')
|
|
5194
5494
|
.all(plan.id);
|
|
@@ -5243,11 +5543,11 @@ export class ControlPlaneStore {
|
|
|
5243
5543
|
const output = this.getSourcePlan(plan.id);
|
|
5244
5544
|
const operationReceipt = { idempotencyKey: key, operation: 'start-source-release', inputDigest, result: output, createdAt: now };
|
|
5245
5545
|
this.#insertReceipt(operationReceipt);
|
|
5246
|
-
this.#
|
|
5546
|
+
this.#commitMutation();
|
|
5247
5547
|
return operationReceipt;
|
|
5248
5548
|
}
|
|
5249
5549
|
catch (error) {
|
|
5250
|
-
this.#
|
|
5550
|
+
this.#rollbackMutation();
|
|
5251
5551
|
throw error;
|
|
5252
5552
|
}
|
|
5253
5553
|
});
|
|
@@ -5271,7 +5571,7 @@ export class ControlPlaneStore {
|
|
|
5271
5571
|
await reverifySourceReleaseAuthorization(plan.releaseAuthorization, plan, input.resolveAuthorizationAuthority);
|
|
5272
5572
|
return withCurrentSource(plan.gapId, () => {
|
|
5273
5573
|
const now = this.#now();
|
|
5274
|
-
this.#
|
|
5574
|
+
this.#beginMutation();
|
|
5275
5575
|
try {
|
|
5276
5576
|
const current = this.getSourcePlan(plan.id);
|
|
5277
5577
|
const currentExpected = expectedSourceRelease(current.status);
|
|
@@ -5291,7 +5591,7 @@ export class ControlPlaneStore {
|
|
|
5291
5591
|
if (prior.bindingDigest !== controlPlaneDigest(releaseRequestBinding(expectedRequest))) {
|
|
5292
5592
|
throw new ControlPlaneStoreError('conflict', 'durable release operation payload changed for the same phase/fence');
|
|
5293
5593
|
}
|
|
5294
|
-
this.#
|
|
5594
|
+
this.#commitMutation();
|
|
5295
5595
|
return prior;
|
|
5296
5596
|
}
|
|
5297
5597
|
const operationId = `release-operation-${randomUUID()}`;
|
|
@@ -5301,11 +5601,11 @@ export class ControlPlaneStore {
|
|
|
5301
5601
|
operation_id, binding_digest, request_digest, request_json, status, receipt_digest, receipt_json, created_at, completed_at, applied_at)
|
|
5302
5602
|
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
5603
|
const operation = this.getSourceReleaseOperation(operationId);
|
|
5304
|
-
this.#
|
|
5604
|
+
this.#commitMutation();
|
|
5305
5605
|
return operation;
|
|
5306
5606
|
}
|
|
5307
5607
|
catch (error) {
|
|
5308
|
-
this.#
|
|
5608
|
+
this.#rollbackMutation();
|
|
5309
5609
|
throw error;
|
|
5310
5610
|
}
|
|
5311
5611
|
});
|
|
@@ -5370,13 +5670,13 @@ export class ControlPlaneStore {
|
|
|
5370
5670
|
withCurrentSource(plan.gapId, () => { });
|
|
5371
5671
|
await reverifySourceReleaseAuthorization(operation.request.authorization, plan, input.resolveAuthorizationAuthority);
|
|
5372
5672
|
const claimed = withCurrentSource(plan.gapId, () => {
|
|
5373
|
-
this.#
|
|
5673
|
+
this.#beginMutation();
|
|
5374
5674
|
try {
|
|
5375
5675
|
const current = this.getSourcePlan(plan.id);
|
|
5376
5676
|
const currentOperation = this.getSourceReleaseOperation(operation.operationId);
|
|
5377
5677
|
assertCurrent(current, currentOperation);
|
|
5378
5678
|
if (currentOperation.receipt !== undefined) {
|
|
5379
|
-
this.#
|
|
5679
|
+
this.#commitMutation();
|
|
5380
5680
|
return false;
|
|
5381
5681
|
}
|
|
5382
5682
|
const dispatch = this.#database.prepare('SELECT * FROM source_release_dispatches WHERE operation_id = ?').get(operation.operationId);
|
|
@@ -5386,11 +5686,11 @@ export class ControlPlaneStore {
|
|
|
5386
5686
|
}
|
|
5387
5687
|
this.#database.prepare(`INSERT INTO source_release_dispatches (operation_id, status, claimed_at, completed_at)
|
|
5388
5688
|
VALUES (?, 'claimed', ?, NULL)`).run(operation.operationId, this.#now());
|
|
5389
|
-
this.#
|
|
5689
|
+
this.#commitMutation();
|
|
5390
5690
|
return true;
|
|
5391
5691
|
}
|
|
5392
5692
|
catch (error) {
|
|
5393
|
-
this.#
|
|
5693
|
+
this.#rollbackMutation();
|
|
5394
5694
|
throw error;
|
|
5395
5695
|
}
|
|
5396
5696
|
});
|
|
@@ -5399,7 +5699,7 @@ export class ControlPlaneStore {
|
|
|
5399
5699
|
const receipt = await input.execute(operation.request);
|
|
5400
5700
|
await input.resolveAuthority(receipt).verify(receipt, plan, operation.request);
|
|
5401
5701
|
return withCurrentSource(plan.gapId, () => {
|
|
5402
|
-
this.#
|
|
5702
|
+
this.#beginMutation();
|
|
5403
5703
|
try {
|
|
5404
5704
|
const current = this.getSourcePlan(plan.id);
|
|
5405
5705
|
const currentOperation = this.getSourceReleaseOperation(operation.operationId);
|
|
@@ -5417,11 +5717,11 @@ export class ControlPlaneStore {
|
|
|
5417
5717
|
WHERE operation_id = ? AND status = 'claimed'`).run(now, operation.operationId);
|
|
5418
5718
|
if (Number(completed.changes) !== 1)
|
|
5419
5719
|
throw new ControlPlaneStoreError('conflict', 'source release completion lost its dispatch claim');
|
|
5420
|
-
this.#
|
|
5720
|
+
this.#commitMutation();
|
|
5421
5721
|
return receipt;
|
|
5422
5722
|
}
|
|
5423
5723
|
catch (error) {
|
|
5424
|
-
this.#
|
|
5724
|
+
this.#rollbackMutation();
|
|
5425
5725
|
throw error;
|
|
5426
5726
|
}
|
|
5427
5727
|
});
|
|
@@ -5455,14 +5755,14 @@ export class ControlPlaneStore {
|
|
|
5455
5755
|
await reverifySourceReleaseAuthorization(operation.request.authorization, plan, input.resolveAuthorizationAuthority);
|
|
5456
5756
|
await input.resolveAuthority(input.receipt).verify(input.receipt, plan, operation.request);
|
|
5457
5757
|
return withCurrentSource(plan.gapId, () => {
|
|
5458
|
-
this.#
|
|
5758
|
+
this.#beginMutation();
|
|
5459
5759
|
try {
|
|
5460
5760
|
const current = this.getSourcePlan(plan.id), currentOperation = this.getSourceReleaseOperation(operation.operationId);
|
|
5461
5761
|
assertCurrent(current, currentOperation);
|
|
5462
5762
|
if (currentOperation.receipt !== undefined) {
|
|
5463
5763
|
if (controlPlaneDigest(currentOperation.receipt) !== controlPlaneDigest(input.receipt))
|
|
5464
5764
|
throw new ControlPlaneStoreError('conflict', 'source release receipt differs from completed operation');
|
|
5465
|
-
this.#
|
|
5765
|
+
this.#commitMutation();
|
|
5466
5766
|
return currentOperation.receipt;
|
|
5467
5767
|
}
|
|
5468
5768
|
const dispatch = this.#database.prepare('SELECT * FROM source_release_dispatches WHERE operation_id = ?').get(operation.operationId);
|
|
@@ -5479,11 +5779,11 @@ export class ControlPlaneStore {
|
|
|
5479
5779
|
if (Number(completed.changes) !== 1)
|
|
5480
5780
|
throw new ControlPlaneStoreError('conflict', 'source release receipt lost its dispatch claim');
|
|
5481
5781
|
}
|
|
5482
|
-
this.#
|
|
5782
|
+
this.#commitMutation();
|
|
5483
5783
|
return input.receipt;
|
|
5484
5784
|
}
|
|
5485
5785
|
catch (error) {
|
|
5486
|
-
this.#
|
|
5786
|
+
this.#rollbackMutation();
|
|
5487
5787
|
throw error;
|
|
5488
5788
|
}
|
|
5489
5789
|
});
|
|
@@ -5549,7 +5849,7 @@ export class ControlPlaneStore {
|
|
|
5549
5849
|
}
|
|
5550
5850
|
const now = this.#now();
|
|
5551
5851
|
return withCurrentSource(plan.gapId, () => {
|
|
5552
|
-
this.#
|
|
5852
|
+
this.#beginMutation();
|
|
5553
5853
|
try {
|
|
5554
5854
|
const current = this.getSourcePlan(plan.id);
|
|
5555
5855
|
const currentExpected = expectedSourceRelease(current.status);
|
|
@@ -5589,18 +5889,18 @@ export class ControlPlaneStore {
|
|
|
5589
5889
|
}
|
|
5590
5890
|
const operationReceipt = { idempotencyKey: key, operation: 'source-release', inputDigest, result: output, createdAt: now };
|
|
5591
5891
|
this.#insertReceipt(operationReceipt);
|
|
5592
|
-
this.#
|
|
5892
|
+
this.#commitMutation();
|
|
5593
5893
|
return operationReceipt;
|
|
5594
5894
|
}
|
|
5595
5895
|
catch (error) {
|
|
5596
|
-
this.#
|
|
5896
|
+
this.#rollbackMutation();
|
|
5597
5897
|
throw error;
|
|
5598
5898
|
}
|
|
5599
5899
|
});
|
|
5600
5900
|
}
|
|
5601
5901
|
async prepareSourcePublishReconciliation(input) {
|
|
5602
5902
|
const now = this.#now();
|
|
5603
|
-
this.#
|
|
5903
|
+
this.#beginMutation();
|
|
5604
5904
|
try {
|
|
5605
5905
|
const plan = this.getSourcePlan(input.planId);
|
|
5606
5906
|
if (plan.status !== 'publish-ambiguous' || plan.revision !== input.expectedRevision || plan.release?.fence !== input.expectedFence) {
|
|
@@ -5643,18 +5943,18 @@ export class ControlPlaneStore {
|
|
|
5643
5943
|
const prior = sourcePublishReconciliationFromRow(priorRow);
|
|
5644
5944
|
if (prior.bindingDigest !== bindingDigest)
|
|
5645
5945
|
throw new ControlPlaneStoreError('conflict', 'durable publish reconciliation payload changed');
|
|
5646
|
-
this.#
|
|
5946
|
+
this.#commitMutation();
|
|
5647
5947
|
return prior;
|
|
5648
5948
|
}
|
|
5649
5949
|
this.#database.prepare(`INSERT INTO source_publish_reconciliations (plan_id, release_id, release_fence, attempt, operation_id,
|
|
5650
5950
|
binding_digest, request_digest, request_json, status, receipt_digest, receipt_json, created_at, completed_at, applied_at)
|
|
5651
5951
|
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
5952
|
const operation = this.getSourcePublishReconciliationOperation(operationId);
|
|
5653
|
-
this.#
|
|
5953
|
+
this.#commitMutation();
|
|
5654
5954
|
return operation;
|
|
5655
5955
|
}
|
|
5656
5956
|
catch (error) {
|
|
5657
|
-
this.#
|
|
5957
|
+
this.#rollbackMutation();
|
|
5658
5958
|
throw error;
|
|
5659
5959
|
}
|
|
5660
5960
|
}
|
|
@@ -5666,7 +5966,7 @@ export class ControlPlaneStore {
|
|
|
5666
5966
|
}
|
|
5667
5967
|
async runSourcePublishReconciliation(input) {
|
|
5668
5968
|
try {
|
|
5669
|
-
this.#
|
|
5969
|
+
this.#beginMutation();
|
|
5670
5970
|
}
|
|
5671
5971
|
catch {
|
|
5672
5972
|
throw new ControlPlaneStoreError('conflict', 'publish reconciliation single-flight is held');
|
|
@@ -5678,7 +5978,7 @@ export class ControlPlaneStore {
|
|
|
5678
5978
|
|| operation.fence !== input.expectedFence)
|
|
5679
5979
|
throw new ControlPlaneStoreError('conflict', 'publish reconciliation lost its revision/fence');
|
|
5680
5980
|
if (operation.receipt !== undefined) {
|
|
5681
|
-
this.#
|
|
5981
|
+
this.#commitMutation();
|
|
5682
5982
|
return operation.receipt;
|
|
5683
5983
|
}
|
|
5684
5984
|
await reverifySourceReleaseAuthorization(operation.request.authorization, plan, input.resolveAuthorizationAuthority);
|
|
@@ -5689,11 +5989,11 @@ export class ControlPlaneStore {
|
|
|
5689
5989
|
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
5990
|
if (Number(result.changes) !== 1)
|
|
5691
5991
|
throw new ControlPlaneStoreError('conflict', 'publish reconciliation completion lost single-flight');
|
|
5692
|
-
this.#
|
|
5992
|
+
this.#commitMutation();
|
|
5693
5993
|
return receipt;
|
|
5694
5994
|
}
|
|
5695
5995
|
catch (error) {
|
|
5696
|
-
this.#
|
|
5996
|
+
this.#rollbackMutation();
|
|
5697
5997
|
throw error;
|
|
5698
5998
|
}
|
|
5699
5999
|
}
|
|
@@ -5726,7 +6026,7 @@ export class ControlPlaneStore {
|
|
|
5726
6026
|
const verified = await input.resolveAuthority(input.receipt).verify(input.receipt, plan, operation.request);
|
|
5727
6027
|
const outcome = verified.evidence.outcome;
|
|
5728
6028
|
const now = this.#now();
|
|
5729
|
-
this.#
|
|
6029
|
+
this.#beginMutation();
|
|
5730
6030
|
try {
|
|
5731
6031
|
let nextStatus = 'publish-ambiguous';
|
|
5732
6032
|
let nextFence = input.expectedFence;
|
|
@@ -5759,11 +6059,11 @@ export class ControlPlaneStore {
|
|
|
5759
6059
|
const output = this.getSourcePlan(plan.id);
|
|
5760
6060
|
const operationReceipt = { idempotencyKey: input.idempotencyKey, operation: 'reconcile-source-publish', inputDigest, result: output, createdAt: now };
|
|
5761
6061
|
this.#insertReceipt(operationReceipt);
|
|
5762
|
-
this.#
|
|
6062
|
+
this.#commitMutation();
|
|
5763
6063
|
return operationReceipt;
|
|
5764
6064
|
}
|
|
5765
6065
|
catch (error) {
|
|
5766
|
-
this.#
|
|
6066
|
+
this.#rollbackMutation();
|
|
5767
6067
|
throw error;
|
|
5768
6068
|
}
|
|
5769
6069
|
}
|