@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.
Files changed (86) hide show
  1. package/README.md +54 -20
  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-feedback.d.ts +16 -1
  6. package/lib/creation-capability-feedback.d.ts.map +1 -1
  7. package/lib/creation-capability-feedback.js +82 -5
  8. package/lib/creation-capability-feedback.js.map +1 -1
  9. package/lib/creation-capability-journal.d.ts.map +1 -1
  10. package/lib/creation-capability-journal.js +2 -0
  11. package/lib/creation-capability-journal.js.map +1 -1
  12. package/lib/creation-capability-runtime.d.ts.map +1 -1
  13. package/lib/creation-capability-runtime.js +2 -0
  14. package/lib/creation-capability-runtime.js.map +1 -1
  15. package/lib/creation-capability-types.d.ts +1 -0
  16. package/lib/creation-capability-types.d.ts.map +1 -1
  17. package/lib/linux-source-build.d.ts +13 -2
  18. package/lib/linux-source-build.d.ts.map +1 -1
  19. package/lib/linux-source-build.js.map +1 -1
  20. package/lib/live-qualification-authority.js +2 -2
  21. package/lib/live-qualification-authority.js.map +1 -1
  22. package/lib/owner-instruction-drain.d.ts +60 -0
  23. package/lib/owner-instruction-drain.d.ts.map +1 -0
  24. package/lib/owner-instruction-drain.js +101 -0
  25. package/lib/owner-instruction-drain.js.map +1 -0
  26. package/lib/revision-capability-feedback.d.ts +165 -0
  27. package/lib/revision-capability-feedback.d.ts.map +1 -0
  28. package/lib/revision-capability-feedback.js +316 -0
  29. package/lib/revision-capability-feedback.js.map +1 -0
  30. package/lib/revision-capability-journal.d.ts +23 -0
  31. package/lib/revision-capability-journal.d.ts.map +1 -1
  32. package/lib/revision-capability-journal.js +124 -15
  33. package/lib/revision-capability-journal.js.map +1 -1
  34. package/lib/revision-capability-runtime.d.ts +19 -1
  35. package/lib/revision-capability-runtime.d.ts.map +1 -1
  36. package/lib/revision-capability-runtime.js +80 -7
  37. package/lib/revision-capability-runtime.js.map +1 -1
  38. package/lib/revision-capability-types.d.ts +50 -4
  39. package/lib/revision-capability-types.d.ts.map +1 -1
  40. package/lib/service.d.ts +174 -3
  41. package/lib/service.d.ts.map +1 -1
  42. package/lib/service.js +732 -68
  43. package/lib/service.js.map +1 -1
  44. package/lib/source-adoption-authority.js +2 -2
  45. package/lib/source-adoption-authority.js.map +1 -1
  46. package/lib/source-approval-authority.js +3 -3
  47. package/lib/source-approval-authority.js.map +1 -1
  48. package/lib/source-build.d.ts +33 -53
  49. package/lib/source-build.d.ts.map +1 -1
  50. package/lib/source-build.js +28 -408
  51. package/lib/source-build.js.map +1 -1
  52. package/lib/source-job-types.d.ts +111 -4
  53. package/lib/source-job-types.d.ts.map +1 -1
  54. package/lib/source-jobs.d.ts +68 -4
  55. package/lib/source-jobs.d.ts.map +1 -1
  56. package/lib/source-jobs.js +296 -46
  57. package/lib/source-jobs.js.map +1 -1
  58. package/lib/source-release-authority.js +2 -2
  59. package/lib/source-release-authority.js.map +1 -1
  60. package/lib/source-revision.d.ts +29 -3
  61. package/lib/source-revision.d.ts.map +1 -1
  62. package/lib/source-revision.js +40 -9
  63. package/lib/source-revision.js.map +1 -1
  64. package/lib/source-workspace.d.ts +11 -4
  65. package/lib/source-workspace.d.ts.map +1 -1
  66. package/lib/source-workspace.js +51 -17
  67. package/lib/source-workspace.js.map +1 -1
  68. package/lib/sqlite.d.ts +6 -7
  69. package/lib/sqlite.d.ts.map +1 -1
  70. package/lib/sqlite.js +255 -17
  71. package/lib/sqlite.js.map +1 -1
  72. package/lib/store.d.ts +142 -3
  73. package/lib/store.d.ts.map +1 -1
  74. package/lib/store.js +732 -63
  75. package/lib/store.js.map +1 -1
  76. package/lib/systemd-host-authority.js +4 -4
  77. package/lib/systemd-host-authority.js.map +1 -1
  78. package/lib/task-observation-authority.js +2 -2
  79. package/lib/task-observation-authority.js.map +1 -1
  80. package/lib/version.d.ts +1 -1
  81. package/lib/version.js +1 -1
  82. package/lib/work-item-creation-contract.d.ts +3 -1
  83. package/lib/work-item-creation-contract.d.ts.map +1 -1
  84. package/lib/work-item-creation-contract.js +10 -1
  85. package/lib/work-item-creation-contract.js.map +1 -1
  86. 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');
@@ -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' 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,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 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 });
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 && (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)) {
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 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;
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
- throw new ControlPlaneStoreError('invalid-state', 'prepared revision requires an owner task failure gap');
2890
- this.#assertOwnerTaskFailureGapAdmission(plan.gapId);
2891
- 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
+ }
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 { plan, job, reference: owner };
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 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;
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 (!reference || controlPlaneDigest(plan.sourceRevision?.parent) !== controlPlaneDigest(certificate.parent) || !plan.sourceCheck || !plan.preparedEvidence || !plan.sourceRevision?.growthRun
3069
- || !revisionVerificationMatchesPreparedEvidence(certificate, plan.preparedEvidence)
3070
- || !job.intent.revision?.growthRun || plan.digest !== certificate.plan.digest || plan.name !== certificate.plan.name
3071
- || plan.sourceCheck.treeDigest !== certificate.plan.sourceTreeDigest
3072
- || plan.sourceCheck.patchDigest !== certificate.plan.sourcePatchDigest
3073
- || plan.preparedEvidence.pack.sha256 !== certificate.plan.artifactSha256
3074
- || plan.preparedEvidence.pack.sizeBytes !== certificate.plan.artifactBytes
3075
- || plan.generatorDigest !== certificate.plan.generatorDigest
3076
- || controlPlaneDigest(reference) !== certificate.source.referenceDigest
3077
- || controlPlaneDigest(reference.owner) !== certificate.source.ownerDigest
3078
- || sourceGrowthRunDigest(plan.sourceRevision.growthRun) !== certificate.source.growthRunDigest
3079
- || sourceGrowthRunDigest(job.intent.revision.growthRun) !== certificate.source.growthRunDigest
3080
- || controlPlaneDigest(plan.sourceRevision.growthRun.model) !== controlPlaneDigest(certificate.model)
3081
- || certificate.expiresAt > Math.min(plan.expiresAt, plan.sourceRevision.grant.expiresAt, plan.sourceRevision.growthRun.expiresAt, certificate.authority.expiresAt)) {
3082
- 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
+ }
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.revisionRegressionAcceptance
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
- || certificate.source.referenceDigest !== controlPlaneDigest(reference)
3222
- || certificate.source.ownerDigest !== controlPlaneDigest(reference.owner)
3223
- || certificate.source.growthRunDigest !== sourceGrowthRunDigest(run)
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(sourceFromRow);
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
- 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);
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,