@victframework/runtime 0.1.0
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/dist/agent-governance.d.ts +285 -0
- package/dist/agent-governance.js +438 -0
- package/dist/agent-governance.js.map +1 -0
- package/dist/agent-registry.d.ts +103 -0
- package/dist/agent-registry.js +758 -0
- package/dist/agent-registry.js.map +1 -0
- package/dist/agent-types.d.ts +433 -0
- package/dist/agent-types.js +480 -0
- package/dist/agent-types.js.map +1 -0
- package/dist/authority.d.ts +83 -0
- package/dist/authority.js +170 -0
- package/dist/authority.js.map +1 -0
- package/dist/boundary-conformance.d.ts +51 -0
- package/dist/boundary-conformance.js +364 -0
- package/dist/boundary-conformance.js.map +1 -0
- package/dist/control-conformance.d.ts +68 -0
- package/dist/control-conformance.js +1131 -0
- package/dist/control-conformance.js.map +1 -0
- package/dist/control-in-memory.d.ts +257 -0
- package/dist/control-in-memory.js +886 -0
- package/dist/control-in-memory.js.map +1 -0
- package/dist/control-types.d.ts +1017 -0
- package/dist/control-types.js +779 -0
- package/dist/control-types.js.map +1 -0
- package/dist/durable-run.d.ts +73 -0
- package/dist/durable-run.js +262 -0
- package/dist/durable-run.js.map +1 -0
- package/dist/effect-policy.d.ts +22 -0
- package/dist/effect-policy.js +45 -0
- package/dist/effect-policy.js.map +1 -0
- package/dist/errors.d.ts +36 -0
- package/dist/errors.js +71 -0
- package/dist/errors.js.map +1 -0
- package/dist/in-memory-stores.d.ts +16 -0
- package/dist/in-memory-stores.js +522 -0
- package/dist/in-memory-stores.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/operator-config.d.ts +141 -0
- package/dist/operator-config.js +289 -0
- package/dist/operator-config.js.map +1 -0
- package/dist/orchestration-activation.d.ts +116 -0
- package/dist/orchestration-activation.js +223 -0
- package/dist/orchestration-activation.js.map +1 -0
- package/dist/orchestration-canary-conformance.d.ts +10 -0
- package/dist/orchestration-canary-conformance.js +726 -0
- package/dist/orchestration-canary-conformance.js.map +1 -0
- package/dist/orchestration-commands.d.ts +47 -0
- package/dist/orchestration-commands.js +538 -0
- package/dist/orchestration-commands.js.map +1 -0
- package/dist/orchestration-conformance.d.ts +59 -0
- package/dist/orchestration-conformance.js +594 -0
- package/dist/orchestration-conformance.js.map +1 -0
- package/dist/orchestration-driver-types.d.ts +163 -0
- package/dist/orchestration-driver-types.js +21 -0
- package/dist/orchestration-driver-types.js.map +1 -0
- package/dist/orchestration-driver.d.ts +48 -0
- package/dist/orchestration-driver.js +1030 -0
- package/dist/orchestration-driver.js.map +1 -0
- package/dist/orchestration-in-memory.d.ts +18 -0
- package/dist/orchestration-in-memory.js +1313 -0
- package/dist/orchestration-in-memory.js.map +1 -0
- package/dist/orchestration-join-conformance.d.ts +20 -0
- package/dist/orchestration-join-conformance.js +542 -0
- package/dist/orchestration-join-conformance.js.map +1 -0
- package/dist/orchestration-plan.d.ts +52 -0
- package/dist/orchestration-plan.js +265 -0
- package/dist/orchestration-plan.js.map +1 -0
- package/dist/orchestration-race-conformance.d.ts +35 -0
- package/dist/orchestration-race-conformance.js +1265 -0
- package/dist/orchestration-race-conformance.js.map +1 -0
- package/dist/orchestration-remediation-conformance.d.ts +54 -0
- package/dist/orchestration-remediation-conformance.js +632 -0
- package/dist/orchestration-remediation-conformance.js.map +1 -0
- package/dist/orchestration-store-types.d.ts +568 -0
- package/dist/orchestration-store-types.js +2 -0
- package/dist/orchestration-store-types.js.map +1 -0
- package/dist/pack-conformance.d.ts +59 -0
- package/dist/pack-conformance.js +222 -0
- package/dist/pack-conformance.js.map +1 -0
- package/dist/pack-install.d.ts +44 -0
- package/dist/pack-install.js +126 -0
- package/dist/pack-install.js.map +1 -0
- package/dist/registry.d.ts +107 -0
- package/dist/registry.js +481 -0
- package/dist/registry.js.map +1 -0
- package/dist/runtime.d.ts +158 -0
- package/dist/runtime.js +1341 -0
- package/dist/runtime.js.map +1 -0
- package/dist/serialization.d.ts +26 -0
- package/dist/serialization.js +162 -0
- package/dist/serialization.js.map +1 -0
- package/dist/store-conformance.d.ts +45 -0
- package/dist/store-conformance.js +876 -0
- package/dist/store-conformance.js.map +1 -0
- package/dist/store-errors.d.ts +49 -0
- package/dist/store-errors.js +48 -0
- package/dist/store-errors.js.map +1 -0
- package/dist/store-types.d.ts +331 -0
- package/dist/store-types.js +14 -0
- package/dist/store-types.js.map +1 -0
- package/dist/store-validation.d.ts +63 -0
- package/dist/store-validation.js +164 -0
- package/dist/store-validation.js.map +1 -0
- package/dist/stream-hub.d.ts +151 -0
- package/dist/stream-hub.js +402 -0
- package/dist/stream-hub.js.map +1 -0
- package/dist/testing.d.ts +25 -0
- package/dist/testing.js +19 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +221 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +45 -0
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,1341 @@
|
|
|
1
|
+
import { compileGraph, executeGraph } from '@victframework/kernel';
|
|
2
|
+
import { OrchestrationDriver } from './orchestration-driver.js';
|
|
3
|
+
import { ORCHESTRATION_LIMITS } from './orchestration-driver-types.js';
|
|
4
|
+
import { cancelRun as orchestrationCancelRun, processDueTimers as processOrchestrationDueTimers, recoverOrchestration as recoverOrchestrationCommands, resolveBlocked as resolveBlockedCommand, signalWait, } from './orchestration-commands.js';
|
|
5
|
+
import { loadManifest } from './orchestration-activation.js';
|
|
6
|
+
import { deriveIdempotencyKey, deriveInvocationId } from './orchestration-activation.js';
|
|
7
|
+
import { NEUTRAL_JSON_CONTRACT_ID } from '@victframework/contracts';
|
|
8
|
+
import { CapabilityRegistry } from './registry.js';
|
|
9
|
+
import { decideEffectAuthorization } from './effect-policy.js';
|
|
10
|
+
import { VictRuntimeError, classifyInvocationFailure, runtimeError, sanitiseThrownError, } from './errors.js';
|
|
11
|
+
import { DurableRunTracker } from './durable-run.js';
|
|
12
|
+
import { createInMemoryStores } from './in-memory-stores.js';
|
|
13
|
+
import { deepFreeze, parseStoredJson, toCanonicalJson } from './serialization.js';
|
|
14
|
+
import { VictStoreError } from './store-errors.js';
|
|
15
|
+
import { ACTIVATION_MANIFEST_SCHEMA, ACTIVATION_MANIFEST_SCHEMA_V2, RUN_EVENT_SCHEMA, } from './store-types.js';
|
|
16
|
+
const RETENTION_VALUES = ['none', 'summary', 'full'];
|
|
17
|
+
const RECOVERY_CODE = 'VICT_RUN_INTERRUPTED_BY_RESTART';
|
|
18
|
+
const RECOVERY_REASON = 'The process executing this run ended before the run reached a terminal state.';
|
|
19
|
+
const RECOVERY_REMEDIATION = 'Automatic resume is unavailable at this stage. Inspect the run and its events, then start a deliberate new run; the new run receives a new run id.';
|
|
20
|
+
export { RECOVERY_CODE, RECOVERY_REASON, RECOVERY_REMEDIATION };
|
|
21
|
+
/**
|
|
22
|
+
* The usable in-process Vict runtime.
|
|
23
|
+
*
|
|
24
|
+
* Composes a capability registry, the active activation snapshot, execution
|
|
25
|
+
* policy, and durable semantic stores (activation catalog + execution
|
|
26
|
+
* store). Contains no ARA-specific logic.
|
|
27
|
+
*
|
|
28
|
+
* Safety model:
|
|
29
|
+
* - Activation is atomic from the caller's perspective: the candidate is
|
|
30
|
+
* compiled, snapshotted, and published/selected in the catalog BEFORE the
|
|
31
|
+
* in-memory snapshot is replaced. A failed compile or storage write leaves
|
|
32
|
+
* the previously active graph selected and runnable.
|
|
33
|
+
* - Activation manifests are immutable and durable; every run pins exactly
|
|
34
|
+
* one activationVersion (RUN-001), persisted with the run.
|
|
35
|
+
* - Runs execute against the pinned snapshot; in-flight runs cannot observe
|
|
36
|
+
* registry changes. Every run transition and its events are committed
|
|
37
|
+
* atomically (DATA-003) with optimistic concurrency, and the durable
|
|
38
|
+
* write-ahead rule holds: a capability is invoked only after its
|
|
39
|
+
* `node.started` intent (and every preceding durable write) has committed.
|
|
40
|
+
* - Test doubles are snapshotted at run start; replacing a double mid-run
|
|
41
|
+
* affects only later runs. Duplicate `registerDouble` is rejected; use
|
|
42
|
+
* `replaceDouble`.
|
|
43
|
+
* - Effect policy is enforced before any capability runs (see effect-policy).
|
|
44
|
+
* Irreversible effects never run their real implementation in simulate or
|
|
45
|
+
* test modes; in those modes a registered safe double may run, and without
|
|
46
|
+
* one the operation is blocked.
|
|
47
|
+
* - Run records are retained according to the runtime's `payloadRetention`
|
|
48
|
+
* (default `'summary'`): complete payloads are stored only under explicit
|
|
49
|
+
* `'full'` retention — which transfers responsibility for the persisted
|
|
50
|
+
* content to the caller/operator. Thrown error messages are never stored.
|
|
51
|
+
*/
|
|
52
|
+
export class VictRuntime {
|
|
53
|
+
#registry;
|
|
54
|
+
#stores;
|
|
55
|
+
#clock;
|
|
56
|
+
#ids;
|
|
57
|
+
#defaultOverrides;
|
|
58
|
+
#defaultMaxSteps;
|
|
59
|
+
#retention;
|
|
60
|
+
#orchestrationOptions;
|
|
61
|
+
#active;
|
|
62
|
+
#orchestrationDriverInstance;
|
|
63
|
+
constructor(options = {}) {
|
|
64
|
+
this.#registry = new CapabilityRegistry(options.authority === undefined
|
|
65
|
+
? undefined
|
|
66
|
+
: {
|
|
67
|
+
...(options.authority.grants !== undefined ? { grants: options.authority.grants } : {}),
|
|
68
|
+
...(options.authority.configuration !== undefined
|
|
69
|
+
? { configuration: options.authority.configuration }
|
|
70
|
+
: {}),
|
|
71
|
+
...(options.authority.secrets !== undefined
|
|
72
|
+
? { secrets: options.authority.secrets }
|
|
73
|
+
: {}),
|
|
74
|
+
});
|
|
75
|
+
const retention = options.payloadRetention ?? 'summary';
|
|
76
|
+
if (!RETENTION_VALUES.includes(retention)) {
|
|
77
|
+
throw new VictRuntimeError('VICT_RUNTIME_INVALID_RETENTION', `payloadRetention must be one of ${RETENTION_VALUES.map((value) => `'${value}'`).join(', ')}; received ${JSON.stringify(retention)}.`);
|
|
78
|
+
}
|
|
79
|
+
this.#retention = retention;
|
|
80
|
+
if (options.stores !== undefined) {
|
|
81
|
+
const stores = options.stores;
|
|
82
|
+
if (typeof stores.catalog?.publish !== 'function' ||
|
|
83
|
+
typeof stores.execution?.createRun !== 'function') {
|
|
84
|
+
throw new VictRuntimeError('VICT_RUNTIME_INVALID_STORES', 'stores must provide conforming `catalog` (ActivationCatalog) and `execution` (ExecutionStore) ports.');
|
|
85
|
+
}
|
|
86
|
+
this.#stores = stores;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
this.#stores = createInMemoryStores();
|
|
90
|
+
}
|
|
91
|
+
this.#clock = options.clock ?? { now: () => Date.now() };
|
|
92
|
+
this.#ids = options.ids ?? {
|
|
93
|
+
runId: () => `run_${globalThis.crypto.randomUUID()}`,
|
|
94
|
+
};
|
|
95
|
+
this.#defaultOverrides = options.policy ?? {};
|
|
96
|
+
this.#defaultMaxSteps = options.maxSteps;
|
|
97
|
+
const orchestrationOptions = options.orchestration ?? {};
|
|
98
|
+
this.#orchestrationOptions = {
|
|
99
|
+
concurrency: orchestrationOptions.concurrency,
|
|
100
|
+
operatorAuthorized: orchestrationOptions.operatorAuthorized ?? false,
|
|
101
|
+
time: orchestrationOptions.time,
|
|
102
|
+
ownerId: orchestrationOptions.ownerId ?? `owner_${globalThis.crypto.randomUUID()}`,
|
|
103
|
+
leaseMs: orchestrationOptions.leaseMs ?? 30_000,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** The Stage 03 durable orchestration driver (lazily constructed). */
|
|
107
|
+
#orchestrationDriver() {
|
|
108
|
+
if (this.#orchestrationDriverInstance === undefined) {
|
|
109
|
+
const orchestration = this.#stores.orchestration;
|
|
110
|
+
if (orchestration === undefined) {
|
|
111
|
+
throw new VictRuntimeError('VICT_RUNTIME_INVALID_STORES', 'Durable orchestration requires a store set with a conforming `orchestration` port (OrchestrationStore). Provide @victframework/store-sqlite ≥ Stage 03 or the default in-memory stores.');
|
|
112
|
+
}
|
|
113
|
+
this.#orchestrationDriverInstance = new OrchestrationDriver({
|
|
114
|
+
registry: this.#registry,
|
|
115
|
+
clock: this.#clock,
|
|
116
|
+
ids: this.#ids,
|
|
117
|
+
defaultOverrides: this.#defaultOverrides,
|
|
118
|
+
retention: this.#retention,
|
|
119
|
+
ownerId: this.#orchestrationOptions.ownerId,
|
|
120
|
+
leaseMs: this.#orchestrationOptions.leaseMs,
|
|
121
|
+
orchestration,
|
|
122
|
+
catalog: { get: (activationVersion) => this.#stores.catalog.get(activationVersion) },
|
|
123
|
+
...(this.#orchestrationOptions.time !== undefined
|
|
124
|
+
? { time: this.#orchestrationOptions.time }
|
|
125
|
+
: {}),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return this.#orchestrationDriverInstance;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Drive an existing orchestration run to terminal/quiescent against its
|
|
132
|
+
* EXACT pinned activation. Selection changes never affect this run; a
|
|
133
|
+
* newer selected activation applies only to future runs.
|
|
134
|
+
*/
|
|
135
|
+
async resumeRun(runId, options = {}) {
|
|
136
|
+
return this.#orchestrationDriver().resumeRun(runId, options);
|
|
137
|
+
}
|
|
138
|
+
/** Idempotently deliver one signal to one exact durable wait. */
|
|
139
|
+
async signal(command) {
|
|
140
|
+
const deps = this.#orchestrationDriver().deps;
|
|
141
|
+
const registry = this.#registry;
|
|
142
|
+
const parseWithPinnedContract = async (activationVersion, contractId, payload) => {
|
|
143
|
+
const manifest = await loadManifest({ catalog: { get: (version) => this.#stores.catalog.get(version) } }, activationVersion);
|
|
144
|
+
const contractRevisions = new Map();
|
|
145
|
+
for (const contract of manifest.contracts) {
|
|
146
|
+
contractRevisions.set(contract.id, contract.revision);
|
|
147
|
+
}
|
|
148
|
+
const revision = contractRevisions.get(contractId);
|
|
149
|
+
const contract = revision === undefined
|
|
150
|
+
? registry.getContract(contractId)
|
|
151
|
+
: registry.getContractRevision(contractId, revision);
|
|
152
|
+
if (!contract) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
message: `Contract '${contractId}' required by the pinned activation is not registered.`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
let parsed;
|
|
159
|
+
try {
|
|
160
|
+
parsed = contract.parse(payload);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Stage 04: a throwing author parser is a sanitized structured
|
|
164
|
+
// rejection — the wait remains open and the thrown message is
|
|
165
|
+
// never echoed.
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
message: `The signal payload could not be validated by contract '${contractId}' (revision '${revision ?? contract.revision}'): the contract parser threw. The wait remains open.`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (!parsed.ok) {
|
|
172
|
+
return {
|
|
173
|
+
ok: false,
|
|
174
|
+
message: `The signal payload was rejected by contract '${contractId}' revision '${revision ?? contract.revision}'; the wait remains open.`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return { ok: true };
|
|
178
|
+
};
|
|
179
|
+
return signalWait(deps, command, parseWithPinnedContract);
|
|
180
|
+
}
|
|
181
|
+
/** Idempotently request cancellation of one orchestration run. */
|
|
182
|
+
async cancel(command) {
|
|
183
|
+
const driver = this.#orchestrationDriver();
|
|
184
|
+
const result = await orchestrationCancelRun(driver.deps, command);
|
|
185
|
+
if (result.ok && result.status === 'accepted') {
|
|
186
|
+
// Cooperatively abort in-flight capability contexts: each capability
|
|
187
|
+
// observes its abort signal and unwinds; the durable transition
|
|
188
|
+
// boundary classifies the attempt as cancelled. No downstream node or
|
|
189
|
+
// retry can start from an aborted attempt.
|
|
190
|
+
driver.abortInflight(command.runId);
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
/** Resolve bounded due timers (timer waits, wait timeouts, retry backoff). */
|
|
195
|
+
async processDueTimers(options = {}) {
|
|
196
|
+
const driver = this.#orchestrationDriver();
|
|
197
|
+
const deps = driver.deps;
|
|
198
|
+
return processOrchestrationDueTimers(deps, {
|
|
199
|
+
runId: options.runId,
|
|
200
|
+
limit: options.limit ?? ORCHESTRATION_LIMITS.defaultTimerBatch,
|
|
201
|
+
}, async (activationVersion) => {
|
|
202
|
+
try {
|
|
203
|
+
const graph = await driver.resolveGraphForDriver(activationVersion);
|
|
204
|
+
return { ok: true, graph };
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return { ok: false };
|
|
208
|
+
}
|
|
209
|
+
}, () => deps.clock.now());
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Explicit boot-time effect-aware recovery for orchestration runs:
|
|
213
|
+
* reclaim policy-permitted expired claims (pure/read recompute; keyed
|
|
214
|
+
* write retries with the same key) and block ambiguous unsafe work.
|
|
215
|
+
* Historical Stage 02 sequential recovery is unchanged.
|
|
216
|
+
*/
|
|
217
|
+
async recoverOrchestration(options = {}) {
|
|
218
|
+
const driver = this.#orchestrationDriver();
|
|
219
|
+
const deps = driver.deps;
|
|
220
|
+
const summary = await recoverOrchestrationCommands(deps, options, async (runId, attempt) => {
|
|
221
|
+
const run = await deps.orchestration.getOrchestrationRun(runId);
|
|
222
|
+
if (!run) {
|
|
223
|
+
return { action: 'skip', reason: 'run not found' };
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const graph = await driver.resolveGraphForDriver(run.activationVersion);
|
|
227
|
+
const node = graph.getNode(attempt.nodeId);
|
|
228
|
+
if (!node) {
|
|
229
|
+
return {
|
|
230
|
+
action: 'block',
|
|
231
|
+
reason: 'the pinned activation cannot resolve the attempt node',
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
const retry = node.retry;
|
|
235
|
+
const effect = attempt.effectClass;
|
|
236
|
+
if (effect === 'pure' || effect === 'read') {
|
|
237
|
+
return { action: 'reclaim' };
|
|
238
|
+
}
|
|
239
|
+
if (effect === 'write') {
|
|
240
|
+
const keyed = attempt.idempotencyKey !== null;
|
|
241
|
+
if (!keyed) {
|
|
242
|
+
return {
|
|
243
|
+
action: 'block',
|
|
244
|
+
reason: 'A write capability without keyed idempotency has an unknown outcome after process loss; it is never replayed.',
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (retry !== undefined && attempt.attemptNumber < retry.maxAttempts) {
|
|
248
|
+
return { action: 'reclaim' };
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
action: 'block',
|
|
252
|
+
reason: 'The keyed write retry policy is exhausted; the outcome remains ambiguous.',
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
action: 'block',
|
|
257
|
+
reason: 'An irreversible capability has an unknown outcome after process loss; it is never replayed.',
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return {
|
|
262
|
+
action: 'skip',
|
|
263
|
+
reason: 'the exact pinned activation is unavailable; the claim is left for explicit resolution',
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
if (options.resume === true && summary.reclaimed.length > 0) {
|
|
268
|
+
const runIds = [...new Set(summary.reclaimed.map((entry) => entry.runId))];
|
|
269
|
+
for (const runId of runIds) {
|
|
270
|
+
await this.resumeRun(runId, { concurrency: options.concurrency });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return summary;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Bounded authorized operator resolution for one blocked run. Denied by
|
|
277
|
+
* default: only runtimes explicitly constructed with
|
|
278
|
+
* `orchestration.operatorAuthorized: true` may resolve blocked work.
|
|
279
|
+
*/
|
|
280
|
+
async resolveBlocked(input) {
|
|
281
|
+
if (this.#orchestrationOptions.operatorAuthorized !== true) {
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
code: 'VICT_ORCH_OPERATOR_DENIED',
|
|
285
|
+
message: 'Operator resolution is denied: no explicit operator authorization is configured for this runtime.',
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const driver = this.#orchestrationDriver();
|
|
289
|
+
const deps = driver.deps;
|
|
290
|
+
return resolveBlockedCommand(deps, input, async (runId, action, output) => {
|
|
291
|
+
const run = await deps.orchestration.getOrchestrationRun(runId);
|
|
292
|
+
if (!run) {
|
|
293
|
+
return { ok: false, code: 'VICT_ORCH_UNKNOWN_RUN', message: 'Run not found.' };
|
|
294
|
+
}
|
|
295
|
+
let graph;
|
|
296
|
+
try {
|
|
297
|
+
graph = await driver.resolveGraphForDriver(run.activationVersion);
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
return {
|
|
301
|
+
ok: false,
|
|
302
|
+
code: 'VICT_ORCH_ACTIVATION_UNAVAILABLE',
|
|
303
|
+
message: error instanceof VictRuntimeError
|
|
304
|
+
? error.message
|
|
305
|
+
: 'The exact pinned activation could not be resolved.',
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const envelopeIdentity = {
|
|
309
|
+
runId: run.runId,
|
|
310
|
+
graphId: run.graphId,
|
|
311
|
+
graphVersion: run.graphVersion,
|
|
312
|
+
capabilitySetVersion: run.capabilitySetVersion,
|
|
313
|
+
activationVersion: run.activationVersion,
|
|
314
|
+
};
|
|
315
|
+
const now = deps.clock.now();
|
|
316
|
+
const VictErr = (code, message) => ({
|
|
317
|
+
code,
|
|
318
|
+
message,
|
|
319
|
+
retryable: false,
|
|
320
|
+
});
|
|
321
|
+
if (action === 'cancel') {
|
|
322
|
+
return {
|
|
323
|
+
ok: true,
|
|
324
|
+
command: {
|
|
325
|
+
runId,
|
|
326
|
+
action: 'cancel',
|
|
327
|
+
reasonCode: input.reasonCode,
|
|
328
|
+
},
|
|
329
|
+
events: [
|
|
330
|
+
{
|
|
331
|
+
type: 'run.cancelled',
|
|
332
|
+
requestId: input.resolutionId,
|
|
333
|
+
reasonCode: input.reasonCode,
|
|
334
|
+
steps: run.steps,
|
|
335
|
+
...envelopeIdentity,
|
|
336
|
+
timestamp: now,
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
if (action === 'fail') {
|
|
342
|
+
return {
|
|
343
|
+
ok: true,
|
|
344
|
+
command: {
|
|
345
|
+
runId,
|
|
346
|
+
action: 'fail',
|
|
347
|
+
reasonCode: input.reasonCode,
|
|
348
|
+
failCode: input.failCode,
|
|
349
|
+
},
|
|
350
|
+
events: [
|
|
351
|
+
{
|
|
352
|
+
type: 'run.failed',
|
|
353
|
+
steps: run.steps,
|
|
354
|
+
error: VictErr(input.failCode ?? 'VICT_ORCH_OPERATOR_FAILED', 'The run was failed by an authorized operator resolution.'),
|
|
355
|
+
...envelopeIdentity,
|
|
356
|
+
timestamp: now,
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
const snapshot = await deps.orchestration.getOrchestrationSnapshot(runId);
|
|
362
|
+
const blockedToken = snapshot?.tokens.find((token) => token.status === 'blocked');
|
|
363
|
+
if (!blockedToken) {
|
|
364
|
+
return {
|
|
365
|
+
ok: false,
|
|
366
|
+
code: 'VICT_ORCH_NOT_BLOCKED',
|
|
367
|
+
message: 'The run has no blocked token.',
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
const node = graph.getNode(blockedToken.nodeId);
|
|
371
|
+
if (input.action === 'retry') {
|
|
372
|
+
if (node?.retry === undefined) {
|
|
373
|
+
return {
|
|
374
|
+
ok: false,
|
|
375
|
+
code: 'VICT_ORCH_OPERATOR_DENIED',
|
|
376
|
+
message: 'Retry is only permitted for work that declares a retry policy.',
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
ok: true,
|
|
381
|
+
command: { runId, action: 'retry', reasonCode: input.reasonCode },
|
|
382
|
+
events: [],
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
// confirm_applied: the output must pass the ORIGINAL pinned output contract.
|
|
386
|
+
if (node?.outputContractId === undefined) {
|
|
387
|
+
return {
|
|
388
|
+
ok: false,
|
|
389
|
+
code: 'VICT_ORCH_OPERATOR_CONFLICT',
|
|
390
|
+
message: 'Confirm-applied requires the blocked node to declare an output contract.',
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const bindings = await driver.resolveBindingsForDriver(run.activationVersion);
|
|
394
|
+
const contract = bindings.contracts.get(node.outputContractId);
|
|
395
|
+
if (!contract) {
|
|
396
|
+
return {
|
|
397
|
+
ok: false,
|
|
398
|
+
code: 'VICT_ORCH_ACTIVATION_UNAVAILABLE',
|
|
399
|
+
message: `Contract '${node.outputContractId}' is not resolvable from the pinned activation.`,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
let parsed;
|
|
403
|
+
try {
|
|
404
|
+
parsed = contract.parse(output);
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
return {
|
|
408
|
+
ok: false,
|
|
409
|
+
code: 'VICT_RUNTIME_CONTRACT_PARSER_THREW',
|
|
410
|
+
message: `The confirmed output could not be validated by contract '${node.outputContractId}': the contract parser threw; confirm-applied cannot bypass validation.`,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
if (!parsed.ok) {
|
|
414
|
+
return {
|
|
415
|
+
ok: false,
|
|
416
|
+
code: 'VICT_ORCH_SIGNAL_CONTRACT_REJECTED',
|
|
417
|
+
message: `The confirmed output was rejected by contract '${node.outputContractId}'; confirm-applied cannot bypass validation.`,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
const successTarget = graph.successTargetOf(node.id);
|
|
421
|
+
const invocationId = deriveInvocationId({
|
|
422
|
+
runId,
|
|
423
|
+
activationVersion: run.activationVersion,
|
|
424
|
+
lineage: blockedToken.lineage,
|
|
425
|
+
nodeId: node.id,
|
|
426
|
+
});
|
|
427
|
+
if (successTarget === undefined) {
|
|
428
|
+
return {
|
|
429
|
+
ok: true,
|
|
430
|
+
command: {
|
|
431
|
+
runId,
|
|
432
|
+
action: 'confirm_applied',
|
|
433
|
+
reasonCode: input.reasonCode,
|
|
434
|
+
continuation: {
|
|
435
|
+
kind: 'complete',
|
|
436
|
+
outputSummary: await import('@victframework/kernel').then((k) => k.summarizeOutput(output)),
|
|
437
|
+
output,
|
|
438
|
+
},
|
|
439
|
+
checkpoint: undefined,
|
|
440
|
+
output,
|
|
441
|
+
failCode: undefined,
|
|
442
|
+
},
|
|
443
|
+
events: [],
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
ok: true,
|
|
448
|
+
command: {
|
|
449
|
+
runId,
|
|
450
|
+
action: 'confirm_applied',
|
|
451
|
+
reasonCode: input.reasonCode,
|
|
452
|
+
continuation: {
|
|
453
|
+
kind: 'advance',
|
|
454
|
+
toNodeId: successTarget,
|
|
455
|
+
payload: parsed.value,
|
|
456
|
+
},
|
|
457
|
+
checkpoint: { tokenId: blockedToken.tokenId, payload: parsed.value },
|
|
458
|
+
output,
|
|
459
|
+
failCode: undefined,
|
|
460
|
+
idempotencyKey: deriveIdempotencyKey({
|
|
461
|
+
runId,
|
|
462
|
+
activationVersion: run.activationVersion,
|
|
463
|
+
lineage: blockedToken.lineage,
|
|
464
|
+
nodeId: node.id,
|
|
465
|
+
invocationId,
|
|
466
|
+
}),
|
|
467
|
+
},
|
|
468
|
+
events: [],
|
|
469
|
+
};
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
registerCapability(definition) {
|
|
473
|
+
this.#registry.registerCapability(definition);
|
|
474
|
+
return this;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Install a batch atomically (used by `installCapabilityPack`). Stages
|
|
478
|
+
* every contract/capability/double and commits only on full success.
|
|
479
|
+
*/
|
|
480
|
+
installCapabilityPackBatch(install) {
|
|
481
|
+
this.#registry.installBatch(install);
|
|
482
|
+
}
|
|
483
|
+
registerContract(contract) {
|
|
484
|
+
this.#registry.registerContract(contract);
|
|
485
|
+
return this;
|
|
486
|
+
}
|
|
487
|
+
/** Register a test double. Duplicate registration is rejected; use `replaceDouble`. */
|
|
488
|
+
registerDouble(capabilityId, invoke, options = {}) {
|
|
489
|
+
this.#registry.registerDouble(capabilityId, invoke, options);
|
|
490
|
+
return this;
|
|
491
|
+
}
|
|
492
|
+
/** Explicitly replace an existing test double. Later runs use the replacement; in-flight runs do not. */
|
|
493
|
+
replaceDouble(capabilityId, invoke) {
|
|
494
|
+
this.#registry.replaceDouble(capabilityId, invoke);
|
|
495
|
+
return this;
|
|
496
|
+
}
|
|
497
|
+
/** True when a test double is registered for the capability. */
|
|
498
|
+
hasDouble(capabilityId) {
|
|
499
|
+
return this.#registry.hasDouble(capabilityId);
|
|
500
|
+
}
|
|
501
|
+
/** The eligible modes of a registered test double (undefined when none is registered). */
|
|
502
|
+
getDoubleModes(capabilityId) {
|
|
503
|
+
return this.#registry.getDoubleModes(capabilityId);
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Compile and activate a graph atomically from the caller's perspective:
|
|
507
|
+
*
|
|
508
|
+
* 1. compile and resolve the candidate completely;
|
|
509
|
+
* 2. snapshot capability and captured contract parsing semantics;
|
|
510
|
+
* 3. build and validate the serializable manifest;
|
|
511
|
+
* 4. publish and select it in the durable catalog (one transaction);
|
|
512
|
+
* 5. only then replace the active in-memory snapshot.
|
|
513
|
+
*
|
|
514
|
+
* A compile failure returns a structured rejection; a storage failure
|
|
515
|
+
* throws a structured store error. In both cases the previously active
|
|
516
|
+
* graph remains selected and runnable, and durable state never claims an
|
|
517
|
+
* activation the catalog did not select.
|
|
518
|
+
*/
|
|
519
|
+
async activate(definition) {
|
|
520
|
+
const result = compileGraph({
|
|
521
|
+
definition,
|
|
522
|
+
capabilities: this.#registry.capabilityIndex(),
|
|
523
|
+
contracts: this.#registry.contractEnvironment(),
|
|
524
|
+
});
|
|
525
|
+
if (!result.ok) {
|
|
526
|
+
return {
|
|
527
|
+
ok: false,
|
|
528
|
+
issues: result.issues,
|
|
529
|
+
previousGraph: this.activeGraph(),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
const snapshot = this.#captureSnapshot(result.graph);
|
|
533
|
+
const manifest = this.#buildManifest(result.graph);
|
|
534
|
+
const canonicalManifest = toCanonicalJson(manifest);
|
|
535
|
+
await this.#stores.catalog.publishAndSelect({
|
|
536
|
+
publish: { manifest, canonicalManifest },
|
|
537
|
+
select: { graphId: result.graph.id },
|
|
538
|
+
});
|
|
539
|
+
this.#active = snapshot;
|
|
540
|
+
return {
|
|
541
|
+
ok: true,
|
|
542
|
+
graphId: result.graph.id,
|
|
543
|
+
graphVersion: result.graph.graphVersion,
|
|
544
|
+
capabilitySetVersion: result.graph.capabilitySetVersion,
|
|
545
|
+
activationVersion: result.graph.activationVersion,
|
|
546
|
+
nodeCount: result.graph.nodeCount,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Restore the exact durable activation for a graph definition.
|
|
551
|
+
*
|
|
552
|
+
* The current registered code (capabilities and contracts) must reproduce
|
|
553
|
+
* the stored activation exactly: the definition is recompiled against the
|
|
554
|
+
* live registry, all three version identities are recomputed, and the
|
|
555
|
+
* rebuilt canonical manifest is compared with the stored one. On an exact
|
|
556
|
+
* match the activation becomes the active in-memory snapshot. On any
|
|
557
|
+
* mismatch the stored manifest is preserved, the currently active graph is
|
|
558
|
+
* left unchanged, no capability is executed, and no “closest” revision is
|
|
559
|
+
* chosen.
|
|
560
|
+
*
|
|
561
|
+
* By default the activation currently selected for `definition.id` is
|
|
562
|
+
* restored; pass `activationVersion` to restore a specific one.
|
|
563
|
+
*/
|
|
564
|
+
async restoreActivation(definition, options = {}) {
|
|
565
|
+
let stored;
|
|
566
|
+
if (options.activationVersion !== undefined) {
|
|
567
|
+
stored = await this.#stores.catalog.get(options.activationVersion);
|
|
568
|
+
if (!stored) {
|
|
569
|
+
return {
|
|
570
|
+
ok: false,
|
|
571
|
+
code: 'VICT_RUNTIME_ACTIVATION_NOT_FOUND',
|
|
572
|
+
message: `No stored activation '${options.activationVersion}' exists in the catalog.`,
|
|
573
|
+
expectedActivationVersion: options.activationVersion,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
stored = await this.#stores.catalog.getSelected(definition.id);
|
|
579
|
+
if (!stored) {
|
|
580
|
+
return {
|
|
581
|
+
ok: false,
|
|
582
|
+
code: 'VICT_RUNTIME_ACTIVATION_NOT_FOUND',
|
|
583
|
+
message: `No activation is selected for graph '${definition.id}' in the catalog.`,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
let manifest;
|
|
588
|
+
try {
|
|
589
|
+
manifest = parseManifest(stored);
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
return {
|
|
593
|
+
ok: false,
|
|
594
|
+
code: 'VICT_RUNTIME_ACTIVATION_UNAVAILABLE',
|
|
595
|
+
message: 'The stored activation manifest is corrupt and cannot be read.',
|
|
596
|
+
expectedActivationVersion: stored.activationVersion,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
const compiled = compileGraph({
|
|
600
|
+
definition,
|
|
601
|
+
capabilities: this.#registry.capabilityIndex(),
|
|
602
|
+
contracts: this.#registry.contractEnvironment(),
|
|
603
|
+
});
|
|
604
|
+
if (!compiled.ok) {
|
|
605
|
+
return {
|
|
606
|
+
ok: false,
|
|
607
|
+
code: 'VICT_RUNTIME_ACTIVATION_UNAVAILABLE',
|
|
608
|
+
message: 'The current registered code cannot compile the graph; required capabilities or contracts are missing or invalid.',
|
|
609
|
+
expectedActivationVersion: stored.activationVersion,
|
|
610
|
+
issues: compiled.issues,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
const rebuilt = this.#buildManifest(compiled.graph);
|
|
614
|
+
const rebuiltCanonical = toCanonicalJson(rebuilt);
|
|
615
|
+
if (rebuilt.activationVersion !== stored.activationVersion ||
|
|
616
|
+
rebuiltCanonical !== stored.canonicalManifest) {
|
|
617
|
+
return {
|
|
618
|
+
ok: false,
|
|
619
|
+
code: 'VICT_RUNTIME_ACTIVATION_MISMATCH',
|
|
620
|
+
message: 'The current registered code does not reproduce the stored activation. No capability was executed and the active graph is unchanged.',
|
|
621
|
+
expectedActivationVersion: stored.activationVersion,
|
|
622
|
+
actualActivationVersion: rebuilt.activationVersion,
|
|
623
|
+
differences: describeManifestDifferences(manifest, rebuilt),
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
const snapshot = this.#captureSnapshot(compiled.graph);
|
|
627
|
+
this.#active = snapshot;
|
|
628
|
+
return {
|
|
629
|
+
ok: true,
|
|
630
|
+
graphId: compiled.graph.id,
|
|
631
|
+
graphVersion: compiled.graph.graphVersion,
|
|
632
|
+
capabilitySetVersion: compiled.graph.capabilitySetVersion,
|
|
633
|
+
activationVersion: compiled.graph.activationVersion,
|
|
634
|
+
nodeCount: compiled.graph.nodeCount,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Explicit boot-time recovery (single local owner). Finds runs left in a
|
|
639
|
+
* nonterminal running state by a previous process, atomically transitions
|
|
640
|
+
* each to `blocked`, and appends one safe interruption event. Never
|
|
641
|
+
* invokes or replays a capability. Repeated recovery is idempotent.
|
|
642
|
+
*/
|
|
643
|
+
async recoverInterruptedRuns() {
|
|
644
|
+
return this.#stores.execution.recoverInterruptedRuns({
|
|
645
|
+
code: RECOVERY_CODE,
|
|
646
|
+
reason: RECOVERY_REASON,
|
|
647
|
+
remediation: RECOVERY_REMEDIATION,
|
|
648
|
+
timestamp: this.#clock.now(),
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
activeGraph() {
|
|
652
|
+
const active = this.#active;
|
|
653
|
+
if (!active) {
|
|
654
|
+
return undefined;
|
|
655
|
+
}
|
|
656
|
+
return {
|
|
657
|
+
id: active.graph.id,
|
|
658
|
+
version: active.graph.graphVersion,
|
|
659
|
+
capabilitySetVersion: active.graph.capabilitySetVersion,
|
|
660
|
+
activationVersion: active.graph.activationVersion,
|
|
661
|
+
entryNodeId: active.graph.entryNodeId,
|
|
662
|
+
nodeCount: active.graph.nodeCount,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Execute the active graph. Capability-only graphs keep the verified
|
|
667
|
+
* Stage 02 sequential engine; graphs with control nodes run on the Stage
|
|
668
|
+
* 03 durable orchestration driver (token/attempt state machine). Run
|
|
669
|
+
* transitions and events are committed to the store atomically as the
|
|
670
|
+
* run progresses; when `run()` resolves, the durable record is current.
|
|
671
|
+
*/
|
|
672
|
+
async run(input, options = {}) {
|
|
673
|
+
const snapshot = this.#requireActive();
|
|
674
|
+
if (snapshot.graph.hasControlNodes) {
|
|
675
|
+
return (await this.#orchestrationDriver().startRun(snapshot.graph, input, options.mode ?? 'normal', this.#ids.runId(), {
|
|
676
|
+
concurrency: options.concurrency,
|
|
677
|
+
onEvent: options.onEvent,
|
|
678
|
+
...(options.policy === undefined ? {} : { policy: options.policy }),
|
|
679
|
+
}));
|
|
680
|
+
}
|
|
681
|
+
const mode = options.mode ?? 'normal';
|
|
682
|
+
const overrides = options.policy ?? this.#defaultOverrides;
|
|
683
|
+
const doubles = this.#registry.snapshotDoubles();
|
|
684
|
+
const runId = this.#ids.runId();
|
|
685
|
+
const tracker = new DurableRunTracker(this.#stores.execution, {
|
|
686
|
+
runId,
|
|
687
|
+
graphId: snapshot.graph.id,
|
|
688
|
+
graphVersion: snapshot.graph.graphVersion,
|
|
689
|
+
capabilitySetVersion: snapshot.graph.capabilitySetVersion,
|
|
690
|
+
activationVersion: snapshot.graph.activationVersion,
|
|
691
|
+
mode,
|
|
692
|
+
retention: this.#retention,
|
|
693
|
+
entryNodeId: snapshot.graph.entryNodeId,
|
|
694
|
+
});
|
|
695
|
+
const onEvent = (event) => {
|
|
696
|
+
tracker.onEvent(event);
|
|
697
|
+
options.onEvent?.(event);
|
|
698
|
+
};
|
|
699
|
+
let output;
|
|
700
|
+
try {
|
|
701
|
+
output = await executeGraph({
|
|
702
|
+
graph: snapshot.graph,
|
|
703
|
+
input,
|
|
704
|
+
mode,
|
|
705
|
+
maxSteps: options.maxSteps ?? this.#defaultMaxSteps,
|
|
706
|
+
ports: this.#buildPorts(snapshot, doubles, overrides, onEvent, runId,
|
|
707
|
+
// Durable write-ahead boundary: no capability is invoked before
|
|
708
|
+
// its durable intent (and everything before it) has committed.
|
|
709
|
+
() => tracker.awaitDurableBoundary()),
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
catch (error) {
|
|
713
|
+
// Kernel execution failed (e.g. a storage fail-fast): settle the
|
|
714
|
+
// durable queue, then surface the storage failure or the original error.
|
|
715
|
+
return await tracker.settle(error);
|
|
716
|
+
}
|
|
717
|
+
await tracker.finish(output);
|
|
718
|
+
return toRunResult(output);
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Execute a single node of the active graph in isolation (mode forced to
|
|
722
|
+
* `'test'`). The isolated compile resolves against the activation snapshot,
|
|
723
|
+
* so post-activation registry changes cannot affect it. Does not traverse
|
|
724
|
+
* edges, does not change the active graph, and does not write durable run
|
|
725
|
+
* records. The trace is returned directly.
|
|
726
|
+
*/
|
|
727
|
+
async runNode(nodeId, input, options = {}) {
|
|
728
|
+
const snapshot = this.#requireActive();
|
|
729
|
+
const node = snapshot.graph.getNode(nodeId);
|
|
730
|
+
if (!node) {
|
|
731
|
+
throw new VictRuntimeError('VICT_RUNTIME_UNKNOWN_NODE', `Active graph '${snapshot.graph.id}' has no node '${nodeId}'.`);
|
|
732
|
+
}
|
|
733
|
+
const isolatedDefinition = {
|
|
734
|
+
id: `${snapshot.graph.id}#isolated:${nodeId}`,
|
|
735
|
+
entry: nodeId,
|
|
736
|
+
nodes: [
|
|
737
|
+
{
|
|
738
|
+
id: node.id,
|
|
739
|
+
capability: node.capability,
|
|
740
|
+
input: node.inputContractId,
|
|
741
|
+
output: node.outputContractId,
|
|
742
|
+
},
|
|
743
|
+
],
|
|
744
|
+
edges: [],
|
|
745
|
+
};
|
|
746
|
+
const isolated = compileGraph({
|
|
747
|
+
definition: isolatedDefinition,
|
|
748
|
+
capabilities: snapshot.descriptors,
|
|
749
|
+
contracts: snapshot.contractEnvironment,
|
|
750
|
+
});
|
|
751
|
+
if (!isolated.ok) {
|
|
752
|
+
throw new VictRuntimeError('VICT_RUNTIME_UNKNOWN_NODE', `Isolated node graph for '${nodeId}' failed to compile: ${isolated.issues.map((issue) => issue.message).join(' ')}`);
|
|
753
|
+
}
|
|
754
|
+
// Isolated tests always run in 'test' mode with no policy overrides and
|
|
755
|
+
// no durable records, so no invocation boundary applies here.
|
|
756
|
+
// Irreversible effects never run their real implementation here: a
|
|
757
|
+
// registered safe double may run, and without one the node is blocked.
|
|
758
|
+
const doubles = this.#registry.snapshotDoubles();
|
|
759
|
+
const output = await executeGraph({
|
|
760
|
+
graph: isolated.graph,
|
|
761
|
+
input,
|
|
762
|
+
mode: 'test',
|
|
763
|
+
maxSteps: options.maxSteps ?? this.#defaultMaxSteps,
|
|
764
|
+
ports: this.#buildPorts(snapshot, doubles, {}, options.onEvent, undefined),
|
|
765
|
+
});
|
|
766
|
+
return toRunResult(output);
|
|
767
|
+
}
|
|
768
|
+
/** All stored runs (assembled views with their traces), oldest first. */
|
|
769
|
+
async listRuns() {
|
|
770
|
+
const runs = await this.#stores.execution.listRuns();
|
|
771
|
+
const records = [];
|
|
772
|
+
for (const run of runs) {
|
|
773
|
+
records.push(await this.#composeRunRecord(run));
|
|
774
|
+
}
|
|
775
|
+
return deepFreeze(records);
|
|
776
|
+
}
|
|
777
|
+
/** One stored run (assembled view with its trace), or undefined. */
|
|
778
|
+
async getRun(runId) {
|
|
779
|
+
const run = await this.#stores.execution.getRun(runId);
|
|
780
|
+
if (!run) {
|
|
781
|
+
return undefined;
|
|
782
|
+
}
|
|
783
|
+
return deepFreeze(await this.#composeRunRecord(run));
|
|
784
|
+
}
|
|
785
|
+
async #composeRunRecord(run) {
|
|
786
|
+
const events = await this.#stores.execution.listEvents(run.runId);
|
|
787
|
+
const trace = events.map((event) => storedEventToKernelEvent(event));
|
|
788
|
+
const first = trace[0];
|
|
789
|
+
const last = trace.at(-1);
|
|
790
|
+
const record = {
|
|
791
|
+
runId: run.runId,
|
|
792
|
+
graphId: run.graphId,
|
|
793
|
+
graphVersion: run.graphVersion,
|
|
794
|
+
capabilitySetVersion: run.capabilitySetVersion,
|
|
795
|
+
activationVersion: run.activationVersion,
|
|
796
|
+
mode: run.mode,
|
|
797
|
+
status: run.status,
|
|
798
|
+
startedAt: first?.timestamp ?? run.createdAt,
|
|
799
|
+
durationMs: first && last ? Math.max(0, last.timestamp - first.timestamp) : 0,
|
|
800
|
+
steps: run.steps,
|
|
801
|
+
retention: run.retention,
|
|
802
|
+
currentNodeId: run.currentNodeId,
|
|
803
|
+
recordRevision: run.recordRevision,
|
|
804
|
+
trace,
|
|
805
|
+
...(run.outputSummary !== undefined ? { outputSummary: run.outputSummary } : {}),
|
|
806
|
+
...(run.output !== undefined ? { output: run.output } : {}),
|
|
807
|
+
...(run.error !== undefined ? { error: run.error } : {}),
|
|
808
|
+
};
|
|
809
|
+
return record;
|
|
810
|
+
}
|
|
811
|
+
#requireActive() {
|
|
812
|
+
const active = this.#active;
|
|
813
|
+
if (!active) {
|
|
814
|
+
throw new VictRuntimeError('VICT_RUNTIME_NO_ACTIVE_GRAPH', 'No active graph. Compile and activate a graph with runtime.activate(definition) first.');
|
|
815
|
+
}
|
|
816
|
+
return active;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Capture the immutable snapshot for a freshly compiled graph: frozen
|
|
820
|
+
* copies of the execution-relevant capability bindings and captured
|
|
821
|
+
* (bound) contract parsing handles the graph requires. Deliberately
|
|
822
|
+
* narrow — the snapshot contains only what the activated graph needs, so
|
|
823
|
+
* later registrations are invisible to it.
|
|
824
|
+
*/
|
|
825
|
+
#captureSnapshot(graph) {
|
|
826
|
+
const bindings = new Map();
|
|
827
|
+
const contracts = new Map();
|
|
828
|
+
const requireContract = (contractId) => {
|
|
829
|
+
const existing = contracts.get(contractId);
|
|
830
|
+
if (existing) {
|
|
831
|
+
return existing;
|
|
832
|
+
}
|
|
833
|
+
const live = this.#registry.getContract(contractId);
|
|
834
|
+
if (!live) {
|
|
835
|
+
throw new VictRuntimeError('VICT_RUNTIME_UNKNOWN_NODE', `Activation snapshot could not resolve contract '${contractId}' required by graph '${graph.id}'.`);
|
|
836
|
+
}
|
|
837
|
+
// Capture the parse callable BY VALUE (bound now): replacing
|
|
838
|
+
// `contract.parse` on the caller-owned object later cannot change what
|
|
839
|
+
// this activation executes.
|
|
840
|
+
const captured = {
|
|
841
|
+
id: live.id,
|
|
842
|
+
revision: live.revision,
|
|
843
|
+
expected: live.expected,
|
|
844
|
+
parse: live.parse.bind(live),
|
|
845
|
+
};
|
|
846
|
+
contracts.set(contractId, Object.freeze(captured));
|
|
847
|
+
return captured;
|
|
848
|
+
};
|
|
849
|
+
for (const nodeId of graph.nodeIds) {
|
|
850
|
+
const node = graph.getNode(nodeId);
|
|
851
|
+
if (!node || (node.kind !== 'capability' && node.kind !== 'decision')) {
|
|
852
|
+
continue; // control nodes carry no capability binding
|
|
853
|
+
}
|
|
854
|
+
const live = this.#registry.getCapability(node.capability);
|
|
855
|
+
if (!live) {
|
|
856
|
+
throw new VictRuntimeError('VICT_RUNTIME_CAPABILITY_MISSING', `Activation snapshot could not resolve capability '${node.capability}' required by graph '${graph.id}'.`);
|
|
857
|
+
}
|
|
858
|
+
if (!bindings.has(node.capability)) {
|
|
859
|
+
bindings.set(node.capability, Object.freeze({
|
|
860
|
+
id: live.id,
|
|
861
|
+
revision: live.revision,
|
|
862
|
+
effect: live.effect,
|
|
863
|
+
invoke: live.invoke,
|
|
864
|
+
inputContractId: live.input?.id,
|
|
865
|
+
inputRevision: live.input?.revision,
|
|
866
|
+
outputContractId: live.output?.id,
|
|
867
|
+
outputRevision: live.output?.revision,
|
|
868
|
+
...(descriptorAuthorityOf(live) !== undefined
|
|
869
|
+
? { authority: descriptorAuthorityOf(live) }
|
|
870
|
+
: {}),
|
|
871
|
+
}));
|
|
872
|
+
}
|
|
873
|
+
if (live.input !== undefined) {
|
|
874
|
+
requireContract(live.input.id);
|
|
875
|
+
}
|
|
876
|
+
if (live.output !== undefined) {
|
|
877
|
+
requireContract(live.output.id);
|
|
878
|
+
}
|
|
879
|
+
if (node.inputContractId !== undefined) {
|
|
880
|
+
requireContract(node.inputContractId);
|
|
881
|
+
}
|
|
882
|
+
if (node.outputContractId !== undefined) {
|
|
883
|
+
requireContract(node.outputContractId);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
const frozenBindings = new Map(bindings);
|
|
887
|
+
const frozenContracts = new Map(contracts);
|
|
888
|
+
const descriptors = {
|
|
889
|
+
getCapabilityDescriptor: (capabilityId) => {
|
|
890
|
+
const binding = frozenBindings.get(capabilityId);
|
|
891
|
+
if (!binding) {
|
|
892
|
+
return undefined;
|
|
893
|
+
}
|
|
894
|
+
return {
|
|
895
|
+
id: binding.id,
|
|
896
|
+
revision: binding.revision,
|
|
897
|
+
effect: binding.effect,
|
|
898
|
+
inputContractId: binding.inputContractId,
|
|
899
|
+
inputRevision: binding.inputRevision,
|
|
900
|
+
outputContractId: binding.outputContractId,
|
|
901
|
+
outputRevision: binding.outputRevision,
|
|
902
|
+
...(binding.idempotency !== undefined ? { idempotency: binding.idempotency } : {}),
|
|
903
|
+
...(binding.authority !== undefined ? { authority: binding.authority } : {}),
|
|
904
|
+
};
|
|
905
|
+
},
|
|
906
|
+
};
|
|
907
|
+
const contractEnvironment = {
|
|
908
|
+
has: (contractId) => frozenContracts.has(contractId),
|
|
909
|
+
isCompatible: (from, to) => from === undefined ||
|
|
910
|
+
to === undefined ||
|
|
911
|
+
from === to ||
|
|
912
|
+
from === NEUTRAL_JSON_CONTRACT_ID ||
|
|
913
|
+
to === NEUTRAL_JSON_CONTRACT_ID,
|
|
914
|
+
// Hand the kernel a frozen object whose parse is the captured callable.
|
|
915
|
+
get: (contractId) => {
|
|
916
|
+
const captured = frozenContracts.get(contractId);
|
|
917
|
+
if (!captured) {
|
|
918
|
+
return undefined;
|
|
919
|
+
}
|
|
920
|
+
const view = Object.freeze({
|
|
921
|
+
id: captured.id,
|
|
922
|
+
revision: captured.revision,
|
|
923
|
+
expected: captured.expected,
|
|
924
|
+
parse: (input) => captured.parse(input),
|
|
925
|
+
});
|
|
926
|
+
return view;
|
|
927
|
+
},
|
|
928
|
+
};
|
|
929
|
+
return Object.freeze({
|
|
930
|
+
graph,
|
|
931
|
+
bindings: frozenBindings,
|
|
932
|
+
contracts: frozenContracts,
|
|
933
|
+
descriptors,
|
|
934
|
+
contractEnvironment,
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Build the serializable activation manifest for a compiled graph from the
|
|
939
|
+
* current registry. Contains only serializable meaning; canonical form is
|
|
940
|
+
* stable across processes (registration order, timestamps and functions
|
|
941
|
+
* never enter it).
|
|
942
|
+
*/
|
|
943
|
+
#buildManifest(graph) {
|
|
944
|
+
const bindings = [];
|
|
945
|
+
const contractById = new Map();
|
|
946
|
+
const recordContract = (contractId) => {
|
|
947
|
+
if (contractId === undefined) {
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
const live = this.#registry.getContract(contractId);
|
|
951
|
+
if (!live) {
|
|
952
|
+
throw new VictRuntimeError('VICT_RUNTIME_UNKNOWN_NODE', `Activation manifest could not resolve contract '${contractId}' required by graph '${graph.id}'.`);
|
|
953
|
+
}
|
|
954
|
+
contractById.set(live.id, { id: live.id, revision: live.revision });
|
|
955
|
+
};
|
|
956
|
+
for (const nodeId of graph.nodeIds) {
|
|
957
|
+
const node = graph.getNode(nodeId);
|
|
958
|
+
if (!node || (node.kind !== 'capability' && node.kind !== 'decision')) {
|
|
959
|
+
continue; // control nodes carry no capability binding
|
|
960
|
+
}
|
|
961
|
+
const live = this.#registry.getCapability(node.capability);
|
|
962
|
+
if (!live) {
|
|
963
|
+
throw new VictRuntimeError('VICT_RUNTIME_CAPABILITY_MISSING', `Activation manifest could not resolve capability '${node.capability}' required by graph '${graph.id}'.`);
|
|
964
|
+
}
|
|
965
|
+
const inputId = node.inputContractId ?? live.input?.id;
|
|
966
|
+
const outputId = node.outputContractId ?? live.output?.id;
|
|
967
|
+
const inputRevision = node.inputContractId
|
|
968
|
+
? this.#registry.getContract(node.inputContractId)?.revision
|
|
969
|
+
: live.input?.revision;
|
|
970
|
+
const outputRevision = node.outputContractId
|
|
971
|
+
? this.#registry.getContract(node.outputContractId)?.revision
|
|
972
|
+
: live.output?.revision;
|
|
973
|
+
bindings.push({
|
|
974
|
+
capability: live.id,
|
|
975
|
+
revision: live.revision,
|
|
976
|
+
effect: live.effect,
|
|
977
|
+
input: inputId === undefined ? null : { id: inputId, revision: inputRevision ?? 'unknown' },
|
|
978
|
+
output: outputId === undefined ? null : { id: outputId, revision: outputRevision ?? 'unknown' },
|
|
979
|
+
...(live.idempotency === undefined ? {} : { idempotency: live.idempotency }),
|
|
980
|
+
...(descriptorAuthorityOf(live) !== undefined
|
|
981
|
+
? { authority: descriptorAuthorityOf(live) }
|
|
982
|
+
: {}),
|
|
983
|
+
});
|
|
984
|
+
recordContract(live.input?.id);
|
|
985
|
+
recordContract(live.output?.id);
|
|
986
|
+
recordContract(node.inputContractId);
|
|
987
|
+
recordContract(node.outputContractId);
|
|
988
|
+
}
|
|
989
|
+
const dedupedBindings = dedupeCanonical(bindings);
|
|
990
|
+
// Stage 03: control-node metadata for v2 manifests (control graphs only).
|
|
991
|
+
const waits = [];
|
|
992
|
+
const forks = [];
|
|
993
|
+
const joins = [];
|
|
994
|
+
for (const nodeId of graph.nodeIds) {
|
|
995
|
+
const node = graph.getNode(nodeId);
|
|
996
|
+
if (!node) {
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
if (node.kind === 'wait' && node.wait !== undefined) {
|
|
1000
|
+
const wait = node.wait;
|
|
1001
|
+
waits.push(wait.kind === 'signal'
|
|
1002
|
+
? {
|
|
1003
|
+
nodeId,
|
|
1004
|
+
kind: 'signal',
|
|
1005
|
+
name: wait.name,
|
|
1006
|
+
contract: wait.contract === undefined
|
|
1007
|
+
? null
|
|
1008
|
+
: {
|
|
1009
|
+
id: wait.contract,
|
|
1010
|
+
revision: this.#registry.getContract(wait.contract)?.revision ?? 'unknown',
|
|
1011
|
+
},
|
|
1012
|
+
...(wait.timeoutMs !== undefined ? { timeoutMs: wait.timeoutMs } : {}),
|
|
1013
|
+
}
|
|
1014
|
+
: { nodeId, kind: 'timer', delayMs: wait.delayMs });
|
|
1015
|
+
if (wait.kind === 'signal' && wait.contract !== undefined) {
|
|
1016
|
+
recordContract(wait.contract);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
if (node.kind === 'fork' && node.join !== undefined) {
|
|
1020
|
+
forks.push({
|
|
1021
|
+
forkId: nodeId,
|
|
1022
|
+
joinId: node.join,
|
|
1023
|
+
branchKeys: graph.branchKeysOf(nodeId),
|
|
1024
|
+
maxConcurrency: node.maxConcurrency ?? null,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
if (node.kind === 'join') {
|
|
1028
|
+
const outputId = node.outputContractId;
|
|
1029
|
+
joins.push({
|
|
1030
|
+
joinId: nodeId,
|
|
1031
|
+
forkId: node.fork ?? '',
|
|
1032
|
+
outputContract: outputId === undefined
|
|
1033
|
+
? null
|
|
1034
|
+
: {
|
|
1035
|
+
id: outputId,
|
|
1036
|
+
revision: this.#registry.getContract(outputId)?.revision ?? 'unknown',
|
|
1037
|
+
},
|
|
1038
|
+
});
|
|
1039
|
+
if (outputId !== undefined) {
|
|
1040
|
+
recordContract(outputId);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
// Computed AFTER every recordContract call (capability bindings, wait
|
|
1045
|
+
// signal contracts, and join output contracts) so the pinned contract
|
|
1046
|
+
// environment is complete.
|
|
1047
|
+
const contracts = [...contractById.values()].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
1048
|
+
const isControl = graph.hasControlNodes;
|
|
1049
|
+
return {
|
|
1050
|
+
manifestSchema: isControl ? ACTIVATION_MANIFEST_SCHEMA_V2 : ACTIVATION_MANIFEST_SCHEMA,
|
|
1051
|
+
graphId: graph.id,
|
|
1052
|
+
graph: graph.toDefinition(),
|
|
1053
|
+
graphVersion: graph.graphVersion,
|
|
1054
|
+
capabilitySetVersion: graph.capabilitySetVersion,
|
|
1055
|
+
activationVersion: graph.activationVersion,
|
|
1056
|
+
bindings: dedupedBindings,
|
|
1057
|
+
contracts,
|
|
1058
|
+
...(isControl ? { waits, forks, joins } : {}),
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
#buildPorts(snapshot, doubles, overrides, onEvent, runId, beforeInvoke) {
|
|
1062
|
+
const bindings = snapshot.bindings;
|
|
1063
|
+
const contracts = snapshot.contractEnvironment;
|
|
1064
|
+
return {
|
|
1065
|
+
descriptors: snapshot.descriptors,
|
|
1066
|
+
contracts,
|
|
1067
|
+
onEvent,
|
|
1068
|
+
...(beforeInvoke !== undefined ? { beforeInvoke } : {}),
|
|
1069
|
+
clock: this.#clock,
|
|
1070
|
+
ids: {
|
|
1071
|
+
runId: runId !== undefined ? () => runId : () => this.#ids.runId(),
|
|
1072
|
+
errorId: this.#ids.errorId
|
|
1073
|
+
? () => this.#ids.errorId?.() ?? `err_${randomId()}`
|
|
1074
|
+
: undefined,
|
|
1075
|
+
},
|
|
1076
|
+
policy: {
|
|
1077
|
+
authorize(request) {
|
|
1078
|
+
const decision = decideEffectAuthorization(request, overrides);
|
|
1079
|
+
if (decision.allowed && decision.useDouble) {
|
|
1080
|
+
const registered = doubles.get(request.capabilityId);
|
|
1081
|
+
if (registered === undefined) {
|
|
1082
|
+
return {
|
|
1083
|
+
allowed: false,
|
|
1084
|
+
useDouble: false,
|
|
1085
|
+
reason: `Effect class '${request.effect}' requires a registered test double in '${request.mode}' mode and none is registered.`,
|
|
1086
|
+
remediation: `Register a test double for capability '${request.capabilityId}' with runtime.registerDouble().`,
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
// A pack-declared double is eligible ONLY in its declared modes.
|
|
1090
|
+
// A double declared only for 'test' never runs in 'simulate', and
|
|
1091
|
+
// doubles never run in 'normal' mode (the policy never asks).
|
|
1092
|
+
if (!registered.modes.has(request.mode)) {
|
|
1093
|
+
return {
|
|
1094
|
+
allowed: false,
|
|
1095
|
+
useDouble: false,
|
|
1096
|
+
reason: `The registered test double for capability '${request.capabilityId}' is not eligible in '${request.mode}' mode (eligible modes: ${[...registered.modes].sort().join(', ')}).`,
|
|
1097
|
+
remediation: `Register a double eligible in '${request.mode}' mode, or run in a mode the double declares.`,
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return decision;
|
|
1102
|
+
},
|
|
1103
|
+
},
|
|
1104
|
+
capabilities: {
|
|
1105
|
+
async invoke(capabilityId, input, context) {
|
|
1106
|
+
const useDouble = context.useDouble;
|
|
1107
|
+
const double = useDouble ? doubles.get(capabilityId)?.invoke : undefined;
|
|
1108
|
+
const binding = useDouble ? undefined : bindings.get(capabilityId);
|
|
1109
|
+
const invocationContext = {
|
|
1110
|
+
runId: context.runId,
|
|
1111
|
+
graphId: context.graphId,
|
|
1112
|
+
graphVersion: context.graphVersion,
|
|
1113
|
+
capabilitySetVersion: context.capabilitySetVersion,
|
|
1114
|
+
activationVersion: context.activationVersion,
|
|
1115
|
+
nodeId: context.nodeId,
|
|
1116
|
+
capabilityId,
|
|
1117
|
+
mode: context.mode,
|
|
1118
|
+
step: context.step,
|
|
1119
|
+
invokedVia: useDouble ? 'double' : 'real',
|
|
1120
|
+
};
|
|
1121
|
+
// Thrown messages are untrusted content: they are never copied into
|
|
1122
|
+
// the structured error, only a safe type name and a correlation id.
|
|
1123
|
+
const failureFor = (target) => {
|
|
1124
|
+
return {
|
|
1125
|
+
ok: false,
|
|
1126
|
+
error: runtimeError('VICT_RUNTIME_CAPABILITY_MISSING', `No ${target} available for capability '${capabilityId}' in the pinned activation snapshot.`, { capabilityId, nodeId: context.nodeId }),
|
|
1127
|
+
};
|
|
1128
|
+
};
|
|
1129
|
+
if (useDouble) {
|
|
1130
|
+
if (!double) {
|
|
1131
|
+
return failureFor('test double');
|
|
1132
|
+
}
|
|
1133
|
+
try {
|
|
1134
|
+
return { ok: true, value: await double(input, invocationContext) };
|
|
1135
|
+
}
|
|
1136
|
+
catch (cause) {
|
|
1137
|
+
const sanitised = sanitiseThrownError(cause);
|
|
1138
|
+
return {
|
|
1139
|
+
ok: false,
|
|
1140
|
+
error: runtimeError('VICT_RUNTIME_CAPABILITY_THREW', `Test double for '${capabilityId}' threw during invocation; the thrown message is not retained.`, {
|
|
1141
|
+
capabilityId,
|
|
1142
|
+
nodeId: context.nodeId,
|
|
1143
|
+
invokedVia: 'double',
|
|
1144
|
+
...sanitised,
|
|
1145
|
+
}),
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
if (binding) {
|
|
1150
|
+
try {
|
|
1151
|
+
return {
|
|
1152
|
+
ok: true,
|
|
1153
|
+
value: await binding.invoke(input, invocationContext),
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
catch (cause) {
|
|
1157
|
+
// Stage 04: structured authority failures (permission denied,
|
|
1158
|
+
// secret/configuration unavailable) keep their stable code;
|
|
1159
|
+
// any other throw is reduced to CAPABILITY_THREW.
|
|
1160
|
+
const classified = classifyInvocationFailure(cause, capabilityId, {
|
|
1161
|
+
nodeId: context.nodeId,
|
|
1162
|
+
invokedVia: 'real',
|
|
1163
|
+
});
|
|
1164
|
+
return {
|
|
1165
|
+
ok: false,
|
|
1166
|
+
error: runtimeError(classified.code, classified.message, classified.details),
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return failureFor('implementation');
|
|
1171
|
+
},
|
|
1172
|
+
},
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
function dedupeCanonical(items) {
|
|
1177
|
+
const sorted = [...items].sort((a, b) => {
|
|
1178
|
+
const keyA = toCanonicalJson(a);
|
|
1179
|
+
const keyB = toCanonicalJson(b);
|
|
1180
|
+
return keyA < keyB ? -1 : keyA > keyB ? 1 : 0;
|
|
1181
|
+
});
|
|
1182
|
+
const deduped = [];
|
|
1183
|
+
let previous;
|
|
1184
|
+
for (const item of sorted) {
|
|
1185
|
+
const key = toCanonicalJson(item);
|
|
1186
|
+
if (key !== previous) {
|
|
1187
|
+
deduped.push(item);
|
|
1188
|
+
previous = key;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return deduped;
|
|
1192
|
+
}
|
|
1193
|
+
/** Validate a stored activation's manifest against its row before use. */
|
|
1194
|
+
function parseManifest(stored) {
|
|
1195
|
+
const parsed = parseStoredJson(stored.canonicalManifest, 'activation manifest');
|
|
1196
|
+
if (!parsed ||
|
|
1197
|
+
typeof parsed !== 'object' ||
|
|
1198
|
+
(parsed.manifestSchema !== ACTIVATION_MANIFEST_SCHEMA &&
|
|
1199
|
+
parsed.manifestSchema !== ACTIVATION_MANIFEST_SCHEMA_V2) ||
|
|
1200
|
+
typeof parsed.activationVersion !== 'string' ||
|
|
1201
|
+
typeof parsed.graphId !== 'string' ||
|
|
1202
|
+
typeof parsed.graphVersion !== 'string' ||
|
|
1203
|
+
typeof parsed.capabilitySetVersion !== 'string' ||
|
|
1204
|
+
!Array.isArray(parsed.bindings) ||
|
|
1205
|
+
!Array.isArray(parsed.contracts) ||
|
|
1206
|
+
typeof parsed.graph !== 'object' ||
|
|
1207
|
+
parsed.graph === null) {
|
|
1208
|
+
throw new VictStoreError('VICT_STORE_INVALID_RECORD', 'The stored activation manifest is not a valid manifest for this schema.', { operation: 'catalog.parseManifest', activationVersion: stored.activationVersion });
|
|
1209
|
+
}
|
|
1210
|
+
if (parsed.activationVersion !== stored.activationVersion) {
|
|
1211
|
+
throw new VictStoreError('VICT_STORE_INVALID_RECORD', 'The stored activation manifest does not match its record identity.', {
|
|
1212
|
+
operation: 'catalog.parseManifest',
|
|
1213
|
+
activationVersion: stored.activationVersion,
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
if (parsed.graphId !== stored.graphId ||
|
|
1217
|
+
parsed.graphVersion !== stored.graphVersion ||
|
|
1218
|
+
parsed.capabilitySetVersion !== stored.capabilitySetVersion) {
|
|
1219
|
+
throw new VictStoreError('VICT_STORE_INVALID_RECORD', 'The stored activation manifest disagrees with its identity columns.', { operation: 'catalog.parseManifest', activationVersion: stored.activationVersion });
|
|
1220
|
+
}
|
|
1221
|
+
return parsed;
|
|
1222
|
+
}
|
|
1223
|
+
/** Safe, structural description of a manifest mismatch (no payload values involved). */
|
|
1224
|
+
function describeManifestDifferences(stored, rebuilt) {
|
|
1225
|
+
const differences = [];
|
|
1226
|
+
if (stored.graphVersion !== rebuilt.graphVersion) {
|
|
1227
|
+
differences.push(`graphVersion differs: stored ${stored.graphVersion.slice(0, 18)}…, rebuilt ${rebuilt.graphVersion.slice(0, 18)}… (topology or declaration changed)`);
|
|
1228
|
+
}
|
|
1229
|
+
if (stored.capabilitySetVersion !== rebuilt.capabilitySetVersion) {
|
|
1230
|
+
differences.push('capabilitySetVersion differs (capability revisions, effect classes or contract bindings changed)');
|
|
1231
|
+
const storedByCapability = new Map(stored.bindings.map((b) => [b.capability, b]));
|
|
1232
|
+
for (const binding of rebuilt.bindings) {
|
|
1233
|
+
const expected = storedByCapability.get(binding.capability);
|
|
1234
|
+
if (!expected) {
|
|
1235
|
+
differences.push(`capability '${binding.capability}' is not part of the stored activation`);
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
if (expected.revision !== binding.revision) {
|
|
1239
|
+
differences.push(`capability '${binding.capability}' revision: stored '${expected.revision}', current '${binding.revision}'`);
|
|
1240
|
+
}
|
|
1241
|
+
if (expected.effect !== binding.effect) {
|
|
1242
|
+
differences.push(`capability '${binding.capability}' effect: stored '${expected.effect}', current '${binding.effect}'`);
|
|
1243
|
+
}
|
|
1244
|
+
for (const side of ['input', 'output']) {
|
|
1245
|
+
const expectedContract = expected[side];
|
|
1246
|
+
const actualContract = binding[side];
|
|
1247
|
+
if (expectedContract?.id !== actualContract?.id ||
|
|
1248
|
+
expectedContract?.revision !== actualContract?.revision) {
|
|
1249
|
+
differences.push(`capability '${binding.capability}' ${side} contract: stored ${expectedContract ? `${expectedContract.id}@${expectedContract.revision}` : 'none'}, current ${actualContract ? `${actualContract.id}@${actualContract.revision}` : 'none'}`);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
if (JSON.stringify(stored.graph) !== JSON.stringify(rebuilt.graph)) {
|
|
1255
|
+
differences.push('graph declaration differs from the stored activation');
|
|
1256
|
+
}
|
|
1257
|
+
if (differences.length === 0) {
|
|
1258
|
+
differences.push('activationVersion differs for an unclassified reason');
|
|
1259
|
+
}
|
|
1260
|
+
return differences;
|
|
1261
|
+
}
|
|
1262
|
+
/** Validate a stored event row and rebuild the typed kernel event. */
|
|
1263
|
+
function storedEventToKernelEvent(event) {
|
|
1264
|
+
if (event.eventSchema !== RUN_EVENT_SCHEMA) {
|
|
1265
|
+
throw new VictStoreError('VICT_STORE_INVALID_RECORD', 'A stored event carries an unsupported event schema version.', { operation: 'execution.readEvent', runId: event.runId });
|
|
1266
|
+
}
|
|
1267
|
+
const parsed = parseStoredJson(event.payload, 'run event');
|
|
1268
|
+
if (!parsed ||
|
|
1269
|
+
typeof parsed !== 'object' ||
|
|
1270
|
+
parsed.seq !== event.seq ||
|
|
1271
|
+
parsed.type !== event.type ||
|
|
1272
|
+
parsed.runId !== event.runId ||
|
|
1273
|
+
parsed.graphId !== event.graphId ||
|
|
1274
|
+
parsed.graphVersion !== event.graphVersion ||
|
|
1275
|
+
parsed.capabilitySetVersion !== event.capabilitySetVersion ||
|
|
1276
|
+
parsed.activationVersion !== event.activationVersion ||
|
|
1277
|
+
typeof parsed.timestamp !== 'number' ||
|
|
1278
|
+
!Number.isFinite(parsed.timestamp)) {
|
|
1279
|
+
throw new VictStoreError('VICT_STORE_INVALID_RECORD', 'A stored event payload disagrees with its columns.', { operation: 'execution.readEvent', runId: event.runId });
|
|
1280
|
+
}
|
|
1281
|
+
return parsed;
|
|
1282
|
+
}
|
|
1283
|
+
function toRunResult(output) {
|
|
1284
|
+
const result = {
|
|
1285
|
+
runId: output.runId,
|
|
1286
|
+
graphId: output.graphId,
|
|
1287
|
+
graphVersion: output.graphVersion,
|
|
1288
|
+
capabilitySetVersion: output.capabilitySetVersion,
|
|
1289
|
+
activationVersion: output.activationVersion,
|
|
1290
|
+
status: output.status,
|
|
1291
|
+
trace: output.events,
|
|
1292
|
+
};
|
|
1293
|
+
if (output.output !== undefined) {
|
|
1294
|
+
result.output = output.output;
|
|
1295
|
+
}
|
|
1296
|
+
if (output.error !== undefined) {
|
|
1297
|
+
result.error = output.error;
|
|
1298
|
+
}
|
|
1299
|
+
return result;
|
|
1300
|
+
}
|
|
1301
|
+
function randomId() {
|
|
1302
|
+
const bytes = new Uint8Array(8);
|
|
1303
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
1304
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
1305
|
+
}
|
|
1306
|
+
/** Create a runtime instance. Application code should use the `@victframework/sdk` facade instead. */
|
|
1307
|
+
export function createRuntime(options = {}) {
|
|
1308
|
+
return new VictRuntime(options);
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Snapshot the execution-affecting authority declarations of a live
|
|
1312
|
+
* capability definition (declared names only — never resolved values and
|
|
1313
|
+
* never runtime grants). Used by activation snapshots so runNode's isolated
|
|
1314
|
+
* compile reproduces the exact capability-set identity.
|
|
1315
|
+
*/
|
|
1316
|
+
function descriptorAuthorityOf(definition) {
|
|
1317
|
+
const has = (names) => names !== undefined && names.length > 0;
|
|
1318
|
+
if (!has(definition.permissions) &&
|
|
1319
|
+
!has(definition.configuration) &&
|
|
1320
|
+
!has(definition.requiredConfiguration) &&
|
|
1321
|
+
!has(definition.secrets) &&
|
|
1322
|
+
!has(definition.requiredSecrets)) {
|
|
1323
|
+
return undefined;
|
|
1324
|
+
}
|
|
1325
|
+
return {
|
|
1326
|
+
...(has(definition.permissions)
|
|
1327
|
+
? { permissions: Object.freeze([...(definition.permissions ?? [])]) }
|
|
1328
|
+
: {}),
|
|
1329
|
+
...(has(definition.configuration)
|
|
1330
|
+
? { configuration: Object.freeze([...(definition.configuration ?? [])]) }
|
|
1331
|
+
: {}),
|
|
1332
|
+
...(has(definition.requiredConfiguration)
|
|
1333
|
+
? { requiredConfiguration: Object.freeze([...(definition.requiredConfiguration ?? [])]) }
|
|
1334
|
+
: {}),
|
|
1335
|
+
...(has(definition.secrets) ? { secrets: Object.freeze([...(definition.secrets ?? [])]) } : {}),
|
|
1336
|
+
...(has(definition.requiredSecrets)
|
|
1337
|
+
? { requiredSecrets: Object.freeze([...(definition.requiredSecrets ?? [])]) }
|
|
1338
|
+
: {}),
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
//# sourceMappingURL=runtime.js.map
|