@dsh-enhanced/assistant-evolution 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 +58 -19
- package/cordis.patch.yml +3 -1
- package/lib/guidance.d.ts +2 -0
- package/lib/guidance.d.ts.map +1 -1
- package/lib/guidance.js +7 -1
- package/lib/guidance.js.map +1 -1
- package/lib/index.d.ts +2 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -2
- package/lib/index.js.map +1 -1
- package/lib/review.d.ts +1 -1
- package/lib/review.d.ts.map +1 -1
- package/lib/review.js +22 -0
- package/lib/review.js.map +1 -1
- package/lib/service.d.ts +188 -6
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +903 -45
- 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 +449 -7
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +98 -3
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +1493 -107
- package/lib/store.js.map +1 -1
- package/lib/tools.d.ts.map +1 -1
- package/lib/tools.js +133 -2
- package/lib/tools.js.map +1 -1
- package/lib/types.d.ts +192 -11
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js +2 -0
- 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 +14 -4
package/lib/service.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { isAbsolute, resolve } from 'node:path';
|
|
3
3
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
4
4
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
5
5
|
import Schema from '@deepseek-ai/schemastery';
|
|
6
|
+
import { canonicalEvaluationHostScope, evaluationLearningProjectionDigest, } from '@dsh-enhanced/assistant-evaluation';
|
|
6
7
|
import { ApprovalSettlementConflict, validateApprovalSettlement, } from '@dsh-enhanced/assistant-policy';
|
|
7
8
|
import { buildGuidance, buildGuidanceSnapshot } from './guidance.js';
|
|
8
9
|
import { evolutionMutationReview } from './review.js';
|
|
9
|
-
import { EvolutionStore, EvolutionStoreError, evolutionDigest } from './store.js';
|
|
10
|
+
import { EvolutionStore, EvolutionStoreError, evolutionDigest, supervisedGrowthAnalystProposalIdempotencyKey, } from './store.js';
|
|
10
11
|
import { registerEvolutionTools } from './tools.js';
|
|
11
|
-
import { legacyEvolutionScope } from './types.js';
|
|
12
|
+
import { legacyEvolutionScope, SUPERVISED_GROWTH_ANALYST_CONTRACT_VERSION, } from './types.js';
|
|
12
13
|
const configSchema = Schema.object({
|
|
13
14
|
databasePath: Schema.string().required(),
|
|
14
15
|
evaluationWindow: Schema.number().step(1).min(1).max(10_000).default(20),
|
|
@@ -26,6 +27,17 @@ const configSchema = Schema.object({
|
|
|
26
27
|
autonomousRollback: Schema.boolean().default(false),
|
|
27
28
|
});
|
|
28
29
|
const evolutionApprovalSource = 'dsh-enhanced-assistant-evolution';
|
|
30
|
+
function registrationOwnedByDeliveryPresentation(registration) {
|
|
31
|
+
const candidate = registration;
|
|
32
|
+
try {
|
|
33
|
+
return typeof candidate.owner === 'object' && candidate.owner !== null
|
|
34
|
+
&& typeof candidate.owner.ownsTrustedDeliveryPresentationRegistration === 'function'
|
|
35
|
+
&& candidate.owner.ownsTrustedDeliveryPresentationRegistration(registration);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
29
41
|
export class AssistantEvolutionError extends Error {
|
|
30
42
|
code;
|
|
31
43
|
constructor(code, message) {
|
|
@@ -34,6 +46,51 @@ export class AssistantEvolutionError extends Error {
|
|
|
34
46
|
this.name = 'AssistantEvolutionError';
|
|
35
47
|
}
|
|
36
48
|
}
|
|
49
|
+
/** Fixed Policy subject used by the non-model supervised-growth runbook. */
|
|
50
|
+
export const HOST_RECOVERY_BACKGROUND_ID = 'dsh-enhanced-assistant-recovery';
|
|
51
|
+
/** The only Automation allowed to use the model-visible analyst capability. */
|
|
52
|
+
export const SUPERVISED_GROWTH_ANALYST_AUTOMATION_ID = 'heartbeat:supervised-growth-analyst';
|
|
53
|
+
const evolutionHostScopeBrand = Symbol('assistant-evolution.host-scope');
|
|
54
|
+
/** Canonical, immutable and intentionally non-serializable Host scope token. */
|
|
55
|
+
export function canonicalEvolutionHostScope(input) {
|
|
56
|
+
const scopeKey = canonicalEvolutionScope(input.workspace, input.preset);
|
|
57
|
+
const [workspace, preset] = JSON.parse(scopeKey);
|
|
58
|
+
const branded = { workspace, preset };
|
|
59
|
+
Object.defineProperty(branded, evolutionHostScopeBrand, {
|
|
60
|
+
value: true, enumerable: false, configurable: false, writable: false,
|
|
61
|
+
});
|
|
62
|
+
return Object.freeze(branded);
|
|
63
|
+
}
|
|
64
|
+
function exactEvolutionHostScope(input) {
|
|
65
|
+
if (typeof input !== 'object' || input === null || !Object.isFrozen(input)
|
|
66
|
+
|| input[evolutionHostScopeBrand] !== true) {
|
|
67
|
+
throw new AssistantEvolutionError('invalid-input', 'Host evolution operations require a canonical immutable scope');
|
|
68
|
+
}
|
|
69
|
+
const canonical = canonicalEvolutionHostScope(input);
|
|
70
|
+
if (canonical.workspace !== input.workspace || canonical.preset !== input.preset) {
|
|
71
|
+
throw new AssistantEvolutionError('invalid-input', 'Host evolution scope is not canonical');
|
|
72
|
+
}
|
|
73
|
+
return input;
|
|
74
|
+
}
|
|
75
|
+
function hostText(value, label, maxBytes) {
|
|
76
|
+
if (typeof value !== 'string') {
|
|
77
|
+
throw new AssistantEvolutionError('invalid-input', `${label} must be a string`);
|
|
78
|
+
}
|
|
79
|
+
const normalized = value.normalize('NFC').trim();
|
|
80
|
+
if (normalized === '' || Buffer.byteLength(normalized, 'utf8') > maxBytes
|
|
81
|
+
|| hasControl(normalized)) {
|
|
82
|
+
throw new AssistantEvolutionError('invalid-input', `${label} is invalid`);
|
|
83
|
+
}
|
|
84
|
+
return normalized;
|
|
85
|
+
}
|
|
86
|
+
function hasControl(value) {
|
|
87
|
+
for (const character of value) {
|
|
88
|
+
const codePoint = character.codePointAt(0);
|
|
89
|
+
if (codePoint <= 0x1f || codePoint === 0x7f)
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
37
94
|
export function canonicalEvolutionScope(workspace, preset) {
|
|
38
95
|
const normalizedPreset = preset.normalize('NFC').trim();
|
|
39
96
|
if (!isAbsolute(workspace) || normalizedPreset === '') {
|
|
@@ -76,13 +133,15 @@ function ruleIdFromStableMutation(stable) {
|
|
|
76
133
|
*/
|
|
77
134
|
export class AssistantEvolutionService extends Service {
|
|
78
135
|
static Config = configSchema;
|
|
79
|
-
static inject = ['assistantPolicy'];
|
|
136
|
+
static inject = ['assistantPolicy', 'assistantEvaluation'];
|
|
80
137
|
store;
|
|
81
138
|
policy;
|
|
139
|
+
context;
|
|
82
140
|
config;
|
|
83
|
-
injected = new WeakSet();
|
|
84
141
|
now;
|
|
85
142
|
delivery;
|
|
143
|
+
presentationProducerGeneration = `assistant-evolution-presentation:${randomUUID()}`;
|
|
144
|
+
presentationSink;
|
|
86
145
|
active = true;
|
|
87
146
|
lastReconciledAt = 0;
|
|
88
147
|
constructor(ctx, input, options = {}) {
|
|
@@ -96,6 +155,7 @@ export class AssistantEvolutionService extends Service {
|
|
|
96
155
|
const policy = ctx.get('assistantPolicy');
|
|
97
156
|
if (policy === undefined)
|
|
98
157
|
throw new Error('assistant-evolution: assistantPolicy service is required');
|
|
158
|
+
this.context = ctx;
|
|
99
159
|
this.policy = policy;
|
|
100
160
|
this.now = options.now ?? Date.now;
|
|
101
161
|
this.store = new EvolutionStore({
|
|
@@ -105,6 +165,7 @@ export class AssistantEvolutionService extends Service {
|
|
|
105
165
|
ctx.inject(['assistantDelivery'], deliveryCtx => {
|
|
106
166
|
const delivery = deliveryCtx.get('assistantDelivery');
|
|
107
167
|
this.delivery = delivery;
|
|
168
|
+
this.reconcileApplicationPresentations();
|
|
108
169
|
return () => {
|
|
109
170
|
if (this.delivery === delivery)
|
|
110
171
|
this.delivery = undefined;
|
|
@@ -124,6 +185,7 @@ export class AssistantEvolutionService extends Service {
|
|
|
124
185
|
});
|
|
125
186
|
ctx.effect(() => () => {
|
|
126
187
|
this.active = false;
|
|
188
|
+
this.presentationSink?.dispose();
|
|
127
189
|
this.store.close();
|
|
128
190
|
}, 'assistant-evolution.database');
|
|
129
191
|
if (this.config.reconcileIntervalMs > 0) {
|
|
@@ -149,7 +211,47 @@ export class AssistantEvolutionService extends Service {
|
|
|
149
211
|
lastReconciledAt: this.lastReconciledAt,
|
|
150
212
|
});
|
|
151
213
|
}
|
|
152
|
-
|
|
214
|
+
trustedDeliveryPresentationProducerGeneration() {
|
|
215
|
+
this.assertActive();
|
|
216
|
+
return this.presentationProducerGeneration;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Accept only the publisher minted by the exact live Delivery instance.
|
|
220
|
+
* The registration is held privately; it is never surfaced to an Agent,
|
|
221
|
+
* tool schema, proposal payload, or Evolution read API.
|
|
222
|
+
*/
|
|
223
|
+
registerTrustedDeliveryPresentationSink(registration) {
|
|
224
|
+
this.assertActive();
|
|
225
|
+
if (registration.protocol !== 'assistant-delivery/trusted-presentation-producer/v1'
|
|
226
|
+
|| registration.producer !== 'assistant-evolution'
|
|
227
|
+
|| registration.generation !== this.presentationProducerGeneration
|
|
228
|
+
|| typeof registration.publish !== 'function'
|
|
229
|
+
|| !registrationOwnedByDeliveryPresentation(registration)) {
|
|
230
|
+
throw new AssistantEvolutionError('forbidden', 'trusted Delivery presentation registration is invalid');
|
|
231
|
+
}
|
|
232
|
+
const current = this.presentationSink;
|
|
233
|
+
if (current !== undefined) {
|
|
234
|
+
if (current.registration === registration)
|
|
235
|
+
return current.dispose;
|
|
236
|
+
throw new AssistantEvolutionError('forbidden', 'trusted Delivery presentation sink is already registered');
|
|
237
|
+
}
|
|
238
|
+
const token = Symbol('assistant-evolution.trusted-delivery-presentation');
|
|
239
|
+
let live = true;
|
|
240
|
+
const dispose = () => {
|
|
241
|
+
if (!live)
|
|
242
|
+
return;
|
|
243
|
+
live = false;
|
|
244
|
+
if (this.presentationSink?.token === token)
|
|
245
|
+
this.presentationSink = undefined;
|
|
246
|
+
};
|
|
247
|
+
this.presentationSink = Object.freeze({ token, registration, dispose });
|
|
248
|
+
// Existing durable terminal receipts become eligible as soon as their
|
|
249
|
+
// exact Delivery publisher appears; the local outbox remains authoritative
|
|
250
|
+
// if Delivery rejects or later revokes this generation.
|
|
251
|
+
this.reconcileApplicationPresentations();
|
|
252
|
+
return dispose;
|
|
253
|
+
}
|
|
254
|
+
/** Record one model-reported outcome as learning-ineligible operational audit. */
|
|
153
255
|
recordEpisode(agent, input) {
|
|
154
256
|
const scopeKey = this.authorize(agent, 'append', `situation:${input.situation}`);
|
|
155
257
|
return this.store.recordEpisode({
|
|
@@ -159,14 +261,17 @@ export class AssistantEvolutionService extends Service {
|
|
|
159
261
|
detail: input.detail,
|
|
160
262
|
source: 'foreground',
|
|
161
263
|
trust: 'self-reported',
|
|
264
|
+
evidenceKind: 'operational',
|
|
162
265
|
...(input.ruleId === undefined ? {} : { claimedRuleId: input.ruleId }),
|
|
163
266
|
occurredAt: input.occurredAt,
|
|
164
267
|
idempotencyKey: input.idempotencyKey,
|
|
165
268
|
});
|
|
166
269
|
}
|
|
167
270
|
/**
|
|
168
|
-
* Record an outcome observed by trusted local infrastructure rather
|
|
169
|
-
* turn, for example a finished background automation run.
|
|
271
|
+
* Record an execution outcome observed by trusted local infrastructure rather
|
|
272
|
+
* than a model turn, for example a finished background automation run. It is
|
|
273
|
+
* deliberately operational and learning-ineligible: execution success/failure
|
|
274
|
+
* does not establish objective task quality.
|
|
170
275
|
*
|
|
171
276
|
* There is no Agent to authorize here, so this is deliberately narrow: it can
|
|
172
277
|
* only append evidence. It cannot adopt, retire, or read rules, so an automation
|
|
@@ -200,11 +305,96 @@ export class AssistantEvolutionService extends Service {
|
|
|
200
305
|
scopeKey,
|
|
201
306
|
source: 'automation',
|
|
202
307
|
trust: trusted ? 'trusted' : 'legacy',
|
|
308
|
+
evidenceKind: trusted ? 'operational' : 'legacy-unknown',
|
|
203
309
|
...(attributed
|
|
204
310
|
? { ruleId: receipt.ruleId, guidanceVersion: receipt.guidanceVersion }
|
|
205
311
|
: input.ruleId === undefined ? {} : { claimedRuleId: input.ruleId }),
|
|
206
312
|
});
|
|
207
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Deprecated unsafe v5 shape. Free caller-supplied quality fields can never
|
|
316
|
+
* produce learning-eligible evidence again; use projectEvaluationOutcome().
|
|
317
|
+
*/
|
|
318
|
+
recordEvaluationOutcome(input) {
|
|
319
|
+
this.assertActive();
|
|
320
|
+
void input;
|
|
321
|
+
throw new AssistantEvolutionError('forbidden', 'free-form Evaluation projection is disabled; use projectEvaluationOutcome with an authoritative receipt');
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Project one exact trusted Evaluation row into the learning ledger. The
|
|
325
|
+
* caller cannot supply outcome, detail, kind, time, situation or attribution.
|
|
326
|
+
*/
|
|
327
|
+
projectEvaluationOutcome(input) {
|
|
328
|
+
this.assertActive();
|
|
329
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)
|
|
330
|
+
|| Object.keys(input).sort().join(',') !== 'evaluationId,scope') {
|
|
331
|
+
throw new AssistantEvolutionError('invalid-input', 'Evaluation projection accepts only an exact scope and evaluationId');
|
|
332
|
+
}
|
|
333
|
+
const scope = exactEvolutionHostScope(input.scope);
|
|
334
|
+
const evaluationId = hostText(input.evaluationId, 'evaluationId', 200);
|
|
335
|
+
const receipt = this.trustedTaskLearningReceipt(scope, evaluationId);
|
|
336
|
+
const projected = this.applyTrustedTaskLearningReceipt(scope, receipt);
|
|
337
|
+
if (projected.episode === undefined) {
|
|
338
|
+
throw new AssistantEvolutionError('forbidden', 'the canonical task revision retracts rather than upserts a behavioural learning vote');
|
|
339
|
+
}
|
|
340
|
+
return projected.episode;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Narrow structural seam used only by Evaluation's durable outbox. The raw
|
|
344
|
+
* scope is canonicalized here; quality fields are still fetched exclusively
|
|
345
|
+
* from the trusted Evaluation ledger.
|
|
346
|
+
*/
|
|
347
|
+
projectTrustedEvaluationOutcome(_input) {
|
|
348
|
+
this.assertActive();
|
|
349
|
+
throw new AssistantEvolutionError('forbidden', 'raw Evaluation projection protocol is retired; use the versioned task-revision capability');
|
|
350
|
+
}
|
|
351
|
+
/** Evaluation protocol v2 sink. Every success echoes the exact applied revision. */
|
|
352
|
+
projectTrustedEvaluationTaskRevision(input) {
|
|
353
|
+
this.assertActive();
|
|
354
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)
|
|
355
|
+
|| Object.keys(input).sort().join(',') !== 'evaluationId,scope'
|
|
356
|
+
|| typeof input.scope !== 'object' || input.scope === null || Array.isArray(input.scope)
|
|
357
|
+
|| Object.keys(input.scope).sort().join(',') !== 'preset,workspace') {
|
|
358
|
+
throw new AssistantEvolutionError('invalid-input', 'trusted Evaluation projection input is invalid');
|
|
359
|
+
}
|
|
360
|
+
const evaluationId = hostText(input.evaluationId, 'evaluationId', 200);
|
|
361
|
+
const scope = canonicalEvolutionHostScope(input.scope);
|
|
362
|
+
const receipt = this.trustedTaskLearningReceipt(scope, evaluationId);
|
|
363
|
+
const projected = this.applyTrustedTaskLearningReceipt(scope, receipt);
|
|
364
|
+
return Object.freeze({
|
|
365
|
+
triggerOutcomeId: evaluationId,
|
|
366
|
+
subjectKind: receipt.projection.subjectKind,
|
|
367
|
+
subjectRef: receipt.projection.subjectRef,
|
|
368
|
+
version: receipt.projection.version,
|
|
369
|
+
digest: receipt.projection.digest,
|
|
370
|
+
scopeWatermark: receipt.scopeWatermark,
|
|
371
|
+
disposition: receipt.projection.disposition,
|
|
372
|
+
status: projected.replayed ? 'replayed' : 'applied',
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Called by Assistant Automations only after it installed the immutable
|
|
377
|
+
* production execution context and bound the Policy initiator. This closes
|
|
378
|
+
* the session-start ordering gap without weakening background Policy.
|
|
379
|
+
*/
|
|
380
|
+
injectAutomationGuidance(agent, execution) {
|
|
381
|
+
this.assertActive();
|
|
382
|
+
const current = agent.ctx
|
|
383
|
+
.get('assistantAutomationExecution');
|
|
384
|
+
const installed = current;
|
|
385
|
+
if (typeof current !== 'object' || current === null || Array.isArray(current)
|
|
386
|
+
|| Object.keys(current).sort().join(',') !== 'automationId,mode,occurrenceId'
|
|
387
|
+
|| installed?.mode !== execution.mode
|
|
388
|
+
|| installed.automationId !== execution.automationId
|
|
389
|
+
|| installed.occurrenceId !== execution.occurrenceId
|
|
390
|
+
|| Object.keys(execution).sort().join(',') !== 'automationId,mode,occurrenceId'
|
|
391
|
+
|| execution.mode !== 'production'
|
|
392
|
+
|| hostText(execution.automationId, 'automationId', 500) !== execution.automationId
|
|
393
|
+
|| hostText(execution.occurrenceId, 'occurrenceId', 500) !== execution.occurrenceId) {
|
|
394
|
+
throw new AssistantEvolutionError('forbidden', 'automation guidance requires the exact installed production execution context');
|
|
395
|
+
}
|
|
396
|
+
this.injectGuidance(agent);
|
|
397
|
+
}
|
|
208
398
|
/** Query an exact post-injection receipt for trusted automation attribution. */
|
|
209
399
|
async captureAutomationExposure(input) {
|
|
210
400
|
this.assertActive();
|
|
@@ -221,20 +411,151 @@ export class AssistantEvolutionService extends Service {
|
|
|
221
411
|
/** Candidate rule changes implied by current evidence. Never auto-applied. */
|
|
222
412
|
candidates(agent) {
|
|
223
413
|
const scopeKey = this.authorize(agent, 'inspect', 'candidates');
|
|
224
|
-
return this.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
414
|
+
return this.candidatesForScope(scopeKey);
|
|
415
|
+
}
|
|
416
|
+
/** Agent-free, exact-scope candidate inspection for the fixed Host runbook. */
|
|
417
|
+
hostCandidates(input) {
|
|
418
|
+
const scopeKey = this.authorizeHost(input, 'inspect', 'candidates');
|
|
419
|
+
return this.candidatesForScope(scopeKey);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Review exactly one adoption candidate from the dedicated production
|
|
423
|
+
* analyst. The random review token is durable and bound to this occurrence;
|
|
424
|
+
* no foreground or preview session can obtain one.
|
|
425
|
+
*/
|
|
426
|
+
reviewSupervisedGrowthAdoption(agent) {
|
|
427
|
+
const execution = this.authorizeSupervisedGrowthAnalyst(agent, 'inspect', 'analyst-adoption');
|
|
428
|
+
// Even review requires the route that a later proposal will use. This
|
|
429
|
+
// prevents an analyst with no owner destination from minting stale tokens.
|
|
430
|
+
this.approvalRoute(agent, undefined);
|
|
431
|
+
const candidate = this.candidatesForScope(execution.scopeKey)
|
|
432
|
+
.filter(entry => entry.kind === 'adopt')
|
|
433
|
+
.sort((left, right) => {
|
|
434
|
+
const rateOrder = right.stats.failures * left.stats.total
|
|
435
|
+
- left.stats.failures * right.stats.total;
|
|
436
|
+
return rateOrder || right.stats.failures - left.stats.failures
|
|
437
|
+
|| right.stats.total - left.stats.total
|
|
438
|
+
|| left.situation.localeCompare(right.situation);
|
|
439
|
+
})[0];
|
|
440
|
+
if (candidate === undefined) {
|
|
441
|
+
return Object.freeze({ contractVersion: SUPERVISED_GROWTH_ANALYST_CONTRACT_VERSION });
|
|
442
|
+
}
|
|
443
|
+
const frozen = this.store.registerSupervisedGrowthAnalystReview({
|
|
444
|
+
scopeKey: execution.scopeKey,
|
|
445
|
+
occurrenceId: execution.occurrenceId,
|
|
446
|
+
candidate,
|
|
447
|
+
evidenceWindow: this.config.evaluationWindow,
|
|
448
|
+
});
|
|
449
|
+
return Object.freeze({
|
|
450
|
+
contractVersion: SUPERVISED_GROWTH_ANALYST_CONTRACT_VERSION,
|
|
451
|
+
candidate: Object.freeze({
|
|
452
|
+
...frozen.review.evidence,
|
|
453
|
+
reviewToken: frozen.review.reviewToken,
|
|
454
|
+
proposalExists: frozen.proposalExists,
|
|
455
|
+
}),
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Turn one frozen analyst review into the normal owner approval. Identity is
|
|
460
|
+
* evidence-derived; wording changes and concurrent executions join the first
|
|
461
|
+
* durable proposal instead of producing several cards.
|
|
462
|
+
*/
|
|
463
|
+
proposeSupervisedGrowthAdoption(agent, input) {
|
|
464
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)
|
|
465
|
+
|| Object.keys(input).sort().join(',') !== 'guidance,reviewToken') {
|
|
466
|
+
throw new AssistantEvolutionError('invalid-input', 'analyst proposal accepts only reviewToken and guidance');
|
|
467
|
+
}
|
|
468
|
+
const execution = this.authorizeSupervisedGrowthAnalyst(agent, 'propose', 'proposals');
|
|
469
|
+
const route = this.approvalRoute(agent, undefined);
|
|
470
|
+
const reviewToken = hostText(input.reviewToken, 'reviewToken', 200);
|
|
471
|
+
const review = this.store.getSupervisedGrowthAnalystReview(reviewToken);
|
|
472
|
+
if (review === undefined || review.scopeKey !== execution.scopeKey
|
|
473
|
+
|| review.occurrenceId !== execution.occurrenceId) {
|
|
474
|
+
throw new AssistantEvolutionError('not-found', 'analyst review token does not belong to this production execution');
|
|
475
|
+
}
|
|
476
|
+
const evidence = review.evidence;
|
|
477
|
+
if (evidence.contractVersion !== SUPERVISED_GROWTH_ANALYST_CONTRACT_VERSION
|
|
478
|
+
|| evidence.total < this.config.minSample
|
|
479
|
+
|| evidence.failures / evidence.total < this.config.adoptFailureRate) {
|
|
480
|
+
throw new AssistantEvolutionError('forbidden', 'analyst review no longer satisfies adoption thresholds');
|
|
481
|
+
}
|
|
482
|
+
const idempotencyKey = supervisedGrowthAnalystProposalIdempotencyKey({
|
|
483
|
+
scopeKey: execution.scopeKey,
|
|
484
|
+
situation: evidence.situation,
|
|
485
|
+
evidenceDigest: evidence.evidenceDigest,
|
|
486
|
+
evidenceTotal: evidence.evidenceTotal,
|
|
487
|
+
contractVersion: evidence.contractVersion,
|
|
232
488
|
});
|
|
489
|
+
const ruleId = ruleIdFromStableMutation(evolutionDigest([
|
|
490
|
+
'supervised-growth-analyst-rule/v1',
|
|
491
|
+
idempotencyKey,
|
|
492
|
+
]));
|
|
493
|
+
const mutation = {
|
|
494
|
+
op: 'adopt',
|
|
495
|
+
ruleId,
|
|
496
|
+
input: {
|
|
497
|
+
scopeKey: execution.scopeKey,
|
|
498
|
+
situation: evidence.situation,
|
|
499
|
+
guidance: input.guidance,
|
|
500
|
+
},
|
|
501
|
+
baseline: {
|
|
502
|
+
scopeKey: execution.scopeKey,
|
|
503
|
+
situation: evidence.situation,
|
|
504
|
+
failures: evidence.failures,
|
|
505
|
+
total: evidence.total,
|
|
506
|
+
},
|
|
507
|
+
evidence: {
|
|
508
|
+
sampleEpisodeIds: evidence.sampleEpisodeIds,
|
|
509
|
+
digest: evidence.evidenceDigest,
|
|
510
|
+
total: evidence.evidenceTotal,
|
|
511
|
+
window: evidence.evidenceWindow,
|
|
512
|
+
scopeWatermark: evidence.scopeWatermark,
|
|
513
|
+
taskRevisions: evidence.taskRevisions,
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
const ttlMs = this.config.defaultProposalTtlMs;
|
|
517
|
+
const requester = `automation:${SUPERVISED_GROWTH_ANALYST_AUTOMATION_ID}`;
|
|
518
|
+
const reviewPresentation = evolutionMutationReview(mutation);
|
|
519
|
+
const creationIntent = {
|
|
520
|
+
idempotencyKey,
|
|
521
|
+
requester,
|
|
522
|
+
principal: route.principal,
|
|
523
|
+
action: reviewPresentation.action,
|
|
524
|
+
resource: reviewPresentation.resource,
|
|
525
|
+
diff: reviewPresentation.diff,
|
|
526
|
+
summary: reviewPresentation.summary,
|
|
527
|
+
ttlMs,
|
|
528
|
+
...(route.dispatch === undefined ? {} : { dispatch: route.dispatch }),
|
|
529
|
+
};
|
|
530
|
+
const local = this.withLearningWriterFence(mutation, () => (this.store.createSupervisedGrowthAnalystProposal({
|
|
531
|
+
reviewToken,
|
|
532
|
+
scopeKey: execution.scopeKey,
|
|
533
|
+
occurrenceId: execution.occurrenceId,
|
|
534
|
+
requester,
|
|
535
|
+
principal: route.principal,
|
|
536
|
+
mutation,
|
|
537
|
+
expiresAt: Date.now() + ttlMs,
|
|
538
|
+
creationIntent,
|
|
539
|
+
})));
|
|
540
|
+
if (local.proposal.policyProposalId !== undefined) {
|
|
541
|
+
if (local.proposal.status !== 'pending') {
|
|
542
|
+
return result(local.proposal, true, local.proposal.resultRuleId === undefined
|
|
543
|
+
? undefined
|
|
544
|
+
: this.store.getRule(local.proposal.resultRuleId));
|
|
545
|
+
}
|
|
546
|
+
return this.settleAttached(local.proposal, true) ?? result(local.proposal, true);
|
|
547
|
+
}
|
|
548
|
+
return this.submitLocalProposal(local.proposal, local.replayed);
|
|
233
549
|
}
|
|
234
550
|
listRules(agent, status) {
|
|
235
551
|
const scopeKey = this.authorize(agent, 'inspect', 'rules');
|
|
236
552
|
return this.store.listRules(scopeKey, status);
|
|
237
553
|
}
|
|
554
|
+
/** Agent-free, exact-scope rule inspection for the fixed Host runbook. */
|
|
555
|
+
hostListRules(input) {
|
|
556
|
+
const scopeKey = this.authorizeHost(input, 'inspect', `rules:${input.status ?? 'all'}`);
|
|
557
|
+
return this.store.listRules(scopeKey, input.status);
|
|
558
|
+
}
|
|
238
559
|
/**
|
|
239
560
|
* Remove one exact active guidance generation through the opt-in low-risk lane.
|
|
240
561
|
*
|
|
@@ -243,11 +564,42 @@ export class AssistantEvolutionService extends Service {
|
|
|
243
564
|
* delegates the evidence/risk/reason decision to one transactional Host path.
|
|
244
565
|
*/
|
|
245
566
|
rollback(agent, input) {
|
|
567
|
+
const normalized = this.normalizedRollbackInput(input);
|
|
568
|
+
const scopeKey = this.authorize(agent, 'rollback', `rule:${normalized.ruleId}`);
|
|
569
|
+
return this.rollbackForScope(scopeKey, normalized);
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Retire one exact active rule by CAS. The Store admits only quality-eligible
|
|
573
|
+
* objective/verification evidence attributed to an exact exposure receipt;
|
|
574
|
+
* operational automation outcomes cannot satisfy this lane.
|
|
575
|
+
*/
|
|
576
|
+
hostRollbackOne(input) {
|
|
577
|
+
const normalized = this.normalizedRollbackInput(input);
|
|
578
|
+
const scopeKey = this.authorizeHost(input, 'rollback', `rule:${normalized.ruleId}`);
|
|
579
|
+
return this.rollbackForScope(scopeKey, normalized);
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Ask the authenticated owner to remove one exact active guidance version.
|
|
583
|
+
*
|
|
584
|
+
* This is intentionally a proposal, not a direct rollback: the current Agent
|
|
585
|
+
* supplies the exact workspace/preset scope, Delivery supplies the owner
|
|
586
|
+
* principal, and Policy freezes a distinct `evolution.owner-undo` review
|
|
587
|
+
* tuple. Approval can only retire the rule; it cannot revise its guidance or
|
|
588
|
+
* grant authority. No regression sample is required because the owner is the
|
|
589
|
+
* authority withdrawing their own earlier approval.
|
|
590
|
+
*/
|
|
591
|
+
requestOwnerUndo(agent, input) {
|
|
246
592
|
this.assertActive();
|
|
247
|
-
if (
|
|
248
|
-
throw new AssistantEvolutionError('
|
|
593
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
594
|
+
throw new AssistantEvolutionError('invalid-input', 'owner undo input must be an object');
|
|
595
|
+
}
|
|
596
|
+
const keys = Object.keys(input).sort().join(',');
|
|
597
|
+
if (keys !== 'expectedVersion,operationId,ruleId'
|
|
598
|
+
&& keys !== 'expectedVersion,operationId,ruleId,ttlMs') {
|
|
599
|
+
throw new AssistantEvolutionError('invalid-input', 'owner undo accepts only ruleId, expectedVersion, operationId, and optional ttlMs');
|
|
249
600
|
}
|
|
250
|
-
const
|
|
601
|
+
const scopeKey = this.authorize(agent, 'propose', 'proposals');
|
|
602
|
+
const ruleId = hostText(input.ruleId, 'ruleId', 200);
|
|
251
603
|
if (!/^rule-[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u
|
|
252
604
|
.test(ruleId)) {
|
|
253
605
|
throw new AssistantEvolutionError('invalid-input', 'ruleId must be an immutable server-issued rule ID');
|
|
@@ -256,16 +608,73 @@ export class AssistantEvolutionService extends Service {
|
|
|
256
608
|
|| input.expectedVersion > 1_000_000_000) {
|
|
257
609
|
throw new AssistantEvolutionError('invalid-input', 'expectedVersion must be a positive safe integer');
|
|
258
610
|
}
|
|
259
|
-
const
|
|
260
|
-
|
|
611
|
+
const operationId = hostText(input.operationId, 'operationId', 500);
|
|
612
|
+
const ttlMs = input.ttlMs ?? this.config.defaultProposalTtlMs;
|
|
613
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs < 1) {
|
|
614
|
+
throw new AssistantEvolutionError('invalid-input', 'ttlMs must be a positive safe integer');
|
|
615
|
+
}
|
|
616
|
+
const targetRule = this.store.getRule(ruleId);
|
|
617
|
+
if (targetRule === undefined || targetRule.scopeKey !== scopeKey) {
|
|
618
|
+
throw new AssistantEvolutionError('not-found', 'evolution rule was not found in the Agent scope');
|
|
619
|
+
}
|
|
620
|
+
const route = this.approvalRoute(agent, undefined);
|
|
621
|
+
const requester = `agent:${agent.session.header.agentPreset}:owner-undo`;
|
|
622
|
+
const mutation = {
|
|
623
|
+
op: 'owner-undo',
|
|
261
624
|
scopeKey,
|
|
262
625
|
ruleId,
|
|
626
|
+
situation: targetRule.situation,
|
|
627
|
+
guidance: targetRule.guidance,
|
|
628
|
+
generation: targetRule.generation,
|
|
263
629
|
expectedVersion: input.expectedVersion,
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
630
|
+
reason: 'Owner-approved immediate guidance undo.',
|
|
631
|
+
};
|
|
632
|
+
// operationId is the caller's retry identity. Deliberately exclude the
|
|
633
|
+
// target tuple from this key so reusing one operation for another target is
|
|
634
|
+
// detected as an idempotency conflict instead of creating a second card.
|
|
635
|
+
const stable = evolutionDigest(['owner-undo-v1', scopeKey, route.principal, operationId]);
|
|
636
|
+
const idempotencyKey = `evolution-owner-undo:${stable}`;
|
|
637
|
+
const review = evolutionMutationReview(mutation);
|
|
638
|
+
const creationIntent = {
|
|
639
|
+
idempotencyKey,
|
|
640
|
+
requester,
|
|
641
|
+
principal: route.principal,
|
|
642
|
+
action: review.action,
|
|
643
|
+
resource: review.resource,
|
|
644
|
+
diff: review.diff,
|
|
645
|
+
summary: review.summary,
|
|
646
|
+
ttlMs,
|
|
647
|
+
...(route.dispatch === undefined ? {} : { dispatch: route.dispatch }),
|
|
648
|
+
};
|
|
649
|
+
const prior = this.store.getProposalByIdempotencyKey(idempotencyKey);
|
|
650
|
+
if (prior === undefined) {
|
|
651
|
+
if (targetRule.status !== 'active') {
|
|
652
|
+
throw new AssistantEvolutionError('not-found', 'active evolution rule was not found in the Agent scope');
|
|
653
|
+
}
|
|
654
|
+
if (targetRule.version !== input.expectedVersion) {
|
|
655
|
+
throw new AssistantEvolutionError('invalid-input', 'owner undo expectedVersion is stale');
|
|
656
|
+
}
|
|
657
|
+
if (this.store.ruleApprovalPrincipal(ruleId) !== route.principal) {
|
|
658
|
+
throw new AssistantEvolutionError('forbidden', 'owner undo route does not match the principal who approved this guidance');
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const local = this.store.createProposal({
|
|
662
|
+
idempotencyKey,
|
|
663
|
+
requester,
|
|
664
|
+
principal: route.principal,
|
|
665
|
+
mutation,
|
|
666
|
+
// Policy proposal expiry uses the wall clock; the injectable clock is
|
|
667
|
+
// reserved for deterministic ledger/audit timestamps.
|
|
668
|
+
expiresAt: Date.now() + ttlMs,
|
|
669
|
+
creationIntent,
|
|
268
670
|
});
|
|
671
|
+
if (local.policyProposalId !== undefined) {
|
|
672
|
+
if (local.status !== 'pending') {
|
|
673
|
+
return result(local, true, local.resultRuleId === undefined ? undefined : this.store.getRule(local.resultRuleId));
|
|
674
|
+
}
|
|
675
|
+
return this.settleAttached(local, true) ?? result(local, true);
|
|
676
|
+
}
|
|
677
|
+
return this.submitLocalProposal(local, false);
|
|
269
678
|
}
|
|
270
679
|
/**
|
|
271
680
|
* Propose adopting or retiring a rule. Returns a pending proposal; the owner
|
|
@@ -306,6 +715,9 @@ export class AssistantEvolutionService extends Service {
|
|
|
306
715
|
sampleEpisodeIds: candidate.evidence.map(entry => entry.episodeId),
|
|
307
716
|
digest: candidate.evidenceDigest,
|
|
308
717
|
total: candidate.evidenceTotal,
|
|
718
|
+
window: this.config.evaluationWindow,
|
|
719
|
+
scopeWatermark: candidate.scopeWatermark,
|
|
720
|
+
taskRevisions: candidate.taskRevisions,
|
|
309
721
|
},
|
|
310
722
|
};
|
|
311
723
|
}
|
|
@@ -330,7 +742,11 @@ export class AssistantEvolutionService extends Service {
|
|
|
330
742
|
}
|
|
331
743
|
mutation = {
|
|
332
744
|
op: 'retire',
|
|
745
|
+
scopeKey,
|
|
333
746
|
ruleId: existing.id,
|
|
747
|
+
situation: existing.situation,
|
|
748
|
+
guidance: existing.guidance,
|
|
749
|
+
generation: existing.generation,
|
|
334
750
|
expectedVersion: existing.version,
|
|
335
751
|
reason: input.mutation.reason,
|
|
336
752
|
evaluation: candidate.stats,
|
|
@@ -339,6 +755,9 @@ export class AssistantEvolutionService extends Service {
|
|
|
339
755
|
sampleEpisodeIds: candidate.evidence.map(entry => entry.episodeId),
|
|
340
756
|
digest: candidate.evidenceDigest,
|
|
341
757
|
total: candidate.evidenceTotal,
|
|
758
|
+
window: this.config.evaluationWindow,
|
|
759
|
+
scopeWatermark: candidate.scopeWatermark,
|
|
760
|
+
taskRevisions: candidate.taskRevisions,
|
|
342
761
|
},
|
|
343
762
|
};
|
|
344
763
|
}
|
|
@@ -359,14 +778,14 @@ export class AssistantEvolutionService extends Service {
|
|
|
359
778
|
ttlMs,
|
|
360
779
|
...(route.dispatch === undefined ? {} : { dispatch: route.dispatch }),
|
|
361
780
|
};
|
|
362
|
-
const local = this.store.createProposal({
|
|
781
|
+
const local = this.withLearningWriterFence(mutation, () => this.store.createProposal({
|
|
363
782
|
idempotencyKey,
|
|
364
783
|
requester,
|
|
365
784
|
principal: route.principal,
|
|
366
785
|
mutation,
|
|
367
786
|
expiresAt: Date.now() + ttlMs,
|
|
368
787
|
creationIntent,
|
|
369
|
-
});
|
|
788
|
+
}));
|
|
370
789
|
if (local.policyProposalId !== undefined) {
|
|
371
790
|
if (local.status !== 'pending') {
|
|
372
791
|
return result(local, true, local.resultRuleId === undefined ? undefined : this.store.getRule(local.resultRuleId));
|
|
@@ -384,7 +803,17 @@ export class AssistantEvolutionService extends Service {
|
|
|
384
803
|
reconcileProposals(limit) {
|
|
385
804
|
this.assertActive();
|
|
386
805
|
const settled = [];
|
|
387
|
-
|
|
806
|
+
const bounded = limit ?? this.config.reconcileLimit;
|
|
807
|
+
const queue = new Map();
|
|
808
|
+
for (const proposal of this.store.listUnattachedProposalIntents(bounded)) {
|
|
809
|
+
queue.set(proposal.proposalId, proposal);
|
|
810
|
+
}
|
|
811
|
+
for (const proposal of this.store.listPendingProposals(bounded)) {
|
|
812
|
+
if (queue.size >= bounded)
|
|
813
|
+
break;
|
|
814
|
+
queue.set(proposal.proposalId, proposal);
|
|
815
|
+
}
|
|
816
|
+
for (const pending of queue.values()) {
|
|
388
817
|
try {
|
|
389
818
|
const result = pending.policyProposalId === undefined
|
|
390
819
|
? this.submitLocalProposal(pending, false)
|
|
@@ -401,12 +830,72 @@ export class AssistantEvolutionService extends Service {
|
|
|
401
830
|
this.store.deferPendingProposal(pending.proposalId);
|
|
402
831
|
}
|
|
403
832
|
}
|
|
833
|
+
this.reconcileApplicationPresentations(limit);
|
|
404
834
|
this.lastReconciledAt = this.now();
|
|
405
835
|
return settled;
|
|
406
836
|
}
|
|
837
|
+
/**
|
|
838
|
+
* Publish domain terminal receipts independently from proposal polling. A
|
|
839
|
+
* crash after Delivery accepts but before the local ack simply republishes the
|
|
840
|
+
* same revision and digest on restart.
|
|
841
|
+
*/
|
|
842
|
+
reconcileApplicationPresentations(limit) {
|
|
843
|
+
this.assertActive();
|
|
844
|
+
const sink = this.presentationSink;
|
|
845
|
+
if (sink === undefined)
|
|
846
|
+
return 0;
|
|
847
|
+
const bounded = limit ?? this.config.reconcileLimit;
|
|
848
|
+
let published = 0;
|
|
849
|
+
for (const entry of this.store.listPendingEvolutionApplicationReceipts(bounded)) {
|
|
850
|
+
if (this.presentationSink?.token !== sink.token)
|
|
851
|
+
return published;
|
|
852
|
+
const receipt = entry.receipt;
|
|
853
|
+
try {
|
|
854
|
+
sink.registration.publish(this.applicationPresentation(receipt));
|
|
855
|
+
this.store.settleEvolutionApplicationPublication({
|
|
856
|
+
localProposalId: receipt.localProposalId,
|
|
857
|
+
receiptDigest: receipt.receiptDigest,
|
|
858
|
+
outcome: 'published',
|
|
859
|
+
});
|
|
860
|
+
published += 1;
|
|
861
|
+
}
|
|
862
|
+
catch (error) {
|
|
863
|
+
const code = error instanceof Error
|
|
864
|
+
? `delivery-${error.name.normalize('NFC').toLowerCase().replaceAll(/[^a-z0-9]+/gu, '-').slice(0, 80)}`
|
|
865
|
+
: 'delivery-unknown-error';
|
|
866
|
+
this.store.settleEvolutionApplicationPublication({
|
|
867
|
+
localProposalId: receipt.localProposalId,
|
|
868
|
+
receiptDigest: receipt.receiptDigest,
|
|
869
|
+
outcome: 'retry',
|
|
870
|
+
error: code,
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return published;
|
|
875
|
+
}
|
|
876
|
+
applicationPresentation(receipt) {
|
|
877
|
+
return Object.freeze({
|
|
878
|
+
presentationKey: `approval-application:${receipt.policyProposalId}`,
|
|
879
|
+
originalOutboxIdempotencyKey: `approval-card:${receipt.policyProposalId}`,
|
|
880
|
+
revision: receipt.revision,
|
|
881
|
+
presentation: Object.freeze({
|
|
882
|
+
kind: 'approval-application',
|
|
883
|
+
policyProposalId: receipt.policyProposalId,
|
|
884
|
+
localProposalId: receipt.localProposalId,
|
|
885
|
+
applicationStatus: receipt.applicationStatus,
|
|
886
|
+
operation: receipt.operation,
|
|
887
|
+
terminalAt: receipt.terminalAt,
|
|
888
|
+
receiptDigest: receipt.receiptDigest,
|
|
889
|
+
...(receipt.ruleId === undefined ? {} : { ruleId: receipt.ruleId }),
|
|
890
|
+
...(receipt.resultingRuleVersion === undefined
|
|
891
|
+
? {} : { resultingRuleVersion: receipt.resultingRuleVersion }),
|
|
892
|
+
...(receipt.ruleStatus === undefined ? {} : { ruleStatus: receipt.ruleStatus }),
|
|
893
|
+
}),
|
|
894
|
+
});
|
|
895
|
+
}
|
|
407
896
|
submitLocalProposal(local, replayed) {
|
|
408
897
|
const intent = local.creationIntent;
|
|
409
|
-
if (intent === undefined) {
|
|
898
|
+
if (intent === undefined || (intent.dispatch !== undefined && intent.dispatch.routeVersion !== 2)) {
|
|
410
899
|
const conflict = this.store.settleProposal({
|
|
411
900
|
proposalId: local.proposalId,
|
|
412
901
|
securityConflict: true,
|
|
@@ -422,7 +911,7 @@ export class AssistantEvolutionService extends Service {
|
|
|
422
911
|
diff: intent.diff,
|
|
423
912
|
summary: intent.summary,
|
|
424
913
|
notAfter: local.expiresAt,
|
|
425
|
-
...(intent.dispatch ===
|
|
914
|
+
...(intent.dispatch?.routeVersion === 2 ? { dispatch: intent.dispatch } : {}),
|
|
426
915
|
};
|
|
427
916
|
const recovered = this.policy.recoverOrCreateProposal(recoveryInput);
|
|
428
917
|
if (recovered.kind === 'abandoned') {
|
|
@@ -430,7 +919,8 @@ export class AssistantEvolutionService extends Service {
|
|
|
430
919
|
proposalId: local.proposalId,
|
|
431
920
|
securityConflict: true,
|
|
432
921
|
});
|
|
433
|
-
|
|
922
|
+
const completed = this.store.completeAbandonedProposalRecovery(local.proposalId);
|
|
923
|
+
return result(completed, replayed || recovered.replayed || conflict.replayed, conflict.rule);
|
|
434
924
|
}
|
|
435
925
|
const decision = recovered.proposal;
|
|
436
926
|
const snapshot = this.policy.getProposal(decision.proposalId);
|
|
@@ -488,12 +978,27 @@ export class AssistantEvolutionService extends Service {
|
|
|
488
978
|
return undefined;
|
|
489
979
|
try {
|
|
490
980
|
const terminal = validateApprovalSettlement(snapshot, local.settlementExpectation);
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
981
|
+
let applied;
|
|
982
|
+
try {
|
|
983
|
+
applied = this.withLearningWriterFence(local.mutation, () => this.store.settleProposal({
|
|
984
|
+
proposalId: local.proposalId,
|
|
985
|
+
policyStatus: terminal.status,
|
|
986
|
+
policyVersion: terminal.version,
|
|
987
|
+
reviewExpectation: local.settlementExpectation,
|
|
988
|
+
}));
|
|
989
|
+
}
|
|
990
|
+
catch (error) {
|
|
991
|
+
if (!(error instanceof AssistantEvolutionError) || error.code !== 'forbidden')
|
|
992
|
+
throw error;
|
|
993
|
+
// A stale/missing authoritative fence is itself the domain terminal.
|
|
994
|
+
// This conflict-only transaction cannot apply a rule and therefore does
|
|
995
|
+
// not need to acquire Evaluation in the reverse lock order.
|
|
996
|
+
applied = this.store.settleProposal({
|
|
997
|
+
proposalId: local.proposalId,
|
|
998
|
+
securityConflict: true,
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
this.reconcileApplicationPresentations();
|
|
497
1002
|
return result(applied.proposal, replayed || applied.replayed, applied.rule);
|
|
498
1003
|
}
|
|
499
1004
|
catch (error) {
|
|
@@ -503,6 +1008,7 @@ export class AssistantEvolutionService extends Service {
|
|
|
503
1008
|
proposalId: local.proposalId,
|
|
504
1009
|
securityConflict: true,
|
|
505
1010
|
});
|
|
1011
|
+
this.reconcileApplicationPresentations();
|
|
506
1012
|
return result(conflict.proposal, replayed || conflict.replayed, conflict.rule);
|
|
507
1013
|
}
|
|
508
1014
|
}
|
|
@@ -510,7 +1016,8 @@ export class AssistantEvolutionService extends Service {
|
|
|
510
1016
|
if (this.delivery !== undefined) {
|
|
511
1017
|
const dispatch = this.delivery.prepareAgentApproval(agent, { sourceId: evolutionApprovalSource });
|
|
512
1018
|
const workspace = agent?.session.header.cwd;
|
|
513
|
-
if (dispatch.
|
|
1019
|
+
if (dispatch.routeVersion !== 2
|
|
1020
|
+
|| dispatch.sourceId !== evolutionApprovalSource || dispatch.workspace !== workspace
|
|
514
1021
|
|| dispatch.principal.trim() === '') {
|
|
515
1022
|
throw new AssistantEvolutionError('invalid-input', 'authenticated approval route does not match the Agent');
|
|
516
1023
|
}
|
|
@@ -525,6 +1032,315 @@ export class AssistantEvolutionService extends Service {
|
|
|
525
1032
|
}
|
|
526
1033
|
return { principal };
|
|
527
1034
|
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Acquire Evaluation first and Evolution only inside its synchronous callback.
|
|
1037
|
+
* No code holding an Evolution writer lock is allowed to call this method.
|
|
1038
|
+
*/
|
|
1039
|
+
withLearningWriterFence(mutation, callback) {
|
|
1040
|
+
if (mutation.op === 'owner-undo')
|
|
1041
|
+
return callback();
|
|
1042
|
+
const evidence = mutation.evidence;
|
|
1043
|
+
if (evidence === undefined || evidence.scopeWatermark === undefined
|
|
1044
|
+
|| evidence.taskRevisions === undefined || evidence.taskRevisions.length !== evidence.total
|
|
1045
|
+
|| evidence.taskRevisions.length < 1) {
|
|
1046
|
+
throw new AssistantEvolutionError('forbidden', 'evidence-dependent Evolution operations require a complete Evaluation writer fence');
|
|
1047
|
+
}
|
|
1048
|
+
const scopeKey = mutation.op === 'adopt' ? mutation.input.scopeKey : mutation.scopeKey;
|
|
1049
|
+
let scope;
|
|
1050
|
+
try {
|
|
1051
|
+
const parsed = JSON.parse(scopeKey);
|
|
1052
|
+
if (!Array.isArray(parsed) || parsed.length !== 2
|
|
1053
|
+
|| typeof parsed[0] !== 'string' || typeof parsed[1] !== 'string')
|
|
1054
|
+
throw new Error('invalid');
|
|
1055
|
+
scope = { workspace: parsed[0], preset: parsed[1] };
|
|
1056
|
+
}
|
|
1057
|
+
catch {
|
|
1058
|
+
throw new AssistantEvolutionError('forbidden', 'Evolution evidence scope is not canonical');
|
|
1059
|
+
}
|
|
1060
|
+
const evaluation = this.context.get('assistantEvaluation');
|
|
1061
|
+
if (evaluation === undefined
|
|
1062
|
+
|| typeof evaluation.withTrustedLearningWriterFence !== 'function') {
|
|
1063
|
+
throw new AssistantEvolutionError('forbidden', 'authoritative Evaluation writer fence capability is unavailable');
|
|
1064
|
+
}
|
|
1065
|
+
const result = evaluation.withTrustedLearningWriterFence({
|
|
1066
|
+
scope: canonicalEvaluationHostScope(scope),
|
|
1067
|
+
scopeWatermark: evidence.scopeWatermark,
|
|
1068
|
+
evidence: evidence.taskRevisions,
|
|
1069
|
+
}, callback);
|
|
1070
|
+
if (typeof result !== 'object' || result === null || !Object.isFrozen(result)
|
|
1071
|
+
|| result.matched !== true) {
|
|
1072
|
+
const reason = typeof result === 'object' && result !== null && 'reason' in result
|
|
1073
|
+
? String(result.reason)
|
|
1074
|
+
: 'invalid-fence-receipt';
|
|
1075
|
+
throw new AssistantEvolutionError('forbidden', `Evolution evidence is stale at the authoritative writer fence: ${reason}`);
|
|
1076
|
+
}
|
|
1077
|
+
return result.value;
|
|
1078
|
+
}
|
|
1079
|
+
candidatesForScope(scopeKey) {
|
|
1080
|
+
return this.store.candidates({
|
|
1081
|
+
scopeKey,
|
|
1082
|
+
window: this.config.evaluationWindow,
|
|
1083
|
+
minSample: this.config.minSample,
|
|
1084
|
+
adoptFailureRate: this.config.adoptFailureRate,
|
|
1085
|
+
retireFailureRate: this.config.retireFailureRate,
|
|
1086
|
+
limit: this.config.maxCandidates,
|
|
1087
|
+
evidenceSampleLimit: this.config.maxEvidenceSamples,
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
trustedTaskLearningReceipt(scope, evaluationId) {
|
|
1091
|
+
const evaluation = this.context.get('assistantEvaluation');
|
|
1092
|
+
if (evaluation === undefined || typeof evaluation.getTrustedTaskLearningProjection !== 'function') {
|
|
1093
|
+
throw new AssistantEvolutionError('forbidden', 'authoritative assistantEvaluation lookup is unavailable');
|
|
1094
|
+
}
|
|
1095
|
+
const receipt = evaluation.getTrustedTaskLearningProjection({
|
|
1096
|
+
scope: canonicalEvaluationHostScope(scope),
|
|
1097
|
+
outcomeId: evaluationId,
|
|
1098
|
+
});
|
|
1099
|
+
if (receipt === undefined) {
|
|
1100
|
+
throw new AssistantEvolutionError('not-found', 'trusted Evaluation outcome was not found in the exact scope');
|
|
1101
|
+
}
|
|
1102
|
+
const scopeKey = canonicalEvolutionScope(scope.workspace, scope.preset);
|
|
1103
|
+
if (!Object.isFrozen(receipt) || !Object.isFrozen(receipt.scope)
|
|
1104
|
+
|| !Object.isFrozen(receipt.projection)
|
|
1105
|
+
|| receipt.triggerOutcomeId !== evaluationId || receipt.scopeKey !== scopeKey
|
|
1106
|
+
|| receipt.scope.workspace !== scope.workspace
|
|
1107
|
+
|| receipt.scope.preset !== scope.preset
|
|
1108
|
+
|| !Number.isSafeInteger(receipt.scopeWatermark) || receipt.scopeWatermark < 1
|
|
1109
|
+
|| (receipt.projection.subjectKind !== 'automation-run'
|
|
1110
|
+
&& receipt.projection.subjectKind !== 'outcome')
|
|
1111
|
+
|| hostText(receipt.projection.subjectRef, 'projection.subjectRef', 1_000)
|
|
1112
|
+
!== receipt.projection.subjectRef
|
|
1113
|
+
|| !Number.isSafeInteger(receipt.projection.version) || receipt.projection.version < 1
|
|
1114
|
+
|| receipt.projection.version > 1_000_000_000
|
|
1115
|
+
|| !/^[a-f\d]{64}$/u.test(receipt.projection.digest)
|
|
1116
|
+
|| (receipt.projection.disposition !== 'upsert'
|
|
1117
|
+
&& receipt.projection.disposition !== 'retract')
|
|
1118
|
+
|| evaluationLearningProjectionDigest(receipt) !== receipt.projection.digest) {
|
|
1119
|
+
throw new AssistantEvolutionError('forbidden', 'Evaluation returned a mismatched trusted receipt');
|
|
1120
|
+
}
|
|
1121
|
+
if (receipt.execution !== undefined && (!Object.isFrozen(receipt.execution)
|
|
1122
|
+
|| !Object.isFrozen(receipt.execution.source)
|
|
1123
|
+
|| !Object.isFrozen(receipt.execution.evaluator)
|
|
1124
|
+
|| !Object.isFrozen(receipt.execution.evidence)
|
|
1125
|
+
|| receipt.execution.evidence.some(entry => !Object.isFrozen(entry)))) {
|
|
1126
|
+
throw new AssistantEvolutionError('forbidden', 'Evaluation execution component is mutable');
|
|
1127
|
+
}
|
|
1128
|
+
if (receipt.objective !== undefined && (!Object.isFrozen(receipt.objective)
|
|
1129
|
+
|| !Object.isFrozen(receipt.objective.source)
|
|
1130
|
+
|| !Object.isFrozen(receipt.objective.evaluator)
|
|
1131
|
+
|| !Object.isFrozen(receipt.objective.evidence)
|
|
1132
|
+
|| receipt.objective.evidence.some(entry => !Object.isFrozen(entry)))) {
|
|
1133
|
+
throw new AssistantEvolutionError('forbidden', 'Evaluation objective component is mutable');
|
|
1134
|
+
}
|
|
1135
|
+
if (receipt.projection.disposition === 'upsert'
|
|
1136
|
+
&& (receipt.objective === undefined
|
|
1137
|
+
|| (receipt.objective.status !== 'achieved' && receipt.objective.status !== 'not-achieved')
|
|
1138
|
+
|| receipt.projection.evidenceOutcomeId !== receipt.objective.outcomeId)) {
|
|
1139
|
+
throw new AssistantEvolutionError('forbidden', 'an upsert task revision requires one exact binary objective component');
|
|
1140
|
+
}
|
|
1141
|
+
return receipt;
|
|
1142
|
+
}
|
|
1143
|
+
applyTrustedTaskLearningReceipt(scope, receipt) {
|
|
1144
|
+
const scopeKey = canonicalEvolutionScope(scope.workspace, scope.preset);
|
|
1145
|
+
if (receipt.projection.disposition === 'retract') {
|
|
1146
|
+
return this.store.applyTaskLearningProjection({
|
|
1147
|
+
scopeKey,
|
|
1148
|
+
scopeWatermark: receipt.scopeWatermark,
|
|
1149
|
+
subjectKind: receipt.projection.subjectKind,
|
|
1150
|
+
subjectRef: receipt.projection.subjectRef,
|
|
1151
|
+
version: receipt.projection.version,
|
|
1152
|
+
digest: receipt.projection.digest,
|
|
1153
|
+
disposition: 'retract',
|
|
1154
|
+
situation: receipt.situation,
|
|
1155
|
+
occurredAt: receipt.execution?.occurredAt ?? receipt.objective?.occurredAt ?? this.now(),
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
const objective = receipt.objective;
|
|
1159
|
+
const verified = this.verifiedAutomationAttribution(receipt, scopeKey);
|
|
1160
|
+
if (verified?.executionKind === 'host') {
|
|
1161
|
+
throw new AssistantEvolutionError('forbidden', 'Host runbook outcomes are not behavioural learning subjects');
|
|
1162
|
+
}
|
|
1163
|
+
return this.store.applyTaskLearningProjection({
|
|
1164
|
+
scopeKey,
|
|
1165
|
+
scopeWatermark: receipt.scopeWatermark,
|
|
1166
|
+
subjectKind: receipt.projection.subjectKind,
|
|
1167
|
+
subjectRef: receipt.projection.subjectRef,
|
|
1168
|
+
version: receipt.projection.version,
|
|
1169
|
+
digest: receipt.projection.digest,
|
|
1170
|
+
disposition: 'upsert',
|
|
1171
|
+
situation: receipt.situation,
|
|
1172
|
+
outcome: objective.status === 'achieved' ? 'succeeded' : 'failed',
|
|
1173
|
+
detail: `authoritative Evaluation objective: ${objective.status}`,
|
|
1174
|
+
evidenceRef: objective.outcomeId,
|
|
1175
|
+
occurredAt: receipt.execution?.occurredAt ?? objective.occurredAt,
|
|
1176
|
+
...(verified?.attribution === undefined ? {} : verified.attribution),
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
verifiedAutomationAttribution(receipt, scopeKey) {
|
|
1180
|
+
if (receipt.projection.subjectKind !== 'automation-run') {
|
|
1181
|
+
return undefined;
|
|
1182
|
+
}
|
|
1183
|
+
const execution = receipt.execution;
|
|
1184
|
+
const runReferences = execution?.evidence.filter(entry => entry.kind === 'automation-run') ?? [];
|
|
1185
|
+
if (execution === undefined || runReferences.length !== 1
|
|
1186
|
+
|| runReferences[0].ref !== receipt.projection.subjectRef
|
|
1187
|
+
|| !receipt.situation.startsWith('automation:')
|
|
1188
|
+
|| execution.source.kind !== 'automation'
|
|
1189
|
+
|| execution.source.id !== 'assistant-automations'
|
|
1190
|
+
|| execution.evaluator.id !== 'assistant-automations'
|
|
1191
|
+
|| !/^(?:terminal|host-runbook)-v[1-9][0-9]*$/u.test(execution.evaluator.version)) {
|
|
1192
|
+
throw new AssistantEvolutionError('forbidden', 'Evaluation receipt has no exact authoritative Automation execution component');
|
|
1193
|
+
}
|
|
1194
|
+
const automationId = receipt.situation.slice('automation:'.length);
|
|
1195
|
+
if (automationId === '') {
|
|
1196
|
+
throw new AssistantEvolutionError('forbidden', 'Evaluation receipt has an invalid automation situation');
|
|
1197
|
+
}
|
|
1198
|
+
const reference = runReferences[0];
|
|
1199
|
+
const resolver = this.context.get('assistantAutomations');
|
|
1200
|
+
if (resolver === undefined || typeof resolver.resolveQualityEvidence !== 'function'
|
|
1201
|
+
|| typeof resolver.validateQualityEvidence !== 'function') {
|
|
1202
|
+
throw new AssistantEvolutionError('forbidden', 'authoritative assistantAutomations production proof is unavailable');
|
|
1203
|
+
}
|
|
1204
|
+
let proof;
|
|
1205
|
+
let valid = false;
|
|
1206
|
+
try {
|
|
1207
|
+
proof = resolver.resolveQualityEvidence({
|
|
1208
|
+
automationId,
|
|
1209
|
+
runId: reference.ref,
|
|
1210
|
+
expectedScope: { workspace: receipt.scope.workspace, preset: receipt.scope.preset },
|
|
1211
|
+
expectedSituation: receipt.situation,
|
|
1212
|
+
expectedOccurredAt: execution.occurredAt,
|
|
1213
|
+
evidenceRef: { kind: 'automation-run', ref: reference.ref },
|
|
1214
|
+
});
|
|
1215
|
+
valid = proof !== undefined && resolver.validateQualityEvidence(proof);
|
|
1216
|
+
}
|
|
1217
|
+
catch {
|
|
1218
|
+
throw new AssistantEvolutionError('forbidden', 'assistantAutomations rejected the production quality proof');
|
|
1219
|
+
}
|
|
1220
|
+
if (!valid || proof === undefined || !Object.isFrozen(proof)
|
|
1221
|
+
|| (proof.executionKind !== 'agent' && proof.executionKind !== 'host')
|
|
1222
|
+
|| proof.automationId !== automationId || proof.runId !== reference.ref
|
|
1223
|
+
|| proof.scope.workspace !== receipt.scope.workspace
|
|
1224
|
+
|| proof.scope.preset !== receipt.scope.preset
|
|
1225
|
+
|| proof.situation !== receipt.situation || proof.occurredAt !== execution.occurredAt
|
|
1226
|
+
|| (proof.status === 'timed_out' ? 'timed-out' : proof.status) !== execution.status
|
|
1227
|
+
|| proof.evidenceRef.kind !== 'automation-run' || proof.evidenceRef.ref !== reference.ref) {
|
|
1228
|
+
throw new AssistantEvolutionError('forbidden', 'assistantAutomations returned a mismatched production quality proof');
|
|
1229
|
+
}
|
|
1230
|
+
const hasRule = proof.ruleId !== undefined;
|
|
1231
|
+
const hasGuidanceVersion = proof.guidanceVersion !== undefined;
|
|
1232
|
+
// A normal Agent automation always has a session id. Before the first rule
|
|
1233
|
+
// exists there is intentionally no guidance attribution; that baseline is
|
|
1234
|
+
// what makes bootstrap learning possible.
|
|
1235
|
+
if (proof.executionKind === 'host') {
|
|
1236
|
+
if (proof.sessionId !== undefined || hasRule || hasGuidanceVersion) {
|
|
1237
|
+
throw new AssistantEvolutionError('forbidden', 'Host automation proof contains Agent attribution');
|
|
1238
|
+
}
|
|
1239
|
+
return Object.freeze({
|
|
1240
|
+
executionKind: 'host',
|
|
1241
|
+
learningSubjectRef: JSON.stringify(['automation-run', proof.runId]),
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
if (!hasRule && !hasGuidanceVersion) {
|
|
1245
|
+
return Object.freeze({
|
|
1246
|
+
executionKind: 'agent',
|
|
1247
|
+
learningSubjectRef: JSON.stringify(['automation-run', proof.runId]),
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
if (proof.sessionId === undefined || !hasRule || !hasGuidanceVersion) {
|
|
1251
|
+
throw new AssistantEvolutionError('forbidden', 'automation guidance attribution proof is incomplete');
|
|
1252
|
+
}
|
|
1253
|
+
const exposure = this.store.captureGuidanceExposure(proof.sessionId, scopeKey, receipt.situation);
|
|
1254
|
+
if (exposure === undefined || exposure.ruleId !== proof.ruleId
|
|
1255
|
+
|| exposure.guidanceVersion !== proof.guidanceVersion
|
|
1256
|
+
|| execution.occurredAt < exposure.exposedAt) {
|
|
1257
|
+
throw new AssistantEvolutionError('forbidden', 'automation guidance attribution does not match an exact Evolution exposure');
|
|
1258
|
+
}
|
|
1259
|
+
return Object.freeze({
|
|
1260
|
+
executionKind: 'agent',
|
|
1261
|
+
learningSubjectRef: JSON.stringify(['automation-run', proof.runId]),
|
|
1262
|
+
attribution: Object.freeze({ ruleId: exposure.ruleId, guidanceVersion: exposure.guidanceVersion }),
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
normalizedRollbackInput(input) {
|
|
1266
|
+
this.assertActive();
|
|
1267
|
+
if (!this.config.autonomousRollback) {
|
|
1268
|
+
throw new AssistantEvolutionError('forbidden', 'autonomous evolution rollback is disabled');
|
|
1269
|
+
}
|
|
1270
|
+
const ruleId = hostText(input.ruleId, 'ruleId', 200);
|
|
1271
|
+
if (!/^rule-[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u
|
|
1272
|
+
.test(ruleId)) {
|
|
1273
|
+
throw new AssistantEvolutionError('invalid-input', 'ruleId must be an immutable server-issued rule ID');
|
|
1274
|
+
}
|
|
1275
|
+
if (!Number.isSafeInteger(input.expectedVersion) || input.expectedVersion < 1
|
|
1276
|
+
|| input.expectedVersion > 1_000_000_000) {
|
|
1277
|
+
throw new AssistantEvolutionError('invalid-input', 'expectedVersion must be a positive safe integer');
|
|
1278
|
+
}
|
|
1279
|
+
return { ruleId, expectedVersion: input.expectedVersion };
|
|
1280
|
+
}
|
|
1281
|
+
rollbackForScope(scopeKey, input) {
|
|
1282
|
+
return this.store.rollbackRule({
|
|
1283
|
+
scopeKey,
|
|
1284
|
+
ruleId: input.ruleId,
|
|
1285
|
+
expectedVersion: input.expectedVersion,
|
|
1286
|
+
window: this.config.evaluationWindow,
|
|
1287
|
+
minSample: this.config.minSample,
|
|
1288
|
+
retireFailureRate: this.config.retireFailureRate,
|
|
1289
|
+
evidenceSampleLimit: this.config.maxEvidenceSamples,
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
authorizeHost(input, action, resourceId) {
|
|
1293
|
+
this.assertActive();
|
|
1294
|
+
const scope = exactEvolutionHostScope(input.scope);
|
|
1295
|
+
const principal = hostText(input.principal, 'principal', 500);
|
|
1296
|
+
const operationId = hostText(input.operationId, 'operationId', 500);
|
|
1297
|
+
const canonicalResourceId = hostText(resourceId, 'resourceId', 500);
|
|
1298
|
+
const scopeKey = canonicalEvolutionScope(scope.workspace, scope.preset);
|
|
1299
|
+
const idempotencyDigest = createHash('sha256').update(JSON.stringify([
|
|
1300
|
+
HOST_RECOVERY_BACKGROUND_ID,
|
|
1301
|
+
scopeKey,
|
|
1302
|
+
principal,
|
|
1303
|
+
action,
|
|
1304
|
+
canonicalResourceId,
|
|
1305
|
+
operationId,
|
|
1306
|
+
])).digest('hex');
|
|
1307
|
+
const decision = this.policy.authorize({
|
|
1308
|
+
subject: {
|
|
1309
|
+
kind: 'background',
|
|
1310
|
+
id: HOST_RECOVERY_BACKGROUND_ID,
|
|
1311
|
+
workspace: scope.workspace,
|
|
1312
|
+
principal,
|
|
1313
|
+
},
|
|
1314
|
+
action,
|
|
1315
|
+
resource: { kind: 'evolution', id: canonicalResourceId },
|
|
1316
|
+
context: { initiator: 'background' },
|
|
1317
|
+
}, { idempotencyKey: `evolution-host:${idempotencyDigest}` });
|
|
1318
|
+
if (decision.effect !== 'allow') {
|
|
1319
|
+
throw new AssistantEvolutionError('forbidden', `policy denied Host evolution ${action}`);
|
|
1320
|
+
}
|
|
1321
|
+
return scopeKey;
|
|
1322
|
+
}
|
|
1323
|
+
authorizeSupervisedGrowthAnalyst(agent, action, resourceId) {
|
|
1324
|
+
const scopeKey = this.authorize(agent, action, resourceId);
|
|
1325
|
+
const execution = agent.ctx
|
|
1326
|
+
.get('assistantAutomationExecution');
|
|
1327
|
+
if (typeof execution !== 'object' || execution === null || Array.isArray(execution)
|
|
1328
|
+
|| !Object.isFrozen(execution)
|
|
1329
|
+
|| Object.keys(execution).sort().join(',') !== 'automationId,mode,occurrenceId') {
|
|
1330
|
+
throw new AssistantEvolutionError('forbidden', 'adoption analyst requires the immutable Automation execution context');
|
|
1331
|
+
}
|
|
1332
|
+
const installed = execution;
|
|
1333
|
+
if (installed.mode !== 'production'
|
|
1334
|
+
|| installed.automationId !== SUPERVISED_GROWTH_ANALYST_AUTOMATION_ID
|
|
1335
|
+
|| typeof installed.occurrenceId !== 'string') {
|
|
1336
|
+
throw new AssistantEvolutionError('forbidden', 'adoption analyst is restricted to its exact production Automation');
|
|
1337
|
+
}
|
|
1338
|
+
const occurrenceId = hostText(installed.occurrenceId, 'occurrenceId', 500);
|
|
1339
|
+
if (occurrenceId !== installed.occurrenceId) {
|
|
1340
|
+
throw new AssistantEvolutionError('forbidden', 'analyst occurrence identity is not canonical');
|
|
1341
|
+
}
|
|
1342
|
+
return Object.freeze({ scopeKey, occurrenceId });
|
|
1343
|
+
}
|
|
528
1344
|
/** Guidance block for the current active rules, as injected into a session. */
|
|
529
1345
|
guidance(agent) {
|
|
530
1346
|
this.assertActive();
|
|
@@ -540,8 +1356,32 @@ export class AssistantEvolutionService extends Service {
|
|
|
540
1356
|
});
|
|
541
1357
|
}
|
|
542
1358
|
injectGuidance(agent) {
|
|
543
|
-
if (!this.active
|
|
1359
|
+
if (!this.active)
|
|
544
1360
|
return;
|
|
1361
|
+
const execution = agent.ctx
|
|
1362
|
+
.get('assistantAutomationExecution');
|
|
1363
|
+
// Missing execution context is a normal foreground session. If a Host marks
|
|
1364
|
+
// the Agent as automation-scoped, only production may receive guidance or
|
|
1365
|
+
// write an exposure receipt. Malformed future modes fail closed as preview.
|
|
1366
|
+
let exactAutomationSituation;
|
|
1367
|
+
if (execution !== undefined) {
|
|
1368
|
+
if (typeof execution !== 'object' || execution === null || Array.isArray(execution)
|
|
1369
|
+
|| Object.keys(execution).sort().join(',') !== 'automationId,mode,occurrenceId')
|
|
1370
|
+
return;
|
|
1371
|
+
const installed = execution;
|
|
1372
|
+
if (installed.mode !== 'production' || typeof installed.automationId !== 'string'
|
|
1373
|
+
|| typeof installed.occurrenceId !== 'string')
|
|
1374
|
+
return;
|
|
1375
|
+
let automationId;
|
|
1376
|
+
try {
|
|
1377
|
+
automationId = hostText(installed.automationId, 'automationId', 500);
|
|
1378
|
+
hostText(installed.occurrenceId, 'occurrenceId', 500);
|
|
1379
|
+
}
|
|
1380
|
+
catch {
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
exactAutomationSituation = `automation:${automationId}`;
|
|
1384
|
+
}
|
|
545
1385
|
const workspace = agent.session.header.cwd;
|
|
546
1386
|
const preset = agent.session.header.agentPreset;
|
|
547
1387
|
if (workspace === undefined || !isAbsolute(workspace) || preset === undefined || preset.trim() === '')
|
|
@@ -553,15 +1393,34 @@ export class AssistantEvolutionService extends Service {
|
|
|
553
1393
|
return;
|
|
554
1394
|
const scopeKey = canonicalEvolutionScope(workspace, preset);
|
|
555
1395
|
const sessionId = String(agent.session.id);
|
|
1396
|
+
const exposedCount = this.store.countGuidanceExposures(sessionId, scopeKey);
|
|
1397
|
+
const remaining = Math.max(0, this.config.maxInjectedRules - exposedCount);
|
|
1398
|
+
// Before Automation installs its execution context the session is
|
|
1399
|
+
// indistinguishable from foreground. With a multi-rule budget, reserve one
|
|
1400
|
+
// slot so the exact post-setup rule can still fit inside the global cap.
|
|
1401
|
+
const reserved = execution === undefined && this.config.maxInjectedRules > 1 ? 1 : 0;
|
|
1402
|
+
const injectionLimit = Math.max(0, remaining - reserved);
|
|
1403
|
+
if (injectionLimit === 0)
|
|
1404
|
+
return;
|
|
556
1405
|
const unseen = this.store.listRules(scopeKey, 'active').filter(rule => !this.store.hasGuidanceExposure(sessionId, scopeKey, rule.id, rule.generation));
|
|
557
|
-
|
|
1406
|
+
// A production Automation's exact rule must be selected before the global
|
|
1407
|
+
// max-rules slice. This is also safe when the post-setup hook follows an
|
|
1408
|
+
// earlier session-start: durable exposures remove already injected rules,
|
|
1409
|
+
// so the hook can still inject the previously omitted exact rule.
|
|
1410
|
+
const prioritized = exactAutomationSituation === undefined
|
|
1411
|
+
? unseen
|
|
1412
|
+
: [
|
|
1413
|
+
...unseen.filter(rule => rule.situation === exactAutomationSituation),
|
|
1414
|
+
...unseen.filter(rule => rule.situation !== exactAutomationSituation),
|
|
1415
|
+
];
|
|
1416
|
+
const snapshot = buildGuidanceSnapshot(prioritized, {
|
|
558
1417
|
maxBytes: this.config.maxGuidanceBytes,
|
|
559
|
-
maxRules:
|
|
1418
|
+
maxRules: injectionLimit,
|
|
1419
|
+
...(exactAutomationSituation === undefined
|
|
1420
|
+
? {} : { prioritySituation: exactAutomationSituation }),
|
|
560
1421
|
});
|
|
561
|
-
if (snapshot.text === '')
|
|
562
|
-
this.injected.add(agent);
|
|
1422
|
+
if (snapshot.text === '')
|
|
563
1423
|
return;
|
|
564
|
-
}
|
|
565
1424
|
agent.inject(createUserMessage({
|
|
566
1425
|
content: [{ type: 'text', text: snapshot.text }],
|
|
567
1426
|
source: { kind: 'plugin', plugin: 'assistant-evolution' },
|
|
@@ -575,7 +1434,6 @@ export class AssistantEvolutionService extends Service {
|
|
|
575
1434
|
guidanceVersion: rule.generation,
|
|
576
1435
|
});
|
|
577
1436
|
}
|
|
578
|
-
this.injected.add(agent);
|
|
579
1437
|
}
|
|
580
1438
|
authorize(agent, action, resourceId) {
|
|
581
1439
|
this.assertActive();
|