@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.
- package/README.md +37 -7
- package/lib/coordinator.d.ts +45 -5
- package/lib/coordinator.d.ts.map +1 -1
- package/lib/coordinator.js +418 -86
- package/lib/coordinator.js.map +1 -1
- package/lib/growth.d.ts +120 -0
- package/lib/growth.d.ts.map +1 -0
- package/lib/growth.js +383 -0
- package/lib/growth.js.map +1 -0
- package/lib/host-executors.d.ts +25 -0
- package/lib/host-executors.d.ts.map +1 -0
- package/lib/host-executors.js +181 -0
- package/lib/host-executors.js.map +1 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/operator.d.ts +2 -0
- package/lib/operator.d.ts.map +1 -1
- package/lib/operator.js +6 -2
- package/lib/operator.js.map +1 -1
- package/lib/proposals.d.ts +2 -2
- package/lib/proposals.d.ts.map +1 -1
- package/lib/proposals.js +34 -22
- package/lib/proposals.js.map +1 -1
- package/lib/runner.d.ts +28 -2
- package/lib/runner.d.ts.map +1 -1
- package/lib/runner.js +524 -73
- package/lib/runner.js.map +1 -1
- package/lib/schedule.d.ts.map +1 -1
- package/lib/schedule.js +37 -9
- package/lib/schedule.js.map +1 -1
- package/lib/service.d.ts +109 -4
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +987 -16
- 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 +369 -41
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +175 -1
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +1889 -71
- package/lib/store.js.map +1 -1
- package/lib/tools.js +1 -1
- package/lib/tools.js.map +1 -1
- package/lib/types.d.ts +354 -11
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js +13 -1
- package/lib/types.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.d.ts.map +1 -1
- package/lib/version.js +1 -1
- package/lib/version.js.map +1 -1
- 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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
173
|
-
//
|
|
174
|
-
|
|
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.
|
|
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.
|
|
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() === '') {
|