@dsh-enhanced/plugin-control-plane 0.1.66 → 0.1.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -22
- package/lib/cli.d.ts.map +1 -1
- package/lib/cli.js +66 -11
- package/lib/cli.js.map +1 -1
- package/lib/creation-capability-execution.d.ts +5 -1
- package/lib/creation-capability-execution.d.ts.map +1 -1
- package/lib/creation-capability-execution.js +42 -0
- package/lib/creation-capability-execution.js.map +1 -1
- package/lib/creation-capability-feedback.d.ts +16 -1
- package/lib/creation-capability-feedback.d.ts.map +1 -1
- package/lib/creation-capability-feedback.js +82 -5
- package/lib/creation-capability-feedback.js.map +1 -1
- package/lib/creation-capability-journal.d.ts.map +1 -1
- package/lib/creation-capability-journal.js +2 -0
- package/lib/creation-capability-journal.js.map +1 -1
- package/lib/creation-capability-runtime.d.ts +47 -1
- package/lib/creation-capability-runtime.d.ts.map +1 -1
- package/lib/creation-capability-runtime.js +105 -1
- package/lib/creation-capability-runtime.js.map +1 -1
- package/lib/creation-capability-types.d.ts +1 -0
- package/lib/creation-capability-types.d.ts.map +1 -1
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/linux-source-build.d.ts +13 -2
- package/lib/linux-source-build.d.ts.map +1 -1
- package/lib/linux-source-build.js.map +1 -1
- package/lib/live-qualification-authority.js +2 -2
- package/lib/live-qualification-authority.js.map +1 -1
- package/lib/owner-instruction-drain.d.ts +60 -0
- package/lib/owner-instruction-drain.d.ts.map +1 -0
- package/lib/owner-instruction-drain.js +101 -0
- package/lib/owner-instruction-drain.js.map +1 -0
- package/lib/revision-capability-feedback.d.ts +165 -0
- package/lib/revision-capability-feedback.d.ts.map +1 -0
- package/lib/revision-capability-feedback.js +316 -0
- package/lib/revision-capability-feedback.js.map +1 -0
- package/lib/revision-capability-journal.d.ts +126 -0
- package/lib/revision-capability-journal.d.ts.map +1 -0
- package/lib/revision-capability-journal.js +981 -0
- package/lib/revision-capability-journal.js.map +1 -0
- package/lib/revision-capability-runtime.d.ts +92 -0
- package/lib/revision-capability-runtime.d.ts.map +1 -0
- package/lib/revision-capability-runtime.js +635 -0
- package/lib/revision-capability-runtime.js.map +1 -0
- package/lib/revision-capability-types.d.ts +335 -0
- package/lib/revision-capability-types.d.ts.map +1 -0
- package/lib/revision-capability-types.js +2 -0
- package/lib/revision-capability-types.js.map +1 -0
- package/lib/service.d.ts +199 -4
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +925 -70
- package/lib/service.js.map +1 -1
- package/lib/source-adoption-authority.js +2 -2
- package/lib/source-adoption-authority.js.map +1 -1
- package/lib/source-approval-authority.js +3 -3
- package/lib/source-approval-authority.js.map +1 -1
- package/lib/source-build.d.ts +33 -53
- package/lib/source-build.d.ts.map +1 -1
- package/lib/source-build.js +28 -408
- package/lib/source-build.js.map +1 -1
- package/lib/source-job-types.d.ts +111 -4
- package/lib/source-job-types.d.ts.map +1 -1
- package/lib/source-jobs.d.ts +72 -3
- package/lib/source-jobs.d.ts.map +1 -1
- package/lib/source-jobs.js +314 -43
- package/lib/source-jobs.js.map +1 -1
- package/lib/source-release-authority.js +2 -2
- package/lib/source-release-authority.js.map +1 -1
- package/lib/source-revision.d.ts +29 -3
- package/lib/source-revision.d.ts.map +1 -1
- package/lib/source-revision.js +40 -9
- package/lib/source-revision.js.map +1 -1
- package/lib/source-workspace.d.ts +11 -4
- package/lib/source-workspace.d.ts.map +1 -1
- package/lib/source-workspace.js +51 -18
- package/lib/source-workspace.js.map +1 -1
- package/lib/sqlite.d.ts +6 -7
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +255 -17
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +143 -4
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +739 -66
- package/lib/store.js.map +1 -1
- package/lib/systemd-host-authority.js +4 -4
- package/lib/systemd-host-authority.js.map +1 -1
- package/lib/task-observation-authority.js +2 -2
- package/lib/task-observation-authority.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/work-item-creation-contract.d.ts +3 -1
- package/lib/work-item-creation-contract.d.ts.map +1 -1
- package/lib/work-item-creation-contract.js +10 -1
- package/lib/work-item-creation-contract.js.map +1 -1
- package/package.json +11 -8
package/lib/store.js
CHANGED
|
@@ -9,13 +9,13 @@ import { discover, parseCatalog } from './catalog.js';
|
|
|
9
9
|
import { parseApprovalReceipt } from './approval.js';
|
|
10
10
|
import { parseSourcePublishReconciliationReceipt, parseSourcePublishReconciliationRequest, parseSourceReleaseAuthorization, parseSourceReleaseReceipt, parseSourceReleaseRequest, parseVerifiedSourceReleaseAuthorization } from './release.js';
|
|
11
11
|
import { controlPlaneOperationReceiptDigest, controlPlaneSchemaVersion, openControlPlaneDatabase, openControlPlaneDatabaseReadOnly } from './sqlite.js';
|
|
12
|
-
import { validateSourceBuildConfig } from './source-build.js';
|
|
12
|
+
import { isLegacyDockerSourceBuildConfig, validateLegacyDockerSourceBuildConfig, validateSourceBuildConfig } from './source-build.js';
|
|
13
13
|
import { validateSourceBaselineConfig, verifySourceBaselineHistory } from './source-baseline.js';
|
|
14
14
|
import { validateScopedPluginFiles } from './source-workspace.js';
|
|
15
15
|
import { validateCreationCapabilitySource } from './creation-capability-source.js';
|
|
16
16
|
import { validateSourceRevisionBinding } from './source-revision.js';
|
|
17
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
|
+
import { anySourceGrowthRunDigest as sourceGrowthRunDigest, growthObjectDigest, isSourceOwnerContinuation, isPluginRevisionTaskSource, validateAnySourceGrowthRunBinding, validatePluginRevisionRegressionCertificate, validatePluginRevisionVerificationCertificate, validatePluginCreationVerificationCertificate, validateRevisionAcceptanceAuthorityRef } from '@dsh-enhanced/assistant-growth-contract';
|
|
19
19
|
import { validateAdoptionHandoffTerms } from './adoption-handoff.js';
|
|
20
20
|
import { assertForegroundDeployment, assertForegroundTask } from './foreground-deployment.js';
|
|
21
21
|
import { assertTaskObservationBatch, getTaskObservationRecord, readTaskObservationContext } from './task-observation-store.js';
|
|
@@ -125,6 +125,102 @@ function canonical(value) {
|
|
|
125
125
|
export function controlPlaneDigest(value) {
|
|
126
126
|
return createHash('sha256').update(canonical(value)).digest('hex');
|
|
127
127
|
}
|
|
128
|
+
const OWNER_INSTRUCTION_REVIEW_FREEZE_PROTOCOL = 'plugin-control-plane/owner-instruction-review-freeze/v1';
|
|
129
|
+
const OWNER_INSTRUCTION_REVIEW_BUDGET_PROTOCOL = 'plugin-control-plane/owner-instruction-review-budget/v1';
|
|
130
|
+
const REVIEW_SUPPLIER_BYTES = 512;
|
|
131
|
+
const REVIEW_WINDOW_MAX_MS = 86_400_000;
|
|
132
|
+
function reviewPlainRecord(value, keys, label) {
|
|
133
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype
|
|
134
|
+
|| Object.getOwnPropertySymbols(value).length !== 0) {
|
|
135
|
+
throw new ControlPlaneStoreError('invalid-input', label + ' is invalid');
|
|
136
|
+
}
|
|
137
|
+
const record = value;
|
|
138
|
+
const own = Object.keys(record);
|
|
139
|
+
if (own.length !== keys.length || !keys.every(key => Object.hasOwn(record, key))) {
|
|
140
|
+
throw new ControlPlaneStoreError('invalid-input', label + ' is invalid');
|
|
141
|
+
}
|
|
142
|
+
return record;
|
|
143
|
+
}
|
|
144
|
+
function reviewSupplierText(value, label) {
|
|
145
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > REVIEW_SUPPLIER_BYTES
|
|
146
|
+
|| Buffer.byteLength(value, 'utf8') > REVIEW_SUPPLIER_BYTES || value.trim() !== value || /\p{Cc}/u.test(value)) {
|
|
147
|
+
throw new ControlPlaneStoreError('invalid-input', label + ' is invalid');
|
|
148
|
+
}
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
function reviewSupplierOf(value) {
|
|
152
|
+
const hasEffort = value !== null && typeof value === 'object' && !Array.isArray(value) && Object.hasOwn(value, 'reasoningEffort');
|
|
153
|
+
const item = reviewPlainRecord(value, hasEffort ? ['provider', 'model', 'reasoningEffort'] : ['provider', 'model'], 'review supplier');
|
|
154
|
+
return Object.freeze({
|
|
155
|
+
provider: reviewSupplierText(item['provider'], 'review supplier provider'),
|
|
156
|
+
model: reviewSupplierText(item['model'], 'review supplier model'),
|
|
157
|
+
...(item['reasoningEffort'] === undefined ? {} : { reasoningEffort: reviewSupplierText(item['reasoningEffort'], 'review supplier reasoning effort') }),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
export function validateOwnerInstructionReviewWindow(value) {
|
|
161
|
+
const item = reviewPlainRecord(value, ['verificationMs', 'regressionMs', 'adoptionMs'], 'owner instruction review window');
|
|
162
|
+
const window = { verificationMs: item['verificationMs'], regressionMs: item['regressionMs'], adoptionMs: item['adoptionMs'] };
|
|
163
|
+
for (const [key, item] of Object.entries(window)) {
|
|
164
|
+
if (!Number.isSafeInteger(item) || item < 0 || item > REVIEW_WINDOW_MAX_MS) {
|
|
165
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction review window ' + key + ' is invalid');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return Object.freeze(window);
|
|
169
|
+
}
|
|
170
|
+
function reviewBudgetOf(value) {
|
|
171
|
+
const item = reviewPlainRecord(value, ['protocol', 'authority', 'verifierKeyDigest', 'window'], 'owner instruction review budget');
|
|
172
|
+
if (item['protocol'] !== OWNER_INSTRUCTION_REVIEW_BUDGET_PROTOCOL || typeof item['verifierKeyDigest'] !== 'string'
|
|
173
|
+
|| !/^[a-f0-9]{64}$/u.test(item['verifierKeyDigest'])) {
|
|
174
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction review budget is invalid');
|
|
175
|
+
}
|
|
176
|
+
const authority = structuredClone(item['authority']);
|
|
177
|
+
try {
|
|
178
|
+
validateRevisionAcceptanceAuthorityRef(authority);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction review authority is invalid');
|
|
182
|
+
}
|
|
183
|
+
return Object.freeze({ protocol: OWNER_INSTRUCTION_REVIEW_BUDGET_PROTOCOL, authority: Object.freeze(authority),
|
|
184
|
+
verifierKeyDigest: item['verifierKeyDigest'], window: validateOwnerInstructionReviewWindow(item['window']) });
|
|
185
|
+
}
|
|
186
|
+
function ownerInstructionReviewFreezePayload(supplier, budget) {
|
|
187
|
+
return { protocol: OWNER_INSTRUCTION_REVIEW_FREEZE_PROTOCOL,
|
|
188
|
+
supplier: Object.freeze({ ...supplier }), budget };
|
|
189
|
+
}
|
|
190
|
+
/** Validates one stored freeze, including its self-digest. */
|
|
191
|
+
export function validateOwnerInstructionReviewFreeze(value) {
|
|
192
|
+
const item = reviewPlainRecord(value, ['protocol', 'supplier', 'budget', 'digest'], 'owner instruction review freeze');
|
|
193
|
+
if (item['protocol'] !== OWNER_INSTRUCTION_REVIEW_FREEZE_PROTOCOL || typeof item['digest'] !== 'string'
|
|
194
|
+
|| !/^[a-f0-9]{64}$/u.test(item['digest'])) {
|
|
195
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction review freeze is invalid');
|
|
196
|
+
}
|
|
197
|
+
const supplier = reviewSupplierOf(item['supplier']);
|
|
198
|
+
const budget = reviewBudgetOf(item['budget']);
|
|
199
|
+
if (controlPlaneDigest(ownerInstructionReviewFreezePayload(supplier, budget)) !== item['digest']) {
|
|
200
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction review freeze digest differs');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** Builds the immutable review freeze written with a new owner instruction. */
|
|
204
|
+
export function createOwnerInstructionReviewFreeze(input) {
|
|
205
|
+
if (!input || typeof input !== 'object')
|
|
206
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction review freeze is invalid');
|
|
207
|
+
const supplier = reviewSupplierOf(input.supplier);
|
|
208
|
+
const budget = reviewBudgetOf(input.budget);
|
|
209
|
+
const unsigned = ownerInstructionReviewFreezePayload(supplier, budget);
|
|
210
|
+
const freeze = Object.freeze({ ...unsigned, digest: controlPlaneDigest(unsigned) });
|
|
211
|
+
validateOwnerInstructionReviewFreeze(freeze);
|
|
212
|
+
return freeze;
|
|
213
|
+
}
|
|
214
|
+
/** True only when a stored freeze is structurally valid and self-consistent. */
|
|
215
|
+
export function ownerInstructionReviewFreezeMatches(value) {
|
|
216
|
+
try {
|
|
217
|
+
validateOwnerInstructionReviewFreeze(value);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
128
224
|
/** Structural validation precedes this exact prepared-build binding check. */
|
|
129
225
|
function creationVerificationMatchesPreparedEvidence(certificate, evidence) {
|
|
130
226
|
if (certificate.protocol === 'assistant-growth/creation-verification/v1') {
|
|
@@ -145,6 +241,10 @@ function creationVerificationMatchesPreparedEvidence(certificate, evidence) {
|
|
|
145
241
|
}
|
|
146
242
|
/** Legacy revision v1 has no execution claim; Linux requires its own signed build provenance. */
|
|
147
243
|
function revisionVerificationMatchesPreparedEvidence(certificate, evidence) {
|
|
244
|
+
// v3 binds review evidence through the signed plan and instruction. It
|
|
245
|
+
// carries no native execution provenance and cannot authorize adoption.
|
|
246
|
+
if (certificate.protocol === 'assistant-growth/revision-verification/v3')
|
|
247
|
+
return !Object.hasOwn(certificate, 'execution');
|
|
148
248
|
if (certificate.protocol === 'assistant-growth/revision-verification/v1') {
|
|
149
249
|
return evidence.schemaVersion !== 3 && !Object.hasOwn(certificate, 'execution');
|
|
150
250
|
}
|
|
@@ -241,9 +341,9 @@ function creationBindingFromStored(value) {
|
|
|
241
341
|
return Object.freeze({ grant: Object.freeze(normalizeSourceCreationGrant(binding['grant'])), generatorDigest: binding['generatorDigest'],
|
|
242
342
|
...(growthRun === undefined ? {} : { growthRun: Object.freeze(growthRun) }) });
|
|
243
343
|
}
|
|
244
|
-
function revisionBindingFromStored(value) {
|
|
344
|
+
function revisionBindingFromStored(value, options) {
|
|
245
345
|
try {
|
|
246
|
-
validateSourceRevisionBinding(value);
|
|
346
|
+
validateSourceRevisionBinding(value, options);
|
|
247
347
|
}
|
|
248
348
|
catch {
|
|
249
349
|
throw new ControlPlaneStoreError('invalid-input', 'source revision binding is invalid');
|
|
@@ -828,7 +928,7 @@ function preparedArtifactBytesFromRow(row, pack) {
|
|
|
828
928
|
}
|
|
829
929
|
return bytes;
|
|
830
930
|
}
|
|
831
|
-
function sourceFromRow(row) {
|
|
931
|
+
function sourceFromRow(row, allowWithoutGrowthRun = false) {
|
|
832
932
|
if (row.mode !== 'create' && row.mode !== 'modify' && row.mode !== 'prepared-create' && row.mode !== 'prepared-revise') {
|
|
833
933
|
throw new ControlPlaneStoreError('invalid-state', 'stored source plan has an unknown mode');
|
|
834
934
|
}
|
|
@@ -861,7 +961,7 @@ function sourceFromRow(row) {
|
|
|
861
961
|
throw new ControlPlaneStoreError('invalid-state', 'stored source creation binding is corrupt');
|
|
862
962
|
}
|
|
863
963
|
}
|
|
864
|
-
const revision = row.revision_json === null ? undefined : revisionBindingFromStored(JSON.parse(row.revision_json));
|
|
964
|
+
const revision = row.revision_json === null ? undefined : revisionBindingFromStored(JSON.parse(row.revision_json), { allowWithoutGrowthRun });
|
|
865
965
|
const prepared = row.mode === 'modify' || row.mode === 'prepared-create' || row.mode === 'prepared-revise';
|
|
866
966
|
if (prepared !== (preparedEvidence !== undefined) || (row.mode === 'modify' && preparedEvidence?.schemaVersion === 2)
|
|
867
967
|
|| (row.mode === 'prepared-create') !== (creation !== undefined) || (row.mode === 'prepared-revise') !== (revision !== undefined)
|
|
@@ -929,7 +1029,7 @@ function sourceFromRow(row) {
|
|
|
929
1029
|
...(row.release_failure_phase === null ? {} : { failurePhase: row.release_failure_phase }),
|
|
930
1030
|
...(row.release_failure_code === null ? {} : { failureCode: row.release_failure_code }), updatedAt: row.updated_at } }) };
|
|
931
1031
|
}
|
|
932
|
-
function sourceSnapshotFromStored(value) {
|
|
1032
|
+
function sourceSnapshotFromStored(value, allowWithoutGrowthRun = false) {
|
|
933
1033
|
const item = objectRecord(value, 'stored source plan snapshot');
|
|
934
1034
|
// `mode` was introduced in schema v13; receipt rows written by older binaries
|
|
935
1035
|
// omit it and are backfilled to 'create'. `preparedEvidence` exists only on
|
|
@@ -967,7 +1067,7 @@ function sourceSnapshotFromStored(value) {
|
|
|
967
1067
|
generatorDigest: item['generatorDigest'], scope: item['scope'], createdAt: item['createdAt'], expiresAt: item['expiresAt'] };
|
|
968
1068
|
const preparedEvidence = item['preparedEvidence'] === undefined ? undefined : preparedEvidenceFromStored(item['preparedEvidence']);
|
|
969
1069
|
const creation = item['creation'] === undefined ? undefined : creationBindingFromStored(item['creation']);
|
|
970
|
-
const revision = item['sourceRevision'] === undefined ? undefined : revisionBindingFromStored(item['sourceRevision']);
|
|
1070
|
+
const revision = item['sourceRevision'] === undefined ? undefined : revisionBindingFromStored(item['sourceRevision'], { allowWithoutGrowthRun });
|
|
971
1071
|
if ((mode === 'prepared-create' || mode === 'prepared-revise') && (creation ?? revision)?.generatorDigest !== item['generatorDigest'])
|
|
972
1072
|
throw new ControlPlaneStoreError('invalid-state', 'stored source creation generator changed');
|
|
973
1073
|
const approval = item['approval'] === undefined ? undefined : verifiedApprovalFromStored(item['approval'], item['id'], item['digest'], Number(item['createdAt']), Number(item['expiresAt']));
|
|
@@ -1233,7 +1333,8 @@ function sourceJobIntentFromStored(value) {
|
|
|
1233
1333
|
const native = backend === 'macos-seatbelt' || backend === 'linux-native';
|
|
1234
1334
|
exactKeys(intent, ['authority', 'owner', 'ownerDigest', 'trustDigest', 'repository', 'name', 'gapId', 'gapRevision', 'gapDigest',
|
|
1235
1335
|
'baseCommit', 'files', 'ttlMs', 'build', 'worktree', native ? 'nativeResource' : 'containerName', ...(Object.hasOwn(intent, 'baseline') ? ['baseline'] : []),
|
|
1236
|
-
...(Object.hasOwn(intent, 'mode') ? ['mode'] : []), ...(Object.hasOwn(intent, 'creation') ? ['creation'] : []), ...(Object.hasOwn(intent, 'revision') ? ['revision'] : [])
|
|
1336
|
+
...(Object.hasOwn(intent, 'mode') ? ['mode'] : []), ...(Object.hasOwn(intent, 'creation') ? ['creation'] : []), ...(Object.hasOwn(intent, 'revision') ? ['revision'] : []),
|
|
1337
|
+
...(Object.hasOwn(intent, 'ownerInstruction') ? ['ownerInstruction'] : [])], 'source job intent');
|
|
1237
1338
|
const authority = objectRecord(intent['authority'], 'source job authority');
|
|
1238
1339
|
exactKeys(authority, ['id', 'digest', 'expiresAt', 'maxSubmissions'], 'source job authority');
|
|
1239
1340
|
const owner = objectRecord(intent['owner'], 'source job owner');
|
|
@@ -1259,7 +1360,11 @@ function sourceJobIntentFromStored(value) {
|
|
|
1259
1360
|
|| (backend === 'macos-seatbelt' && (intent['mode'] !== 'create' && intent['mode'] !== 'revise-created'))
|
|
1260
1361
|
|| (intent['mode'] !== undefined && intent['mode'] !== 'create' && intent['mode'] !== 'revise-created')
|
|
1261
1362
|
|| (intent['mode'] === 'create') !== Object.hasOwn(intent, 'creation')
|
|
1262
|
-
|| (intent['mode'] === 'revise-created') !== Object.hasOwn(intent, 'revision')
|
|
1363
|
+
|| (intent['mode'] === 'revise-created') !== Object.hasOwn(intent, 'revision')
|
|
1364
|
+
|| (Object.hasOwn(intent, 'ownerInstruction') && intent['mode'] !== 'revise-created')
|
|
1365
|
+
|| (Object.hasOwn(intent, 'ownerInstruction')
|
|
1366
|
+
&& intent['revision'] !== null && typeof intent['revision'] === 'object'
|
|
1367
|
+
&& Object.hasOwn(intent['revision'], 'growthRun'))) {
|
|
1263
1368
|
throw new ControlPlaneStoreError('invalid-input', 'source job intent is invalid');
|
|
1264
1369
|
}
|
|
1265
1370
|
if (native) {
|
|
@@ -1297,17 +1402,45 @@ function sourceJobIntentFromStored(value) {
|
|
|
1297
1402
|
throw new ControlPlaneStoreError('invalid-input', 'source job files are invalid');
|
|
1298
1403
|
}
|
|
1299
1404
|
const creation = intent['mode'] === 'create' ? creationBindingFromStored(intent['creation']) : undefined;
|
|
1300
|
-
const revision = intent['mode'] === 'revise-created'
|
|
1405
|
+
const revision = intent['mode'] === 'revise-created'
|
|
1406
|
+
? revisionBindingFromStored(intent['revision'], { allowWithoutGrowthRun: Object.hasOwn(intent, 'ownerInstruction') }) : undefined;
|
|
1407
|
+
let ownerInstruction;
|
|
1408
|
+
if (Object.hasOwn(intent, 'ownerInstruction')) {
|
|
1409
|
+
const item = objectRecord(intent['ownerInstruction'], 'source job owner instruction');
|
|
1410
|
+
const keys = ['instructionId', 'digest'];
|
|
1411
|
+
if (Object.hasOwn(item, 'reviewFreezeDigest'))
|
|
1412
|
+
keys.push('reviewFreezeDigest');
|
|
1413
|
+
if (Object.hasOwn(item, 'reviewWindow'))
|
|
1414
|
+
keys.push('reviewWindow');
|
|
1415
|
+
exactKeys(item, keys, 'source job owner instruction');
|
|
1416
|
+
if (typeof item['instructionId'] !== 'string' || !KEY.test(item['instructionId'])
|
|
1417
|
+
|| typeof item['digest'] !== 'string' || !DIGEST.test(item['digest'])
|
|
1418
|
+
|| (item['reviewFreezeDigest'] === undefined) !== (item['reviewWindow'] === undefined)
|
|
1419
|
+
|| item['reviewFreezeDigest'] !== undefined && (typeof item['reviewFreezeDigest'] !== 'string' || !DIGEST.test(item['reviewFreezeDigest']))) {
|
|
1420
|
+
throw new ControlPlaneStoreError('invalid-input', 'source job owner instruction is invalid');
|
|
1421
|
+
}
|
|
1422
|
+
ownerInstruction = Object.freeze({
|
|
1423
|
+
instructionId: item['instructionId'], digest: item['digest'],
|
|
1424
|
+
...(item['reviewFreezeDigest'] === undefined ? {} : {
|
|
1425
|
+
reviewFreezeDigest: item['reviewFreezeDigest'],
|
|
1426
|
+
reviewWindow: validateOwnerInstructionReviewWindow(item['reviewWindow'])
|
|
1427
|
+
}),
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1301
1430
|
if (revision !== undefined && !intent['name'].startsWith(revision.grant.namePrefix))
|
|
1302
1431
|
throw new ControlPlaneStoreError('invalid-input', 'source job plugin name is outside its revision grant');
|
|
1303
1432
|
if (creation !== undefined && !intent['name'].startsWith(creation.grant.namePrefix)) {
|
|
1304
1433
|
throw new ControlPlaneStoreError('invalid-input', 'source job plugin name is outside its creation grant');
|
|
1305
1434
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1435
|
+
if (isLegacyDockerSourceBuildConfig(intent['build']))
|
|
1436
|
+
validateLegacyDockerSourceBuildConfig(intent['build']);
|
|
1437
|
+
else {
|
|
1438
|
+
try {
|
|
1439
|
+
validateSourceBuildConfig(intent['build']);
|
|
1440
|
+
}
|
|
1441
|
+
catch {
|
|
1442
|
+
throw new ControlPlaneStoreError('invalid-input', 'source job build configuration is invalid');
|
|
1443
|
+
}
|
|
1311
1444
|
}
|
|
1312
1445
|
if (Object.hasOwn(intent, 'baseline')) {
|
|
1313
1446
|
try {
|
|
@@ -1330,7 +1463,8 @@ function sourceJobIntentFromStored(value) {
|
|
|
1330
1463
|
: { containerName: intent['containerName'] }),
|
|
1331
1464
|
...(creation === undefined ? {} : { mode: 'create', creation }),
|
|
1332
1465
|
...(revision === undefined ? {} : { mode: 'revise-created', revision }),
|
|
1333
|
-
...(Object.hasOwn(intent, 'baseline') ? { baseline: Object.freeze(intent['baseline']) } : {})
|
|
1466
|
+
...(Object.hasOwn(intent, 'baseline') ? { baseline: Object.freeze(intent['baseline']) } : {}),
|
|
1467
|
+
...(ownerInstruction === undefined ? {} : { ownerInstruction }) });
|
|
1334
1468
|
}
|
|
1335
1469
|
function exactInputRecord(value, label) {
|
|
1336
1470
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
@@ -1879,7 +2013,9 @@ export class ControlPlaneStore {
|
|
|
1879
2013
|
}
|
|
1880
2014
|
if (options.adoptionCoordinatorId && options.withOwnerActivationFence)
|
|
1881
2015
|
throw new ControlPlaneStoreError('invalid-input', 'adoption coordinator cannot hold owner fence');
|
|
1882
|
-
this.#database = options.readOnly === true
|
|
2016
|
+
this.#database = options.readOnly === true || options.readOnly === 'live'
|
|
2017
|
+
? openControlPlaneDatabaseReadOnly(options.path, options.readOnly === 'live' ? 'live' : 'checkpoint')
|
|
2018
|
+
: openControlPlaneDatabase(options.path);
|
|
1883
2019
|
this.#now = options.now ?? Date.now;
|
|
1884
2020
|
this.#withLiveQualificationFence = options.withLiveQualificationFence;
|
|
1885
2021
|
this.#withOwnerActivationFence = options.withOwnerActivationFence;
|
|
@@ -1932,6 +2068,7 @@ export class ControlPlaneStore {
|
|
|
1932
2068
|
throw new ControlPlaneStoreError('invalid-input', 'gap limit must be 1..100');
|
|
1933
2069
|
return this.#database.prepare(`SELECT gap.* FROM capability_gaps gap
|
|
1934
2070
|
WHERE gap.status = 'open' AND gap.idempotency_key NOT LIKE 'owner-task-failure:%'
|
|
2071
|
+
AND gap.idempotency_key NOT LIKE 'owner-instruction:%'
|
|
1935
2072
|
AND NOT EXISTS (SELECT 1 FROM owner_task_failure_gaps sidecar WHERE sidecar.gap_id = gap.id)
|
|
1936
2073
|
ORDER BY gap.roi DESC, gap.created_at, gap.id LIMIT ?`).all(limit).map(gapFromRow);
|
|
1937
2074
|
}
|
|
@@ -2058,6 +2195,373 @@ export class ControlPlaneStore {
|
|
|
2058
2195
|
getOwnerTaskFailureReference(gapId) {
|
|
2059
2196
|
return readOwnerTaskFailureReference(this.#database, gapId);
|
|
2060
2197
|
}
|
|
2198
|
+
/**
|
|
2199
|
+
* Record one Host-authenticated owner revision instruction. Idempotent on
|
|
2200
|
+
* idempotencyKey: reusing a key with a different payload fails closed, and
|
|
2201
|
+
* reusing the same full payload returns the stored instruction. The owner
|
|
2202
|
+
* receipt, files digest and independent-review freeze are written once.
|
|
2203
|
+
*/
|
|
2204
|
+
recordOwnerInstruction(input) {
|
|
2205
|
+
const instructionId = bounded(input.instructionId, 'instructionId', 160);
|
|
2206
|
+
const ownerRouteId = bounded(input.ownerRouteId, 'ownerRouteId', 200);
|
|
2207
|
+
const planId = bounded(input.planId, 'planId', 160);
|
|
2208
|
+
const pluginName = bounded(input.pluginName, 'pluginName', 160);
|
|
2209
|
+
const idempotencyKey = bounded(input.idempotencyKey, 'idempotencyKey', 160);
|
|
2210
|
+
if (!KEY.test(instructionId) || !KEY.test(planId) || !KEY.test(idempotencyKey) || !KEY.test(ownerRouteId)) {
|
|
2211
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction identity is invalid');
|
|
2212
|
+
}
|
|
2213
|
+
if (!DIGEST.test(input.filesDigest))
|
|
2214
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction files digest is invalid');
|
|
2215
|
+
if (!Number.isSafeInteger(input.createdAt) || input.createdAt < 1
|
|
2216
|
+
|| !Number.isSafeInteger(input.expiresAt) || input.expiresAt <= input.createdAt) {
|
|
2217
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction timing is invalid');
|
|
2218
|
+
}
|
|
2219
|
+
const ownerDigest = controlPlaneDigest(input.ownerReceipt);
|
|
2220
|
+
if (!DIGEST.test(ownerDigest))
|
|
2221
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner receipt is invalid');
|
|
2222
|
+
validateOwnerInstructionReviewFreeze(input.review);
|
|
2223
|
+
const review = Object.freeze({ ...input.review });
|
|
2224
|
+
if (review.budget.authority.expiresAt <= input.createdAt) {
|
|
2225
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction review authority is already expired');
|
|
2226
|
+
}
|
|
2227
|
+
const receiptJson = JSON.stringify(input.ownerReceipt);
|
|
2228
|
+
const prior = this.#database.prepare('SELECT * FROM source_owner_instructions WHERE idempotency_key = ? OR instruction_id = ?')
|
|
2229
|
+
.get(idempotencyKey, instructionId);
|
|
2230
|
+
if (prior !== undefined) {
|
|
2231
|
+
const stored = this.ownerInstructionFromRow(prior);
|
|
2232
|
+
// Retries may recompute createdAt; every frozen field must match. A
|
|
2233
|
+
// reused id or key with a different payload is a conflict either way.
|
|
2234
|
+
const expected = this.#ownerInstructionPayload({ ...input, ownerDigest, createdAt: stored.createdAt });
|
|
2235
|
+
const storedReview = stored.instructionId === instructionId ? this.reviewFreezeFromRow(prior) : undefined;
|
|
2236
|
+
if (stored.idempotencyKey !== idempotencyKey || stored.instructionId !== instructionId
|
|
2237
|
+
|| controlPlaneDigest(stored) !== controlPlaneDigest(expected)
|
|
2238
|
+
|| storedReview === undefined || controlPlaneDigest(storedReview) !== controlPlaneDigest(review)) {
|
|
2239
|
+
throw new ControlPlaneStoreError('conflict', 'owner instruction identity or idempotency key was reused with different input');
|
|
2240
|
+
}
|
|
2241
|
+
return stored;
|
|
2242
|
+
}
|
|
2243
|
+
this.#database.prepare(`INSERT INTO source_owner_instructions
|
|
2244
|
+
(instruction_id, owner_receipt_json, owner_digest, owner_route_id, plan_id, plugin_name,
|
|
2245
|
+
files_digest, created_at, expires_at, idempotency_key,
|
|
2246
|
+
review_supplier_json, review_budget_json, review_freeze_digest)
|
|
2247
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(instructionId, receiptJson, ownerDigest, ownerRouteId, planId, pluginName, input.filesDigest, input.createdAt, input.expiresAt, idempotencyKey, JSON.stringify(review.supplier), JSON.stringify(review.budget), review.digest);
|
|
2248
|
+
const row = this.#database.prepare('SELECT * FROM source_owner_instructions WHERE instruction_id = ?')
|
|
2249
|
+
.get(instructionId);
|
|
2250
|
+
return this.ownerInstructionFromRow(row);
|
|
2251
|
+
}
|
|
2252
|
+
/**
|
|
2253
|
+
* The immutable review supplier/budget written with one owner instruction.
|
|
2254
|
+
* Undefined only for rows recorded before schema 41; every execution path
|
|
2255
|
+
* treats that as unavailable and fails closed rather than consulting the live
|
|
2256
|
+
* owner route or live verifier configuration.
|
|
2257
|
+
*/
|
|
2258
|
+
getOwnerInstructionReviewFreeze(instructionId) {
|
|
2259
|
+
if (typeof instructionId !== 'string' || !KEY.test(instructionId)) {
|
|
2260
|
+
throw new ControlPlaneStoreError('invalid-input', 'instruction id is invalid');
|
|
2261
|
+
}
|
|
2262
|
+
const row = this.#database.prepare('SELECT * FROM source_owner_instructions WHERE instruction_id = ?')
|
|
2263
|
+
.get(instructionId);
|
|
2264
|
+
if (row === undefined)
|
|
2265
|
+
return undefined;
|
|
2266
|
+
return this.reviewFreezeFromRow(row);
|
|
2267
|
+
}
|
|
2268
|
+
reviewFreezeFromRow(row) {
|
|
2269
|
+
const values = [row.review_supplier_json, row.review_budget_json, row.review_freeze_digest];
|
|
2270
|
+
if (values.every(value => value === null))
|
|
2271
|
+
return undefined;
|
|
2272
|
+
if (values.some(value => value === null)) {
|
|
2273
|
+
throw new ControlPlaneStoreError('invalid-state', 'stored owner instruction review freeze is corrupt');
|
|
2274
|
+
}
|
|
2275
|
+
let supplier, budget;
|
|
2276
|
+
try {
|
|
2277
|
+
supplier = JSON.parse(row.review_supplier_json);
|
|
2278
|
+
budget = JSON.parse(row.review_budget_json);
|
|
2279
|
+
}
|
|
2280
|
+
catch {
|
|
2281
|
+
throw new ControlPlaneStoreError('invalid-state', 'stored owner instruction review freeze is corrupt');
|
|
2282
|
+
}
|
|
2283
|
+
const freeze = { protocol: OWNER_INSTRUCTION_REVIEW_FREEZE_PROTOCOL,
|
|
2284
|
+
supplier, budget, digest: row.review_freeze_digest };
|
|
2285
|
+
try {
|
|
2286
|
+
validateOwnerInstructionReviewFreeze(freeze);
|
|
2287
|
+
}
|
|
2288
|
+
catch {
|
|
2289
|
+
throw new ControlPlaneStoreError('invalid-state', 'stored owner instruction review freeze is corrupt');
|
|
2290
|
+
}
|
|
2291
|
+
return Object.freeze(freeze);
|
|
2292
|
+
}
|
|
2293
|
+
getOwnerInstruction(instructionId) {
|
|
2294
|
+
if (typeof instructionId !== 'string' || !KEY.test(instructionId)) {
|
|
2295
|
+
throw new ControlPlaneStoreError('invalid-input', 'instruction id is invalid');
|
|
2296
|
+
}
|
|
2297
|
+
const row = this.#database.prepare('SELECT * FROM source_owner_instructions WHERE instruction_id = ?')
|
|
2298
|
+
.get(instructionId);
|
|
2299
|
+
return row === undefined ? undefined : this.ownerInstructionFromRow(row);
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Records a structured owner-instruction request written by the control-plane
|
|
2303
|
+
* CLI. This table is CLI-writable and drained by the running service's SourceJobs
|
|
2304
|
+
* continuation tick, which re-validates the owner route inside its trust boundary
|
|
2305
|
+
* before enqueueing; the CLI never writes source_jobs and the model tool surface
|
|
2306
|
+
* never reaches this table. The request freezes the exact candidate files (bound
|
|
2307
|
+
* by filesDigest) and expires.
|
|
2308
|
+
*/
|
|
2309
|
+
recordOwnerInstructionRequest(input) {
|
|
2310
|
+
const planId = bounded(input.planId, 'planId', 160);
|
|
2311
|
+
const pluginName = bounded(input.pluginName, 'pluginName', 160);
|
|
2312
|
+
const idempotencyKey = bounded(input.idempotencyKey, 'idempotencyKey', 160);
|
|
2313
|
+
if (!KEY.test(planId) || !KEY.test(idempotencyKey) || pluginName.normalize('NFC') !== pluginName
|
|
2314
|
+
|| pluginName.length === 0 || pluginName.length > 160) {
|
|
2315
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction request identity is invalid');
|
|
2316
|
+
}
|
|
2317
|
+
if (typeof input.expectedBaseCommit !== 'string' || !COMMIT.test(input.expectedBaseCommit)) {
|
|
2318
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction request base commit is invalid');
|
|
2319
|
+
}
|
|
2320
|
+
if (!Number.isSafeInteger(input.ttlMs) || input.ttlMs < 900_000 || input.ttlMs > 86_400_000) {
|
|
2321
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction request ttl is invalid');
|
|
2322
|
+
}
|
|
2323
|
+
if (!Number.isSafeInteger(input.createdAt) || input.createdAt < 1) {
|
|
2324
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction request timing is invalid');
|
|
2325
|
+
}
|
|
2326
|
+
const files = structuredClone(input.files);
|
|
2327
|
+
validateSourceCreationFiles(files);
|
|
2328
|
+
const filesDigest = controlPlaneDigest(files);
|
|
2329
|
+
const filesJson = JSON.stringify(files);
|
|
2330
|
+
if (filesJson.length > 1_048_576)
|
|
2331
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction request files are too large');
|
|
2332
|
+
const expiresAt = input.createdAt + input.ttlMs;
|
|
2333
|
+
const requestId = `oir-${controlPlaneDigest({ planId, expectedBaseCommit: input.expectedBaseCommit, idempotencyKey }).slice(0, 32)}`;
|
|
2334
|
+
const prior = this.#database.prepare('SELECT * FROM source_owner_instruction_requests WHERE idempotency_key = ? OR request_id = ?')
|
|
2335
|
+
.get(idempotencyKey, requestId);
|
|
2336
|
+
if (prior !== undefined) {
|
|
2337
|
+
const stored = this.ownerInstructionRequestFromRow(prior);
|
|
2338
|
+
// Retries are idempotent; a reused key/id with a different payload is a conflict.
|
|
2339
|
+
if (stored.idempotencyKey !== idempotencyKey || stored.requestId !== requestId
|
|
2340
|
+
|| stored.planId !== planId || stored.pluginName !== pluginName
|
|
2341
|
+
|| stored.expectedBaseCommit !== input.expectedBaseCommit || stored.filesDigest !== filesDigest
|
|
2342
|
+
|| stored.ttlMs !== input.ttlMs) {
|
|
2343
|
+
throw new ControlPlaneStoreError('conflict', 'owner instruction request identity or idempotency key was reused with different input');
|
|
2344
|
+
}
|
|
2345
|
+
return stored;
|
|
2346
|
+
}
|
|
2347
|
+
this.#database.prepare(`INSERT INTO source_owner_instruction_requests
|
|
2348
|
+
(request_id, plan_id, plugin_name, expected_base_commit, files_json, files_digest, ttl_ms,
|
|
2349
|
+
idempotency_key, created_at, expires_at, status, failure_code)
|
|
2350
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', NULL)`).run(requestId, planId, pluginName, input.expectedBaseCommit, filesJson, filesDigest, input.ttlMs, idempotencyKey, input.createdAt, expiresAt);
|
|
2351
|
+
const row = this.#database.prepare('SELECT * FROM source_owner_instruction_requests WHERE request_id = ?')
|
|
2352
|
+
.get(requestId);
|
|
2353
|
+
return this.ownerInstructionRequestFromRow(row);
|
|
2354
|
+
}
|
|
2355
|
+
getOwnerInstructionRequest(requestId) {
|
|
2356
|
+
if (typeof requestId !== 'string' || !KEY.test(requestId)) {
|
|
2357
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction request id is invalid');
|
|
2358
|
+
}
|
|
2359
|
+
const row = this.#database.prepare('SELECT * FROM source_owner_instruction_requests WHERE request_id = ?')
|
|
2360
|
+
.get(requestId);
|
|
2361
|
+
return row === undefined ? undefined : this.ownerInstructionRequestFromRow(row);
|
|
2362
|
+
}
|
|
2363
|
+
/** Historical status projection, independent of the candidate payload and current owner authorization. */
|
|
2364
|
+
inspectOwnerInstructionRequest(requestId) {
|
|
2365
|
+
if (typeof requestId !== 'string' || !KEY.test(requestId)) {
|
|
2366
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction request id is invalid');
|
|
2367
|
+
}
|
|
2368
|
+
this.#database.exec('BEGIN');
|
|
2369
|
+
try {
|
|
2370
|
+
const row = this.#database.prepare(`SELECT request_id, plan_id, plugin_name, expected_base_commit, status,
|
|
2371
|
+
failure_code, created_at, expires_at, instruction_id, job_id FROM source_owner_instruction_requests WHERE request_id = ?`)
|
|
2372
|
+
.get(requestId);
|
|
2373
|
+
if (row === undefined)
|
|
2374
|
+
return undefined;
|
|
2375
|
+
if (!/^oir-[a-f0-9]{32}$/u.test(row.request_id) || !KEY.test(row.plan_id) || !COMMIT.test(row.expected_base_commit)
|
|
2376
|
+
|| bounded(row.plugin_name, 'stored owner instruction plugin name', 160) !== row.plugin_name
|
|
2377
|
+
|| !['pending', 'claimed', 'accepted', 'rejected'].includes(row.status)
|
|
2378
|
+
|| (row.status === 'rejected') !== (row.failure_code !== null)
|
|
2379
|
+
|| (row.failure_code !== null && !/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,119}$/u.test(row.failure_code))
|
|
2380
|
+
|| !Number.isSafeInteger(row.created_at) || row.created_at < 1
|
|
2381
|
+
|| !Number.isSafeInteger(row.expires_at) || row.expires_at <= row.created_at
|
|
2382
|
+
|| (row.status !== 'accepted' && (row.instruction_id !== null || row.job_id !== null))
|
|
2383
|
+
|| (row.instruction_id !== null && !KEY.test(row.instruction_id))
|
|
2384
|
+
|| (row.job_id !== null && !/^source-job-[a-f0-9]{64}$/u.test(row.job_id))) {
|
|
2385
|
+
throw new ControlPlaneStoreError('invalid-state', 'stored owner instruction request status is corrupt');
|
|
2386
|
+
}
|
|
2387
|
+
const job = row.status === 'accepted' && row.instruction_id !== null && row.job_id !== null
|
|
2388
|
+
? this.#inspectRecordedOwnerInstructionJob(requestId) : null;
|
|
2389
|
+
const jobAssociation = job !== null ? 'matched' : row.status === 'accepted' ? 'unknown' : null;
|
|
2390
|
+
return { requestId: row.request_id, planId: row.plan_id, pluginName: row.plugin_name,
|
|
2391
|
+
expectedBaseCommit: row.expected_base_commit, status: row.status, failureCode: row.failure_code,
|
|
2392
|
+
createdAt: row.created_at, expiresAt: row.expires_at, instructionId: row.instruction_id, jobId: row.job_id,
|
|
2393
|
+
jobAssociation, job };
|
|
2394
|
+
}
|
|
2395
|
+
finally {
|
|
2396
|
+
this.#database.exec('ROLLBACK');
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
#inspectRecordedOwnerInstructionJob(requestId) {
|
|
2400
|
+
try {
|
|
2401
|
+
const request = this.getOwnerInstructionRequest(requestId);
|
|
2402
|
+
if (request === undefined || request.instructionId === null || request.jobId === null)
|
|
2403
|
+
return null;
|
|
2404
|
+
const instruction = this.getOwnerInstruction(request.instructionId);
|
|
2405
|
+
const job = this.getSourceJob(request.jobId);
|
|
2406
|
+
if (instruction === undefined || job === undefined)
|
|
2407
|
+
return null;
|
|
2408
|
+
const intent = job.intent;
|
|
2409
|
+
if (instruction.idempotencyKey !== request.idempotencyKey || job.idempotencyKey !== request.idempotencyKey
|
|
2410
|
+
|| instruction.planId !== request.planId || intent.revision?.parent.planId !== request.planId
|
|
2411
|
+
|| instruction.pluginName !== request.pluginName || intent.name !== request.pluginName
|
|
2412
|
+
|| instruction.filesDigest !== request.filesDigest || controlPlaneDigest(intent.files) !== request.filesDigest
|
|
2413
|
+
|| instruction.createdAt !== request.createdAt || instruction.expiresAt !== request.expiresAt
|
|
2414
|
+
|| intent.baseCommit !== request.expectedBaseCommit || intent.ttlMs !== request.ttlMs
|
|
2415
|
+
|| intent.mode !== 'revise-created' || intent.ownerDigest !== instruction.ownerDigest
|
|
2416
|
+
|| intent.ownerInstruction?.instructionId !== instruction.instructionId
|
|
2417
|
+
|| intent.ownerInstruction.digest !== controlPlaneDigest(instruction))
|
|
2418
|
+
return null;
|
|
2419
|
+
return { status: job.status, failureCode: job.failureCode ?? null, candidatePlanId: job.planId ?? null };
|
|
2420
|
+
}
|
|
2421
|
+
catch (error) {
|
|
2422
|
+
if (error instanceof ControlPlaneStoreError)
|
|
2423
|
+
return null;
|
|
2424
|
+
throw error;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
/** Pending, unexpired requests, oldest first. This read never quarantines rows. */
|
|
2428
|
+
listPendingOwnerInstructionRequests(now, limit = 16) {
|
|
2429
|
+
return this.#pendingOwnerInstructionRows(now, limit).map(row => this.ownerInstructionRequestFromRow(row));
|
|
2430
|
+
}
|
|
2431
|
+
#pendingOwnerInstructionRows(now, limit) {
|
|
2432
|
+
if (!Number.isSafeInteger(now) || now < 1 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
|
|
2433
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction request query bounds are invalid');
|
|
2434
|
+
}
|
|
2435
|
+
// Sort only request identities before loading the bounded batch of files.
|
|
2436
|
+
return this.#database.prepare(`SELECT r.* FROM (
|
|
2437
|
+
SELECT request_id FROM source_owner_instruction_requests WHERE status = 'pending' AND expires_at > ?
|
|
2438
|
+
ORDER BY created_at, request_id LIMIT ?
|
|
2439
|
+
) batch JOIN source_owner_instruction_requests r ON r.request_id = batch.request_id
|
|
2440
|
+
ORDER BY r.created_at, r.request_id`).all(now, limit);
|
|
2441
|
+
}
|
|
2442
|
+
/** Decode and quarantine exactly one batch. Never re-read beyond its limit after quarantine. */
|
|
2443
|
+
readOwnerInstructionDrainBatch(now, limit) {
|
|
2444
|
+
const rows = this.#pendingOwnerInstructionRows(now, limit);
|
|
2445
|
+
const pending = [];
|
|
2446
|
+
let quarantined = 0;
|
|
2447
|
+
for (const row of rows) {
|
|
2448
|
+
try {
|
|
2449
|
+
pending.push(this.ownerInstructionRequestFromRow(row));
|
|
2450
|
+
}
|
|
2451
|
+
catch {
|
|
2452
|
+
const result = this.#database.prepare(`UPDATE source_owner_instruction_requests
|
|
2453
|
+
SET status = 'rejected', failure_code = 'request-corrupt'
|
|
2454
|
+
WHERE request_id = ? AND status = 'pending' AND claim_generation = ?`).run(row.request_id, row.claim_generation);
|
|
2455
|
+
quarantined += Number(result.changes);
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
return { pending, quarantined };
|
|
2459
|
+
}
|
|
2460
|
+
/** Read-only readiness for the existing native scheduler wake. */
|
|
2461
|
+
hasOwnerInstructionRequestWork(now, claimTimeoutMs) {
|
|
2462
|
+
return this.#database.prepare(`SELECT 1 FROM source_owner_instruction_requests
|
|
2463
|
+
WHERE status = 'pending' OR (status = 'claimed' AND
|
|
2464
|
+
(expires_at <= ? OR claimed_at IS NULL OR claimed_at <= ?)) LIMIT 1`).get(now, now - claimTimeoutMs) !== undefined;
|
|
2465
|
+
}
|
|
2466
|
+
expireOwnerInstructionRequests(now) {
|
|
2467
|
+
this.#database.prepare(`UPDATE source_owner_instruction_requests
|
|
2468
|
+
SET status = 'rejected', claimed_at = NULL, failure_code = 'request-expired'
|
|
2469
|
+
WHERE status IN ('pending', 'claimed') AND expires_at <= ?`).run(now);
|
|
2470
|
+
}
|
|
2471
|
+
/** Pending-to-claimed CAS returns a durable, monotonically increasing claim generation. */
|
|
2472
|
+
claimOwnerInstructionRequest(requestId, now) {
|
|
2473
|
+
if (typeof requestId !== 'string' || !KEY.test(requestId) || !Number.isSafeInteger(now) || now < 1) {
|
|
2474
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction request claim is invalid');
|
|
2475
|
+
}
|
|
2476
|
+
const result = this.#database.prepare(`UPDATE source_owner_instruction_requests
|
|
2477
|
+
SET status = 'claimed', claimed_at = ?, claim_generation = claim_generation + 1
|
|
2478
|
+
WHERE request_id = ? AND status = 'pending' AND expires_at > ? RETURNING claim_generation`)
|
|
2479
|
+
.get(now, requestId, now);
|
|
2480
|
+
return result?.claim_generation;
|
|
2481
|
+
}
|
|
2482
|
+
/** Requeue stale claims, including legacy v38 claims without a timestamp. */
|
|
2483
|
+
reclaimOrphanedOwnerInstructionRequests(now, claimTimeoutMs) {
|
|
2484
|
+
if (!Number.isSafeInteger(now) || now < 1 || !Number.isSafeInteger(claimTimeoutMs) || claimTimeoutMs < 1) {
|
|
2485
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction request orphan reclamation is invalid');
|
|
2486
|
+
}
|
|
2487
|
+
const result = this.#database.prepare(`UPDATE source_owner_instruction_requests
|
|
2488
|
+
SET status = 'pending', claimed_at = NULL
|
|
2489
|
+
WHERE status = 'claimed' AND (claimed_at IS NULL OR claimed_at <= ?) AND expires_at > ?`).run(now - claimTimeoutMs, now);
|
|
2490
|
+
return Number(result.changes);
|
|
2491
|
+
}
|
|
2492
|
+
rejectOwnerInstructionRequest(requestId, generation, failureCode) {
|
|
2493
|
+
this.#assertOwnerInstructionClaim(requestId, generation);
|
|
2494
|
+
if (typeof failureCode !== 'string' || failureCode.length === 0 || failureCode.length > 120) {
|
|
2495
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction request rejection is invalid');
|
|
2496
|
+
}
|
|
2497
|
+
const result = this.#database.prepare(`UPDATE source_owner_instruction_requests
|
|
2498
|
+
SET status = 'rejected', claimed_at = NULL, failure_code = ?
|
|
2499
|
+
WHERE request_id = ? AND status = 'claimed' AND claim_generation = ?`).run(failureCode, requestId, generation);
|
|
2500
|
+
return Number(result.changes) === 1;
|
|
2501
|
+
}
|
|
2502
|
+
acceptOwnerInstructionRequest(requestId, generation, receipt) {
|
|
2503
|
+
this.#assertOwnerInstructionClaim(requestId, generation);
|
|
2504
|
+
if (!KEY.test(receipt.instructionId) || !/^source-job-[a-f0-9]{64}$/u.test(receipt.jobId)) {
|
|
2505
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction request admission receipt is invalid');
|
|
2506
|
+
}
|
|
2507
|
+
const result = this.#database.prepare(`UPDATE source_owner_instruction_requests
|
|
2508
|
+
SET status = 'accepted', claimed_at = NULL, instruction_id = ?, job_id = ?
|
|
2509
|
+
WHERE request_id = ? AND status = 'claimed' AND claim_generation = ?`)
|
|
2510
|
+
.run(receipt.instructionId, receipt.jobId, requestId, generation);
|
|
2511
|
+
return Number(result.changes) === 1;
|
|
2512
|
+
}
|
|
2513
|
+
releaseOwnerInstructionRequestClaim(requestId, generation) {
|
|
2514
|
+
this.#assertOwnerInstructionClaim(requestId, generation);
|
|
2515
|
+
const result = this.#database.prepare(`UPDATE source_owner_instruction_requests SET status = 'pending', claimed_at = NULL
|
|
2516
|
+
WHERE request_id = ? AND status = 'claimed' AND claim_generation = ?`).run(requestId, generation);
|
|
2517
|
+
return Number(result.changes) === 1;
|
|
2518
|
+
}
|
|
2519
|
+
#assertOwnerInstructionClaim(requestId, generation) {
|
|
2520
|
+
if (typeof requestId !== 'string' || !KEY.test(requestId) || !Number.isSafeInteger(generation) || generation < 1) {
|
|
2521
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner instruction request claim is invalid');
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
ownerInstructionRequestFromRow(row) {
|
|
2525
|
+
let files;
|
|
2526
|
+
try {
|
|
2527
|
+
files = JSON.parse(row.files_json);
|
|
2528
|
+
}
|
|
2529
|
+
catch {
|
|
2530
|
+
throw new ControlPlaneStoreError('invalid-state', 'stored owner instruction request files are corrupt');
|
|
2531
|
+
}
|
|
2532
|
+
if (!Array.isArray(files) || controlPlaneDigest(files) !== row.files_digest) {
|
|
2533
|
+
throw new ControlPlaneStoreError('invalid-state', 'stored owner instruction request digest is corrupt');
|
|
2534
|
+
}
|
|
2535
|
+
return { requestId: row.request_id, planId: row.plan_id, pluginName: row.plugin_name,
|
|
2536
|
+
expectedBaseCommit: row.expected_base_commit, files, filesDigest: row.files_digest, ttlMs: row.ttl_ms,
|
|
2537
|
+
idempotencyKey: row.idempotency_key, createdAt: row.created_at, expiresAt: row.expires_at,
|
|
2538
|
+
status: row.status, failureCode: row.failure_code, claimedAt: row.claimed_at,
|
|
2539
|
+
claimGeneration: row.claim_generation, instructionId: row.instruction_id, jobId: row.job_id };
|
|
2540
|
+
}
|
|
2541
|
+
#ownerInstructionPayload(input) {
|
|
2542
|
+
return { instructionId: input.instructionId, ownerReceipt: input.ownerReceipt, ownerDigest: input.ownerDigest,
|
|
2543
|
+
ownerRouteId: input.ownerRouteId, planId: input.planId, pluginName: input.pluginName,
|
|
2544
|
+
filesDigest: input.filesDigest, createdAt: input.createdAt, expiresAt: input.expiresAt,
|
|
2545
|
+
idempotencyKey: input.idempotencyKey };
|
|
2546
|
+
}
|
|
2547
|
+
ownerInstructionFromRow(row) {
|
|
2548
|
+
let ownerReceipt;
|
|
2549
|
+
try {
|
|
2550
|
+
ownerReceipt = JSON.parse(row.owner_receipt_json);
|
|
2551
|
+
}
|
|
2552
|
+
catch {
|
|
2553
|
+
throw new ControlPlaneStoreError('invalid-state', 'stored owner instruction receipt is corrupt');
|
|
2554
|
+
}
|
|
2555
|
+
if (controlPlaneDigest(ownerReceipt) !== row.owner_digest) {
|
|
2556
|
+
throw new ControlPlaneStoreError('invalid-state', 'stored owner instruction digest is corrupt');
|
|
2557
|
+
}
|
|
2558
|
+
return {
|
|
2559
|
+
instructionId: row.instruction_id, ownerReceipt, ownerDigest: row.owner_digest,
|
|
2560
|
+
ownerRouteId: row.owner_route_id, planId: row.plan_id, pluginName: row.plugin_name,
|
|
2561
|
+
filesDigest: row.files_digest, createdAt: row.created_at, expiresAt: row.expires_at,
|
|
2562
|
+
idempotencyKey: row.idempotency_key,
|
|
2563
|
+
};
|
|
2564
|
+
}
|
|
2061
2565
|
withOwnerTaskFailureGapAdmission(gapId, callback) {
|
|
2062
2566
|
if (this.#adoptionCoordinatorId !== undefined)
|
|
2063
2567
|
throw new ControlPlaneStoreError('invalid-state', 'adoption coordinator cannot create or approve owner work');
|
|
@@ -2399,6 +2903,15 @@ export class ControlPlaneStore {
|
|
|
2399
2903
|
throw new ControlPlaneStoreError('conflict', 'source job quota authority changed');
|
|
2400
2904
|
return { sourceSubmissions: Math.max(0, a.maxSubmissions - (row?.submissions ?? 0)), creates: Math.max(0, c.maxCreates - (grant?.creates ?? 0)) };
|
|
2401
2905
|
}
|
|
2906
|
+
/** Read-only revision quota companion of inspectSourceJobQuota; mirrors the enqueue-side invariants. */
|
|
2907
|
+
inspectSourceJobRevisionQuota(input) {
|
|
2908
|
+
const a = input.authority, g = input.revision;
|
|
2909
|
+
const row = this.#database.prepare('SELECT * FROM source_job_authorities WHERE authority_id=?').get(a.id);
|
|
2910
|
+
const grant = this.#database.prepare('SELECT * FROM source_revision_grants WHERE grant_id=?').get(g.id);
|
|
2911
|
+
if (row && (row.authority_digest !== a.digest || row.expires_at !== a.expiresAt || row.max_submissions !== a.maxSubmissions) || grant && (grant.grant_digest !== controlPlaneDigest(g) || grant.expires_at !== g.expiresAt || grant.max_revisions !== g.maxRevisions))
|
|
2912
|
+
throw new ControlPlaneStoreError('conflict', 'source job revision quota authority changed');
|
|
2913
|
+
return { sourceSubmissions: Math.max(0, a.maxSubmissions - (row?.submissions ?? 0)), revisions: Math.max(0, g.maxRevisions - (grant?.revisions ?? 0)) };
|
|
2914
|
+
}
|
|
2402
2915
|
enqueueSourceJob(input) {
|
|
2403
2916
|
if (!/^source-job-[a-f0-9]{64}$/u.test(input.id) || input.automationId !== input.id) {
|
|
2404
2917
|
throw new ControlPlaneStoreError('invalid-input', 'source job identity is invalid');
|
|
@@ -2422,8 +2935,19 @@ export class ControlPlaneStore {
|
|
|
2422
2935
|
throw new ControlPlaneStoreError('expired', 'source job authority is expired');
|
|
2423
2936
|
if (intent.creation !== undefined && now >= intent.creation.grant.expiresAt)
|
|
2424
2937
|
throw new ControlPlaneStoreError('expired', 'source creation grant is expired');
|
|
2425
|
-
if (intent.revision !== undefined && (
|
|
2938
|
+
if (intent.revision !== undefined && (now >= intent.revision.grant.expiresAt
|
|
2939
|
+
|| (this.getOwnerTaskFailureReference(intent.gapId) === undefined && intent.ownerInstruction === undefined))) {
|
|
2426
2940
|
throw new ControlPlaneStoreError('expired', 'revision grant or owner task is unavailable');
|
|
2941
|
+
}
|
|
2942
|
+
// Re-validate the frozen instruction at the writer boundary: a revoked or
|
|
2943
|
+
// changed instruction must never produce a source job, even after restart.
|
|
2944
|
+
if (intent.ownerInstruction !== undefined) {
|
|
2945
|
+
const instruction = this.getOwnerInstruction(intent.ownerInstruction.instructionId);
|
|
2946
|
+
if (instruction === undefined || controlPlaneDigest(instruction) !== intent.ownerInstruction.digest
|
|
2947
|
+
|| instruction.expiresAt <= now) {
|
|
2948
|
+
throw new ControlPlaneStoreError('expired', 'owner instruction is unavailable or changed');
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2427
2951
|
const growthRun = intent.creation?.growthRun ?? intent.revision?.growthRun;
|
|
2428
2952
|
if (growthRun !== undefined && (now >= growthRun.generationDeadlineAt || now >= growthRun.expiresAt))
|
|
2429
2953
|
throw new ControlPlaneStoreError('expired', 'source growth generation window is expired');
|
|
@@ -2536,12 +3060,14 @@ export class ControlPlaneStore {
|
|
|
2536
3060
|
created_at DESC, id DESC LIMIT ?`).all(limit).map(sourceJobFromRow);
|
|
2537
3061
|
}
|
|
2538
3062
|
/** Prepared owner continuations have a bounded recovery query, independent of job history. */
|
|
2539
|
-
listPreparedSourceApprovalJobs(includeRelease = false, includeExecution = false, includeAdoption = false, includeCreation = false, includeCreationAdoption = false, includeRevision = false, includeRegression = false) {
|
|
3063
|
+
listPreparedSourceApprovalJobs(includeRelease = false, includeExecution = false, includeAdoption = false, includeCreation = false, includeCreationAdoption = false, includeRevision = false, includeRegression = false, includeRevisionAdoption = false) {
|
|
2540
3064
|
return this.#database.prepare(`SELECT j.* FROM source_jobs j JOIN source_plans p ON p.id = j.plan_id
|
|
2541
|
-
JOIN owner_task_failure_gaps g ON g.gap_id = p.gap_id
|
|
3065
|
+
LEFT JOIN owner_task_failure_gaps g ON g.gap_id = p.gap_id
|
|
2542
3066
|
LEFT JOIN source_adoptions a ON a.source_plan_id = p.id
|
|
2543
3067
|
LEFT JOIN activation_plans ap ON ap.id = a.activation_plan_id
|
|
2544
|
-
WHERE j.status = 'prepared'
|
|
3068
|
+
WHERE j.status = 'prepared'
|
|
3069
|
+
AND (g.gap_id IS NOT NULL OR (p.mode = 'prepared-revise' AND json_type(j.intent_json, '$.ownerInstruction') = 'object'))
|
|
3070
|
+
AND ((p.mode = 'modify' AND (p.status = 'pending-approval' OR (? = 1 AND p.status IN ('approved', 'ready-for-human-review'))
|
|
2545
3071
|
OR (? = 1 AND p.status IN ('awaiting-pr', 'awaiting-review', 'awaiting-merge', 'awaiting-build', 'awaiting-sign', 'awaiting-publish',
|
|
2546
3072
|
'awaiting-registry-verify', 'awaiting-catalog-admission'))
|
|
2547
3073
|
OR (? = 1 AND p.status = 'release-complete' AND (ap.id IS NULL OR ap.status NOT IN ('activated', 'rolled-back', 'rejected')))))
|
|
@@ -2551,14 +3077,20 @@ export class ControlPlaneStore {
|
|
|
2551
3077
|
OR (? = 1 AND p.mode = 'prepared-create' AND p.status = 'pending-approval'
|
|
2552
3078
|
AND EXISTS (SELECT 1 FROM source_creation_verifications v WHERE v.plan_id = p.id AND v.status = 'verified'))
|
|
2553
3079
|
OR (? = 1 AND p.mode = 'prepared-revise' AND p.status = 'pending-approval'
|
|
2554
|
-
AND json_type(j.intent_json, '$.revision.growthRun.revisionAcceptance') IS NOT NULL
|
|
3080
|
+
AND (json_type(j.intent_json, '$.revision.growthRun.revisionAcceptance') IS NOT NULL
|
|
3081
|
+
OR (g.gap_id IS NULL AND json_type(j.intent_json, '$.ownerInstruction') = 'object'))
|
|
2555
3082
|
AND NOT EXISTS (SELECT 1 FROM source_revision_verifications v WHERE v.plan_id=p.id))
|
|
2556
3083
|
OR (? = 1 AND p.mode = 'prepared-revise' AND p.status = 'pending-approval'
|
|
2557
3084
|
AND json_type(j.intent_json, '$.revision.growthRun.revisionRegressionAcceptance') IS NOT NULL
|
|
2558
3085
|
AND EXISTS (SELECT 1 FROM source_revision_verifications v WHERE v.plan_id=p.id AND v.status='verified')
|
|
2559
|
-
AND NOT EXISTS (SELECT 1 FROM source_revision_regressions v WHERE v.plan_id=p.id))
|
|
3086
|
+
AND NOT EXISTS (SELECT 1 FROM source_revision_regressions v WHERE v.plan_id=p.id))
|
|
3087
|
+
OR (? = 1 AND p.mode = 'prepared-revise' AND p.status = 'pending-approval'
|
|
3088
|
+
AND g.gap_id IS NOT NULL
|
|
3089
|
+
AND EXISTS (SELECT 1 FROM source_revision_verifications v WHERE v.plan_id=p.id AND v.status='verified')
|
|
3090
|
+
AND (json_type(j.intent_json, '$.revision.growthRun.revisionRegressionAcceptance') IS NULL
|
|
3091
|
+
OR EXISTS (SELECT 1 FROM source_revision_regressions r WHERE r.plan_id=p.id AND r.status='verified'))))
|
|
2560
3092
|
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'))
|
|
2561
|
-
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);
|
|
3093
|
+
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);
|
|
2562
3094
|
}
|
|
2563
3095
|
bindSourceJobDefinition(input) {
|
|
2564
3096
|
if (!Number.isSafeInteger(input.revision) || input.revision < 1 || !DIGEST.test(input.definitionHash))
|
|
@@ -2648,7 +3180,10 @@ export class ControlPlaneStore {
|
|
|
2648
3180
|
|| (creation !== undefined && (creation.generatorDigest !== input.generatorDigest || !name.startsWith(creation.grant.namePrefix)))) {
|
|
2649
3181
|
throw new ControlPlaneStoreError('invalid-input', 'prepared source creation binding is invalid');
|
|
2650
3182
|
}
|
|
2651
|
-
const
|
|
3183
|
+
const instructionJob = mode === 'prepared-revise' && input.sourceJob !== undefined
|
|
3184
|
+
? this.getSourceJob(input.sourceJob.jobId)?.intent.ownerInstruction : undefined;
|
|
3185
|
+
const revision = input.revision === undefined ? undefined
|
|
3186
|
+
: revisionBindingFromStored(input.revision, { allowWithoutGrowthRun: instructionJob !== undefined });
|
|
2652
3187
|
if ((mode === 'prepared-revise') !== (revision !== undefined)
|
|
2653
3188
|
|| (revision !== undefined && (revision.generatorDigest !== input.generatorDigest || !name.startsWith(revision.grant.namePrefix)))) {
|
|
2654
3189
|
throw new ControlPlaneStoreError('invalid-input', 'prepared source revision binding is invalid');
|
|
@@ -2680,9 +3215,6 @@ export class ControlPlaneStore {
|
|
|
2680
3215
|
|| createHash('sha256').update(input.preparedArtifact).digest('hex') !== preparedEvidence.pack.sha256) {
|
|
2681
3216
|
throw new ControlPlaneStoreError('invalid-input', 'prepared source artifact differs from checked package evidence');
|
|
2682
3217
|
}
|
|
2683
|
-
if (this.getOwnerTaskFailureReference(input.gapId) === undefined) {
|
|
2684
|
-
throw new ControlPlaneStoreError('invalid-state', 'prepared source artifact requires an owner task failure gap');
|
|
2685
|
-
}
|
|
2686
3218
|
preparedArtifact = Buffer.from(input.preparedArtifact);
|
|
2687
3219
|
}
|
|
2688
3220
|
positiveInteger(input.ttlMs, 'ttlMs');
|
|
@@ -2755,6 +3287,16 @@ export class ControlPlaneStore {
|
|
|
2755
3287
|
|| (mode === 'prepared-revise' && controlPlaneDigest(sourceJob.intent.revision) !== controlPlaneDigest(revision))) {
|
|
2756
3288
|
throw new ControlPlaneStoreError('conflict', 'source job completion lost its immutable running binding');
|
|
2757
3289
|
}
|
|
3290
|
+
const reference = this.getOwnerTaskFailureReference(input.gapId);
|
|
3291
|
+
if (sourceJob.intent.ownerInstruction !== undefined) {
|
|
3292
|
+
if (reference !== undefined || mode !== 'prepared-revise') {
|
|
3293
|
+
throw new ControlPlaneStoreError('invalid-state', 'prepared revision owner sources conflict');
|
|
3294
|
+
}
|
|
3295
|
+
this.#assertRevisionInstruction(sourceJob, now);
|
|
3296
|
+
}
|
|
3297
|
+
else if (preparedArtifact !== undefined && reference === undefined) {
|
|
3298
|
+
throw new ControlPlaneStoreError('invalid-state', 'prepared source artifact requires an owner task failure gap');
|
|
3299
|
+
}
|
|
2758
3300
|
if (now >= sourceJob.expiresAt || (creation !== undefined && now >= creation.grant.expiresAt) || (revision !== undefined && now >= revision.grant.expiresAt))
|
|
2759
3301
|
throw new ControlPlaneStoreError('expired', 'source job authority is expired');
|
|
2760
3302
|
if (mode === 'prepared-create' && this.getOwnerTaskFailureReference(input.gapId) !== undefined
|
|
@@ -2762,7 +3304,8 @@ export class ControlPlaneStore {
|
|
|
2762
3304
|
throw new ControlPlaneStoreError('expired', 'task-backed creation growth run is absent or expired');
|
|
2763
3305
|
}
|
|
2764
3306
|
}
|
|
2765
|
-
if (revision !== undefined &&
|
|
3307
|
+
if (revision !== undefined && revision.growthRun !== undefined
|
|
3308
|
+
&& (this.getOwnerTaskFailureReference(input.gapId) === undefined || now >= revision.growthRun.expiresAt)) {
|
|
2766
3309
|
throw new ControlPlaneStoreError('expired', 'revision requires a current owner task and frozen growth run');
|
|
2767
3310
|
}
|
|
2768
3311
|
if (preparedArtifact !== undefined) {
|
|
@@ -2824,7 +3367,19 @@ export class ControlPlaneStore {
|
|
|
2824
3367
|
const row = this.#database.prepare('SELECT * FROM source_plans WHERE id = ?').get(id);
|
|
2825
3368
|
if (row === undefined)
|
|
2826
3369
|
throw new ControlPlaneStoreError('not-found', 'source plan not found');
|
|
2827
|
-
return
|
|
3370
|
+
return this.#readSourcePlanRow(row);
|
|
3371
|
+
}
|
|
3372
|
+
#readSourcePlanRow(row) {
|
|
3373
|
+
const plan = sourceFromRow(row, row.mode === 'prepared-revise');
|
|
3374
|
+
if (plan.sourceRevision !== undefined && plan.sourceRevision.growthRun === undefined) {
|
|
3375
|
+
// Historical reads can parse the v3 form only when the matching durable
|
|
3376
|
+
// job and instruction still bind this plan. Authority use rechecks time.
|
|
3377
|
+
if (this.getOwnerTaskFailureReference(plan.gapId) !== undefined) {
|
|
3378
|
+
throw new ControlPlaneStoreError('invalid-state', 'prepared revision owner sources conflict');
|
|
3379
|
+
}
|
|
3380
|
+
this.#assertRevisionInstruction(this.#retainedPreparedRevisionJob(plan));
|
|
3381
|
+
}
|
|
3382
|
+
return plan;
|
|
2828
3383
|
}
|
|
2829
3384
|
/** Exact durable source job for a currently admitted prepared creation. */
|
|
2830
3385
|
getPreparedSourceJob(planId) {
|
|
@@ -2880,22 +3435,64 @@ export class ControlPlaneStore {
|
|
|
2880
3435
|
|| this.#now() >= plan.expiresAt || this.#now() >= plan.sourceRevision.grant.expiresAt) {
|
|
2881
3436
|
throw new ControlPlaneStoreError('invalid-state', 'prepared revision requires a current pending plan');
|
|
2882
3437
|
}
|
|
3438
|
+
const job = this.#retainedPreparedRevisionJob(plan);
|
|
2883
3439
|
const owner = this.getOwnerTaskFailureReference(plan.gapId);
|
|
2884
|
-
if (owner === undefined)
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
3440
|
+
if (owner === undefined) {
|
|
3441
|
+
this.#assertRevisionInstruction(job, this.#now());
|
|
3442
|
+
}
|
|
3443
|
+
else {
|
|
3444
|
+
this.#assertOwnerTaskFailureGapAdmission(plan.gapId);
|
|
3445
|
+
if (job.intent.ownerInstruction !== undefined || job.intent.ownerDigest !== controlPlaneDigest(owner.owner)) {
|
|
3446
|
+
throw new ControlPlaneStoreError('invalid-state', 'prepared revision owner task binding changed');
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
return job;
|
|
3450
|
+
}
|
|
3451
|
+
/**
|
|
3452
|
+
* Host-only content-free benefit intent projection. Reading the pinned
|
|
3453
|
+
* contract digest needs no owner-gap admission: it is Host metadata already
|
|
3454
|
+
* validated against the plan binding, never task content or execution
|
|
3455
|
+
* authority.
|
|
3456
|
+
*/
|
|
3457
|
+
inspectRevisionBenefitIntent(planId) {
|
|
3458
|
+
const plan = this.getSourcePlan(planId);
|
|
3459
|
+
if (plan.mode !== 'prepared-revise' || plan.sourceRevision === undefined)
|
|
3460
|
+
return undefined;
|
|
3461
|
+
const job = this.#retainedPreparedRevisionJob(plan);
|
|
3462
|
+
const pinned = job.intent.revision?.grant.benefit;
|
|
3463
|
+
return Object.freeze({
|
|
3464
|
+
...(pinned === undefined ? {} : { pinnedContractDigest: pinned.contractDigest }),
|
|
3465
|
+
growthRunAcceptance: job.intent.revision?.growthRun?.revisionAcceptance !== undefined,
|
|
3466
|
+
});
|
|
3467
|
+
}
|
|
3468
|
+
#assertRevisionInstruction(job, now) {
|
|
3469
|
+
const binding = job.intent.ownerInstruction;
|
|
3470
|
+
const instruction = binding === undefined ? undefined : this.getOwnerInstruction(binding.instructionId);
|
|
3471
|
+
if (!instruction || controlPlaneDigest(instruction) !== binding.digest || (now !== undefined && (instruction.expiresAt <= now || job.expiresAt <= now))
|
|
3472
|
+
|| instruction.planId !== job.intent.revision?.parent.planId
|
|
3473
|
+
|| instruction.pluginName !== job.intent.name || job.intent.revision?.growthRun !== undefined
|
|
3474
|
+
|| instruction.ownerDigest !== job.intent.ownerDigest || instruction.filesDigest !== controlPlaneDigest(job.intent.files)
|
|
3475
|
+
|| this.getGap(job.intent.gapId).idempotencyKey !== `owner-instruction:${instruction.instructionId}`) {
|
|
3476
|
+
throw new ControlPlaneStoreError('invalid-state', 'prepared revision requires a current exact owner instruction');
|
|
3477
|
+
}
|
|
2888
3478
|
}
|
|
2889
3479
|
/** Read-only exact historical binding for an already signed adoption. Caller must hold the current owner fence. */
|
|
2890
3480
|
getRetainedPreparedRevision(planId) {
|
|
2891
3481
|
const plan = this.getSourcePlan(planId);
|
|
3482
|
+
const owner = this.getOwnerTaskFailureReference(plan.gapId);
|
|
3483
|
+
if (owner === undefined)
|
|
3484
|
+
throw new ControlPlaneStoreError('invalid-state', 'retained revision requires an owner task failure gap');
|
|
3485
|
+
const job = this.#retainedPreparedRevisionJob(plan);
|
|
3486
|
+
if (job.intent.ownerInstruction !== undefined || job.intent.ownerDigest !== controlPlaneDigest(owner.owner)) {
|
|
3487
|
+
throw new ControlPlaneStoreError('invalid-state', 'retained revision owner task binding changed');
|
|
3488
|
+
}
|
|
3489
|
+
return { plan, job, reference: owner };
|
|
3490
|
+
}
|
|
3491
|
+
#retainedPreparedRevisionJob(plan) {
|
|
2892
3492
|
if (plan.mode !== 'prepared-revise' || !['pending-approval', 'expired'].includes(plan.status)
|
|
2893
3493
|
|| plan.sourceRevision === undefined || plan.preparedEvidence === undefined || plan.sourceCheck === undefined) {
|
|
2894
3494
|
throw new ControlPlaneStoreError('invalid-state', 'retained revision requires exact prepared evidence');
|
|
2895
3495
|
}
|
|
2896
|
-
const owner = this.getOwnerTaskFailureReference(plan.gapId);
|
|
2897
|
-
if (owner === undefined)
|
|
2898
|
-
throw new ControlPlaneStoreError('invalid-state', 'retained revision requires an owner task failure gap');
|
|
2899
3496
|
const row = this.#database.prepare('SELECT * FROM source_jobs WHERE plan_id = ?')
|
|
2900
3497
|
.get(plan.id);
|
|
2901
3498
|
if (row === undefined)
|
|
@@ -2906,7 +3503,6 @@ export class ControlPlaneStore {
|
|
|
2906
3503
|
|| job.intent.revision === undefined || job.intent.gapId !== plan.gapId
|
|
2907
3504
|
|| job.intent.gapRevision !== plan.gapSnapshot.revision
|
|
2908
3505
|
|| gap.inputDigest !== plan.gapSnapshot.inputDigest
|
|
2909
|
-
|| job.intent.ownerDigest !== controlPlaneDigest(owner.owner)
|
|
2910
3506
|
|| controlPlaneDigest(job.intent.owner) !== job.intent.ownerDigest
|
|
2911
3507
|
|| controlPlaneDigest(job.intent.revision) !== controlPlaneDigest(plan.sourceRevision)
|
|
2912
3508
|
|| job.intent.revision.generatorDigest !== plan.generatorDigest
|
|
@@ -2916,7 +3512,7 @@ export class ControlPlaneStore {
|
|
|
2916
3512
|
|| plan.expiresAt !== plan.createdAt + job.intent.ttlMs) {
|
|
2917
3513
|
throw new ControlPlaneStoreError('invalid-state', 'prepared revision source job binding is corrupt');
|
|
2918
3514
|
}
|
|
2919
|
-
return
|
|
3515
|
+
return job;
|
|
2920
3516
|
}
|
|
2921
3517
|
/** Host-only package bytes for an owner-admitted, still-pending created plugin. */
|
|
2922
3518
|
readPreparedSourceArtifact(planId) {
|
|
@@ -3025,7 +3621,8 @@ export class ControlPlaneStore {
|
|
|
3025
3621
|
}
|
|
3026
3622
|
claimCreationVerification(planId) {
|
|
3027
3623
|
const job = this.getPreparedSourceJob(planId);
|
|
3028
|
-
const
|
|
3624
|
+
const run = job.intent.creation?.growthRun;
|
|
3625
|
+
const pinned = run && run.protocol !== 'assistant-growth/source-run/v3' ? run.creationAcceptance : undefined;
|
|
3029
3626
|
if (!pinned || this.#now() >= pinned.expiresAt) {
|
|
3030
3627
|
throw new ControlPlaneStoreError('invalid-state', 'creation verification requires a current pre-author policy binding');
|
|
3031
3628
|
}
|
|
@@ -3061,21 +3658,42 @@ export class ControlPlaneStore {
|
|
|
3061
3658
|
const job = this.getPreparedRevisionJob(plan.id);
|
|
3062
3659
|
const reference = this.getOwnerTaskFailureReference(plan.gapId);
|
|
3063
3660
|
validateCreationCapabilitySource(source, certificate);
|
|
3064
|
-
if (
|
|
3065
|
-
||
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3661
|
+
if (isPluginRevisionTaskSource(certificate.source)) {
|
|
3662
|
+
if (!reference || controlPlaneDigest(plan.sourceRevision?.parent) !== controlPlaneDigest(certificate.parent) || !plan.sourceCheck || !plan.preparedEvidence || !plan.sourceRevision?.growthRun
|
|
3663
|
+
|| !revisionVerificationMatchesPreparedEvidence(certificate, plan.preparedEvidence)
|
|
3664
|
+
|| !job.intent.revision?.growthRun || plan.digest !== certificate.plan.digest || plan.name !== certificate.plan.name
|
|
3665
|
+
|| plan.sourceCheck.treeDigest !== certificate.plan.sourceTreeDigest
|
|
3666
|
+
|| plan.sourceCheck.patchDigest !== certificate.plan.sourcePatchDigest
|
|
3667
|
+
|| plan.preparedEvidence.pack.sha256 !== certificate.plan.artifactSha256
|
|
3668
|
+
|| plan.preparedEvidence.pack.sizeBytes !== certificate.plan.artifactBytes
|
|
3669
|
+
|| plan.generatorDigest !== certificate.plan.generatorDigest
|
|
3670
|
+
|| controlPlaneDigest(reference) !== certificate.source.referenceDigest
|
|
3671
|
+
|| controlPlaneDigest(reference.owner) !== certificate.source.ownerDigest
|
|
3672
|
+
|| sourceGrowthRunDigest(plan.sourceRevision.growthRun) !== certificate.source.growthRunDigest
|
|
3673
|
+
|| sourceGrowthRunDigest(job.intent.revision.growthRun) !== certificate.source.growthRunDigest
|
|
3674
|
+
|| controlPlaneDigest(plan.sourceRevision.growthRun.model) !== controlPlaneDigest(certificate.model)
|
|
3675
|
+
|| certificate.expiresAt > Math.min(plan.expiresAt, plan.sourceRevision.grant.expiresAt, plan.sourceRevision.growthRun.expiresAt, certificate.authority.expiresAt)) {
|
|
3676
|
+
throw new ControlPlaneStoreError('invalid-input', 'revision verification certificate does not match exact prepared source');
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
else {
|
|
3680
|
+
// Owner-instruction revisions: the signed instruction source replaces
|
|
3681
|
+
// the task reference and growth run, which must be absent on both sides.
|
|
3682
|
+
const instruction = job.intent.ownerInstruction;
|
|
3683
|
+
if (!instruction || plan.sourceRevision?.growthRun !== undefined || job.intent.revision?.growthRun !== undefined
|
|
3684
|
+
|| !plan.sourceCheck || !plan.preparedEvidence || certificate.protocol !== 'assistant-growth/revision-verification/v3'
|
|
3685
|
+
|| plan.digest !== certificate.plan.digest || plan.name !== certificate.plan.name
|
|
3686
|
+
|| plan.sourceCheck.treeDigest !== certificate.plan.sourceTreeDigest
|
|
3687
|
+
|| plan.sourceCheck.patchDigest !== certificate.plan.sourcePatchDigest
|
|
3688
|
+
|| plan.preparedEvidence.pack.sha256 !== certificate.plan.artifactSha256
|
|
3689
|
+
|| plan.preparedEvidence.pack.sizeBytes !== certificate.plan.artifactBytes
|
|
3690
|
+
|| plan.generatorDigest !== certificate.plan.generatorDigest
|
|
3691
|
+
|| controlPlaneDigest(plan.sourceRevision?.parent) !== controlPlaneDigest(certificate.parent)
|
|
3692
|
+
|| certificate.source.instructionDigest !== instruction.digest
|
|
3693
|
+
|| growthObjectDigest(job.intent.owner) !== certificate.source.ownerDigest
|
|
3694
|
+
|| certificate.expiresAt > Math.min(plan.expiresAt, plan.sourceRevision.grant.expiresAt, certificate.authority.expiresAt)) {
|
|
3695
|
+
throw new ControlPlaneStoreError('invalid-input', 'instruction revision certificate does not match exact prepared source');
|
|
3696
|
+
}
|
|
3079
3697
|
}
|
|
3080
3698
|
this.readPreparedSourceArtifact(plan.id);
|
|
3081
3699
|
if (source.baseCommit !== plan.baseCommit || controlPlaneDigest(source.scope) !== controlPlaneDigest(plan.scope))
|
|
@@ -3159,7 +3777,7 @@ export class ControlPlaneStore {
|
|
|
3159
3777
|
claimRevisionVerification(planId) {
|
|
3160
3778
|
const job = this.getPreparedRevisionJob(planId);
|
|
3161
3779
|
const pinned = job.intent.revision?.growthRun?.revisionAcceptance;
|
|
3162
|
-
if (!pinned || this.#now() >= pinned.expiresAt) {
|
|
3780
|
+
if (job.intent.ownerInstruction === undefined && (!pinned || this.#now() >= pinned.expiresAt)) {
|
|
3163
3781
|
throw new ControlPlaneStoreError('invalid-state', 'revision verification requires a current pre-author policy binding');
|
|
3164
3782
|
}
|
|
3165
3783
|
this.#beginMutation();
|
|
@@ -3197,11 +3815,17 @@ export class ControlPlaneStore {
|
|
|
3197
3815
|
const source = this.readRevisionSource(plan.id);
|
|
3198
3816
|
const reference = this.getOwnerTaskFailureReference(plan.gapId);
|
|
3199
3817
|
const run = plan.sourceRevision?.growthRun;
|
|
3818
|
+
// Regressions exist only for task-failure revisions with frozen acceptance pins.
|
|
3819
|
+
if (!candidate || !isPluginRevisionTaskSource(candidate.source)
|
|
3820
|
+
|| candidate.protocol === 'assistant-growth/revision-verification/v3') {
|
|
3821
|
+
throw new ControlPlaneStoreError('invalid-input', 'owner-instruction revisions carry no regression acceptance');
|
|
3822
|
+
}
|
|
3823
|
+
const candidateSource = candidate.source;
|
|
3200
3824
|
const previousJobs = new Set([...(parent?.observations ?? []), ...(candidate?.observations ?? [])].map(item => item.jobId));
|
|
3201
3825
|
if (certificate.verifiedAt > this.#now() || certificate.expiresAt <= this.#now()
|
|
3202
3826
|
|| !candidate || !parent || !source || !reference || !run?.revisionRegressionAcceptance
|
|
3203
3827
|
|| !revisionRegressionMatchesCandidate(certificate, candidate, parent)
|
|
3204
|
-
|| !job.intent.revision?.growthRun
|
|
3828
|
+
|| !job.intent.revision?.growthRun?.revisionRegressionAcceptance
|
|
3205
3829
|
|| controlPlaneDigest(certificate.plan) !== controlPlaneDigest(candidate.plan)
|
|
3206
3830
|
|| controlPlaneDigest(certificate.source) !== controlPlaneDigest(candidate.source)
|
|
3207
3831
|
|| controlPlaneDigest(certificate.parent) !== controlPlaneDigest(candidate.parent)
|
|
@@ -3214,10 +3838,9 @@ export class ControlPlaneStore {
|
|
|
3214
3838
|
|| controlPlaneDigest(certificate.model) !== controlPlaneDigest(run.model)
|
|
3215
3839
|
|| controlPlaneDigest(certificate.authority) !== controlPlaneDigest(run.revisionRegressionAcceptance)
|
|
3216
3840
|
|| controlPlaneDigest(certificate.authority) !== controlPlaneDigest(job.intent.revision.growthRun.revisionRegressionAcceptance)
|
|
3217
|
-
||
|
|
3218
|
-
||
|
|
3219
|
-
||
|
|
3220
|
-
|| sourceGrowthRunDigest(run) !== sourceGrowthRunDigest(job.intent.revision.growthRun)
|
|
3841
|
+
|| candidateSource.referenceDigest !== controlPlaneDigest(reference)
|
|
3842
|
+
|| candidateSource.ownerDigest !== controlPlaneDigest(reference.owner)
|
|
3843
|
+
|| candidateSource.growthRunDigest !== sourceGrowthRunDigest(run)
|
|
3221
3844
|
|| certificate.observations.length !== parent.observations.length
|
|
3222
3845
|
|| certificate.observations.some((item, index) => item.caseId !== parent.observations[index]?.caseId
|
|
3223
3846
|
|| previousJobs.has(item.parent.jobId) || previousJobs.has(item.candidate.jobId))
|
|
@@ -3315,6 +3938,53 @@ export class ControlPlaneStore {
|
|
|
3315
3938
|
if (Number(result.changes) !== 1)
|
|
3316
3939
|
throw new ControlPlaneStoreError('conflict', 'revision regression claim is not current');
|
|
3317
3940
|
}
|
|
3941
|
+
/**
|
|
3942
|
+
* P1-4 non-inferiority outcome for one evaluated plan window. Rows exist
|
|
3943
|
+
* only under a verified revision verification (FK enforces it); the
|
|
3944
|
+
* contract digest pins which owner contract produced the verdict.
|
|
3945
|
+
*/
|
|
3946
|
+
recordRevisionBenefit(planId, input) {
|
|
3947
|
+
if (!/^[a-f0-9]{64}$/u.test(input.contractDigest)
|
|
3948
|
+
|| !['insufficient', 'inferior', 'non-inferior'].includes(input.status)
|
|
3949
|
+
|| typeof input.reason !== 'string' || !/^[a-z][a-z0-9-]{0,127}$/u.test(input.reason)) {
|
|
3950
|
+
throw new ControlPlaneStoreError('invalid-input', 'revision benefit outcome fields are invalid');
|
|
3951
|
+
}
|
|
3952
|
+
const evaluationJson = JSON.stringify(input.evaluation);
|
|
3953
|
+
if (Buffer.byteLength(evaluationJson) > 1_048_576) {
|
|
3954
|
+
throw new ControlPlaneStoreError('invalid-input', 'revision benefit evaluation exceeds its bound');
|
|
3955
|
+
}
|
|
3956
|
+
this.#beginMutation();
|
|
3957
|
+
try {
|
|
3958
|
+
this.#database.prepare(`INSERT INTO source_revision_benefit_outcomes
|
|
3959
|
+
(plan_id, contract_digest, status, reason, evaluation_json, recorded_at) VALUES (?, ?, ?, ?, ?, ?)
|
|
3960
|
+
ON CONFLICT(plan_id) DO UPDATE SET contract_digest=excluded.contract_digest, status=excluded.status,
|
|
3961
|
+
reason=excluded.reason, evaluation_json=excluded.evaluation_json, recorded_at=excluded.recorded_at
|
|
3962
|
+
WHERE source_revision_benefit_outcomes.recorded_at <= excluded.recorded_at`)
|
|
3963
|
+
.run(planId, input.contractDigest, input.status, input.reason, evaluationJson, this.#now());
|
|
3964
|
+
this.#commitMutation();
|
|
3965
|
+
}
|
|
3966
|
+
catch (error) {
|
|
3967
|
+
this.#rollbackMutation();
|
|
3968
|
+
throw error;
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
getRevisionBenefitStatus(planId) {
|
|
3972
|
+
const row = this.#database.prepare('SELECT status FROM source_revision_benefit_outcomes WHERE plan_id=?')
|
|
3973
|
+
.get(planId);
|
|
3974
|
+
return row?.status;
|
|
3975
|
+
}
|
|
3976
|
+
inspectRevisionBenefitOutcome(planId) {
|
|
3977
|
+
const row = this.#database.prepare(`SELECT contract_digest,status,reason,recorded_at
|
|
3978
|
+
FROM source_revision_benefit_outcomes WHERE plan_id=?`).get(planId);
|
|
3979
|
+
if (row === undefined)
|
|
3980
|
+
return undefined;
|
|
3981
|
+
if (!/^[a-f0-9]{64}$/u.test(row.contract_digest)
|
|
3982
|
+
|| !['insufficient', 'inferior', 'non-inferior'].includes(row.status)
|
|
3983
|
+
|| !/^[a-z][a-z0-9-]{0,127}$/u.test(row.reason) || !Number.isSafeInteger(row.recorded_at) || row.recorded_at < 1) {
|
|
3984
|
+
throw new ControlPlaneStoreError('invalid-state', 'revision benefit outcome record is corrupt');
|
|
3985
|
+
}
|
|
3986
|
+
return { contractDigest: row.contract_digest, status: row.status, reason: row.reason, recordedAt: row.recorded_at };
|
|
3987
|
+
}
|
|
3318
3988
|
readRevisionSource(planId) {
|
|
3319
3989
|
const certificate = this.getRevisionVerification(planId);
|
|
3320
3990
|
if (!certificate)
|
|
@@ -3593,7 +4263,7 @@ export class ControlPlaneStore {
|
|
|
3593
4263
|
? this.#database.prepare(`SELECT * FROM source_plans WHERE mode = 'modify' ORDER BY created_at, id`).all()
|
|
3594
4264
|
: this.#database.prepare(`SELECT * FROM source_plans WHERE mode = 'modify' AND expires_at < ? ORDER BY created_at, id`)
|
|
3595
4265
|
.all(filter.expiredBefore);
|
|
3596
|
-
return rows.map(sourceFromRow);
|
|
4266
|
+
return rows.map(row => sourceFromRow(row));
|
|
3597
4267
|
}
|
|
3598
4268
|
/** Isolated worktrees owned by the background prepared flows, including owner-granted creation. */
|
|
3599
4269
|
listPreparedSourcePlans(filter) {
|
|
@@ -3602,7 +4272,7 @@ export class ControlPlaneStore {
|
|
|
3602
4272
|
.all()
|
|
3603
4273
|
: this.#database.prepare(`SELECT * FROM source_plans WHERE mode IN ('modify', 'prepared-create', 'prepared-revise') AND expires_at < ? ORDER BY created_at, id`)
|
|
3604
4274
|
.all(filter.expiredBefore);
|
|
3605
|
-
return rows.map(
|
|
4275
|
+
return rows.map(row => this.#readSourcePlanRow(row));
|
|
3606
4276
|
}
|
|
3607
4277
|
/** Retire an expired prepared plan before removing its worktree. The CAS
|
|
3608
4278
|
* fences owner approval/review, and releases only this plan's gap claim. */
|
|
@@ -6241,8 +6911,9 @@ export class ControlPlaneStore {
|
|
|
6241
6911
|
const receipt = this.#receipt(idempotencyKey, operation, inputDigest);
|
|
6242
6912
|
if (receipt === undefined)
|
|
6243
6913
|
return undefined;
|
|
6244
|
-
const result = sourceSnapshotFromStored(receipt.result);
|
|
6245
6914
|
const authoritative = this.getSourcePlan(planId);
|
|
6915
|
+
const result = sourceSnapshotFromStored(receipt.result, authoritative.sourceRevision !== undefined
|
|
6916
|
+
&& authoritative.sourceRevision.growthRun === undefined);
|
|
6246
6917
|
if (result.id !== planId || result.digest !== authoritative.digest || result.gapId !== authoritative.gapId
|
|
6247
6918
|
|| result.revision > authoritative.revision
|
|
6248
6919
|
|| (result.approval !== undefined && controlPlaneDigest(result.approval) !== controlPlaneDigest(authoritative.approval))
|
|
@@ -6261,7 +6932,9 @@ export class ControlPlaneStore {
|
|
|
6261
6932
|
const receipt = this.#receipt(idempotencyKey, operation, inputDigest);
|
|
6262
6933
|
if (receipt === undefined)
|
|
6263
6934
|
return undefined;
|
|
6264
|
-
|
|
6935
|
+
// The authoritative read below must prove the linked owner instruction
|
|
6936
|
+
// for the historical v3 shape before this receipt can be returned.
|
|
6937
|
+
const snapshot = sourceSnapshotFromStored(receipt.result, true);
|
|
6265
6938
|
const authoritative = this.getSourcePlan(snapshot.id);
|
|
6266
6939
|
const replayBinding = { operation: 'create-source-plan', gapId: snapshot.gapId, repository: snapshot.repository,
|
|
6267
6940
|
worktree: snapshot.worktree, baseCommit: snapshot.baseCommit, name: snapshot.name, generatorDigest: snapshot.generatorDigest,
|