@dsh-enhanced/plugin-control-plane 0.1.48 → 0.1.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -15
- package/bin/dsh-live-qualification-authority.js +15 -0
- package/bin/dsh-local-release-adapter.js +328 -8
- package/bin/dsh-systemd-host-attestor.js +359 -9
- package/bin/dsh-systemd-host-authority.js +12 -0
- package/lib/adoption-coordinator.d.ts +3 -1
- package/lib/adoption-coordinator.d.ts.map +1 -1
- package/lib/adoption-coordinator.js +24 -9
- package/lib/adoption-coordinator.js.map +1 -1
- package/lib/cli.d.ts.map +1 -1
- package/lib/cli.js +33 -14
- package/lib/cli.js.map +1 -1
- package/lib/creation-capability-feedback.d.ts +23 -0
- package/lib/creation-capability-feedback.d.ts.map +1 -0
- package/lib/creation-capability-feedback.js +132 -0
- package/lib/creation-capability-feedback.js.map +1 -0
- package/lib/creation-capability-journal.d.ts +66 -0
- package/lib/creation-capability-journal.d.ts.map +1 -0
- package/lib/creation-capability-journal.js +751 -0
- package/lib/creation-capability-journal.js.map +1 -0
- package/lib/creation-capability-runtime.d.ts +63 -0
- package/lib/creation-capability-runtime.d.ts.map +1 -0
- package/lib/creation-capability-runtime.js +564 -0
- package/lib/creation-capability-runtime.js.map +1 -0
- package/lib/creation-capability-source.d.ts +37 -0
- package/lib/creation-capability-source.d.ts.map +1 -0
- package/lib/creation-capability-source.js +328 -0
- package/lib/creation-capability-source.js.map +1 -0
- package/lib/creation-capability-types.d.ts +252 -0
- package/lib/creation-capability-types.d.ts.map +1 -0
- package/lib/creation-capability-types.js +2 -0
- package/lib/creation-capability-types.js.map +1 -0
- package/lib/deployment-readiness.d.ts +16 -0
- package/lib/deployment-readiness.d.ts.map +1 -1
- package/lib/deployment-readiness.js +48 -1
- package/lib/deployment-readiness.js.map +1 -1
- package/lib/effect-blocked-replay.js +2 -2
- package/lib/effect-blocked-replay.js.map +1 -1
- package/lib/foreground-deployment-runtime.d.ts +3 -0
- package/lib/foreground-deployment-runtime.d.ts.map +1 -1
- package/lib/foreground-deployment-runtime.js +55 -13
- package/lib/foreground-deployment-runtime.js.map +1 -1
- package/lib/foreground-deployment.d.ts.map +1 -1
- package/lib/foreground-deployment.js +5 -0
- package/lib/foreground-deployment.js.map +1 -1
- package/lib/host-attestor.d.ts +2 -0
- package/lib/host-attestor.d.ts.map +1 -1
- package/lib/host-attestor.js +8 -1
- package/lib/host-attestor.js.map +1 -1
- package/lib/host-inputs.d.ts +13 -0
- package/lib/host-inputs.d.ts.map +1 -0
- package/lib/host-inputs.js +162 -0
- package/lib/host-inputs.js.map +1 -0
- package/lib/host-maintenance.d.ts +61 -0
- package/lib/host-maintenance.d.ts.map +1 -0
- package/lib/host-maintenance.js +158 -0
- package/lib/host-maintenance.js.map +1 -0
- package/lib/host-readiness.d.ts +5 -0
- package/lib/host-readiness.d.ts.map +1 -0
- package/lib/host-readiness.js +22 -0
- package/lib/host-readiness.js.map +1 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +7 -0
- package/lib/index.js.map +1 -1
- package/lib/live-qualification-authority.d.ts +32 -0
- package/lib/live-qualification-authority.d.ts.map +1 -0
- package/lib/live-qualification-authority.js +280 -0
- package/lib/live-qualification-authority.js.map +1 -0
- package/lib/live-qualification-client.d.ts +4 -0
- package/lib/live-qualification-client.d.ts.map +1 -0
- package/lib/live-qualification-client.js +22 -0
- package/lib/live-qualification-client.js.map +1 -0
- package/lib/live-qualification-runtime.d.ts +78 -0
- package/lib/live-qualification-runtime.d.ts.map +1 -0
- package/lib/live-qualification-runtime.js +367 -0
- package/lib/live-qualification-runtime.js.map +1 -0
- package/lib/live-qualification.d.ts +73 -0
- package/lib/live-qualification.d.ts.map +1 -0
- package/lib/live-qualification.js +164 -0
- package/lib/live-qualification.js.map +1 -0
- package/lib/owner-task-gaps.d.ts +2 -0
- package/lib/owner-task-gaps.d.ts.map +1 -1
- package/lib/owner-task-gaps.js +9 -1
- package/lib/owner-task-gaps.js.map +1 -1
- package/lib/runtime-epoch.d.ts +69 -0
- package/lib/runtime-epoch.d.ts.map +1 -0
- package/lib/runtime-epoch.js +139 -0
- package/lib/runtime-epoch.js.map +1 -0
- package/lib/runtime-observer.d.ts.map +1 -1
- package/lib/runtime-observer.js +42 -6
- package/lib/runtime-observer.js.map +1 -1
- package/lib/service.d.ts +247 -4
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +1332 -27
- package/lib/service.js.map +1 -1
- package/lib/source-adoption-authority.d.ts +3 -0
- package/lib/source-adoption-authority.d.ts.map +1 -1
- package/lib/source-adoption-authority.js +13 -3
- package/lib/source-adoption-authority.js.map +1 -1
- package/lib/source-adoption-runner.d.ts +3 -0
- package/lib/source-adoption-runner.d.ts.map +1 -1
- package/lib/source-adoption-runner.js +16 -4
- package/lib/source-adoption-runner.js.map +1 -1
- package/lib/source-baseline.d.ts +37 -0
- package/lib/source-baseline.d.ts.map +1 -0
- package/lib/source-baseline.js +250 -0
- package/lib/source-baseline.js.map +1 -0
- package/lib/source-build.d.ts +17 -1
- package/lib/source-build.d.ts.map +1 -1
- package/lib/source-build.js +501 -7
- package/lib/source-build.js.map +1 -1
- package/lib/source-context.d.ts +18 -0
- package/lib/source-context.d.ts.map +1 -1
- package/lib/source-context.js +85 -11
- package/lib/source-context.js.map +1 -1
- package/lib/source-creation.d.ts +106 -0
- package/lib/source-creation.d.ts.map +1 -0
- package/lib/source-creation.js +664 -0
- package/lib/source-creation.js.map +1 -0
- package/lib/source-job-types.d.ts +19 -0
- package/lib/source-job-types.d.ts.map +1 -1
- package/lib/source-jobs.d.ts +30 -3
- package/lib/source-jobs.d.ts.map +1 -1
- package/lib/source-jobs.js +382 -40
- package/lib/source-jobs.js.map +1 -1
- package/lib/source-maintenance.d.ts +66 -0
- package/lib/source-maintenance.d.ts.map +1 -0
- package/lib/source-maintenance.js +177 -0
- package/lib/source-maintenance.js.map +1 -0
- package/lib/source-revision.d.ts +57 -0
- package/lib/source-revision.d.ts.map +1 -0
- package/lib/source-revision.js +175 -0
- package/lib/source-revision.js.map +1 -0
- package/lib/source-workspace.d.ts +19 -14
- package/lib/source-workspace.d.ts.map +1 -1
- package/lib/source-workspace.js +90 -17
- package/lib/source-workspace.js.map +1 -1
- package/lib/sqlite.d.ts +13 -3
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +362 -1
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +258 -3
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +2211 -95
- package/lib/store.js.map +1 -1
- package/lib/systemd-host-authority.d.ts +68 -0
- package/lib/systemd-host-authority.d.ts.map +1 -0
- package/lib/systemd-host-authority.js +424 -0
- package/lib/systemd-host-authority.js.map +1 -0
- package/lib/task-observation-evidence.d.ts +32 -0
- package/lib/task-observation-evidence.d.ts.map +1 -0
- package/lib/task-observation-evidence.js +62 -0
- package/lib/task-observation-evidence.js.map +1 -0
- package/lib/task-observation-runtime.d.ts +2 -3
- package/lib/task-observation-runtime.d.ts.map +1 -1
- package/lib/task-observation-runtime.js +7 -39
- package/lib/task-observation-runtime.js.map +1 -1
- package/lib/task-observation-store.js +1 -1
- package/lib/trust.d.ts.map +1 -1
- package/lib/trust.js +0 -0
- package/lib/trust.js.map +1 -1
- package/lib/types.d.ts +43 -6
- package/lib/types.d.ts.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +21 -14
package/lib/source-jobs.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
3
|
import { realpath } from 'node:fs/promises';
|
|
4
|
+
import { SourceGrowthRunUnavailableError, isSourceOwnerContinuation, validateSourceGrowthRunBinding } from '@dsh-enhanced/assistant-growth-contract';
|
|
5
|
+
import { automationDefinitionDigest } from '@dsh-enhanced/assistant-automations';
|
|
4
6
|
import { controlPlaneDigest, expectedSourceRelease } from './store.js';
|
|
5
7
|
import { awaitSourceSignal, inspectSourceContext } from './source-context.js';
|
|
6
8
|
import { assertPluginModificationAllowed, removeSourceJobWorktree, validateScopedPluginFiles } from './source-workspace.js';
|
|
7
9
|
import { assertManagedVersionPaths } from './source-versioning.js';
|
|
10
|
+
import { resolveSourceBaseline, validateSourceBaselineConfig } from './source-baseline.js';
|
|
8
11
|
import { removeSourceJobContainer } from './source-build.js';
|
|
12
|
+
import { inspectSourceRevisionContext, validateSourceRevisionGrant } from './source-revision.js';
|
|
13
|
+
import { inspectSourceCreationContext, validateSourceCreationFiles, validateSourceCreationGrant } from './source-creation.js';
|
|
9
14
|
import { inheritedEnvironment } from './trust.js';
|
|
10
15
|
export const SOURCE_JOB_OWNER = 'plugin-control-plane-source';
|
|
11
16
|
const EXECUTOR = 'plugin-control-plane-source-check';
|
|
@@ -13,8 +18,11 @@ const CATALOG = controlPlaneDigest({ executor: EXECUTOR, version: 1, operation:
|
|
|
13
18
|
const CONTINUATION_EXECUTOR = 'plugin-control-plane-source-continuations-v1';
|
|
14
19
|
const CONTINUATION_CATALOG = controlPlaneDigest({ executor: CONTINUATION_EXECUTOR, version: 1, operation: 'continue-prepared-source-plan' });
|
|
15
20
|
const CONTINUATION_AUTOMATION = 'source-job-prepared-continuations';
|
|
21
|
+
export { SourceGrowthRunUnavailableError } from '@dsh-enhanced/assistant-growth-contract';
|
|
22
|
+
class SourceGrowthNativeMismatchError extends Error {
|
|
23
|
+
}
|
|
16
24
|
export function validateSourceJobsConfig(value, build) {
|
|
17
|
-
const allowed = new Set(['authorityId', 'expiresAt', 'maxSubmissions', 'repository', 'ownerRouteId', 'principalId', 'workspace', 'preset', 'budgetId', 'budgetAmount']);
|
|
25
|
+
const allowed = new Set(['authorityId', 'expiresAt', 'maxSubmissions', 'repository', 'baseline', 'creation', 'revision', 'ownerRouteId', 'principalId', 'workspace', 'preset', 'budgetId', 'budgetAmount']);
|
|
18
26
|
if (value === null || typeof value !== 'object' || Array.isArray(value) || build === undefined
|
|
19
27
|
|| Object.keys(value).some(key => !allowed.has(key))
|
|
20
28
|
|| ![value.authorityId, value.ownerRouteId, value.principalId, value.repository, value.workspace, value.preset].every(text => typeof text === 'string' && text !== '' && text.normalize('NFC').trim() === text && text.length <= 4096 && !/[\p{Cc}]/u.test(text))
|
|
@@ -26,9 +34,16 @@ export function validateSourceJobsConfig(value, build) {
|
|
|
26
34
|
|| !Number.isSafeInteger(value.budgetAmount) || value.budgetAmount < 1) {
|
|
27
35
|
throw new Error('plugin-control-plane: invalid sourceJobs authority configuration');
|
|
28
36
|
}
|
|
37
|
+
if (value.baseline !== undefined)
|
|
38
|
+
validateSourceBaselineConfig(value.baseline);
|
|
39
|
+
if (value.creation !== undefined)
|
|
40
|
+
validateSourceCreationGrant(value.creation);
|
|
41
|
+
if (value.revision !== undefined)
|
|
42
|
+
validateSourceRevisionGrant(value.revision);
|
|
29
43
|
}
|
|
30
44
|
function projection(job) {
|
|
31
45
|
return { id: job.id, name: job.intent.name, gapId: job.intent.gapId, baseCommit: job.intent.baseCommit,
|
|
46
|
+
...(job.intent.mode === undefined ? {} : { mode: job.intent.mode }),
|
|
32
47
|
status: job.status, createdAt: job.createdAt, expiresAt: job.expiresAt,
|
|
33
48
|
...(job.planId === undefined ? {} : { planId: job.planId }), ...(job.failureCode === undefined ? {} : { failureCode: job.failureCode }) };
|
|
34
49
|
}
|
|
@@ -37,6 +52,8 @@ export class SourceJobRuntime {
|
|
|
37
52
|
options;
|
|
38
53
|
abort = new AbortController();
|
|
39
54
|
flights = new Set();
|
|
55
|
+
growthFlights = new Set();
|
|
56
|
+
growthControllers = new Set();
|
|
40
57
|
authorityDigest;
|
|
41
58
|
unregister;
|
|
42
59
|
unregisterContinuation;
|
|
@@ -59,7 +76,15 @@ export class SourceJobRuntime {
|
|
|
59
76
|
const executor = {
|
|
60
77
|
descriptor: { executorId: EXECUTOR, contractVersion: 1, catalogDigest: CATALOG },
|
|
61
78
|
accepts: spec => spec.executorId === EXECUTOR && spec.executorContractVersion === 1 && spec.catalogDigest === CATALOG && spec.runbookId === EXECUTOR && spec.runbookVersion === 1,
|
|
62
|
-
execute: input =>
|
|
79
|
+
execute: input => {
|
|
80
|
+
const job = this.options.store.getSourceJobByAutomation(input.automationId);
|
|
81
|
+
const flight = this.track(this.execute(input));
|
|
82
|
+
if ((job?.intent.creation?.growthRun ?? job?.intent.revision?.growthRun) !== undefined) {
|
|
83
|
+
this.growthFlights.add(flight);
|
|
84
|
+
void flight.then(() => this.growthFlights.delete(flight), () => this.growthFlights.delete(flight));
|
|
85
|
+
}
|
|
86
|
+
return flight;
|
|
87
|
+
},
|
|
63
88
|
};
|
|
64
89
|
try {
|
|
65
90
|
this.unregister = this.options.ports.automations.registerHostExecutor(executor);
|
|
@@ -90,23 +115,57 @@ export class SourceJobRuntime {
|
|
|
90
115
|
catch { /* original error wins */ }
|
|
91
116
|
throw error;
|
|
92
117
|
}
|
|
118
|
+
this.reconcileQueued();
|
|
119
|
+
}
|
|
120
|
+
/** Nudge the existing native one-shot registrations after a Host provider appears. */
|
|
121
|
+
reconcileQueued() {
|
|
122
|
+
if (!this.active)
|
|
123
|
+
return;
|
|
93
124
|
// Reconcile only queued intents. A previous claim is unknown and never replayed.
|
|
94
125
|
for (let job of this.options.store.listSourceJobs(100)) {
|
|
95
126
|
if (job.status !== 'queued')
|
|
96
127
|
continue;
|
|
97
128
|
try {
|
|
98
129
|
job = this.refreshQueued(job);
|
|
99
|
-
if (job.status === 'queued')
|
|
100
|
-
this.assertOwner(job);
|
|
130
|
+
if (job.status === 'queued')
|
|
101
131
|
this.schedule(job);
|
|
102
|
-
}
|
|
103
132
|
}
|
|
104
|
-
catch {
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error instanceof SourceGrowthRunUnavailableError)
|
|
135
|
+
continue;
|
|
105
136
|
const current = this.options.store.getSourceJob(job.id);
|
|
137
|
+
// A persisted rearm is an in-progress cross-ledger handoff. Retain it
|
|
138
|
+
// for exact native receipt replay after a crash or transient fault.
|
|
139
|
+
if (current?.previousDefinitionHash !== undefined && !(error instanceof SourceGrowthNativeMismatchError))
|
|
140
|
+
continue;
|
|
106
141
|
if (current?.status === 'queued')
|
|
107
142
|
this.options.store.settleSourceJob({ id: current.id, revision: current.revision, status: 'failed', failureCode: 'source-job-reconcile-rejected' });
|
|
108
143
|
}
|
|
109
144
|
}
|
|
145
|
+
this.reconcileContinuations();
|
|
146
|
+
}
|
|
147
|
+
/** Provider removal revokes in-flight authority without replaying a claimed job. */
|
|
148
|
+
growthProviderDisposed() {
|
|
149
|
+
for (const controller of this.growthControllers)
|
|
150
|
+
controller.abort(new Error('source growth run producer disposed'));
|
|
151
|
+
this.options.store.interruptGrowthSourceJobs();
|
|
152
|
+
if (!this.active)
|
|
153
|
+
return Promise.allSettled(this.growthFlights).then(() => undefined);
|
|
154
|
+
for (const job of this.options.store.listSourceJobs(100)) {
|
|
155
|
+
if (job.status !== 'queued' || (job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) === undefined || job.definitionHash === undefined)
|
|
156
|
+
continue;
|
|
157
|
+
try {
|
|
158
|
+
const health = this.options.ports.automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
159
|
+
if (health.definitionHash !== job.definitionHash)
|
|
160
|
+
continue;
|
|
161
|
+
if (health.automationStatus === 'active')
|
|
162
|
+
this.options.ports.automations.pauseSystemOwned({ owner: SOURCE_JOB_OWNER,
|
|
163
|
+
operationId: `${job.id}:growth-provider-pause:v${health.definitionVersion}`, automationId: job.automationId,
|
|
164
|
+
definitionHash: job.definitionHash, expectedVersion: health.definitionVersion });
|
|
165
|
+
}
|
|
166
|
+
catch { /* claim guard remains authoritative */ }
|
|
167
|
+
}
|
|
168
|
+
return Promise.allSettled(this.growthFlights).then(() => undefined);
|
|
110
169
|
}
|
|
111
170
|
async continuePrepared(job, signal) {
|
|
112
171
|
if (!job.planId || !this.options.store.getOwnerTaskFailureReference(job.intent.gapId))
|
|
@@ -130,6 +189,8 @@ export class SourceJobRuntime {
|
|
|
130
189
|
throw new Error('source job continuation changed');
|
|
131
190
|
job = current;
|
|
132
191
|
const initial = this.options.store.getSourcePlan(planId);
|
|
192
|
+
if (initial.mode !== 'modify' && initial.mode !== 'prepared-create' && initial.mode !== 'prepared-revise')
|
|
193
|
+
return;
|
|
133
194
|
// An ordinary grant or plan must not be advanced after expiry. Adoption has
|
|
134
195
|
// its own durable post-release path and is deliberately retained below.
|
|
135
196
|
if (initial.expiresAt <= Date.now() && initial.status !== 'release-complete')
|
|
@@ -142,6 +203,35 @@ export class SourceJobRuntime {
|
|
|
142
203
|
signal.throwIfAborted();
|
|
143
204
|
this.assertOwner(job);
|
|
144
205
|
};
|
|
206
|
+
if (initial.mode === 'prepared-revise') {
|
|
207
|
+
if (job.intent.mode !== 'revise-created' || !job.intent.revision?.growthRun.revisionAcceptance || !initial.sourceRevision?.growthRun.revisionAcceptance)
|
|
208
|
+
return;
|
|
209
|
+
await assertCurrent();
|
|
210
|
+
if (this.options.store.getRevisionVerificationStatus(planId) === undefined && this.options.verifyPreparedRevision)
|
|
211
|
+
await this.options.verifyPreparedRevision(job, signal);
|
|
212
|
+
if (this.options.store.getRevisionVerificationStatus(planId) === 'verified'
|
|
213
|
+
&& this.options.store.getRevisionRegressionStatus(planId) === undefined
|
|
214
|
+
&& job.intent.revision.growthRun.revisionRegressionAcceptance && this.options.verifyPreparedRevisionRegression) {
|
|
215
|
+
await assertCurrent();
|
|
216
|
+
await this.options.verifyPreparedRevisionRegression(job, signal);
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (initial.mode === 'prepared-create') {
|
|
221
|
+
if (job.intent.mode !== 'create'
|
|
222
|
+
|| !job.intent.creation?.growthRun?.creationAcceptance || !initial.creation?.growthRun?.creationAcceptance)
|
|
223
|
+
return;
|
|
224
|
+
await assertCurrent();
|
|
225
|
+
if (this.options.store.getCreationVerificationStatus(planId) === undefined && this.options.verifyPreparedCreation) {
|
|
226
|
+
await this.options.verifyPreparedCreation(job, signal);
|
|
227
|
+
}
|
|
228
|
+
if (this.options.store.getCreationVerificationStatus(planId) === 'verified'
|
|
229
|
+
&& this.options.creationAdoptionEligible?.(planId) && this.options.adoptVerifiedCreation) {
|
|
230
|
+
await assertCurrent();
|
|
231
|
+
await this.options.adoptVerifiedCreation(job, signal);
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
145
235
|
if (this.options.adoptReleased && this.options.store.getSourcePlan(planId).status === 'release-complete') {
|
|
146
236
|
await this.options.adoptReleased(job, signal, assertCurrent);
|
|
147
237
|
return;
|
|
@@ -220,15 +310,53 @@ export class SourceJobRuntime {
|
|
|
220
310
|
return this.options.withGapSourceFence(gapId, owner, callback);
|
|
221
311
|
}
|
|
222
312
|
assertOwner(job) {
|
|
223
|
-
|
|
224
|
-
|
|
313
|
+
const current = this.receipt();
|
|
314
|
+
const taskBacked = this.options.store.getOwnerTaskFailureReference(job.intent.gapId) !== undefined;
|
|
315
|
+
if (!this.available() || job.expiresAt <= Date.now() || (job.intent.mode === 'create' && (!this.options.config.creation || !job.intent.creation
|
|
316
|
+
|| job.intent.creation.grant.expiresAt <= Date.now() || controlPlaneDigest(job.intent.creation.grant) !== controlPlaneDigest(this.options.config.creation)))
|
|
317
|
+
|| (job.intent.mode === 'revise-created' && (!this.options.config.revision || !job.intent.revision
|
|
318
|
+
|| job.intent.revision.grant.expiresAt <= Date.now() || controlPlaneDigest(job.intent.revision.grant) !== controlPlaneDigest(this.options.config.revision)))
|
|
319
|
+
|| job.intent.authority.digest !== this.authorityDigest
|
|
320
|
+
|| job.intent.authority.id !== this.options.config.authorityId
|
|
321
|
+
|| controlPlaneDigest(job.intent.owner) !== job.intent.ownerDigest
|
|
322
|
+
|| (taskBacked ? !isSourceOwnerContinuation(current, job.intent.owner)
|
|
323
|
+
: controlPlaneDigest(current) !== job.intent.ownerDigest))
|
|
225
324
|
throw new Error('source job authority changed or expired');
|
|
226
|
-
|
|
325
|
+
if (job.intent.revision) {
|
|
326
|
+
const parent = this.options.inspectRevisionParent?.(job.intent.revision.parent.planId);
|
|
327
|
+
if (!parent || parent.name !== job.intent.name || controlPlaneDigest(parent.parent) !== controlPlaneDigest(job.intent.revision.parent))
|
|
328
|
+
throw new Error('source revision parent changed');
|
|
329
|
+
}
|
|
330
|
+
this.withGapSource(job.intent.gapId, job.intent.owner, () => this.assertGrowthRun((job.intent.creation?.growthRun ?? job.intent.revision?.growthRun), job.intent.mode, job.intent.gapId, job.intent.owner, false));
|
|
331
|
+
}
|
|
332
|
+
assertGrowthRun(growthRun, mode, gapId, owner, generation) {
|
|
333
|
+
const taskBacked = this.options.store.getOwnerTaskFailureReference(gapId) !== undefined;
|
|
334
|
+
if ((mode !== 'create' && mode !== 'revise-created') || !taskBacked) {
|
|
335
|
+
if (growthRun !== undefined)
|
|
336
|
+
throw new Error('source growth run is outside a task-backed creation');
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (growthRun === undefined)
|
|
340
|
+
throw new Error('task-backed creation requires a frozen source growth run');
|
|
341
|
+
validateSourceGrowthRunBinding(growthRun);
|
|
342
|
+
if (!this.options.assertGrowthRun)
|
|
343
|
+
throw new SourceGrowthRunUnavailableError('source growth run producer unavailable');
|
|
344
|
+
this.options.assertGrowthRun(growthRun, gapId, owner, generation);
|
|
227
345
|
}
|
|
228
346
|
enqueue(input) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
347
|
+
if (input.mode !== 'create' && input.mode !== 'revise-created' && input.growthRun !== undefined)
|
|
348
|
+
throw new Error('modify source job cannot carry a growth run');
|
|
349
|
+
if (input.mode === 'create' || input.mode === 'revise-created')
|
|
350
|
+
validateSourceCreationFiles(input.files);
|
|
351
|
+
else {
|
|
352
|
+
if (input.mode !== undefined)
|
|
353
|
+
throw new Error('invalid source job mode');
|
|
354
|
+
validateScopedPluginFiles(input.files);
|
|
355
|
+
if (this.options.build.versioning === 'patch')
|
|
356
|
+
assertManagedVersionPaths(input.files);
|
|
357
|
+
}
|
|
358
|
+
if ((input.mode === 'revise-created') !== (typeof input.parentPlanId === 'string' && input.parentPlanId.length > 0))
|
|
359
|
+
throw new Error('source revision parent is missing or outside revision mode');
|
|
232
360
|
return this.track(this.enqueueOwned({ ...input, files: structuredClone(input.files), owner: structuredClone(input.owner) }));
|
|
233
361
|
}
|
|
234
362
|
async enqueueOwned(input) {
|
|
@@ -236,17 +364,35 @@ export class SourceJobRuntime {
|
|
|
236
364
|
const assertCurrent = async () => { signal.throwIfAborted(); await awaitSourceSignal(signal, input.assertCurrent); this.withGapSource(input.gapId, this.assertCaller(input.owner), () => { }); if (!this.available())
|
|
237
365
|
throw new Error('source job authority unavailable'); signal.throwIfAborted(); };
|
|
238
366
|
await assertCurrent();
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
367
|
+
if (input.mode === 'create' || input.mode === 'revise-created')
|
|
368
|
+
validateSourceCreationFiles(input.files);
|
|
369
|
+
else {
|
|
370
|
+
validateScopedPluginFiles(input.files);
|
|
371
|
+
if (this.options.build.versioning === 'patch')
|
|
372
|
+
assertManagedVersionPaths(input.files);
|
|
373
|
+
assertPluginModificationAllowed(input.name);
|
|
374
|
+
}
|
|
243
375
|
if (typeof input.idempotencyKey !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u.test(input.idempotencyKey)
|
|
244
376
|
|| !Number.isSafeInteger(input.ttlMs) || input.ttlMs < 900_000 || input.ttlMs > 86_400_000)
|
|
245
377
|
throw new Error('invalid source job request');
|
|
246
378
|
const config = this.options.config;
|
|
379
|
+
if (input.mode === 'create' && (config.creation === undefined || config.creation.expiresAt <= Date.now()))
|
|
380
|
+
throw new Error('source creation grant unavailable or expired');
|
|
381
|
+
if (input.mode === 'revise-created' && (config.revision === undefined || config.revision.expiresAt <= Date.now()))
|
|
382
|
+
throw new Error('source revision grant unavailable or expired');
|
|
247
383
|
if (input.repository !== config.repository || await realpath(input.repository) !== config.repository)
|
|
248
384
|
throw new Error('source job repository mismatch');
|
|
249
|
-
const
|
|
385
|
+
const currentOwner = this.assertCaller(input.owner);
|
|
386
|
+
const reference = this.options.store.getOwnerTaskFailureReference(input.gapId);
|
|
387
|
+
if (input.mode === 'revise-created' && !reference)
|
|
388
|
+
throw new Error('revision requires an authenticated owner task failure');
|
|
389
|
+
const owner = reference?.owner ?? currentOwner;
|
|
390
|
+
if (reference && !isSourceOwnerContinuation(currentOwner, owner))
|
|
391
|
+
throw new Error('source job task owner changed');
|
|
392
|
+
this.withGapSource(input.gapId, currentOwner, () => this.assertGrowthRun(input.growthRun, input.mode, input.gapId, owner, true));
|
|
393
|
+
const parent = input.mode === 'revise-created' ? this.options.inspectRevisionParent?.(input.parentPlanId) : undefined;
|
|
394
|
+
if (input.mode === 'revise-created' && (!parent || parent.name !== input.name))
|
|
395
|
+
throw new Error('adopted revision parent unavailable or mismatched');
|
|
250
396
|
const trust = await awaitSourceSignal(signal, this.options.trust);
|
|
251
397
|
for (const outstanding of this.options.store.listSourceJobs(1))
|
|
252
398
|
this.refreshQueued(outstanding);
|
|
@@ -256,6 +402,12 @@ export class SourceJobRuntime {
|
|
|
256
402
|
if (prior !== undefined) {
|
|
257
403
|
const frozen = prior.intent;
|
|
258
404
|
if (frozen.authority.digest !== this.authorityDigest || frozen.ownerDigest !== controlPlaneDigest(owner) || frozen.trustDigest !== controlPlaneDigest(trust)
|
|
405
|
+
|| frozen.mode !== input.mode || (input.mode === 'create' && (controlPlaneDigest(frozen.creation?.grant) !== controlPlaneDigest(config.creation)
|
|
406
|
+
|| (frozen.creation?.growthRun === undefined) !== (input.growthRun === undefined)
|
|
407
|
+
|| (input.growthRun !== undefined && controlPlaneDigest(frozen.creation?.growthRun) !== controlPlaneDigest(input.growthRun))))
|
|
408
|
+
|| (input.mode === 'revise-created' && (controlPlaneDigest(frozen.revision?.grant) !== controlPlaneDigest(config.revision)
|
|
409
|
+
|| controlPlaneDigest(frozen.revision?.parent) !== controlPlaneDigest(parent.parent)
|
|
410
|
+
|| controlPlaneDigest(frozen.revision?.growthRun) !== controlPlaneDigest(input.growthRun)))
|
|
259
411
|
|| frozen.repository !== input.repository || frozen.name !== input.name || frozen.gapId !== input.gapId || frozen.baseCommit !== input.expectedBaseCommit
|
|
260
412
|
|| frozen.ttlMs !== input.ttlMs || controlPlaneDigest(frozen.files) !== controlPlaneDigest(input.files))
|
|
261
413
|
throw new Error('source job idempotency conflict');
|
|
@@ -267,17 +419,36 @@ export class SourceJobRuntime {
|
|
|
267
419
|
const gap = this.options.store.getGap(input.gapId);
|
|
268
420
|
if (gap.status !== 'open' || gap.candidateId !== undefined)
|
|
269
421
|
throw new Error('source job gap is not open');
|
|
270
|
-
const
|
|
422
|
+
const environment = inheritedEnvironment(trust);
|
|
423
|
+
const baselineCommit = config.baseline === undefined ? undefined : await resolveSourceBaseline({ repository: config.repository,
|
|
424
|
+
config: config.baseline, environment, signal, assertCurrent, trust,
|
|
425
|
+
readHistory: () => this.options.store.getSourceBaselineHistory(config.repository),
|
|
426
|
+
readMaintenance: () => this.options.store.getSourceMaintenanceRecords(config.repository) });
|
|
427
|
+
const context = { repository: config.repository, name: input.name, paths: [], baseCommit: input.expectedBaseCommit,
|
|
428
|
+
...(baselineCommit === undefined ? {} : { baselineCommit }), environment, signal, assertCurrent };
|
|
429
|
+
const source = input.mode === 'create'
|
|
430
|
+
? await inspectSourceCreationContext({ ...context, grant: config.creation })
|
|
431
|
+
: input.mode === 'revise-created'
|
|
432
|
+
? await inspectSourceRevisionContext({ ...context, grant: config.revision, parent: parent.parent, parentSource: parent.source, parentCertificate: parent.certificate })
|
|
433
|
+
: await inspectSourceContext(context);
|
|
271
434
|
const intent = {
|
|
272
435
|
authority: { id: config.authorityId, digest: this.authorityDigest, expiresAt: config.expiresAt, maxSubmissions: config.maxSubmissions },
|
|
273
436
|
owner, ownerDigest: controlPlaneDigest(owner), trustDigest: controlPlaneDigest(trust), repository: config.repository, name: input.name,
|
|
437
|
+
...(config.baseline === undefined ? {} : { baseline: structuredClone(config.baseline) }),
|
|
438
|
+
...(input.mode === 'create' ? { mode: 'create', creation: { grant: structuredClone(config.creation), generatorDigest: source.generatorDigest,
|
|
439
|
+
...(input.growthRun === undefined ? {} : { growthRun: structuredClone(input.growthRun) }) } } : {}),
|
|
440
|
+
...(input.mode === 'revise-created' ? { mode: 'revise-created', revision: { grant: structuredClone(config.revision),
|
|
441
|
+
generatorDigest: source.generatorDigest, parent: structuredClone(parent.parent), growthRun: structuredClone(input.growthRun) } } : {}),
|
|
274
442
|
gapId: gap.id, gapRevision: gap.revision, gapDigest: controlPlaneDigest(gap), baseCommit: source.baseCommit,
|
|
275
443
|
files: structuredClone(input.files), ttlMs: input.ttlMs, build: structuredClone(this.options.build),
|
|
276
444
|
worktree: join(this.options.statePath, 'source-worktrees', `worktree-job-${token}`), containerName: `dsh-source-job-${token}`,
|
|
277
445
|
};
|
|
278
446
|
await assertCurrent();
|
|
279
447
|
// Synchronous acceptance and registration: no Agent signal is persisted.
|
|
280
|
-
const job = this.withGapSource(gap.id, owner, () =>
|
|
448
|
+
const job = this.withGapSource(gap.id, owner, () => {
|
|
449
|
+
this.assertGrowthRun(input.growthRun, input.mode, gap.id, owner, true);
|
|
450
|
+
return this.options.store.enqueueSourceJob({ id, automationId: id, idempotencyKey: input.idempotencyKey, intent });
|
|
451
|
+
});
|
|
281
452
|
if (job.status === 'queued')
|
|
282
453
|
this.scheduleOrFail(job);
|
|
283
454
|
return projection(this.options.store.getSourceJob(id));
|
|
@@ -285,8 +456,8 @@ export class SourceJobRuntime {
|
|
|
285
456
|
inspect(input) {
|
|
286
457
|
const owner = this.assertCaller(input.owner);
|
|
287
458
|
const job = this.options.store.getSourceJob(input.id);
|
|
288
|
-
//
|
|
289
|
-
//
|
|
459
|
+
// Inspection and cleanup may cross a session binding. A task-backed job
|
|
460
|
+
// retains its frozen owner digest and may continue under the same owner.
|
|
290
461
|
if (job === undefined || job.intent.owner.authorityId !== owner.authorityId
|
|
291
462
|
|| job.intent.owner.principalId !== owner.principalId || job.intent.owner.principalRecordId !== owner.principalRecordId
|
|
292
463
|
|| job.intent.owner.principalVersion !== owner.principalVersion || job.intent.owner.workspace !== owner.workspace
|
|
@@ -299,15 +470,21 @@ export class SourceJobRuntime {
|
|
|
299
470
|
if (job.status !== 'queued')
|
|
300
471
|
return job;
|
|
301
472
|
let code;
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
473
|
+
let providerUnavailable = false;
|
|
474
|
+
if (job.intent.mode === 'create' && this.options.store.getOwnerTaskFailureReference(job.intent.gapId) !== undefined
|
|
475
|
+
&& (job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) === undefined)
|
|
476
|
+
code = 'source-job-growth-run-missing';
|
|
477
|
+
else {
|
|
478
|
+
try {
|
|
479
|
+
this.withGapSource(job.intent.gapId, job.intent.owner, () => { providerUnavailable = this.growthProducerUnavailable(job); });
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
code = 'source-job-task-source-changed';
|
|
483
|
+
}
|
|
307
484
|
}
|
|
308
485
|
if (job.expiresAt <= Date.now())
|
|
309
486
|
code = 'source-job-expired';
|
|
310
|
-
else if (job.definitionHash !== undefined) {
|
|
487
|
+
else if (code === undefined && job.definitionHash !== undefined && !providerUnavailable && (job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) === undefined) {
|
|
311
488
|
const health = this.options.ports.automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
312
489
|
const run = health.latestTerminalRuns.production;
|
|
313
490
|
if (run !== undefined && run.createdAt >= job.createdAt && run.immutableContext.state === 'verified'
|
|
@@ -319,9 +496,20 @@ export class SourceJobRuntime {
|
|
|
319
496
|
// loses its claim. No resubmission or second native occurrence is created.
|
|
320
497
|
return code === undefined ? job : this.options.store.settleSourceJob({ id: job.id, revision: job.revision, status: 'failed', failureCode: code });
|
|
321
498
|
}
|
|
499
|
+
growthProducerUnavailable(job) {
|
|
500
|
+
try {
|
|
501
|
+
this.assertGrowthRun((job.intent.creation?.growthRun ?? job.intent.revision?.growthRun), job.intent.mode, job.intent.gapId, job.intent.owner, false);
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
if (error instanceof SourceGrowthRunUnavailableError)
|
|
506
|
+
return true;
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
322
510
|
definition(job) {
|
|
323
511
|
const config = this.options.config;
|
|
324
|
-
return { name: `Source check: ${job.intent.name}`, schedule: { kind: 'at', at: new Date(job.createdAt + 1000).toISOString() },
|
|
512
|
+
return { name: `Source check: ${job.intent.name}`, schedule: { kind: 'at', at: new Date(job.dispatchAt ?? job.createdAt + 1000).toISOString() },
|
|
325
513
|
workspace: config.workspace, agentPreset: config.preset, timeoutMs: job.intent.build.timeoutMs + 120_000 + (this.options.advanceReleased ? this.options.releaseTimeoutMs ?? 40_000 : 0) + (this.options.adoptReleased ? this.options.adoptionTimeoutMs ?? 60_000 : 0),
|
|
326
514
|
misfire: { kind: 'latest' }, overlap: 'skip', retrySafety: 'never', maxRetries: 0, principal: config.principalId,
|
|
327
515
|
...(config.budgetId === undefined ? {} : { budgetId: config.budgetId, budgetAmount: config.budgetAmount }),
|
|
@@ -332,26 +520,86 @@ export class SourceJobRuntime {
|
|
|
332
520
|
schedule(job) {
|
|
333
521
|
this.assertOwner(job);
|
|
334
522
|
const automations = this.options.ports.automations;
|
|
523
|
+
if (job.definitionHash !== undefined && (job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) !== undefined && job.occurrenceId === undefined) {
|
|
524
|
+
const health = automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
525
|
+
if (health.definitionHash !== job.definitionHash)
|
|
526
|
+
throw new Error('source job native definition changed before rearm');
|
|
527
|
+
const dueAt = job.dispatchAt ?? job.createdAt + 1000;
|
|
528
|
+
const terminal = health.latestTerminalRuns.production;
|
|
529
|
+
if ((health.automationStatus === 'paused' && dueAt <= Date.now())
|
|
530
|
+
|| (terminal !== undefined && terminal.immutableContext.state === 'verified'
|
|
531
|
+
&& terminal.immutableContext.definitionHash === job.definitionHash && terminal.createdAt >= job.createdAt)) {
|
|
532
|
+
job = this.withGapSource(job.intent.gapId, job.intent.owner, () => {
|
|
533
|
+
this.assertGrowthRun(job.intent.creation?.growthRun ?? job.intent.revision?.growthRun, job.intent.mode, job.intent.gapId, job.intent.owner, false);
|
|
534
|
+
return this.options.store.rearmQueuedGrowthSourceJob({ id: job.id, revision: job.revision,
|
|
535
|
+
priorDefinitionHash: job.definitionHash, priorDefinitionVersion: health.definitionVersion, dispatchAt: Date.now() + 1000 });
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
335
539
|
const definition = this.definition(job);
|
|
540
|
+
const expectedHash = automationDefinitionDigest(definition);
|
|
541
|
+
if (job.previousDefinitionHash !== undefined) {
|
|
542
|
+
if (typeof automations.reconcileSystemExact !== 'function')
|
|
543
|
+
throw new SourceGrowthRunUnavailableError('exact native source rearm unavailable');
|
|
544
|
+
const health = automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
545
|
+
const activation = automations.inspectSystemOwnedActivation({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
546
|
+
if (activation?.activationNonce !== job.intentDigest || activation.ownerRouteId !== this.options.config.ownerRouteId
|
|
547
|
+
|| !((health.definitionHash === job.previousDefinitionHash && health.definitionVersion === job.previousDefinitionVersion)
|
|
548
|
+
|| (health.definitionHash === expectedHash && health.definitionVersion === job.previousDefinitionVersion + 1))) {
|
|
549
|
+
throw new SourceGrowthNativeMismatchError('source job native rearm lineage changed');
|
|
550
|
+
}
|
|
551
|
+
automations.reconcileSystemExact({ owner: SOURCE_JOB_OWNER, automationId: job.automationId,
|
|
552
|
+
idempotencyKey: `${job.id}:rearm:${job.dispatchAt}`, desiredStatus: 'active', definition,
|
|
553
|
+
expectedDefinitionHash: job.previousDefinitionHash, expectedVersion: job.previousDefinitionVersion });
|
|
554
|
+
const activated = automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
555
|
+
if (activated.definitionHash !== expectedHash || activated.definitionVersion !== job.previousDefinitionVersion + 1
|
|
556
|
+
|| activated.automationStatus !== 'active')
|
|
557
|
+
throw new SourceGrowthNativeMismatchError('source job exact native rearm was not active');
|
|
558
|
+
this.assertOwner(job);
|
|
559
|
+
this.options.store.bindSourceJobDefinition({ id: job.id, revision: job.revision, definitionHash: expectedHash });
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
336
562
|
if (job.definitionHash === undefined) {
|
|
337
|
-
|
|
563
|
+
const prior = job.previousDefinitionHash;
|
|
564
|
+
const existing = (() => {
|
|
565
|
+
try {
|
|
566
|
+
return automations.inspectSystemOwnedActivation({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
})();
|
|
572
|
+
if (existing !== undefined && (existing.activationNonce !== job.intentDigest || existing.ownerRouteId !== this.options.config.ownerRouteId
|
|
573
|
+
|| (existing.definitionHash !== expectedHash && existing.definitionHash !== prior))) {
|
|
574
|
+
throw new Error('source job native definition differs from persisted rearm');
|
|
575
|
+
}
|
|
576
|
+
automations.reconcileSystem({ owner: SOURCE_JOB_OWNER, automationId: job.automationId,
|
|
577
|
+
idempotencyKey: `${job.id}:prepare:${job.dispatchAt ?? job.createdAt + 1000}`, desiredStatus: 'paused', definition });
|
|
338
578
|
const registered = automations.inspectSystemOwnedActivation({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
339
|
-
if (registered === undefined || registered.activationNonce !== job.intentDigest || registered.ownerRouteId !== this.options.config.ownerRouteId
|
|
579
|
+
if (registered === undefined || registered.activationNonce !== job.intentDigest || registered.ownerRouteId !== this.options.config.ownerRouteId
|
|
580
|
+
|| registered.definitionHash !== expectedHash)
|
|
340
581
|
throw new Error('source job registration binding mismatch');
|
|
341
582
|
job = this.options.store.bindSourceJobDefinition({ id: job.id, revision: job.revision, definitionHash: registered.definitionHash });
|
|
342
583
|
}
|
|
343
584
|
const current = automations.inspectSystemOwnedActivation({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
344
585
|
if (current === undefined || current.definitionHash !== job.definitionHash || current.activationNonce !== job.intentDigest || current.ownerRouteId !== this.options.config.ownerRouteId)
|
|
345
586
|
throw new Error('source job automation changed');
|
|
346
|
-
automations.
|
|
587
|
+
const health = automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
588
|
+
if (health.definitionHash !== job.definitionHash)
|
|
589
|
+
throw new Error('source job native definition changed');
|
|
590
|
+
automations.reconcileSystem({ owner: SOURCE_JOB_OWNER, automationId: job.automationId,
|
|
591
|
+
idempotencyKey: `${job.id}:activate:v${health.definitionVersion}`, desiredStatus: 'active', definition });
|
|
592
|
+
const activated = automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
593
|
+
if (activated.definitionHash !== job.definitionHash || activated.automationStatus !== 'active')
|
|
594
|
+
throw new Error('source job native activation did not become active');
|
|
347
595
|
}
|
|
348
596
|
hasContinuations() {
|
|
349
|
-
return !!(this.options.approvePrepared || this.options.releasePrepared || this.options.advanceReleased || this.options.adoptReleased);
|
|
597
|
+
return !!(this.options.approvePrepared || this.options.verifyPreparedRevisionRegression || this.options.verifyPreparedRevision || this.options.verifyPreparedCreation || this.options.adoptVerifiedCreation || this.options.releasePrepared || this.options.advanceReleased || this.options.adoptReleased);
|
|
350
598
|
}
|
|
351
599
|
continuationJobs() {
|
|
352
600
|
if (!this.hasContinuations())
|
|
353
601
|
return [];
|
|
354
|
-
return this.options.store.listPreparedSourceApprovalJobs(this.options.releasePrepared !== undefined, this.options.advanceReleased !== undefined, this.options.adoptReleased !== undefined)
|
|
602
|
+
return this.options.store.listPreparedSourceApprovalJobs(this.options.releasePrepared !== undefined, this.options.advanceReleased !== undefined, this.options.adoptReleased !== undefined, this.options.verifyPreparedCreation !== undefined, this.options.adoptVerifiedCreation !== undefined, this.options.verifyPreparedRevision !== undefined, this.options.verifyPreparedRevisionRegression !== undefined)
|
|
355
603
|
.filter(job => this.eligibleContinuation(job));
|
|
356
604
|
}
|
|
357
605
|
/** Admission reads durable state and validates the current owner; it never invokes continuation approval or release hooks. */
|
|
@@ -365,8 +613,50 @@ export class SourceJobRuntime {
|
|
|
365
613
|
catch {
|
|
366
614
|
return false;
|
|
367
615
|
}
|
|
616
|
+
if (plan.mode === 'prepared-revise') {
|
|
617
|
+
if (job.intent.mode !== 'revise-created' || plan.status !== 'pending-approval' || !plan.sourceRevision?.growthRun.revisionAcceptance)
|
|
618
|
+
return false;
|
|
619
|
+
const status = this.options.store.getRevisionVerificationStatus(plan.id);
|
|
620
|
+
const verify = status === undefined && this.options.verifyPreparedRevision !== undefined;
|
|
621
|
+
const regression = status === 'verified' && plan.sourceRevision.growthRun.revisionRegressionAcceptance !== undefined
|
|
622
|
+
&& this.options.store.getRevisionRegressionStatus(plan.id) === undefined && this.options.verifyPreparedRevisionRegression !== undefined;
|
|
623
|
+
if (!verify && !regression)
|
|
624
|
+
return false;
|
|
625
|
+
try {
|
|
626
|
+
this.assertOwner(job);
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
return plan.expiresAt > Date.now();
|
|
632
|
+
}
|
|
633
|
+
if (plan.mode === 'prepared-create') {
|
|
634
|
+
if (job.intent.mode !== 'create' || plan.status !== 'pending-approval'
|
|
635
|
+
|| !job.intent.creation?.growthRun?.creationAcceptance || !plan.creation?.growthRun?.creationAcceptance)
|
|
636
|
+
return false;
|
|
637
|
+
const verification = this.options.store.getCreationVerificationStatus(plan.id);
|
|
638
|
+
const verify = verification === undefined && this.options.verifyPreparedCreation !== undefined;
|
|
639
|
+
const adopt = verification === 'verified' && this.options.adoptVerifiedCreation !== undefined
|
|
640
|
+
&& this.options.creationAdoptionEligible?.(plan.id) === true;
|
|
641
|
+
if (!verify && !adopt)
|
|
642
|
+
return false;
|
|
643
|
+
if (job.intent.authority.digest !== this.authorityDigest || job.intent.authority.id !== this.options.config.authorityId)
|
|
644
|
+
return false;
|
|
645
|
+
try {
|
|
646
|
+
this.assertOwner(job);
|
|
647
|
+
this.withGapSource(job.intent.gapId, job.intent.owner, () => {
|
|
648
|
+
this.assertGrowthRun(job.intent.creation?.growthRun, job.intent.mode, job.intent.gapId, job.intent.owner, false);
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
return plan.expiresAt > Date.now();
|
|
655
|
+
}
|
|
656
|
+
if (plan.mode !== 'modify' || job.intent.mode === 'create')
|
|
657
|
+
return false;
|
|
368
658
|
const adoption = plan.status === 'release-complete' ? this.options.store.findSourceAdoption(plan.id) : undefined;
|
|
369
|
-
const recovery = adoption !== undefined && ((adoption.status === 'approved' && adoption.dossier.handoff !== undefined) || ['staging', 'awaiting-reload', 'awaiting-readiness', 'awaiting-effect-blocked-replay', 'awaiting-shadow', 'awaiting-canary', 'awaiting-soak', 'awaiting-health', 'commit-pending', 'rollback-pending'].includes(adoption.status));
|
|
659
|
+
const recovery = adoption !== undefined && ((adoption.status === 'approved' && adoption.dossier.handoff !== undefined) || ['staging', 'awaiting-reload', 'awaiting-readiness', 'awaiting-live-tasks', 'awaiting-effect-blocked-replay', 'awaiting-shadow', 'awaiting-canary', 'awaiting-soak', 'awaiting-health', 'commit-pending', 'rollback-pending'].includes(adoption.status));
|
|
370
660
|
if (recovery)
|
|
371
661
|
return this.options.adoptReleased !== undefined;
|
|
372
662
|
if (job.intent.authority.digest !== this.authorityDigest || job.intent.authority.id !== this.options.config.authorityId)
|
|
@@ -390,12 +680,12 @@ export class SourceJobRuntime {
|
|
|
390
680
|
continuationTimeoutMs() {
|
|
391
681
|
const release = this.options.releaseTimeoutMs ?? 40_000, adoption = this.options.adoptionTimeoutMs ?? 60_000;
|
|
392
682
|
const needed = (this.options.adoptReleased ? adoption : 0) + (this.options.advanceReleased ? release + 40_000 : this.options.releasePrepared ? 40_000 : 10_000);
|
|
393
|
-
return Math.max(10_000, needed);
|
|
683
|
+
return Math.max(this.options.verifyPreparedCreation ? 1_800_000 : 10_000, needed);
|
|
394
684
|
}
|
|
395
685
|
continuationSignalTimeoutMs(job) {
|
|
396
686
|
if (job.planId) {
|
|
397
687
|
const plan = this.options.store.getSourcePlan(job.planId), adoption = plan.status === 'release-complete' ? this.options.store.findSourceAdoption(plan.id) : undefined;
|
|
398
|
-
if (adoption && ((adoption.status === 'approved' && adoption.dossier.handoff !== undefined) || ['staging', 'awaiting-reload', 'awaiting-readiness', 'awaiting-effect-blocked-replay', 'awaiting-shadow', 'awaiting-canary', 'awaiting-soak', 'awaiting-health', 'commit-pending', 'rollback-pending'].includes(adoption.status)))
|
|
688
|
+
if (adoption && ((adoption.status === 'approved' && adoption.dossier.handoff !== undefined) || ['staging', 'awaiting-reload', 'awaiting-readiness', 'awaiting-live-tasks', 'awaiting-effect-blocked-replay', 'awaiting-shadow', 'awaiting-canary', 'awaiting-soak', 'awaiting-health', 'commit-pending', 'rollback-pending'].includes(adoption.status)))
|
|
399
689
|
return this.continuationTimeoutMs();
|
|
400
690
|
}
|
|
401
691
|
return Math.max(1, Math.min(this.continuationTimeoutMs(), this.options.config.expiresAt - Date.now()));
|
|
@@ -461,8 +751,12 @@ export class SourceJobRuntime {
|
|
|
461
751
|
try {
|
|
462
752
|
this.schedule(job);
|
|
463
753
|
}
|
|
464
|
-
catch {
|
|
754
|
+
catch (error) {
|
|
755
|
+
if (error instanceof SourceGrowthRunUnavailableError)
|
|
756
|
+
return;
|
|
465
757
|
const current = this.options.store.getSourceJob(job.id);
|
|
758
|
+
if (current?.previousDefinitionHash !== undefined && !(error instanceof SourceGrowthNativeMismatchError))
|
|
759
|
+
return;
|
|
466
760
|
if (current?.status === 'queued')
|
|
467
761
|
this.options.store.settleSourceJob({ id: current.id, revision: current.revision, status: 'failed', failureCode: 'source-job-registration-rejected' });
|
|
468
762
|
}
|
|
@@ -472,8 +766,45 @@ export class SourceJobRuntime {
|
|
|
472
766
|
const failure = (unknown) => ({ outcome: unknown ? 'unknown' : 'failed', failureClass: unknown ? 'unknown' : 'configuration', failurePhase: 'host-execution', failureCode: 'source-job-rejected', sideEffectState: unknown ? 'unknown' : 'none', retryability: unknown ? 'unsafe' : 'after-intervention' });
|
|
473
767
|
if (job === undefined || job.status !== 'queued')
|
|
474
768
|
return failure(job?.status === 'unknown' || job?.status === 'running');
|
|
769
|
+
// A stale occurrence from before a persisted rearm can never claim this
|
|
770
|
+
// job. It has acquired no source resource, so leave the queued CAS intact.
|
|
771
|
+
if ((job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) !== undefined && input.definitionHash !== job.definitionHash) {
|
|
772
|
+
const expectedPending = job.previousDefinitionHash === undefined ? undefined : automationDefinitionDigest(this.definition(job));
|
|
773
|
+
if (expectedPending === input.definitionHash && input.executionMode === 'production'
|
|
774
|
+
&& input.activationNonce === job.intentDigest && input.catalogDigest === CATALOG
|
|
775
|
+
&& input.ownerRouteId === job.intent.owner.authorityId && input.principal === job.intent.owner.principalId
|
|
776
|
+
&& input.targetScope.workspace === job.intent.owner.workspace && input.targetScope.preset === job.intent.owner.agentPreset) {
|
|
777
|
+
try {
|
|
778
|
+
// The new native occurrence reached us before its cross-ledger DB
|
|
779
|
+
// acknowledgement. It has not claimed source resources. Bind the
|
|
780
|
+
// exact native receipt, then persist another one-shot before this
|
|
781
|
+
// occurrence can be considered consumed.
|
|
782
|
+
this.schedule(job);
|
|
783
|
+
const rebound = this.options.store.getSourceJob(job.id);
|
|
784
|
+
if (rebound?.status === 'queued' && rebound.definitionHash === input.definitionHash && rebound.occurrenceId === undefined) {
|
|
785
|
+
const health = this.options.ports.automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
786
|
+
if (health.definitionHash !== rebound.definitionHash)
|
|
787
|
+
throw new Error('source job premature occurrence native binding changed');
|
|
788
|
+
const pending = this.withGapSource(rebound.intent.gapId, rebound.intent.owner, () => {
|
|
789
|
+
this.assertGrowthRun(rebound.intent.creation?.growthRun ?? rebound.intent.revision?.growthRun, rebound.intent.mode, rebound.intent.gapId, rebound.intent.owner, false);
|
|
790
|
+
return this.options.store.rearmQueuedGrowthSourceJob({ id: rebound.id, revision: rebound.revision,
|
|
791
|
+
priorDefinitionHash: rebound.definitionHash, priorDefinitionVersion: health.definitionVersion, dispatchAt: Date.now() + 1000 });
|
|
792
|
+
});
|
|
793
|
+
this.schedule(pending);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
catch { /* persisted queued state remains fail-closed for exact recovery */ }
|
|
797
|
+
}
|
|
798
|
+
return failure(false);
|
|
799
|
+
}
|
|
475
800
|
let claimed = false;
|
|
476
|
-
const
|
|
801
|
+
const growthController = (job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) === undefined ? undefined : new AbortController();
|
|
802
|
+
if (growthController)
|
|
803
|
+
this.growthControllers.add(growthController);
|
|
804
|
+
const signal = AbortSignal.any([input.signal, this.abort.signal,
|
|
805
|
+
...(growthController ? [growthController.signal] : []),
|
|
806
|
+
AbortSignal.timeout(Math.max(1, Math.min(job.intent.build.timeoutMs + 120_000 + (this.options.verifyPreparedCreation ? 1_800_000 : 0)
|
|
807
|
+
+ (this.options.advanceReleased ? this.options.releaseTimeoutMs ?? 40_000 : 0) + (this.options.adoptReleased ? this.options.adoptionTimeoutMs ?? 60_000 : 0), job.expiresAt - Date.now())))]);
|
|
477
808
|
try {
|
|
478
809
|
this.assertOwner(job);
|
|
479
810
|
signal.throwIfAborted();
|
|
@@ -499,7 +830,9 @@ export class SourceJobRuntime {
|
|
|
499
830
|
await this.options.prepare(owned, signal, assertCurrent);
|
|
500
831
|
if (this.options.store.getSourceJob(owned.id)?.status !== 'prepared')
|
|
501
832
|
throw new Error('source job completion was not committed');
|
|
502
|
-
if ((
|
|
833
|
+
if ((owned.intent.mode === 'create' ? this.options.verifyPreparedCreation !== undefined
|
|
834
|
+
: owned.intent.mode === 'revise-created' ? this.options.verifyPreparedRevision !== undefined : !!(this.options.approvePrepared || this.options.releasePrepared || this.options.advanceReleased || this.options.adoptReleased))
|
|
835
|
+
&& this.options.store.getOwnerTaskFailureReference(owned.intent.gapId)) {
|
|
503
836
|
this.reconcileContinuations();
|
|
504
837
|
this.assertOwner(owned);
|
|
505
838
|
await this.continuePrepared(this.options.store.getSourceJob(owned.id), signal);
|
|
@@ -507,13 +840,22 @@ export class SourceJobRuntime {
|
|
|
507
840
|
}
|
|
508
841
|
return { outcome: 'succeeded', failureClass: 'none', failurePhase: 'none', failureCode: 'none', sideEffectState: 'possible', retryability: 'unsafe' };
|
|
509
842
|
}
|
|
510
|
-
catch {
|
|
843
|
+
catch (error) {
|
|
511
844
|
const current = this.options.store.getSourceJob(job.id);
|
|
845
|
+
if (!claimed && error instanceof SourceGrowthRunUnavailableError)
|
|
846
|
+
return failure(false);
|
|
847
|
+
if (!claimed && current?.status === 'queued'
|
|
848
|
+
&& (current.revision !== job.revision || current.definitionHash !== input.definitionHash))
|
|
849
|
+
return failure(false);
|
|
512
850
|
if (current?.status === 'queued' || (current?.status === 'running' && current.occurrenceId === input.occurrenceId)) {
|
|
513
851
|
this.options.store.settleSourceJob({ id: current.id, revision: current.revision, status: claimed ? 'unknown' : 'failed', failureCode: claimed ? 'source-job-interrupted' : 'source-job-preflight-rejected' });
|
|
514
852
|
}
|
|
515
853
|
return failure(claimed);
|
|
516
854
|
}
|
|
855
|
+
finally {
|
|
856
|
+
if (growthController)
|
|
857
|
+
this.growthControllers.delete(growthController);
|
|
858
|
+
}
|
|
517
859
|
}
|
|
518
860
|
/** Explicit Host action. Unknown keeps its capacity until exact cleanup succeeds. */
|
|
519
861
|
reconcileUnknown(input) {
|