@dsh-enhanced/plugin-control-plane 0.1.48 → 0.1.53
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 +187 -15
- package/bin/dsh-live-qualification-authority.js +15 -0
- package/bin/dsh-local-release-adapter.js +331 -9
- 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 +775 -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 +578 -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 +265 -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/native-source-build-worker.d.ts +4 -0
- package/lib/native-source-build-worker.d.ts.map +1 -0
- package/lib/native-source-build-worker.js +87 -0
- package/lib/native-source-build-worker.js.map +1 -0
- package/lib/native-source-build.d.ts +36 -0
- package/lib/native-source-build.d.ts.map +1 -0
- package/lib/native-source-build.js +143 -0
- package/lib/native-source-build.js.map +1 -0
- package/lib/native-source-pack.d.ts +11 -0
- package/lib/native-source-pack.d.ts.map +1 -0
- package/lib/native-source-pack.js +83 -0
- package/lib/native-source-pack.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-protocol.d.ts +3 -3
- package/lib/runtime-observer-protocol.js +5 -5
- package/lib/runtime-observer-protocol.js.map +1 -1
- 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 +248 -4
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +1431 -64
- 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-approval-authority.d.ts.map +1 -1
- package/lib/source-approval-authority.js +1 -0
- package/lib/source-approval-authority.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 +43 -9
- package/lib/source-build.d.ts.map +1 -1
- package/lib/source-build.js +670 -57
- 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 +28 -1
- package/lib/source-job-types.d.ts.map +1 -1
- package/lib/source-jobs.d.ts +33 -3
- package/lib/source-jobs.d.ts.map +1 -1
- package/lib/source-jobs.js +420 -43
- 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 +2257 -96
- 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 +57 -7
- package/lib/types.d.ts.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +22 -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', 'allowedModes', '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,26 @@ 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.allowedModes !== undefined && (!Array.isArray(value.allowedModes) || value.allowedModes.length < 1
|
|
40
|
+
|| value.allowedModes.length > 3 || new Set(value.allowedModes).size !== value.allowedModes.length
|
|
41
|
+
|| value.allowedModes.some(mode => !['create', 'modify', 'revise-created'].includes(mode)))) {
|
|
42
|
+
throw new Error('plugin-control-plane: invalid sourceJobs allowedModes');
|
|
43
|
+
}
|
|
44
|
+
if (value.creation !== undefined)
|
|
45
|
+
validateSourceCreationGrant(value.creation);
|
|
46
|
+
if (value.revision !== undefined)
|
|
47
|
+
validateSourceRevisionGrant(value.revision);
|
|
48
|
+
if (build !== undefined && 'backend' in build
|
|
49
|
+
&& (!value.allowedModes || value.allowedModes.some(mode => mode === 'modify')
|
|
50
|
+
|| value.creation === undefined && value.revision === undefined)) {
|
|
51
|
+
throw new Error('plugin-control-plane: native source jobs require finite create/revise-created grants');
|
|
52
|
+
}
|
|
29
53
|
}
|
|
30
54
|
function projection(job) {
|
|
31
55
|
return { id: job.id, name: job.intent.name, gapId: job.intent.gapId, baseCommit: job.intent.baseCommit,
|
|
56
|
+
...(job.intent.mode === undefined ? {} : { mode: job.intent.mode }),
|
|
32
57
|
status: job.status, createdAt: job.createdAt, expiresAt: job.expiresAt,
|
|
33
58
|
...(job.planId === undefined ? {} : { planId: job.planId }), ...(job.failureCode === undefined ? {} : { failureCode: job.failureCode }) };
|
|
34
59
|
}
|
|
@@ -37,6 +62,8 @@ export class SourceJobRuntime {
|
|
|
37
62
|
options;
|
|
38
63
|
abort = new AbortController();
|
|
39
64
|
flights = new Set();
|
|
65
|
+
growthFlights = new Set();
|
|
66
|
+
growthControllers = new Set();
|
|
40
67
|
authorityDigest;
|
|
41
68
|
unregister;
|
|
42
69
|
unregisterContinuation;
|
|
@@ -59,7 +86,15 @@ export class SourceJobRuntime {
|
|
|
59
86
|
const executor = {
|
|
60
87
|
descriptor: { executorId: EXECUTOR, contractVersion: 1, catalogDigest: CATALOG },
|
|
61
88
|
accepts: spec => spec.executorId === EXECUTOR && spec.executorContractVersion === 1 && spec.catalogDigest === CATALOG && spec.runbookId === EXECUTOR && spec.runbookVersion === 1,
|
|
62
|
-
execute: input =>
|
|
89
|
+
execute: input => {
|
|
90
|
+
const job = this.options.store.getSourceJobByAutomation(input.automationId);
|
|
91
|
+
const flight = this.track(this.execute(input));
|
|
92
|
+
if ((job?.intent.creation?.growthRun ?? job?.intent.revision?.growthRun) !== undefined) {
|
|
93
|
+
this.growthFlights.add(flight);
|
|
94
|
+
void flight.then(() => this.growthFlights.delete(flight), () => this.growthFlights.delete(flight));
|
|
95
|
+
}
|
|
96
|
+
return flight;
|
|
97
|
+
},
|
|
63
98
|
};
|
|
64
99
|
try {
|
|
65
100
|
this.unregister = this.options.ports.automations.registerHostExecutor(executor);
|
|
@@ -90,23 +125,57 @@ export class SourceJobRuntime {
|
|
|
90
125
|
catch { /* original error wins */ }
|
|
91
126
|
throw error;
|
|
92
127
|
}
|
|
128
|
+
this.reconcileQueued();
|
|
129
|
+
}
|
|
130
|
+
/** Nudge the existing native one-shot registrations after a Host provider appears. */
|
|
131
|
+
reconcileQueued() {
|
|
132
|
+
if (!this.active)
|
|
133
|
+
return;
|
|
93
134
|
// Reconcile only queued intents. A previous claim is unknown and never replayed.
|
|
94
135
|
for (let job of this.options.store.listSourceJobs(100)) {
|
|
95
136
|
if (job.status !== 'queued')
|
|
96
137
|
continue;
|
|
97
138
|
try {
|
|
98
139
|
job = this.refreshQueued(job);
|
|
99
|
-
if (job.status === 'queued')
|
|
100
|
-
this.assertOwner(job);
|
|
140
|
+
if (job.status === 'queued')
|
|
101
141
|
this.schedule(job);
|
|
102
|
-
}
|
|
103
142
|
}
|
|
104
|
-
catch {
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error instanceof SourceGrowthRunUnavailableError)
|
|
145
|
+
continue;
|
|
105
146
|
const current = this.options.store.getSourceJob(job.id);
|
|
147
|
+
// A persisted rearm is an in-progress cross-ledger handoff. Retain it
|
|
148
|
+
// for exact native receipt replay after a crash or transient fault.
|
|
149
|
+
if (current?.previousDefinitionHash !== undefined && !(error instanceof SourceGrowthNativeMismatchError))
|
|
150
|
+
continue;
|
|
106
151
|
if (current?.status === 'queued')
|
|
107
152
|
this.options.store.settleSourceJob({ id: current.id, revision: current.revision, status: 'failed', failureCode: 'source-job-reconcile-rejected' });
|
|
108
153
|
}
|
|
109
154
|
}
|
|
155
|
+
this.reconcileContinuations();
|
|
156
|
+
}
|
|
157
|
+
/** Provider removal revokes in-flight authority without replaying a claimed job. */
|
|
158
|
+
growthProviderDisposed() {
|
|
159
|
+
for (const controller of this.growthControllers)
|
|
160
|
+
controller.abort(new Error('source growth run producer disposed'));
|
|
161
|
+
this.options.store.interruptGrowthSourceJobs();
|
|
162
|
+
if (!this.active)
|
|
163
|
+
return Promise.allSettled(this.growthFlights).then(() => undefined);
|
|
164
|
+
for (const job of this.options.store.listSourceJobs(100)) {
|
|
165
|
+
if (job.status !== 'queued' || (job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) === undefined || job.definitionHash === undefined)
|
|
166
|
+
continue;
|
|
167
|
+
try {
|
|
168
|
+
const health = this.options.ports.automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
169
|
+
if (health.definitionHash !== job.definitionHash)
|
|
170
|
+
continue;
|
|
171
|
+
if (health.automationStatus === 'active')
|
|
172
|
+
this.options.ports.automations.pauseSystemOwned({ owner: SOURCE_JOB_OWNER,
|
|
173
|
+
operationId: `${job.id}:growth-provider-pause:v${health.definitionVersion}`, automationId: job.automationId,
|
|
174
|
+
definitionHash: job.definitionHash, expectedVersion: health.definitionVersion });
|
|
175
|
+
}
|
|
176
|
+
catch { /* claim guard remains authoritative */ }
|
|
177
|
+
}
|
|
178
|
+
return Promise.allSettled(this.growthFlights).then(() => undefined);
|
|
110
179
|
}
|
|
111
180
|
async continuePrepared(job, signal) {
|
|
112
181
|
if (!job.planId || !this.options.store.getOwnerTaskFailureReference(job.intent.gapId))
|
|
@@ -122,6 +191,7 @@ export class SourceJobRuntime {
|
|
|
122
191
|
}
|
|
123
192
|
}
|
|
124
193
|
async continuePreparedLocked(job, signal) {
|
|
194
|
+
this.assertMode(job.intent.mode);
|
|
125
195
|
if (!job.planId || !this.options.store.getOwnerTaskFailureReference(job.intent.gapId))
|
|
126
196
|
return;
|
|
127
197
|
const planId = job.planId;
|
|
@@ -130,6 +200,8 @@ export class SourceJobRuntime {
|
|
|
130
200
|
throw new Error('source job continuation changed');
|
|
131
201
|
job = current;
|
|
132
202
|
const initial = this.options.store.getSourcePlan(planId);
|
|
203
|
+
if (initial.mode !== 'modify' && initial.mode !== 'prepared-create' && initial.mode !== 'prepared-revise')
|
|
204
|
+
return;
|
|
133
205
|
// An ordinary grant or plan must not be advanced after expiry. Adoption has
|
|
134
206
|
// its own durable post-release path and is deliberately retained below.
|
|
135
207
|
if (initial.expiresAt <= Date.now() && initial.status !== 'release-complete')
|
|
@@ -142,6 +214,35 @@ export class SourceJobRuntime {
|
|
|
142
214
|
signal.throwIfAborted();
|
|
143
215
|
this.assertOwner(job);
|
|
144
216
|
};
|
|
217
|
+
if (initial.mode === 'prepared-revise') {
|
|
218
|
+
if (job.intent.mode !== 'revise-created' || !job.intent.revision?.growthRun.revisionAcceptance || !initial.sourceRevision?.growthRun.revisionAcceptance)
|
|
219
|
+
return;
|
|
220
|
+
await assertCurrent();
|
|
221
|
+
if (this.options.store.getRevisionVerificationStatus(planId) === undefined && this.options.verifyPreparedRevision)
|
|
222
|
+
await this.options.verifyPreparedRevision(job, signal);
|
|
223
|
+
if (this.options.store.getRevisionVerificationStatus(planId) === 'verified'
|
|
224
|
+
&& this.options.store.getRevisionRegressionStatus(planId) === undefined
|
|
225
|
+
&& job.intent.revision.growthRun.revisionRegressionAcceptance && this.options.verifyPreparedRevisionRegression) {
|
|
226
|
+
await assertCurrent();
|
|
227
|
+
await this.options.verifyPreparedRevisionRegression(job, signal);
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (initial.mode === 'prepared-create') {
|
|
232
|
+
if (job.intent.mode !== 'create'
|
|
233
|
+
|| !job.intent.creation?.growthRun?.creationAcceptance || !initial.creation?.growthRun?.creationAcceptance)
|
|
234
|
+
return;
|
|
235
|
+
await assertCurrent();
|
|
236
|
+
if (this.options.store.getCreationVerificationStatus(planId) === undefined && this.options.verifyPreparedCreation) {
|
|
237
|
+
await this.options.verifyPreparedCreation(job, signal);
|
|
238
|
+
}
|
|
239
|
+
if (this.options.store.getCreationVerificationStatus(planId) === 'verified'
|
|
240
|
+
&& this.options.creationAdoptionEligible?.(planId) && this.options.adoptVerifiedCreation) {
|
|
241
|
+
await assertCurrent();
|
|
242
|
+
await this.options.adoptVerifiedCreation(job, signal);
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
145
246
|
if (this.options.adoptReleased && this.options.store.getSourcePlan(planId).status === 'release-complete') {
|
|
146
247
|
await this.options.adoptReleased(job, signal, assertCurrent);
|
|
147
248
|
return;
|
|
@@ -219,34 +320,100 @@ export class SourceJobRuntime {
|
|
|
219
320
|
throw new Error('source job task provenance service unavailable');
|
|
220
321
|
return this.options.withGapSourceFence(gapId, owner, callback);
|
|
221
322
|
}
|
|
323
|
+
modeAllowed(mode) {
|
|
324
|
+
return this.options.config.allowedModes?.includes(mode ?? 'modify') ?? true;
|
|
325
|
+
}
|
|
326
|
+
assertMode(mode) {
|
|
327
|
+
if (!this.modeAllowed(mode))
|
|
328
|
+
throw new Error('source job mode is not authorized');
|
|
329
|
+
}
|
|
222
330
|
assertOwner(job) {
|
|
223
|
-
|
|
224
|
-
|
|
331
|
+
this.assertMode(job.intent.mode);
|
|
332
|
+
const current = this.receipt();
|
|
333
|
+
const taskBacked = this.options.store.getOwnerTaskFailureReference(job.intent.gapId) !== undefined;
|
|
334
|
+
if (!this.available() || job.expiresAt <= Date.now() || (job.intent.mode === 'create' && (!this.options.config.creation || !job.intent.creation
|
|
335
|
+
|| job.intent.creation.grant.expiresAt <= Date.now() || controlPlaneDigest(job.intent.creation.grant) !== controlPlaneDigest(this.options.config.creation)))
|
|
336
|
+
|| (job.intent.mode === 'revise-created' && (!this.options.config.revision || !job.intent.revision
|
|
337
|
+
|| job.intent.revision.grant.expiresAt <= Date.now() || controlPlaneDigest(job.intent.revision.grant) !== controlPlaneDigest(this.options.config.revision)))
|
|
338
|
+
|| job.intent.authority.digest !== this.authorityDigest
|
|
339
|
+
|| job.intent.authority.id !== this.options.config.authorityId
|
|
340
|
+
|| controlPlaneDigest(job.intent.owner) !== job.intent.ownerDigest
|
|
341
|
+
|| (taskBacked ? !isSourceOwnerContinuation(current, job.intent.owner)
|
|
342
|
+
: controlPlaneDigest(current) !== job.intent.ownerDigest))
|
|
225
343
|
throw new Error('source job authority changed or expired');
|
|
226
|
-
|
|
344
|
+
if (job.intent.revision) {
|
|
345
|
+
const parent = this.options.inspectRevisionParent?.(job.intent.revision.parent.planId);
|
|
346
|
+
if (!parent || parent.name !== job.intent.name || controlPlaneDigest(parent.parent) !== controlPlaneDigest(job.intent.revision.parent))
|
|
347
|
+
throw new Error('source revision parent changed');
|
|
348
|
+
}
|
|
349
|
+
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));
|
|
350
|
+
}
|
|
351
|
+
assertGrowthRun(growthRun, mode, gapId, owner, generation) {
|
|
352
|
+
const taskBacked = this.options.store.getOwnerTaskFailureReference(gapId) !== undefined;
|
|
353
|
+
if ((mode !== 'create' && mode !== 'revise-created') || !taskBacked) {
|
|
354
|
+
if (growthRun !== undefined)
|
|
355
|
+
throw new Error('source growth run is outside a task-backed creation');
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (growthRun === undefined)
|
|
359
|
+
throw new Error('task-backed creation requires a frozen source growth run');
|
|
360
|
+
validateSourceGrowthRunBinding(growthRun);
|
|
361
|
+
if (!this.options.assertGrowthRun)
|
|
362
|
+
throw new SourceGrowthRunUnavailableError('source growth run producer unavailable');
|
|
363
|
+
this.options.assertGrowthRun(growthRun, gapId, owner, generation);
|
|
227
364
|
}
|
|
228
365
|
enqueue(input) {
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
|
|
366
|
+
this.assertMode(input.mode);
|
|
367
|
+
if (input.mode !== 'create' && input.mode !== 'revise-created' && input.growthRun !== undefined)
|
|
368
|
+
throw new Error('modify source job cannot carry a growth run');
|
|
369
|
+
if (input.mode === 'create' || input.mode === 'revise-created')
|
|
370
|
+
validateSourceCreationFiles(input.files);
|
|
371
|
+
else {
|
|
372
|
+
if (input.mode !== undefined)
|
|
373
|
+
throw new Error('invalid source job mode');
|
|
374
|
+
validateScopedPluginFiles(input.files);
|
|
375
|
+
if (this.options.build.versioning === 'patch')
|
|
376
|
+
assertManagedVersionPaths(input.files);
|
|
377
|
+
}
|
|
378
|
+
if ((input.mode === 'revise-created') !== (typeof input.parentPlanId === 'string' && input.parentPlanId.length > 0))
|
|
379
|
+
throw new Error('source revision parent is missing or outside revision mode');
|
|
232
380
|
return this.track(this.enqueueOwned({ ...input, files: structuredClone(input.files), owner: structuredClone(input.owner) }));
|
|
233
381
|
}
|
|
234
382
|
async enqueueOwned(input) {
|
|
383
|
+
this.assertMode(input.mode);
|
|
235
384
|
const signal = AbortSignal.any([input.signal, this.abort.signal, AbortSignal.timeout(15_000)]);
|
|
236
385
|
const assertCurrent = async () => { signal.throwIfAborted(); await awaitSourceSignal(signal, input.assertCurrent); this.withGapSource(input.gapId, this.assertCaller(input.owner), () => { }); if (!this.available())
|
|
237
386
|
throw new Error('source job authority unavailable'); signal.throwIfAborted(); };
|
|
238
387
|
await assertCurrent();
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
388
|
+
if (input.mode === 'create' || input.mode === 'revise-created')
|
|
389
|
+
validateSourceCreationFiles(input.files);
|
|
390
|
+
else {
|
|
391
|
+
validateScopedPluginFiles(input.files);
|
|
392
|
+
if (this.options.build.versioning === 'patch')
|
|
393
|
+
assertManagedVersionPaths(input.files);
|
|
394
|
+
assertPluginModificationAllowed(input.name);
|
|
395
|
+
}
|
|
243
396
|
if (typeof input.idempotencyKey !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u.test(input.idempotencyKey)
|
|
244
397
|
|| !Number.isSafeInteger(input.ttlMs) || input.ttlMs < 900_000 || input.ttlMs > 86_400_000)
|
|
245
398
|
throw new Error('invalid source job request');
|
|
246
399
|
const config = this.options.config;
|
|
400
|
+
if (input.mode === 'create' && (config.creation === undefined || config.creation.expiresAt <= Date.now()))
|
|
401
|
+
throw new Error('source creation grant unavailable or expired');
|
|
402
|
+
if (input.mode === 'revise-created' && (config.revision === undefined || config.revision.expiresAt <= Date.now()))
|
|
403
|
+
throw new Error('source revision grant unavailable or expired');
|
|
247
404
|
if (input.repository !== config.repository || await realpath(input.repository) !== config.repository)
|
|
248
405
|
throw new Error('source job repository mismatch');
|
|
249
|
-
const
|
|
406
|
+
const currentOwner = this.assertCaller(input.owner);
|
|
407
|
+
const reference = this.options.store.getOwnerTaskFailureReference(input.gapId);
|
|
408
|
+
if (input.mode === 'revise-created' && !reference)
|
|
409
|
+
throw new Error('revision requires an authenticated owner task failure');
|
|
410
|
+
const owner = reference?.owner ?? currentOwner;
|
|
411
|
+
if (reference && !isSourceOwnerContinuation(currentOwner, owner))
|
|
412
|
+
throw new Error('source job task owner changed');
|
|
413
|
+
this.withGapSource(input.gapId, currentOwner, () => this.assertGrowthRun(input.growthRun, input.mode, input.gapId, owner, true));
|
|
414
|
+
const parent = input.mode === 'revise-created' ? this.options.inspectRevisionParent?.(input.parentPlanId) : undefined;
|
|
415
|
+
if (input.mode === 'revise-created' && (!parent || parent.name !== input.name))
|
|
416
|
+
throw new Error('adopted revision parent unavailable or mismatched');
|
|
250
417
|
const trust = await awaitSourceSignal(signal, this.options.trust);
|
|
251
418
|
for (const outstanding of this.options.store.listSourceJobs(1))
|
|
252
419
|
this.refreshQueued(outstanding);
|
|
@@ -256,6 +423,12 @@ export class SourceJobRuntime {
|
|
|
256
423
|
if (prior !== undefined) {
|
|
257
424
|
const frozen = prior.intent;
|
|
258
425
|
if (frozen.authority.digest !== this.authorityDigest || frozen.ownerDigest !== controlPlaneDigest(owner) || frozen.trustDigest !== controlPlaneDigest(trust)
|
|
426
|
+
|| frozen.mode !== input.mode || (input.mode === 'create' && (controlPlaneDigest(frozen.creation?.grant) !== controlPlaneDigest(config.creation)
|
|
427
|
+
|| (frozen.creation?.growthRun === undefined) !== (input.growthRun === undefined)
|
|
428
|
+
|| (input.growthRun !== undefined && controlPlaneDigest(frozen.creation?.growthRun) !== controlPlaneDigest(input.growthRun))))
|
|
429
|
+
|| (input.mode === 'revise-created' && (controlPlaneDigest(frozen.revision?.grant) !== controlPlaneDigest(config.revision)
|
|
430
|
+
|| controlPlaneDigest(frozen.revision?.parent) !== controlPlaneDigest(parent.parent)
|
|
431
|
+
|| controlPlaneDigest(frozen.revision?.growthRun) !== controlPlaneDigest(input.growthRun)))
|
|
259
432
|
|| frozen.repository !== input.repository || frozen.name !== input.name || frozen.gapId !== input.gapId || frozen.baseCommit !== input.expectedBaseCommit
|
|
260
433
|
|| frozen.ttlMs !== input.ttlMs || controlPlaneDigest(frozen.files) !== controlPlaneDigest(input.files))
|
|
261
434
|
throw new Error('source job idempotency conflict');
|
|
@@ -267,17 +440,39 @@ export class SourceJobRuntime {
|
|
|
267
440
|
const gap = this.options.store.getGap(input.gapId);
|
|
268
441
|
if (gap.status !== 'open' || gap.candidateId !== undefined)
|
|
269
442
|
throw new Error('source job gap is not open');
|
|
270
|
-
const
|
|
443
|
+
const environment = inheritedEnvironment(trust);
|
|
444
|
+
const baselineCommit = config.baseline === undefined ? undefined : await resolveSourceBaseline({ repository: config.repository,
|
|
445
|
+
config: config.baseline, environment, signal, assertCurrent, trust,
|
|
446
|
+
readHistory: () => this.options.store.getSourceBaselineHistory(config.repository),
|
|
447
|
+
readMaintenance: () => this.options.store.getSourceMaintenanceRecords(config.repository) });
|
|
448
|
+
const context = { repository: config.repository, name: input.name, paths: [], baseCommit: input.expectedBaseCommit,
|
|
449
|
+
...(baselineCommit === undefined ? {} : { baselineCommit }), environment, signal, assertCurrent };
|
|
450
|
+
const source = input.mode === 'create'
|
|
451
|
+
? await inspectSourceCreationContext({ ...context, grant: config.creation })
|
|
452
|
+
: input.mode === 'revise-created'
|
|
453
|
+
? await inspectSourceRevisionContext({ ...context, grant: config.revision, parent: parent.parent, parentSource: parent.source, parentCertificate: parent.certificate })
|
|
454
|
+
: await inspectSourceContext(context);
|
|
271
455
|
const intent = {
|
|
272
456
|
authority: { id: config.authorityId, digest: this.authorityDigest, expiresAt: config.expiresAt, maxSubmissions: config.maxSubmissions },
|
|
273
457
|
owner, ownerDigest: controlPlaneDigest(owner), trustDigest: controlPlaneDigest(trust), repository: config.repository, name: input.name,
|
|
458
|
+
...(config.baseline === undefined ? {} : { baseline: structuredClone(config.baseline) }),
|
|
459
|
+
...(input.mode === 'create' ? { mode: 'create', creation: { grant: structuredClone(config.creation), generatorDigest: source.generatorDigest,
|
|
460
|
+
...(input.growthRun === undefined ? {} : { growthRun: structuredClone(input.growthRun) }) } } : {}),
|
|
461
|
+
...(input.mode === 'revise-created' ? { mode: 'revise-created', revision: { grant: structuredClone(config.revision),
|
|
462
|
+
generatorDigest: source.generatorDigest, parent: structuredClone(parent.parent), growthRun: structuredClone(input.growthRun) } } : {}),
|
|
274
463
|
gapId: gap.id, gapRevision: gap.revision, gapDigest: controlPlaneDigest(gap), baseCommit: source.baseCommit,
|
|
275
464
|
files: structuredClone(input.files), ttlMs: input.ttlMs, build: structuredClone(this.options.build),
|
|
276
|
-
worktree: join(this.options.statePath, 'source-worktrees', `worktree-job-${token}`),
|
|
465
|
+
worktree: join(this.options.statePath, 'source-worktrees', `worktree-job-${token}`),
|
|
466
|
+
...('backend' in this.options.build
|
|
467
|
+
? { nativeResource: { backend: 'macos-seatbelt', key: id } }
|
|
468
|
+
: { containerName: `dsh-source-job-${token}` }),
|
|
277
469
|
};
|
|
278
470
|
await assertCurrent();
|
|
279
471
|
// Synchronous acceptance and registration: no Agent signal is persisted.
|
|
280
|
-
const job = this.withGapSource(gap.id, owner, () =>
|
|
472
|
+
const job = this.withGapSource(gap.id, owner, () => {
|
|
473
|
+
this.assertGrowthRun(input.growthRun, input.mode, gap.id, owner, true);
|
|
474
|
+
return this.options.store.enqueueSourceJob({ id, automationId: id, idempotencyKey: input.idempotencyKey, intent });
|
|
475
|
+
});
|
|
281
476
|
if (job.status === 'queued')
|
|
282
477
|
this.scheduleOrFail(job);
|
|
283
478
|
return projection(this.options.store.getSourceJob(id));
|
|
@@ -285,8 +480,8 @@ export class SourceJobRuntime {
|
|
|
285
480
|
inspect(input) {
|
|
286
481
|
const owner = this.assertCaller(input.owner);
|
|
287
482
|
const job = this.options.store.getSourceJob(input.id);
|
|
288
|
-
//
|
|
289
|
-
//
|
|
483
|
+
// Inspection and cleanup may cross a session binding. A task-backed job
|
|
484
|
+
// retains its frozen owner digest and may continue under the same owner.
|
|
290
485
|
if (job === undefined || job.intent.owner.authorityId !== owner.authorityId
|
|
291
486
|
|| job.intent.owner.principalId !== owner.principalId || job.intent.owner.principalRecordId !== owner.principalRecordId
|
|
292
487
|
|| job.intent.owner.principalVersion !== owner.principalVersion || job.intent.owner.workspace !== owner.workspace
|
|
@@ -298,16 +493,25 @@ export class SourceJobRuntime {
|
|
|
298
493
|
refreshQueued(job) {
|
|
299
494
|
if (job.status !== 'queued')
|
|
300
495
|
return job;
|
|
496
|
+
if (!this.modeAllowed(job.intent.mode))
|
|
497
|
+
return this.options.store.settleSourceJob({ id: job.id, revision: job.revision,
|
|
498
|
+
status: 'failed', failureCode: 'source-job-mode-revoked' });
|
|
301
499
|
let code;
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
500
|
+
let providerUnavailable = false;
|
|
501
|
+
if (job.intent.mode === 'create' && this.options.store.getOwnerTaskFailureReference(job.intent.gapId) !== undefined
|
|
502
|
+
&& (job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) === undefined)
|
|
503
|
+
code = 'source-job-growth-run-missing';
|
|
504
|
+
else {
|
|
505
|
+
try {
|
|
506
|
+
this.withGapSource(job.intent.gapId, job.intent.owner, () => { providerUnavailable = this.growthProducerUnavailable(job); });
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
code = 'source-job-task-source-changed';
|
|
510
|
+
}
|
|
307
511
|
}
|
|
308
512
|
if (job.expiresAt <= Date.now())
|
|
309
513
|
code = 'source-job-expired';
|
|
310
|
-
else if (job.definitionHash !== undefined) {
|
|
514
|
+
else if (code === undefined && job.definitionHash !== undefined && !providerUnavailable && (job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) === undefined) {
|
|
311
515
|
const health = this.options.ports.automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
312
516
|
const run = health.latestTerminalRuns.production;
|
|
313
517
|
if (run !== undefined && run.createdAt >= job.createdAt && run.immutableContext.state === 'verified'
|
|
@@ -319,9 +523,20 @@ export class SourceJobRuntime {
|
|
|
319
523
|
// loses its claim. No resubmission or second native occurrence is created.
|
|
320
524
|
return code === undefined ? job : this.options.store.settleSourceJob({ id: job.id, revision: job.revision, status: 'failed', failureCode: code });
|
|
321
525
|
}
|
|
526
|
+
growthProducerUnavailable(job) {
|
|
527
|
+
try {
|
|
528
|
+
this.assertGrowthRun((job.intent.creation?.growthRun ?? job.intent.revision?.growthRun), job.intent.mode, job.intent.gapId, job.intent.owner, false);
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
if (error instanceof SourceGrowthRunUnavailableError)
|
|
533
|
+
return true;
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
322
537
|
definition(job) {
|
|
323
538
|
const config = this.options.config;
|
|
324
|
-
return { name: `Source check: ${job.intent.name}`, schedule: { kind: 'at', at: new Date(job.createdAt + 1000).toISOString() },
|
|
539
|
+
return { name: `Source check: ${job.intent.name}`, schedule: { kind: 'at', at: new Date(job.dispatchAt ?? job.createdAt + 1000).toISOString() },
|
|
325
540
|
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
541
|
misfire: { kind: 'latest' }, overlap: 'skip', retrySafety: 'never', maxRetries: 0, principal: config.principalId,
|
|
327
542
|
...(config.budgetId === undefined ? {} : { budgetId: config.budgetId, budgetAmount: config.budgetAmount }),
|
|
@@ -332,27 +547,87 @@ export class SourceJobRuntime {
|
|
|
332
547
|
schedule(job) {
|
|
333
548
|
this.assertOwner(job);
|
|
334
549
|
const automations = this.options.ports.automations;
|
|
550
|
+
if (job.definitionHash !== undefined && (job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) !== undefined && job.occurrenceId === undefined) {
|
|
551
|
+
const health = automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
552
|
+
if (health.definitionHash !== job.definitionHash)
|
|
553
|
+
throw new Error('source job native definition changed before rearm');
|
|
554
|
+
const dueAt = job.dispatchAt ?? job.createdAt + 1000;
|
|
555
|
+
const terminal = health.latestTerminalRuns.production;
|
|
556
|
+
if ((health.automationStatus === 'paused' && dueAt <= Date.now())
|
|
557
|
+
|| (terminal !== undefined && terminal.immutableContext.state === 'verified'
|
|
558
|
+
&& terminal.immutableContext.definitionHash === job.definitionHash && terminal.createdAt >= job.createdAt)) {
|
|
559
|
+
job = this.withGapSource(job.intent.gapId, job.intent.owner, () => {
|
|
560
|
+
this.assertGrowthRun(job.intent.creation?.growthRun ?? job.intent.revision?.growthRun, job.intent.mode, job.intent.gapId, job.intent.owner, false);
|
|
561
|
+
return this.options.store.rearmQueuedGrowthSourceJob({ id: job.id, revision: job.revision,
|
|
562
|
+
priorDefinitionHash: job.definitionHash, priorDefinitionVersion: health.definitionVersion, dispatchAt: Date.now() + 1000 });
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
335
566
|
const definition = this.definition(job);
|
|
567
|
+
const expectedHash = automationDefinitionDigest(definition);
|
|
568
|
+
if (job.previousDefinitionHash !== undefined) {
|
|
569
|
+
if (typeof automations.reconcileSystemExact !== 'function')
|
|
570
|
+
throw new SourceGrowthRunUnavailableError('exact native source rearm unavailable');
|
|
571
|
+
const health = automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
572
|
+
const activation = automations.inspectSystemOwnedActivation({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
573
|
+
if (activation?.activationNonce !== job.intentDigest || activation.ownerRouteId !== this.options.config.ownerRouteId
|
|
574
|
+
|| !((health.definitionHash === job.previousDefinitionHash && health.definitionVersion === job.previousDefinitionVersion)
|
|
575
|
+
|| (health.definitionHash === expectedHash && health.definitionVersion === job.previousDefinitionVersion + 1))) {
|
|
576
|
+
throw new SourceGrowthNativeMismatchError('source job native rearm lineage changed');
|
|
577
|
+
}
|
|
578
|
+
automations.reconcileSystemExact({ owner: SOURCE_JOB_OWNER, automationId: job.automationId,
|
|
579
|
+
idempotencyKey: `${job.id}:rearm:${job.dispatchAt}`, desiredStatus: 'active', definition,
|
|
580
|
+
expectedDefinitionHash: job.previousDefinitionHash, expectedVersion: job.previousDefinitionVersion });
|
|
581
|
+
const activated = automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
582
|
+
if (activated.definitionHash !== expectedHash || activated.definitionVersion !== job.previousDefinitionVersion + 1
|
|
583
|
+
|| activated.automationStatus !== 'active')
|
|
584
|
+
throw new SourceGrowthNativeMismatchError('source job exact native rearm was not active');
|
|
585
|
+
this.assertOwner(job);
|
|
586
|
+
this.options.store.bindSourceJobDefinition({ id: job.id, revision: job.revision, definitionHash: expectedHash });
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
336
589
|
if (job.definitionHash === undefined) {
|
|
337
|
-
|
|
590
|
+
const prior = job.previousDefinitionHash;
|
|
591
|
+
const existing = (() => {
|
|
592
|
+
try {
|
|
593
|
+
return automations.inspectSystemOwnedActivation({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
594
|
+
}
|
|
595
|
+
catch {
|
|
596
|
+
return undefined;
|
|
597
|
+
}
|
|
598
|
+
})();
|
|
599
|
+
if (existing !== undefined && (existing.activationNonce !== job.intentDigest || existing.ownerRouteId !== this.options.config.ownerRouteId
|
|
600
|
+
|| (existing.definitionHash !== expectedHash && existing.definitionHash !== prior))) {
|
|
601
|
+
throw new Error('source job native definition differs from persisted rearm');
|
|
602
|
+
}
|
|
603
|
+
automations.reconcileSystem({ owner: SOURCE_JOB_OWNER, automationId: job.automationId,
|
|
604
|
+
idempotencyKey: `${job.id}:prepare:${job.dispatchAt ?? job.createdAt + 1000}`, desiredStatus: 'paused', definition });
|
|
338
605
|
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
|
|
606
|
+
if (registered === undefined || registered.activationNonce !== job.intentDigest || registered.ownerRouteId !== this.options.config.ownerRouteId
|
|
607
|
+
|| registered.definitionHash !== expectedHash)
|
|
340
608
|
throw new Error('source job registration binding mismatch');
|
|
341
609
|
job = this.options.store.bindSourceJobDefinition({ id: job.id, revision: job.revision, definitionHash: registered.definitionHash });
|
|
342
610
|
}
|
|
343
611
|
const current = automations.inspectSystemOwnedActivation({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
344
612
|
if (current === undefined || current.definitionHash !== job.definitionHash || current.activationNonce !== job.intentDigest || current.ownerRouteId !== this.options.config.ownerRouteId)
|
|
345
613
|
throw new Error('source job automation changed');
|
|
346
|
-
automations.
|
|
614
|
+
const health = automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
615
|
+
if (health.definitionHash !== job.definitionHash)
|
|
616
|
+
throw new Error('source job native definition changed');
|
|
617
|
+
automations.reconcileSystem({ owner: SOURCE_JOB_OWNER, automationId: job.automationId,
|
|
618
|
+
idempotencyKey: `${job.id}:activate:v${health.definitionVersion}`, desiredStatus: 'active', definition });
|
|
619
|
+
const activated = automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
620
|
+
if (activated.definitionHash !== job.definitionHash || activated.automationStatus !== 'active')
|
|
621
|
+
throw new Error('source job native activation did not become active');
|
|
347
622
|
}
|
|
348
623
|
hasContinuations() {
|
|
349
|
-
return !!(this.options.approvePrepared || this.options.releasePrepared || this.options.advanceReleased || this.options.adoptReleased);
|
|
624
|
+
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
625
|
}
|
|
351
626
|
continuationJobs() {
|
|
352
627
|
if (!this.hasContinuations())
|
|
353
628
|
return [];
|
|
354
|
-
return this.options.store.listPreparedSourceApprovalJobs(this.options.releasePrepared !== undefined, this.options.advanceReleased !== undefined, this.options.adoptReleased !== undefined)
|
|
355
|
-
.filter(job => this.eligibleContinuation(job));
|
|
629
|
+
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)
|
|
630
|
+
.filter(job => this.modeAllowed(job.intent.mode) && this.eligibleContinuation(job));
|
|
356
631
|
}
|
|
357
632
|
/** Admission reads durable state and validates the current owner; it never invokes continuation approval or release hooks. */
|
|
358
633
|
eligibleContinuation(job) {
|
|
@@ -365,8 +640,50 @@ export class SourceJobRuntime {
|
|
|
365
640
|
catch {
|
|
366
641
|
return false;
|
|
367
642
|
}
|
|
643
|
+
if (plan.mode === 'prepared-revise') {
|
|
644
|
+
if (job.intent.mode !== 'revise-created' || plan.status !== 'pending-approval' || !plan.sourceRevision?.growthRun.revisionAcceptance)
|
|
645
|
+
return false;
|
|
646
|
+
const status = this.options.store.getRevisionVerificationStatus(plan.id);
|
|
647
|
+
const verify = status === undefined && this.options.verifyPreparedRevision !== undefined;
|
|
648
|
+
const regression = status === 'verified' && plan.sourceRevision.growthRun.revisionRegressionAcceptance !== undefined
|
|
649
|
+
&& this.options.store.getRevisionRegressionStatus(plan.id) === undefined && this.options.verifyPreparedRevisionRegression !== undefined;
|
|
650
|
+
if (!verify && !regression)
|
|
651
|
+
return false;
|
|
652
|
+
try {
|
|
653
|
+
this.assertOwner(job);
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
return plan.expiresAt > Date.now();
|
|
659
|
+
}
|
|
660
|
+
if (plan.mode === 'prepared-create') {
|
|
661
|
+
if (job.intent.mode !== 'create' || plan.status !== 'pending-approval'
|
|
662
|
+
|| !job.intent.creation?.growthRun?.creationAcceptance || !plan.creation?.growthRun?.creationAcceptance)
|
|
663
|
+
return false;
|
|
664
|
+
const verification = this.options.store.getCreationVerificationStatus(plan.id);
|
|
665
|
+
const verify = verification === undefined && this.options.verifyPreparedCreation !== undefined;
|
|
666
|
+
const adopt = verification === 'verified' && this.options.adoptVerifiedCreation !== undefined
|
|
667
|
+
&& this.options.creationAdoptionEligible?.(plan.id) === true;
|
|
668
|
+
if (!verify && !adopt)
|
|
669
|
+
return false;
|
|
670
|
+
if (job.intent.authority.digest !== this.authorityDigest || job.intent.authority.id !== this.options.config.authorityId)
|
|
671
|
+
return false;
|
|
672
|
+
try {
|
|
673
|
+
this.assertOwner(job);
|
|
674
|
+
this.withGapSource(job.intent.gapId, job.intent.owner, () => {
|
|
675
|
+
this.assertGrowthRun(job.intent.creation?.growthRun, job.intent.mode, job.intent.gapId, job.intent.owner, false);
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
return plan.expiresAt > Date.now();
|
|
682
|
+
}
|
|
683
|
+
if (plan.mode !== 'modify' || job.intent.mode === 'create')
|
|
684
|
+
return false;
|
|
368
685
|
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));
|
|
686
|
+
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
687
|
if (recovery)
|
|
371
688
|
return this.options.adoptReleased !== undefined;
|
|
372
689
|
if (job.intent.authority.digest !== this.authorityDigest || job.intent.authority.id !== this.options.config.authorityId)
|
|
@@ -390,12 +707,12 @@ export class SourceJobRuntime {
|
|
|
390
707
|
continuationTimeoutMs() {
|
|
391
708
|
const release = this.options.releaseTimeoutMs ?? 40_000, adoption = this.options.adoptionTimeoutMs ?? 60_000;
|
|
392
709
|
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);
|
|
710
|
+
return Math.max(this.options.verifyPreparedCreation ? 1_800_000 : 10_000, needed);
|
|
394
711
|
}
|
|
395
712
|
continuationSignalTimeoutMs(job) {
|
|
396
713
|
if (job.planId) {
|
|
397
714
|
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)))
|
|
715
|
+
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
716
|
return this.continuationTimeoutMs();
|
|
400
717
|
}
|
|
401
718
|
return Math.max(1, Math.min(this.continuationTimeoutMs(), this.options.config.expiresAt - Date.now()));
|
|
@@ -461,8 +778,12 @@ export class SourceJobRuntime {
|
|
|
461
778
|
try {
|
|
462
779
|
this.schedule(job);
|
|
463
780
|
}
|
|
464
|
-
catch {
|
|
781
|
+
catch (error) {
|
|
782
|
+
if (error instanceof SourceGrowthRunUnavailableError)
|
|
783
|
+
return;
|
|
465
784
|
const current = this.options.store.getSourceJob(job.id);
|
|
785
|
+
if (current?.previousDefinitionHash !== undefined && !(error instanceof SourceGrowthNativeMismatchError))
|
|
786
|
+
return;
|
|
466
787
|
if (current?.status === 'queued')
|
|
467
788
|
this.options.store.settleSourceJob({ id: current.id, revision: current.revision, status: 'failed', failureCode: 'source-job-registration-rejected' });
|
|
468
789
|
}
|
|
@@ -472,8 +793,45 @@ export class SourceJobRuntime {
|
|
|
472
793
|
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
794
|
if (job === undefined || job.status !== 'queued')
|
|
474
795
|
return failure(job?.status === 'unknown' || job?.status === 'running');
|
|
796
|
+
// A stale occurrence from before a persisted rearm can never claim this
|
|
797
|
+
// job. It has acquired no source resource, so leave the queued CAS intact.
|
|
798
|
+
if ((job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) !== undefined && input.definitionHash !== job.definitionHash) {
|
|
799
|
+
const expectedPending = job.previousDefinitionHash === undefined ? undefined : automationDefinitionDigest(this.definition(job));
|
|
800
|
+
if (expectedPending === input.definitionHash && input.executionMode === 'production'
|
|
801
|
+
&& input.activationNonce === job.intentDigest && input.catalogDigest === CATALOG
|
|
802
|
+
&& input.ownerRouteId === job.intent.owner.authorityId && input.principal === job.intent.owner.principalId
|
|
803
|
+
&& input.targetScope.workspace === job.intent.owner.workspace && input.targetScope.preset === job.intent.owner.agentPreset) {
|
|
804
|
+
try {
|
|
805
|
+
// The new native occurrence reached us before its cross-ledger DB
|
|
806
|
+
// acknowledgement. It has not claimed source resources. Bind the
|
|
807
|
+
// exact native receipt, then persist another one-shot before this
|
|
808
|
+
// occurrence can be considered consumed.
|
|
809
|
+
this.schedule(job);
|
|
810
|
+
const rebound = this.options.store.getSourceJob(job.id);
|
|
811
|
+
if (rebound?.status === 'queued' && rebound.definitionHash === input.definitionHash && rebound.occurrenceId === undefined) {
|
|
812
|
+
const health = this.options.ports.automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
|
|
813
|
+
if (health.definitionHash !== rebound.definitionHash)
|
|
814
|
+
throw new Error('source job premature occurrence native binding changed');
|
|
815
|
+
const pending = this.withGapSource(rebound.intent.gapId, rebound.intent.owner, () => {
|
|
816
|
+
this.assertGrowthRun(rebound.intent.creation?.growthRun ?? rebound.intent.revision?.growthRun, rebound.intent.mode, rebound.intent.gapId, rebound.intent.owner, false);
|
|
817
|
+
return this.options.store.rearmQueuedGrowthSourceJob({ id: rebound.id, revision: rebound.revision,
|
|
818
|
+
priorDefinitionHash: rebound.definitionHash, priorDefinitionVersion: health.definitionVersion, dispatchAt: Date.now() + 1000 });
|
|
819
|
+
});
|
|
820
|
+
this.schedule(pending);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
catch { /* persisted queued state remains fail-closed for exact recovery */ }
|
|
824
|
+
}
|
|
825
|
+
return failure(false);
|
|
826
|
+
}
|
|
475
827
|
let claimed = false;
|
|
476
|
-
const
|
|
828
|
+
const growthController = (job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) === undefined ? undefined : new AbortController();
|
|
829
|
+
if (growthController)
|
|
830
|
+
this.growthControllers.add(growthController);
|
|
831
|
+
const signal = AbortSignal.any([input.signal, this.abort.signal,
|
|
832
|
+
...(growthController ? [growthController.signal] : []),
|
|
833
|
+
AbortSignal.timeout(Math.max(1, Math.min(job.intent.build.timeoutMs + 120_000 + (this.options.verifyPreparedCreation ? 1_800_000 : 0)
|
|
834
|
+
+ (this.options.advanceReleased ? this.options.releaseTimeoutMs ?? 40_000 : 0) + (this.options.adoptReleased ? this.options.adoptionTimeoutMs ?? 60_000 : 0), job.expiresAt - Date.now())))]);
|
|
477
835
|
try {
|
|
478
836
|
this.assertOwner(job);
|
|
479
837
|
signal.throwIfAborted();
|
|
@@ -499,7 +857,9 @@ export class SourceJobRuntime {
|
|
|
499
857
|
await this.options.prepare(owned, signal, assertCurrent);
|
|
500
858
|
if (this.options.store.getSourceJob(owned.id)?.status !== 'prepared')
|
|
501
859
|
throw new Error('source job completion was not committed');
|
|
502
|
-
if ((
|
|
860
|
+
if ((owned.intent.mode === 'create' ? this.options.verifyPreparedCreation !== undefined
|
|
861
|
+
: owned.intent.mode === 'revise-created' ? this.options.verifyPreparedRevision !== undefined : !!(this.options.approvePrepared || this.options.releasePrepared || this.options.advanceReleased || this.options.adoptReleased))
|
|
862
|
+
&& this.options.store.getOwnerTaskFailureReference(owned.intent.gapId)) {
|
|
503
863
|
this.reconcileContinuations();
|
|
504
864
|
this.assertOwner(owned);
|
|
505
865
|
await this.continuePrepared(this.options.store.getSourceJob(owned.id), signal);
|
|
@@ -507,13 +867,22 @@ export class SourceJobRuntime {
|
|
|
507
867
|
}
|
|
508
868
|
return { outcome: 'succeeded', failureClass: 'none', failurePhase: 'none', failureCode: 'none', sideEffectState: 'possible', retryability: 'unsafe' };
|
|
509
869
|
}
|
|
510
|
-
catch {
|
|
870
|
+
catch (error) {
|
|
511
871
|
const current = this.options.store.getSourceJob(job.id);
|
|
872
|
+
if (!claimed && error instanceof SourceGrowthRunUnavailableError)
|
|
873
|
+
return failure(false);
|
|
874
|
+
if (!claimed && current?.status === 'queued'
|
|
875
|
+
&& (current.revision !== job.revision || current.definitionHash !== input.definitionHash))
|
|
876
|
+
return failure(false);
|
|
512
877
|
if (current?.status === 'queued' || (current?.status === 'running' && current.occurrenceId === input.occurrenceId)) {
|
|
513
878
|
this.options.store.settleSourceJob({ id: current.id, revision: current.revision, status: claimed ? 'unknown' : 'failed', failureCode: claimed ? 'source-job-interrupted' : 'source-job-preflight-rejected' });
|
|
514
879
|
}
|
|
515
880
|
return failure(claimed);
|
|
516
881
|
}
|
|
882
|
+
finally {
|
|
883
|
+
if (growthController)
|
|
884
|
+
this.growthControllers.delete(growthController);
|
|
885
|
+
}
|
|
517
886
|
}
|
|
518
887
|
/** Explicit Host action. Unknown keeps its capacity until exact cleanup succeeds. */
|
|
519
888
|
reconcileUnknown(input) {
|
|
@@ -525,7 +894,15 @@ export class SourceJobRuntime {
|
|
|
525
894
|
const trust = await this.options.trust();
|
|
526
895
|
if (controlPlaneDigest(trust) !== job.intent.trustDigest)
|
|
527
896
|
throw new Error('source job trust changed');
|
|
528
|
-
|
|
897
|
+
if ('backend' in job.intent.build) {
|
|
898
|
+
if (job.intent.nativeResource?.backend !== 'macos-seatbelt' || job.intent.nativeResource.key !== job.id
|
|
899
|
+
|| await this.options.inspectNativeQuiescence?.(job.id) !== 'quiescent') {
|
|
900
|
+
throw new Error('native source job has no valid durable quiescence receipt');
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
await removeSourceJobContainer(job.intent.build, { id: job.id, containerName: job.intent.containerName });
|
|
905
|
+
}
|
|
529
906
|
await removeSourceJobWorktree({ stateRoot: join(this.options.statePath, 'source-worktrees'), repository: job.intent.repository, worktree: job.intent.worktree, baseCommit: job.intent.baseCommit, environment: inheritedEnvironment(trust) });
|
|
530
907
|
this.inspect(input);
|
|
531
908
|
return projection(this.options.store.settleSourceJob({ id: job.id, revision: job.revision, status: 'failed', failureCode: 'source-job-resources-reconciled' }));
|