@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.
Files changed (97) hide show
  1. package/README.md +96 -22
  2. package/lib/cli.d.ts.map +1 -1
  3. package/lib/cli.js +66 -11
  4. package/lib/cli.js.map +1 -1
  5. package/lib/creation-capability-execution.d.ts +5 -1
  6. package/lib/creation-capability-execution.d.ts.map +1 -1
  7. package/lib/creation-capability-execution.js +42 -0
  8. package/lib/creation-capability-execution.js.map +1 -1
  9. package/lib/creation-capability-feedback.d.ts +16 -1
  10. package/lib/creation-capability-feedback.d.ts.map +1 -1
  11. package/lib/creation-capability-feedback.js +82 -5
  12. package/lib/creation-capability-feedback.js.map +1 -1
  13. package/lib/creation-capability-journal.d.ts.map +1 -1
  14. package/lib/creation-capability-journal.js +2 -0
  15. package/lib/creation-capability-journal.js.map +1 -1
  16. package/lib/creation-capability-runtime.d.ts +47 -1
  17. package/lib/creation-capability-runtime.d.ts.map +1 -1
  18. package/lib/creation-capability-runtime.js +105 -1
  19. package/lib/creation-capability-runtime.js.map +1 -1
  20. package/lib/creation-capability-types.d.ts +1 -0
  21. package/lib/creation-capability-types.d.ts.map +1 -1
  22. package/lib/index.d.ts +3 -0
  23. package/lib/index.d.ts.map +1 -1
  24. package/lib/index.js +2 -0
  25. package/lib/index.js.map +1 -1
  26. package/lib/linux-source-build.d.ts +13 -2
  27. package/lib/linux-source-build.d.ts.map +1 -1
  28. package/lib/linux-source-build.js.map +1 -1
  29. package/lib/live-qualification-authority.js +2 -2
  30. package/lib/live-qualification-authority.js.map +1 -1
  31. package/lib/owner-instruction-drain.d.ts +60 -0
  32. package/lib/owner-instruction-drain.d.ts.map +1 -0
  33. package/lib/owner-instruction-drain.js +101 -0
  34. package/lib/owner-instruction-drain.js.map +1 -0
  35. package/lib/revision-capability-feedback.d.ts +165 -0
  36. package/lib/revision-capability-feedback.d.ts.map +1 -0
  37. package/lib/revision-capability-feedback.js +316 -0
  38. package/lib/revision-capability-feedback.js.map +1 -0
  39. package/lib/revision-capability-journal.d.ts +126 -0
  40. package/lib/revision-capability-journal.d.ts.map +1 -0
  41. package/lib/revision-capability-journal.js +981 -0
  42. package/lib/revision-capability-journal.js.map +1 -0
  43. package/lib/revision-capability-runtime.d.ts +92 -0
  44. package/lib/revision-capability-runtime.d.ts.map +1 -0
  45. package/lib/revision-capability-runtime.js +635 -0
  46. package/lib/revision-capability-runtime.js.map +1 -0
  47. package/lib/revision-capability-types.d.ts +335 -0
  48. package/lib/revision-capability-types.d.ts.map +1 -0
  49. package/lib/revision-capability-types.js +2 -0
  50. package/lib/revision-capability-types.js.map +1 -0
  51. package/lib/service.d.ts +199 -4
  52. package/lib/service.d.ts.map +1 -1
  53. package/lib/service.js +925 -70
  54. package/lib/service.js.map +1 -1
  55. package/lib/source-adoption-authority.js +2 -2
  56. package/lib/source-adoption-authority.js.map +1 -1
  57. package/lib/source-approval-authority.js +3 -3
  58. package/lib/source-approval-authority.js.map +1 -1
  59. package/lib/source-build.d.ts +33 -53
  60. package/lib/source-build.d.ts.map +1 -1
  61. package/lib/source-build.js +28 -408
  62. package/lib/source-build.js.map +1 -1
  63. package/lib/source-job-types.d.ts +111 -4
  64. package/lib/source-job-types.d.ts.map +1 -1
  65. package/lib/source-jobs.d.ts +72 -3
  66. package/lib/source-jobs.d.ts.map +1 -1
  67. package/lib/source-jobs.js +314 -43
  68. package/lib/source-jobs.js.map +1 -1
  69. package/lib/source-release-authority.js +2 -2
  70. package/lib/source-release-authority.js.map +1 -1
  71. package/lib/source-revision.d.ts +29 -3
  72. package/lib/source-revision.d.ts.map +1 -1
  73. package/lib/source-revision.js +40 -9
  74. package/lib/source-revision.js.map +1 -1
  75. package/lib/source-workspace.d.ts +11 -4
  76. package/lib/source-workspace.d.ts.map +1 -1
  77. package/lib/source-workspace.js +51 -18
  78. package/lib/source-workspace.js.map +1 -1
  79. package/lib/sqlite.d.ts +6 -7
  80. package/lib/sqlite.d.ts.map +1 -1
  81. package/lib/sqlite.js +255 -17
  82. package/lib/sqlite.js.map +1 -1
  83. package/lib/store.d.ts +143 -4
  84. package/lib/store.d.ts.map +1 -1
  85. package/lib/store.js +739 -66
  86. package/lib/store.js.map +1 -1
  87. package/lib/systemd-host-authority.js +4 -4
  88. package/lib/systemd-host-authority.js.map +1 -1
  89. package/lib/task-observation-authority.js +2 -2
  90. package/lib/task-observation-authority.js.map +1 -1
  91. package/lib/version.d.ts +1 -1
  92. package/lib/version.js +1 -1
  93. package/lib/work-item-creation-contract.d.ts +3 -1
  94. package/lib/work-item-creation-contract.d.ts.map +1 -1
  95. package/lib/work-item-creation-contract.js +10 -1
  96. package/lib/work-item-creation-contract.js.map +1 -1
  97. 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'] : [])], 'source job intent');
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' ? revisionBindingFromStored(intent['revision']) : undefined;
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
- try {
1307
- validateSourceBuildConfig(intent['build']);
1308
- }
1309
- catch {
1310
- throw new ControlPlaneStoreError('invalid-input', 'source job build configuration is invalid');
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 ? openControlPlaneDatabaseReadOnly(options.path) : openControlPlaneDatabase(options.path);
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 && (this.getOwnerTaskFailureReference(intent.gapId) === undefined || now >= intent.revision.grant.expiresAt))
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' AND ((p.mode = 'modify' AND (p.status = 'pending-approval' OR (? = 1 AND p.status IN ('approved', 'ready-for-human-review'))
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 revision = input.revision === undefined ? undefined : revisionBindingFromStored(input.revision);
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 && (this.getOwnerTaskFailureReference(input.gapId) === undefined || now >= revision.growthRun.expiresAt)) {
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 sourceFromRow(row);
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
- throw new ControlPlaneStoreError('invalid-state', 'prepared revision requires an owner task failure gap');
2886
- this.#assertOwnerTaskFailureGapAdmission(plan.gapId);
2887
- return this.getRetainedPreparedRevision(planId).job;
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 { plan, job, reference: owner };
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 pinned = job.intent.creation?.growthRun?.creationAcceptance;
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 (!reference || controlPlaneDigest(plan.sourceRevision?.parent) !== controlPlaneDigest(certificate.parent) || !plan.sourceCheck || !plan.preparedEvidence || !plan.sourceRevision?.growthRun
3065
- || !revisionVerificationMatchesPreparedEvidence(certificate, plan.preparedEvidence)
3066
- || !job.intent.revision?.growthRun || plan.digest !== certificate.plan.digest || plan.name !== certificate.plan.name
3067
- || plan.sourceCheck.treeDigest !== certificate.plan.sourceTreeDigest
3068
- || plan.sourceCheck.patchDigest !== certificate.plan.sourcePatchDigest
3069
- || plan.preparedEvidence.pack.sha256 !== certificate.plan.artifactSha256
3070
- || plan.preparedEvidence.pack.sizeBytes !== certificate.plan.artifactBytes
3071
- || plan.generatorDigest !== certificate.plan.generatorDigest
3072
- || controlPlaneDigest(reference) !== certificate.source.referenceDigest
3073
- || controlPlaneDigest(reference.owner) !== certificate.source.ownerDigest
3074
- || sourceGrowthRunDigest(plan.sourceRevision.growthRun) !== certificate.source.growthRunDigest
3075
- || sourceGrowthRunDigest(job.intent.revision.growthRun) !== certificate.source.growthRunDigest
3076
- || controlPlaneDigest(plan.sourceRevision.growthRun.model) !== controlPlaneDigest(certificate.model)
3077
- || certificate.expiresAt > Math.min(plan.expiresAt, plan.sourceRevision.grant.expiresAt, plan.sourceRevision.growthRun.expiresAt, certificate.authority.expiresAt)) {
3078
- throw new ControlPlaneStoreError('invalid-input', 'revision verification certificate does not match exact prepared source');
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.revisionRegressionAcceptance
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
- || certificate.source.referenceDigest !== controlPlaneDigest(reference)
3218
- || certificate.source.ownerDigest !== controlPlaneDigest(reference.owner)
3219
- || certificate.source.growthRunDigest !== sourceGrowthRunDigest(run)
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(sourceFromRow);
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
- const snapshot = sourceSnapshotFromStored(receipt.result);
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,