@dsh-enhanced/assistant-automations 0.1.7 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +37 -7
  2. package/lib/coordinator.d.ts +45 -5
  3. package/lib/coordinator.d.ts.map +1 -1
  4. package/lib/coordinator.js +418 -86
  5. package/lib/coordinator.js.map +1 -1
  6. package/lib/growth.d.ts +120 -0
  7. package/lib/growth.d.ts.map +1 -0
  8. package/lib/growth.js +383 -0
  9. package/lib/growth.js.map +1 -0
  10. package/lib/host-executors.d.ts +25 -0
  11. package/lib/host-executors.d.ts.map +1 -0
  12. package/lib/host-executors.js +181 -0
  13. package/lib/host-executors.js.map +1 -0
  14. package/lib/index.d.ts +1 -1
  15. package/lib/index.d.ts.map +1 -1
  16. package/lib/index.js +1 -1
  17. package/lib/index.js.map +1 -1
  18. package/lib/operator.d.ts +2 -0
  19. package/lib/operator.d.ts.map +1 -1
  20. package/lib/operator.js +6 -2
  21. package/lib/operator.js.map +1 -1
  22. package/lib/proposals.d.ts +2 -2
  23. package/lib/proposals.d.ts.map +1 -1
  24. package/lib/proposals.js +34 -22
  25. package/lib/proposals.js.map +1 -1
  26. package/lib/runner.d.ts +28 -2
  27. package/lib/runner.d.ts.map +1 -1
  28. package/lib/runner.js +524 -73
  29. package/lib/runner.js.map +1 -1
  30. package/lib/schedule.d.ts.map +1 -1
  31. package/lib/schedule.js +37 -9
  32. package/lib/schedule.js.map +1 -1
  33. package/lib/service.d.ts +109 -4
  34. package/lib/service.d.ts.map +1 -1
  35. package/lib/service.js +987 -16
  36. package/lib/service.js.map +1 -1
  37. package/lib/sqlite.d.ts +1 -1
  38. package/lib/sqlite.d.ts.map +1 -1
  39. package/lib/sqlite.js +369 -41
  40. package/lib/sqlite.js.map +1 -1
  41. package/lib/store.d.ts +175 -1
  42. package/lib/store.d.ts.map +1 -1
  43. package/lib/store.js +1889 -71
  44. package/lib/store.js.map +1 -1
  45. package/lib/tools.js +1 -1
  46. package/lib/tools.js.map +1 -1
  47. package/lib/types.d.ts +354 -11
  48. package/lib/types.d.ts.map +1 -1
  49. package/lib/types.js +13 -1
  50. package/lib/types.js.map +1 -1
  51. package/lib/version.d.ts +1 -1
  52. package/lib/version.d.ts.map +1 -1
  53. package/lib/version.js +1 -1
  54. package/lib/version.js.map +1 -1
  55. package/package.json +10 -8
package/lib/service.js CHANGED
@@ -2,12 +2,39 @@ import { isAbsolute, resolve } from 'node:path';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { Context, Service } from '@deepseek-ai/cordis';
4
4
  import Schema from '@deepseek-ai/schemastery';
5
+ import { canonicalEvaluationHostScope, } from '@dsh-enhanced/assistant-evaluation';
6
+ import { canonicalGrowthJson, growthObjectDigest, WORKFLOW_MODEL_TURN_CATALOG_ID, validateGrowthAutomationApprovalReceipt, validateGrowthAutomationApprovalRequest, validateGrowthAutomationArtifactRequest, validateGrowthAutomationProposalReceipt, validateGrowthAutomationProposalRequest, validateGrowthCanaryInspectionReceipt, validateGrowthCanaryInspectionRequest, validateGrowthCanaryReceipt, validateGrowthPromotionReceipt, validateGrowthReplayReceipt, validateGrowthRollbackReceipt, validateGrowthShadowReceipt, validateResolvedWorkflowAutomationTemplate, withGrowthPortReceiptDigest, } from '@dsh-enhanced/assistant-growth-contract';
5
7
  import { AutomationArtifactStore } from './artifacts.js';
8
+ import { HostAutomationExecutorRegistry } from './host-executors.js';
9
+ import { GROWTH_AUTOMATION_OWNER, GrowthAutomationStore, } from './growth.js';
6
10
  import { AutomationCoordinator, } from './coordinator.js';
7
11
  import { AutomationProposalManager, AutomationProposalStore } from './proposals.js';
8
- import { DshAutomationRunner } from './runner.js';
12
+ import { DshAutomationRunner, HostAutomationRunner, RoutedAutomationRunner } from './runner.js';
9
13
  import { AutomationStore } from './store.js';
10
14
  import { registerAutomationTools } from './tools.js';
15
+ import { isHostAutomationDefinition } from './types.js';
16
+ function registrationOwnedByEvaluation(registration) {
17
+ const candidate = registration;
18
+ try {
19
+ return typeof candidate.owner === 'object' && candidate.owner !== null
20
+ && typeof candidate.owner.ownsTrustedAutomationEvaluationRegistration === 'function'
21
+ && candidate.owner.ownsTrustedAutomationEvaluationRegistration(registration);
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ function registrationOwnedByDeliveryPresentation(registration) {
28
+ const candidate = registration;
29
+ try {
30
+ return typeof candidate.owner === 'object' && candidate.owner !== null
31
+ && typeof candidate.owner.ownsTrustedDeliveryPresentationRegistration === 'function'
32
+ && candidate.owner.ownsTrustedDeliveryPresentationRegistration(registration);
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
11
38
  export class AssistantAutomationsError extends Error {
12
39
  code;
13
40
  constructor(code, message) {
@@ -72,6 +99,32 @@ const configSchema = Schema.object({
72
99
  function policyError(decision) {
73
100
  return new AssistantAutomationsError('policy-denied', `assistant-automations policy denied operation: ${decision.reasonCode}`);
74
101
  }
102
+ function growthIdentity(input) {
103
+ return Object.freeze({
104
+ contractVersion: 1,
105
+ operationId: input.operationId,
106
+ experimentId: input.experimentId,
107
+ candidateId: input.candidateId,
108
+ candidateRevision: input.candidateRevision,
109
+ candidateDigest: input.candidateDigest,
110
+ });
111
+ }
112
+ function boundedGrowthKey(prefix, value) {
113
+ return `${prefix}:${growthObjectDigest(value)}`;
114
+ }
115
+ const growthEffectBlockerAttestation = Object.freeze({
116
+ contract: 'assistant-automations-effect-blocker/v1',
117
+ blockedEffects: Object.freeze(['delivery', 'tool-execution']),
118
+ implementationDigest: growthObjectDigest({
119
+ contract: 'assistant-automations-growth-effect-blocker-implementation/v1',
120
+ immutablePausedTaskAdmission: true,
121
+ executionMode: 'preview',
122
+ supportedWorkflowKind: 'model-turn-only',
123
+ validatedOrchestrationStep: WORKFLOW_MODEL_TURN_CATALOG_ID,
124
+ runnerToolAllowlist: [],
125
+ terminalDelivery: 'suppressed',
126
+ }),
127
+ });
75
128
  class PolicyBoundRunner {
76
129
  policy;
77
130
  delegate;
@@ -92,10 +145,23 @@ class PolicyBoundRunner {
92
145
  context: { initiator: 'background' },
93
146
  }, { idempotencyKey: `automation-execute:${input.occurrence.id}` });
94
147
  if (decision.effect !== 'allow') {
148
+ const host = isHostAutomationDefinition(input.automation.definition);
95
149
  return Promise.resolve({
96
150
  outcome: 'cancelled',
97
151
  output: `policy denied background execution: ${decision.reasonCode}`,
98
152
  usage: {},
153
+ diagnostic: Object.freeze({
154
+ schemaVersion: 1,
155
+ failureClass: 'policy',
156
+ failurePhase: 'preflight',
157
+ failureCode: 'policy-denied',
158
+ promptSubmissionState: host ? 'not-applicable' : 'not-submitted',
159
+ sideEffectState: 'none',
160
+ retryability: 'after-intervention',
161
+ budgetSettlementState: input.automation.definition.budgetId === undefined
162
+ ? 'not-required'
163
+ : 'not-reserved',
164
+ }),
99
165
  });
100
166
  }
101
167
  return this.delegate.run(input);
@@ -105,11 +171,19 @@ export class AssistantAutomationsService extends Service {
105
171
  static Config = configSchema;
106
172
  store;
107
173
  proposalStore;
174
+ growthStore;
108
175
  proposals;
109
176
  policy;
110
177
  coordinator;
178
+ hostExecutors = new HostAutomationExecutorRegistry();
111
179
  config;
180
+ evaluationProducerGeneration = `assistant-automations:${randomUUID()}`;
181
+ presentationProducerGeneration = `assistant-automations-presentation:${randomUUID()}`;
182
+ evaluationSink;
183
+ presentationSink;
112
184
  approvalDelivery;
185
+ workflowTemplateResolver;
186
+ evaluation;
113
187
  active = true;
114
188
  constructor(ctx, input) {
115
189
  super(ctx, 'assistantAutomations');
@@ -131,14 +205,20 @@ export class AssistantAutomationsService extends Service {
131
205
  throw new Error('assistant-automations: assistantPolicy service is required');
132
206
  this.config = config;
133
207
  this.policy = policy;
134
- this.approvalDelivery = ctx.get('assistantDelivery');
208
+ const initialDelivery = ctx.get('assistantDelivery');
209
+ this.approvalDelivery = initialDelivery;
210
+ this.workflowTemplateResolver = initialDelivery;
211
+ this.evaluation = ctx.get('assistantEvaluation');
135
212
  this.store = new AutomationStore({ path: config.databasePath });
136
213
  this.proposalStore = new AutomationProposalStore({ path: config.databasePath });
214
+ this.growthStore = new GrowthAutomationStore(config.databasePath);
137
215
  this.proposals = new AutomationProposalManager(this.store, this.proposalStore, policy);
138
216
  const artifacts = new AutomationArtifactStore({ rootPath: config.runsPath, maxBytes: config.maxArtifactBytes });
139
- const runner = new PolicyBoundRunner(policy, new DshAutomationRunner(ctx, policy, {
217
+ const runner = new PolicyBoundRunner(policy, new RoutedAutomationRunner(new DshAutomationRunner(ctx, policy, {
140
218
  allowUnbudgetedExecution: config.allowUnbudgetedExecution,
141
- }));
219
+ }), new HostAutomationRunner(this.hostExecutors, policy, {
220
+ allowUnbudgetedExecution: config.allowUnbudgetedExecution,
221
+ })));
142
222
  this.coordinator = new AutomationCoordinator({
143
223
  store: this.store,
144
224
  artifacts,
@@ -150,15 +230,31 @@ export class AssistantAutomationsService extends Service {
150
230
  maxCatchUp: config.maxCatchUp,
151
231
  maxConcurrency: config.maxConcurrency,
152
232
  tickIntervalMs: config.tickIntervalMs,
233
+ hostExecutors: this.hostExecutors,
153
234
  });
235
+ if (initialDelivery !== undefined) {
236
+ this.coordinator.setDeliveryDispatcher(this.deliveryDispatcher(initialDelivery));
237
+ }
154
238
  ctx.inject(['assistantDelivery'], deliveryCtx => {
155
239
  const delivery = deliveryCtx.get('assistantDelivery');
156
240
  this.approvalDelivery = delivery;
157
- this.coordinator.setDeliveryDispatcher(delivery);
241
+ this.workflowTemplateResolver = delivery;
242
+ this.coordinator.setDeliveryDispatcher(this.deliveryDispatcher(delivery));
158
243
  return () => {
159
- if (this.approvalDelivery === delivery)
244
+ if (this.approvalDelivery === delivery) {
160
245
  this.approvalDelivery = undefined;
161
- this.coordinator.setDeliveryDispatcher(undefined);
246
+ if (this.workflowTemplateResolver === delivery)
247
+ this.workflowTemplateResolver = undefined;
248
+ this.coordinator.setDeliveryDispatcher(undefined);
249
+ }
250
+ };
251
+ });
252
+ ctx.inject(['assistantEvaluation'], evaluationCtx => {
253
+ const evaluation = evaluationCtx.get('assistantEvaluation');
254
+ this.evaluation = evaluation;
255
+ return () => {
256
+ if (this.evaluation === evaluation)
257
+ this.evaluation = undefined;
162
258
  };
163
259
  });
164
260
  // Optional: when a learning plugin is composed, finished runs become evidence.
@@ -169,13 +265,9 @@ export class AssistantAutomationsService extends Service {
169
265
  this.coordinator.setOutcomeRecorder(recorder);
170
266
  return () => this.coordinator.setOutcomeRecorder(undefined);
171
267
  });
172
- // Evaluation has an independent durable outbox. Installing or restarting it
173
- // later cannot be hidden by Evolution's already-recorded evidence state.
174
- ctx.inject(['assistantEvaluation'], evaluationCtx => {
175
- const recorder = evaluationCtx.get('assistantEvaluation');
176
- this.coordinator.setEvaluationRecorder(recorder);
177
- return () => this.coordinator.setEvaluationRecorder(undefined);
178
- });
268
+ // Evaluation attaches in the opposite direction through the private
269
+ // producer capability below. Passing the public Evaluation service to the
270
+ // coordinator would let an ordinary caller self-assert `trust: trusted`.
179
271
  if (config.schedulerEnabled)
180
272
  this.coordinator.start();
181
273
  if (config.reconcileIntervalMs > 0) {
@@ -196,11 +288,712 @@ export class AssistantAutomationsService extends Service {
196
288
  ctx.inject(['tools'], toolsCtx => registerAutomationTools(toolsCtx, this));
197
289
  ctx.effect(() => async () => {
198
290
  this.active = false;
291
+ this.evaluationSink = undefined;
292
+ this.presentationSink?.dispose();
293
+ this.coordinator.setEvaluationRecorder(undefined);
199
294
  await this.coordinator.stop();
200
295
  this.proposalStore.close();
296
+ this.growthStore.close();
201
297
  this.store.close();
202
298
  }, 'assistant-automations.runtime');
203
299
  }
300
+ trustedEvaluationProducerGeneration() {
301
+ this.assertActive();
302
+ return this.evaluationProducerGeneration;
303
+ }
304
+ trustedDeliveryPresentationProducerGeneration() {
305
+ this.assertActive();
306
+ return this.presentationProducerGeneration;
307
+ }
308
+ /**
309
+ * Delivery creates this publisher and can revoke it on either service's
310
+ * reload. Automations retains it only as a private coordinator adapter; no
311
+ * Agent, Automation definition, or public Delivery dispatcher can mint one.
312
+ */
313
+ registerTrustedDeliveryPresentationSink(registration) {
314
+ this.assertActive();
315
+ if (registration.protocol !== 'assistant-delivery/trusted-presentation-producer/v1'
316
+ || registration.producer !== 'assistant-automations'
317
+ || registration.generation !== this.presentationProducerGeneration
318
+ || typeof registration.publish !== 'function'
319
+ || !registrationOwnedByDeliveryPresentation(registration)) {
320
+ throw new AssistantAutomationsError('runtime-conflict', 'trusted Delivery presentation registration is invalid');
321
+ }
322
+ const current = this.presentationSink;
323
+ if (current !== undefined) {
324
+ if (current.registration === registration)
325
+ return current.dispose;
326
+ throw new AssistantAutomationsError('runtime-conflict', 'trusted Delivery presentation sink is already registered');
327
+ }
328
+ const token = Symbol('assistant-automations.trusted-delivery-presentation');
329
+ let live = true;
330
+ const dispose = () => {
331
+ if (!live)
332
+ return;
333
+ live = false;
334
+ if (this.presentationSink?.token === token)
335
+ this.presentationSink = undefined;
336
+ };
337
+ this.presentationSink = Object.freeze({ token, registration, dispose });
338
+ return dispose;
339
+ }
340
+ deliveryDispatcher(delivery) {
341
+ return Object.freeze({
342
+ enqueueBackground: (input) => delivery.enqueueBackground(input),
343
+ enqueueAutomationResult: (input) => delivery.enqueueAutomationResult(input),
344
+ enqueueBackgroundRoute: (input) => delivery.enqueueBackgroundRoute(input),
345
+ // This is a closure over the private registration rather than a method
346
+ // taken from the public Delivery service object.
347
+ publishDeliveryPresentation: (input) => this.publishAutomationIncidentPresentation(input),
348
+ });
349
+ }
350
+ publishAutomationIncidentPresentation(input) {
351
+ this.assertActive();
352
+ const sink = this.presentationSink;
353
+ if (sink === undefined) {
354
+ throw new AssistantAutomationsError('runtime-conflict', 'trusted Delivery presentation sink is unavailable');
355
+ }
356
+ return sink.registration.publish(input);
357
+ }
358
+ /**
359
+ * Bind the one exact Evaluation instance allowed to consume this service's
360
+ * durable terminal outbox. The registration object is a process-local
361
+ * capability; it is never exposed as a model tool or accepted from payload
362
+ * data.
363
+ */
364
+ registerTrustedAutomationEvaluationSink(registration) {
365
+ this.assertActive();
366
+ if (registration.protocol !== 'assistant-evaluation/trusted-producer/v1'
367
+ || registration.producer !== 'assistant-automations'
368
+ || registration.generation !== this.evaluationProducerGeneration
369
+ || typeof registration.issueCapability !== 'function'
370
+ || typeof registration.append !== 'function'
371
+ || !registrationOwnedByEvaluation(registration)) {
372
+ throw new AssistantAutomationsError('runtime-conflict', 'trusted Evaluation registration is invalid');
373
+ }
374
+ if (this.evaluationSink !== undefined) {
375
+ throw new AssistantAutomationsError('runtime-conflict', 'trusted Evaluation sink is already registered');
376
+ }
377
+ const token = Symbol('assistant-automations.trusted-evaluation');
378
+ this.evaluationSink = Object.freeze({ token, registration });
379
+ this.coordinator.setEvaluationRecorder(Object.freeze({
380
+ append: (outcome) => this.appendTrustedEvaluation(token, outcome),
381
+ }));
382
+ let active = true;
383
+ return () => {
384
+ if (!active)
385
+ return;
386
+ active = false;
387
+ if (this.evaluationSink?.token !== token)
388
+ return;
389
+ this.evaluationSink = undefined;
390
+ this.coordinator.setEvaluationRecorder(undefined);
391
+ };
392
+ }
393
+ appendTrustedEvaluation(token, outcome) {
394
+ this.assertActive();
395
+ const sink = this.evaluationSink;
396
+ const runEvidence = outcome.evidence.filter(entry => entry.kind === 'automation-run');
397
+ const automationId = outcome.situation.startsWith('automation:')
398
+ ? outcome.situation.slice('automation:'.length)
399
+ : '';
400
+ const runId = runEvidence[0]?.ref;
401
+ const proof = runId === undefined ? undefined : this.store.getProvenProductionRun(runId);
402
+ const expectedExecutionStatus = proof?.run.status === 'timed_out'
403
+ ? 'timed-out'
404
+ : proof?.run.status;
405
+ const hostObjectiveProven = proof !== undefined
406
+ && isHostAutomationDefinition(proof.automation.definition)
407
+ && (proof.run.status === 'succeeded'
408
+ || proof.run.status === 'failed' || proof.run.status === 'timed_out');
409
+ const expectedObjectiveStatus = proof === undefined || !hostObjectiveProven
410
+ ? 'unknown'
411
+ : proof.run.status === 'succeeded' ? 'achieved' : 'not-achieved';
412
+ const expectedEvaluatorVersion = hostObjectiveProven ? 'host-runbook-v1' : 'terminal-v1';
413
+ if (sink?.token !== token || outcome.executionMode !== 'production'
414
+ || automationId === '' || runEvidence.length !== 1
415
+ || runId === undefined || proof === undefined
416
+ || proof.run.automationId !== automationId
417
+ || proof.automation.definition.workspace !== outcome.scope.workspace
418
+ || proof.automation.definition.agentPreset !== outcome.scope.preset
419
+ || outcome.situation !== `automation:${automationId}`
420
+ || outcome.executionStatus !== expectedExecutionStatus
421
+ || outcome.objectiveStatus !== expectedObjectiveStatus
422
+ || outcome.occurredAt !== proof.run.createdAt
423
+ || outcome.idempotencyKey !== `assistant-automations:terminal:${runId}:v1`
424
+ || outcome.evaluator.version !== expectedEvaluatorVersion
425
+ || outcome.source.kind !== 'automation' || outcome.source.id !== 'assistant-automations'
426
+ || outcome.trust !== 'trusted' || outcome.evaluator.id !== 'assistant-automations') {
427
+ throw new AssistantAutomationsError('runtime-conflict', 'trusted Evaluation outbox payload is invalid');
428
+ }
429
+ const claims = Object.freeze({
430
+ scope: Object.freeze({ ...outcome.scope }),
431
+ automationId,
432
+ situation: outcome.situation,
433
+ runId,
434
+ executionMode: 'production',
435
+ executionStatus: outcome.executionStatus,
436
+ objectiveStatus: outcome.objectiveStatus,
437
+ deliveryStatus: outcome.deliveryStatus,
438
+ metrics: Object.freeze({ ...outcome.metrics }),
439
+ occurredAt: outcome.occurredAt,
440
+ idempotencyKey: outcome.idempotencyKey,
441
+ evaluatorVersion: outcome.evaluator.version,
442
+ });
443
+ const capabilityReceipt = sink.registration.issueCapability(claims);
444
+ if (this.evaluationSink?.token !== token) {
445
+ throw new AssistantAutomationsError('runtime-conflict', 'trusted Evaluation capability became stale');
446
+ }
447
+ const receipt = sink.registration.append({
448
+ capabilityReceipt,
449
+ automationId,
450
+ runId: claims.runId,
451
+ idempotencyKey: claims.idempotencyKey,
452
+ });
453
+ if (receipt.idempotencyKey !== claims.idempotencyKey
454
+ || receipt.source.kind !== 'automation' || receipt.source.id !== 'assistant-automations'
455
+ || receipt.trust !== 'trusted') {
456
+ throw new AssistantAutomationsError('runtime-conflict', 'trusted Evaluation receipt is invalid');
457
+ }
458
+ return receipt;
459
+ }
460
+ /**
461
+ * Re-prove Delivery's mutable owner/template authority immediately before a
462
+ * Growth phase can submit a model turn, expose production output, or promote
463
+ * the definition. Rollback deliberately does not use this gate: losing the
464
+ * old owner route must never prevent reducing authority.
465
+ */
466
+ hasShadowEffectBlocker(run) {
467
+ // Preview execution has no Delivery lane at all. `undefined` is the
468
+ // durable Store representation of that structural no-send state; a
469
+ // pending/enqueued/suppressed delivery record is not interchangeable with
470
+ // a preview blocker and must fail closed here.
471
+ return run.executionMode === 'preview'
472
+ && run.deliveryStatus === undefined
473
+ && run.deliveryRef === undefined
474
+ && run.evidenceStatus === 'suppressed'
475
+ && run.usage['toolCalls'] === 0;
476
+ }
477
+ requireLiveGrowthArtifact(artifact) {
478
+ const resolver = this.workflowTemplateResolver;
479
+ if (resolver === undefined || typeof resolver.resolveWorkflowAutomationTemplate !== 'function') {
480
+ throw new AssistantAutomationsError('runtime-conflict', 'Delivery template authority is unavailable');
481
+ }
482
+ const live = validateResolvedWorkflowAutomationTemplate(resolver.resolveWorkflowAutomationTemplate({
483
+ contractVersion: 1,
484
+ template: {
485
+ templateRef: artifact.templateRef,
486
+ templateDigest: artifact.templateDigest,
487
+ privacyAttestation: artifact.privacyAttestation,
488
+ },
489
+ scope: { workspace: artifact.workspace, preset: artifact.preset },
490
+ ownerBindingId: artifact.ownerBindingId,
491
+ }));
492
+ const definition = this.store.get(artifact.automationId);
493
+ const observedCatalog = [...new Set(artifact.steps.map(step => step.catalogId))].sort();
494
+ const exact = definition !== undefined && definition.owner === GROWTH_AUTOMATION_OWNER
495
+ && !isHostAutomationDefinition(definition.definition)
496
+ && canonicalGrowthJson(live.template) === canonicalGrowthJson({
497
+ templateRef: artifact.templateRef,
498
+ templateDigest: artifact.templateDigest,
499
+ privacyAttestation: artifact.privacyAttestation,
500
+ })
501
+ && live.scope.workspace === artifact.workspace && live.scope.preset === artifact.preset
502
+ && live.ownerBindingId === artifact.ownerBindingId && live.principalId === artifact.principalId
503
+ && canonicalGrowthJson(live.toolCatalogIds) === canonicalGrowthJson(observedCatalog)
504
+ && definition.definition.name === live.name && definition.definition.prompt === live.prompt
505
+ && canonicalGrowthJson(definition.definition.schedule) === canonicalGrowthJson(live.schedule)
506
+ && definition.definition.workspace === live.scope.workspace
507
+ && definition.definition.agentPreset === live.scope.preset
508
+ && definition.definition.timeoutMs === live.timeoutMs
509
+ && canonicalGrowthJson(definition.definition.allowedTools) === canonicalGrowthJson([])
510
+ && definition.definition.maxToolCalls === 0
511
+ && definition.definition.principal === live.principalId
512
+ && definition.definition.approvalBindingId === live.ownerBindingId
513
+ && definition.definition.deliveryBindingId === live.deliveryBindingId
514
+ && this.store.getDefinitionHash(definition.id) === artifact.definitionHash;
515
+ if (!exact || definition === undefined) {
516
+ throw new AssistantAutomationsError('runtime-conflict', 'Growth owner, template, or materialized Automation changed before execution');
517
+ }
518
+ return definition;
519
+ }
520
+ async requestWorkflowAutomation(inputValue) {
521
+ this.assertActive();
522
+ const input = validateGrowthAutomationProposalRequest(inputValue);
523
+ const replay = this.growthStore.beginOperation('approval-proposal', input);
524
+ if (replay !== undefined)
525
+ return validateGrowthAutomationProposalReceipt(replay, growthIdentity(input));
526
+ const resolver = this.workflowTemplateResolver;
527
+ if (resolver === undefined || typeof resolver.resolveWorkflowAutomationTemplate !== 'function') {
528
+ throw new AssistantAutomationsError('runtime-conflict', 'Growth materialization requires the private Delivery workflow template resolver');
529
+ }
530
+ const resolved = validateResolvedWorkflowAutomationTemplate(resolver.resolveWorkflowAutomationTemplate({
531
+ contractVersion: 1,
532
+ template: input.template,
533
+ scope: input.scope,
534
+ ownerBindingId: input.ownerBindingId,
535
+ }));
536
+ if (canonicalGrowthJson(resolved.template) !== canonicalGrowthJson(input.template)
537
+ || canonicalGrowthJson(resolved.scope) !== canonicalGrowthJson(input.scope)
538
+ || resolved.ownerBindingId !== input.ownerBindingId) {
539
+ throw new AssistantAutomationsError('runtime-conflict', 'Delivery resolved a different workflow template or owner scope');
540
+ }
541
+ const approvalDelivery = this.approvalDelivery;
542
+ if (approvalDelivery === undefined || typeof approvalDelivery.prepareWorkflowApproval !== 'function') {
543
+ throw new AssistantAutomationsError('runtime-conflict', 'Growth approval requires the private Delivery v2 approval route resolver');
544
+ }
545
+ const dispatch = approvalDelivery.prepareWorkflowApproval({
546
+ sourceId: GROWTH_AUTOMATION_OWNER,
547
+ contractVersion: 1,
548
+ template: input.template,
549
+ scope: input.scope,
550
+ ownerBindingId: input.ownerBindingId,
551
+ });
552
+ if (dispatch.routeVersion !== 2
553
+ || dispatch.sourceId !== GROWTH_AUTOMATION_OWNER
554
+ || dispatch.bindingId !== resolved.ownerBindingId
555
+ || dispatch.workspace !== resolved.scope.workspace
556
+ || dispatch.principal !== resolved.principalId) {
557
+ throw new AssistantAutomationsError('runtime-conflict', 'Delivery resolved a different workflow approval route');
558
+ }
559
+ const requestedCatalog = [...resolved.toolCatalogIds];
560
+ const observedCatalog = [...new Set(input.steps.map(step => step.catalogId))].sort();
561
+ if (canonicalGrowthJson(observedCatalog) !== canonicalGrowthJson(requestedCatalog)) {
562
+ throw new AssistantAutomationsError('invalid-proposal', 'Growth workflow catalog must exactly match its observed step catalog');
563
+ }
564
+ const orchestrationSteps = input.steps.filter(step => step.catalogId === WORKFLOW_MODEL_TURN_CATALOG_ID);
565
+ const requestedTools = requestedCatalog.filter(id => id !== WORKFLOW_MODEL_TURN_CATALOG_ID);
566
+ // The current shadow executor has a real empty tool plane, so it can prove
567
+ // delivery/tool effects are impossible only for Delivery's built-in model
568
+ // turn. A tool-bearing workflow remains representable in the trace contract
569
+ // but cannot enter approval until a Host-owned per-step interception receipt
570
+ // exists; zero observed calls is never treated as that proof.
571
+ if (orchestrationSteps.length !== 1 || input.steps.length !== 1 || requestedTools.length !== 0) {
572
+ throw new AssistantAutomationsError('invalid-proposal', 'Growth shadow currently supports only one assistant.agent-turn step and fails closed for tool workflows');
573
+ }
574
+ if (requestedTools.some(tool => !this.config.proposalDefaults.allowedTools.includes(tool))
575
+ || new Set(requestedTools).size !== requestedTools.length
576
+ || canonicalGrowthJson([...new Set(input.steps.map(step => step.catalogId))].sort())
577
+ !== canonicalGrowthJson(requestedCatalog)) {
578
+ throw new AssistantAutomationsError('invalid-proposal', 'Growth workflow catalog must exactly match observed steps and configured trusted tools');
579
+ }
580
+ const defaults = this.config.proposalDefaults;
581
+ const misfire = defaults.misfireKind === 'bounded-replay'
582
+ ? { kind: 'bounded-replay', limit: defaults.misfireLimit }
583
+ : { kind: defaults.misfireKind };
584
+ const definition = Object.freeze({
585
+ name: resolved.name,
586
+ prompt: resolved.prompt,
587
+ schedule: resolved.schedule,
588
+ workspace: resolved.scope.workspace,
589
+ agentPreset: resolved.scope.preset,
590
+ provider: defaults.provider,
591
+ model: defaults.model,
592
+ allowedTools: Object.freeze(requestedTools),
593
+ timeoutMs: resolved.timeoutMs,
594
+ maxOutputTokens: defaults.maxOutputTokens,
595
+ maxToolCalls: 0,
596
+ misfire,
597
+ overlap: defaults.overlap,
598
+ retrySafety: defaults.retrySafety,
599
+ maxRetries: defaults.maxRetries,
600
+ principal: resolved.principalId,
601
+ budgetId: defaults.budgetId,
602
+ budgetAmount: defaults.budgetAmount,
603
+ approvalBindingId: resolved.ownerBindingId,
604
+ deliveryBindingId: resolved.deliveryBindingId,
605
+ });
606
+ const automationId = this.growthStore.automationId(input);
607
+ const artifactId = this.growthStore.artifactId(input);
608
+ const diff = JSON.stringify({
609
+ contract: 'assistant-growth-approval-diff/v2',
610
+ artifactId,
611
+ automationId,
612
+ candidateId: input.candidateId,
613
+ candidateRevision: input.candidateRevision,
614
+ candidateDigest: input.candidateDigest,
615
+ template: input.template,
616
+ scope: input.scope,
617
+ ownerBindingId: input.ownerBindingId,
618
+ exactBehavior: {
619
+ name: resolved.name,
620
+ prompt: resolved.prompt,
621
+ schedule: resolved.schedule,
622
+ timeoutMs: resolved.timeoutMs,
623
+ provider: defaults.provider,
624
+ model: defaults.model,
625
+ maxOutputTokens: defaults.maxOutputTokens,
626
+ orchestrationCatalogIds: resolved.toolCatalogIds,
627
+ executableTools: requestedTools,
628
+ approvalBindingId: resolved.ownerBindingId,
629
+ deliveryBindingId: resolved.deliveryBindingId,
630
+ },
631
+ initialState: 'paused',
632
+ }, null, 2);
633
+ const approval = this.policy.recoverOrCreateProposal({
634
+ idempotencyKey: boundedGrowthKey('growth-approval', growthIdentity(input)),
635
+ requester: GROWTH_AUTOMATION_OWNER,
636
+ principal: resolved.principalId,
637
+ action: 'create',
638
+ resource: { kind: 'automation', id: automationId },
639
+ diff,
640
+ summary: 'Approve learned workflow as a paused Automation',
641
+ notAfter: input.deadlineAt,
642
+ dispatch,
643
+ });
644
+ if (approval.kind === 'abandoned') {
645
+ const receipt = withGrowthPortReceiptDigest({
646
+ ...growthIdentity(input), outcome: 'expired',
647
+ });
648
+ return validateGrowthAutomationProposalReceipt(this.growthStore.completeOperation('approval-proposal', input, receipt), growthIdentity(input));
649
+ }
650
+ const automation = this.reconcileSystem({
651
+ owner: GROWTH_AUTOMATION_OWNER,
652
+ automationId,
653
+ idempotencyKey: boundedGrowthKey('growth-materialize', growthIdentity(input)),
654
+ desiredStatus: 'paused',
655
+ definition,
656
+ });
657
+ const stored = this.growthStore.upsertArtifact({
658
+ request: input,
659
+ resolved,
660
+ automation,
661
+ proposalId: approval.proposal.proposalId,
662
+ approvalDiffHash: approval.proposal.diffHash,
663
+ policyStatus: approval.proposal.status,
664
+ });
665
+ const receipt = approval.proposal.status === 'approved'
666
+ ? withGrowthPortReceiptDigest({
667
+ ...growthIdentity(input),
668
+ outcome: 'approved-paused',
669
+ proposalId: approval.proposal.proposalId,
670
+ artifactId: stored.artifactId,
671
+ artifactVersion: stored.definitionVersion,
672
+ artifactDigest: stored.definitionHash,
673
+ })
674
+ : approval.proposal.status === 'pending'
675
+ ? withGrowthPortReceiptDigest({
676
+ ...growthIdentity(input),
677
+ outcome: 'approval-pending',
678
+ proposalId: approval.proposal.proposalId,
679
+ })
680
+ : withGrowthPortReceiptDigest({
681
+ ...growthIdentity(input),
682
+ outcome: approval.proposal.status,
683
+ proposalId: approval.proposal.proposalId,
684
+ });
685
+ return validateGrowthAutomationProposalReceipt(this.growthStore.completeOperation('approval-proposal', input, receipt), growthIdentity(input));
686
+ }
687
+ async settleWorkflowAutomation(inputValue) {
688
+ this.assertActive();
689
+ const input = validateGrowthAutomationApprovalRequest(inputValue);
690
+ const replay = this.growthStore.beginOperation('approval-settlement', input);
691
+ if (replay !== undefined)
692
+ return validateGrowthAutomationApprovalReceipt(replay, growthIdentity(input));
693
+ const artifact = this.growthStore.byExperiment(input.experimentId);
694
+ const proposal = this.policy.getProposal(input.proposalId);
695
+ const definition = artifact === undefined ? undefined : this.store.get(artifact.automationId);
696
+ const exact = artifact !== undefined && artifact.proposalId === input.proposalId
697
+ && artifact.candidateId === input.candidateId
698
+ && artifact.candidateRevision === input.candidateRevision
699
+ && artifact.candidateDigest === input.candidateDigest
700
+ && proposal?.requester === GROWTH_AUTOMATION_OWNER
701
+ && proposal.principal === artifact.principalId
702
+ && proposal.action === 'create'
703
+ && proposal.resource.kind === 'automation'
704
+ && proposal.resource.id === artifact.automationId
705
+ && proposal.diffHash === artifact.approvalDiffHash
706
+ && proposal.expiresAt === artifact.deadlineAt
707
+ && definition?.owner === GROWTH_AUTOMATION_OWNER
708
+ && definition.status === 'paused'
709
+ && definition.version === artifact.definitionVersion
710
+ && this.store.getDefinitionHash(artifact.automationId) === artifact.definitionHash;
711
+ if (!exact || artifact === undefined || proposal === undefined) {
712
+ const receipt = withGrowthPortReceiptDigest({
713
+ ...growthIdentity(input), outcome: 'conflicted', proposalId: input.proposalId,
714
+ });
715
+ return validateGrowthAutomationApprovalReceipt(this.growthStore.completeOperation('approval-settlement', input, receipt), growthIdentity(input));
716
+ }
717
+ if (proposal.status === 'approved') {
718
+ const resolver = this.workflowTemplateResolver;
719
+ if (resolver === undefined || typeof resolver.resolveWorkflowAutomationTemplate !== 'function') {
720
+ throw new AssistantAutomationsError('runtime-conflict', 'Delivery template authority is unavailable');
721
+ }
722
+ const live = validateResolvedWorkflowAutomationTemplate(resolver.resolveWorkflowAutomationTemplate({
723
+ contractVersion: 1,
724
+ template: {
725
+ templateRef: artifact.templateRef,
726
+ templateDigest: artifact.templateDigest,
727
+ privacyAttestation: artifact.privacyAttestation,
728
+ },
729
+ scope: { workspace: artifact.workspace, preset: artifact.preset },
730
+ ownerBindingId: artifact.ownerBindingId,
731
+ }));
732
+ if (live.template.templateRef !== artifact.templateRef
733
+ || live.template.templateDigest !== artifact.templateDigest
734
+ || canonicalGrowthJson(live.template.privacyAttestation)
735
+ !== canonicalGrowthJson(artifact.privacyAttestation)
736
+ || live.scope.workspace !== artifact.workspace || live.scope.preset !== artifact.preset
737
+ || live.principalId !== artifact.principalId
738
+ || live.deliveryBindingId !== definition.definition.deliveryBindingId
739
+ || live.ownerBindingId !== definition.definition.approvalBindingId) {
740
+ throw new AssistantAutomationsError('runtime-conflict', 'Growth owner route changed before approval settlement');
741
+ }
742
+ }
743
+ this.growthStore.updateApproval(artifact.artifactId, input.proposalId, proposal.status);
744
+ const receipt = proposal.status === 'approved'
745
+ ? withGrowthPortReceiptDigest({
746
+ ...growthIdentity(input), outcome: 'approved-paused', proposalId: input.proposalId,
747
+ artifactId: artifact.artifactId, artifactVersion: artifact.definitionVersion,
748
+ artifactDigest: artifact.definitionHash,
749
+ })
750
+ : proposal.status === 'pending'
751
+ ? withGrowthPortReceiptDigest({
752
+ ...growthIdentity(input), outcome: 'approval-pending', proposalId: input.proposalId,
753
+ })
754
+ : withGrowthPortReceiptDigest({
755
+ ...growthIdentity(input), outcome: proposal.status, proposalId: input.proposalId,
756
+ });
757
+ return validateGrowthAutomationApprovalReceipt(this.growthStore.completeOperation('approval-settlement', input, receipt), growthIdentity(input));
758
+ }
759
+ replayWorkflowAutomation(inputValue) {
760
+ this.assertActive();
761
+ const input = validateGrowthAutomationArtifactRequest(inputValue);
762
+ const replay = this.growthStore.beginOperation('replay', input);
763
+ if (replay !== undefined)
764
+ return validateGrowthReplayReceipt(replay, input);
765
+ const artifact = this.growthStore.requireArtifact(input);
766
+ if (artifact.state !== 'paused') {
767
+ throw new AssistantAutomationsError('runtime-conflict', 'replay requires an exact paused artifact');
768
+ }
769
+ this.requireLiveGrowthArtifact(artifact);
770
+ // This is a content-free historical replay: it checks the immutable
771
+ // Delivery→Growth evidence window and never schedules or executes anything.
772
+ const replayDigest = growthObjectDigest({
773
+ contract: 'assistant-automations-growth-replay/v1',
774
+ evidenceDigest: artifact.evidenceDigest,
775
+ evidenceCount: artifact.evidenceCount,
776
+ templateDigest: artifact.templateDigest,
777
+ steps: artifact.steps,
778
+ });
779
+ const receipt = withGrowthPortReceiptDigest({
780
+ ...growthIdentity(input),
781
+ artifactId: input.artifactId,
782
+ artifactVersion: input.artifactVersion,
783
+ artifactDigest: input.artifactDigest,
784
+ outcome: artifact.evidenceCount > 0 ? 'passed' : 'failed',
785
+ replayDigest,
786
+ });
787
+ return validateGrowthReplayReceipt(this.growthStore.completeOperation('replay', input, receipt), input);
788
+ }
789
+ async shadowWorkflowAutomation(inputValue) {
790
+ this.assertActive();
791
+ const input = validateGrowthAutomationArtifactRequest(inputValue);
792
+ const replay = this.growthStore.beginOperation('shadow', input);
793
+ if (replay !== undefined)
794
+ return validateGrowthShadowReceipt(replay, input);
795
+ const artifact = this.growthStore.requireArtifact(input);
796
+ this.requireLiveGrowthArtifact(artifact);
797
+ const task = this.growthStore.createExecutionTask({ artifact, operationId: input.operationId, kind: 'shadow' });
798
+ let run = this.store.getRun(`run-${task.taskId}`);
799
+ if (run === undefined) {
800
+ await this.coordinator.tick();
801
+ await this.coordinator.whenIdle();
802
+ run = this.store.getRun(`run-${task.taskId}`);
803
+ }
804
+ if (run === undefined || !this.hasShadowEffectBlocker(run)) {
805
+ throw new AssistantAutomationsError('runtime-conflict', 'shadow execution lacks durable delivery/tool effect-blocker proof');
806
+ }
807
+ const receipt = withGrowthPortReceiptDigest({
808
+ ...growthIdentity(input),
809
+ artifactId: input.artifactId,
810
+ artifactVersion: input.artifactVersion,
811
+ artifactDigest: input.artifactDigest,
812
+ outcome: run.status === 'succeeded' ? 'passed' : 'failed',
813
+ effectsBlocked: true,
814
+ effectBlockerAttestation: growthEffectBlockerAttestation,
815
+ shadowDigest: growthObjectDigest({
816
+ contract: 'assistant-automations-growth-shadow/v1',
817
+ runId: run.id,
818
+ status: run.status,
819
+ definitionHash: run.definitionHash,
820
+ implementationDigest: growthEffectBlockerAttestation.implementationDigest,
821
+ }),
822
+ });
823
+ return validateGrowthShadowReceipt(this.growthStore.completeOperation('shadow', input, receipt), input);
824
+ }
825
+ async canaryWorkflowAutomation(inputValue) {
826
+ this.assertActive();
827
+ const input = validateGrowthAutomationArtifactRequest(inputValue);
828
+ const replay = this.growthStore.beginOperation('canary', input);
829
+ if (replay !== undefined)
830
+ return validateGrowthCanaryReceipt(replay, input);
831
+ const artifact = this.growthStore.requireArtifact(input);
832
+ this.requireLiveGrowthArtifact(artifact);
833
+ const task = this.growthStore.createExecutionTask({ artifact, operationId: input.operationId, kind: 'canary' });
834
+ let run = this.store.getRun(`run-${task.taskId}`);
835
+ if (run === undefined) {
836
+ await this.coordinator.tick();
837
+ await this.coordinator.whenIdle();
838
+ run = this.store.getRun(`run-${task.taskId}`);
839
+ }
840
+ if (run === undefined || run.executionMode !== 'production') {
841
+ throw new AssistantAutomationsError('runtime-conflict', 'canary exposure has no durable production run');
842
+ }
843
+ this.growthStore.recordCanaryRun(artifact.artifactId, run);
844
+ const terminalFailure = run.status !== 'succeeded';
845
+ const receipt = withGrowthPortReceiptDigest({
846
+ ...growthIdentity(input),
847
+ artifactId: input.artifactId,
848
+ artifactVersion: input.artifactVersion,
849
+ artifactDigest: input.artifactDigest,
850
+ outcome: terminalFailure ? 'failed' : 'pending',
851
+ exposureCount: 1,
852
+ exposureOperationId: `${input.experimentId}:canary`,
853
+ });
854
+ return validateGrowthCanaryReceipt(this.growthStore.completeOperation('canary', input, receipt), input);
855
+ }
856
+ inspectWorkflowCanary(inputValue) {
857
+ this.assertActive();
858
+ const input = validateGrowthCanaryInspectionRequest(inputValue);
859
+ const replay = this.growthStore.beginOperation('canary-inspection', input);
860
+ if (replay !== undefined)
861
+ return validateGrowthCanaryInspectionReceipt(replay, input);
862
+ const artifact = this.growthStore.requireArtifact(input);
863
+ const run = artifact.canaryRunId === undefined ? undefined : this.store.getRun(artifact.canaryRunId);
864
+ if (artifact.canaryTaskId === undefined || artifact.canaryRunId === undefined || run === undefined
865
+ || run.taskId !== artifact.canaryTaskId || run.executionMode !== 'production') {
866
+ throw new AssistantAutomationsError('runtime-conflict', 'canary inspection has no exact exposure proof');
867
+ }
868
+ let proof;
869
+ const evaluation = this.evaluation;
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);
886
+ const failed = run.status !== 'succeeded';
887
+ const evaluationDigest = !passed ? undefined : growthObjectDigest({
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
+ });
898
+ if (passed) {
899
+ this.growthStore.recordCanaryEvaluation({
900
+ artifactId: artifact.artifactId,
901
+ runId: run.id,
902
+ evaluationId: proof.id,
903
+ evaluationDigest: evaluationDigest,
904
+ });
905
+ }
906
+ const receipt = withGrowthPortReceiptDigest({
907
+ ...growthIdentity(input),
908
+ artifactId: input.artifactId,
909
+ artifactVersion: input.artifactVersion,
910
+ artifactDigest: input.artifactDigest,
911
+ outcome: passed ? 'passed' : failed ? 'failed' : 'pending',
912
+ exposureCount: 1,
913
+ exposureOperationId: input.exposureOperationId,
914
+ ...(passed ? {
915
+ evaluationDigest: evaluationDigest,
916
+ evaluationTrust: 'trusted',
917
+ objectiveStatus: 'achieved',
918
+ } : {}),
919
+ });
920
+ return validateGrowthCanaryInspectionReceipt(this.growthStore.completeOperation('canary-inspection', input, receipt), input);
921
+ }
922
+ promoteWorkflowAutomation(inputValue) {
923
+ this.assertActive();
924
+ const input = validateGrowthAutomationArtifactRequest(inputValue);
925
+ const replay = this.growthStore.beginOperation('promotion', input);
926
+ if (replay !== undefined)
927
+ return validateGrowthPromotionReceipt(replay, input);
928
+ const artifact = this.growthStore.requireArtifact(input);
929
+ if (artifact.state !== 'canary-pending' || artifact.canaryRunId === undefined
930
+ || artifact.canaryEvaluationId === undefined || artifact.canaryEvaluationDigest === undefined) {
931
+ throw new AssistantAutomationsError('runtime-conflict', 'promotion requires the exact canary artifact');
932
+ }
933
+ const definition = this.requireLiveGrowthArtifact(artifact);
934
+ if (definition === undefined || definition.owner !== GROWTH_AUTOMATION_OWNER
935
+ || this.store.getDefinitionHash(definition.id) !== input.artifactDigest
936
+ || !((definition.status === 'paused' && definition.version === input.artifactVersion)
937
+ || (definition.status === 'active' && definition.version === input.artifactVersion + 1))) {
938
+ throw new AssistantAutomationsError('runtime-conflict', 'promotion artifact changed before exact CAS');
939
+ }
940
+ const promoted = definition.status === 'active'
941
+ ? definition
942
+ : this.reconcileSystem({
943
+ owner: GROWTH_AUTOMATION_OWNER,
944
+ automationId: definition.id,
945
+ idempotencyKey: boundedGrowthKey('growth-promote', input),
946
+ desiredStatus: 'active',
947
+ definition: definition.definition,
948
+ });
949
+ const stored = this.growthStore.completePromotion({ request: input, automation: promoted });
950
+ const receipt = withGrowthPortReceiptDigest({
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,
958
+ });
959
+ return validateGrowthPromotionReceipt(this.growthStore.completeOperation('promotion', input, receipt), input);
960
+ }
961
+ rollbackWorkflowAutomation(inputValue) {
962
+ this.assertActive();
963
+ const input = validateGrowthAutomationArtifactRequest(inputValue);
964
+ const replay = this.growthStore.beginOperation('rollback', input);
965
+ if (replay !== undefined)
966
+ return validateGrowthRollbackReceipt(replay, input);
967
+ const artifact = this.growthStore.requireArtifact(input);
968
+ let automation = this.store.get(artifact.automationId);
969
+ if (automation === undefined || automation.owner !== GROWTH_AUTOMATION_OWNER
970
+ || this.store.getDefinitionHash(automation.id) !== input.artifactDigest
971
+ || !((automation.status === 'active' && automation.version === input.artifactVersion)
972
+ || (automation.status === 'paused'
973
+ && (automation.version === input.artifactVersion
974
+ || automation.version === input.artifactVersion + 1)))) {
975
+ throw new AssistantAutomationsError('runtime-conflict', 'rollback artifact changed before exact CAS');
976
+ }
977
+ if (automation.status === 'active') {
978
+ this.store.pauseSystemOwned({
979
+ owner: GROWTH_AUTOMATION_OWNER,
980
+ operationId: boundedGrowthKey('growth-rollback-pause', input),
981
+ automationId: automation.id,
982
+ definitionHash: input.artifactDigest,
983
+ expectedVersion: input.artifactVersion,
984
+ });
985
+ automation = this.store.get(automation.id);
986
+ }
987
+ this.growthStore.completeRollback({ request: input, automation });
988
+ const receipt = withGrowthPortReceiptDigest({
989
+ ...growthIdentity(input),
990
+ artifactId: input.artifactId,
991
+ artifactVersion: input.artifactVersion,
992
+ artifactDigest: input.artifactDigest,
993
+ outcome: 'rolled-back',
994
+ });
995
+ return validateGrowthRollbackReceipt(this.growthStore.completeOperation('rollback', input, receipt), input);
996
+ }
204
997
  propose(agent, input) {
205
998
  const identity = this.authorizeAgent(agent, 'propose', input.mutation.automationId, `automation-propose:${input.idempotencyKey}`);
206
999
  const approval = this.resolveApprovalRoute(agent, identity, input.principal);
@@ -248,7 +1041,8 @@ export class AssistantAutomationsService extends Service {
248
1041
  if (current !== undefined && (current.definition.workspace !== identity.workspace
249
1042
  || current.definition.agentPreset !== identity.agentPreset
250
1043
  || current.definition.principal !== approval.principal
251
- || current.definition.deliveryBindingId !== approval.dispatch?.bindingId)) {
1044
+ || (current.definition.approvalBindingId ?? current.definition.deliveryBindingId)
1045
+ !== approval.dispatch?.bindingId)) {
252
1046
  throw new AssistantAutomationsError('missing-approval-route', 'automation lifecycle proposal does not match the immutable owner route');
253
1047
  }
254
1048
  mutation = input.mutation;
@@ -365,6 +1159,48 @@ export class AssistantAutomationsService extends Service {
365
1159
  throw new Error('assistant-automations: dry-run task was not claimed by this duty owner');
366
1160
  return { occurrence: this.store.getOccurrence(occurrence.id), run };
367
1161
  }
1162
+ /** Exact Host-only preview lane for one system-owned immutable definition. */
1163
+ async runSystemDry(input) {
1164
+ this.assertActive();
1165
+ const current = this.store.get(input.automationId);
1166
+ if (current === undefined || current.owner !== input.owner) {
1167
+ throw new AssistantAutomationsError('not-found', 'system automation was not found for the exact owner');
1168
+ }
1169
+ if (this.store.getDefinitionHash(input.automationId) !== input.definitionHash) {
1170
+ throw new AssistantAutomationsError('invalid-input', 'system dry-run definition hash is stale');
1171
+ }
1172
+ const decision = this.policy.authorize({
1173
+ subject: {
1174
+ kind: 'background', id: input.owner,
1175
+ workspace: current.definition.workspace, principal: current.definition.principal,
1176
+ },
1177
+ action: 'run-dry',
1178
+ resource: { kind: 'automation', id: input.automationId },
1179
+ context: { initiator: 'background' },
1180
+ }, { idempotencyKey: [
1181
+ 'automation-system-dry', input.owner, input.automationId,
1182
+ input.definitionHash, input.idempotencyKey,
1183
+ ].join(':') });
1184
+ if (decision.effect !== 'allow')
1185
+ throw policyError(decision);
1186
+ const occurrence = this.store.createManual({
1187
+ automationId: input.automationId,
1188
+ requestId: `system-dry:${input.owner}:${input.definitionHash}:${input.idempotencyKey}`,
1189
+ dryRun: true,
1190
+ });
1191
+ let run = this.store.listRuns({ automationId: input.automationId, limit: 1_000 })
1192
+ .find(value => value.occurrenceId === occurrence.id);
1193
+ if (run === undefined) {
1194
+ await this.coordinator.tick();
1195
+ await this.coordinator.whenIdle();
1196
+ run = this.store.listRuns({ automationId: input.automationId, limit: 1_000 })
1197
+ .find(value => value.occurrenceId === occurrence.id);
1198
+ }
1199
+ if (run === undefined) {
1200
+ throw new Error('assistant-automations: system dry-run was not claimed by this duty owner');
1201
+ }
1202
+ return { occurrence: this.store.getOccurrence(occurrence.id), run };
1203
+ }
368
1204
  ingestExternal(input) {
369
1205
  this.assertActive();
370
1206
  const automation = this.store.get(input.automationId);
@@ -418,6 +1254,140 @@ export class AssistantAutomationsService extends Service {
418
1254
  this.assertActive();
419
1255
  return this.store.health();
420
1256
  }
1257
+ /** Host-only, content-free projection for recovery/runbook controllers. */
1258
+ inspectSystemOwned(input) {
1259
+ this.assertActive();
1260
+ return this.store.inspectSystemOwned(input);
1261
+ }
1262
+ /** Host-only bounded identity inventory; definitions and scopes never leave the store. */
1263
+ listSystemOwned(input) {
1264
+ this.assertActive();
1265
+ return this.store.listSystemOwned(input);
1266
+ }
1267
+ /** Host-only exact CAS used by fleet owners to retire removed jobs safely. */
1268
+ pauseSystemOwned(input) {
1269
+ this.assertActive();
1270
+ const current = this.store.get(input.automationId);
1271
+ if (current === undefined || current.owner !== input.owner) {
1272
+ throw new AssistantAutomationsError('not-found', 'system-owned automation was not found for the exact owner');
1273
+ }
1274
+ if (this.store.getDefinitionHash(input.automationId) !== input.definitionHash) {
1275
+ throw new AssistantAutomationsError('invalid-input', 'system pause definition hash does not match the current automation definition');
1276
+ }
1277
+ const decision = this.policy.authorize({
1278
+ subject: {
1279
+ kind: 'background', id: input.owner,
1280
+ workspace: current.definition.workspace, principal: current.definition.principal,
1281
+ },
1282
+ action: 'reconcile',
1283
+ resource: { kind: 'automation', id: input.automationId },
1284
+ context: { initiator: 'background' },
1285
+ }, { idempotencyKey: [
1286
+ 'automation-system-pause', input.owner, input.automationId,
1287
+ input.definitionHash, input.expectedVersion, input.operationId,
1288
+ ].join(':') });
1289
+ if (decision.effect !== 'allow')
1290
+ throw policyError(decision);
1291
+ return this.store.pauseSystemOwned(input);
1292
+ }
1293
+ resolveQualityEvidence(input) {
1294
+ this.assertActive();
1295
+ return this.store.resolveQualityEvidence(input);
1296
+ }
1297
+ resolveDeliveryEvidence(input) {
1298
+ this.assertActive();
1299
+ return this.store.resolveDeliveryEvidence(input);
1300
+ }
1301
+ validateQualityEvidence(receipt) {
1302
+ this.assertActive();
1303
+ return this.store.validateQualityEvidence(receipt);
1304
+ }
1305
+ /** Process-local Host executor registration; never exposed as a model tool. */
1306
+ registerHostExecutor(executor) {
1307
+ this.assertActive();
1308
+ return this.hostExecutors.register(executor);
1309
+ }
1310
+ /**
1311
+ * Host/operator seam for one exact circuit repair. This is deliberately not
1312
+ * registered as a model tool. The current immutable definition hash must
1313
+ * still match, so an old repaired hash cannot clear a newer definition or an
1314
+ * ABA-reverted definition behind the operator's back.
1315
+ */
1316
+ probeCircuit(input) {
1317
+ this.assertActive();
1318
+ const current = this.store.get(input.automationId);
1319
+ if (current === undefined || current.owner !== input.owner) {
1320
+ throw new AssistantAutomationsError('not-found', 'system-owned circuit was not found for the exact owner');
1321
+ }
1322
+ if (this.store.getDefinitionHash(input.automationId) !== input.definitionHash) {
1323
+ throw new AssistantAutomationsError('invalid-input', 'circuit probe definition hash does not match the current automation definition');
1324
+ }
1325
+ const decision = this.policy.authorize({
1326
+ subject: {
1327
+ kind: 'background', id: input.owner,
1328
+ workspace: current.definition.workspace, principal: current.definition.principal,
1329
+ },
1330
+ action: 'repair',
1331
+ resource: { kind: 'automation', id: `${input.automationId}:circuit:${input.definitionHash}` },
1332
+ context: { initiator: 'background' },
1333
+ }, { idempotencyKey: [
1334
+ 'automation-circuit-probe', input.owner, input.automationId,
1335
+ input.definitionHash, input.expectedCircuitVersion, input.operationId,
1336
+ ].join(':') });
1337
+ if (decision.effect !== 'allow')
1338
+ throw policyError(decision);
1339
+ const leaseMs = input.leaseMs
1340
+ ?? Math.min(86_400_000, Math.max(this.config.taskLeaseMs, current.definition.timeoutMs));
1341
+ return this.store.armCircuitProbe({
1342
+ owner: input.owner,
1343
+ operationId: input.operationId,
1344
+ automationId: input.automationId,
1345
+ definitionHash: input.definitionHash,
1346
+ expectedVersion: input.expectedCircuitVersion,
1347
+ now: Date.now(),
1348
+ leaseMs,
1349
+ });
1350
+ }
1351
+ /**
1352
+ * Recovery seam that atomically arms an exact circuit and schedules its one
1353
+ * production canary. Unlike probeCircuit(), callers cannot leave a durable
1354
+ * half-open circuit without a corresponding task.
1355
+ */
1356
+ probeCircuitAndScheduleCanary(input) {
1357
+ this.assertActive();
1358
+ const current = this.store.get(input.automationId);
1359
+ if (current === undefined || current.owner !== input.owner) {
1360
+ throw new AssistantAutomationsError('not-found', 'system-owned circuit was not found for the exact owner');
1361
+ }
1362
+ if (this.store.getDefinitionHash(input.automationId) !== input.definitionHash) {
1363
+ throw new AssistantAutomationsError('invalid-input', 'circuit canary definition hash does not match the current automation definition');
1364
+ }
1365
+ const decision = this.policy.authorize({
1366
+ subject: {
1367
+ kind: 'background', id: input.owner,
1368
+ workspace: current.definition.workspace, principal: current.definition.principal,
1369
+ },
1370
+ action: 'repair',
1371
+ resource: { kind: 'automation', id: `${input.automationId}:circuit:${input.definitionHash}` },
1372
+ context: { initiator: 'background' },
1373
+ }, { idempotencyKey: [
1374
+ 'automation-circuit-canary', input.owner, input.automationId,
1375
+ input.definitionHash, input.expectedCircuitVersion, input.operationId,
1376
+ ].join(':') });
1377
+ if (decision.effect !== 'allow')
1378
+ throw policyError(decision);
1379
+ const leaseMs = input.leaseMs
1380
+ ?? Math.min(86_400_000, Math.max(this.config.taskLeaseMs, current.definition.timeoutMs));
1381
+ return this.store.probeCircuitAndScheduleCanary({
1382
+ owner: input.owner,
1383
+ operationId: input.operationId,
1384
+ automationId: input.automationId,
1385
+ definitionHash: input.definitionHash,
1386
+ expectedVersion: input.expectedCircuitVersion,
1387
+ now: Date.now(),
1388
+ leaseMs,
1389
+ });
1390
+ }
421
1391
  start() {
422
1392
  this.assertActive();
423
1393
  this.coordinator.start();
@@ -455,7 +1425,8 @@ export class AssistantAutomationsService extends Service {
455
1425
  const dispatch = delivery.prepareAgentApproval(agent, {
456
1426
  sourceId: 'dsh-enhanced-assistant-automations',
457
1427
  });
458
- if (dispatch.sourceId !== 'dsh-enhanced-assistant-automations'
1428
+ if (dispatch.routeVersion !== 2
1429
+ || dispatch.sourceId !== 'dsh-enhanced-assistant-automations'
459
1430
  || dispatch.workspace !== identity.workspace
460
1431
  || dispatch.bindingId.trim() === ''
461
1432
  || dispatch.principal.trim() === '') {