@unson/brainbase-mcp 0.44.0 → 0.44.2
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/contracts/canonical-task/operation-schema.sql +58 -0
- package/contracts/canonical-task/search-indexes.sql +10 -0
- package/contracts/canonical-task/store-schema.sql +77 -0
- package/dist/automation-run.d.ts +213 -0
- package/dist/automation-run.js +479 -0
- package/dist/canonical-task-operations.d.ts +205 -0
- package/dist/canonical-task-operations.js +700 -0
- package/dist/canonical-task-postgres.d.ts +139 -0
- package/dist/canonical-task-postgres.js +554 -0
- package/dist/canonical-task-service.d.ts +13 -0
- package/dist/canonical-task-service.js +55 -3
- package/dist/canonical-task-store-config.d.ts +31 -0
- package/dist/canonical-task-store-config.js +53 -0
- package/dist/config-parser.d.ts +298 -0
- package/dist/config-parser.js +593 -0
- package/dist/connected-onboarding.d.ts +57 -0
- package/dist/connected-onboarding.js +37 -0
- package/dist/durable-wait-http.d.ts +3 -1
- package/dist/durable-wait-http.js +73 -6
- package/dist/durable-waits.d.ts +93 -3
- package/dist/durable-waits.js +320 -8
- package/dist/honcho.d.ts +25 -0
- package/dist/honcho.js +81 -0
- package/dist/judgment-event-adapter.d.ts +72 -0
- package/dist/judgment-event-adapter.js +114 -0
- package/dist/judgment-event-outbox.d.ts +182 -0
- package/dist/judgment-event-outbox.js +568 -0
- package/dist/judgment-execution-outcome.d.ts +46 -0
- package/dist/judgment-execution-outcome.js +149 -0
- package/dist/judgment-host.d.ts +18 -7
- package/dist/judgment-host.js +82 -34
- package/dist/judgment-lifecycle-core.d.ts +198 -0
- package/dist/judgment-lifecycle-core.js +464 -0
- package/dist/judgment-node-evidence.d.ts +57 -0
- package/dist/judgment-node-evidence.js +297 -0
- package/dist/judgment-runtime-resolution.d.ts +264 -0
- package/dist/judgment-runtime-resolution.js +1248 -0
- package/dist/judgment-runtime-resolution.public.d.ts +255 -0
- package/dist/knowledge-event.d.ts +28 -0
- package/dist/knowledge-event.js +114 -0
- package/dist/mcp-runtime.d.ts +75 -0
- package/dist/mcp-runtime.js +188 -0
- package/dist/meeting-automation-runtime.d.ts +102 -0
- package/dist/meeting-automation-runtime.js +249 -0
- package/dist/meeting-automation.d.ts +155 -0
- package/dist/meeting-automation.js +341 -0
- package/dist/meeting-minutes-context-receipt.d.ts +91 -0
- package/dist/meeting-minutes-context-receipt.js +280 -0
- package/dist/meeting-source-sync.d.ts +89 -0
- package/dist/meeting-source-sync.js +230 -0
- package/dist/mesh.d.ts +58 -0
- package/dist/mesh.js +134 -0
- package/dist/onboarding-runtime.d.ts +47 -0
- package/dist/onboarding-runtime.js +4719 -0
- package/dist/routine-cycle.d.ts +51 -0
- package/dist/routine-cycle.js +153 -0
- package/dist/routine-liveness.d.ts +49 -0
- package/dist/routine-liveness.js +227 -0
- package/dist/routine-runtime.d.ts +101 -0
- package/dist/routine-runtime.js +237 -0
- package/dist/routines.d.ts +3 -0
- package/dist/routines.js +3 -0
- package/dist/run-receipt.d.ts +40 -0
- package/dist/run-receipt.js +635 -0
- package/dist/runtime-host.d.ts +64 -0
- package/dist/runtime-host.js +70 -0
- package/dist/world-work.d.ts +16 -0
- package/package.json +115 -3
- package/ui/objective-editor.js +47 -11
- package/ui/workspace-kit.css +33 -1
- package/ui/workspace-kit.js +13 -2
- package/ui/world/world-canvas-ui.css +4 -49
- package/ui/world/world-canvas-ui.js +26 -71
- package/ui/world/world-district.js +18 -3
- package/ui/world/world-placement.js +13 -0
- package/ui/world/world-work-rail.js +7 -1
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { normalizeRunReceipt } from './run-receipt.js';
|
|
3
|
+
import { resolveRuntimeHostContext } from './runtime-host.js';
|
|
4
|
+
function assertRequest(request) {
|
|
5
|
+
if (!request || typeof request !== 'object') {
|
|
6
|
+
throw new Error('automation run request is required');
|
|
7
|
+
}
|
|
8
|
+
if (typeof request.workflowId !== 'string' || request.workflowId.trim() === '') {
|
|
9
|
+
throw new Error('automation run request workflowId must be a non-empty string');
|
|
10
|
+
}
|
|
11
|
+
if (typeof request.externalRunId !== 'string' || request.externalRunId.trim() === '') {
|
|
12
|
+
throw new Error('automation run request externalRunId must be a non-empty string');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function assertPorts(ports) {
|
|
16
|
+
if (!ports.runner || typeof ports.runner !== 'object' || typeof ports.runner.start !== 'function') {
|
|
17
|
+
throw new Error('automation runner port with start is required');
|
|
18
|
+
}
|
|
19
|
+
if (!ports.receipt || typeof ports.receipt !== 'object'
|
|
20
|
+
|| typeof ports.receipt.persist !== 'function'
|
|
21
|
+
|| typeof ports.receipt.query !== 'function') {
|
|
22
|
+
throw new Error('automation receipt port with persist and query is required');
|
|
23
|
+
}
|
|
24
|
+
if (!ports.runStore || typeof ports.runStore !== 'object'
|
|
25
|
+
|| typeof ports.runStore.claim !== 'function'
|
|
26
|
+
|| typeof ports.runStore.markRunnerStarted !== 'function'
|
|
27
|
+
|| typeof ports.runStore.markReceiptPersisted !== 'function'
|
|
28
|
+
|| typeof ports.runStore.complete !== 'function'
|
|
29
|
+
|| typeof ports.runStore.release !== 'function'
|
|
30
|
+
|| typeof ports.runStore.read !== 'function') {
|
|
31
|
+
throw new Error('automation runStore port with atomic claim, phase checkpoints, complete, release, and read is required');
|
|
32
|
+
}
|
|
33
|
+
if (ports.lifecycle !== undefined) {
|
|
34
|
+
if (!ports.lifecycle || typeof ports.lifecycle !== 'object') {
|
|
35
|
+
throw new Error('automation lifecycle port must be an object');
|
|
36
|
+
}
|
|
37
|
+
for (const [name, value] of [
|
|
38
|
+
['lookup', ports.lifecycle.lookup],
|
|
39
|
+
['designGate', ports.lifecycle.designGate],
|
|
40
|
+
['checkpoint', ports.lifecycle.checkpoint],
|
|
41
|
+
['humanGate', ports.lifecycle.humanGate],
|
|
42
|
+
['transition', ports.lifecycle.transition]
|
|
43
|
+
]) {
|
|
44
|
+
if (value !== undefined && typeof value !== 'function') {
|
|
45
|
+
throw new Error(`automation lifecycle port ${name} must be a function`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function stableValue(value, seen = new WeakSet()) {
|
|
51
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
52
|
+
return value;
|
|
53
|
+
if (typeof value === 'number') {
|
|
54
|
+
if (Number.isNaN(value))
|
|
55
|
+
return { __type: 'number', value: 'NaN' };
|
|
56
|
+
if (value === Infinity)
|
|
57
|
+
return { __type: 'number', value: 'Infinity' };
|
|
58
|
+
if (value === -Infinity)
|
|
59
|
+
return { __type: 'number', value: '-Infinity' };
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === 'bigint')
|
|
63
|
+
return { __type: 'bigint', value: value.toString() };
|
|
64
|
+
if (typeof value === 'undefined')
|
|
65
|
+
return { __type: 'undefined' };
|
|
66
|
+
if (typeof value === 'function' || typeof value === 'symbol') {
|
|
67
|
+
return { __type: typeof value, value: String(value) };
|
|
68
|
+
}
|
|
69
|
+
if (value instanceof Date)
|
|
70
|
+
return { __type: 'date', value: value.toISOString() };
|
|
71
|
+
if (typeof value !== 'object')
|
|
72
|
+
return String(value);
|
|
73
|
+
if (seen.has(value))
|
|
74
|
+
throw new Error('automation run request contains a cyclic identity value');
|
|
75
|
+
seen.add(value);
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
const result = value.map((entry) => stableValue(entry, seen));
|
|
78
|
+
seen.delete(value);
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
const result = {};
|
|
82
|
+
for (const key of Object.keys(value).sort()) {
|
|
83
|
+
result[key] = stableValue(value[key], seen);
|
|
84
|
+
}
|
|
85
|
+
seen.delete(value);
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
function digest(value) {
|
|
89
|
+
return createHash('sha256').update(JSON.stringify(stableValue(value))).digest('hex');
|
|
90
|
+
}
|
|
91
|
+
function stringFromRecord(value, keys) {
|
|
92
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
93
|
+
return undefined;
|
|
94
|
+
const record = value;
|
|
95
|
+
for (const key of keys) {
|
|
96
|
+
const candidate = record[key];
|
|
97
|
+
if (typeof candidate === 'string' && candidate.trim() !== '')
|
|
98
|
+
return candidate.trim();
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
function requiredNamespaceValue(name, ...values) {
|
|
103
|
+
for (const value of values) {
|
|
104
|
+
if (typeof value === 'string' && value.trim() !== '')
|
|
105
|
+
return value.trim();
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`automation run namespace ${name} must be explicit`);
|
|
108
|
+
}
|
|
109
|
+
function runNamespace(request, context) {
|
|
110
|
+
const identity = context.identity;
|
|
111
|
+
const graph = context.graph;
|
|
112
|
+
return {
|
|
113
|
+
tenantId: requiredNamespaceValue('tenantId', context.tenantId),
|
|
114
|
+
projectId: requiredNamespaceValue('projectId', request.projectId),
|
|
115
|
+
organizationId: requiredNamespaceValue('organizationId', request.organizationId, request.orgId, stringFromRecord(identity, ['organizationId', 'organization_id', 'orgId', 'org_id']), stringFromRecord(graph, ['organizationId', 'organization_id', 'orgId', 'org_id'])),
|
|
116
|
+
actorId: requiredNamespaceValue('actorId', request.actorId, stringFromRecord(identity, ['actorId', 'actor_id', 'userId', 'user_id', 'subject', 'sub']))
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function buildRunIdentity(operation, request, context, previousRunId) {
|
|
120
|
+
const namespace = runNamespace(request, context);
|
|
121
|
+
const base = {
|
|
122
|
+
operation,
|
|
123
|
+
namespace,
|
|
124
|
+
workflowId: request.workflowId,
|
|
125
|
+
automationId: request.automationId || null,
|
|
126
|
+
sourceType: request.sourceType || null,
|
|
127
|
+
externalRunId: request.externalRunId,
|
|
128
|
+
previousRunId: previousRunId || null,
|
|
129
|
+
payload: request.payload === undefined ? null : request.payload,
|
|
130
|
+
metadata: request.metadata === undefined ? null : request.metadata
|
|
131
|
+
};
|
|
132
|
+
const fingerprint = digest(base);
|
|
133
|
+
const key = `automation-run:${digest({
|
|
134
|
+
operation,
|
|
135
|
+
namespace,
|
|
136
|
+
workflowId: request.workflowId,
|
|
137
|
+
externalRunId: request.externalRunId
|
|
138
|
+
})}`;
|
|
139
|
+
return {
|
|
140
|
+
operation,
|
|
141
|
+
key,
|
|
142
|
+
fingerprint,
|
|
143
|
+
runnerIdempotencyKey: `automation-run:${fingerprint}`,
|
|
144
|
+
namespace
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function conflictError(request, phase) {
|
|
148
|
+
const error = new Error(`automation run '${request.externalRunId}' is already in progress`);
|
|
149
|
+
Object.assign(error, {
|
|
150
|
+
code: 'automation_run_in_progress',
|
|
151
|
+
status: 409,
|
|
152
|
+
statusCode: 409,
|
|
153
|
+
externalRunId: request.externalRunId,
|
|
154
|
+
...(phase ? { phase } : {})
|
|
155
|
+
});
|
|
156
|
+
return error;
|
|
157
|
+
}
|
|
158
|
+
function identityConflictError(request) {
|
|
159
|
+
const error = new Error(`automation run '${request.externalRunId}' conflicts with an existing identity`);
|
|
160
|
+
Object.assign(error, {
|
|
161
|
+
code: 'automation_run_identity_conflict',
|
|
162
|
+
status: 409,
|
|
163
|
+
statusCode: 409,
|
|
164
|
+
externalRunId: request.externalRunId
|
|
165
|
+
});
|
|
166
|
+
return error;
|
|
167
|
+
}
|
|
168
|
+
function uncertainError(request, phase) {
|
|
169
|
+
const error = new Error(`automation run '${request.externalRunId}' has an uncertain durable outcome`);
|
|
170
|
+
Object.assign(error, {
|
|
171
|
+
code: 'automation_run_uncertain',
|
|
172
|
+
status: 409,
|
|
173
|
+
statusCode: 409,
|
|
174
|
+
externalRunId: request.externalRunId,
|
|
175
|
+
...(phase ? { phase } : {})
|
|
176
|
+
});
|
|
177
|
+
return error;
|
|
178
|
+
}
|
|
179
|
+
function invalidClaimError() {
|
|
180
|
+
return new Error('automation runStore claim returned an invalid state');
|
|
181
|
+
}
|
|
182
|
+
function replayResult(stored, context) {
|
|
183
|
+
return {
|
|
184
|
+
runnerResult: stored.runnerResult,
|
|
185
|
+
normalizedReceipt: stored.normalized || normalizeRunReceipt(stored.payload),
|
|
186
|
+
persistedReceipt: stored.persistedReceipt,
|
|
187
|
+
context,
|
|
188
|
+
replayed: true
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function workflowLifecycleError(code, request, reason) {
|
|
192
|
+
const suffix = typeof reason === 'string' && reason.trim() !== '' ? `: ${reason}` : '';
|
|
193
|
+
const error = new Error(`${code}${suffix}`);
|
|
194
|
+
Object.assign(error, {
|
|
195
|
+
code,
|
|
196
|
+
status: 409,
|
|
197
|
+
statusCode: 409,
|
|
198
|
+
externalRunId: request.externalRunId
|
|
199
|
+
});
|
|
200
|
+
return error;
|
|
201
|
+
}
|
|
202
|
+
function requireWorkflowGate(decision, code, request) {
|
|
203
|
+
const allowed = decision === true
|
|
204
|
+
|| (typeof decision === 'object'
|
|
205
|
+
&& decision !== null
|
|
206
|
+
&& decision.allowed === true);
|
|
207
|
+
if (!allowed) {
|
|
208
|
+
const reason = typeof decision === 'object' && decision !== null ? decision.reason : undefined;
|
|
209
|
+
throw workflowLifecycleError(code, request, reason);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
export class AutomationRunService {
|
|
213
|
+
ports;
|
|
214
|
+
constructor(ports) {
|
|
215
|
+
assertPorts(ports);
|
|
216
|
+
this.ports = ports;
|
|
217
|
+
}
|
|
218
|
+
async start(request) {
|
|
219
|
+
return this.execute('automation.start', request, (context, idempotencyKey) => this.ports.runner.start({ request, context, idempotencyKey }));
|
|
220
|
+
}
|
|
221
|
+
async run(request) {
|
|
222
|
+
return this.start(request);
|
|
223
|
+
}
|
|
224
|
+
async retry(request, previousRunId) {
|
|
225
|
+
assertRequest(request);
|
|
226
|
+
if (typeof previousRunId !== 'string' || previousRunId.trim() === '') {
|
|
227
|
+
throw new Error('previousRunId must be a non-empty string');
|
|
228
|
+
}
|
|
229
|
+
if (typeof this.ports.runner.retry !== 'function') {
|
|
230
|
+
throw new Error('automation runner retry port is required');
|
|
231
|
+
}
|
|
232
|
+
return this.execute('automation.retry', request, (context, idempotencyKey) => this.ports.runner.retry({ request, context, previousRunId, idempotencyKey }), previousRunId);
|
|
233
|
+
}
|
|
234
|
+
async rerun(request, previousRunId) {
|
|
235
|
+
return this.retry(request, previousRunId);
|
|
236
|
+
}
|
|
237
|
+
async history(request) {
|
|
238
|
+
assertRequest(request);
|
|
239
|
+
const input = {
|
|
240
|
+
operation: 'automation.history',
|
|
241
|
+
automationId: request.automationId,
|
|
242
|
+
runId: request.externalRunId,
|
|
243
|
+
request
|
|
244
|
+
};
|
|
245
|
+
const context = await resolveRuntimeHostContext(this.ports, input);
|
|
246
|
+
return this.ports.receipt.query({ request, context });
|
|
247
|
+
}
|
|
248
|
+
async getRun(request) {
|
|
249
|
+
return this.history(request);
|
|
250
|
+
}
|
|
251
|
+
async execute(operation, request, run, previousRunId) {
|
|
252
|
+
assertRequest(request);
|
|
253
|
+
const input = {
|
|
254
|
+
operation,
|
|
255
|
+
automationId: request.automationId,
|
|
256
|
+
runId: request.externalRunId,
|
|
257
|
+
request
|
|
258
|
+
};
|
|
259
|
+
const context = await resolveRuntimeHostContext(this.ports, input);
|
|
260
|
+
const identity = buildRunIdentity(operation, request, context, previousRunId);
|
|
261
|
+
const claimInput = { ...identity, request, context };
|
|
262
|
+
const lifecycle = this.ports.lifecycle;
|
|
263
|
+
let workflow;
|
|
264
|
+
const preparedLifecycleInput = () => ({
|
|
265
|
+
request,
|
|
266
|
+
context,
|
|
267
|
+
identity,
|
|
268
|
+
phase: 'prepared',
|
|
269
|
+
workflow,
|
|
270
|
+
previousRunId
|
|
271
|
+
});
|
|
272
|
+
// Workflow lookup and policy gates are host-owned. They run before the
|
|
273
|
+
// durable claim so a denied design/HITL decision cannot leave a retryable
|
|
274
|
+
// record behind, while the core still fixes their order before the runner.
|
|
275
|
+
if (lifecycle?.lookup) {
|
|
276
|
+
workflow = await lifecycle.lookup(preparedLifecycleInput());
|
|
277
|
+
if (workflow === null || workflow === undefined) {
|
|
278
|
+
throw workflowLifecycleError('automation_workflow_lookup_failed', request);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (lifecycle?.designGate) {
|
|
282
|
+
requireWorkflowGate(await lifecycle.designGate(preparedLifecycleInput()), 'automation_workflow_design_gate_denied', request);
|
|
283
|
+
}
|
|
284
|
+
if (lifecycle?.humanGate) {
|
|
285
|
+
requireWorkflowGate(await lifecycle.humanGate(preparedLifecycleInput()), 'automation_workflow_human_gate_denied', request);
|
|
286
|
+
}
|
|
287
|
+
// A rejected claim is not safe to release: the backing store may have
|
|
288
|
+
// committed it while the response was lost. Fail closed and let a durable
|
|
289
|
+
// reconciliation worker inspect the record.
|
|
290
|
+
const claim = await this.ports.runStore.claim(claimInput);
|
|
291
|
+
if (!claim || typeof claim !== 'object' || typeof claim.status !== 'string') {
|
|
292
|
+
throw invalidClaimError();
|
|
293
|
+
}
|
|
294
|
+
if (claim.status === 'conflict') {
|
|
295
|
+
if (claim.reason === 'identity_mismatch')
|
|
296
|
+
throw identityConflictError(request);
|
|
297
|
+
throw uncertainError(request, claim.phase);
|
|
298
|
+
}
|
|
299
|
+
if (claim.status === 'replay') {
|
|
300
|
+
const stored = await this.ports.runStore.read(claimInput);
|
|
301
|
+
if (stored.status === 'completed') {
|
|
302
|
+
if (lifecycle?.transition) {
|
|
303
|
+
try {
|
|
304
|
+
await lifecycle.transition({
|
|
305
|
+
...preparedLifecycleInput(),
|
|
306
|
+
phase: 'completed',
|
|
307
|
+
from: 'receipt_persisted',
|
|
308
|
+
to: 'completed'
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
// The runStore has already committed the external side effect;
|
|
313
|
+
// retrying the runner is forbidden while the host transition is
|
|
314
|
+
// reconciled separately.
|
|
315
|
+
throw uncertainError(request, 'completed');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return replayResult(stored, context);
|
|
319
|
+
}
|
|
320
|
+
if (stored.status === 'uncertain')
|
|
321
|
+
throw uncertainError(request, stored.phase);
|
|
322
|
+
throw new Error(`automation runStore replay readback was not completed for ${request.externalRunId}`);
|
|
323
|
+
}
|
|
324
|
+
if (claim.status === 'in_progress') {
|
|
325
|
+
const stored = await this.ports.runStore.read(claimInput);
|
|
326
|
+
if (stored.status === 'completed') {
|
|
327
|
+
if (lifecycle?.transition) {
|
|
328
|
+
try {
|
|
329
|
+
await lifecycle.transition({
|
|
330
|
+
...preparedLifecycleInput(),
|
|
331
|
+
phase: 'completed',
|
|
332
|
+
from: 'receipt_persisted',
|
|
333
|
+
to: 'completed'
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
throw uncertainError(request, 'completed');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return replayResult(stored, context);
|
|
341
|
+
}
|
|
342
|
+
if (stored.status === 'uncertain')
|
|
343
|
+
throw uncertainError(request, stored.phase);
|
|
344
|
+
throw conflictError(request, stored.phase || claim.phase);
|
|
345
|
+
}
|
|
346
|
+
if (claim.status !== 'claimed' || typeof claim.claimToken !== 'string' || claim.claimToken.trim() === '') {
|
|
347
|
+
throw invalidClaimError();
|
|
348
|
+
}
|
|
349
|
+
let phase = 'claimed';
|
|
350
|
+
try {
|
|
351
|
+
if (lifecycle?.checkpoint) {
|
|
352
|
+
phase = 'unknown';
|
|
353
|
+
await lifecycle.checkpoint({
|
|
354
|
+
...preparedLifecycleInput(),
|
|
355
|
+
phase: 'claimed',
|
|
356
|
+
claimToken: claim.claimToken
|
|
357
|
+
});
|
|
358
|
+
phase = 'claimed';
|
|
359
|
+
}
|
|
360
|
+
// The marker is durable and precedes the external call. If it fails, the
|
|
361
|
+
// state is unknown and must never be made retryable automatically.
|
|
362
|
+
phase = 'unknown';
|
|
363
|
+
await this.ports.runStore.markRunnerStarted({ ...claimInput, claimToken: claim.claimToken });
|
|
364
|
+
phase = 'runner_started';
|
|
365
|
+
if (lifecycle?.transition) {
|
|
366
|
+
phase = 'unknown';
|
|
367
|
+
await lifecycle.transition({
|
|
368
|
+
...preparedLifecycleInput(),
|
|
369
|
+
phase: 'runner_started',
|
|
370
|
+
claimToken: claim.claimToken,
|
|
371
|
+
from: 'claimed',
|
|
372
|
+
to: 'runner_started'
|
|
373
|
+
});
|
|
374
|
+
phase = 'runner_started';
|
|
375
|
+
}
|
|
376
|
+
const runnerResult = await run(context, identity.runnerIdempotencyKey);
|
|
377
|
+
if (!runnerResult || typeof runnerResult !== 'object' || !('receipt' in runnerResult)) {
|
|
378
|
+
throw new Error(`automation runner ${operation} must return a receipt`);
|
|
379
|
+
}
|
|
380
|
+
const result = runnerResult;
|
|
381
|
+
const normalizedReceipt = normalizeRunReceipt(result.receipt);
|
|
382
|
+
const persistedReceipt = await this.ports.receipt.persist({
|
|
383
|
+
payload: result.receipt,
|
|
384
|
+
normalized: normalizedReceipt,
|
|
385
|
+
context
|
|
386
|
+
});
|
|
387
|
+
phase = 'unknown';
|
|
388
|
+
await this.ports.runStore.markReceiptPersisted({
|
|
389
|
+
...claimInput,
|
|
390
|
+
claimToken: claim.claimToken,
|
|
391
|
+
payload: result.receipt,
|
|
392
|
+
normalized: normalizedReceipt,
|
|
393
|
+
runnerResult: result.result,
|
|
394
|
+
persistedReceipt
|
|
395
|
+
});
|
|
396
|
+
phase = 'receipt_persisted';
|
|
397
|
+
if (lifecycle?.transition) {
|
|
398
|
+
phase = 'unknown';
|
|
399
|
+
await lifecycle.transition({
|
|
400
|
+
...preparedLifecycleInput(),
|
|
401
|
+
phase: 'receipt_persisted',
|
|
402
|
+
claimToken: claim.claimToken,
|
|
403
|
+
from: 'runner_started',
|
|
404
|
+
to: 'receipt_persisted'
|
|
405
|
+
});
|
|
406
|
+
phase = 'receipt_persisted';
|
|
407
|
+
}
|
|
408
|
+
phase = 'unknown';
|
|
409
|
+
await this.ports.runStore.complete({
|
|
410
|
+
...claimInput,
|
|
411
|
+
claimToken: claim.claimToken,
|
|
412
|
+
payload: result.receipt,
|
|
413
|
+
normalized: normalizedReceipt,
|
|
414
|
+
runnerResult: result.result,
|
|
415
|
+
persistedReceipt
|
|
416
|
+
});
|
|
417
|
+
phase = 'completed';
|
|
418
|
+
// Readback is part of the contract. A lost response after complete must
|
|
419
|
+
// not cause a caller to invoke the runner again.
|
|
420
|
+
phase = 'unknown';
|
|
421
|
+
const stored = await this.ports.runStore.read(claimInput);
|
|
422
|
+
if (!stored || stored.status !== 'completed') {
|
|
423
|
+
throw uncertainError(request, stored?.phase || 'unknown');
|
|
424
|
+
}
|
|
425
|
+
if (lifecycle?.transition) {
|
|
426
|
+
phase = 'unknown';
|
|
427
|
+
await lifecycle.transition({
|
|
428
|
+
...preparedLifecycleInput(),
|
|
429
|
+
phase: 'completed',
|
|
430
|
+
claimToken: claim.claimToken,
|
|
431
|
+
from: 'receipt_persisted',
|
|
432
|
+
to: 'completed'
|
|
433
|
+
});
|
|
434
|
+
phase = 'completed';
|
|
435
|
+
}
|
|
436
|
+
phase = 'completed';
|
|
437
|
+
return {
|
|
438
|
+
runnerResult: stored.runnerResult,
|
|
439
|
+
normalizedReceipt: stored.normalized || normalizeRunReceipt(stored.payload),
|
|
440
|
+
persistedReceipt: stored.persistedReceipt,
|
|
441
|
+
context
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
const outcome = phase === 'claimed' ? 'retryable' : 'uncertain';
|
|
446
|
+
const releasePhase = outcome === 'retryable' ? 'retryable' : phase;
|
|
447
|
+
await Promise.resolve(this.ports.runStore.release({
|
|
448
|
+
...claimInput,
|
|
449
|
+
claimToken: claim.claimToken,
|
|
450
|
+
outcome,
|
|
451
|
+
phase: releasePhase,
|
|
452
|
+
error
|
|
453
|
+
})).catch(() => undefined);
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/** Thin compatibility facade for hosts that previously composed a control runtime. */
|
|
459
|
+
export class AutomationControlRuntime {
|
|
460
|
+
service;
|
|
461
|
+
constructor(service) {
|
|
462
|
+
this.service = service;
|
|
463
|
+
}
|
|
464
|
+
start(request) {
|
|
465
|
+
return this.service.start(request);
|
|
466
|
+
}
|
|
467
|
+
retry(request, previousRunId) {
|
|
468
|
+
return this.service.retry(request, previousRunId);
|
|
469
|
+
}
|
|
470
|
+
history(request) {
|
|
471
|
+
return this.service.history(request);
|
|
472
|
+
}
|
|
473
|
+
getRun(request) {
|
|
474
|
+
return this.service.getRun(request);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
export function createAutomationRunService(ports) {
|
|
478
|
+
return new AutomationRunService(ports);
|
|
479
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { CanonicalTaskPgClient, CanonicalTaskPgPool } from './canonical-task-postgres.js';
|
|
2
|
+
type JsonRecord = Record<string, unknown>;
|
|
3
|
+
export type CanonicalTaskOperationError = Error & {
|
|
4
|
+
code: string;
|
|
5
|
+
status: number;
|
|
6
|
+
details: JsonRecord;
|
|
7
|
+
};
|
|
8
|
+
export declare function canonicalTaskOperationError(code: string, message: string, status?: number, details?: JsonRecord): CanonicalTaskOperationError;
|
|
9
|
+
export interface CanonicalTaskOperationRepositoryOptions {
|
|
10
|
+
pool?: CanonicalTaskPgPool | null;
|
|
11
|
+
writerToken?: string | null;
|
|
12
|
+
processIdentity?: JsonRecord | null;
|
|
13
|
+
writerEnabled?: boolean;
|
|
14
|
+
operationWaitTimeoutMs?: number;
|
|
15
|
+
operationPollIntervalMs?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface CanonicalTaskReadinessRow {
|
|
18
|
+
ready: boolean;
|
|
19
|
+
writer_token: string | null;
|
|
20
|
+
manifest_hash: string | null;
|
|
21
|
+
schema_version: string | null;
|
|
22
|
+
source_head: string | null;
|
|
23
|
+
evidence_hash: string | null;
|
|
24
|
+
evidence_path: string | null;
|
|
25
|
+
reason: string | null;
|
|
26
|
+
updated_at: unknown;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Single-writer coordination for Canonical Task mutations
|
|
30
|
+
* (`contracts/canonical-task/operation-schema.sql`). One process owns the
|
|
31
|
+
* writer token; every mutation is recorded by scope and operation key so a
|
|
32
|
+
* retry with the same input replays and a different input conflicts.
|
|
33
|
+
*/
|
|
34
|
+
export declare class CanonicalTaskOperationRepository {
|
|
35
|
+
/**
|
|
36
|
+
* A PostgreSQL coordinator is a durable mutation boundary. Callers must
|
|
37
|
+
* bind it to the release readiness gate before exposing mutations.
|
|
38
|
+
*/
|
|
39
|
+
readonly requiresReadiness = true;
|
|
40
|
+
readonly pool: CanonicalTaskPgPool | null;
|
|
41
|
+
readonly writerToken: string | null;
|
|
42
|
+
readonly processIdentity: JsonRecord | null;
|
|
43
|
+
readonly writerEnabled: boolean;
|
|
44
|
+
readonly operationWaitTimeoutMs: number;
|
|
45
|
+
readonly operationPollIntervalMs: number;
|
|
46
|
+
writerClaimed: boolean;
|
|
47
|
+
activeOperations: number;
|
|
48
|
+
constructor({ pool, writerToken, processIdentity, writerEnabled, operationWaitTimeoutMs, operationPollIntervalMs, }?: CanonicalTaskOperationRepositoryOptions);
|
|
49
|
+
private identityJson;
|
|
50
|
+
private connect;
|
|
51
|
+
waitForCompletedOperation({ scope, operationKey, fingerprint }: {
|
|
52
|
+
scope: string;
|
|
53
|
+
operationKey: string;
|
|
54
|
+
fingerprint: string;
|
|
55
|
+
}): Promise<unknown>;
|
|
56
|
+
assertConfigured(): void;
|
|
57
|
+
assertWriterEnabled(): void;
|
|
58
|
+
claimWriter({ sourceHead }?: {
|
|
59
|
+
sourceHead?: string | null;
|
|
60
|
+
}): Promise<JsonRecord>;
|
|
61
|
+
assertWriter(client?: CanonicalTaskPgClient | null): Promise<void>;
|
|
62
|
+
readReadiness(): Promise<CanonicalTaskReadinessRow | null>;
|
|
63
|
+
reconcileReadiness({ manifestHash, schemaVersion, sourceHead, allowWriterRebind }?: {
|
|
64
|
+
manifestHash?: string | null;
|
|
65
|
+
schemaVersion?: string | null;
|
|
66
|
+
sourceHead?: string | null;
|
|
67
|
+
allowWriterRebind?: boolean;
|
|
68
|
+
}): Promise<CanonicalTaskReadinessRow | null>;
|
|
69
|
+
rebindReadinessSourceHead({ manifestHash, schemaVersion, fromHead, toHead }?: {
|
|
70
|
+
manifestHash?: string | null;
|
|
71
|
+
schemaVersion?: string | null;
|
|
72
|
+
fromHead?: string | null;
|
|
73
|
+
toHead?: string | null;
|
|
74
|
+
}): Promise<CanonicalTaskReadinessRow>;
|
|
75
|
+
releaseWriter(): Promise<boolean>;
|
|
76
|
+
execute<T>(args: CanonicalTaskCoordinatedRequest<T>): Promise<T>;
|
|
77
|
+
executeCoordinated<T>({ scope, operationKey, fingerprint, recover, projectResult, run }: CanonicalTaskCoordinatedRequest<T>): Promise<T>;
|
|
78
|
+
executePreparedDelete<T>(args: CanonicalTaskPreparedDeleteCoordination<T>): Promise<T>;
|
|
79
|
+
executePreparedDeleteCoordinated<T>({ operationKey, versionClaimKey, fingerprint, versionFingerprint, principalNamespace, prepare, findTask, removeTask, authorizeReplay, }: CanonicalTaskPreparedDeleteCoordination<T>): Promise<T>;
|
|
80
|
+
completePreparedDelete<T>({ operationKey, versionClaimKey, row, findTask, removeTask, authorizeReplay }: {
|
|
81
|
+
operationKey: string;
|
|
82
|
+
versionClaimKey: string;
|
|
83
|
+
row: {
|
|
84
|
+
fingerprint: string;
|
|
85
|
+
result_json: unknown;
|
|
86
|
+
authorization_snapshot?: unknown;
|
|
87
|
+
};
|
|
88
|
+
findTask: () => Promise<unknown>;
|
|
89
|
+
removeTask: (task: any) => Promise<void>;
|
|
90
|
+
authorizeReplay?: (authorizationSnapshot: JsonRecord, result: T) => Promise<void>;
|
|
91
|
+
}): Promise<T>;
|
|
92
|
+
}
|
|
93
|
+
export interface CanonicalTaskCoordinatedRequest<T> {
|
|
94
|
+
scope: string;
|
|
95
|
+
operationKey: string;
|
|
96
|
+
fingerprint: string;
|
|
97
|
+
recover?: (() => Promise<{
|
|
98
|
+
recovered: boolean;
|
|
99
|
+
result?: T;
|
|
100
|
+
}>) | null;
|
|
101
|
+
projectResult?: (value: T) => unknown;
|
|
102
|
+
run: () => Promise<T>;
|
|
103
|
+
}
|
|
104
|
+
export interface CanonicalTaskPreparedDeleteCoordination<T> {
|
|
105
|
+
operationKey: string;
|
|
106
|
+
versionClaimKey: string;
|
|
107
|
+
fingerprint: string;
|
|
108
|
+
versionFingerprint: string;
|
|
109
|
+
principalNamespace: string;
|
|
110
|
+
prepare: () => Promise<{
|
|
111
|
+
result: T;
|
|
112
|
+
authorizationSnapshot?: JsonRecord;
|
|
113
|
+
task?: unknown;
|
|
114
|
+
}>;
|
|
115
|
+
findTask: () => Promise<unknown>;
|
|
116
|
+
removeTask: (task: any) => Promise<void>;
|
|
117
|
+
/** Reauthorize a completed or already-applied delete from its durable snapshot. */
|
|
118
|
+
authorizeReplay?: (authorizationSnapshot: JsonRecord, result: T) => Promise<void>;
|
|
119
|
+
}
|
|
120
|
+
export declare class CanonicalTaskReadinessError extends Error {
|
|
121
|
+
readonly code: string;
|
|
122
|
+
readonly status = 503;
|
|
123
|
+
details?: JsonRecord;
|
|
124
|
+
constructor(code?: string, message?: string);
|
|
125
|
+
}
|
|
126
|
+
export type CanonicalTaskSourceHeadRebindGuard = (input: {
|
|
127
|
+
fromHead?: string | null;
|
|
128
|
+
toHead?: string | null;
|
|
129
|
+
}) => Promise<{
|
|
130
|
+
allowed: boolean;
|
|
131
|
+
reason?: string;
|
|
132
|
+
changedPaths?: string[];
|
|
133
|
+
changedPackages?: string[];
|
|
134
|
+
error?: string;
|
|
135
|
+
}>;
|
|
136
|
+
interface CanonicalTaskReadinessLogger {
|
|
137
|
+
warn?(message: string): void;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Opens Canonical Task mutation only while the persisted readiness row was
|
|
141
|
+
* blessed for this release: the same writer, manifest, schema and source head.
|
|
142
|
+
*/
|
|
143
|
+
export declare class CanonicalTaskReadiness {
|
|
144
|
+
readonly operationRepository: CanonicalTaskOperationRepository | null;
|
|
145
|
+
readonly expected: {
|
|
146
|
+
manifestHash: string | null;
|
|
147
|
+
schemaVersion: string | null;
|
|
148
|
+
sourceHead: string | null;
|
|
149
|
+
};
|
|
150
|
+
readonly sourceHeadRebindGuard: CanonicalTaskSourceHeadRebindGuard | null;
|
|
151
|
+
readonly logger: CanonicalTaskReadinessLogger | null;
|
|
152
|
+
ready: boolean;
|
|
153
|
+
reason: string | null;
|
|
154
|
+
evidence: JsonRecord | null;
|
|
155
|
+
constructor({ operationRepository, manifestHash, schemaVersion, sourceHead, sourceHeadRebindGuard, logger, }?: {
|
|
156
|
+
operationRepository?: CanonicalTaskOperationRepository | null;
|
|
157
|
+
manifestHash?: string | null;
|
|
158
|
+
schemaVersion?: string | null;
|
|
159
|
+
sourceHead?: string | null;
|
|
160
|
+
sourceHeadRebindGuard?: CanonicalTaskSourceHeadRebindGuard | null;
|
|
161
|
+
logger?: CanonicalTaskReadinessLogger | null;
|
|
162
|
+
});
|
|
163
|
+
open(evidence?: JsonRecord | null): void;
|
|
164
|
+
close(reason?: string): void;
|
|
165
|
+
matches(row: CanonicalTaskReadinessRow | null): boolean;
|
|
166
|
+
refresh({ allowWriterRebind }?: {
|
|
167
|
+
allowWriterRebind?: boolean | undefined;
|
|
168
|
+
}): Promise<{
|
|
169
|
+
ready: boolean;
|
|
170
|
+
reason?: string | null;
|
|
171
|
+
evidence?: JsonRecord | null;
|
|
172
|
+
}>;
|
|
173
|
+
attemptSourceHeadRebind(): Promise<boolean>;
|
|
174
|
+
initialize(): Promise<{
|
|
175
|
+
ready: boolean;
|
|
176
|
+
reason?: string | null;
|
|
177
|
+
evidence?: JsonRecord | null;
|
|
178
|
+
error?: unknown;
|
|
179
|
+
}>;
|
|
180
|
+
assertMutationReady(): Promise<void>;
|
|
181
|
+
}
|
|
182
|
+
type ExecFile = (file: string, args: string[]) => Promise<{
|
|
183
|
+
stdout: string | Buffer;
|
|
184
|
+
}>;
|
|
185
|
+
export interface CanonicalTaskSourceHeadGuardOptions {
|
|
186
|
+
repoDir?: string | null;
|
|
187
|
+
/** Repository paths whose change can alter Canonical Task mutation semantics. Tenant configuration. */
|
|
188
|
+
guardedPaths: string[];
|
|
189
|
+
/**
|
|
190
|
+
* Installed packages whose locked version can alter Canonical Task mutation
|
|
191
|
+
* semantics (for example this package). A change of the locked entry blocks
|
|
192
|
+
* the rebind even when no guarded path changed.
|
|
193
|
+
*/
|
|
194
|
+
guardedPackages?: string[];
|
|
195
|
+
lockfilePath?: string;
|
|
196
|
+
execFile?: ExecFile;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Decides whether readiness blessed for one source head may follow a deploy
|
|
200
|
+
* to another head. It allows the rebind only when git proves that no guarded
|
|
201
|
+
* path and no guarded package changed between them; anything unknown stays
|
|
202
|
+
* fail-closed.
|
|
203
|
+
*/
|
|
204
|
+
export declare function createCanonicalTaskSourceHeadGuard({ repoDir, guardedPaths, guardedPackages, lockfilePath, execFile, }: CanonicalTaskSourceHeadGuardOptions): CanonicalTaskSourceHeadRebindGuard;
|
|
205
|
+
export {};
|