@dsh-enhanced/assistant-automations 0.1.24 → 0.1.31
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 +18 -5
- package/lib/acceptance.d.ts +52 -0
- package/lib/acceptance.d.ts.map +1 -0
- package/lib/acceptance.js +2 -0
- package/lib/acceptance.js.map +1 -0
- package/lib/coordinator.d.ts +5 -0
- package/lib/coordinator.d.ts.map +1 -1
- package/lib/coordinator.js +21 -0
- package/lib/coordinator.js.map +1 -1
- package/lib/external-event.d.ts +32 -0
- package/lib/external-event.d.ts.map +1 -0
- package/lib/external-event.js +114 -0
- package/lib/external-event.js.map +1 -0
- package/lib/growth.d.ts +10 -0
- package/lib/growth.d.ts.map +1 -1
- package/lib/growth.js +49 -16
- package/lib/growth.js.map +1 -1
- package/lib/index.d.ts +6 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -1
- package/lib/operator.d.ts +68 -2
- package/lib/operator.d.ts.map +1 -1
- package/lib/operator.js +922 -6
- package/lib/operator.js.map +1 -1
- package/lib/preparation.d.ts +52 -0
- package/lib/preparation.d.ts.map +1 -0
- package/lib/preparation.js +191 -0
- package/lib/preparation.js.map +1 -0
- package/lib/runner.d.ts +29 -0
- package/lib/runner.d.ts.map +1 -1
- package/lib/runner.js +72 -2
- package/lib/runner.js.map +1 -1
- package/lib/service.d.ts +37 -0
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +269 -60
- package/lib/service.js.map +1 -1
- package/lib/sqlite.d.ts +1 -1
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +77 -3
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +55 -0
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +233 -14
- package/lib/store.js.map +1 -1
- package/lib/types.d.ts +3 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +29 -23
package/lib/service.js
CHANGED
|
@@ -10,8 +10,10 @@ import { GROWTH_AUTOMATION_OWNER, GrowthAutomationStore, } from './growth.js';
|
|
|
10
10
|
import { AutomationCoordinator, } from './coordinator.js';
|
|
11
11
|
import { AutomationProposalManager, AutomationProposalStore } from './proposals.js';
|
|
12
12
|
import { DshAutomationRunner, HostAutomationRunner, RoutedAutomationRunner } from './runner.js';
|
|
13
|
+
import { AutomationPreparationRunner } from './preparation.js';
|
|
13
14
|
import { AutomationStore } from './store.js';
|
|
14
15
|
import { registerAutomationTools } from './tools.js';
|
|
16
|
+
import { parseExternalEventEnvelope } from './external-event.js';
|
|
15
17
|
import { isHostAutomationDefinition } from './types.js';
|
|
16
18
|
function registrationOwnedByEvaluation(registration) {
|
|
17
19
|
const candidate = registration;
|
|
@@ -174,11 +176,16 @@ export class AssistantAutomationsService extends Service {
|
|
|
174
176
|
growthStore;
|
|
175
177
|
proposals;
|
|
176
178
|
policy;
|
|
179
|
+
preparationRunner;
|
|
177
180
|
coordinator;
|
|
178
181
|
hostExecutors = new HostAutomationExecutorRegistry();
|
|
179
182
|
config;
|
|
180
183
|
evaluationProducerGeneration = `assistant-automations:${randomUUID()}`;
|
|
181
184
|
presentationProducerGeneration = `assistant-automations-presentation:${randomUUID()}`;
|
|
185
|
+
acceptanceProducerGeneration = `assistant-automations-acceptance:${randomUUID()}`;
|
|
186
|
+
acceptanceSink;
|
|
187
|
+
/** Once a live verifier requires acceptance, a transient detach must not reopen prompt submission. */
|
|
188
|
+
acceptanceRequired = false;
|
|
182
189
|
evaluationSink;
|
|
183
190
|
presentationSink;
|
|
184
191
|
approvalDelivery;
|
|
@@ -205,6 +212,7 @@ export class AssistantAutomationsService extends Service {
|
|
|
205
212
|
throw new Error('assistant-automations: assistantPolicy service is required');
|
|
206
213
|
this.config = config;
|
|
207
214
|
this.policy = policy;
|
|
215
|
+
this.preparationRunner = new AutomationPreparationRunner(ctx, policy);
|
|
208
216
|
const initialDelivery = ctx.get('assistantDelivery');
|
|
209
217
|
this.approvalDelivery = initialDelivery;
|
|
210
218
|
this.workflowTemplateResolver = initialDelivery;
|
|
@@ -216,10 +224,13 @@ export class AssistantAutomationsService extends Service {
|
|
|
216
224
|
const artifacts = new AutomationArtifactStore({ rootPath: config.runsPath, maxBytes: config.maxArtifactBytes });
|
|
217
225
|
const runner = new PolicyBoundRunner(policy, new RoutedAutomationRunner(new DshAutomationRunner(ctx, policy, {
|
|
218
226
|
allowUnbudgetedExecution: config.allowUnbudgetedExecution,
|
|
227
|
+
prepareAcceptance: input => this.prepareTaskAcceptance(input),
|
|
228
|
+
acceptanceEnabled: () => this.acceptanceSink !== undefined || this.acceptanceRequired,
|
|
219
229
|
}), new HostAutomationRunner(this.hostExecutors, policy, {
|
|
220
230
|
allowUnbudgetedExecution: config.allowUnbudgetedExecution,
|
|
221
231
|
})));
|
|
222
232
|
this.coordinator = new AutomationCoordinator({
|
|
233
|
+
beforeDispatch: () => this.revalidatePromotedWorkflows(true),
|
|
223
234
|
store: this.store,
|
|
224
235
|
artifacts,
|
|
225
236
|
runner,
|
|
@@ -252,9 +263,15 @@ export class AssistantAutomationsService extends Service {
|
|
|
252
263
|
ctx.inject(['assistantEvaluation'], evaluationCtx => {
|
|
253
264
|
const evaluation = evaluationCtx.get('assistantEvaluation');
|
|
254
265
|
this.evaluation = evaluation;
|
|
266
|
+
const unsubscribe = evaluation.onTrustedTaskChange?.(() => this.revalidatePromotedWorkflows());
|
|
267
|
+
this.revalidatePromotedWorkflows();
|
|
255
268
|
return () => {
|
|
256
|
-
|
|
269
|
+
unsubscribe?.();
|
|
270
|
+
if (this.evaluation === evaluation) {
|
|
257
271
|
this.evaluation = undefined;
|
|
272
|
+
if (this.active)
|
|
273
|
+
this.revalidatePromotedWorkflows();
|
|
274
|
+
}
|
|
258
275
|
};
|
|
259
276
|
});
|
|
260
277
|
// Optional: when a learning plugin is composed, finished runs become evidence.
|
|
@@ -289,6 +306,7 @@ export class AssistantAutomationsService extends Service {
|
|
|
289
306
|
ctx.effect(() => async () => {
|
|
290
307
|
this.active = false;
|
|
291
308
|
this.evaluationSink = undefined;
|
|
309
|
+
this.acceptanceSink = undefined;
|
|
292
310
|
this.presentationSink?.dispose();
|
|
293
311
|
this.coordinator.setEvaluationRecorder(undefined);
|
|
294
312
|
await this.coordinator.stop();
|
|
@@ -301,10 +319,86 @@ export class AssistantAutomationsService extends Service {
|
|
|
301
319
|
this.assertActive();
|
|
302
320
|
return this.evaluationProducerGeneration;
|
|
303
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Generate a bounded, unverified Proactive draft in a fresh Session. The
|
|
324
|
+
* caller owns the durable decision claim and result persistence.
|
|
325
|
+
*/
|
|
326
|
+
runPreparation(input, signal, assertCurrent) {
|
|
327
|
+
this.assertActive();
|
|
328
|
+
return this.preparationRunner.run(input, signal, assertCurrent);
|
|
329
|
+
}
|
|
304
330
|
trustedDeliveryPresentationProducerGeneration() {
|
|
305
331
|
this.assertActive();
|
|
306
332
|
return this.presentationProducerGeneration;
|
|
307
333
|
}
|
|
334
|
+
/** Private verifier producer generation; invalidated with this service. */
|
|
335
|
+
trustedAcceptanceProducerGeneration() {
|
|
336
|
+
this.assertActive();
|
|
337
|
+
return this.acceptanceProducerGeneration;
|
|
338
|
+
}
|
|
339
|
+
registerTaskAcceptanceSink(registration) {
|
|
340
|
+
this.assertActive();
|
|
341
|
+
const verifier = this.ctx.get('assistantVerifier', false);
|
|
342
|
+
if (registration.protocol !== 'assistant-verifier/host-producer/v1'
|
|
343
|
+
|| registration.generation !== this.acceptanceProducerGeneration
|
|
344
|
+
|| typeof registration.prepare !== 'function' || typeof registration.completed !== 'function'
|
|
345
|
+
|| typeof verifier?.ownsTaskAcceptanceRegistration !== 'function'
|
|
346
|
+
|| !verifier.ownsTaskAcceptanceRegistration(registration)) {
|
|
347
|
+
throw new AssistantAutomationsError('runtime-conflict', 'task acceptance registration is invalid');
|
|
348
|
+
}
|
|
349
|
+
if (this.acceptanceSink !== undefined) {
|
|
350
|
+
if (this.acceptanceSink.registration === registration) {
|
|
351
|
+
const token = this.acceptanceSink.token;
|
|
352
|
+
return () => { if (this.acceptanceSink?.token === token)
|
|
353
|
+
this.acceptanceSink = undefined; };
|
|
354
|
+
}
|
|
355
|
+
throw new AssistantAutomationsError('runtime-conflict', 'task acceptance sink is already registered');
|
|
356
|
+
}
|
|
357
|
+
const token = Symbol('assistant-automations.task-acceptance');
|
|
358
|
+
this.acceptanceSink = Object.freeze({ token, registration });
|
|
359
|
+
if (registration.requiresAcceptance === true)
|
|
360
|
+
this.acceptanceRequired = true;
|
|
361
|
+
let live = true;
|
|
362
|
+
return () => {
|
|
363
|
+
if (!live)
|
|
364
|
+
return;
|
|
365
|
+
live = false;
|
|
366
|
+
if (this.acceptanceSink?.token === token)
|
|
367
|
+
this.acceptanceSink = undefined;
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
async inspectAcceptedExecution(contract) {
|
|
371
|
+
this.assertActive();
|
|
372
|
+
return this.store.inspectAcceptedExecution(contract);
|
|
373
|
+
}
|
|
374
|
+
prepareTaskAcceptance(input) {
|
|
375
|
+
const sink = this.acceptanceSink;
|
|
376
|
+
if (sink === undefined) {
|
|
377
|
+
if (this.acceptanceRequired) {
|
|
378
|
+
throw new AssistantAutomationsError('runtime-conflict', 'required task acceptance verifier is unavailable');
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (input.owner === undefined || input.binding === undefined) {
|
|
383
|
+
if (this.acceptanceRequired) {
|
|
384
|
+
throw new AssistantAutomationsError('missing-approval-route', 'required task acceptance lacks an authenticated Delivery owner');
|
|
385
|
+
}
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const handle = sink.registration.prepare(Object.freeze({ scope: Object.freeze(input.scope), owner: input.owner,
|
|
389
|
+
task: Object.freeze({ kind: 'automation-run', ref: `run-${input.taskId}` }), objective: input.objective, }));
|
|
390
|
+
if (handle === null) {
|
|
391
|
+
if (this.acceptanceRequired) {
|
|
392
|
+
throw new AssistantAutomationsError('runtime-conflict', 'required task acceptance profile is unavailable');
|
|
393
|
+
}
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
this.store.bindTaskAcceptance({ taskId: input.taskId, contractId: handle.contractId, contractDigest: handle.contractDigest,
|
|
397
|
+
scope: input.scope, owner: input.owner, bindingId: input.binding.id, bindingVersion: input.binding.version,
|
|
398
|
+
bindingGeneration: input.binding.generation,
|
|
399
|
+
dispatchedAt: Date.now(),
|
|
400
|
+
});
|
|
401
|
+
}
|
|
308
402
|
/**
|
|
309
403
|
* Delivery creates this publisher and can revoke it on either service's
|
|
310
404
|
* reload. Automations retains it only as a private coordinator adapter; no
|
|
@@ -857,50 +951,31 @@ export class AssistantAutomationsService extends Service {
|
|
|
857
951
|
this.assertActive();
|
|
858
952
|
const input = validateGrowthCanaryInspectionRequest(inputValue);
|
|
859
953
|
const replay = this.growthStore.beginOperation('canary-inspection', input);
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
954
|
+
const artifact = replay === undefined
|
|
955
|
+
? this.growthStore.requireArtifact(input)
|
|
956
|
+
: this.growthStore.requireCanaryInspectionArtifact(input);
|
|
957
|
+
if (replay !== undefined) {
|
|
958
|
+
const receipt = validateGrowthCanaryInspectionReceipt(replay, input);
|
|
959
|
+
if (receipt.outcome === 'passed')
|
|
960
|
+
this.requireCurrentCanaryProof(artifact);
|
|
961
|
+
return receipt;
|
|
962
|
+
}
|
|
863
963
|
const run = artifact.canaryRunId === undefined ? undefined : this.store.getRun(artifact.canaryRunId);
|
|
864
964
|
if (artifact.canaryTaskId === undefined || artifact.canaryRunId === undefined || run === undefined
|
|
865
965
|
|| run.taskId !== artifact.canaryTaskId || run.executionMode !== 'production') {
|
|
866
966
|
throw new AssistantAutomationsError('runtime-conflict', 'canary inspection has no exact exposure proof');
|
|
867
967
|
}
|
|
868
|
-
|
|
869
|
-
const
|
|
870
|
-
if (evaluation !== undefined && run.status === 'succeeded') {
|
|
871
|
-
const scope = canonicalEvaluationHostScope({ workspace: artifact.workspace, preset: artifact.preset });
|
|
872
|
-
const matches = evaluation.query({
|
|
873
|
-
scope,
|
|
874
|
-
situation: `automation:${artifact.automationId}`,
|
|
875
|
-
objectiveStatus: 'achieved',
|
|
876
|
-
trust: 'trusted',
|
|
877
|
-
limit: 100,
|
|
878
|
-
}).filter(outcome => outcome.evidence.some(entry => entry.kind === 'automation-run' && entry.ref === run.id));
|
|
879
|
-
if (matches.length === 1) {
|
|
880
|
-
proof = evaluation.getTrustedOutcome({ scope, outcomeId: matches[0].id });
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
const passed = proof !== undefined && proof.trust === 'trusted'
|
|
884
|
-
&& proof.objectiveStatus === 'achieved'
|
|
885
|
-
&& proof.evidence.some(entry => entry.kind === 'automation-run' && entry.ref === run.id);
|
|
968
|
+
const proof = run.status === 'succeeded' ? this.currentCanaryProof(artifact) : undefined;
|
|
969
|
+
const passed = proof !== undefined;
|
|
886
970
|
const failed = run.status !== 'succeeded';
|
|
887
|
-
const evaluationDigest =
|
|
888
|
-
contract: 'assistant-automations-growth-evaluation-proof/v1',
|
|
889
|
-
id: proof.id,
|
|
890
|
-
scopeKey: proof.scopeKey,
|
|
891
|
-
situation: proof.situation,
|
|
892
|
-
objectiveStatus: proof.objectiveStatus,
|
|
893
|
-
trust: proof.trust,
|
|
894
|
-
evidence: proof.evidence,
|
|
895
|
-
occurredAt: proof.occurredAt,
|
|
896
|
-
evaluator: proof.evaluator,
|
|
897
|
-
});
|
|
971
|
+
const evaluationDigest = proof?.projection.digest;
|
|
898
972
|
if (passed) {
|
|
899
973
|
this.growthStore.recordCanaryEvaluation({
|
|
900
974
|
artifactId: artifact.artifactId,
|
|
901
975
|
runId: run.id,
|
|
902
|
-
evaluationId: proof.
|
|
976
|
+
evaluationId: proof.triggerOutcomeId,
|
|
903
977
|
evaluationDigest: evaluationDigest,
|
|
978
|
+
proof: proof,
|
|
904
979
|
});
|
|
905
980
|
}
|
|
906
981
|
const receipt = withGrowthPortReceiptDigest({
|
|
@@ -919,6 +994,110 @@ export class AssistantAutomationsService extends Service {
|
|
|
919
994
|
});
|
|
920
995
|
return validateGrowthCanaryInspectionReceipt(this.growthStore.completeOperation('canary-inspection', input, receipt), input);
|
|
921
996
|
}
|
|
997
|
+
currentCanaryProof(artifact) {
|
|
998
|
+
if (artifact.canaryRunId === undefined || this.evaluation === undefined
|
|
999
|
+
|| typeof this.evaluation.getTrustedAutomationRunLearningProjection !== 'function')
|
|
1000
|
+
return undefined;
|
|
1001
|
+
const scope = canonicalEvaluationHostScope({ workspace: artifact.workspace, preset: artifact.preset });
|
|
1002
|
+
const proof = this.evaluation.getTrustedAutomationRunLearningProjection({ scope, runId: artifact.canaryRunId });
|
|
1003
|
+
if (proof === undefined || proof.scope.workspace !== scope.workspace || proof.scope.preset !== scope.preset
|
|
1004
|
+
|| proof.situation !== `automation:${artifact.automationId}`
|
|
1005
|
+
|| proof.projection.subjectKind !== 'automation-run' || proof.projection.subjectRef !== artifact.canaryRunId
|
|
1006
|
+
|| proof.projection.disposition !== 'upsert' || proof.execution?.status !== 'succeeded'
|
|
1007
|
+
|| proof.objective?.status !== 'achieved')
|
|
1008
|
+
return undefined;
|
|
1009
|
+
return proof;
|
|
1010
|
+
}
|
|
1011
|
+
requireCurrentCanaryProof(artifact) {
|
|
1012
|
+
const current = this.currentCanaryProof(artifact);
|
|
1013
|
+
const saved = artifact.canaryEvaluationProof;
|
|
1014
|
+
// Legacy append-only proofs deliberately fail closed. Never upgrade an old
|
|
1015
|
+
// inspection receipt to a different canonical revision during promotion.
|
|
1016
|
+
if (current === undefined || saved === undefined
|
|
1017
|
+
|| saved.scopeKey !== current.scopeKey
|
|
1018
|
+
|| saved.projection.subjectKind !== current.projection.subjectKind
|
|
1019
|
+
|| saved.projection.subjectRef !== current.projection.subjectRef
|
|
1020
|
+
|| saved.projection.version !== current.projection.version
|
|
1021
|
+
|| saved.projection.digest !== current.projection.digest
|
|
1022
|
+
|| saved.projection.disposition !== 'upsert'
|
|
1023
|
+
|| artifact.canaryEvaluationDigest !== current.projection.digest) {
|
|
1024
|
+
throw new AssistantAutomationsError('runtime-conflict', 'canonical canary evidence changed or is unavailable');
|
|
1025
|
+
}
|
|
1026
|
+
return current;
|
|
1027
|
+
}
|
|
1028
|
+
workflowInvalidationListeners = new Set();
|
|
1029
|
+
onWorkflowEvidenceInvalidated(listener) {
|
|
1030
|
+
this.workflowInvalidationListeners.add(listener);
|
|
1031
|
+
for (const artifact of this.growthStore.listDeployedArtifacts()) {
|
|
1032
|
+
const version = this.growthStore.rollbackVersion(artifact.experimentId);
|
|
1033
|
+
if (artifact.state === 'rolled-back' && version !== undefined)
|
|
1034
|
+
listener(artifact.experimentId, { id: artifact.artifactId, version, digest: artifact.definitionHash });
|
|
1035
|
+
}
|
|
1036
|
+
return () => { this.workflowInvalidationListeners.delete(listener); };
|
|
1037
|
+
}
|
|
1038
|
+
/** Revalidate deployed proof before scheduling, on authoritative writes, and after restart. */
|
|
1039
|
+
revalidatePromotedWorkflows(strict = false) {
|
|
1040
|
+
let unsafe = false;
|
|
1041
|
+
for (let artifact of this.growthStore.listDeployedArtifacts()) {
|
|
1042
|
+
try {
|
|
1043
|
+
if (artifact.state === 'rolled-back') {
|
|
1044
|
+
const version = this.growthStore.rollbackVersion(artifact.experimentId);
|
|
1045
|
+
if (version !== undefined)
|
|
1046
|
+
for (const listener of this.workflowInvalidationListeners)
|
|
1047
|
+
listener(artifact.experimentId, { id: artifact.artifactId, version, digest: artifact.definitionHash });
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
if (artifact.state === 'canary-pending') {
|
|
1051
|
+
const live = this.store.get(artifact.automationId);
|
|
1052
|
+
if (live?.status !== 'active' || live.version !== artifact.definitionVersion + 1
|
|
1053
|
+
|| live.owner !== GROWTH_AUTOMATION_OWNER || this.store.getDefinitionHash(live.id) !== artifact.definitionHash)
|
|
1054
|
+
continue;
|
|
1055
|
+
if (this.currentCanaryProof(artifact) !== undefined)
|
|
1056
|
+
continue;
|
|
1057
|
+
// Recover activation whose acknowledgement was lost before examining its proof.
|
|
1058
|
+
const recoveredRequest = {
|
|
1059
|
+
contractVersion: 1, operationId: `${artifact.experimentId}:promotion`,
|
|
1060
|
+
experimentId: artifact.experimentId, candidateId: artifact.candidateId,
|
|
1061
|
+
candidateRevision: artifact.candidateRevision, candidateDigest: artifact.candidateDigest,
|
|
1062
|
+
artifactId: artifact.artifactId, artifactVersion: artifact.definitionVersion, artifactDigest: artifact.definitionHash,
|
|
1063
|
+
};
|
|
1064
|
+
artifact = this.growthStore.completePromotion({ automation: live, request: recoveredRequest,
|
|
1065
|
+
receipt: withGrowthPortReceiptDigest({ ...growthIdentity(recoveredRequest),
|
|
1066
|
+
artifactId: artifact.artifactId, artifactVersion: artifact.definitionVersion,
|
|
1067
|
+
artifactDigest: artifact.definitionHash, outcome: 'promoted',
|
|
1068
|
+
resultingArtifactVersion: live.version, resultingArtifactDigest: artifact.definitionHash, }),
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
if (artifact.state !== 'promoted')
|
|
1072
|
+
continue;
|
|
1073
|
+
// Deployed artifacts require current exact-run success, while a newer
|
|
1074
|
+
// positive owner judgement may strengthen an already successful proof.
|
|
1075
|
+
if (this.currentCanaryProof(artifact) !== undefined)
|
|
1076
|
+
continue;
|
|
1077
|
+
this.rollbackWorkflowAutomation({
|
|
1078
|
+
contractVersion: 1, operationId: `${artifact.experimentId}:rollback`,
|
|
1079
|
+
experimentId: artifact.experimentId, candidateId: artifact.candidateId,
|
|
1080
|
+
candidateRevision: artifact.candidateRevision, candidateDigest: artifact.candidateDigest,
|
|
1081
|
+
artifactId: artifact.artifactId, artifactVersion: artifact.definitionVersion,
|
|
1082
|
+
artifactDigest: artifact.definitionHash,
|
|
1083
|
+
});
|
|
1084
|
+
for (const listener of this.workflowInvalidationListeners)
|
|
1085
|
+
listener(artifact.experimentId, { id: artifact.artifactId, version: artifact.definitionVersion, digest: artifact.definitionHash });
|
|
1086
|
+
}
|
|
1087
|
+
catch {
|
|
1088
|
+
// Keep the durable artifact / pending rollback for retry. A different
|
|
1089
|
+
// deployed definition is unrelated and must not be paused or block it.
|
|
1090
|
+
const live = this.store.get(artifact.automationId);
|
|
1091
|
+
if (live?.status === 'active' && live.owner === GROWTH_AUTOMATION_OWNER
|
|
1092
|
+
&& this.store.getDefinitionHash(live.id) === artifact.definitionHash
|
|
1093
|
+
&& (live.version === artifact.definitionVersion
|
|
1094
|
+
|| (artifact.state === 'canary-pending' && live.version === artifact.definitionVersion + 1)))
|
|
1095
|
+
unsafe = true;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
if (strict && unsafe)
|
|
1099
|
+
throw new AssistantAutomationsError('runtime-conflict', 'exact deployed canary rollback must finish before dispatch');
|
|
1100
|
+
}
|
|
922
1101
|
promoteWorkflowAutomation(inputValue) {
|
|
923
1102
|
this.assertActive();
|
|
924
1103
|
const input = validateGrowthAutomationArtifactRequest(inputValue);
|
|
@@ -930,33 +1109,44 @@ export class AssistantAutomationsService extends Service {
|
|
|
930
1109
|
|| artifact.canaryEvaluationId === undefined || artifact.canaryEvaluationDigest === undefined) {
|
|
931
1110
|
throw new AssistantAutomationsError('runtime-conflict', 'promotion requires the exact canary artifact');
|
|
932
1111
|
}
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1112
|
+
const proof = this.requireCurrentCanaryProof(artifact);
|
|
1113
|
+
const fenced = this.evaluation.withTrustedCanonicalLearningWriterFence({
|
|
1114
|
+
scope: canonicalEvaluationHostScope({ workspace: artifact.workspace, preset: artifact.preset }),
|
|
1115
|
+
scopeWatermark: proof.scopeWatermark,
|
|
1116
|
+
evidence: [{ ...proof.projection, disposition: 'upsert' }],
|
|
1117
|
+
}, () => {
|
|
1118
|
+
const definition = this.requireLiveGrowthArtifact(artifact);
|
|
1119
|
+
if (definition === undefined || definition.owner !== GROWTH_AUTOMATION_OWNER
|
|
1120
|
+
|| this.store.getDefinitionHash(definition.id) !== input.artifactDigest
|
|
1121
|
+
|| !((definition.status === 'paused' && definition.version === input.artifactVersion)
|
|
1122
|
+
|| (definition.status === 'active' && definition.version === input.artifactVersion + 1))) {
|
|
1123
|
+
throw new AssistantAutomationsError('runtime-conflict', 'promotion artifact changed before exact CAS');
|
|
1124
|
+
}
|
|
1125
|
+
const promoted = definition.status === 'active'
|
|
1126
|
+
? definition
|
|
1127
|
+
: this.reconcileSystem({
|
|
1128
|
+
owner: GROWTH_AUTOMATION_OWNER,
|
|
1129
|
+
automationId: definition.id,
|
|
1130
|
+
idempotencyKey: boundedGrowthKey('growth-promote', input),
|
|
1131
|
+
desiredStatus: 'active',
|
|
1132
|
+
definition: definition.definition,
|
|
1133
|
+
});
|
|
1134
|
+
const receipt = withGrowthPortReceiptDigest({
|
|
1135
|
+
...growthIdentity(input),
|
|
1136
|
+
artifactId: input.artifactId,
|
|
1137
|
+
artifactVersion: input.artifactVersion,
|
|
1138
|
+
artifactDigest: input.artifactDigest,
|
|
1139
|
+
outcome: 'promoted',
|
|
1140
|
+
resultingArtifactVersion: promoted.version,
|
|
1141
|
+
resultingArtifactDigest: input.artifactDigest,
|
|
948
1142
|
});
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
...growthIdentity(input),
|
|
952
|
-
artifactId: input.artifactId,
|
|
953
|
-
artifactVersion: input.artifactVersion,
|
|
954
|
-
artifactDigest: input.artifactDigest,
|
|
955
|
-
outcome: 'promoted',
|
|
956
|
-
resultingArtifactVersion: stored.definitionVersion,
|
|
957
|
-
resultingArtifactDigest: stored.definitionHash,
|
|
1143
|
+
this.growthStore.completePromotion({ request: input, automation: promoted, receipt });
|
|
1144
|
+
return validateGrowthPromotionReceipt(this.growthStore.completeOperation('promotion', input, receipt), input);
|
|
958
1145
|
});
|
|
959
|
-
|
|
1146
|
+
if (!fenced.matched) {
|
|
1147
|
+
throw new AssistantAutomationsError('runtime-conflict', `canonical canary evidence fence: ${fenced.reason}`);
|
|
1148
|
+
}
|
|
1149
|
+
return fenced.value;
|
|
960
1150
|
}
|
|
961
1151
|
rollbackWorkflowAutomation(inputValue) {
|
|
962
1152
|
this.assertActive();
|
|
@@ -984,7 +1174,6 @@ export class AssistantAutomationsService extends Service {
|
|
|
984
1174
|
});
|
|
985
1175
|
automation = this.store.get(automation.id);
|
|
986
1176
|
}
|
|
987
|
-
this.growthStore.completeRollback({ request: input, automation });
|
|
988
1177
|
const receipt = withGrowthPortReceiptDigest({
|
|
989
1178
|
...growthIdentity(input),
|
|
990
1179
|
artifactId: input.artifactId,
|
|
@@ -992,6 +1181,7 @@ export class AssistantAutomationsService extends Service {
|
|
|
992
1181
|
artifactDigest: input.artifactDigest,
|
|
993
1182
|
outcome: 'rolled-back',
|
|
994
1183
|
});
|
|
1184
|
+
this.growthStore.completeRollback({ request: input, automation, receipt });
|
|
995
1185
|
return validateGrowthRollbackReceipt(this.growthStore.completeOperation('rollback', input, receipt), input);
|
|
996
1186
|
}
|
|
997
1187
|
propose(agent, input) {
|
|
@@ -1203,6 +1393,11 @@ export class AssistantAutomationsService extends Service {
|
|
|
1203
1393
|
}
|
|
1204
1394
|
ingestExternal(input) {
|
|
1205
1395
|
this.assertActive();
|
|
1396
|
+
const envelope = input.envelope === undefined ? undefined : parseExternalEventEnvelope(input.envelope);
|
|
1397
|
+
if (envelope !== undefined && (envelope.source.id !== input.sourceId || envelope.event.id !== input.eventId
|
|
1398
|
+
|| envelope.event.occurredAt !== input.occurredAt || envelope.target.automationId !== input.automationId)) {
|
|
1399
|
+
throw new AssistantAutomationsError('invalid-input', 'external event envelope does not bind this ingest input');
|
|
1400
|
+
}
|
|
1206
1401
|
const automation = this.store.get(input.automationId);
|
|
1207
1402
|
const decision = this.policy.authorize({
|
|
1208
1403
|
subject: {
|
|
@@ -1220,6 +1415,7 @@ export class AssistantAutomationsService extends Service {
|
|
|
1220
1415
|
automationId: input.automationId,
|
|
1221
1416
|
externalEventId: `${input.sourceId}:${input.eventId}`,
|
|
1222
1417
|
occurredAt: input.occurredAt,
|
|
1418
|
+
...(envelope === undefined ? {} : { envelope }),
|
|
1223
1419
|
});
|
|
1224
1420
|
}
|
|
1225
1421
|
reconcileSystem(input) {
|
|
@@ -1259,6 +1455,19 @@ export class AssistantAutomationsService extends Service {
|
|
|
1259
1455
|
this.assertActive();
|
|
1260
1456
|
return this.store.inspectSystemOwned(input);
|
|
1261
1457
|
}
|
|
1458
|
+
/** Host-only binding metadata for exact restart reconciliation; no prompt or task content. */
|
|
1459
|
+
inspectSystemOwnedActivation = (input) => {
|
|
1460
|
+
this.assertActive();
|
|
1461
|
+
const current = this.store.get(input.automationId);
|
|
1462
|
+
if (!current)
|
|
1463
|
+
return undefined;
|
|
1464
|
+
if (current.owner !== input.owner || current.definition.execution?.kind !== 'host')
|
|
1465
|
+
throw new AssistantAutomationsError('not-found', 'exact system-owned Host automation was not found');
|
|
1466
|
+
const definitionHash = this.store.getDefinitionHash(input.automationId);
|
|
1467
|
+
if (!definitionHash)
|
|
1468
|
+
throw new AssistantAutomationsError('not-found', 'system-owned definition hash is unavailable');
|
|
1469
|
+
return Object.freeze({ definitionHash, activationNonce: current.definition.execution.activationNonce, ownerRouteId: current.definition.execution.ownerRouteId });
|
|
1470
|
+
};
|
|
1262
1471
|
/** Host-only bounded identity inventory; definitions and scopes never leave the store. */
|
|
1263
1472
|
listSystemOwned(input) {
|
|
1264
1473
|
this.assertActive();
|