@dsh-enhanced/assistant-automations 0.1.6 → 0.1.12
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 +39 -7
- package/lib/coordinator.d.ts +57 -5
- package/lib/coordinator.d.ts.map +1 -1
- package/lib/coordinator.js +508 -88
- 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 +525 -73
- package/lib/runner.js.map +1 -1
- package/lib/service.d.ts +110 -4
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +1021 -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 +450 -2
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +211 -1
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +2260 -58
- package/lib/store.js.map +1 -1
- package/lib/tools.d.ts.map +1 -1
- package/lib/tools.js +18 -6
- package/lib/tools.js.map +1 -1
- package/lib/types.d.ts +399 -9
- 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 +16 -5
package/lib/service.js
CHANGED
|
@@ -1,13 +1,40 @@
|
|
|
1
|
-
import { isAbsolute } from 'node:path';
|
|
1
|
+
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,6 +265,9 @@ export class AssistantAutomationsService extends Service {
|
|
|
169
265
|
this.coordinator.setOutcomeRecorder(recorder);
|
|
170
266
|
return () => this.coordinator.setOutcomeRecorder(undefined);
|
|
171
267
|
});
|
|
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`.
|
|
172
271
|
if (config.schedulerEnabled)
|
|
173
272
|
this.coordinator.start();
|
|
174
273
|
if (config.reconcileIntervalMs > 0) {
|
|
@@ -189,11 +288,712 @@ export class AssistantAutomationsService extends Service {
|
|
|
189
288
|
ctx.inject(['tools'], toolsCtx => registerAutomationTools(toolsCtx, this));
|
|
190
289
|
ctx.effect(() => async () => {
|
|
191
290
|
this.active = false;
|
|
291
|
+
this.evaluationSink = undefined;
|
|
292
|
+
this.presentationSink?.dispose();
|
|
293
|
+
this.coordinator.setEvaluationRecorder(undefined);
|
|
192
294
|
await this.coordinator.stop();
|
|
193
295
|
this.proposalStore.close();
|
|
296
|
+
this.growthStore.close();
|
|
194
297
|
this.store.close();
|
|
195
298
|
}, 'assistant-automations.runtime');
|
|
196
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
|
+
}
|
|
197
997
|
propose(agent, input) {
|
|
198
998
|
const identity = this.authorizeAgent(agent, 'propose', input.mutation.automationId, `automation-propose:${input.idempotencyKey}`);
|
|
199
999
|
const approval = this.resolveApprovalRoute(agent, identity, input.principal);
|
|
@@ -241,7 +1041,8 @@ export class AssistantAutomationsService extends Service {
|
|
|
241
1041
|
if (current !== undefined && (current.definition.workspace !== identity.workspace
|
|
242
1042
|
|| current.definition.agentPreset !== identity.agentPreset
|
|
243
1043
|
|| current.definition.principal !== approval.principal
|
|
244
|
-
|| current.definition.
|
|
1044
|
+
|| (current.definition.approvalBindingId ?? current.definition.deliveryBindingId)
|
|
1045
|
+
!== approval.dispatch?.bindingId)) {
|
|
245
1046
|
throw new AssistantAutomationsError('missing-approval-route', 'automation lifecycle proposal does not match the immutable owner route');
|
|
246
1047
|
}
|
|
247
1048
|
mutation = input.mutation;
|
|
@@ -301,15 +1102,42 @@ export class AssistantAutomationsService extends Service {
|
|
|
301
1102
|
return this.store.list();
|
|
302
1103
|
}
|
|
303
1104
|
history(agent, input = {}) {
|
|
304
|
-
this.authorizeAgent(agent, 'history', input.automationId ?? 'catalog');
|
|
1105
|
+
const identity = this.authorizeAgent(agent, 'history', input.automationId ?? 'catalog');
|
|
1106
|
+
if (input.runId !== undefined && input.automationId !== undefined) {
|
|
1107
|
+
throw new AssistantAutomationsError('invalid-input', 'history accepts runId or automationId, not both');
|
|
1108
|
+
}
|
|
305
1109
|
const limit = input.limit ?? 20;
|
|
1110
|
+
if (input.runId !== undefined) {
|
|
1111
|
+
if (!/^run-task-occ-[a-f0-9]{64}$/u.test(input.runId)) {
|
|
1112
|
+
throw new AssistantAutomationsError('invalid-input', 'runId is not a canonical Automation run id');
|
|
1113
|
+
}
|
|
1114
|
+
const run = this.store.getRun(input.runId);
|
|
1115
|
+
const snapshot = run === undefined ? undefined : this.store.getRunExecutionSnapshot(run.id);
|
|
1116
|
+
const sameScope = snapshot !== undefined
|
|
1117
|
+
&& resolve(snapshot.definition.workspace.normalize('NFC')) === resolve(identity.workspace.normalize('NFC'))
|
|
1118
|
+
&& snapshot.definition.agentPreset.normalize('NFC').trim()
|
|
1119
|
+
=== identity.agentPreset.normalize('NFC').trim();
|
|
1120
|
+
if (run === undefined || !sameScope) {
|
|
1121
|
+
// Do not reveal whether a cross-scope run exists.
|
|
1122
|
+
throw new AssistantAutomationsError('not-found', 'automation run was not found in the current Agent scope');
|
|
1123
|
+
}
|
|
1124
|
+
const occurrence = this.store.getOccurrence(run.occurrenceId);
|
|
1125
|
+
if (occurrence === undefined) {
|
|
1126
|
+
throw new AssistantAutomationsError('not-found', 'automation run occurrence was not found');
|
|
1127
|
+
}
|
|
1128
|
+
return { occurrences: [occurrence], runs: [run] };
|
|
1129
|
+
}
|
|
1130
|
+
const runs = this.store.listRunsForExecutionScope({
|
|
1131
|
+
...identity,
|
|
1132
|
+
...(input.automationId === undefined ? {} : { automationId: input.automationId }),
|
|
1133
|
+
limit,
|
|
1134
|
+
});
|
|
306
1135
|
return {
|
|
307
|
-
occurrences:
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
runs: this.store.listRuns({
|
|
311
|
-
...(input.automationId === undefined ? {} : { automationId: input.automationId }), limit,
|
|
1136
|
+
occurrences: runs.flatMap(value => {
|
|
1137
|
+
const occurrence = this.store.getOccurrence(value.occurrenceId);
|
|
1138
|
+
return occurrence === undefined ? [] : [occurrence];
|
|
312
1139
|
}),
|
|
1140
|
+
runs,
|
|
313
1141
|
};
|
|
314
1142
|
}
|
|
315
1143
|
async runDry(agent, input) {
|
|
@@ -331,6 +1159,48 @@ export class AssistantAutomationsService extends Service {
|
|
|
331
1159
|
throw new Error('assistant-automations: dry-run task was not claimed by this duty owner');
|
|
332
1160
|
return { occurrence: this.store.getOccurrence(occurrence.id), run };
|
|
333
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
|
+
}
|
|
334
1204
|
ingestExternal(input) {
|
|
335
1205
|
this.assertActive();
|
|
336
1206
|
const automation = this.store.get(input.automationId);
|
|
@@ -384,6 +1254,140 @@ export class AssistantAutomationsService extends Service {
|
|
|
384
1254
|
this.assertActive();
|
|
385
1255
|
return this.store.health();
|
|
386
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
|
+
}
|
|
387
1391
|
start() {
|
|
388
1392
|
this.assertActive();
|
|
389
1393
|
this.coordinator.start();
|
|
@@ -421,7 +1425,8 @@ export class AssistantAutomationsService extends Service {
|
|
|
421
1425
|
const dispatch = delivery.prepareAgentApproval(agent, {
|
|
422
1426
|
sourceId: 'dsh-enhanced-assistant-automations',
|
|
423
1427
|
});
|
|
424
|
-
if (dispatch.
|
|
1428
|
+
if (dispatch.routeVersion !== 2
|
|
1429
|
+
|| dispatch.sourceId !== 'dsh-enhanced-assistant-automations'
|
|
425
1430
|
|| dispatch.workspace !== identity.workspace
|
|
426
1431
|
|| dispatch.bindingId.trim() === ''
|
|
427
1432
|
|| dispatch.principal.trim() === '') {
|