@dsh-enhanced/plugin-control-plane 0.1.67 → 0.1.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -20
- package/lib/cli.d.ts.map +1 -1
- package/lib/cli.js +66 -11
- package/lib/cli.js.map +1 -1
- package/lib/creation-capability-feedback.d.ts +16 -1
- package/lib/creation-capability-feedback.d.ts.map +1 -1
- package/lib/creation-capability-feedback.js +82 -5
- package/lib/creation-capability-feedback.js.map +1 -1
- package/lib/creation-capability-journal.d.ts.map +1 -1
- package/lib/creation-capability-journal.js +2 -0
- package/lib/creation-capability-journal.js.map +1 -1
- package/lib/creation-capability-runtime.d.ts.map +1 -1
- package/lib/creation-capability-runtime.js +2 -0
- package/lib/creation-capability-runtime.js.map +1 -1
- package/lib/creation-capability-types.d.ts +1 -0
- package/lib/creation-capability-types.d.ts.map +1 -1
- package/lib/linux-source-build.d.ts +13 -2
- package/lib/linux-source-build.d.ts.map +1 -1
- package/lib/linux-source-build.js.map +1 -1
- package/lib/live-qualification-authority.js +2 -2
- package/lib/live-qualification-authority.js.map +1 -1
- package/lib/owner-instruction-drain.d.ts +60 -0
- package/lib/owner-instruction-drain.d.ts.map +1 -0
- package/lib/owner-instruction-drain.js +101 -0
- package/lib/owner-instruction-drain.js.map +1 -0
- package/lib/revision-capability-feedback.d.ts +165 -0
- package/lib/revision-capability-feedback.d.ts.map +1 -0
- package/lib/revision-capability-feedback.js +316 -0
- package/lib/revision-capability-feedback.js.map +1 -0
- package/lib/revision-capability-journal.d.ts +23 -0
- package/lib/revision-capability-journal.d.ts.map +1 -1
- package/lib/revision-capability-journal.js +124 -15
- package/lib/revision-capability-journal.js.map +1 -1
- package/lib/revision-capability-runtime.d.ts +19 -1
- package/lib/revision-capability-runtime.d.ts.map +1 -1
- package/lib/revision-capability-runtime.js +80 -7
- package/lib/revision-capability-runtime.js.map +1 -1
- package/lib/revision-capability-types.d.ts +50 -4
- package/lib/revision-capability-types.d.ts.map +1 -1
- package/lib/service.d.ts +174 -3
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +732 -68
- package/lib/service.js.map +1 -1
- package/lib/source-adoption-authority.js +2 -2
- package/lib/source-adoption-authority.js.map +1 -1
- package/lib/source-approval-authority.js +3 -3
- package/lib/source-approval-authority.js.map +1 -1
- package/lib/source-build.d.ts +33 -53
- package/lib/source-build.d.ts.map +1 -1
- package/lib/source-build.js +28 -408
- package/lib/source-build.js.map +1 -1
- package/lib/source-job-types.d.ts +111 -4
- package/lib/source-job-types.d.ts.map +1 -1
- package/lib/source-jobs.d.ts +68 -4
- package/lib/source-jobs.d.ts.map +1 -1
- package/lib/source-jobs.js +296 -46
- package/lib/source-jobs.js.map +1 -1
- package/lib/source-release-authority.js +2 -2
- package/lib/source-release-authority.js.map +1 -1
- package/lib/source-revision.d.ts +29 -3
- package/lib/source-revision.d.ts.map +1 -1
- package/lib/source-revision.js +40 -9
- package/lib/source-revision.js.map +1 -1
- package/lib/source-workspace.d.ts +11 -4
- package/lib/source-workspace.d.ts.map +1 -1
- package/lib/source-workspace.js +51 -17
- package/lib/source-workspace.js.map +1 -1
- package/lib/sqlite.d.ts +6 -7
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +255 -17
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +142 -3
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +732 -63
- package/lib/store.js.map +1 -1
- package/lib/systemd-host-authority.js +4 -4
- package/lib/systemd-host-authority.js.map +1 -1
- package/lib/task-observation-authority.js +2 -2
- package/lib/task-observation-authority.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/work-item-creation-contract.d.ts +3 -1
- package/lib/work-item-creation-contract.d.ts.map +1 -1
- package/lib/work-item-creation-contract.js +10 -1
- package/lib/work-item-creation-contract.js.map +1 -1
- package/package.json +11 -8
package/lib/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');
|
|
@@ -2538,10 +3062,12 @@ export class ControlPlaneStore {
|
|
|
2538
3062
|
/** Prepared owner continuations have a bounded recovery query, independent of job history. */
|
|
2539
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,13 +3077,15 @@ 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
3086
|
AND NOT EXISTS (SELECT 1 FROM source_revision_regressions v WHERE v.plan_id=p.id))
|
|
2560
3087
|
OR (? = 1 AND p.mode = 'prepared-revise' AND p.status = 'pending-approval'
|
|
3088
|
+
AND g.gap_id IS NOT NULL
|
|
2561
3089
|
AND EXISTS (SELECT 1 FROM source_revision_verifications v WHERE v.plan_id=p.id AND v.status='verified')
|
|
2562
3090
|
AND (json_type(j.intent_json, '$.revision.growthRun.revisionRegressionAcceptance') IS NULL
|
|
2563
3091
|
OR EXISTS (SELECT 1 FROM source_revision_regressions r WHERE r.plan_id=p.id AND r.status='verified'))))
|
|
@@ -2652,7 +3180,10 @@ export class ControlPlaneStore {
|
|
|
2652
3180
|
|| (creation !== undefined && (creation.generatorDigest !== input.generatorDigest || !name.startsWith(creation.grant.namePrefix)))) {
|
|
2653
3181
|
throw new ControlPlaneStoreError('invalid-input', 'prepared source creation binding is invalid');
|
|
2654
3182
|
}
|
|
2655
|
-
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 });
|
|
2656
3187
|
if ((mode === 'prepared-revise') !== (revision !== undefined)
|
|
2657
3188
|
|| (revision !== undefined && (revision.generatorDigest !== input.generatorDigest || !name.startsWith(revision.grant.namePrefix)))) {
|
|
2658
3189
|
throw new ControlPlaneStoreError('invalid-input', 'prepared source revision binding is invalid');
|
|
@@ -2684,9 +3215,6 @@ export class ControlPlaneStore {
|
|
|
2684
3215
|
|| createHash('sha256').update(input.preparedArtifact).digest('hex') !== preparedEvidence.pack.sha256) {
|
|
2685
3216
|
throw new ControlPlaneStoreError('invalid-input', 'prepared source artifact differs from checked package evidence');
|
|
2686
3217
|
}
|
|
2687
|
-
if (this.getOwnerTaskFailureReference(input.gapId) === undefined) {
|
|
2688
|
-
throw new ControlPlaneStoreError('invalid-state', 'prepared source artifact requires an owner task failure gap');
|
|
2689
|
-
}
|
|
2690
3218
|
preparedArtifact = Buffer.from(input.preparedArtifact);
|
|
2691
3219
|
}
|
|
2692
3220
|
positiveInteger(input.ttlMs, 'ttlMs');
|
|
@@ -2759,6 +3287,16 @@ export class ControlPlaneStore {
|
|
|
2759
3287
|
|| (mode === 'prepared-revise' && controlPlaneDigest(sourceJob.intent.revision) !== controlPlaneDigest(revision))) {
|
|
2760
3288
|
throw new ControlPlaneStoreError('conflict', 'source job completion lost its immutable running binding');
|
|
2761
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
|
+
}
|
|
2762
3300
|
if (now >= sourceJob.expiresAt || (creation !== undefined && now >= creation.grant.expiresAt) || (revision !== undefined && now >= revision.grant.expiresAt))
|
|
2763
3301
|
throw new ControlPlaneStoreError('expired', 'source job authority is expired');
|
|
2764
3302
|
if (mode === 'prepared-create' && this.getOwnerTaskFailureReference(input.gapId) !== undefined
|
|
@@ -2766,7 +3304,8 @@ export class ControlPlaneStore {
|
|
|
2766
3304
|
throw new ControlPlaneStoreError('expired', 'task-backed creation growth run is absent or expired');
|
|
2767
3305
|
}
|
|
2768
3306
|
}
|
|
2769
|
-
if (revision !== undefined &&
|
|
3307
|
+
if (revision !== undefined && revision.growthRun !== undefined
|
|
3308
|
+
&& (this.getOwnerTaskFailureReference(input.gapId) === undefined || now >= revision.growthRun.expiresAt)) {
|
|
2770
3309
|
throw new ControlPlaneStoreError('expired', 'revision requires a current owner task and frozen growth run');
|
|
2771
3310
|
}
|
|
2772
3311
|
if (preparedArtifact !== undefined) {
|
|
@@ -2828,7 +3367,19 @@ export class ControlPlaneStore {
|
|
|
2828
3367
|
const row = this.#database.prepare('SELECT * FROM source_plans WHERE id = ?').get(id);
|
|
2829
3368
|
if (row === undefined)
|
|
2830
3369
|
throw new ControlPlaneStoreError('not-found', 'source plan not found');
|
|
2831
|
-
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;
|
|
2832
3383
|
}
|
|
2833
3384
|
/** Exact durable source job for a currently admitted prepared creation. */
|
|
2834
3385
|
getPreparedSourceJob(planId) {
|
|
@@ -2884,22 +3435,64 @@ export class ControlPlaneStore {
|
|
|
2884
3435
|
|| this.#now() >= plan.expiresAt || this.#now() >= plan.sourceRevision.grant.expiresAt) {
|
|
2885
3436
|
throw new ControlPlaneStoreError('invalid-state', 'prepared revision requires a current pending plan');
|
|
2886
3437
|
}
|
|
3438
|
+
const job = this.#retainedPreparedRevisionJob(plan);
|
|
2887
3439
|
const owner = this.getOwnerTaskFailureReference(plan.gapId);
|
|
2888
|
-
if (owner === undefined)
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
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
|
+
}
|
|
2892
3478
|
}
|
|
2893
3479
|
/** Read-only exact historical binding for an already signed adoption. Caller must hold the current owner fence. */
|
|
2894
3480
|
getRetainedPreparedRevision(planId) {
|
|
2895
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) {
|
|
2896
3492
|
if (plan.mode !== 'prepared-revise' || !['pending-approval', 'expired'].includes(plan.status)
|
|
2897
3493
|
|| plan.sourceRevision === undefined || plan.preparedEvidence === undefined || plan.sourceCheck === undefined) {
|
|
2898
3494
|
throw new ControlPlaneStoreError('invalid-state', 'retained revision requires exact prepared evidence');
|
|
2899
3495
|
}
|
|
2900
|
-
const owner = this.getOwnerTaskFailureReference(plan.gapId);
|
|
2901
|
-
if (owner === undefined)
|
|
2902
|
-
throw new ControlPlaneStoreError('invalid-state', 'retained revision requires an owner task failure gap');
|
|
2903
3496
|
const row = this.#database.prepare('SELECT * FROM source_jobs WHERE plan_id = ?')
|
|
2904
3497
|
.get(plan.id);
|
|
2905
3498
|
if (row === undefined)
|
|
@@ -2910,7 +3503,6 @@ export class ControlPlaneStore {
|
|
|
2910
3503
|
|| job.intent.revision === undefined || job.intent.gapId !== plan.gapId
|
|
2911
3504
|
|| job.intent.gapRevision !== plan.gapSnapshot.revision
|
|
2912
3505
|
|| gap.inputDigest !== plan.gapSnapshot.inputDigest
|
|
2913
|
-
|| job.intent.ownerDigest !== controlPlaneDigest(owner.owner)
|
|
2914
3506
|
|| controlPlaneDigest(job.intent.owner) !== job.intent.ownerDigest
|
|
2915
3507
|
|| controlPlaneDigest(job.intent.revision) !== controlPlaneDigest(plan.sourceRevision)
|
|
2916
3508
|
|| job.intent.revision.generatorDigest !== plan.generatorDigest
|
|
@@ -2920,7 +3512,7 @@ export class ControlPlaneStore {
|
|
|
2920
3512
|
|| plan.expiresAt !== plan.createdAt + job.intent.ttlMs) {
|
|
2921
3513
|
throw new ControlPlaneStoreError('invalid-state', 'prepared revision source job binding is corrupt');
|
|
2922
3514
|
}
|
|
2923
|
-
return
|
|
3515
|
+
return job;
|
|
2924
3516
|
}
|
|
2925
3517
|
/** Host-only package bytes for an owner-admitted, still-pending created plugin. */
|
|
2926
3518
|
readPreparedSourceArtifact(planId) {
|
|
@@ -3029,7 +3621,8 @@ export class ControlPlaneStore {
|
|
|
3029
3621
|
}
|
|
3030
3622
|
claimCreationVerification(planId) {
|
|
3031
3623
|
const job = this.getPreparedSourceJob(planId);
|
|
3032
|
-
const
|
|
3624
|
+
const run = job.intent.creation?.growthRun;
|
|
3625
|
+
const pinned = run && run.protocol !== 'assistant-growth/source-run/v3' ? run.creationAcceptance : undefined;
|
|
3033
3626
|
if (!pinned || this.#now() >= pinned.expiresAt) {
|
|
3034
3627
|
throw new ControlPlaneStoreError('invalid-state', 'creation verification requires a current pre-author policy binding');
|
|
3035
3628
|
}
|
|
@@ -3065,21 +3658,42 @@ export class ControlPlaneStore {
|
|
|
3065
3658
|
const job = this.getPreparedRevisionJob(plan.id);
|
|
3066
3659
|
const reference = this.getOwnerTaskFailureReference(plan.gapId);
|
|
3067
3660
|
validateCreationCapabilitySource(source, certificate);
|
|
3068
|
-
if (
|
|
3069
|
-
||
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
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
|
+
}
|
|
3083
3697
|
}
|
|
3084
3698
|
this.readPreparedSourceArtifact(plan.id);
|
|
3085
3699
|
if (source.baseCommit !== plan.baseCommit || controlPlaneDigest(source.scope) !== controlPlaneDigest(plan.scope))
|
|
@@ -3163,7 +3777,7 @@ export class ControlPlaneStore {
|
|
|
3163
3777
|
claimRevisionVerification(planId) {
|
|
3164
3778
|
const job = this.getPreparedRevisionJob(planId);
|
|
3165
3779
|
const pinned = job.intent.revision?.growthRun?.revisionAcceptance;
|
|
3166
|
-
if (!pinned || this.#now() >= pinned.expiresAt) {
|
|
3780
|
+
if (job.intent.ownerInstruction === undefined && (!pinned || this.#now() >= pinned.expiresAt)) {
|
|
3167
3781
|
throw new ControlPlaneStoreError('invalid-state', 'revision verification requires a current pre-author policy binding');
|
|
3168
3782
|
}
|
|
3169
3783
|
this.#beginMutation();
|
|
@@ -3201,11 +3815,17 @@ export class ControlPlaneStore {
|
|
|
3201
3815
|
const source = this.readRevisionSource(plan.id);
|
|
3202
3816
|
const reference = this.getOwnerTaskFailureReference(plan.gapId);
|
|
3203
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;
|
|
3204
3824
|
const previousJobs = new Set([...(parent?.observations ?? []), ...(candidate?.observations ?? [])].map(item => item.jobId));
|
|
3205
3825
|
if (certificate.verifiedAt > this.#now() || certificate.expiresAt <= this.#now()
|
|
3206
3826
|
|| !candidate || !parent || !source || !reference || !run?.revisionRegressionAcceptance
|
|
3207
3827
|
|| !revisionRegressionMatchesCandidate(certificate, candidate, parent)
|
|
3208
|
-
|| !job.intent.revision?.growthRun
|
|
3828
|
+
|| !job.intent.revision?.growthRun?.revisionRegressionAcceptance
|
|
3209
3829
|
|| controlPlaneDigest(certificate.plan) !== controlPlaneDigest(candidate.plan)
|
|
3210
3830
|
|| controlPlaneDigest(certificate.source) !== controlPlaneDigest(candidate.source)
|
|
3211
3831
|
|| controlPlaneDigest(certificate.parent) !== controlPlaneDigest(candidate.parent)
|
|
@@ -3218,10 +3838,9 @@ export class ControlPlaneStore {
|
|
|
3218
3838
|
|| controlPlaneDigest(certificate.model) !== controlPlaneDigest(run.model)
|
|
3219
3839
|
|| controlPlaneDigest(certificate.authority) !== controlPlaneDigest(run.revisionRegressionAcceptance)
|
|
3220
3840
|
|| controlPlaneDigest(certificate.authority) !== controlPlaneDigest(job.intent.revision.growthRun.revisionRegressionAcceptance)
|
|
3221
|
-
||
|
|
3222
|
-
||
|
|
3223
|
-
||
|
|
3224
|
-
|| sourceGrowthRunDigest(run) !== sourceGrowthRunDigest(job.intent.revision.growthRun)
|
|
3841
|
+
|| candidateSource.referenceDigest !== controlPlaneDigest(reference)
|
|
3842
|
+
|| candidateSource.ownerDigest !== controlPlaneDigest(reference.owner)
|
|
3843
|
+
|| candidateSource.growthRunDigest !== sourceGrowthRunDigest(run)
|
|
3225
3844
|
|| certificate.observations.length !== parent.observations.length
|
|
3226
3845
|
|| certificate.observations.some((item, index) => item.caseId !== parent.observations[index]?.caseId
|
|
3227
3846
|
|| previousJobs.has(item.parent.jobId) || previousJobs.has(item.candidate.jobId))
|
|
@@ -3319,6 +3938,53 @@ export class ControlPlaneStore {
|
|
|
3319
3938
|
if (Number(result.changes) !== 1)
|
|
3320
3939
|
throw new ControlPlaneStoreError('conflict', 'revision regression claim is not current');
|
|
3321
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
|
+
}
|
|
3322
3988
|
readRevisionSource(planId) {
|
|
3323
3989
|
const certificate = this.getRevisionVerification(planId);
|
|
3324
3990
|
if (!certificate)
|
|
@@ -3597,7 +4263,7 @@ export class ControlPlaneStore {
|
|
|
3597
4263
|
? this.#database.prepare(`SELECT * FROM source_plans WHERE mode = 'modify' ORDER BY created_at, id`).all()
|
|
3598
4264
|
: this.#database.prepare(`SELECT * FROM source_plans WHERE mode = 'modify' AND expires_at < ? ORDER BY created_at, id`)
|
|
3599
4265
|
.all(filter.expiredBefore);
|
|
3600
|
-
return rows.map(sourceFromRow);
|
|
4266
|
+
return rows.map(row => sourceFromRow(row));
|
|
3601
4267
|
}
|
|
3602
4268
|
/** Isolated worktrees owned by the background prepared flows, including owner-granted creation. */
|
|
3603
4269
|
listPreparedSourcePlans(filter) {
|
|
@@ -3606,7 +4272,7 @@ export class ControlPlaneStore {
|
|
|
3606
4272
|
.all()
|
|
3607
4273
|
: this.#database.prepare(`SELECT * FROM source_plans WHERE mode IN ('modify', 'prepared-create', 'prepared-revise') AND expires_at < ? ORDER BY created_at, id`)
|
|
3608
4274
|
.all(filter.expiredBefore);
|
|
3609
|
-
return rows.map(
|
|
4275
|
+
return rows.map(row => this.#readSourcePlanRow(row));
|
|
3610
4276
|
}
|
|
3611
4277
|
/** Retire an expired prepared plan before removing its worktree. The CAS
|
|
3612
4278
|
* fences owner approval/review, and releases only this plan's gap claim. */
|
|
@@ -6245,8 +6911,9 @@ export class ControlPlaneStore {
|
|
|
6245
6911
|
const receipt = this.#receipt(idempotencyKey, operation, inputDigest);
|
|
6246
6912
|
if (receipt === undefined)
|
|
6247
6913
|
return undefined;
|
|
6248
|
-
const result = sourceSnapshotFromStored(receipt.result);
|
|
6249
6914
|
const authoritative = this.getSourcePlan(planId);
|
|
6915
|
+
const result = sourceSnapshotFromStored(receipt.result, authoritative.sourceRevision !== undefined
|
|
6916
|
+
&& authoritative.sourceRevision.growthRun === undefined);
|
|
6250
6917
|
if (result.id !== planId || result.digest !== authoritative.digest || result.gapId !== authoritative.gapId
|
|
6251
6918
|
|| result.revision > authoritative.revision
|
|
6252
6919
|
|| (result.approval !== undefined && controlPlaneDigest(result.approval) !== controlPlaneDigest(authoritative.approval))
|
|
@@ -6265,7 +6932,9 @@ export class ControlPlaneStore {
|
|
|
6265
6932
|
const receipt = this.#receipt(idempotencyKey, operation, inputDigest);
|
|
6266
6933
|
if (receipt === undefined)
|
|
6267
6934
|
return undefined;
|
|
6268
|
-
|
|
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);
|
|
6269
6938
|
const authoritative = this.getSourcePlan(snapshot.id);
|
|
6270
6939
|
const replayBinding = { operation: 'create-source-plan', gapId: snapshot.gapId, repository: snapshot.repository,
|
|
6271
6940
|
worktree: snapshot.worktree, baseCommit: snapshot.baseCommit, name: snapshot.name, generatorDigest: snapshot.generatorDigest,
|